diff --git a/app-instance/backend/.beaver/locks/plugin-skill-write.lock b/app-instance/backend/.beaver/locks/plugin-skill-write.lock new file mode 100644 index 0000000..e69de29 diff --git a/app-instance/backend/beaver/coordinator/local.py b/app-instance/backend/beaver/coordinator/local.py index 51583ee..d1a83cf 100644 --- a/app-instance/backend/beaver/coordinator/local.py +++ b/app-instance/backend/beaver/coordinator/local.py @@ -9,6 +9,7 @@ from beaver.engine.providers import ProviderBundle from beaver.tasks.evidence import EvidenceBuilder, evaluate_node_evidence from .models import DelegationEnvelope, NodeRunResult +from .runtime_defaults import DEFAULT_TEAM_NODE_MAX_TOOL_ITERATIONS class LocalAgentRunner: @@ -55,7 +56,11 @@ class LocalAgentRunner: pinned_skill_names=envelope.inherited_pinned_skills, pinned_skill_contexts=envelope.inherited_pinned_skill_contexts, allowed_tool_names=envelope.allowed_tool_names, - max_tool_iterations=envelope.max_tool_iterations, + max_tool_iterations=( + envelope.max_tool_iterations + if envelope.max_tool_iterations is not None + else DEFAULT_TEAM_NODE_MAX_TOOL_ITERATIONS + ), allow_candidate_generation=allow_candidate_generation, ) loaded = target_loop.boot() @@ -70,7 +75,8 @@ class LocalAgentRunner: envelope.required_evidence, result.output_text, ) - run_succeeded = result.finish_reason == "stop" + raw_tool_call_output = self._looks_like_raw_tool_call(result.output_text) + run_succeeded = result.finish_reason in {"stop", "max_tool_iterations_finalized"} and not raw_tool_call_output if not run_succeeded: completion_status = "failed" elif evidence_gaps: @@ -81,7 +87,10 @@ class LocalAgentRunner: if completion_status == "partial": error = "; ".join(evidence_gaps) else: - error = None if success else (result.output_text or result.finish_reason) + if raw_tool_call_output: + error = "finalized output is a raw tool call" + else: + error = None if success else (result.output_text or result.finish_reason) return NodeRunResult( node_id=envelope.node_id or envelope.agent.name, success=success, @@ -169,3 +178,16 @@ class LocalAgentRunner: "If no published skill matches, return [] and let the node continue without skills." ) return "\n\n".join(sections) + + @staticmethod + def _looks_like_raw_tool_call(output_text: str | None) -> bool: + text = (output_text or "").strip() + if not text: + return False + markers = ( + "<||DSML||tool_calls>", + "<||DSML||invoke", + " tuple[list[BaseTool], Too ObjectBackedTool(WebFetchTool()), ObjectBackedTool(WebSearchTool()), ] + elif category == "team_workflow": + from beaver.team_workflows.mcp_tools import create_team_workflow_tools + + tools = create_team_workflow_tools() else: raise ValueError(f"Unknown local tool category: {category}") return tools, context diff --git a/app-instance/backend/beaver/services/agent_service.py b/app-instance/backend/beaver/services/agent_service.py index c671e38..54280dc 100644 --- a/app-instance/backend/beaver/services/agent_service.py +++ b/app-instance/backend/beaver/services/agent_service.py @@ -68,7 +68,7 @@ class AgentService: self.profile.max_tokens = None self.profile.temperature = 0.2 self.profile.max_context_messages = 1000 - self.profile.max_tool_iterations = 30 + self.profile.max_tool_iterations = 100 if defaults.max_tokens is not None: self.profile.max_tokens = max(1, defaults.max_tokens) if defaults.temperature is not None: diff --git a/app-instance/backend/beaver/services/process_service.py b/app-instance/backend/beaver/services/process_service.py index 0a8e7f0..d094b90 100644 --- a/app-instance/backend/beaver/services/process_service.py +++ b/app-instance/backend/beaver/services/process_service.py @@ -17,6 +17,7 @@ class SessionProcessProjector: runs: dict[str, dict[str, Any]] = {} events: list[dict[str, Any]] = [] artifacts: list[dict[str, Any]] = [] + projected_skill_activation_run_ids: set[str] = set() def add_event( *, @@ -186,6 +187,38 @@ class SessionProcessProjector: }, ) + elif record.event_type == "skill_activation_snapshotted": + run_id = record.run_id or root_run_id + parent_run_id = root_run_id if run_id != root_run_id else None + receipts = [ + item + for item in payload.get("receipts") or [] + if isinstance(item, dict) + ] + selected_skill_names = _receipt_skill_names(receipts) + if selected_skill_names: + projected_skill_activation_run_ids.add(str(run_id)) + add_event( + event_id=_event_id(record, "skill-activation"), + run_id=str(run_id), + parent_run_id=parent_run_id, + kind="skill_selected", + actor_type="system", + actor_id="skill-selector", + actor_name="Skill Selector", + text=f"Selected skill guidance: {', '.join(selected_skill_names)}.", + created_at=_receipt_started_at(receipts) or created_at, + status="done", + metadata={ + "task_id": task_id, + "attempt_index": attempt_index, + "timeline_type": "skill", + "skill_names": selected_skill_names, + "activation_reasons": _receipt_reasons(receipts), + "receipts": receipts, + }, + ) + elif record.event_type in {"task_team_run_completed", "task_team_run_failed"}: team_success = bool(payload.get("team_success")) root["status"] = "running" @@ -203,7 +236,7 @@ class SessionProcessProjector: actor_type="system", actor_id="team", actor_name="Task Team", - text=payload.get("error") or ("Team completed" if team_success else "Team completed with failed nodes"), + text="Team completed" if team_success else "Team 执行未完成 / 子节点失败", created_at=created_at, status="done" if team_success else "error", metadata={**dict(payload), "timeline_type": "agent_team", "team_run_ids": team_run_ids}, @@ -316,7 +349,10 @@ class SessionProcessProjector: "skill_names": activated_skill_names, }, } - if activated_skill_names: + if activated_skill_names and main_run_id not in projected_skill_activation_run_ids: + skill_created_at = _activated_skill_started_at(run_record) or ( + run_record.started_at if run_record is not None else None + ) or created_at add_event( event_id=_event_id(record, "synthesis-skills"), run_id=main_run_id, @@ -326,7 +362,7 @@ class SessionProcessProjector: actor_id="skill-selector", actor_name="Skill Selector", text=f"Selected skill guidance: {', '.join(activated_skill_names)}.", - created_at=created_at, + created_at=skill_created_at, status="done", metadata={ "task_id": task_id, @@ -439,6 +475,48 @@ def _activated_skill_reasons(run_record: Any | None) -> list[str]: return reasons +def _activated_skill_started_at(run_record: Any | None) -> str | None: + if run_record is None: + return None + timestamps = [ + str(getattr(receipt, "activated_at", "") or "").strip() + for receipt in getattr(run_record, "activated_skills", []) or [] + ] + timestamps = [value for value in timestamps if value] + if not timestamps: + return None + return sorted(timestamps)[0] + + +def _receipt_skill_names(receipts: list[dict[str, Any]]) -> list[str]: + names = [] + for receipt in receipts: + skill_name = str(receipt.get("skill_name") or "").strip() + if skill_name: + names.append(skill_name) + return list(dict.fromkeys(names)) + + +def _receipt_reasons(receipts: list[dict[str, Any]]) -> list[str]: + reasons = [] + for receipt in receipts: + reason = str(receipt.get("activation_reason") or "").strip() + if reason: + reasons.append(reason) + return reasons + + +def _receipt_started_at(receipts: list[dict[str, Any]]) -> str | None: + timestamps = [ + str(receipt.get("activated_at") or "").strip() + for receipt in receipts + ] + timestamps = [value for value in timestamps if value] + if not timestamps: + return None + return sorted(timestamps)[0] + + def _tool_call_name(tool_call: dict[str, Any]) -> str: function_payload = tool_call.get("function") if isinstance(function_payload, dict): diff --git a/app-instance/backend/beaver/skills/assembler/task_assembler.py b/app-instance/backend/beaver/skills/assembler/task_assembler.py index ad0a0c0..0597016 100644 --- a/app-instance/backend/beaver/skills/assembler/task_assembler.py +++ b/app-instance/backend/beaver/skills/assembler/task_assembler.py @@ -140,8 +140,6 @@ class SkillAssembler: content_hash=record.content_hash or "" if record is not None else "", activation_reason="llm_selected", tool_hints=list(record.tool_hints) if record is not None else [], - team_template=getattr(record, "team_template", None) if record is not None else None, - team_template_warnings=list(getattr(record, "team_template_warnings", [])) if record is not None else [], ) ) return activated_skills diff --git a/app-instance/backend/beaver/skills/catalog/loader.py b/app-instance/backend/beaver/skills/catalog/loader.py index 901c332..d2d67ce 100644 --- a/app-instance/backend/beaver/skills/catalog/loader.py +++ b/app-instance/backend/beaver/skills/catalog/loader.py @@ -28,7 +28,6 @@ from .utils import ( check_requirements, escape_xml, extract_required_tool_names, - extract_skill_team_template, get_missing_requirements, parse_frontmatter, parse_skill_metadata_blob, @@ -50,8 +49,6 @@ class SkillRecord: tool_hints: list[str] = field(default_factory=list) frontmatter: dict[str, Any] = field(default_factory=dict) description: str = "" - team_template: dict[str, Any] | None = None - team_template_warnings: list[str] = field(default_factory=list) class SkillsLoader: @@ -116,7 +113,6 @@ class SkillsLoader: continue normalized_frontmatter = dict(frontmatter) meta_blob = parse_skill_metadata_blob(frontmatter.get("metadata", "")) - template_result = extract_skill_team_template(body) record = SkillRecord( name=name, path=skill_file, @@ -131,8 +127,6 @@ class SkillsLoader: ), frontmatter=normalized_frontmatter, description=str(frontmatter.get("description") or summarize_body(body) or name), - team_template=template_result.template, - team_template_warnings=template_result.warnings, ) if filter_unavailable and not self._record_available(record): continue @@ -152,7 +146,6 @@ class SkillsLoader: else: path = self.workspace_skills / name / "versions" / loaded.version.version / "SKILL.md" _frontmatter, body = parse_frontmatter(loaded.content) - template_result = extract_skill_team_template(body) record = SkillRecord( name=name, path=path, @@ -167,8 +160,6 @@ class SkillsLoader: ), frontmatter=dict(loaded.version.frontmatter), description=str(loaded.version.frontmatter.get("description") or loaded.version.summary or name), - team_template=template_result.template, - team_template_warnings=template_result.warnings, ) if filter_unavailable and not self._record_available(record): continue diff --git a/app-instance/backend/beaver/skills/catalog/utils.py b/app-instance/backend/beaver/skills/catalog/utils.py index 97d4cf3..c2f82ce 100644 --- a/app-instance/backend/beaver/skills/catalog/utils.py +++ b/app-instance/backend/beaver/skills/catalog/utils.py @@ -17,7 +17,6 @@ import json import os import re import shutil -from dataclasses import dataclass, field from typing import Any @@ -85,27 +84,6 @@ def strip_frontmatter(content: str) -> str: return body -@dataclass(slots=True) -class SkillTeamTemplateParseResult: - template: dict[str, Any] | None = None - warnings: list[str] = field(default_factory=list) - - -def extract_skill_team_template(body: str) -> SkillTeamTemplateParseResult: - matches = re.findall(r"```beaver-team-template\s*\n(.*?)\n```", body, re.DOTALL) - if not matches: - return SkillTeamTemplateParseResult() - if len(matches) != 1: - return SkillTeamTemplateParseResult(warnings=["skill defines multiple team templates"]) - try: - template = json.loads(matches[0]) - except json.JSONDecodeError: - return SkillTeamTemplateParseResult(warnings=["team template JSON is invalid"]) - if not isinstance(template, dict) or not isinstance(template.get("nodes", []), list): - return SkillTeamTemplateParseResult(warnings=["team template must be an object with a nodes list"]) - return SkillTeamTemplateParseResult(template=template) - - def extract_required_tool_names(body: str) -> list[str]: """从 canonical skill 正文的 `## Required Tools` 段落提取工具名。 diff --git a/app-instance/backend/beaver/tasks/attempt_orchestrator.py b/app-instance/backend/beaver/tasks/attempt_orchestrator.py index 4fd58d3..e8be682 100644 --- a/app-instance/backend/beaver/tasks/attempt_orchestrator.py +++ b/app-instance/backend/beaver/tasks/attempt_orchestrator.py @@ -5,12 +5,11 @@ from __future__ import annotations from time import perf_counter from typing import Any, Callable -from beaver.coordinator.models import ExecutionNode, TeamRunResult from beaver.engine import AgentRunResult from beaver.engine.context import SkillContext from beaver.prompts.main_agent import normalize_main_agent_prompt_locale -from .evidence import EvidenceBuilder, RunEvidence, TaskEvidencePacket, render_task_evidence +from .evidence import EvidenceBuilder, TaskEvidencePacket, render_task_evidence from .models import TaskRecord from .planner import TaskExecutionPlan @@ -46,7 +45,7 @@ class TaskAttemptOrchestrator: output_language_instruction = self._output_language_instruction(prompt_locale) provider_bundle = kwargs.get("provider_bundle") or self.make_provider_bundle_for_task(self.loaded, kwargs) kwargs = dict(kwargs) - team_provider_bundle_factory = kwargs.pop("team_provider_bundle_factory", None) + kwargs.pop("team_provider_bundle_factory", None) kwargs["provider_bundle"] = provider_bundle attempt_index = int(task.metadata.get("latest_attempt_index") or 0) + 1 @@ -87,75 +86,17 @@ class TaskAttemptOrchestrator: **plan.to_event_payload(), }, ) - team_summaries: list[str] = [] - team_execution_context = "" - team_result: TeamRunResult | None = None if plan.is_team: - team_result, team_error = await self._run_team_for_task( - plan, - task=task, - parent_session_id=kwargs["session_id"], - provider_bundle_factory=team_provider_bundle_factory - or self._build_team_provider_bundle_factory(kwargs), + plan = TaskExecutionPlan.single( + "legacy_planner_team_ignored", + planner_adaptation=plan.planner_adaptation, ) - if team_result is not None: - team_summaries = [self._team_summary_for_validation(team_result)] - team_packet = TaskEvidencePacket( - task_id=task.task_id, - attempt_index=attempt_index, - main_run=None, - team_runs=self._team_run_evidence(team_result), - team_node_results=list(team_result.node_results), - final_output="", - ) - team_execution_context = self._join_context( - self._team_execution_context(plan, team_result), - "Rendered team evidence:\n" + render_task_evidence(team_packet), - ) - self._append_task_observation( - session_manager, - task.session_id, - event_type="task_team_run_completed" if team_result.success else "task_team_run_failed", - payload={ - "task_id": task.task_id, - "attempt_index": attempt_index, - "plan_mode": plan.mode, - "strategy": plan.graph.strategy if plan.graph else None, - "node_ids": [node.node_id for node in plan.graph.nodes] if plan.graph else [], - "team_run_ids": team_result.run_ids, - "team_success": team_result.success, - "node_results": self._team_node_results_for_event(plan, team_result), - "reason": plan.reason, - "error": None if team_result.success else "one or more team nodes failed", - }, - ) - else: - team_summaries = [f"Team execution failed: {team_error}"] - team_execution_context = self._failed_team_execution_context(plan, team_error or "unknown error") - self._append_task_observation( - session_manager, - task.session_id, - event_type="task_team_run_failed", - payload={ - "task_id": task.task_id, - "attempt_index": attempt_index, - "plan_mode": plan.mode, - "strategy": plan.graph.strategy if plan.graph else None, - "node_ids": [node.node_id for node in plan.graph.nodes] if plan.graph else [], - "team_run_ids": [], - "team_success": False, - "reason": plan.reason, - "error": team_error, - }, - ) - - outcome_context, incomplete_prefix, outcome_metadata = self._team_synthesis_outcome( - plan, - team_result, - prompt_locale=prompt_locale, - ) - if plan.is_team: - team_execution_context = self._join_context(outcome_context, team_execution_context) + outcome_metadata = { + "task_outcome": "single", + "incomplete_node_ids": [], + "node_statuses": {}, + "evidence_gaps": {}, + } attempt_kwargs = dict(kwargs) attempt_kwargs.update( @@ -171,22 +112,15 @@ class TaskAttemptOrchestrator: attempt_kwargs["execution_context"] = self._join_context( base_execution_context, output_language_instruction, - team_execution_context, ) - if plan.is_team and team_execution_context: - attempt_kwargs["include_tools"] = False - attempt_kwargs["max_tool_iterations"] = 0 attempt_kwargs["skill_selection_context"] = self._build_skill_selection_context( task=task, user_message=message, attempt_index=attempt_index, plan=plan, - team_summaries=team_summaries, ) result = await runner(message, **attempt_kwargs) - if outcome_metadata["task_outcome"] == "incomplete": - result.output_text = self._apply_incomplete_prefix(result.output_text, incomplete_prefix) self._append_task_observation( session_manager, task.session_id, @@ -210,7 +144,6 @@ class TaskAttemptOrchestrator: task=task, attempt_index=attempt_index, result=result, - team_result=team_result, ) evidence_text = render_task_evidence(evidence_packet) evidence_debug = { @@ -256,31 +189,6 @@ class TaskAttemptOrchestrator: result.validation_result = None return result - async def _run_team_for_task( - self, - plan: TaskExecutionPlan, - *, - task: TaskRecord, - parent_session_id: str, - provider_bundle_factory: Any, - ) -> tuple[TeamRunResult | None, str | None]: - if plan.graph is None: - return None, "team plan did not include an execution graph" - try: - from beaver.services.team_service import TeamService - - result = await TeamService(self.create_loop()).run_team( - plan.graph, - parent_task_id=task.task_id, - parent_session_id=parent_session_id, - parent_run_id=None, - provider_bundle_factory=provider_bundle_factory, - allow_candidate_generation=False, - ) - return result, None - except Exception as exc: - return None, str(exc) - async def _assemble_task_attempt_skills( self, *, @@ -396,7 +304,6 @@ class TaskAttemptOrchestrator: user_message: str, attempt_index: int, plan: TaskExecutionPlan | None = None, - team_summaries: list[str] | None = None, ) -> str: phase = f"attempt_{attempt_index}" if task.feedback and task.feedback[-1].get("acceptance_type") == "revise": @@ -445,8 +352,6 @@ class TaskAttemptOrchestrator: ) ) sections.append("Execution plan:\n" + "\n".join(plan_lines)) - if team_summaries: - sections.append("Team execution summaries:\n" + "\n\n".join(team_summaries)[:2400]) sections.append( "Skill selection instruction:\n" "Prefer reusing previously activated skills when they still match the Task. " @@ -476,140 +381,6 @@ class TaskAttemptOrchestrator: def _join_context(*parts: str | None) -> str: return "\n\n".join(part.strip() for part in parts if part and part.strip()) - @staticmethod - def _team_summary_for_validation(result: TeamRunResult) -> str: - lines = [ - f"success={result.success}", - f"task_id={result.task_id or ''}", - "summary:", - result.summary, - "nodes:", - ] - for node in result.node_results: - lines.append( - f"- {node.node_id}: success={node.success} finish_reason={node.finish_reason} " - f"error={node.error or ''} output={node.output_text[:500]}" - ) - return "\n".join(lines) - - @staticmethod - def _team_node_results_for_event(plan: TaskExecutionPlan, result: TeamRunResult) -> list[dict[str, Any]]: - nodes = {node.node_id: node for node in plan.graph.nodes} if plan.graph else {} - payloads: list[dict[str, Any]] = [] - for item in result.node_results: - payload = item.to_dict() - node = nodes.get(item.node_id) - if node is not None: - payload["selected_skill_names"] = list(node.inherited_pinned_skills) - payload["ephemeral_skill_names"] = [ - skill.name for skill in node.inherited_pinned_skill_contexts - ] - payload["skill_query"] = node.agent.metadata.get("skill_query") - payload["ephemeral_guidance_id"] = node.agent.metadata.get("ephemeral_guidance_id") - payload["ephemeral_guidance_name"] = node.agent.metadata.get("ephemeral_guidance_name") - payload["ephemeral_used"] = bool(node.inherited_pinned_skill_contexts) - payloads.append(payload) - return payloads - - @staticmethod - def _team_run_evidence(result: TeamRunResult | None) -> list[RunEvidence]: - if result is None: - return [] - return [node.evidence for node in result.node_results if node.evidence is not None] - - @staticmethod - def _team_synthesis_outcome( - plan: TaskExecutionPlan, - result: TeamRunResult | None, - *, - prompt_locale: str | None = None, - ) -> tuple[str, str, dict[str, Any]]: - if not plan.is_team or plan.graph is None: - metadata = { - "task_outcome": "single", - "incomplete_node_ids": [], - "node_statuses": {}, - "evidence_gaps": {}, - } - return "Task outcome: single", "", metadata - - result_by_node = { - item.node_id: item - for item in (result.node_results if result is not None else []) - } - node_statuses: dict[str, str] = {} - evidence_gaps: dict[str, list[str]] = {} - incomplete_node_ids: list[str] = [] - detail_lines: list[str] = [] - successful_lines: list[str] = [] - for node in plan.graph.nodes: - node_result = result_by_node.get(node.node_id) - status = node_result.completion_status if node_result is not None else "not_run" - node_statuses[node.node_id] = status - gaps = list(node_result.evidence_gaps) if node_result is not None else [] - if gaps: - evidence_gaps[node.node_id] = gaps - if node.required_for_completion and status != "succeeded": - incomplete_node_ids.append(node.node_id) - detail_lines.append( - f"- {node.node_id}: status={status}, " - f"finish_reason={node_result.finish_reason if node_result is not None else 'not_run'}, " - f"error={(node_result.error or '') if node_result is not None else 'node did not run'}, " - f"evidence_gaps={gaps}" - ) - elif node_result is not None and status == "succeeded": - successful_lines.append(f"- {node.node_id}: {node_result.output_text[:1000]}") - - task_outcome = "incomplete" if incomplete_node_ids else "complete" - metadata = { - "task_outcome": task_outcome, - "incomplete_node_ids": incomplete_node_ids, - "node_statuses": node_statuses, - "evidence_gaps": evidence_gaps, - } - context_parts = [ - f"Task outcome: {task_outcome}", - "Incomplete node IDs: " + (", ".join(incomplete_node_ids) or "none"), - ] - if detail_lines: - context_parts.append("Incomplete required node details:\n" + "\n".join(detail_lines)) - if successful_lines: - context_parts.append("Available successful node evidence:\n" + "\n".join(successful_lines)) - if task_outcome == "incomplete": - context_parts.append( - "Synthesis requirement: produce a partial report from available evidence and explicitly state " - "that the task is incomplete, partially completed, or missing required evidence." - ) - prefix = TaskAttemptOrchestrator._incomplete_prefix(prompt_locale) if incomplete_node_ids else "" - return "\n\n".join(context_parts), prefix, metadata - - @staticmethod - def _incomplete_prefix(prompt_locale: str | None) -> str: - locale = normalize_main_agent_prompt_locale(prompt_locale) - if locale == "en": - return "Task incomplete: some required steps failed or lack required evidence. The report below uses available results only.\n\n" - if locale == "zh-Hant": - return "任務未完成:部分必要步驟失敗或缺少必要證據。以下內容僅基於現有結果。\n\n" - return "任务未完成:部分必要步骤失败或缺少必要证据。以下内容仅基于现有结果。\n\n" - - @staticmethod - def _apply_incomplete_prefix(output_text: str, prefix: str) -> str: - normalized = output_text.lower() - notices = ( - "任务未完成", - "任務未完成", - "部分完成", - "缺少证据", - "缺少證據", - "task incomplete", - "incomplete task", - "partially complete", - "missing evidence", - ) - if any(notice in normalized for notice in notices): - return output_text - return prefix + output_text.lstrip() - def _build_task_evidence_packet( self, *, @@ -617,7 +388,6 @@ class TaskAttemptOrchestrator: task: TaskRecord, attempt_index: int, result: AgentRunResult, - team_result: TeamRunResult | None, ) -> TaskEvidencePacket: main_run = EvidenceBuilder(session_manager).build_run_evidence( result.session_id, @@ -629,67 +399,7 @@ class TaskAttemptOrchestrator: task_id=task.task_id, attempt_index=attempt_index, main_run=main_run, - team_runs=self._team_run_evidence(team_result), - team_node_results=list(team_result.node_results) if team_result is not None else [], + team_runs=[], + team_node_results=[], final_output=result.output_text, ) - - @staticmethod - def _team_execution_context(plan: TaskExecutionPlan, result: TeamRunResult) -> str: - node_lines = [ - ( - f"- {node.node_id}: success={node.success}, finish_reason={node.finish_reason}, " - f"run_id={node.run_id or ''}, error={node.error or ''}\n{node.output_text}" - ) - for node in result.node_results - ] - return "\n\n".join( - item - for item in [ - "Task team execution result:", - f"Planner reason: {plan.reason}", - f"Strategy: {plan.graph.strategy if plan.graph else ''}", - f"Team success: {result.success}", - f"Team summary:\n{result.summary}", - "Node results:\n" + "\n\n".join(node_lines), - ( - "Final synthesis instruction:\n" + plan.final_synthesis_instruction - if plan.final_synthesis_instruction - else None - ), - ( - "Use successful team outputs as internal evidence. If one or more nodes failed, " - "do not blindly repeat failed tool calls. Produce a user-visible fallback answer " - "with available evidence and clearly state any missing or uncertain data." - ), - ] - if item - ) - - @staticmethod - def _failed_team_execution_context(plan: TaskExecutionPlan, error: str) -> str: - return "\n\n".join( - [ - "Task team execution failed before final synthesis.", - f"Planner reason: {plan.reason}", - f"Strategy: {plan.graph.strategy if plan.graph else ''}", - f"Error: {error}", - ( - "Proceed as the main agent. Do not blindly repeat failed tool calls; " - "produce a user-visible fallback answer with available evidence and clearly " - "state any missing or uncertain data." - ), - ] - ) - - def _build_team_provider_bundle_factory(self, kwargs: dict[str, Any]) -> Any: - def factory(node: ExecutionNode) -> Any: - node_kwargs = dict(kwargs) - node_kwargs.pop("provider_bundle", None) - if node.agent.model: - node_kwargs["model"] = node.agent.model - if node.agent.provider_name: - node_kwargs["provider_name"] = node.agent.provider_name - return self.make_provider_bundle_for_task(self.loaded, node_kwargs) - - return factory diff --git a/app-instance/backend/beaver/tasks/evidence.py b/app-instance/backend/beaver/tasks/evidence.py index b328434..9635abc 100644 --- a/app-instance/backend/beaver/tasks/evidence.py +++ b/app-instance/backend/beaver/tasks/evidence.py @@ -155,7 +155,10 @@ def evaluate_node_evidence( if not output_text.strip(): _append_unique(gaps, "missing required evidence: output") else: - _append_unique(gaps, f"unsupported evidence requirement: {requirement}") + # v1 only enforces the coarse machine-readable requirements above. + # Natural-language evidence requirements are preserved for later + # LLM-based validation and must not fail a node deterministically. + continue return gaps diff --git a/app-instance/backend/beaver/tasks/planner.py b/app-instance/backend/beaver/tasks/planner.py index 7d76d2d..747c98a 100644 --- a/app-instance/backend/beaver/tasks/planner.py +++ b/app-instance/backend/beaver/tasks/planner.py @@ -1,39 +1,27 @@ -"""Internal Task execution planner for single-agent vs team execution.""" +"""Internal Task execution planner for single-agent task attempts. + +Team execution is now started explicitly through local Team Workflow MCP tools. +This planner only records why the normal Task attempt should continue as a +single root-agent run. +""" from __future__ import annotations -import asyncio -import json import os from dataclasses import dataclass, field from typing import Any, Literal -from beaver.coordinator.models import AgentDescriptor, ExecutionGraph, ExecutionNode +from beaver.coordinator.models import ExecutionGraph from beaver.engine.context import SkillContext from beaver.engine.providers import ProviderBundle -from beaver.tools.registry import ToolRegistry from .models import TaskRecord -from .skill_resolver import SkillResolutionReport, TaskSkillResolver +from .skill_resolver import SkillResolutionReport TaskExecutionMode = Literal["single", "team"] -# Temporary name-based denylist until high-risk tool approval is implemented. -# Keep this policy centralized so planner behavior cannot drift by call site. -HIGH_RISK_PLANNER_TOOL_NAMES = frozenset( - { - "delete_file", - "execute_command", - "external_send", - "send_email", - "terminal", - "write_file", - } -) - - def _agent_team_enabled() -> bool: return os.getenv("BEAVER_AGENT_TEAM_ENABLED", "1").strip().lower() not in {"0", "false", "no", "off"} @@ -96,37 +84,7 @@ class TaskExecutionPlan: class TaskExecutionPlanner: - """Plan whether a Task attempt should run through a team first.""" - - _MAX_NODES = 6 - _MAX_DEPTH = 4 - _SUPPORTED_STRATEGIES = {"sequence", "parallel", "dag"} - _ALLOWED_NODE_FIELDS = { - "node_id", - "task", - "use_skill", - "skill_query", - "depends_on", - "input_contract", - "output_contract", - "requested_tools", - "required_evidence", - "evidence_contract", - "validation_rules", - "required_for_completion", - "block_downstream_on_partial", - "max_tool_iterations", - "constraints", - } - - def __init__( - self, - *, - task_skill_resolver: TaskSkillResolver | None = None, - tool_registry: ToolRegistry | None = None, - ) -> None: - self.task_skill_resolver = task_skill_resolver - self.tool_registry = tool_registry + """Return the current Task execution mode for the root AgentLoop.""" async def plan( self, @@ -144,122 +102,7 @@ class TaskExecutionPlanner: return TaskExecutionPlan.single("planner_disabled_by_environment") if not self._needs_team_planning(task=task, user_message=user_message): return TaskExecutionPlan.single("planner_skipped_simple_task") - - provider = None - model = None - if provider_bundle is not None: - provider = provider_bundle.auxiliary_provider or provider_bundle.main_provider - runtime = provider_bundle.auxiliary_runtime or provider_bundle.main_runtime - model = getattr(runtime, "model", None) - if provider is None: - return TaskExecutionPlan.single("planner_provider_unavailable") - selected_template, base_adaptation = self._select_team_template(activated_skills or []) - try: - response = await asyncio.wait_for( - provider.chat( - messages=[ - { - "role": "system", - "content": ( - "You choose whether an internal Beaver Task attempt should run as a single " - "main-agent pass or use a small sub-agent team first. Return only compact JSON." - ), - }, - { - "role": "user", - "content": self._prompt( - task=task, - user_message=user_message, - attempt_index=attempt_index, - skill_summaries=skill_summaries or [], - tool_hints=tool_hints or [], - activated_skills=activated_skills or [], - selected_template=selected_template, - ), - }, - ], - tools=None, - model=model, - max_tokens=4096, - temperature=0.0, - ), - timeout=timeout_seconds, - ) - try: - plan = self._from_json_or_raise(response.content or "") - except Exception as first_error: - repair_response = await asyncio.wait_for( - provider.chat( - messages=[ - { - "role": "system", - "content": "Repair invalid Beaver task planner JSON. Return only one compact JSON object.", - }, - { - "role": "user", - "content": ( - "Repair the invalid planner JSON using the task-only schema from the original " - f"request. Validation error: {first_error}\nInvalid output:\n{response.content or ''}" - ), - }, - ], - tools=None, - model=model, - max_tokens=4096, - temperature=0.0, - ), - timeout=timeout_seconds, - ) - try: - plan = self._from_json_or_raise(repair_response.content or "") - except Exception as repair_error: - return TaskExecutionPlan.single( - "planner_fallback_single", - fallback_error=f"initial validation: {first_error}; repair validation: {repair_error}", - planner_adaptation=base_adaptation, - ) - self._merge_adaptation(plan, base_adaptation) - return await self._resolve_plan( - plan, - task=task, - user_message=user_message, - attempt_index=attempt_index, - provider_bundle=provider_bundle, - ) - except Exception as exc: - detail = str(exc) - error = f"{type(exc).__name__}: {detail}" if detail else type(exc).__name__ - return TaskExecutionPlan.single("planner_failed", fallback_error=error) - - async def _resolve_plan( - self, - plan: TaskExecutionPlan, - *, - task: TaskRecord, - user_message: str, - attempt_index: int, - provider_bundle: ProviderBundle | None, - ) -> TaskExecutionPlan: - if not plan.is_team or self.task_skill_resolver is None: - return plan - if provider_bundle is None: - return TaskExecutionPlan.single("planner_fallback_single", fallback_error="task_skill_resolver_provider_unavailable") - try: - assert plan.graph is not None - graph, reports = await self.task_skill_resolver.resolve_graph( - plan.graph, - task=task, - user_message=user_message, - attempt_index=attempt_index, - provider_bundle=provider_bundle, - ) - graph.validate() - plan.graph = graph - plan.skill_resolution_report = reports - self._merge_skill_resolution_adaptation(plan, reports) - return plan - except Exception as exc: - return TaskExecutionPlan.single("planner_fallback_single", fallback_error=f"task_skill_resolver_failed: {exc}") + return TaskExecutionPlan.single("planner_team_replaced_by_workflow_tools") @staticmethod def _needs_team_planning(*, task: TaskRecord, user_message: str) -> bool: @@ -306,307 +149,3 @@ class TaskExecutionPlanner: "端到端", ) return any(marker in text for marker in complex_markers) - - def from_json(self, text: str) -> TaskExecutionPlan: - try: - return self._from_json_or_raise(text) - except Exception as exc: - return TaskExecutionPlan.single("planner_fallback_single", fallback_error=str(exc)) - - def _from_json_or_raise(self, text: str) -> TaskExecutionPlan: - payload = self._parse_json_object(text) - mode = str(payload.get("mode") or "single").strip().lower() - reason = str(payload.get("reason") or "") - adaptation = self._adaptation_from_payload(payload) - if mode != "team": - return TaskExecutionPlan.single( - reason or "planner_selected_single", - planner_adaptation=adaptation, - ) - - graph = self._graph_from_payload(payload, adaptation=adaptation) - graph.validate(max_depth=self._MAX_DEPTH) - return TaskExecutionPlan( - mode="team", - reason=reason or "planner_selected_team", - graph=graph, - final_synthesis_instruction=str(payload.get("final_synthesis_instruction") or ""), - planner_adaptation=adaptation, - ) - - def _graph_from_payload( - self, - payload: dict[str, Any], - *, - adaptation: dict[str, Any], - ) -> ExecutionGraph: - strategy = str(payload.get("strategy") or "sequence").strip().lower() - if strategy not in self._SUPPORTED_STRATEGIES: - raise ValueError(f"Unsupported team strategy: {strategy}") - raw_nodes = payload.get("nodes") - if not isinstance(raw_nodes, list) or not raw_nodes: - raise ValueError("Team plan requires at least one node") - if len(raw_nodes) > self._MAX_NODES: - raise ValueError(f"Team plan exceeds max node count {self._MAX_NODES}") - - nodes: list[ExecutionNode] = [] - for index, item in enumerate(raw_nodes, start=1): - if not isinstance(item, dict): - raise ValueError("Each team node must be an object") - unsupported = sorted(set(item) - self._ALLOWED_NODE_FIELDS) - if unsupported: - raise ValueError(f"Unsupported team node field(s): {', '.join(unsupported)}") - node_id = str(item.get("node_id") or f"node_{index}").strip() - task = str(item.get("task") or "").strip() - if not node_id or not task: - raise ValueError("Each team node requires node_id and task") - allowed_tool_names = self._resolve_requested_tools( - item.get("requested_tools"), - warnings=adaptation["warnings"], - ) - use_skill = _optional_str(item.get("use_skill")) - skill_query = _optional_str(item.get("skill_query")) or task - if use_skill is not None or "skill_query" in item: - adaptation.setdefault("node_skill_bindings", []).append( - { - "node_id": node_id, - "use_skill": use_skill, - "skill_query": skill_query, - } - ) - nodes.append( - ExecutionNode( - node_id=node_id, - task=task, - agent=AgentDescriptor( - name=node_id, - role="", - system_prompt="", - metadata={ - "use_skill": use_skill, - "skill_query": skill_query, - "required_capabilities": [], - "requested_tags": [], - "sub_agent_kind": "generic_skill_worker", - }, - ), - depends_on=[str(dep) for dep in item.get("depends_on") or []], - constraints=[str(value) for value in item.get("constraints") or []], - input_contract=_dict_value(item.get("input_contract")), - output_contract=_dict_value(item.get("output_contract")), - allowed_tool_names=allowed_tool_names, - required_evidence=_string_list(item.get("required_evidence")), - evidence_contract=_dict_value(item.get("evidence_contract")), - validation_rules=_string_list(item.get("validation_rules")), - required_for_completion=bool(item.get("required_for_completion", True)), - block_downstream_on_partial=bool(item.get("block_downstream_on_partial", False)), - max_tool_iterations=_optional_int(item.get("max_tool_iterations")), - ) - ) - return ExecutionGraph(strategy=strategy, nodes=nodes) # type: ignore[arg-type] - - def _resolve_requested_tools(self, value: Any, *, warnings: list[str]) -> list[str] | None: - if value is None: - return None - result: list[str] = [] - for name in _string_list(value): - if name.lower() in HIGH_RISK_PLANNER_TOOL_NAMES: - _append_unique(warnings, f"requires_high_risk_review: {name}") - continue - if self.tool_registry is None or self.tool_registry.get(name) is None: - _append_unique(warnings, f"unknown tool removed: {name}") - continue - result.append(name) - return result - - @staticmethod - def _adaptation_from_payload(payload: dict[str, Any]) -> dict[str, Any]: - raw = payload.get("adaptation") - adaptation = dict(raw) if isinstance(raw, dict) else {} - adaptation["warnings"] = _string_list(adaptation.get("warnings")) - return adaptation - - @staticmethod - def _select_team_template( - activated_skills: list[SkillContext], - ) -> tuple[SkillContext | None, dict[str, Any]]: - candidates = [ - skill - for skill in activated_skills - if isinstance(skill.team_template, dict) and isinstance(skill.team_template.get("nodes"), list) - ] - selected = candidates[0] if candidates else None - warnings: list[str] = [] - for skill in activated_skills: - for warning in skill.team_template_warnings: - _append_unique(warnings, f"{skill.name}: {warning}") - return selected, { - "template_used": False, - "selected_template": selected.name if selected else None, - "selection_reason": ( - "first activated skill with a valid team template" - if selected - else "no activated skill has a valid team template" - ), - "ignored_templates": [skill.name for skill in candidates[1:]], - "warnings": warnings, - } - - @staticmethod - def _merge_adaptation(plan: TaskExecutionPlan, base: dict[str, Any]) -> None: - payload = dict(plan.planner_adaptation) - warnings: list[str] = [] - for warning in [*base.get("warnings", []), *payload.get("warnings", [])]: - _append_unique(warnings, str(warning)) - merged = { - "template_used": bool(payload.get("template_used", False)), - "selected_template": base.get("selected_template"), - "selection_reason": base.get("selection_reason"), - "ignored_templates": list(base.get("ignored_templates", [])), - "warnings": warnings, - } - if isinstance(payload.get("node_skill_bindings"), list): - merged["node_skill_bindings"] = [dict(item) for item in payload["node_skill_bindings"] if isinstance(item, dict)] - plan.planner_adaptation = merged - - @staticmethod - def _merge_skill_resolution_adaptation( - plan: TaskExecutionPlan, - reports: list[SkillResolutionReport], - ) -> None: - warnings = plan.planner_adaptation.setdefault("warnings", []) - bindings = plan.planner_adaptation.get("node_skill_bindings") - binding_by_node = { - str(item.get("node_id")): item - for item in bindings or [] - if isinstance(item, dict) - } - for report in reports: - for warning in report.warnings: - _append_unique(warnings, warning) - binding = binding_by_node.get(report.node_id) - if binding is not None and report.requested_skill_name and not report.exact_binding_used: - binding["fallback_reason"] = f"use_skill unresolved; {report.reason}" - - @staticmethod - def _prompt( - *, - task: TaskRecord, - user_message: str, - attempt_index: int, - skill_summaries: list[str] | None = None, - tool_hints: list[str] | None = None, - activated_skills: list[SkillContext] | None = None, - selected_template: SkillContext | None = None, - ) -> str: - history_note = "" - if task.feedback: - history_note = "\nRelevant task history:\n" + json.dumps(task.feedback[-5:], ensure_ascii=False) - skill_note = "" - if skill_summaries: - skill_note = "\nActivated skill summaries:\n" + "\n".join(f"- {item}" for item in skill_summaries) - guidance_note = "" - if activated_skills: - guidance_note = "\nActivated Skill guidance:\n" + "\n".join( - f"[{skill.name}]\n{skill.content}" for skill in activated_skills - ) - template_note = "" - if selected_template is not None: - template_note = "\nPrimary Skill team template:\n" + json.dumps( - { - "skill_name": selected_template.name, - "skill_version": selected_template.version, - "template": selected_template.team_template, - }, - ensure_ascii=False, - indent=2, - ) - tool_note = "" - if tool_hints: - tool_note = "\nActivated skill tool hints:\n" + "\n".join(f"- {item}" for item in tool_hints) - return ( - "Decide execution mode for this internal Task attempt.\n" - "Use mode=team only when independent research, review, implementation slices, or staged checks " - "would materially improve the result. Otherwise use mode=single.\n\n" - "JSON schema:\n" - "{\n" - ' "mode": "single" | "team",\n' - ' "reason": "short reason",\n' - ' "strategy": "sequence" | "parallel" | "dag",\n' - ' "nodes": [{"node_id": "collect", "task": "...", "use_skill": "optional exact skill", ' - '"skill_query": "optional dynamic skill query", "depends_on": [], ' - '"input_contract": {}, "output_contract": {}, "requested_tools": [], ' - '"required_evidence": [], "evidence_contract": {}, "validation_rules": [], ' - '"required_for_completion": true, "block_downstream_on_partial": false, ' - '"max_tool_iterations": 3, "constraints": []}],\n' - ' "adaptation": {"template_used": true, "warnings": []},\n' - ' "final_synthesis_instruction": "how the main agent should synthesize team output"\n' - "}\n\n" - "Node definitions are task-only. Never output agent or role fields. Use at most one primary " - "Skill template; treat all other activated Skills as guidance.\n\n" - f"Task goal:\n{task.goal}\n\n" - f"Current user request:\n{user_message}\n\n" - f"Attempt index: {attempt_index}\n" - f"{skill_note}" - f"{guidance_note}" - f"{template_note}" - f"{tool_note}" - f"{history_note}" - ) - - @staticmethod - def _parse_json_object(text: str) -> dict[str, Any]: - cleaned = text.strip() - if cleaned.startswith("```"): - cleaned = cleaned.strip("`") - if cleaned.lower().startswith("json"): - cleaned = cleaned[4:].strip() - start = cleaned.find("{") - end = cleaned.rfind("}") - if start >= 0 and end >= start: - cleaned = cleaned[start : end + 1] - payload = json.loads(cleaned) - if not isinstance(payload, dict): - raise ValueError("planner response must be a JSON object") - return payload - - -def _optional_str(value: Any) -> str | None: - if value in (None, ""): - return None - text = str(value).strip() - return text or None - - -def _optional_int(value: Any) -> int | None: - if value in (None, ""): - return None - if isinstance(value, bool): - raise ValueError("max_tool_iterations must be an integer") - result = int(value) - if result < 0: - raise ValueError("max_tool_iterations must be non-negative") - return result - - -def _dict_value(value: Any) -> dict[str, Any]: - return dict(value) if isinstance(value, dict) else {} - - -def _append_unique(values: list[str], value: str) -> None: - if value and value not in values: - values.append(value) - - -def _string_list(value: Any) -> list[str]: - if not isinstance(value, list): - if isinstance(value, str): - value = [item.strip() for item in value.split(",")] - else: - return [] - result: list[str] = [] - for item in value: - text = str(item).strip() - if text and text not in result: - result.append(text) - return result diff --git a/app-instance/backend/beaver/team_workflows/__init__.py b/app-instance/backend/beaver/team_workflows/__init__.py new file mode 100644 index 0000000..efb979d --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/__init__.py @@ -0,0 +1,2 @@ +"""Local team workflow graph builders.""" + diff --git a/app-instance/backend/beaver/team_workflows/agent_rearrange.py b/app-instance/backend/beaver/team_workflows/agent_rearrange.py new file mode 100644 index 0000000..f211bf9 --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/agent_rearrange.py @@ -0,0 +1,70 @@ +"""AgentRearrange graph builder using arrow/comma flow syntax.""" + +from __future__ import annotations + +from typing import Any, Iterable + +from beaver.coordinator.models import ExecutionGraph + +from .base import ( + WorkflowAgentSpec, + agent_name_set, + build_graph_from_dependencies, + edges_to_dependencies, + parse_agents, + validate_no_disconnected_agents, +) + +WORKFLOW_NAME = "AgentRearrange" + + +def build_graph( + *, + task: str, + agents: Iterable[WorkflowAgentSpec | dict[str, Any]], + flow: str, +) -> ExecutionGraph: + del task + parsed = parse_agents(agents) + edges = parse_flow(flow, known_agents=agent_name_set(parsed)) + dependencies = edges_to_dependencies(agents=parsed, edges=edges) + validate_no_disconnected_agents(agents=parsed, dependencies=dependencies) + return build_graph_from_dependencies( + workflow_name=WORKFLOW_NAME, + strategy="dag", + agents=parsed, + dependencies=dependencies, + ) + + +def parse_flow(flow: str, *, known_agents: set[str]) -> list[tuple[str, str]]: + stages = _parse_stages(flow) + edges: list[tuple[str, str]] = [] + for stage in stages: + for name in stage: + if name not in known_agents: + raise ValueError(f"workflow flow references unknown agent: {name}") + for left, right in zip(stages, stages[1:], strict=False): + for source in left: + for target in right: + edge = (source, target) + if edge not in edges: + edges.append(edge) + return edges + + +def _parse_stages(flow: str) -> list[list[str]]: + raw_flow = str(flow or "").strip() + if not raw_flow: + raise ValueError("workflow flow is required") + stages: list[list[str]] = [] + for raw_stage in raw_flow.split("->"): + names = [name.strip() for name in raw_stage.split(",") if name.strip()] + if not names: + raise ValueError("workflow flow contains an empty stage") + if len(names) != len(set(names)): + raise ValueError("workflow flow contains duplicate agent names in a stage") + stages.append(names) + if len(stages) < 2: + raise ValueError("workflow flow must contain at least two stages") + return stages diff --git a/app-instance/backend/beaver/team_workflows/base.py b/app-instance/backend/beaver/team_workflows/base.py new file mode 100644 index 0000000..ee85edd --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/base.py @@ -0,0 +1,273 @@ +"""Shared builders for local team workflow graph construction.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Iterable, Literal + +from beaver.coordinator.models import AgentDescriptor, ExecutionGraph, ExecutionNode + + +GraphStrategy = Literal["sequence", "parallel", "dag"] + + +@dataclass(slots=True) +class WorkflowAgentSpec: + name: str + instruction: str + use_skill: str | None = None + skill_query: str | None = None + allowed_tool_names: list[str] | None = None + required_evidence: list[str] = field(default_factory=list) + evidence_contract: dict[str, Any] = field(default_factory=dict) + validation_rules: list[str] = field(default_factory=list) + required_for_completion: bool = True + block_downstream_on_partial: bool = False + max_tool_iterations: int | None = None + constraints: list[str] = field(default_factory=list) + expected_output: str | None = None + input_contract: dict[str, Any] = field(default_factory=dict) + output_contract: dict[str, Any] = field(default_factory=dict) + + +@dataclass(slots=True) +class WorkflowBuildResult: + graph: ExecutionGraph + workflow_name: str + + +def parse_agents(raw_agents: Iterable[WorkflowAgentSpec | dict[str, Any]]) -> list[WorkflowAgentSpec]: + agents: list[WorkflowAgentSpec] = [] + for index, raw in enumerate(raw_agents, start=1): + if isinstance(raw, WorkflowAgentSpec): + spec = raw + elif isinstance(raw, dict): + spec = _agent_from_dict(raw, index=index) + else: + raise ValueError("workflow agents must be objects") + agents.append(spec) + validate_agent_names(agents) + return agents + + +def validate_agent_names(agents: list[WorkflowAgentSpec]) -> None: + if not agents: + raise ValueError("workflow requires at least one agent") + seen: set[str] = set() + for agent in agents: + if not agent.name: + raise ValueError("workflow agent name is required") + if not agent.instruction: + raise ValueError(f"workflow agent {agent.name!r} requires instruction") + if agent.name in seen: + raise ValueError(f"workflow agent names must be unique: {agent.name}") + seen.add(agent.name) + + +def agent_name_set(agents: list[WorkflowAgentSpec]) -> set[str]: + return {agent.name for agent in agents} + + +def build_graph_from_dependencies( + *, + workflow_name: str, + strategy: GraphStrategy, + agents: list[WorkflowAgentSpec], + dependencies: dict[str, list[str]], +) -> ExecutionGraph: + nodes = [ + build_node( + workflow_name=workflow_name, + agent=agent, + depends_on=dependencies.get(agent.name, []), + ) + for agent in agents + ] + graph = ExecutionGraph(strategy=strategy, nodes=nodes) + graph.validate() + return graph + + +def build_node( + *, + workflow_name: str, + agent: WorkflowAgentSpec, + depends_on: list[str], +) -> ExecutionNode: + metadata = { + "sub_agent_kind": "generic_skill_worker", + "workflow_tool": workflow_name, + "workflow_agent_name": agent.name, + } + if agent.use_skill: + metadata["use_skill"] = agent.use_skill + if agent.skill_query: + metadata["skill_query"] = agent.skill_query + return ExecutionNode( + node_id=agent.name, + task=agent.instruction, + agent=AgentDescriptor( + name=agent.name, + role="", + system_prompt="", + metadata=metadata, + ), + depends_on=list(depends_on), + constraints=list(agent.constraints), + expected_output=agent.expected_output, + input_contract=dict(agent.input_contract), + output_contract=dict(agent.output_contract), + allowed_tool_names=( + None if agent.allowed_tool_names is None else list(agent.allowed_tool_names) + ), + required_evidence=list(agent.required_evidence), + evidence_contract=dict(agent.evidence_contract), + validation_rules=list(agent.validation_rules), + required_for_completion=agent.required_for_completion, + block_downstream_on_partial=agent.block_downstream_on_partial, + max_tool_iterations=agent.max_tool_iterations, + ) + + +def edges_to_dependencies( + *, + agents: list[WorkflowAgentSpec], + edges: Iterable[tuple[str, str] | list[str]], +) -> dict[str, list[str]]: + known = agent_name_set(agents) + dependencies = {agent.name: [] for agent in agents} + for raw_edge in edges: + source, target = _parse_edge(raw_edge) + if source not in known: + raise ValueError(f"workflow edge references unknown agent: {source}") + if target not in known: + raise ValueError(f"workflow edge references unknown agent: {target}") + if source == target: + raise ValueError(f"workflow edge creates a self-cycle: {source}") + if source not in dependencies[target]: + dependencies[target].append(source) + return dependencies + + +def validate_output_agent( + *, + agents: list[WorkflowAgentSpec], + dependencies: dict[str, list[str]], + output_agent: str, + allow_disconnected: bool = False, +) -> None: + known = agent_name_set(agents) + if output_agent not in known: + raise ValueError(f"workflow output_agent references unknown agent: {output_agent}") + + upstream = _upstream_nodes(output_agent, dependencies) + if not upstream: + raise ValueError(f"workflow output_agent {output_agent!r} must be reachable from upstream agents") + + if allow_disconnected: + return + + connected = set(upstream) + connected.add(output_agent) + disconnected = sorted(known - connected) + if disconnected: + raise ValueError(f"workflow has disconnected agent(s): {', '.join(disconnected)}") + + +def validate_no_disconnected_agents( + *, + agents: list[WorkflowAgentSpec], + dependencies: dict[str, list[str]], +) -> None: + known = agent_name_set(agents) + connected: set[str] = set() + for target, sources in dependencies.items(): + if sources: + connected.add(target) + connected.update(sources) + disconnected = sorted(known - connected) + if disconnected: + raise ValueError(f"workflow has disconnected agent(s): {', '.join(disconnected)}") + + +def _agent_from_dict(raw: dict[str, Any], *, index: int) -> WorkflowAgentSpec: + name = _required_str(raw.get("name"), f"agents[{index}].name") + instruction = _required_str(raw.get("instruction"), f"agents[{index}].instruction") + return WorkflowAgentSpec( + name=name, + instruction=instruction, + use_skill=_optional_str(raw.get("use_skill")), + skill_query=_optional_str(raw.get("skill_query")), + allowed_tool_names=_optional_string_list(raw.get("allowed_tool_names")), + required_evidence=_string_list(raw.get("required_evidence")), + evidence_contract=_dict(raw.get("evidence_contract")), + validation_rules=_string_list(raw.get("validation_rules")), + required_for_completion=bool(raw.get("required_for_completion", True)), + block_downstream_on_partial=bool(raw.get("block_downstream_on_partial", False)), + max_tool_iterations=_optional_int(raw.get("max_tool_iterations")), + constraints=_string_list(raw.get("constraints")), + expected_output=_optional_str(raw.get("expected_output")), + input_contract=_dict(raw.get("input_contract")), + output_contract=_dict(raw.get("output_contract")), + ) + + +def _parse_edge(raw_edge: tuple[str, str] | list[str]) -> tuple[str, str]: + if not isinstance(raw_edge, (list, tuple)) or len(raw_edge) != 2: + raise ValueError("workflow edges must be [source, target] pairs") + source = _required_str(raw_edge[0], "edge source") + target = _required_str(raw_edge[1], "edge target") + return source, target + + +def _upstream_nodes(node_id: str, dependencies: dict[str, list[str]]) -> set[str]: + result: set[str] = set() + + def visit(current: str) -> None: + for dependency in dependencies.get(current, []): + if dependency in result: + continue + result.add(dependency) + visit(dependency) + + visit(node_id) + return result + + +def _required_str(value: Any, label: str) -> str: + text = str(value or "").strip() + if not text: + raise ValueError(f"{label} is required") + return text + + +def _optional_str(value: Any) -> str | None: + text = str(value or "").strip() + return text or None + + +def _string_list(value: Any) -> list[str]: + if value is None: + return [] + if not isinstance(value, list): + raise ValueError("expected a list of strings") + return [str(item).strip() for item in value if str(item).strip()] + + +def _optional_string_list(value: Any) -> list[str] | None: + if value is None: + return None + return _string_list(value) + + +def _dict(value: Any) -> dict[str, Any]: + return dict(value) if isinstance(value, dict) else {} + + +def _optional_int(value: Any) -> int | None: + if value is None: + return None + try: + return int(value) + except (TypeError, ValueError) as exc: + raise ValueError("max_tool_iterations must be an integer") from exc diff --git a/app-instance/backend/beaver/team_workflows/concurrent.py b/app-instance/backend/beaver/team_workflows/concurrent.py new file mode 100644 index 0000000..a01271b --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/concurrent.py @@ -0,0 +1,26 @@ +"""ConcurrentWorkflow graph builder.""" + +from __future__ import annotations + +from typing import Any, Iterable + +from beaver.coordinator.models import ExecutionGraph + +from .base import WorkflowAgentSpec, build_graph_from_dependencies, parse_agents + +WORKFLOW_NAME = "ConcurrentWorkflow" + + +def build_graph( + *, + task: str, + agents: Iterable[WorkflowAgentSpec | dict[str, Any]], +) -> ExecutionGraph: + del task + parsed = parse_agents(agents) + return build_graph_from_dependencies( + workflow_name=WORKFLOW_NAME, + strategy="parallel", + agents=parsed, + dependencies={agent.name: [] for agent in parsed}, + ) diff --git a/app-instance/backend/beaver/team_workflows/executor.py b/app-instance/backend/beaver/team_workflows/executor.py new file mode 100644 index 0000000..bcfae93 --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/executor.py @@ -0,0 +1,174 @@ +"""Runtime bridge for local team workflow MCP tools.""" + +from __future__ import annotations + +import json +from typing import Any, Callable + +from beaver.coordinator.models import ExecutionGraph, TeamRunResult +from beaver.tools.base import ToolContext, ToolResult + +from . import agent_rearrange, concurrent, graph, mixture_of_agents, sequential + +GraphBuilder = Callable[..., ExecutionGraph] + + +class TeamWorkflowExecutor: + """Execute workflow MCP calls inside the current Beaver runtime.""" + + _BUILDERS: dict[str, GraphBuilder] = { + "SequentialWorkflow": sequential.build_graph, + "ConcurrentWorkflow": concurrent.build_graph, + "MixtureOfAgents": mixture_of_agents.build_graph, + "AgentRearrange": agent_rearrange.build_graph, + "GraphWorkflow": graph.build_graph, + } + + async def execute( + self, + workflow_name: str, + arguments: dict[str, Any], + context: ToolContext, + *, + tool_name: str | None = None, + ) -> ToolResult: + exposed_name = tool_name or workflow_name + try: + if str(context.metadata.get("source") or "").startswith("team:"): + raise ValueError("nested_team_workflow_not_allowed") + builder = self._BUILDERS.get(workflow_name) + if builder is None: + raise ValueError(f"unknown team workflow tool: {workflow_name}") + graph = builder(**dict(arguments or {})) + parent_task_id = _task_id(context) + parent_session_id = _session_id(context) + result = await self._run_team( + context=context, + graph=graph, + parent_task_id=parent_task_id, + parent_session_id=parent_session_id, + ) + payload = _success_payload( + workflow_name=workflow_name, + graph=graph, + result=result, + ) + return ToolResult( + success=True, + content=json.dumps(payload, ensure_ascii=False), + tool_name=exposed_name, + raw_output=payload, + ) + except Exception as exc: + payload = { + "success": False, + "workflow": workflow_name, + "error": str(exc), + } + return ToolResult( + success=False, + content=json.dumps(payload, ensure_ascii=False), + tool_name=exposed_name, + error=str(exc), + raw_output=payload, + ) + + async def _run_team( + self, + *, + context: ToolContext, + graph: ExecutionGraph, + parent_task_id: str, + parent_session_id: str, + ) -> TeamRunResult: + runner = context.services.get("agent_team_runner") + parent_run_id = _run_id(context) + if runner is not None: + return await runner( + graph, + parent_task_id=parent_task_id, + parent_session_id=parent_session_id, + parent_run_id=parent_run_id, + ) + + agent_loop = context.services.get("agent_loop") + if agent_loop is None: + raise ValueError("team workflow execution requires agent_loop or agent_team_runner") + provider_bundle = context.services.get("provider_bundle") + + def provider_bundle_factory(_node: Any) -> Any: + return provider_bundle + + from beaver.engine import AgentLoop + from beaver.services.team_service import TeamService + + loaded = context.services.get("loaded") + team_loop = AgentLoop(profile=agent_loop.profile, loader=agent_loop.loader) + team_loop.loaded = loaded + return await TeamService(team_loop).run_team( + graph, + parent_task_id=parent_task_id, + parent_session_id=parent_session_id, + parent_run_id=parent_run_id, + provider_bundle_factory=provider_bundle_factory if provider_bundle is not None else None, + allow_candidate_generation=False, + ) + + +def _task_id(context: ToolContext) -> str: + value = str(context.services.get("task_id") or context.metadata.get("task_id") or "").strip() + if not value: + raise ValueError("team workflow execution requires task_id") + return value + + +def _session_id(context: ToolContext) -> str: + value = str(context.session_id or context.services.get("session_id") or "").strip() + if not value: + raise ValueError("team workflow execution requires session_id") + return value + + +def _run_id(context: ToolContext) -> str | None: + return str(context.services.get("run_id") or context.metadata.get("run_id") or "").strip() or None + + +def _success_payload( + *, + workflow_name: str, + graph: ExecutionGraph, + result: TeamRunResult, +) -> dict[str, Any]: + return { + "success": result.success, + "workflow": workflow_name, + "summary": result.summary, + "run_ids": list(result.run_ids), + "session_ids": list(result.session_ids), + "node_results": [item.to_dict() for item in result.node_results], + "graph": _graph_to_dict(graph), + } + + +def _graph_to_dict(graph: ExecutionGraph) -> dict[str, Any]: + return { + "strategy": graph.strategy, + "nodes": [ + { + "node_id": node.node_id, + "task": node.task, + "depends_on": list(node.depends_on), + "allowed_tool_names": ( + None if node.allowed_tool_names is None else list(node.allowed_tool_names) + ), + "required_evidence": list(node.required_evidence), + "evidence_contract": dict(node.evidence_contract), + "validation_rules": list(node.validation_rules), + "required_for_completion": node.required_for_completion, + "block_downstream_on_partial": node.block_downstream_on_partial, + "max_tool_iterations": node.max_tool_iterations, + "metadata": dict(node.agent.metadata), + } + for node in graph.nodes + ], + } diff --git a/app-instance/backend/beaver/team_workflows/graph.py b/app-instance/backend/beaver/team_workflows/graph.py new file mode 100644 index 0000000..1618e72 --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/graph.py @@ -0,0 +1,45 @@ +"""GraphWorkflow explicit DAG builder.""" + +from __future__ import annotations + +from typing import Any, Iterable + +from beaver.coordinator.models import ExecutionGraph + +from .base import ( + WorkflowAgentSpec, + build_graph_from_dependencies, + edges_to_dependencies, + parse_agents, + validate_output_agent, +) + +WORKFLOW_NAME = "GraphWorkflow" + + +def build_graph( + *, + task: str, + agents: Iterable[WorkflowAgentSpec | dict[str, Any]], + edges: Iterable[tuple[str, str] | list[str]], + output_agent: str, + allow_disconnected: bool = False, +) -> ExecutionGraph: + del task + parsed = parse_agents(agents) + edge_list = list(edges or []) + if not edge_list: + raise ValueError("GraphWorkflow requires edges") + dependencies = edges_to_dependencies(agents=parsed, edges=edge_list) + validate_output_agent( + agents=parsed, + dependencies=dependencies, + output_agent=str(output_agent or "").strip(), + allow_disconnected=allow_disconnected, + ) + return build_graph_from_dependencies( + workflow_name=WORKFLOW_NAME, + strategy="dag", + agents=parsed, + dependencies=dependencies, + ) diff --git a/app-instance/backend/beaver/team_workflows/mcp_tools.py b/app-instance/backend/beaver/team_workflows/mcp_tools.py new file mode 100644 index 0000000..c33b5b3 --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/mcp_tools.py @@ -0,0 +1,261 @@ +"""MCP schema tools for local team workflow graph builders.""" + +from __future__ import annotations + +import json +from typing import Any, Callable + +from beaver.coordinator.models import ExecutionGraph +from beaver.tools.base import BaseTool, ToolContext, ToolResult, ToolSpec + +from . import agent_rearrange, concurrent, graph, mixture_of_agents, sequential + +GraphBuilder = Callable[..., ExecutionGraph] + + +def create_team_workflow_tools() -> list[BaseTool]: + return [ + TeamWorkflowSchemaTool( + name="SequentialWorkflow", + description=( + "Build a sequential Beaver team workflow graph. Use this for staged work " + "where each agent depends on the previous agent's output." + ), + input_schema=_sequential_schema(), + builder=sequential.build_graph, + ), + TeamWorkflowSchemaTool( + name="ConcurrentWorkflow", + description=( + "Build a concurrent Beaver team workflow graph. Use this only when agents " + "can work independently on the same task." + ), + input_schema=_concurrent_schema(), + builder=concurrent.build_graph, + ), + TeamWorkflowSchemaTool( + name="MixtureOfAgents", + description=( + "Build a mixture-of-agents Beaver team workflow graph where independent " + "expert agents feed one aggregator agent." + ), + input_schema=_mixture_schema(), + builder=mixture_of_agents.build_graph, + ), + TeamWorkflowSchemaTool( + name="AgentRearrange", + description=( + "Build a Beaver team workflow graph from strict flow syntax. Use '->' for " + "stage order and ',' for agents in the same parallel stage." + ), + input_schema=_agent_rearrange_schema(), + builder=agent_rearrange.build_graph, + ), + TeamWorkflowSchemaTool( + name="GraphWorkflow", + description=( + "Build an explicit Beaver DAG workflow graph. Use this advanced tool only " + "when the dependency edges must be specified directly." + ), + input_schema=_graph_schema(), + builder=graph.build_graph, + ), + ] + + +class TeamWorkflowSchemaTool(BaseTool): + def __init__( + self, + *, + name: str, + description: str, + input_schema: dict[str, Any], + builder: GraphBuilder, + ) -> None: + self._spec = ToolSpec( + name=name, + description=description, + input_schema=input_schema, + toolset="team_workflow", + always_available=False, + metadata={"category": "team_workflow"}, + ) + self._builder = builder + + @property + def spec(self) -> ToolSpec: + return self._spec + + async def invoke(self, arguments: dict[str, Any], context: ToolContext) -> ToolResult: + del context + try: + graph = self._builder(**dict(arguments or {})) + payload = { + "success": True, + "workflow": self.spec.name, + "graph": _graph_to_dict(graph), + } + return ToolResult( + success=True, + content=json.dumps(payload, ensure_ascii=False), + tool_name=self.spec.name, + raw_output=payload, + ) + except Exception as exc: + payload = {"success": False, "workflow": self.spec.name, "error": str(exc)} + return ToolResult( + success=False, + content=json.dumps(payload, ensure_ascii=False), + tool_name=self.spec.name, + error=str(exc), + raw_output=payload, + ) + + +def _graph_to_dict(graph: ExecutionGraph) -> dict[str, Any]: + return { + "strategy": graph.strategy, + "nodes": [ + { + "node_id": node.node_id, + "task": node.task, + "depends_on": list(node.depends_on), + "allowed_tool_names": ( + None if node.allowed_tool_names is None else list(node.allowed_tool_names) + ), + "required_evidence": list(node.required_evidence), + "evidence_contract": dict(node.evidence_contract), + "validation_rules": list(node.validation_rules), + "required_for_completion": node.required_for_completion, + "block_downstream_on_partial": node.block_downstream_on_partial, + "max_tool_iterations": node.max_tool_iterations, + "metadata": dict(node.agent.metadata), + } + for node in graph.nodes + ], + } + + +def _sequential_schema() -> dict[str, Any]: + return { + "type": "object", + "properties": { + "task": _task_schema(), + "agents": _agents_schema(), + }, + "required": ["task", "agents"], + "additionalProperties": False, + } + + +def _concurrent_schema() -> dict[str, Any]: + return { + "type": "object", + "properties": { + "task": _task_schema(), + "agents": _agents_schema(), + }, + "required": ["task", "agents"], + "additionalProperties": False, + } + + +def _mixture_schema() -> dict[str, Any]: + return { + "type": "object", + "properties": { + "task": _task_schema(), + "agents": _agents_schema(description="Expert agents that run independently before aggregation."), + "aggregator": _agent_schema(description="Aggregator agent that synthesizes expert outputs."), + }, + "required": ["task", "agents", "aggregator"], + "additionalProperties": False, + } + + +def _agent_rearrange_schema() -> dict[str, Any]: + return { + "type": "object", + "properties": { + "task": _task_schema(), + "agents": _agents_schema(), + "flow": { + "type": "string", + "description": "Strict flow syntax, e.g. 'collector -> tactics, players -> synthesizer'.", + }, + }, + "required": ["task", "agents", "flow"], + "additionalProperties": False, + } + + +def _graph_schema() -> dict[str, Any]: + return { + "type": "object", + "properties": { + "task": _task_schema(), + "agents": _agents_schema(), + "edges": { + "type": "array", + "description": "Directed dependency edges as [source_agent, target_agent] pairs.", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": {"type": "string"}, + }, + }, + "output_agent": { + "type": "string", + "description": "Final output/synthesis agent. Must be reachable from upstream agents.", + }, + "allow_disconnected": { + "type": "boolean", + "description": "Allow agents that are not connected to output_agent. Defaults to false.", + }, + }, + "required": ["task", "agents", "edges", "output_agent"], + "additionalProperties": False, + } + + +def _task_schema() -> dict[str, Any]: + return { + "type": "string", + "description": "Overall user task this workflow supports.", + } + + +def _agents_schema(*, description: str = "Workflow agents in the order or set used by this workflow.") -> dict[str, Any]: + return { + "type": "array", + "description": description, + "items": _agent_schema(), + "minItems": 1, + } + + +def _agent_schema(*, description: str = "One workflow agent slot.") -> dict[str, Any]: + return { + "type": "object", + "description": description, + "properties": { + "name": {"type": "string"}, + "instruction": {"type": "string"}, + "use_skill": {"type": "string"}, + "skill_query": {"type": "string"}, + "allowed_tool_names": {"type": "array", "items": {"type": "string"}}, + "required_evidence": {"type": "array", "items": {"type": "string"}}, + "evidence_contract": {"type": "object"}, + "validation_rules": {"type": "array", "items": {"type": "string"}}, + "required_for_completion": {"type": "boolean"}, + "block_downstream_on_partial": {"type": "boolean"}, + "max_tool_iterations": {"type": "integer"}, + "constraints": {"type": "array", "items": {"type": "string"}}, + "expected_output": {"type": "string"}, + "input_contract": {"type": "object"}, + "output_contract": {"type": "object"}, + }, + "required": ["name", "instruction"], + "additionalProperties": False, + } diff --git a/app-instance/backend/beaver/team_workflows/mixture_of_agents.py b/app-instance/backend/beaver/team_workflows/mixture_of_agents.py new file mode 100644 index 0000000..24764dc --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/mixture_of_agents.py @@ -0,0 +1,37 @@ +"""MixtureOfAgents graph builder.""" + +from __future__ import annotations + +from typing import Any, Iterable + +from beaver.coordinator.models import ExecutionGraph + +from .base import ( + WorkflowAgentSpec, + build_graph_from_dependencies, + parse_agents, + validate_agent_names, +) + +WORKFLOW_NAME = "MixtureOfAgents" + + +def build_graph( + *, + task: str, + agents: Iterable[WorkflowAgentSpec | dict[str, Any]], + aggregator: WorkflowAgentSpec | dict[str, Any], +) -> ExecutionGraph: + del task + experts = parse_agents(agents) + parsed_aggregator = parse_agents([aggregator])[0] + all_agents = [*experts, parsed_aggregator] + validate_agent_names(all_agents) + dependencies = {agent.name: [] for agent in all_agents} + dependencies[parsed_aggregator.name] = [agent.name for agent in experts] + return build_graph_from_dependencies( + workflow_name=WORKFLOW_NAME, + strategy="dag", + agents=all_agents, + dependencies=dependencies, + ) diff --git a/app-instance/backend/beaver/team_workflows/sequential.py b/app-instance/backend/beaver/team_workflows/sequential.py new file mode 100644 index 0000000..98b1497 --- /dev/null +++ b/app-instance/backend/beaver/team_workflows/sequential.py @@ -0,0 +1,29 @@ +"""SequentialWorkflow graph builder.""" + +from __future__ import annotations + +from typing import Any, Iterable + +from beaver.coordinator.models import ExecutionGraph + +from .base import WorkflowAgentSpec, build_graph_from_dependencies, parse_agents + +WORKFLOW_NAME = "SequentialWorkflow" + + +def build_graph( + *, + task: str, + agents: Iterable[WorkflowAgentSpec | dict[str, Any]], +) -> ExecutionGraph: + del task + parsed = parse_agents(agents) + dependencies = {agent.name: [] for agent in parsed} + for previous, current in zip(parsed, parsed[1:], strict=False): + dependencies[current.name].append(previous.name) + return build_graph_from_dependencies( + workflow_name=WORKFLOW_NAME, + strategy="sequence", + agents=parsed, + dependencies=dependencies, + ) diff --git a/app-instance/backend/beaver/tools/mcp/wrapper.py b/app-instance/backend/beaver/tools/mcp/wrapper.py index 59c2739..dd3f71f 100644 --- a/app-instance/backend/beaver/tools/mcp/wrapper.py +++ b/app-instance/backend/beaver/tools/mcp/wrapper.py @@ -68,6 +68,15 @@ class MCPToolWrapper(BaseTool): ) async def invoke(self, arguments: dict[str, Any], context: ToolContext) -> ToolResult: + if self.category == "team_workflow": + from beaver.team_workflows.executor import TeamWorkflowExecutor + + return await TeamWorkflowExecutor().execute( + self.original_name, + dict(arguments or {}), + context, + tool_name=self.spec.name, + ) try: result = await asyncio.wait_for( self.call_tool(self.original_name, dict(arguments or {})), diff --git a/app-instance/backend/tests/unit/test_agent_loop.py b/app-instance/backend/tests/unit/test_agent_loop.py index 6c8d8d4..b9574b0 100644 --- a/app-instance/backend/tests/unit/test_agent_loop.py +++ b/app-instance/backend/tests/unit/test_agent_loop.py @@ -1,10 +1,13 @@ import asyncio import json from contextlib import suppress +from types import SimpleNamespace from typing import Any from beaver.engine import AgentLoop, AgentRunResult, EngineLoader from beaver.engine import loop as loop_module +from beaver.engine.providers.base import LLMProvider, LLMResponse +from beaver.engine.providers.factory import ProviderBundle def _run_result(run_id: str, output_text: str) -> AgentRunResult: @@ -49,7 +52,7 @@ def test_running_loop_handles_reentrant_submit_direct(tmp_path) -> None: asyncio.run(run_case()) -def test_web_search_loop_guard_stops_after_repeated_low_quality_results() -> None: +def test_web_search_loop_guard_keeps_successful_low_quality_results_available() -> None: guard = loop_module._WebSearchLoopGuard() low_quality = json.dumps( { @@ -63,21 +66,106 @@ def test_web_search_loop_guard_stops_after_repeated_low_quality_results() -> Non assert guard.observe_result("web_search", low_quality) is None assert guard.observe_result("web_search", low_quality) is None - guidance = guard.observe_result("web_search", low_quality) - - assert guidance is not None - assert guidance["finish_reason"] == "web_search_low_quality_budget" - assert "weather beijing" in guidance["message"] + assert guard.observe_result("web_search", low_quality) is None def test_web_search_loop_guard_resets_after_useful_result() -> None: guard = loop_module._WebSearchLoopGuard() - low_quality = json.dumps({"success": True, "query": "weather", "quality": "low", "results": []}) + failed_search = json.dumps({"success": False, "query": "weather", "error": "timeout"}) useful = json.dumps({"success": True, "query": "weather", "quality": "high", "results": []}) - assert guard.observe_result("web_search", low_quality) is None + assert guard.observe_result("web_search", failed_search) is None assert guard.observe_result("web_search", useful) is None - assert guard.observe_result("web_search", low_quality) is None - assert guard.observe_result("web_search", low_quality) is None + assert guard.observe_result("web_search", failed_search) is None + assert guard.observe_result("web_search", failed_search) is None - assert guard.observe_result("web_search", low_quality) is not None + assert guard.observe_result("web_search", failed_search) is not None + + +class RecordingProvider(LLMProvider): + def __init__(self) -> None: + super().__init__() + self.tool_names_by_call: list[list[str]] = [] + + async def chat( + self, + messages: list[dict], + tools: list[dict] | None = None, + model: str | None = None, + max_tokens: int | None = None, + temperature: float = 0.7, + thinking_enabled: bool | None = None, + ) -> LLMResponse: + self.tool_names_by_call.append( + [ + str(tool.get("function", {}).get("name") or tool.get("name")) + for tool in tools or [] + ] + ) + return LLMResponse("done", provider_name="stub", model="stub-model") + + def get_default_model(self) -> str: + return "stub-model" + + +def _bundle(provider: RecordingProvider) -> ProviderBundle: + return ProviderBundle( + main_runtime=SimpleNamespace(model="stub-model", provider_name="stub"), + main_provider=provider, + ) + + +def test_task_mode_root_does_not_expose_agent_team_tool(tmp_path) -> None: + provider = RecordingProvider() + loop = AgentLoop(loader=EngineLoader(workspace=tmp_path)) + + asyncio.run( + loop.process_direct( + "compare financial reports", + session_id="session", + task_id="task-1", + task_mode=True, + include_skill_assembly=False, + provider_bundle=_bundle(provider), + ) + ) + + assert "run_agent_team" not in provider.tool_names_by_call[0] + + +def test_default_engine_registry_does_not_register_agent_team_tool(tmp_path) -> None: + loaded = AgentLoop(loader=EngineLoader(workspace=tmp_path)).boot() + + assert loaded.tool_registry is not None + assert loaded.tool_registry.get("run_agent_team") is None + assert "run_agent_team" not in loaded.tools + + +def test_non_task_and_team_node_do_not_expose_agent_team_tool(tmp_path) -> None: + non_task_provider = RecordingProvider() + loop = AgentLoop(loader=EngineLoader(workspace=tmp_path)) + asyncio.run( + loop.process_direct( + "ordinary chat", + session_id="session", + include_skill_assembly=False, + provider_bundle=_bundle(non_task_provider), + ) + ) + + team_node_provider = RecordingProvider() + asyncio.run( + loop.process_direct( + "team child", + session_id="session:team:child", + parent_session_id="session", + source="team:child", + task_id="task-1", + task_mode=True, + include_skill_assembly=False, + provider_bundle=_bundle(team_node_provider), + ) + ) + + assert "run_agent_team" not in non_task_provider.tool_names_by_call[0] + assert "run_agent_team" not in team_node_provider.tool_names_by_call[0] diff --git a/app-instance/backend/tests/unit/test_agent_team_v1.py b/app-instance/backend/tests/unit/test_agent_team_v1.py index 2503a4e..999938e 100644 --- a/app-instance/backend/tests/unit/test_agent_team_v1.py +++ b/app-instance/backend/tests/unit/test_agent_team_v1.py @@ -15,6 +15,7 @@ from beaver.engine import AgentLoop, EngineLoader from beaver.engine.context import SkillContext from beaver.engine.providers.base import LLMProvider, LLMResponse from beaver.engine.providers.factory import ProviderBundle +from beaver.engine.session.manager import SessionManager from beaver.services.team_service import TeamService from beaver.skills.assembler import SkillAssemblyResult from beaver.skills.drafts import DraftService @@ -232,9 +233,9 @@ def test_unknown_evidence_requirement_makes_node_partial(tmp_path: Path) -> None result = asyncio.run(LocalAgentRunner(loop).run(envelope, provider_bundle=_bundle(provider))) - assert result.success is False - assert result.completion_status == "partial" - assert result.evidence_gaps == ["unsupported evidence requirement: unknown_type"] + assert result.success is True + assert result.completion_status == "succeeded" + assert result.evidence_gaps == [] def test_team_node_preserves_evidence_when_finish_reason_is_not_stop(tmp_path: Path) -> None: @@ -257,6 +258,90 @@ def test_team_node_preserves_evidence_when_finish_reason_is_not_stop(tmp_path: P assert result.evidence.finish_reason == "max_tool_iterations" +def test_team_node_accepts_finalized_tool_budget_output(tmp_path: Path) -> None: + loop = _loop(tmp_path) + provider = RecordingProvider([_response("usable finalized output", finish_reason="max_tool_iterations_finalized")]) + envelope = DelegationEnvelope( + parent_task_id="task-parent", + parent_session_id="session-root", + parent_run_id="run-root", + agent=AgentDescriptor(name="researcher", role="research"), + task="research the requested topic", + node_id="research", + ) + + result = asyncio.run(LocalAgentRunner(loop).run(envelope, provider_bundle=_bundle(provider))) + + assert result.success is True + assert result.completion_status == "succeeded" + assert result.finish_reason == "max_tool_iterations_finalized" + + +def test_team_node_rejects_finalized_raw_tool_call_output(tmp_path: Path) -> None: + loop = _loop(tmp_path) + provider = RecordingProvider( + [ + _response( + '<||DSML||tool_calls><||DSML||invoke name="web_fetch">', + finish_reason="max_tool_iterations_finalized", + ) + ] + ) + envelope = DelegationEnvelope( + parent_task_id="task-parent", + parent_session_id="session-root", + parent_run_id="run-root", + agent=AgentDescriptor(name="researcher", role="research"), + task="research the requested topic", + node_id="research", + ) + + result = asyncio.run(LocalAgentRunner(loop).run(envelope, provider_bundle=_bundle(provider))) + + assert result.success is False + assert result.completion_status == "failed" + assert result.error == "finalized output is a raw tool call" + + +def test_team_node_defaults_to_larger_tool_iteration_budget(tmp_path: Path) -> None: + session_manager = SessionManager(tmp_path) + captured_kwargs: dict[str, object] = {} + + class CapturingLoop: + profile = SimpleNamespace() + loader = None + is_running = False + + async def process_direct(self, task: str, **kwargs: object) -> SimpleNamespace: + captured_kwargs.update(kwargs) + session_id = str(kwargs["session_id"]) + run_id = "run-captured" + session_manager.ensure_session(session_id, source="test") + return SimpleNamespace( + session_id=session_id, + run_id=run_id, + output_text="done", + finish_reason="stop", + ) + + def boot(self) -> SimpleNamespace: + return SimpleNamespace(session_manager=session_manager) + + envelope = DelegationEnvelope( + parent_task_id="task-parent", + parent_session_id="session-root", + parent_run_id="run-root", + agent=AgentDescriptor(name="researcher", role="research"), + task="research the requested topic", + node_id="research", + ) + + result = asyncio.run(LocalAgentRunner(CapturingLoop()).run(envelope)) + + assert result.success is True + assert captured_kwargs["max_tool_iterations"] == 100 + + def test_pinned_skill_is_injected_into_delegated_run(tmp_path: Path) -> None: _publish_skill( tmp_path, diff --git a/app-instance/backend/tests/unit/test_config_loader.py b/app-instance/backend/tests/unit/test_config_loader.py index 081899c..5254f2b 100644 --- a/app-instance/backend/tests/unit/test_config_loader.py +++ b/app-instance/backend/tests/unit/test_config_loader.py @@ -323,6 +323,14 @@ def test_agent_defaults_include_runtime_controls(tmp_path) -> None: service.close() +def test_agent_default_tool_iteration_budget_is_100(tmp_path) -> None: + service = AgentService(config_path=tmp_path / "config.json") + + assert service.profile.max_tool_iterations == 100 + + service.close() + + def test_agent_config_api_persists_and_reloads_defaults(tmp_path) -> None: config_path = tmp_path / "config.json" config_path.write_text(json.dumps({"agents": {"defaults": {}}}), encoding="utf-8") @@ -514,3 +522,16 @@ def test_load_config_adds_managed_local_mcp_servers(tmp_path) -> None: assert local.managed is True assert local.display_name == "个人智能体文件系统工具" assert "beaver.interfaces.mcp.tools_server" in local.args + + team_workflow = config.tools.mcp_servers["local_team_workflow_mcp"] + assert team_workflow.transport == "stdio" + assert team_workflow.kind == "local" + assert team_workflow.category == "team_workflow" + assert team_workflow.managed is True + assert team_workflow.display_name == "本地 Agent Team Workflow 工具" + assert team_workflow.args == [ + "-m", + "beaver.interfaces.mcp.tools_server", + "--category", + "team_workflow", + ] diff --git a/app-instance/backend/tests/unit/test_legacy_team_cleanup.py b/app-instance/backend/tests/unit/test_legacy_team_cleanup.py new file mode 100644 index 0000000..4582ba1 --- /dev/null +++ b/app-instance/backend/tests/unit/test_legacy_team_cleanup.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +import asyncio +from types import SimpleNamespace +from typing import Any + +from beaver.coordinator import AgentDescriptor, ExecutionGraph, ExecutionNode +from beaver.engine import AgentRunResult +from beaver.tasks import TaskExecutionPlan, TaskRecord +from beaver.tasks.attempt_orchestrator import TaskAttemptOrchestrator + + +class FakeTaskService: + def start_run(self, task_id: str, **_: Any) -> None: + return None + + def append_run(self, task_id: str, run_id: str, **_: Any) -> TaskRecord: + return self.task + + +class FakeSessionManager: + def __init__(self) -> None: + self.events: list[dict[str, Any]] = [] + + def append_message(self, session_id: str, **kwargs: Any) -> None: + self.events.append({"session_id": session_id, **kwargs}) + + def update_latest_assistant_event_payload(self, *args: Any, **kwargs: Any) -> None: + return None + + def get_run_event_records(self, session_id: str, run_id: str) -> list[Any]: + return [] + + +class LegacyTeamPlanner: + async def plan(self, **_: Any) -> TaskExecutionPlan: + return TaskExecutionPlan( + mode="team", + reason="legacy plan should be ignored by orchestrator", + graph=ExecutionGraph( + strategy="sequence", + nodes=[ + ExecutionNode("collect", "Collect", AgentDescriptor(name="collect")), + ], + ), + ) + + +def _task() -> TaskRecord: + return TaskRecord( + task_id="task-1", + session_id="session-1", + description="finance comparison", + goal="finance comparison", + constraints=[], + priority=0, + status="open", + creator="test", + created_at="now", + updated_at="now", + ) + + +def test_builtin_tools_do_not_export_legacy_agent_team_tool() -> None: + import beaver.tools.builtins as builtins + + assert "AgentTeamTool" not in builtins.__all__ + assert not hasattr(builtins, "AgentTeamTool") + + +def test_task_orchestrator_does_not_execute_legacy_planner_team_graph() -> None: + task = _task() + task_service = FakeTaskService() + task_service.task = task + session_manager = FakeSessionManager() + loaded = SimpleNamespace( + task_service=task_service, + task_execution_planner=LegacyTeamPlanner(), + session_manager=session_manager, + run_memory_store=None, + ) + orchestrator = TaskAttemptOrchestrator( + loaded=loaded, + create_loop=lambda: None, + make_provider_bundle_for_task=lambda *_: None, + ) + + async def fail_if_called(*args: Any, **kwargs: Any) -> Any: + raise AssertionError("legacy planner team graph must not start TeamService") + + async def runner(message: str, **kwargs: Any) -> AgentRunResult: + return AgentRunResult( + session_id="session-1", + run_id="main-run", + output_text="single path", + finish_reason="stop", + tool_iterations=0, + ) + + orchestrator._run_team_for_task = fail_if_called # type: ignore[method-assign] + result = asyncio.run( + orchestrator.run( + message="compare finance", + runner=runner, + kwargs={ + "session_id": "session-1", + "provider_bundle": SimpleNamespace(), + "include_skill_assembly": False, + }, + task=task, + ) + ) + + assert result.output_text == "single path" + synthesis_events = [ + event + for event in session_manager.events + if event.get("event_type") == "task_synthesis_completed" + ] + assert synthesis_events[0]["event_payload"]["task_outcome"] == "single" diff --git a/app-instance/backend/tests/unit/test_mcp_tools_server.py b/app-instance/backend/tests/unit/test_mcp_tools_server.py index 52a0b24..effcfbc 100644 --- a/app-instance/backend/tests/unit/test_mcp_tools_server.py +++ b/app-instance/backend/tests/unit/test_mcp_tools_server.py @@ -20,3 +20,30 @@ def test_local_filesystem_mcp_exposes_personal_user_file_tools_only(tmp_path) -> assert "search_files" not in names assert "list_directory" not in names assert all("personal agent file system" in tool.spec.description for tool in tools) + + +def test_team_workflow_mcp_exposes_workflow_tool_schemas(tmp_path) -> None: + tools, _context = _category_tools("team_workflow", tmp_path) + + specs = {tool.spec.name: tool.spec for tool in tools} + + assert list(specs) == [ + "SequentialWorkflow", + "ConcurrentWorkflow", + "MixtureOfAgents", + "AgentRearrange", + "GraphWorkflow", + ] + assert specs["SequentialWorkflow"].input_schema["required"] == ["task", "agents"] + assert specs["SequentialWorkflow"].input_schema["properties"]["agents"]["items"]["required"] == [ + "name", + "instruction", + ] + assert specs["GraphWorkflow"].input_schema["required"] == [ + "task", + "agents", + "edges", + "output_agent", + ] + assert specs["GraphWorkflow"].input_schema["properties"]["edges"]["items"]["minItems"] == 2 + assert specs["AgentRearrange"].input_schema["required"] == ["task", "agents", "flow"] diff --git a/app-instance/backend/tests/unit/test_process_projection.py b/app-instance/backend/tests/unit/test_process_projection.py index f3f409b..063fc0a 100644 --- a/app-instance/backend/tests/unit/test_process_projection.py +++ b/app-instance/backend/tests/unit/test_process_projection.py @@ -205,6 +205,7 @@ def test_process_projection_maps_failed_task_team_events(tmp_path: Path) -> None team_event = next(event for event in projection["events"] if event["kind"] == "agent_team_created") assert team_event["status"] == "error" + assert team_event["text"] == "Team 执行未完成 / 子节点失败" assert team_event["metadata"]["timeline_type"] == "agent_team" assert team_event["metadata"]["team_run_ids"] == ["failed-sub-run"] @@ -297,6 +298,101 @@ def test_process_projection_emits_skill_card_from_main_run_receipts(tmp_path: Pa assert skill_events assert skill_events[0]["metadata"]["timeline_type"] == "skill" assert skill_events[0]["metadata"]["skill_names"] == ["web-operation"] + assert skill_events[0]["created_at"] == "2026-01-01T00:00:03+00:00" + + +def test_process_projection_prefers_skill_activation_snapshot_over_synthesis_fallback(tmp_path: Path) -> None: + session = SessionManager(tmp_path) + run_store = RunMemoryStore(tmp_path / "memory" / "runs") + run_store.append_run_record( + RunRecord( + run_id="main-run", + session_id="web:test", + task_id="task-1", + attempt_index=1, + task_text="main task", + started_at="2026-01-01T00:00:03+00:00", + ended_at="2026-01-01T00:00:04+00:00", + success=True, + finish_reason="stop", + activated_skills=[ + SkillActivationReceipt( + run_id="main-run", + session_id="web:test", + skill_name="web-operation", + skill_version="1", + content_hash="hash", + activated_at="2026-01-01T00:00:03+00:00", + activation_reason="Needs live web lookup.", + ) + ], + ) + ) + session.append_message( + "web:test", + role="system", + event_type="task_execution_planned", + event_payload={ + "task_id": "task-1", + "attempt_index": 1, + "plan_mode": "single", + "strategy": "single", + "selected_skill_names": [], + }, + context_visible=False, + ) + session.append_message( + "web:test", + run_id="main-run", + role="system", + event_type="skill_activation_snapshotted", + event_payload={ + "task_id": "task-1", + "attempt_index": 1, + "receipts": [ + { + "run_id": "main-run", + "session_id": "web:test", + "skill_name": "web-operation", + "skill_version": "1", + "content_hash": "hash", + "activated_at": "2026-01-01T00:00:03+00:00", + "activation_reason": "Needs live web lookup.", + } + ], + }, + context_visible=False, + ) + session.append_message( + "web:test", + run_id="main-run", + role="assistant", + event_type="assistant_message_added", + event_payload={"task_id": "task-1", "attempt_index": 1}, + content="Searching", + tool_calls=[{"id": "call-1", "name": "web_fetch", "arguments": {"url": "https://example.com"}}], + context_visible=False, + ) + session.append_message( + "web:test", + role="system", + event_type="task_synthesis_completed", + event_payload={"task_id": "task-1", "attempt_index": 1, "main_run_id": "main-run"}, + context_visible=False, + ) + + projection = SessionProcessProjector(session, run_store).project("web:test") + + skill_events = [ + event + for event in projection["events"] + if event["kind"] == "skill_selected" and event["run_id"] == "main-run" + ] + assert len(skill_events) == 1 + assert skill_events[0]["event_id"].endswith(":skill-activation") + assert skill_events[0]["created_at"] == "2026-01-01T00:00:03+00:00" + tool_event = next(event for event in projection["events"] if event["kind"] == "tool_call_started") + assert projection["events"].index(skill_events[0]) < projection["events"].index(tool_event) def test_process_projection_emits_tool_cards_from_run_messages(tmp_path: Path) -> None: diff --git a/app-instance/backend/tests/unit/test_skill_team_template.py b/app-instance/backend/tests/unit/test_skill_team_template.py index f0ca37d..1d586a1 100644 --- a/app-instance/backend/tests/unit/test_skill_team_template.py +++ b/app-instance/backend/tests/unit/test_skill_team_template.py @@ -2,38 +2,9 @@ from __future__ import annotations from beaver.skills.assembler.task_assembler import SkillAssembler from beaver.skills.catalog.loader import SkillsLoader -from beaver.skills.catalog.utils import extract_skill_team_template -def test_extract_team_template_returns_none_when_block_is_absent() -> None: - result = extract_skill_team_template("# Ordinary Skill") - - assert result.template is None - assert result.warnings == [] - - -def test_extract_team_template_parses_valid_json_block() -> None: - result = extract_skill_team_template( - "```beaver-team-template\n" - '{"version": 1, "nodes": [{"node_id": "collect", "task": "Collect"}]}\n' - "```" - ) - - assert result.template == { - "version": 1, - "nodes": [{"node_id": "collect", "task": "Collect"}], - } - assert result.warnings == [] - - -def test_invalid_template_is_warning_not_skill_load_failure() -> None: - result = extract_skill_team_template("```beaver-team-template\nnot-json\n```") - - assert result.template is None - assert result.warnings == ["team template JSON is invalid"] - - -def test_loader_and_assembler_propagate_team_template_to_skill_context(tmp_path) -> None: +def test_beaver_team_template_block_is_not_runtime_metadata(tmp_path) -> None: skill_dir = tmp_path / "plugin-skills" / "financial-comparison" skill_dir.mkdir(parents=True) (skill_dir / "SKILL.md").write_text( @@ -56,10 +27,7 @@ def test_loader_and_assembler_propagate_team_template_to_skill_context(tmp_path) context = SkillAssembler(loader)._activate_skill_contexts(["financial-comparison"])[0] assert record is not None - assert record.team_template == { - "version": 1, - "nodes": [{"node_id": "collect", "task": "Collect official sources"}], - } - assert record.team_template_warnings == [] - assert context.team_template == record.team_template - assert context.team_template_warnings == [] + assert not hasattr(record, "team_template") + assert not hasattr(record, "team_template_warnings") + assert not hasattr(context, "team_template") + assert not hasattr(context, "team_template_warnings") diff --git a/app-instance/backend/tests/unit/test_task_evidence.py b/app-instance/backend/tests/unit/test_task_evidence.py index 5549fcb..2584d81 100644 --- a/app-instance/backend/tests/unit/test_task_evidence.py +++ b/app-instance/backend/tests/unit/test_task_evidence.py @@ -55,12 +55,11 @@ def test_evaluate_node_evidence_accepts_url_in_successful_tool_content() -> None assert evaluate_node_evidence(evidence, ["tool_result", "url"], "done") == [] -def test_evaluate_node_evidence_checks_output_and_unknown_requirements() -> None: +def test_evaluate_node_evidence_checks_output_and_ignores_natural_language_requirements() -> None: evidence = _run_evidence() - assert evaluate_node_evidence(evidence, ["output", "unknown_type"], " ") == [ + assert evaluate_node_evidence(evidence, ["output", "至少3个价格信息来源"], " ") == [ "missing required evidence: output", - "unsupported evidence requirement: unknown_type", ] diff --git a/app-instance/backend/tests/unit/test_task_execution_planner.py b/app-instance/backend/tests/unit/test_task_execution_planner.py index be5ea78..75d06c3 100644 --- a/app-instance/backend/tests/unit/test_task_execution_planner.py +++ b/app-instance/backend/tests/unit/test_task_execution_planner.py @@ -3,19 +3,15 @@ from __future__ import annotations import asyncio from types import SimpleNamespace -from beaver.engine.context import SkillContext from beaver.engine.providers.base import LLMProvider, LLMResponse from beaver.engine.providers.factory import ProviderBundle -from beaver.tasks import SkillResolutionReport, TaskExecutionPlanner, TaskRecord -from beaver.tools.base import BaseTool, ToolContext, ToolResult, ToolSpec -from beaver.tools.registry import ToolRegistry +from beaver.tasks import TaskExecutionPlanner, TaskRecord class PlannerProvider(LLMProvider): - def __init__(self, response: str) -> None: + def __init__(self) -> None: super().__init__() - self.response = response - self.calls: list[dict] = [] + self.calls = 0 async def chat( self, @@ -25,59 +21,18 @@ class PlannerProvider(LLMProvider): max_tokens: int = 4096, temperature: float = 0.7, ) -> LLMResponse: - self.calls.append( - { - "messages": messages, - "max_tokens": max_tokens, - "temperature": temperature, - "model": model, - "tools": tools, - } + self.calls += 1 + return LLMResponse( + content='{"mode":"team"}', + finish_reason="stop", + provider_name="stub", + model="stub-model", ) - return LLMResponse(content=self.response, finish_reason="stop", provider_name="stub", model="stub-model") def get_default_model(self) -> str: return "stub-model" -class HangingPlannerProvider(LLMProvider): - async def chat( - self, - messages: list[dict], - tools: list[dict] | None = None, - model: str | None = None, - max_tokens: int = 4096, - temperature: float = 0.7, - ) -> LLMResponse: - await asyncio.sleep(10) - return LLMResponse(content='{"mode":"team"}', finish_reason="stop", provider_name="stub", model="stub-model") - - def get_default_model(self) -> str: - return "stub-model" - - -class SequencedPlannerProvider(PlannerProvider): - def __init__(self, responses: list[str]) -> None: - super().__init__(responses[0]) - self.responses = list(responses) - - async def chat(self, *args, **kwargs) -> LLMResponse: - self.response = self.responses.pop(0) - return await super().chat(*args, **kwargs) - - -class StubTool(BaseTool): - def __init__(self, name: str) -> None: - self._spec = ToolSpec(name=name, description=name, input_schema={"type": "object"}) - - @property - def spec(self) -> ToolSpec: - return self._spec - - async def invoke(self, arguments: dict, context: ToolContext) -> ToolResult: - raise AssertionError("Planner tests do not execute tools") - - def _task() -> TaskRecord: return TaskRecord( task_id="task-1", @@ -93,55 +48,15 @@ def _task() -> TaskRecord: ) -def _bundle(response: str) -> ProviderBundle: - provider = PlannerProvider(response) +def _bundle(provider: PlannerProvider) -> ProviderBundle: return ProviderBundle( main_runtime=SimpleNamespace(model="stub-model", provider_name="stub"), main_provider=provider, ) -def _bundle_with_provider(provider: LLMProvider) -> ProviderBundle: - return ProviderBundle( - main_runtime=SimpleNamespace(model="stub-model", provider_name="stub"), - main_provider=provider, - ) - - -def _registry() -> ToolRegistry: - registry = ToolRegistry() - registry.register_many([StubTool("web_search"), StubTool("web_fetch"), StubTool("terminal")]) - return registry - - -def _hanging_bundle() -> ProviderBundle: - return ProviderBundle( - main_runtime=SimpleNamespace(model="stub-model", provider_name="stub"), - main_provider=HangingPlannerProvider(), - ) - - -def test_planner_selects_single_mode() -> None: - plan = asyncio.run( - TaskExecutionPlanner().plan( - task=_task(), - user_message="implement workflow", - attempt_index=1, - provider_bundle=_bundle('{"mode":"single","reason":"main agent is enough"}'), - ) - ) - - assert plan.mode == "single" - assert plan.graph is None - assert plan.reason == "main agent is enough" - - -def test_planner_skips_llm_for_simple_task() -> None: - provider = PlannerProvider('{"mode":"team","reason":"should not be used"}') - bundle = ProviderBundle( - main_runtime=SimpleNamespace(model="stub-model", provider_name="stub"), - main_provider=provider, - ) +def test_planner_skips_provider_for_simple_task() -> None: + provider = PlannerProvider() task = _task() task.description = "查询深圳天气" task.goal = "查询深圳天气" @@ -151,409 +66,55 @@ def test_planner_skips_llm_for_simple_task() -> None: task=task, user_message="帮我查一下今天深圳天气", attempt_index=1, - provider_bundle=bundle, + provider_bundle=_bundle(provider), ) ) assert plan.mode == "single" assert plan.graph is None assert plan.reason == "planner_skipped_simple_task" - assert provider.calls == [] + assert provider.calls == 0 -def test_planner_builds_team_graph() -> None: - bundle = _bundle( - """ - { - "mode": "team", - "reason": "needs parallel review", - "strategy": "dag", - "nodes": [ - {"node_id": "research", "task": "research options"}, - {"node_id": "review", "task": "review result", "depends_on": ["research"]} - ], - "final_synthesis_instruction": "merge the findings" - } - """ - ) - provider = bundle.main_provider +def test_planner_replaces_team_planning_with_workflow_tools_without_provider_call() -> None: + provider = PlannerProvider() + plan = asyncio.run( TaskExecutionPlanner().plan( task=_task(), - user_message="implement workflow", + user_message="research and compare workflow options", attempt_index=1, - provider_bundle=bundle, + provider_bundle=_bundle(provider), skill_summaries=["docker-debug: Use docker logs before editing config."], tool_hints=["terminal", "search_files"], ) ) - assert plan.is_team - assert plan.graph is not None - assert plan.graph.strategy == "dag" - assert [node.node_id for node in plan.graph.nodes] == ["research", "review"] - assert plan.graph.nodes[1].depends_on == ["research"] - assert plan.final_synthesis_instruction == "merge the findings" - assert isinstance(provider, PlannerProvider) - prompt = provider.calls[0]["messages"][1]["content"] - assert "Activated skill summaries" in prompt - assert "docker-debug: Use docker logs before editing config." in prompt - assert "terminal" in prompt - assert "search_files" in prompt + assert not plan.is_team + assert plan.mode == "single" + assert plan.graph is None + assert plan.reason == "planner_team_replaced_by_workflow_tools" + assert plan.final_synthesis_instruction == "" + assert provider.calls == 0 -def test_planner_timeout_falls_back_to_single() -> None: +def test_planner_can_be_disabled_by_environment(monkeypatch) -> None: + monkeypatch.setenv("BEAVER_AGENT_TEAM_ENABLED", "0") + provider = PlannerProvider() + plan = asyncio.run( TaskExecutionPlanner().plan( task=_task(), - user_message="implement workflow", + user_message="research and compare workflow options", attempt_index=1, - provider_bundle=_hanging_bundle(), - timeout_seconds=0.01, + provider_bundle=_bundle(provider), ) ) assert plan.mode == "single" - assert plan.reason == "planner_failed" - assert "TimeoutError" in (plan.fallback_error or "") + assert plan.reason == "planner_disabled_by_environment" + assert provider.calls == 0 -def test_planner_team_nodes_use_task_as_internal_skill_query() -> None: - plan = TaskExecutionPlanner().from_json( - """ - { - "mode": "team", - "reason": "needs skill-guided review", - "strategy": "sequence", - "nodes": [ - { - "node_id": "api_review", - "task": "review API compatibility" - } - ] - } - """ - ) - - assert plan.is_team - assert plan.graph is not None - node = plan.graph.nodes[0] - assert node.agent.name == "api_review" - assert node.agent.role == "" - assert node.agent.metadata["skill_query"] == "review API compatibility" - assert node.agent.metadata["required_capabilities"] == [] - - -def test_planner_accepts_use_skill_and_skill_query() -> None: - plan = TaskExecutionPlanner().from_json( - """ - { - "mode": "team", - "strategy": "sequence", - "nodes": [ - { - "node_id": "collect", - "task": "Collect official sources", - "use_skill": "official-source-research", - "skill_query": "official source verification" - } - ] - } - """ - ) - - assert plan.is_team - assert plan.graph is not None - node = plan.graph.nodes[0] - assert node.agent.metadata["use_skill"] == "official-source-research" - assert node.agent.metadata["skill_query"] == "official source verification" - assert node.inherited_pinned_skills == [] - assert node.allowed_tool_names is None - assert plan.planner_adaptation["node_skill_bindings"] == [ - { - "node_id": "collect", - "use_skill": "official-source-research", - "skill_query": "official source verification", - } - ] - - -def test_planner_defaults_skill_query_to_node_task_when_absent() -> None: - plan = TaskExecutionPlanner().from_json( - '{"mode":"team","strategy":"sequence","nodes":[' - '{"node_id":"extract","task":"Extract financial metrics","use_skill":"financial-extraction"}]}' - ) - - assert plan.is_team - assert plan.graph is not None - assert plan.graph.nodes[0].agent.metadata["skill_query"] == "Extract financial metrics" - - -def test_planner_adaptation_records_unresolved_use_skill_fallback() -> None: - planner = TaskExecutionPlanner() - plan = planner.from_json( - '{"mode":"team","strategy":"sequence","nodes":[' - '{"node_id":"extract","task":"Extract metrics","use_skill":"missing-skill",' - '"skill_query":"financial extraction"}]}' - ) - report = SkillResolutionReport( - node_id="extract", - skill_query="financial extraction", - requested_skill_name="missing-skill", - exact_binding_used=False, - warnings=["use_skill unresolved: missing-skill"], - reason="matched published skill", - ) - - planner._merge_skill_resolution_adaptation(plan, [report]) - - assert plan.planner_adaptation["warnings"] == ["use_skill unresolved: missing-skill"] - assert plan.planner_adaptation["node_skill_bindings"][0]["fallback_reason"] == ( - "use_skill unresolved; matched published skill" - ) - - -def test_planner_invalid_outputs_fallback_to_single() -> None: - planner = TaskExecutionPlanner() - invalid_json = planner.from_json("not json") - unknown_strategy = planner.from_json( - '{"mode":"team","strategy":"moa","nodes":[{"node_id":"a","task":"a","agent":{"name":"a"}}]}' - ) - too_many_nodes = planner.from_json( - '{"mode":"team","strategy":"parallel","nodes":[' - + ",".join( - '{"node_id":"n%s","task":"work","agent":{"name":"n%s"}}' % (index, index) - for index in range(7) - ) - + "]}" - ) - cyclic = planner.from_json( - """ - { - "mode": "team", - "strategy": "dag", - "nodes": [ - {"node_id": "a", "task": "a", "agent": {"name": "a"}, "depends_on": ["b"]}, - {"node_id": "b", "task": "b", "agent": {"name": "b"}, "depends_on": ["a"]} - ] - } - """ - ) - - assert invalid_json.mode == "single" - assert unknown_strategy.mode == "single" - assert too_many_nodes.mode == "single" - assert cyclic.mode == "single" - - -def test_template_plan_builds_generic_worker_and_preserves_v1_contract_fields() -> None: - plan = TaskExecutionPlanner(tool_registry=_registry()).from_json( - """ - { - "mode": "team", - "strategy": "dag", - "nodes": [ - { - "node_id": "collect", - "task": "Collect official sources", - "requested_tools": ["web_search"], - "evidence_contract": {"entities": ["MGM", "Galaxy"]}, - "block_downstream_on_partial": true - } - ], - "adaptation": {"template_used": true} - } - """ - ) - - assert plan.is_team - assert plan.graph is not None - node = plan.graph.nodes[0] - assert node.agent.name == "collect" - assert node.agent.role == "" - assert node.agent.metadata["sub_agent_kind"] == "generic_skill_worker" - assert node.allowed_tool_names == ["web_search"] - assert node.evidence_contract == {"entities": ["MGM", "Galaxy"]} - assert node.block_downstream_on_partial is True - assert plan.planner_adaptation["template_used"] is True - - -def test_unknown_tool_is_removed_and_warned() -> None: - plan = TaskExecutionPlanner(tool_registry=_registry()).from_json( - '{"mode":"team","strategy":"sequence","nodes":[' - '{"node_id":"collect","task":"Collect","requested_tools":["web_search","not_real"]}]}' - ) - - assert plan.is_team - assert plan.graph is not None - assert plan.graph.nodes[0].allowed_tool_names == ["web_search"] - assert "unknown tool removed: not_real" in plan.planner_adaptation["warnings"] - - -def test_high_risk_tool_is_removed_without_failing_low_risk_plan() -> None: - plan = TaskExecutionPlanner(tool_registry=_registry()).from_json( - '{"mode":"team","strategy":"sequence","nodes":[' - '{"node_id":"collect","task":"Collect","requested_tools":["web_search","terminal"]}]}' - ) - - assert plan.is_team - assert plan.graph is not None - assert plan.graph.nodes[0].allowed_tool_names == ["web_search"] - assert "requires_high_risk_review: terminal" in plan.planner_adaptation["warnings"] - - -def test_planner_rejects_agent_and_role_node_fields() -> None: - planner = TaskExecutionPlanner(tool_registry=_registry()) - - agent_plan = planner.from_json( - '{"mode":"team","strategy":"sequence","nodes":[' - '{"node_id":"collect","task":"Collect","agent":{"name":"researcher"}}]}' - ) - role_plan = planner.from_json( - '{"mode":"team","strategy":"sequence","nodes":[' - '{"node_id":"collect","task":"Collect","role":"researcher"}]}' - ) - - assert agent_plan.mode == "single" - assert "agent" in (agent_plan.fallback_error or "") - assert role_plan.mode == "single" - assert "role" in (role_plan.fallback_error or "") - - -def test_planner_records_primary_template_selection_and_ignored_templates() -> None: - primary = SkillContext( - name="financial-comparison", - version="v1", - content="Compare official financial disclosures.", - team_template={"version": 1, "nodes": [{"node_id": "collect", "task": "Collect"}]}, - ) - secondary = SkillContext( - name="chart-reporting", - version="v2", - content="Render chart-ready Markdown.", - team_template={"version": 1, "nodes": [{"node_id": "report", "task": "Report"}]}, - ) - provider = PlannerProvider( - '{"mode":"team","strategy":"sequence","nodes":[' - '{"node_id":"collect","task":"Collect official sources"}],' - '"adaptation":{"template_used":true}}' - ) - - plan = asyncio.run( - TaskExecutionPlanner(tool_registry=_registry()).plan( - task=_task(), - user_message="compare financial workflow", - attempt_index=1, - provider_bundle=_bundle_with_provider(provider), - activated_skills=[primary, secondary], - ) - ) - - assert plan.planner_adaptation == { - "template_used": True, - "selected_template": "financial-comparison", - "selection_reason": "first activated skill with a valid team template", - "ignored_templates": ["chart-reporting"], - "warnings": [], - } - prompt = provider.calls[0]["messages"][1]["content"] - assert '"skill_name": "financial-comparison"' in prompt - assert "Compare official financial disclosures." in prompt - assert "Render chart-ready Markdown." in prompt - - -def test_malformed_planner_output_repairs_once_without_tools() -> None: - provider = SequencedPlannerProvider( - [ - "not json", - '{"mode":"team","strategy":"sequence","nodes":[{"node_id":"collect","task":"Collect"}]}', - ] - ) - - plan = asyncio.run( - TaskExecutionPlanner(tool_registry=_registry()).plan( - task=_task(), - user_message="implement workflow", - attempt_index=1, - provider_bundle=_bundle_with_provider(provider), - ) - ) - - assert plan.is_team - assert len(provider.calls) == 2 - assert provider.calls[1]["tools"] is None - assert "Repair the invalid planner JSON" in provider.calls[1]["messages"][1]["content"] - - -def test_failed_planner_repair_falls_back_to_single() -> None: - provider = SequencedPlannerProvider(["not json", "still not json"]) - - plan = asyncio.run( - TaskExecutionPlanner(tool_registry=_registry()).plan( - task=_task(), - user_message="implement workflow", - attempt_index=1, - provider_bundle=_bundle_with_provider(provider), - ) - ) - - assert plan.mode == "single" - assert plan.reason == "planner_fallback_single" - assert len(provider.calls) == 2 - - -def test_finance_template_adapts_to_task_oriented_read_only_graph() -> None: - plan = TaskExecutionPlanner(tool_registry=_registry()).from_json( - """ - { - "mode": "team", - "strategy": "dag", - "nodes": [ - { - "node_id": "collect_official_sources", - "task": "Collect MGM and Galaxy official financial disclosures", - "requested_tools": ["web_search", "web_fetch"], - "required_evidence": ["tool_result", "url"] - }, - { - "node_id": "extract_financial_metrics", - "task": "Extract comparable financial metrics from collected sources", - "depends_on": ["collect_official_sources"], - "requested_tools": ["web_fetch"], - "required_evidence": ["output"] - }, - { - "node_id": "validate_metrics", - "task": "Validate metric units, periods, and source consistency", - "depends_on": ["extract_financial_metrics"], - "required_evidence": ["output"] - }, - { - "node_id": "generate_chart_report", - "task": "Generate a Markdown comparison table and chart-ready data without claiming an image or file artifact", - "depends_on": ["validate_metrics"], - "requested_tools": [], - "required_evidence": ["output"] - } - ] - } - """ - ) - - assert plan.is_team - assert plan.graph is not None - assert [node.node_id for node in plan.graph.nodes] == [ - "collect_official_sources", - "extract_financial_metrics", - "validate_metrics", - "generate_chart_report", - ] - assert all(node.agent.role == "" for node in plan.graph.nodes) - assert not {"researcher", "writer", "reviewer", "analyst"}.intersection( - node.node_id for node in plan.graph.nodes - ) - assert plan.graph.nodes[0].allowed_tool_names == ["web_search", "web_fetch"] - assert plan.graph.nodes[-1].allowed_tool_names == [] - report_task = plan.graph.nodes[-1].task.lower() - assert "markdown" in report_task - assert "without claiming an image or file artifact" in report_task +def test_planner_no_longer_exposes_json_to_team_graph_parser() -> None: + assert not hasattr(TaskExecutionPlanner(), "from_json") diff --git a/app-instance/backend/tests/unit/test_task_team_synthesis_outcome.py b/app-instance/backend/tests/unit/test_task_team_synthesis_outcome.py deleted file mode 100644 index b8c0450..0000000 --- a/app-instance/backend/tests/unit/test_task_team_synthesis_outcome.py +++ /dev/null @@ -1,233 +0,0 @@ -from __future__ import annotations - -import asyncio -from types import SimpleNamespace -from typing import Any - -import pytest - -from beaver.coordinator import AgentDescriptor, ExecutionGraph, ExecutionNode, NodeRunResult, TeamRunResult -from beaver.engine import AgentRunResult -from beaver.tasks import TaskExecutionPlan, TaskRecord -from beaver.tasks.attempt_orchestrator import TaskAttemptOrchestrator - - -def _plan(*, optional_second: bool = False) -> TaskExecutionPlan: - return TaskExecutionPlan( - mode="team", - reason="test team", - graph=ExecutionGraph( - strategy="sequence", - nodes=[ - ExecutionNode("collect", "Collect", AgentDescriptor(name="collect")), - ExecutionNode( - "report", - "Report", - AgentDescriptor(name="report"), - required_for_completion=not optional_second, - ), - ], - ), - ) - - -def _team_result(*results: NodeRunResult) -> TeamRunResult: - return TeamRunResult( - success=all(result.success for result in results), - summary="team summary", - node_results=list(results), - ) - - -def _result(node_id: str, status: str, *, gaps: list[str] | None = None) -> NodeRunResult: - return NodeRunResult( - node_id=node_id, - success=status == "succeeded", - output_text=f"{node_id} output", - finish_reason="blocked" if status == "blocked" else "stop", - error=None if status == "succeeded" else f"{status} node", - completion_status=status, - evidence_gaps=list(gaps or []), - ) - - -def test_required_partial_node_marks_synthesis_incomplete() -> None: - context, prefix, metadata = TaskAttemptOrchestrator._team_synthesis_outcome( - _plan(), - _team_result( - _result("collect", "partial", gaps=["missing required evidence: url"]), - _result("report", "succeeded"), - ), - ) - - assert metadata["task_outcome"] == "incomplete" - assert metadata["incomplete_node_ids"] == ["collect"] - assert metadata["evidence_gaps"] == {"collect": ["missing required evidence: url"]} - assert "Task outcome: incomplete" in context - assert "missing required evidence: url" in context - assert prefix.startswith("任务未完成:") - - -@pytest.mark.parametrize("status", ["failed", "blocked"]) -def test_required_failed_or_blocked_node_marks_synthesis_incomplete(status: str) -> None: - _, prefix, metadata = TaskAttemptOrchestrator._team_synthesis_outcome( - _plan(), - _team_result(_result("collect", status), _result("report", "succeeded")), - ) - - assert metadata["task_outcome"] == "incomplete" - assert metadata["incomplete_node_ids"] == ["collect"] - assert metadata["node_statuses"]["collect"] == status - assert prefix - - -def test_optional_failed_node_does_not_force_incomplete() -> None: - context, prefix, metadata = TaskAttemptOrchestrator._team_synthesis_outcome( - _plan(optional_second=True), - _team_result(_result("collect", "succeeded"), _result("report", "failed")), - ) - - assert metadata["task_outcome"] == "complete" - assert metadata["incomplete_node_ids"] == [] - assert "Task outcome: complete" in context - assert prefix == "" - - -def test_all_required_nodes_succeeded_is_complete() -> None: - _, prefix, metadata = TaskAttemptOrchestrator._team_synthesis_outcome( - _plan(), - _team_result(_result("collect", "succeeded"), _result("report", "succeeded")), - ) - - assert metadata["task_outcome"] == "complete" - assert prefix == "" - - -def test_single_plan_outcome_does_not_add_prefix() -> None: - context, prefix, metadata = TaskAttemptOrchestrator._team_synthesis_outcome( - TaskExecutionPlan.single("single"), - None, - ) - - assert metadata["task_outcome"] == "single" - assert "Task outcome: single" in context - assert prefix == "" - - -class FakeTaskService: - def start_run(self, task_id: str, **_: Any) -> None: - return None - - def append_run(self, task_id: str, run_id: str, **_: Any) -> TaskRecord: - return self.task - - -class FakeSessionManager: - def __init__(self) -> None: - self.events: list[dict[str, Any]] = [] - - def append_message(self, session_id: str, **kwargs: Any) -> None: - self.events.append({"session_id": session_id, **kwargs}) - - def update_latest_assistant_event_payload(self, *args: Any, **kwargs: Any) -> None: - return None - - def get_run_event_records(self, session_id: str, run_id: str) -> list[Any]: - return [] - - -class FixedPlanner: - def __init__(self, plan: TaskExecutionPlan) -> None: - self.fixed_plan = plan - - async def plan(self, **_: Any) -> TaskExecutionPlan: - return self.fixed_plan - - -def _task() -> TaskRecord: - return TaskRecord( - task_id="task-1", - session_id="session-1", - description="finance comparison", - goal="finance comparison", - constraints=[], - priority=0, - status="open", - creator="test", - created_at="now", - updated_at="now", - ) - - -def test_incomplete_team_still_runs_tool_free_synthesis_and_prefixes_output() -> None: - plan = _plan() - team_result = _team_result( - _result("collect", "partial", gaps=["missing required evidence: url"]), - _result("report", "succeeded"), - ) - task = _task() - task_service = FakeTaskService() - task_service.task = task - session_manager = FakeSessionManager() - loaded = SimpleNamespace( - task_service=task_service, - task_execution_planner=FixedPlanner(plan), - session_manager=session_manager, - run_memory_store=None, - ) - orchestrator = TaskAttemptOrchestrator( - loaded=loaded, - create_loop=lambda: None, - make_provider_bundle_for_task=lambda *_: None, - ) - - async def fake_run_team(*args: Any, **kwargs: Any) -> tuple[TeamRunResult, None]: - return team_result, None - - runner_calls: list[dict[str, Any]] = [] - - async def runner(message: str, **kwargs: Any) -> AgentRunResult: - runner_calls.append(kwargs) - return AgentRunResult( - session_id="session-1", - run_id="main-run", - output_text="Available financial comparison.", - finish_reason="stop", - tool_iterations=0, - ) - - orchestrator._run_team_for_task = fake_run_team # type: ignore[method-assign] - result = asyncio.run( - orchestrator.run( - message="compare finance", - runner=runner, - kwargs={ - "session_id": "session-1", - "provider_bundle": SimpleNamespace(), - "include_skill_assembly": False, - }, - task=task, - ) - ) - - assert len(runner_calls) == 1 - assert runner_calls[0]["include_tools"] is False - assert runner_calls[0]["max_tool_iterations"] == 0 - assert "Task outcome: incomplete" in runner_calls[0]["execution_context"] - assert result.output_text.startswith("任务未完成:") - synthesis_event = [event for event in session_manager.events if event.get("event_type") == "task_synthesis_completed"][0] - assert synthesis_event["event_payload"]["task_outcome"] == "incomplete" - assert synthesis_event["event_payload"]["incomplete_node_ids"] == ["collect"] - assert synthesis_event["event_payload"]["node_statuses"] == { - "collect": "partial", - "report": "succeeded", - } - assert synthesis_event["event_payload"]["evidence_gaps"] == { - "collect": ["missing required evidence: url"] - } - - -def test_incomplete_notice_is_not_prefixed_twice() -> None: - text = "任务未完成:缺少官方来源。" - - assert TaskAttemptOrchestrator._apply_incomplete_prefix(text, "任务未完成:部分步骤缺少证据。\n\n") == text diff --git a/app-instance/backend/tests/unit/test_team_workflow_graph.py b/app-instance/backend/tests/unit/test_team_workflow_graph.py new file mode 100644 index 0000000..d29b13c --- /dev/null +++ b/app-instance/backend/tests/unit/test_team_workflow_graph.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +import pytest + +from beaver.team_workflows.agent_rearrange import build_graph as build_rearrange_graph +from beaver.team_workflows.concurrent import build_graph as build_concurrent_graph +from beaver.team_workflows.graph import build_graph as build_explicit_graph +from beaver.team_workflows.mixture_of_agents import build_graph as build_moa_graph +from beaver.team_workflows.sequential import build_graph as build_sequential_graph + + +def _deps(graph) -> dict[str, list[str]]: + return {node.node_id: list(node.depends_on) for node in graph.nodes} + + +def test_sequential_workflow_builds_chain_and_preserves_agent_fields() -> None: + graph = build_sequential_graph( + task="finance report", + agents=[ + { + "name": "source_collector", + "instruction": "Collect official sources", + "skill_query": "official filings", + "allowed_tool_names": ["web_search", "web_fetch"], + "required_evidence": ["url"], + "validation_rules": ["Prefer official sources."], + "block_downstream_on_partial": True, + }, + {"name": "metric_extractor", "instruction": "Extract metrics"}, + {"name": "reporter", "instruction": "Write report"}, + ], + ) + + assert graph.strategy == "sequence" + assert [node.node_id for node in graph.nodes] == [ + "source_collector", + "metric_extractor", + "reporter", + ] + assert _deps(graph) == { + "source_collector": [], + "metric_extractor": ["source_collector"], + "reporter": ["metric_extractor"], + } + first = graph.nodes[0] + assert first.task == "Collect official sources" + assert first.agent.role == "" + assert first.agent.metadata["sub_agent_kind"] == "generic_skill_worker" + assert first.agent.metadata["workflow_tool"] == "SequentialWorkflow" + assert first.agent.metadata["workflow_agent_name"] == "source_collector" + assert first.agent.metadata["skill_query"] == "official filings" + assert first.allowed_tool_names == ["web_search", "web_fetch"] + assert first.required_evidence == ["url"] + assert first.validation_rules == ["Prefer official sources."] + assert first.block_downstream_on_partial is True + + +def test_concurrent_workflow_builds_independent_nodes() -> None: + graph = build_concurrent_graph( + task="research topic", + agents=[ + {"name": "official_sources", "instruction": "Check official sources"}, + {"name": "media_sources", "instruction": "Check media sources"}, + {"name": "data_sources", "instruction": "Check data sources"}, + ], + ) + + assert graph.strategy == "parallel" + assert _deps(graph) == { + "official_sources": [], + "media_sources": [], + "data_sources": [], + } + + +def test_mixture_of_agents_builds_experts_to_aggregator() -> None: + graph = build_moa_graph( + task="analyze match", + agents=[ + {"name": "tactics", "instruction": "Analyze tactics"}, + {"name": "players", "instruction": "Analyze players"}, + {"name": "media", "instruction": "Analyze media"}, + ], + aggregator={"name": "synthesizer", "instruction": "Synthesize report"}, + ) + + assert graph.strategy == "dag" + assert _deps(graph) == { + "tactics": [], + "players": [], + "media": [], + "synthesizer": ["tactics", "players", "media"], + } + assert graph.nodes[-1].agent.metadata["workflow_tool"] == "MixtureOfAgents" + + +def test_agent_rearrange_parses_flow_into_edges() -> None: + graph = build_rearrange_graph( + task="collect then analyze then synthesize", + agents=[ + {"name": "collector", "instruction": "Collect facts"}, + {"name": "tactics", "instruction": "Analyze tactics"}, + {"name": "players", "instruction": "Analyze players"}, + {"name": "media", "instruction": "Analyze media"}, + {"name": "synthesizer", "instruction": "Synthesize report"}, + ], + flow="collector -> tactics, players, media -> synthesizer", + ) + + assert graph.strategy == "dag" + assert _deps(graph) == { + "collector": [], + "tactics": ["collector"], + "players": ["collector"], + "media": ["collector"], + "synthesizer": ["tactics", "players", "media"], + } + + +def test_agent_rearrange_rejects_unknown_agent_in_flow() -> None: + with pytest.raises(ValueError, match="unknown agent"): + build_rearrange_graph( + task="bad flow", + agents=[{"name": "collector", "instruction": "Collect"}], + flow="collector -> missing", + ) + + +def test_graph_workflow_requires_edges_and_output_agent() -> None: + with pytest.raises(ValueError, match="edges"): + build_explicit_graph( + task="bad graph", + agents=[{"name": "collector", "instruction": "Collect"}], + edges=[], + output_agent="collector", + ) + + with pytest.raises(ValueError, match="output_agent"): + build_explicit_graph( + task="bad graph", + agents=[ + {"name": "collector", "instruction": "Collect"}, + {"name": "reporter", "instruction": "Report"}, + ], + edges=[["collector", "reporter"]], + output_agent="missing", + ) + + +def test_graph_workflow_builds_explicit_dag() -> None: + graph = build_explicit_graph( + task="match analysis", + agents=[ + {"name": "collector", "instruction": "Collect facts"}, + {"name": "tactics", "instruction": "Analyze tactics"}, + {"name": "players", "instruction": "Analyze players"}, + {"name": "media", "instruction": "Analyze media"}, + {"name": "synthesizer", "instruction": "Synthesize report"}, + ], + edges=[ + ["collector", "tactics"], + ["collector", "players"], + ["collector", "media"], + ["tactics", "synthesizer"], + ["players", "synthesizer"], + ["media", "synthesizer"], + ], + output_agent="synthesizer", + ) + + assert graph.strategy == "dag" + assert _deps(graph) == { + "collector": [], + "tactics": ["collector"], + "players": ["collector"], + "media": ["collector"], + "synthesizer": ["tactics", "players", "media"], + } + + +def test_graph_workflow_rejects_unknown_cycle_and_disconnected_agents() -> None: + with pytest.raises(ValueError, match="unknown agent"): + build_explicit_graph( + task="bad graph", + agents=[ + {"name": "collector", "instruction": "Collect"}, + {"name": "reporter", "instruction": "Report"}, + ], + edges=[["collector", "missing"]], + output_agent="reporter", + ) + + with pytest.raises(ValueError, match="cyclic"): + build_explicit_graph( + task="bad graph", + agents=[ + {"name": "a", "instruction": "A"}, + {"name": "b", "instruction": "B"}, + ], + edges=[["a", "b"], ["b", "a"]], + output_agent="b", + ) + + with pytest.raises(ValueError, match="disconnected"): + build_explicit_graph( + task="bad graph", + agents=[ + {"name": "collector", "instruction": "Collect"}, + {"name": "reporter", "instruction": "Report"}, + {"name": "orphan", "instruction": "Unused"}, + ], + edges=[["collector", "reporter"]], + output_agent="reporter", + ) diff --git a/app-instance/backend/tests/unit/test_team_workflow_runtime_bridge.py b/app-instance/backend/tests/unit/test_team_workflow_runtime_bridge.py new file mode 100644 index 0000000..4bded31 --- /dev/null +++ b/app-instance/backend/tests/unit/test_team_workflow_runtime_bridge.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +import asyncio +import json +from types import SimpleNamespace +from typing import Any + +from beaver.coordinator import NodeRunResult, TeamRunResult +from beaver.tools import ToolContext +from beaver.tools.mcp.wrapper import MCPToolWrapper + + +def _tool_def(name: str) -> SimpleNamespace: + return SimpleNamespace( + name=name, + description=name, + inputSchema={"type": "object", "properties": {}}, + ) + + +def test_team_workflow_mcp_wrapper_bridges_to_current_team_runtime() -> None: + remote_calls: list[tuple[str, dict[str, Any]]] = [] + captured: dict[str, Any] = {} + + async def call_tool(name: str, arguments: dict[str, Any]) -> Any: + remote_calls.append((name, arguments)) + raise AssertionError("team workflow bridge must not call MCP subprocess") + + async def runner(graph, **kwargs: Any) -> TeamRunResult: + captured["graph"] = graph + captured["kwargs"] = kwargs + return TeamRunResult( + success=True, + summary="team done", + node_results=[ + NodeRunResult("collect", True, "collected", run_id="run-collect"), + NodeRunResult("report", True, "reported", run_id="run-report"), + ], + run_ids=["run-collect", "run-report"], + session_ids=["session:collect", "session:report"], + task_id=kwargs["parent_task_id"], + ) + + wrapper = MCPToolWrapper( + "local_team_workflow_mcp", + _tool_def("SequentialWorkflow"), + call_tool, + category="team_workflow", + kind="local", + ) + context = ToolContext( + session_id="session-1", + services={ + "task_id": "task-1", + "run_id": "run-root", + "agent_team_runner": runner, + }, + metadata={"source": "websocket"}, + ) + + result = asyncio.run( + wrapper.invoke( + { + "task": "finance report", + "agents": [ + {"name": "collect", "instruction": "Collect official sources"}, + {"name": "report", "instruction": "Write report"}, + ], + }, + context, + ) + ) + + payload = json.loads(result.content) + graph = captured["graph"] + + assert remote_calls == [] + assert result.success is True + assert result.tool_name == "mcp_local_team_workflow_mcp_SequentialWorkflow" + assert payload["success"] is True + assert payload["workflow"] == "SequentialWorkflow" + assert payload["summary"] == "team done" + assert payload["run_ids"] == ["run-collect", "run-report"] + assert captured["kwargs"]["parent_task_id"] == "task-1" + assert captured["kwargs"]["parent_session_id"] == "session-1" + assert captured["kwargs"]["parent_run_id"] == "run-root" + assert graph.strategy == "sequence" + assert {node.node_id: list(node.depends_on) for node in graph.nodes} == { + "collect": [], + "report": ["collect"], + } + + +def test_ordinary_mcp_wrapper_still_calls_remote_tool() -> None: + remote_calls: list[tuple[str, dict[str, Any]]] = [] + + async def call_tool(name: str, arguments: dict[str, Any]) -> Any: + remote_calls.append((name, arguments)) + return SimpleNamespace(content=[], structuredContent={"ok": True}) + + wrapper = MCPToolWrapper( + "local_web_mcp", + _tool_def("web_search"), + call_tool, + category="web", + kind="local", + ) + + result = asyncio.run(wrapper.invoke({"query": "beaver"}, ToolContext())) + + assert result.success is True + assert remote_calls == [("web_search", {"query": "beaver"})] + + +def test_team_workflow_bridge_uses_team_service_without_injected_runner(monkeypatch) -> None: + captured: dict[str, Any] = {} + + class FakeTeamService: + def __init__(self, loop: Any) -> None: + captured["loop"] = loop + + async def run_team(self, graph, **kwargs: Any) -> TeamRunResult: + captured["graph"] = graph + captured["kwargs"] = kwargs + return TeamRunResult( + success=True, + summary="service team done", + node_results=[NodeRunResult("only", True, "ok", run_id="run-only")], + run_ids=["run-only"], + session_ids=["session:only"], + task_id=kwargs["parent_task_id"], + ) + + class FakeAgentLoop: + def __init__(self, *, profile: Any, loader: Any) -> None: + self.profile = profile + self.loader = loader + self.loaded = None + + monkeypatch.setattr("beaver.engine.AgentLoop", FakeAgentLoop) + monkeypatch.setattr("beaver.services.team_service.TeamService", FakeTeamService) + + wrapper = MCPToolWrapper( + "local_team_workflow_mcp", + _tool_def("ConcurrentWorkflow"), + call_tool=lambda _name, _arguments: None, # type: ignore[arg-type] + category="team_workflow", + kind="local", + ) + parent_loop = SimpleNamespace(profile="profile", loader="loader") + context = ToolContext( + session_id="session-1", + services={ + "task_id": "task-1", + "run_id": "run-root", + "agent_loop": parent_loop, + "loaded": SimpleNamespace(name="loaded"), + }, + ) + + result = asyncio.run( + wrapper.invoke( + { + "task": "parallel work", + "agents": [{"name": "only", "instruction": "Do work"}], + }, + context, + ) + ) + + payload = json.loads(result.content) + + assert result.success is True + assert payload["summary"] == "service team done" + assert captured["loop"].profile == "profile" + assert captured["loop"].loader == "loader" + assert captured["loop"].loaded.name == "loaded" + assert captured["kwargs"]["parent_task_id"] == "task-1" + assert captured["kwargs"]["parent_session_id"] == "session-1" + assert captured["kwargs"]["parent_run_id"] == "run-root" + assert captured["kwargs"]["allow_candidate_generation"] is False + assert captured["graph"].strategy == "parallel" diff --git a/app-instance/frontend/app/(app)/notifications/[scheduledRunId]/page.tsx b/app-instance/frontend/app/(app)/notifications/[scheduledRunId]/page.tsx index e2b104a..2782bed 100644 --- a/app-instance/frontend/app/(app)/notifications/[scheduledRunId]/page.tsx +++ b/app-instance/frontend/app/(app)/notifications/[scheduledRunId]/page.tsx @@ -146,11 +146,6 @@ export default function NotificationDetailPage() { isThinking={submitting} messagesEndRef={messagesEndRef} messageViewportRef={viewportRef} - processRuns={[]} - processEvents={[]} - processArtifacts={[]} - selectedRunId={null} - onSelectRun={() => {}} onFeedback={() => {}} onRequestRevision={() => {}} /> diff --git a/app-instance/frontend/app/(app)/page.tsx b/app-instance/frontend/app/(app)/page.tsx index b30b671..dc81910 100644 --- a/app-instance/frontend/app/(app)/page.tsx +++ b/app-instance/frontend/app/(app)/page.tsx @@ -77,7 +77,6 @@ export default function ChatPage() { processRuns, processEvents, processArtifacts, - selectedRunId, setSessionId, setMessages, addMessage, @@ -128,12 +127,6 @@ export default function ChatPage() { [processEvents, sessionRunIds] ); - const sessionProcessArtifacts = useMemo( - () => processArtifacts.filter((artifact) => sessionRunIds.has(artifact.run_id)), - [processArtifacts, sessionRunIds] - ); - - const selectedSessionRunId = selectedRunId && sessionRunIds.has(selectedRunId) ? selectedRunId : null; const activeTaskTimelineView = useMemo( () => buildTaskTimelineView({ @@ -710,11 +703,6 @@ export default function ChatPage() { isThinking={isThinking || (isLoading && messages[messages.length - 1]?.role === 'user')} messagesEndRef={messagesEndRef} messageViewportRef={messageViewportRef} - processRuns={sessionProcessRuns} - processEvents={sessionProcessEvents} - processArtifacts={sessionProcessArtifacts} - selectedRunId={selectedSessionRunId} - onSelectRun={(runId) => setSelectedRunId(selectedSessionRunId === runId ? null : runId)} onFeedback={handleFeedback} onRequestRevision={handleRequestRevision} /> @@ -881,6 +869,8 @@ export default function ChatPage() { {activeTaskDetail ? ( diff --git a/app-instance/frontend/app/(app)/tasks/[taskId]/page.tsx b/app-instance/frontend/app/(app)/tasks/[taskId]/page.tsx index b58c946..80fd3ea 100644 --- a/app-instance/frontend/app/(app)/tasks/[taskId]/page.tsx +++ b/app-instance/frontend/app/(app)/tasks/[taskId]/page.tsx @@ -1,20 +1,19 @@ 'use client'; import Link from 'next/link'; -import { useParams, useRouter } from 'next/navigation'; +import { useParams } from 'next/navigation'; import React, { useMemo, useState } from 'react'; -import { AlertCircle, ArrowLeft, Loader2, Trash2 } from 'lucide-react'; +import { AlertCircle, ArrowLeft, Loader2 } from 'lucide-react'; import { + TaskExecutionWorkspace, TaskLiveHeader, - TaskSideRail, - TaskTimeline, type TaskFeedbackItem, type TaskFeedbackType, } from '@/components/task-detail'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { deleteBackendTask, getBackendTask, submitChatFeedback } from '@/lib/api'; +import { getBackendTask, submitChatFeedback } from '@/lib/api'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { useChatStore } from '@/lib/store'; @@ -27,7 +26,6 @@ const TASK_RESULT_REVIEW_ID = 'task-result-review'; export default function TaskDetailPage() { const { locale } = useAppI18n(); - const router = useRouter(); const params = useParams<{ taskId: string }>(); const taskId = decodeURIComponent(Array.isArray(params?.taskId) ? params.taskId[0] : params?.taskId ?? ''); const processRuns = useChatStore((state) => state.processRuns); @@ -120,18 +118,6 @@ export default function TaskDetailPage() { } }; - const deleteCurrentBackendTask = async () => { - if (!backendTask) return; - const title = backendTask.short_title || backendTask.description || backendTask.goal || backendTask.task_id; - if (!window.confirm(pickAppText(locale, `删除任务“${title}”?`, `Delete task "${title}"?`))) { - return; - } - await runAction('delete-backend-task', async () => { - await deleteBackendTask(backendTask.task_id); - router.push('/tasks'); - }); - }; - if (backendTask) { const feedbackItems = backendTask.feedback || []; @@ -139,64 +125,44 @@ export default function TaskDetailPage() {
-
-
-
- -
+
+ {actionError ? ( + + + + {actionError} + + + ) : null} - {actionError ? ( - - - - {actionError} - - - ) : null} - - - runAction(`backend-feedback-${feedbackType}`, async () => { - if (!feedbackRunId) throw new Error(pickAppText(locale, '暂无可验收的运行记录。', 'No run is available for acceptance yet.')); - await submitChatFeedback({ - sessionId: backendTask.session_id, - runId: feedbackRunId, - feedbackType, - comment, - }); - updateMessageFeedback(feedbackRunId, feedbackType); - setRevision(''); - await loadBackendTask(); - }), - }} - /> -
- - + runAction(`backend-feedback-${feedbackType}`, async () => { + if (!feedbackRunId) throw new Error(pickAppText(locale, '暂无可验收的运行记录。', 'No run is available for acceptance yet.')); + await submitChatFeedback({ + sessionId: backendTask.session_id, + runId: feedbackRunId, + feedbackType, + comment, + }); + updateMessageFeedback(feedbackRunId, feedbackType); + setRevision(''); + await loadBackendTask(); + }), + }} />
diff --git a/app-instance/frontend/components/AppShell.tsx b/app-instance/frontend/components/AppShell.tsx index dac1275..53e97e9 100644 --- a/app-instance/frontend/components/AppShell.tsx +++ b/app-instance/frontend/components/AppShell.tsx @@ -10,7 +10,7 @@ export function AppShell({ children }: { children: ReactNode }) { return (
-
+
{children} diff --git a/app-instance/frontend/components/Header.tsx b/app-instance/frontend/components/Header.tsx index b6d3498..8a09176 100644 --- a/app-instance/frontend/components/Header.tsx +++ b/app-instance/frontend/components/Header.tsx @@ -131,8 +131,8 @@ const Header = () => { key={item.href} href={item.href} onClick={compact ? () => setMobileMenuOpen(false) : undefined} - className={`flex h-11 shrink-0 items-center gap-2 rounded-full text-sm font-medium transition-colors ${ - compact ? 'justify-start rounded-lg border border-transparent bg-background px-4' : 'px-4' + className={`flex shrink-0 items-center gap-2 rounded-full text-sm font-medium transition-colors ${ + compact ? 'h-11 justify-start rounded-lg border border-transparent bg-background px-4' : 'h-10 px-3.5' } ${ isActive ? 'bg-primary text-primary-foreground' @@ -151,11 +151,11 @@ const Header = () => { <>
-
+
- + + + + Beaver
-
@@ -58,9 +328,13 @@ function ProgressPanel({ } export function CurrentSessionProgressSidebar({ + task, + process, cards, isLive, }: { + task: BackendTask | null; + process: SessionProcessProjection | null; cards: TaskTimelineCard[]; isLive: boolean; }) { @@ -70,7 +344,7 @@ export function CurrentSessionProgressSidebar({ return ( <>
) : null} diff --git a/app-instance/frontend/components/chat-workbench/MessageList.tsx b/app-instance/frontend/components/chat-workbench/MessageList.tsx index dcebb7f..c649bbf 100644 --- a/app-instance/frontend/components/chat-workbench/MessageList.tsx +++ b/app-instance/frontend/components/chat-workbench/MessageList.tsx @@ -4,10 +4,9 @@ import React from 'react'; import Link from 'next/link'; import { Bot, CheckCircle2, ChevronRight, Loader2, Paperclip, RefreshCcw, ThumbsUp, User, XCircle } from 'lucide-react'; -import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types'; +import type { ChatMessage } from '@/types'; import { getAccessToken, getFileUrl } from '@/lib/api'; import { getTaskCardMessageIndexes, hasVisibleChatContent, normalizedMessageText, shouldDisplayChatMessage } from '@/lib/chat-messages'; -import { AgentTeamBlock } from '@/components/chat-workbench/AgentTeamBlock'; import { MarkdownContent } from '@/components/chat-workbench/MarkdownContent'; import { ScrollArea } from '@/components/ui/scroll-area'; import { @@ -268,14 +267,6 @@ function MessageBubble({ ); } -type AgentTeamGroup = { - rootRun: ProcessRun; - memberRuns: ProcessRun[]; - startedAt: string; -}; - -const TERMINAL_RUN_STATUSES = new Set(['done', 'error', 'cancelled']); - function shouldHideSystemAgentMessage(message: ChatMessage): boolean { if (message.role !== 'assistant' || typeof message.content !== 'string') { return false; @@ -299,72 +290,11 @@ function shouldHideMessage(message: ChatMessage): boolean { return !shouldDisplayChatMessage(message); } -function parseTimelineTime(value?: string | null): number | null { - if (!value) return null; - const parsed = new Date(value).getTime(); - return Number.isFinite(parsed) ? parsed : null; -} - -function buildAgentTeamGroups(processRuns: ProcessRun[]): AgentTeamGroup[] { - const runMap = new Map(processRuns.map((run) => [run.run_id, run])); - const groups = new Map(); - - for (const run of processRuns) { - if (run.actor_type !== 'agent') { - continue; - } - - let root = run; - const seen = new Set([run.run_id]); - let parentId = run.parent_run_id ?? null; - while (parentId) { - const parent = runMap.get(parentId); - if (!parent || seen.has(parent.run_id)) { - break; - } - root = parent; - seen.add(parent.run_id); - parentId = parent.parent_run_id ?? null; - } - - const existing = groups.get(root.run_id); - if (existing) { - existing.memberRuns.push(run); - continue; - } - groups.set(root.run_id, { - rootRun: root, - memberRuns: [run], - startedAt: root.started_at || run.started_at, - }); - } - - return Array.from(groups.values()) - .map((group) => ({ - ...group, - memberRuns: [...group.memberRuns].sort((a: ProcessRun, b: ProcessRun) => { - const at = parseTimelineTime(a.started_at) ?? 0; - const bt = parseTimelineTime(b.started_at) ?? 0; - return at - bt; - }), - })) - .sort((a, b) => { - const at = parseTimelineTime(a.startedAt) ?? 0; - const bt = parseTimelineTime(b.startedAt) ?? 0; - return at - bt; - }); -} - export function MessageList({ messages, isThinking, messagesEndRef, viewportRef, - processRuns, - processEvents, - processArtifacts, - selectedRunId, - onSelectRun, onFeedback, onRequestRevision, }: { @@ -372,11 +302,6 @@ export function MessageList({ isThinking: boolean; messagesEndRef: React.RefObject; viewportRef: React.RefObject; - processRuns: ProcessRun[]; - processEvents: ProcessEvent[]; - processArtifacts: ProcessArtifact[]; - selectedRunId: string | null; - onSelectRun: (runId: string) => void; onFeedback: (runId: string, feedbackType: 'accept' | 'revise' | 'abandon', comment?: string) => void; onRequestRevision: (runId: string) => void; }) { @@ -385,37 +310,6 @@ export function MessageList({ () => messages.filter((message) => !shouldHideMessage(message)), [messages] ); - const teamGroups = React.useMemo( - () => - buildAgentTeamGroups(processRuns).filter((group) => - group.memberRuns.some((run) => !TERMINAL_RUN_STATUSES.has(run.status)) - ), - [processRuns] - ); - const timelineItems = React.useMemo(() => { - const messageItems = visibleMessages.map((message, index) => ({ - kind: 'message' as const, - key: `${message.role}:${message.timestamp || index}:${index}`, - sortTime: parseTimelineTime(message.timestamp) ?? Number.MAX_SAFE_INTEGER / 2 + index, - order: index, - message, - messageIndex: index, - })); - const teamItems = teamGroups.map((group, index) => ({ - kind: 'team' as const, - key: `team:${group.rootRun.run_id}`, - sortTime: parseTimelineTime(group.startedAt) ?? Number.MAX_SAFE_INTEGER / 2 + visibleMessages.length + index, - order: visibleMessages.length + index, - group, - })); - - return [...messageItems, ...teamItems].sort((a, b) => { - if (a.sortTime !== b.sortTime) { - return a.sortTime - b.sortTime; - } - return a.order - b.order; - }); - }, [teamGroups, visibleMessages]); const taskCardMessageIndexes = React.useMemo( () => getTaskCardMessageIndexes(visibleMessages), [visibleMessages] @@ -439,7 +333,7 @@ export function MessageList({ return (
- {visibleMessages.length === 0 && teamGroups.length === 0 && !isThinking && ( + {visibleMessages.length === 0 && !isThinking && (

Beaver

@@ -447,28 +341,16 @@ export function MessageList({
)} - {timelineItems.map((item) => - item.kind === 'message' ? ( - - ) : ( - - ) - )} + {visibleMessages.map((message, index) => ( + + ))} {isThinking && (
diff --git a/app-instance/frontend/components/task-detail/TaskExecutionWorkspace.tsx b/app-instance/frontend/components/task-detail/TaskExecutionWorkspace.tsx new file mode 100644 index 0000000..9fe73c2 --- /dev/null +++ b/app-instance/frontend/components/task-detail/TaskExecutionWorkspace.tsx @@ -0,0 +1,638 @@ +'use client'; + +import React from 'react'; +import { + BarChart3, + CheckCircle2, + ChevronDown, + Clock3, + Database, + Download, + Eye, + FileImage, + FileJson, + FileText, + Globe2, + Grid2X2, + ListFilter, + Network, + PackageOpen, + RefreshCw, + ShieldCheck, + Table2, + UserRound, +} from 'lucide-react'; + +import type { TaskFeedbackType } from '@/components/task-detail/TaskAcceptanceCard'; +import type { TaskResultAcceptance } from '@/components/task-detail/TaskTimelineCard'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { formatTaskRuntimeDuration, formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared'; +import { pickAppText } from '@/lib/i18n/core'; +import { useAppI18n } from '@/lib/i18n/provider'; +import { + buildTaskUiModel, + taskUiStatusClass, + taskUiStatusLabel, + type TaskUiAgentNode, + type TaskUiArtifact, + type TaskUiAttempt, + type TaskUiModel, + type TaskUiStatus, + type TaskUiStep, +} from '@/lib/task-ui-model'; +import { containedLongTextClass, containedPreservedLongTextClass } from '@/lib/text-wrapping'; +import type { BackendTask, SessionProcessProjection, TaskTimelineCard } from '@/types'; + +type Props = { + task: BackendTask; + process: SessionProcessProjection; + cards: TaskTimelineCard[]; + isLive: boolean; + resultAcceptance?: TaskResultAcceptance; + reviewTargetId?: string; +}; + +function StatusBadge({ status, compact = false }: { status: TaskUiStatus; compact?: boolean }) { + const { locale } = useAppI18n(); + return ( + + {taskUiStatusLabel(status, locale)} + + ); +} + +function Section({ + title, + children, + action, + className = '', +}: { + title: string; + children: React.ReactNode; + action?: React.ReactNode; + className?: string; +}) { + return ( +
+
+

{title}

+ {action} +
+ {children} +
+ ); +} + +function EmptyState({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} + +function iconForStep(kind: TaskUiStep['kind']) { + if (kind === 'skill') return Grid2X2; + if (kind === 'tool') return Clock3; + if (kind === 'agent') return Network; + if (kind === 'artifact') return FileText; + if (kind === 'result') return BarChart3; + return FileText; +} + +function statusDotClass(status: TaskUiStatus) { + if (status === 'done') return 'bg-[#22733A]'; + if (status === 'running') return 'bg-[#C47B00]'; + if (status === 'error') return 'bg-[#9D3D2F]'; + if (status === 'cancelled') return 'bg-[#756A64]'; + return 'bg-[#8D8782]'; +} + +function ExecutionFlow({ model }: { model: TaskUiModel }) { + const { locale } = useAppI18n(); + const steps = model.steps.slice(0, 6); + const columnClass = steps.length >= 6 ? 'grid-cols-6' : steps.length >= 4 ? 'grid-cols-4' : steps.length >= 2 ? 'grid-cols-2' : 'grid-cols-1'; + + return ( +
+ 查看详情 + + } + > +
+
+ {steps.length > 1 ?
: null} + {steps.map((step) => { + const Icon = iconForStep(step.kind); + return ( +
+
+ + {step.status === 'done' ? : } + + + + +
+

{step.title}

+
+ {step.createdAt ? {formatTaskRuntimeTime(step.createdAt, locale)} : null} + +
+ {step.summary ? ( +

{step.summary}

+ ) : null} +
+ ); + })} +
+
+
+ ); +} + +function progressColor(status: TaskUiStatus) { + if (status === 'done') return '#137333'; + if (status === 'running') return '#D48500'; + if (status === 'error') return '#9D3D2F'; + if (status === 'cancelled') return '#756A64'; + return '#E3DFDC'; +} + +function AgentCard({ agent, root = false }: { agent: TaskUiAgentNode; root?: boolean }) { + return ( +
+
+
+ + {agent.title || agent.name} +
+
+ +
+
+
+
+
+
{agent.progress}%
+
+ ); +} + +function AgentDAG({ model }: { model: TaskUiModel }) { + const { locale } = useAppI18n(); + const roots = model.agentTree; + const root = roots.find((node) => node.children.length > 0) ?? roots[0]; + const children = root?.children.length ? root.children : roots.filter((node) => node.runId !== root?.runId); + const visibleChildren = children.slice(0, 5); + + if (!model.team.hasTeam) { + return null; + } + + return ( +
+ + {model.team.outcome} + + +
+ } + > +
+ {roots.length === 0 ? ( + {pickAppText(locale, '暂无 Agent Team 数据', 'No Agent Team data yet')} + ) : ( +
+ {visibleChildren.length > 0 ? ( + <> +
+
+ {visibleChildren.map((child, index) => ( +
+ ))} + + ) : null} +
+ +
+ {visibleChildren.length > 0 ? ( +
+ {visibleChildren.map((agent) => ( + + ))} +
+ ) : null} +
+ )} +
+ + ); +} + +function RunPath({ + model, + selectedAttemptId, + onSelectAttempt, +}: { + model: TaskUiModel; + selectedAttemptId: string | null; + onSelectAttempt: (attemptId: string) => void; +}) { + const { locale } = useAppI18n(); + const [expandedIds, setExpandedIds] = React.useState>(() => new Set()); + const attempts = model.attempts.filter((attempt) => attempt.runs.length > 0 || attempt.tools.length > 0 || attempt.result); + + if (attempts.length === 0) return null; + + return ( +
+ {attempts.length} runs + + } + > +
+ {attempts.map((attempt) => ( +
onSelectAttempt(attempt.id)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + onSelectAttempt(attempt.id); + } + }} + role="group" + tabIndex={0} + aria-label={pickAppText(locale, `选择${attempt.title}`, `Select ${attempt.title}`)} + className={`rounded-lg border p-4 transition-colors ${ + selectedAttemptId === attempt.id + ? 'border-[#1D1715] bg-white shadow-[0_6px_18px_rgba(31,24,20,0.06)]' + : 'border-[#E1DCD8] bg-[#FBFAF9]' + } cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1D1715] focus-visible:ring-offset-2`} + > +
+
+
+

{attempt.title}

+ +
+
+ {formatTaskRuntimeTime(attempt.startedAt, locale)} + {attempt.finishedAt ? ` · ${formatAttemptDuration(attempt.startedAt, attempt.finishedAt, locale)}` : ''} +
+
+
+ {attempt.tools.length} tools +
+
+ + {attempt.runs.length > 0 ? ( +
+ {attempt.runs.map((run, index) => ( + + {index > 0 ? : null} + + + {run.actorName || run.title} + + + ))} +
+ ) : null} + + + + {attempt.result && expandedIds.has(attempt.id) ? ( +
+
+ + {pickAppText(locale, '本次结果', 'Attempt result')} + + +
+

+ {attempt.result.summary || attempt.result.title} +

+
+ ) : null} +
+ ))} +
+
+ ); +} + +function formatAttemptDuration(startedAt: string, finishedAt: string, locale: string): string { + const startMs = new Date(startedAt).getTime(); + const finishMs = new Date(finishedAt).getTime(); + if (Number.isNaN(startMs) || Number.isNaN(finishMs) || finishMs < startMs) return '-'; + return formatTaskRuntimeDuration(finishMs - startMs, locale); +} + +function toolsForAttempt(model: TaskUiModel, selectedAttemptId: string | null): TaskUiAttempt { + return ( + model.attempts.find((attempt) => attempt.id === selectedAttemptId) ?? + model.attempts.at(-1) ?? + { + id: 'all', + index: 1, + title: 'Agent', + status: 'waiting', + startedAt: '', + runs: [], + tools: model.tools, + } + ); +} + +function ToolCalls({ model, selectedAttemptId }: { model: TaskUiModel; selectedAttemptId: string | null }) { + const { locale } = useAppI18n(); + const selectedAttempt = toolsForAttempt(model, selectedAttemptId); + const agents = Array.from(new Set(selectedAttempt.tools.map((tool) => tool.actorName || 'Agent'))); + const [selectedAgent, setSelectedAgent] = React.useState(null); + const activeAgent = selectedAgent && agents.includes(selectedAgent) ? selectedAgent : agents[0] ?? 'Agent'; + const visibleTools = selectedAttempt.tools.filter((tool) => (tool.actorName || 'Agent') === activeAgent); + + return ( +
+ + {selectedAttempt.title} + +
+ } + > + {selectedAttempt.tools.length === 0 ? ( +
+ {pickAppText(locale, '暂无工具调用', 'No tool calls yet')} +
+ ) : ( +
+ +
+
+ 工具名称 + 摘要 + 状态 + 运行时间 +
+ {visibleTools.map((tool) => ( +
+ {tool.toolName} + + {tool.summary} + + + {formatToolDuration(tool, locale)} +
+ ))} +
+
+ )} + + ); +} + +function formatToolDuration(tool: TaskUiModel['tools'][number], locale: string): string { + if (typeof tool.durationMs === 'number') { + return formatTaskRuntimeDuration(tool.durationMs, locale); + } + if (tool.status === 'running' && tool.createdAt) { + const startMs = new Date(tool.createdAt).getTime(); + if (!Number.isNaN(startMs)) { + return formatTaskRuntimeDuration(Date.now() - startMs, locale); + } + } + return '-'; +} + +function iconForArtifact(artifact: TaskUiArtifact) { + if (artifact.type === 'json') return FileJson; + if (artifact.type === 'image') return FileImage; + return FileText; +} + +function WorkspaceFiles({ model }: { model: TaskUiModel }) { + const { locale } = useAppI18n(); + + return ( +
+ + +
+ } + > +
+
+ + + +
+ {model.artifacts.length === 0 ? ( +
+ {pickAppText(locale, '暂无 Workspace 文件', 'No workspace files yet')} +
+ ) : ( + <> +
+ 文件名 + 类型 + 大小 + 状态 + 操作 +
+
+ {model.artifacts.slice(0, 6).map((artifact) => { + const Icon = iconForArtifact(artifact); + return ( +
+
+ + + + {artifact.title} +
+ {artifact.type.toUpperCase()} + {artifact.sizeLabel || '-'} + +
+ + +
+
+ ); + })} + {model.artifacts.length > 6 ? ( + + ) : null} +
+ + )} +
+ + ); +} + +function ResultPanel({ + model, + resultAcceptance, + reviewTargetId, +}: { + model: TaskUiModel; + resultAcceptance?: TaskResultAcceptance; + reviewTargetId?: string; +}) { + const { locale } = useAppI18n(); + const [busyAction, setBusyAction] = React.useState(null); + const submit = async (type: TaskFeedbackType) => { + if (!resultAcceptance || busyAction) return; + setBusyAction(type); + try { + await resultAcceptance.onSubmit(type); + } finally { + setBusyAction(null); + } + }; + + return ( +
}> +
+ {model.result.summary ? ( +
+

{model.result.summary}

+ {model.result.bullets.length > 0 ? ( +
+ {model.result.bullets.map((item, index) => { + const Icon = [Globe2, Table2, BarChart3, ShieldCheck][index % 4]; + return ( +
+ + {item} +
+ ); + })} +
+ ) : null} +
+ ) : ( + {pickAppText(locale, '暂无本轮结果', 'No result for this run yet')} + )} +
+ + + +
+
+
+ ); +} + +export function TaskExecutionWorkspace({ task, process, cards, resultAcceptance, reviewTargetId }: Props) { + const { locale } = useAppI18n(); + const model = React.useMemo( + () => buildTaskUiModel({ task, process, cards, locale }), + [cards, locale, process, task], + ); + const latestAttemptId = model.attempts.at(-1)?.id ?? null; + const [selectedAttemptState, setSelectedAttemptState] = React.useState(latestAttemptId); + const selectedAttemptId = model.attempts.some((attempt) => attempt.id === selectedAttemptState) + ? selectedAttemptState + : latestAttemptId; + + return ( +
+
+ + + + +
+
+ + +
+
+ ); +} diff --git a/app-instance/frontend/components/task-detail/TaskLiveHeader.tsx b/app-instance/frontend/components/task-detail/TaskLiveHeader.tsx index acc8dce..6be78b9 100644 --- a/app-instance/frontend/components/task-detail/TaskLiveHeader.tsx +++ b/app-instance/frontend/components/task-detail/TaskLiveHeader.tsx @@ -43,17 +43,17 @@ export function TaskLiveHeader({ task, activeLabel, durationMs, reviewTargetId } const showReviewLink = Boolean(reviewTargetId && ['awaiting_acceptance', 'needs_revision'].includes(task.status)); return ( -
-
+
+
- - + + +
+
+
+
+
+ +
+ ← → 翻页 + R 重置计时 + Esc 关闭 + 拖动卡片头部移动 · 拖动右下角调整大小 + +
+ + + + diff --git a/docs/presentations/beaver-product-sale/style.css b/docs/presentations/beaver-product-sale/style.css new file mode 100644 index 0000000..d1ad392 --- /dev/null +++ b/docs/presentations/beaver-product-sale/style.css @@ -0,0 +1,13 @@ +/* 1920×1080 / 16:9 Beaver sales deck. All slides share the supplied template's deep-indigo base. */ +.tpl-beaver-sale{--bg:#06062d;--surface:#06062d;--text-1:#fff;--text-2:#d3dced;--text-3:#93a6c9;--accent:#2dd1dd;--accent-2:#86e6eb;--border:rgba(116,155,222,.38);--border-soft:rgba(116,155,222,.22);--radius:0;--shadow:none;font-family:"Inter","Noto Sans SC",sans-serif;background:var(--bg)} +.tpl-beaver-sale .deck,.tpl-beaver-sale .slide{background:var(--bg)}.tpl-beaver-sale .slide{padding:72px 92px;color:var(--text-1);overflow:hidden}.tpl-beaver-sale .slide:before{content:"";position:absolute;inset:0;pointer-events:none;background:radial-gradient(circle at 3% 0%,rgba(43,78,154,.58),transparent 33%),radial-gradient(circle at 97% 88%,rgba(32,70,140,.35),transparent 29%)} +.tpl-beaver-sale .masthead,.tpl-beaver-sale .footer{position:absolute;z-index:3;left:78px;right:78px;display:flex;justify-content:space-between;font-family:"JetBrains Mono",monospace;letter-spacing:.13em}.tpl-beaver-sale .masthead{top:39px;color:#9eb0d0;font-size:11px}.tpl-beaver-sale .masthead span:last-child{color:var(--accent);font-size:15px}.tpl-beaver-sale .footer{bottom:27px;color:#8597ba;font-size:10px}.tpl-beaver-sale .footer .slide-number{color:#c5d2e6} +.tpl-beaver-sale .eyebrow{margin:0 0 15px;color:var(--accent);font-family:"JetBrains Mono",monospace;font-size:11px;font-weight:700;letter-spacing:.13em;text-transform:uppercase}.tpl-beaver-sale h1,.tpl-beaver-sale h2,.tpl-beaver-sale p{position:relative;z-index:1}.tpl-beaver-sale h1{margin:0;color:var(--accent);font-size:49px;line-height:1.14;letter-spacing:-.045em;font-weight:800}.tpl-beaver-sale h2{margin:17px 0 0;color:#fff;font-size:29px;line-height:1.28;letter-spacing:-.025em}.tpl-beaver-sale .takeaway{margin:29px 0 0;color:var(--accent-2);font-size:18px;line-height:1.52;font-weight:650}.tpl-beaver-sale .key-list{position:relative;z-index:1;list-style:none;margin:31px 0 0;padding:0}.tpl-beaver-sale .key-list li{position:relative;margin:0 0 13px;padding-left:18px;color:#f2f5ff;font-size:20px;line-height:1.45}.tpl-beaver-sale .key-list li:before{content:"•";position:absolute;left:0;color:var(--accent);font-size:21px}.tpl-beaver-sale .key-list.compact{margin-top:25px}.tpl-beaver-sale .key-list.compact li{font-size:18px;margin-bottom:11px} +/* image slots */ +.tpl-beaver-sale .media-placeholder{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;box-sizing:border-box;min-width:0;min-height:0;border:1px dashed rgba(106,182,233,.72);background:linear-gradient(135deg,rgba(50,116,194,.18),rgba(13,25,72,.24));color:#b2c4e4;text-align:center}.tpl-beaver-sale .media-placeholder:before,.tpl-beaver-sale .media-placeholder:after{content:"";position:absolute;width:18px;height:18px;border-color:var(--accent);border-style:solid}.tpl-beaver-sale .media-placeholder:before{top:-1px;left:-1px;border-width:2px 0 0 2px}.tpl-beaver-sale .media-placeholder:after{right:-1px;bottom:-1px;border-width:0 2px 2px 0}.tpl-beaver-sale .media-placeholder span{color:var(--accent);font-family:"JetBrains Mono",monospace;font-size:11px;letter-spacing:.13em}.tpl-beaver-sale .media-placeholder b{max-width:80%;margin:14px 0 6px;color:#f3f6ff;font-size:19px;font-weight:650}.tpl-beaver-sale .media-placeholder small{max-width:80%;color:#a7b9da;font-size:12px;line-height:1.45} +/* cover */ +.tpl-beaver-sale .cover .diagonal-lines{position:absolute;inset:0;opacity:.7;background:linear-gradient(118deg,transparent 0 10%,rgba(104,146,235,.35) 10.04% 10.16%,transparent 10.2% 26%,rgba(104,146,235,.2) 26.04% 26.14%,transparent 26.2% 100%),linear-gradient(117deg,transparent 0 78%,rgba(104,146,235,.28) 78.04% 78.16%,transparent 78.2% 100%)}.tpl-beaver-sale .topline{position:absolute;z-index:2;top:43px;left:78px;right:78px;display:flex;justify-content:space-between;color:#bbcce9;font-family:"JetBrains Mono",monospace;font-size:12px;letter-spacing:.15em}.tpl-beaver-sale .topline span:last-child{color:#92abd2;font-size:11px}.tpl-beaver-sale .cover-copy{position:absolute;z-index:2;left:142px;top:282px;width:610px}.tpl-beaver-sale .cover h1{font-size:79px;line-height:1}.tpl-beaver-sale .cover h2{margin-top:13px;color:var(--accent-2);font-size:53px;line-height:1.15}.tpl-beaver-sale .cover-copy>p:last-child{margin:31px 0 0;color:#d4ddec;font-size:21px;line-height:1.55}.tpl-beaver-sale .cover-media{position:absolute;right:142px;top:235px;width:560px;height:475px}.tpl-beaver-sale .deck-footer{position:absolute;z-index:2;left:78px;right:78px;bottom:27px;display:flex;justify-content:space-between;color:#94a7ca;font-family:"JetBrains Mono",monospace;font-size:10px;letter-spacing:.13em} +/* varied page layouts */ +.tpl-beaver-sale .layout-split .split-copy{position:absolute;z-index:2;left:150px;top:245px;width:650px}.tpl-beaver-sale .layout-split .media-right{position:absolute;right:125px;top:210px;width:630px;height:550px}.tpl-beaver-sale .pain .takeaway{max-width:610px}.tpl-beaver-sale .layout-image-left .media-left{position:absolute;left:125px;top:205px;width:590px;height:575px}.tpl-beaver-sale .position-copy{position:absolute;z-index:2;left:805px;top:224px;width:850px}.tpl-beaver-sale .body-copy{margin:23px 0 0;color:var(--text-2);font-size:19px;line-height:1.65}.tpl-beaver-sale .value-grid{display:grid;grid-template-columns:1fr 1fr;gap:0 35px;margin-top:35px}.tpl-beaver-sale .value-grid span{padding:15px 0;border-top:1px solid var(--border);color:#d7e0ef;font-size:17px}.tpl-beaver-sale .value-grid b{color:var(--accent-2);font-weight:700}.tpl-beaver-sale .advantage h2{max-width:620px}.tpl-beaver-sale .layout-top-media .top-title{position:absolute;z-index:2;left:150px;top:130px}.tpl-beaver-sale .top-title>p:last-child{margin:16px 0 0;color:var(--text-2);font-size:19px}.tpl-beaver-sale .media-wide{position:absolute;left:230px;right:230px;top:400px;height:250px}.tpl-beaver-sale .four-points{position:absolute;z-index:2;left:230px;right:230px;top:688px;display:grid;grid-template-columns:repeat(4,1fr);gap:0}.tpl-beaver-sale .four-points span{padding:0 18px;color:#e7edf8;font-size:16px;text-align:center;border-right:1px solid var(--border)}.tpl-beaver-sale .four-points span:last-child{border-right:none}.tpl-beaver-sale .centered{position:absolute;z-index:2;left:0;right:0;top:770px;text-align:center}.tpl-beaver-sale .layout-side-media .media-side{position:absolute;left:140px;top:205px;width:710px;height:570px}.tpl-beaver-sale .side-copy{position:absolute;z-index:2;left:985px;top:235px;width:590px}.tpl-beaver-sale .layout-band .band-copy{position:absolute;z-index:2;left:150px;top:143px}.tpl-beaver-sale .band-copy h2{font-size:27px}.tpl-beaver-sale .media-band{position:absolute;left:150px;right:150px;top:350px;height:255px}.tpl-beaver-sale .layout-split-reverse .media-left{position:absolute;left:125px;top:210px;width:610px;height:560px}.tpl-beaver-sale .right-copy{position:absolute;z-index:2;left:895px;top:235px;width:680px} +.tpl-beaver-sale .layout-collage .scene-copy{position:absolute;z-index:2;left:130px;top:190px;width:650px}.tpl-beaver-sale .media-collage{position:absolute;z-index:2;right:130px;top:187px;display:grid;grid-template-columns:repeat(2,270px);grid-template-rows:250px 250px;gap:20px}.tpl-beaver-sale .media-collage .media-placeholder b{font-size:16px}.tpl-beaver-sale .media-collage .media-placeholder small{font-size:11px}.tpl-beaver-sale .layout-architecture .architecture-copy{position:absolute;z-index:2;left:135px;top:155px;width:765px}.tpl-beaver-sale .architecture-list{margin-top:30px}.tpl-beaver-sale .architecture-list p{display:grid;grid-template-columns:110px 1fr;gap:18px;margin:0;padding:13px 0;border-top:1px solid var(--border)}.tpl-beaver-sale .architecture-list p:last-child{border-bottom:1px solid var(--border)}.tpl-beaver-sale .architecture-list b{color:#9be8ed;font-family:"JetBrains Mono",monospace;font-size:13px}.tpl-beaver-sale .architecture-list span{color:#fff;font-size:16px}.tpl-beaver-sale .architecture-media{position:absolute;right:125px;top:205px;width:640px;height:530px} +@media (max-width:1500px){.tpl-beaver-sale .slide{padding:56px 70px}.tpl-beaver-sale h1{font-size:42px}.tpl-beaver-sale .masthead,.tpl-beaver-sale .footer{left:55px;right:55px}.tpl-beaver-sale .cover-copy{left:105px;top:235px}.tpl-beaver-sale .cover-media{right:105px;width:470px;height:405px}.tpl-beaver-sale .layout-split .split-copy{left:110px;top:210px}.tpl-beaver-sale .layout-split .media-right{right:95px;width:510px;height:465px}.tpl-beaver-sale .layout-image-left .media-left{left:95px;width:470px;height:470px}.tpl-beaver-sale .position-copy{left:655px;top:190px;width:640px}.tpl-beaver-sale .layout-side-media .media-side{left:105px;width:560px;height:470px}.tpl-beaver-sale .side-copy{left:755px;top:210px}.tpl-beaver-sale .right-copy{left:720px}.tpl-beaver-sale .media-collage{right:90px;grid-template-columns:repeat(2,220px);grid-template-rows:200px 200px}.tpl-beaver-sale .layout-collage .scene-copy{left:100px}.tpl-beaver-sale .architecture-media{right:95px;width:500px}.tpl-beaver-sale .layout-architecture .architecture-copy{left:100px;width:620px}} diff --git a/docs/presentations/参考资料/BoardWare-AI-Native-Solution-v13.pdf b/docs/presentations/参考资料/BoardWare-AI-Native-Solution-v13.pdf new file mode 100644 index 0000000..135ba49 Binary files /dev/null and b/docs/presentations/参考资料/BoardWare-AI-Native-Solution-v13.pdf differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-01.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-01.png new file mode 100644 index 0000000..0abce9b Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-01.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-02.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-02.png new file mode 100644 index 0000000..607b481 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-02.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-03.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-03.png new file mode 100644 index 0000000..f3f10ba Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-03.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-04.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-04.png new file mode 100644 index 0000000..e148744 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-04.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-05.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-05.png new file mode 100644 index 0000000..a63104a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-05.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-06.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-06.png new file mode 100644 index 0000000..df1197b Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-06.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-07.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-07.png new file mode 100644 index 0000000..3adfbd2 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-07.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-08.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-08.png new file mode 100644 index 0000000..7d2cca1 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-08.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-09.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-09.png new file mode 100644 index 0000000..f5c24e3 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-09.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-10.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-10.png new file mode 100644 index 0000000..e295ac2 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/beaver-crop-10.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-center_runtime.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-center_runtime.png new file mode 100644 index 0000000..5345cf4 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-center_runtime.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-footer.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-footer.png new file mode 100644 index 0000000..e1d4558 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-footer.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-frame_l.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-frame_l.png new file mode 100644 index 0000000..fe5af41 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-frame_l.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-frame_r.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-frame_r.png new file mode 100644 index 0000000..472c048 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-frame_r.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_01.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_01.png new file mode 100644 index 0000000..0114d29 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_01.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_02.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_02.png new file mode 100644 index 0000000..e6c541e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_02.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_03.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_03.png new file mode 100644 index 0000000..6cd478c Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_03.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_04.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_04.png new file mode 100644 index 0000000..e141907 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_04.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_mobile.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_mobile.png new file mode 100644 index 0000000..9d926a7 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_mobile.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_web.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_web.png new file mode 100644 index 0000000..9490376 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_web.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_wechat.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_wechat.png new file mode 100644 index 0000000..25684da Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-left_wechat.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-logo.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-logo.png new file mode 100644 index 0000000..446c4ce Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-logo.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-mid_pill.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-mid_pill.png new file mode 100644 index 0000000..f14a235 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-mid_pill.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_feishu.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_feishu.png new file mode 100644 index 0000000..3fece9a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_feishu.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_glass.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_glass.png new file mode 100644 index 0000000..44d2697 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_glass.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_m5.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_m5.png new file mode 100644 index 0000000..cd6b522 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-right_m5.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-slogan.png new file mode 100644 index 0000000..d8426d2 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-title.png new file mode 100644 index 0000000..32269f1 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-top_icons.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-top_icons.png new file mode 100644 index 0000000..17234dd Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p01-top_icons.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-agent_panel.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-agent_panel.png new file mode 100644 index 0000000..c2099c0 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-agent_panel.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-beaver.png new file mode 100644 index 0000000..f524876 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-center_repo.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-center_repo.png new file mode 100644 index 0000000..ecddf96 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-center_repo.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-history.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-history.png new file mode 100644 index 0000000..67139f9 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-history.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-left_stack.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-left_stack.png new file mode 100644 index 0000000..adbf65f Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-left_stack.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-logo.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-logo.png new file mode 100644 index 0000000..4799d4b Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-logo.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-security.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-security.png new file mode 100644 index 0000000..2e48dab Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-security.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-slogan.png new file mode 100644 index 0000000..9bdfa41 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-title.png new file mode 100644 index 0000000..13048d7 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p02-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-control.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-control.png new file mode 100644 index 0000000..641497e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-control.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-credential.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-credential.png new file mode 100644 index 0000000..d7fbf78 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-credential.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_bottom.png new file mode 100644 index 0000000..9c9c085 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_middle.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_middle.png new file mode 100644 index 0000000..8ada908 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_middle.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_top.png new file mode 100644 index 0000000..62e76dc Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-left_top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-right_bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-right_bottom.png new file mode 100644 index 0000000..daa8a05 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-right_bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-right_top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-right_top.png new file mode 100644 index 0000000..c959b12 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-right_top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-runtime.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-runtime.png new file mode 100644 index 0000000..165ac6f Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-runtime.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-slogan.png new file mode 100644 index 0000000..242eec1 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-systems.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-systems.png new file mode 100644 index 0000000..fb7feff Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-systems.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-title.png new file mode 100644 index 0000000..aa802dc Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p03-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-center_memory.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-center_memory.png new file mode 100644 index 0000000..fc711bc Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-center_memory.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-left_profile.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-left_profile.png new file mode 100644 index 0000000..8c08dd4 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-left_profile.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-left_top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-left_top.png new file mode 100644 index 0000000..8a74d57 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-left_top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_bottom.png new file mode 100644 index 0000000..d810a95 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_org.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_org.png new file mode 100644 index 0000000..fb38e91 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_org.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_top.png new file mode 100644 index 0000000..12bc200 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-right_top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-slogan.png new file mode 100644 index 0000000..e0dfbc8 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-title.png new file mode 100644 index 0000000..434dee4 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p04-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-beaver.png new file mode 100644 index 0000000..da7bf82 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-left_eval.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-left_eval.png new file mode 100644 index 0000000..e8e5651 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-left_eval.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-loop.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-loop.png new file mode 100644 index 0000000..6b1dd45 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-loop.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-metrics.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-metrics.png new file mode 100644 index 0000000..50dc395 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-metrics.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-slogan.png new file mode 100644 index 0000000..60aaf3f Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-title.png new file mode 100644 index 0000000..a52293e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-versions.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-versions.png new file mode 100644 index 0000000..62dd301 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p05-versions.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-left_fragments.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-left_fragments.png new file mode 100644 index 0000000..da39763 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-left_fragments.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-org_reuse.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-org_reuse.png new file mode 100644 index 0000000..42a2a49 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-org_reuse.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-skill_template.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-skill_template.png new file mode 100644 index 0000000..cc8ff46 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-skill_template.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-slogan.png new file mode 100644 index 0000000..7ea19f8 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-title.png new file mode 100644 index 0000000..ccfa31a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p06-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-app_window.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-app_window.png new file mode 100644 index 0000000..38934f1 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-app_window.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-beaver.png new file mode 100644 index 0000000..44193af Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-blackbox.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-blackbox.png new file mode 100644 index 0000000..d8c1e2f Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-blackbox.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-right_stack.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-right_stack.png new file mode 100644 index 0000000..7f69533 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-right_stack.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-slogan.png new file mode 100644 index 0000000..605153e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-title.png new file mode 100644 index 0000000..0e1bc65 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p07-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-beaver.png new file mode 100644 index 0000000..3e43f2d Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-left_flow.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-left_flow.png new file mode 100644 index 0000000..91ff648 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-left_flow.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-main_flow.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-main_flow.png new file mode 100644 index 0000000..165f073 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-main_flow.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-right_value.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-right_value.png new file mode 100644 index 0000000..e1bf570 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-right_value.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-slogan.png new file mode 100644 index 0000000..1e02f94 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-title.png new file mode 100644 index 0000000..2ddcf15 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p08-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-center_runtime.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-center_runtime.png new file mode 100644 index 0000000..89f70ea Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-center_runtime.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-desc.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-desc.png new file mode 100644 index 0000000..00a012a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-desc.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-footer.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-footer.png new file mode 100644 index 0000000..df35ac3 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-footer.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-left_panels.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-left_panels.png new file mode 100644 index 0000000..65bcbfa Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-left_panels.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-right_panels.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-right_panels.png new file mode 100644 index 0000000..a6b2c51 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-right_panels.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-subtitle.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-subtitle.png new file mode 100644 index 0000000..5c8c53c Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-subtitle.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-title.png new file mode 100644 index 0000000..6ff0e8e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p09-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-beaver.png new file mode 100644 index 0000000..abd71ae Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-bottom_cards.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-bottom_cards.png new file mode 100644 index 0000000..9d01126 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-bottom_cards.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-hub.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-hub.png new file mode 100644 index 0000000..2fe0f1b Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-hub.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-left_stack.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-left_stack.png new file mode 100644 index 0000000..dfd0603 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-left_stack.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-right_stack.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-right_stack.png new file mode 100644 index 0000000..e5d3e9e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-right_stack.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-slogan.png new file mode 100644 index 0000000..d521b5a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-title.png new file mode 100644 index 0000000..a47773a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact-alpha/p10-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-bottom.png new file mode 100644 index 0000000..e4282ad Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-main.png new file mode 100644 index 0000000..ab70533 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-top.png new file mode 100644 index 0000000..33ec589 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-center_runtime.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-center_runtime.png new file mode 100644 index 0000000..e1a2031 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-center_runtime.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-footer.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-footer.png new file mode 100644 index 0000000..eb6afe2 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-footer.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-frame_l.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-frame_l.png new file mode 100644 index 0000000..e0c0a10 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-frame_l.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-frame_r.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-frame_r.png new file mode 100644 index 0000000..42b5b18 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-frame_r.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_01.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_01.png new file mode 100644 index 0000000..9dfd0fb Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_01.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_02.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_02.png new file mode 100644 index 0000000..e7b794e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_02.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_03.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_03.png new file mode 100644 index 0000000..0879051 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_03.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_04.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_04.png new file mode 100644 index 0000000..02b6c49 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_04.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_mobile.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_mobile.png new file mode 100644 index 0000000..a4cadec Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_mobile.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_web.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_web.png new file mode 100644 index 0000000..f916cb4 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_web.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_wechat.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_wechat.png new file mode 100644 index 0000000..d019d68 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-left_wechat.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-logo.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-logo.png new file mode 100644 index 0000000..f651cba Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-logo.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-mid_pill.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-mid_pill.png new file mode 100644 index 0000000..7c11096 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-mid_pill.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_feishu.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_feishu.png new file mode 100644 index 0000000..84969da Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_feishu.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_glass.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_glass.png new file mode 100644 index 0000000..7d3e495 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_glass.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_m5.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_m5.png new file mode 100644 index 0000000..e490bd4 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-right_m5.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-slogan.png new file mode 100644 index 0000000..a2488f3 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-title.png new file mode 100644 index 0000000..13068b1 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-top_icons.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-top_icons.png new file mode 100644 index 0000000..15a930c Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p01-top_icons.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-agent_panel.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-agent_panel.png new file mode 100644 index 0000000..1846569 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-agent_panel.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-beaver.png new file mode 100644 index 0000000..ba29a6b Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-bottom.png new file mode 100644 index 0000000..4789db9 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-main.png new file mode 100644 index 0000000..0e256e0 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-top.png new file mode 100644 index 0000000..18b2e5e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-center_repo.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-center_repo.png new file mode 100644 index 0000000..4c154c1 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-center_repo.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-history.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-history.png new file mode 100644 index 0000000..e726e85 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-history.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-left_stack.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-left_stack.png new file mode 100644 index 0000000..40a9a2d Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-left_stack.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-logo.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-logo.png new file mode 100644 index 0000000..19fa37b Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-logo.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-security.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-security.png new file mode 100644 index 0000000..b55053a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-security.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-slogan.png new file mode 100644 index 0000000..504c087 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-title.png new file mode 100644 index 0000000..d4261eb Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p02-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-bottom.png new file mode 100644 index 0000000..8f14acd Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-main.png new file mode 100644 index 0000000..f648e02 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-top.png new file mode 100644 index 0000000..21c68d1 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-control.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-control.png new file mode 100644 index 0000000..ed80e05 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-control.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-credential.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-credential.png new file mode 100644 index 0000000..92d1482 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-credential.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_bottom.png new file mode 100644 index 0000000..a6569d2 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_middle.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_middle.png new file mode 100644 index 0000000..69cb77e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_middle.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_top.png new file mode 100644 index 0000000..1aa0c12 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-left_top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-right_bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-right_bottom.png new file mode 100644 index 0000000..084ff79 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-right_bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-right_top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-right_top.png new file mode 100644 index 0000000..60dd3e9 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-right_top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-runtime.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-runtime.png new file mode 100644 index 0000000..8ce7b14 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-runtime.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-slogan.png new file mode 100644 index 0000000..0c4037b Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-systems.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-systems.png new file mode 100644 index 0000000..8db8312 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-systems.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-title.png new file mode 100644 index 0000000..8e08dd4 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p03-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-bottom.png new file mode 100644 index 0000000..93211f2 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-main.png new file mode 100644 index 0000000..d4f0358 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-top.png new file mode 100644 index 0000000..311e267 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-center_memory.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-center_memory.png new file mode 100644 index 0000000..7bf182f Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-center_memory.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-left_profile.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-left_profile.png new file mode 100644 index 0000000..f4367a9 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-left_profile.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-left_top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-left_top.png new file mode 100644 index 0000000..45c0070 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-left_top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_bottom.png new file mode 100644 index 0000000..44ab705 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_org.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_org.png new file mode 100644 index 0000000..51f393a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_org.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_top.png new file mode 100644 index 0000000..c178eee Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-right_top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-slogan.png new file mode 100644 index 0000000..df57489 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-title.png new file mode 100644 index 0000000..7e66af2 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p04-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-beaver.png new file mode 100644 index 0000000..d593d04 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-bottom.png new file mode 100644 index 0000000..951471e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-main.png new file mode 100644 index 0000000..fed2272 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-top.png new file mode 100644 index 0000000..705c7a2 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-left_eval.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-left_eval.png new file mode 100644 index 0000000..7362406 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-left_eval.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-loop.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-loop.png new file mode 100644 index 0000000..ddb1016 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-loop.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-metrics.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-metrics.png new file mode 100644 index 0000000..6d3f890 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-metrics.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-slogan.png new file mode 100644 index 0000000..38536ae Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-title.png new file mode 100644 index 0000000..3ef586b Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-versions.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-versions.png new file mode 100644 index 0000000..82aa152 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p05-versions.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-bottom.png new file mode 100644 index 0000000..19e1aaa Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-main.png new file mode 100644 index 0000000..4f38539 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-top.png new file mode 100644 index 0000000..639dff5 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-left_fragments.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-left_fragments.png new file mode 100644 index 0000000..49bea95 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-left_fragments.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-org_reuse.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-org_reuse.png new file mode 100644 index 0000000..aadbfcd Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-org_reuse.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-skill_template.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-skill_template.png new file mode 100644 index 0000000..13bc35d Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-skill_template.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-slogan.png new file mode 100644 index 0000000..dcaa2af Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-title.png new file mode 100644 index 0000000..2f2cc0e Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p06-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-app_window.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-app_window.png new file mode 100644 index 0000000..6d398c9 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-app_window.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-beaver.png new file mode 100644 index 0000000..5990a77 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-bottom.png new file mode 100644 index 0000000..d06fdb0 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-main.png new file mode 100644 index 0000000..643ec82 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-top.png new file mode 100644 index 0000000..a7cc8d5 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-blackbox.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-blackbox.png new file mode 100644 index 0000000..44d44f8 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-blackbox.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-right_stack.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-right_stack.png new file mode 100644 index 0000000..c6b13aa Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-right_stack.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-slogan.png new file mode 100644 index 0000000..b390498 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-title.png new file mode 100644 index 0000000..d760a6a Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p07-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-beaver.png new file mode 100644 index 0000000..4e8fd4d Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-bottom.png new file mode 100644 index 0000000..cdaf883 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-main.png new file mode 100644 index 0000000..292d8a5 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-top.png new file mode 100644 index 0000000..5063903 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-left_flow.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-left_flow.png new file mode 100644 index 0000000..cedfeef Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-left_flow.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-main_flow.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-main_flow.png new file mode 100644 index 0000000..1c5ba11 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-main_flow.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-right_value.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-right_value.png new file mode 100644 index 0000000..5c6e0c4 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-right_value.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-slogan.png new file mode 100644 index 0000000..d0141be Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-title.png new file mode 100644 index 0000000..8f3fdac Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p08-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-bottom.png new file mode 100644 index 0000000..7643e1d Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-main.png new file mode 100644 index 0000000..0a6d13d Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-top.png new file mode 100644 index 0000000..97f4dda Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-center_runtime.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-center_runtime.png new file mode 100644 index 0000000..4b73720 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-center_runtime.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-desc.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-desc.png new file mode 100644 index 0000000..f1f0239 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-desc.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-footer.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-footer.png new file mode 100644 index 0000000..66cbed6 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-footer.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-left_panels.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-left_panels.png new file mode 100644 index 0000000..caf1f3c Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-left_panels.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-right_panels.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-right_panels.png new file mode 100644 index 0000000..a3ca933 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-right_panels.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-subtitle.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-subtitle.png new file mode 100644 index 0000000..814d003 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-subtitle.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-title.png new file mode 100644 index 0000000..06cd3cb Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p09-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-beaver.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-beaver.png new file mode 100644 index 0000000..78f8dee Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-beaver.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-bottom.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-bottom.png new file mode 100644 index 0000000..2a1b11f Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-bottom.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-main.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-main.png new file mode 100644 index 0000000..8f0e526 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-main.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-top.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-top.png new file mode 100644 index 0000000..cf092bb Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bg-top.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bottom_cards.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bottom_cards.png new file mode 100644 index 0000000..2c30708 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-bottom_cards.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-hub.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-hub.png new file mode 100644 index 0000000..7093635 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-hub.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-left_stack.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-left_stack.png new file mode 100644 index 0000000..d740f78 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-left_stack.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-right_stack.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-right_stack.png new file mode 100644 index 0000000..c402c6f Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-right_stack.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-slogan.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-slogan.png new file mode 100644 index 0000000..159dc82 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-slogan.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-title.png b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-title.png new file mode 100644 index 0000000..fe88e07 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/assets/exact/p10-title.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/comparison-contact.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/comparison-contact.jpg new file mode 100644 index 0000000..ffa9ab6 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/comparison-contact.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-01-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-01-compare.jpg new file mode 100644 index 0000000..c2ca557 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-01-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-02-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-02-compare.jpg new file mode 100644 index 0000000..243a0a7 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-02-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-03-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-03-compare.jpg new file mode 100644 index 0000000..faa93bb Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-03-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-04-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-04-compare.jpg new file mode 100644 index 0000000..90054a9 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-04-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-05-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-05-compare.jpg new file mode 100644 index 0000000..5169561 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-05-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-06-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-06-compare.jpg new file mode 100644 index 0000000..f7b7999 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-06-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-07-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-07-compare.jpg new file mode 100644 index 0000000..2fbf182 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-07-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-08-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-08-compare.jpg new file mode 100644 index 0000000..70beff4 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-08-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-09-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-09-compare.jpg new file mode 100644 index 0000000..a5de0cb Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-09-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-10-compare.jpg b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-10-compare.jpg new file mode 100644 index 0000000..e10b517 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/comparison/page-10-compare.jpg differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/index.html b/docs/presentations/参考资料/参考图片-html-ppt/index.html new file mode 100644 index 0000000..09c1f87 --- /dev/null +++ b/docs/presentations/参考资料/参考图片-html-ppt/index.html @@ -0,0 +1,1200 @@ + + + + + + Beaver 高保真网页画册复刻 + + + + + + + + + + + + + + + + + + + + + + + + B + + + + + + + + + +
+
+
+ +

多 Channel 随身连接

+
让 Beaver 进入用户真实工作入口
+
+
+

01微信 / 飞书接入

无缝连接主流沟通工具,在熟悉的环境中高效协作。

+

02小终端实时对话

M5Stack 等便携终端接入,随时随地与 Beaver 对话。

+

03A2UI 设备扩展

支持智能眼镜等 A2UI 设备,自然交互,拓展应用边界。

+

04多端任务同步

跨端同步上下文与任务,无缝衔接,持续推进工作。

+
+
Beaver Runtime
+ +
WEB

Web

浏览器访问,随时使用

+
CHAT

微信 / WeChat

在微信中直接对话

+
APP

移动端

移动办公,高效随行

+
飞书

飞书 / Feishu

连接工作流与团队协同

+
M5

M5Stack 小终端

硬件终端,实时对话

+
A2

智能眼镜

语音交互,解放双手

+
+
+
+
+
+
+

同一个 Agent Runtime,多种交互入口

+
让 Beaver 不只停留在网页,而是连接沟通工具、随身终端和智能设备。
+
+
多人入口覆盖
满足不同场景需要
安全可控
企业级权限与数据保护
开放集成
标准 API,灵活接入
一致体验
同一能力,处处可用
可观测可管理
全渠道接入与使用分析
+
+
+ +
+ +

可追溯文件系统

+
让 Agent 产物成为可管理的企业资产
+

1MiniO 私有化存储

  • 企业自建,数据不出域
  • 高可用、高性能、无限扩展
  • S3 兼容,生态丰富
+

2用户文件权限隔离

  • 多租户隔离,用户级权限
  • 细粒度访问控制(RBAC)
  • 加密存储,安全可控
+

3文件修改可追溯

  • 完整版本历史与变更记录
  • 操作留痕,审计可靠
  • 谁改了什么,一目了然
+

4支持版本回滚扩展

  • 一键回滚到任意版本
  • 保留历史版本,可扩展存储策略
+ +
+

MinIO 私有化存储

+

企业文件仓库

+
+
UP

uploads

用户上传文件

+
TASK

tasks

任务与配置文件

+
</>

outputs

Agent 产出文件

+
TEAM

shared

共享与协作文件

+
+
权限隔离
加密存储
多副本
高可用
S3 兼容
+
+

Agent 与文件交互

GEN

生成

写入 outputs 或其他目录

READ

读取

从仓库读取文件作为上下文

EDIT

修改

产生新版本并记录变更

+

安全边界

网络隔离
访问控制
操作审计
合规治理

+

版本历史(以文件报告.md 为例)

MD

报告.md
所在路径:outputs/

v1.0

创建
2025-05-18 10:30
Alice

v1.1

修改图表数据
2025-05-18 14:22
Bob

v2.0

新增结论章节
2025-05-19 09:15
Carol

更多
版本...

+
让 Agent 生成、读取和修改的文件都可追踪、可恢复、可治理
+
+ +
+

可信协议与工具接入

+
不只是调用工具,而是安全、可控地调用工具
+

1 MCP 工具统一接入

统一接入 MCP Server,无缝连接各类工具与服务。

+
Bot

2 A2A 协议扩展支持

支持 A2A 协议,打通 Agent 之间协作与能力扩展。

+

3 工具调用权限审核

细粒度权限控制与策略审核,全链路审计。

+

4 账号密码外置托管

凭据外置托管,密钥不入 Runtime,更安全可靠。

+

MCP Server

CLOUD
+

A2A Agent

A2A
+

安全控制层

AuthZ

权限鉴权

Token

令牌管理

Audit

操作审计

+
+
BEAVER
RUNTIME
+ +
+

ERP

+

CRM

+

Database

+

Internal API

+

Workflow

+

其他企业服务

+

外置凭据

账号密码外置托管

+
+
让 Agent 接入企业系统时可鉴权、可审计、可控权。
+
+ +
+

长期记忆与用户画像

+
让 Agent 持续理解用户、任务和业务背景
+
多模态

多模态记忆接入

整合文字、图片、语音、文件等信息

+

历史任务经验复用

沉淀任务历史,复用经验,提升效率

+

用户画像持续沉淀

持续更新用户画像,越用越懂用户

+

随身设备记忆同步

多设备记忆无缝同步

+

用户画像

兴趣行为技能偏好
  • 个性化推荐
  • 需求预测
  • 偏好适配
+

组织知识

业务SOP实践案例
  • 知识检索增强
  • 减少重复劳动
  • 提升决策质量
+
+ +

记忆中枢

长期上下文

+
T文字
+
图片
+
语音
+
文件
+
...对话记录
+
任务历史
+
设备记忆
+
用户偏好
+
+
让 Agent 从“临时助手”变成“理解用户的长期工作伙伴”。
+
+ +
+

核心优势四:结果可验证

+
不靠感觉调 Prompt,而靠回放评估优化
+

评估与验证

评估报告

基础 0.77候选 0.78+0.01覆盖 31%替代 69%medium

修订对比

当前版本 v0001
− clarify
− delegate
− send_message
− spawn
− todo

草稿版本 v0002
✓ clarify
✓ delegate
✓ send_message
✓ spawn
✓ todo

安全报告

未发现高风险问题,已通过安全检查。

+

评估优化闭环

+
+

数据驱动
持续进化

+

历史任务 Replay

真实任务回放

+

Skill 版本对比

多版本并行对比

+

用户反馈沉淀

记录评分与反馈

+

失败案例复盘

定位原因,制定规则

+

版本记录

v0002 当前

根据用户反馈与评估结果优化,增强检索逻辑。

v0001 published

新增 tools 获取与 user_files 工具。

v0000 draft

初始版本,基础工具与流程搭建。

+
基础得分0.77
候选得分0.78
提升+0.01
置信度medium
评估样本236
通过率100%
+ +
让 Agent 能力持续进化,而不是一次性试错。
+
+ +
+

核心优势一:经验可复用

+

个人经验碎片

+
+
Prompt
———
+
备忘笔记
+
处理逻辑
$ ...
+
经验总结
+
+
+
清单

固化业务 SOP

  • 标准流程
  • 步骤清单
  • 角色分工
+

约束输出质量

  • 输出结构化
  • 质量校验
  • 质量门禁
+
DOC

SKILL TEMPLATE

企业任务模板

名称 —————————
目标 —————————
适用场景 ———————
输入 —————————
流程 — □ → ◇ → □
输出 —————————
验收标准 ———————

+
API

规范工具调用

  • 工具白名单
  • 参数规范
  • 调用编排
+
版本

支持版本演进

v1.0v2.0v3.0

  • 版本管理
  • 变更追踪
  • 平滑升级
+
+

组织能力沉淀与复用

+

企业级能力库

SKILL TEMPLATE

+
+
产品

产品团队

运营

运营团队

研发

研发团队

财务

财务团队

+
+

跨部门复用 · 快速复制 · 持续优化

+
把个人经验沉淀为组织能力
+
+ +
+

核心优势三:过程可观测

+
Agent 在做什么,全程可见
+

黑盒时代

过程不透明,结果难追溯

+
?
+
+
+
Beaver对话Task通知技能文件工具智能体Outlook市场
+
+

我想要AI去网站查一下MGM和...

+

任务执行流程

+
+

任务已创建

已完成

+

选择 Skill

已完成

+

收集官方来源

已完成

+

提取财务指标

进行中

+

验证数据

等待中

+

生成图表报告

等待中

+
+
+
+

Agent Team (DAG)

+
planner
100%
collector
100%
extractor
60%
validator
0%
report
0%
+

工具调用 (Tool Calls)

+
Agent工具名状态耗时
main-agentweb_search已完成23.4s
collectorread_url已完成18.7s
extractorextract_metrics进行中--
validatorvalidate_metrics等待中--
+
+

Workspace 文件

文件名类型状态
official_sources.jsonJSON完成
mgm_financials.csvCSV完成
chart_report.mdMD进行中
chart.pngPNG进行中

本轮结果(摘要)

已完成网页财务信息分段提取,正在生成图表报告。

+
+
+
+

Skill 选择可见

依据充分,透明可控

+
</>

工具调用可追踪

输入输出、耗时可见

+

中间文件可复查

版本可追溯,便于审查

+

执行状态可解释

结果可信,过程可控

+ +
把 Agent 黑盒变成透明工作流
+
+ +
+

核心优势二:复杂任务可拆解

+
Agent Team 不是角色聊天,而是任务协作
+

复杂任务拆解流程

01

用户提出

复杂任务需求

02

拆解编排

生成子任务 DAG

03

并行执行

多节点协同完成

04

汇总验证

输出可验证结果

+
+
用户任务
+
信息收集完成
+
数据分析进行中
+
检索验证待验证
+
报告生成进行中
+
结果汇总 完成
+
最终输出 完成
+
+ +

核心价值

1

按任务动态组队

2

子任务 DAG 编排

3

并行执行与结果汇总

4

关键步骤可验证

+
让复杂任务从“模型硬答”变成“流程执行”
+
+ +
+

Beaver 产品定位

+
面向复杂任务的 Agent Runtime
+

Beaver 将 Skill、Tool、Workspace、Agent Team 和 Eval 统一起来,构建可执行、可追踪、可验证、可复用的企业 Agent 工作空间。

+

复杂任务编排

+

运行时观测

实时日志与追踪

● ● ● ●

+
+ +
Skill
+
Tool
+
Agent Team
+
Eval
+
Workspace
+

知识与资产

文档
代码
数据
API
模型
...
+

结果验证与评估

柱图
折线
环图
72%48%85%60%
+
可执行任务可执行,结果可交付
可追踪全链路追踪,过程可观测
可验证结果可验证,质量可保障
可复用资产可复用,能力可沉淀
+
+ +
+

SkillHub 能力治理

+
把个人 skill 升级为组织级能力市场
+

模型自学习经验

训练与推理数据
自我迭代结果
技巧与策略沉淀

+

历史任务

任务流程与步骤
成功方案与模板
复用率与效果沉淀

+
反馈

用户反馈

用户评价与打分
问题与建议
改进方向洞察

+ +

SkillHub

企业能力治理与分发平台

审核

发布

回滚

方块

版本与治理

v1.0 草稿v1.1 待审核v1.2 已批准v1.3 已发布v0.x 已回滚

+
+

企业能力统一分发与复用

+
Group

团队 Group

多团队共享能力
权限继承与隔离
复用与协作提效

+
Agent

Agent

Agent 调用执行
能力组合编排
智能体生态复用

+
Work

Workspace

项目空间复用
流程集成调用
沉淀为标准资产

+
+

01Skill 版本与审核

多版本管理与生命周期

+

02企业能力统一分发

标准化上架与目录化管理

+

03Group 级权限控制

细粒度权限与继承

+

04Beaver 运行时联动

运行时动态获取最新能力

+
+
让企业经验可以被审核、分发、复用和持续优化
+
+
+
+
+ + 按 E 切换;编辑模式下 F12 可直接改 DOM 文本 +
+
鼠标滚轮 / ↑ ↓ / PageUp PageDown 翻页
+
1 / 10
+ + + + diff --git a/docs/presentations/参考资料/参考图片-html-ppt/pixel-reference.html b/docs/presentations/参考资料/参考图片-html-ppt/pixel-reference.html new file mode 100644 index 0000000..9049f2c --- /dev/null +++ b/docs/presentations/参考资料/参考图片-html-ppt/pixel-reference.html @@ -0,0 +1,192 @@ + + + + + + Pixel Reference 对比验收 + + + +
+ + + + + + 58% + 用于验收对比,最终展示页不引用整张参考图。 +
+
+
+
+ 参考图 + + Reference + Recreated Overlay +
+ +
+ + + diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-01.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-01.png new file mode 100644 index 0000000..f27f4ed Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-01.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-02.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-02.png new file mode 100644 index 0000000..75e28f8 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-02.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-03.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-03.png new file mode 100644 index 0000000..6a8f9bf Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-03.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-04.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-04.png new file mode 100644 index 0000000..ee68f34 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-04.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-05.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-05.png new file mode 100644 index 0000000..a54d8bf Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-05.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-06.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-06.png new file mode 100644 index 0000000..440f8b8 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-06.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-07.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-07.png new file mode 100644 index 0000000..51674d8 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-07.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-08.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-08.png new file mode 100644 index 0000000..2f336cd Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-08.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-09.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-09.png new file mode 100644 index 0000000..7cf0add Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-09.png differ diff --git a/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-10.png b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-10.png new file mode 100644 index 0000000..2c580a5 Binary files /dev/null and b/docs/presentations/参考资料/参考图片-html-ppt/screenshots/page-10.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_23_46.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_23_46.png new file mode 100644 index 0000000..de93f1b Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_23_46.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_23_57.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_23_57.png new file mode 100644 index 0000000..a2a65d0 Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_23_57.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_02.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_02.png new file mode 100644 index 0000000..9c771b1 Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_02.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_07.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_07.png new file mode 100644 index 0000000..c0c8b38 Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_07.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_20.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_20.png new file mode 100644 index 0000000..1b01336 Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_20.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_25.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_25.png new file mode 100644 index 0000000..b7c11a0 Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_25.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_39.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_39.png new file mode 100644 index 0000000..e84dc85 Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_39.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_46.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_46.png new file mode 100644 index 0000000..65d808f Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_46.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_56.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_56.png new file mode 100644 index 0000000..4759034 Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_24_56.png differ diff --git a/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_26_07.png b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_26_07.png new file mode 100644 index 0000000..0323a27 Binary files /dev/null and b/docs/presentations/参考资料/参考图片/ChatGPT Image 2026年6月25日 16_26_07.png differ diff --git a/docs/superpowers/examples/agent-team-flow-logic-visualization.html b/docs/superpowers/examples/agent-team-flow-logic-visualization.html new file mode 100644 index 0000000..1d54dee --- /dev/null +++ b/docs/superpowers/examples/agent-team-flow-logic-visualization.html @@ -0,0 +1,1526 @@ + + + + + + Beaver Agent Team Explained + + + +
+ + + + + + + + + + + +
+
+

Agent Team 从输入到结果,到底谁负责什么

+

这版按“小白也能看懂”重做:先看成功例子,再看坏例子;每一步都标明输入、负责人、输出。

+
+
+ 输入/选择 + 正确执行 + 校验/降级 + 错误点 + 汇总输出 +
+
+ + + +
+
+
+
+
+

先记住一句话

+ 责任边界 +
+
+
+

用户

+

只说想要什么。

+
例如:做 MGM/Galaxy 财报对比
+
+
+

Skill Resolver

+

负责选 Skill。

+
选中 mgm-galaxy...
+
+
+

Main Agent

+

负责决定是否用 Team,并提交 Team 参数。

+
调用 run_agent_team
+
+
+

TaskExecutionPlanner

+

负责把 JSON 变成 ExecutionGraph。

+
校验 / 修复 / fallback
+
+
+

Scheduler

+

负责按 DAG 调度子 Agent。

+
谁先跑 / 谁等谁
+
+
+

Final Synthesis

+

负责只总结已经完成的 Team 结果。

+
不能再说“我将启动 Team”
+
+
+
+ +
+
+

成功案例:MGM/Galaxy

+ Skill 有模板 +
+
+
读到的 Skill
+
+ mgm-galaxy-financial-chart-report-safe + + 里面有 beaver-team-template + + DAG 已经写好 +
+
+
+
Main Agent 应输出
+
+ 第一轮不要先 web_search + + 调用 run_agent_team + + 传 task-only nodes +
+
+
+
谁加载 DAG
+
+ Main Agent 不直接加载 + + TaskExecutionPlanner 适配 + + Scheduler 执行 +
+
+
+
+

collect_official_sources

+

收集官方来源。

+
应绑定搜索 Skillweb_search/web_fetch
+
+
+

extract_financial_metrics

+

从来源提取指标。

+
depends_on collect
+
+
+

validate_metrics

+

校验周期、币种、单位。

+
no tools
+
+
+

generate_chart_report

+

生成 Markdown 报告。

+
no chart image claim
+
+
+
+ MGM Skill 自身有没有问题? +

DAG 结构是对的。真正可能有问题的是 use_skill: "web-operation" 如果环境里没有这个 exact Skill,会走 fallback。更稳的写法是绑定当前实际存在的搜索 Skill,或者只保留 skill_query

+
+
+ +
+
+

坏案例:韩国队任务

+ Skill 没有 Team 模板 +
+
+
读到的 Skill
+
+ multi-search-engine + + 只是搜索指导 + + 没有 beaver-team-template +
+
+
+
Planner 产出的坏 DAG
+
+ strategy=parallel + + collect + analyze_tactics + analyze_players + analyze_media +
+
+
+
实际加载后
+
+ 4 个节点同时 ready + + 分析节点没等 collect + + 没有上游 evidence +
+
+
+
+

collect 节点

+

有搜索 Skill,有工具,真的查了。

+
tool_count > 0
+
+
+

analyze 节点

+

说“基于搜索结果”,但没有依赖 collect。

+
容易模拟容易空写
+
+
+
+ +
+
+

坏 DAG 的真实形状

+ 问题在图,不在总结 +
+
+
+
Main Agent / Planner 错误输出
+
{
+  "strategy": "parallel",
+  "nodes": [
+    {"node_id": "collect", "task": "搜索韩国队表现"},
+    {"node_id": "analyze_tactics", "task": "基于搜索结果分析战术"},
+    {"node_id": "analyze_players", "task": "基于搜索结果分析球员"},
+    {"node_id": "analyze_media", "task": "基于搜索结果分析媒体"}
+  ]
+}
+
+
+
应该修成这样
+
{
+  "strategy": "dag",
+  "nodes": [
+    {"node_id": "collect", "task": "搜索韩国队表现"},
+    {"node_id": "analyze_tactics", "depends_on": ["collect"]},
+    {"node_id": "analyze_players", "depends_on": ["collect"]},
+    {"node_id": "analyze_media", "depends_on": ["collect"]},
+    {"node_id": "synthesize", "depends_on": [
+      "analyze_tactics", "analyze_players", "analyze_media"
+    ]}
+  ]
+}
+
+
+
+
+
+ +
+
+
+
+

如果 Main Agent 要输出 Team,它到底该输出什么

+ run_agent_team 参数 +
+

Main Agent 不输出“角色”。它输出 run_agent_team 的 JSON 参数:strategy + nodes + depends_on

+
+ +
+

类型 1:顺序流水线

A → B → C
+
+

collect

先拿资料。

+

extract

再提取。

+

report

最后输出。

+
+
+
Main Agent 应输出
+
{
+  "strategy": "dag",
+  "nodes": [
+    {"node_id": "collect", "task": "收集资料"},
+    {"node_id": "extract", "task": "提取指标", "depends_on": ["collect"]},
+    {"node_id": "report", "task": "生成报告", "depends_on": ["extract"]}
+  ]
+}
+
+
+ +
+

类型 2:真正并行

互不依赖
+
+

source_a

独立来源 A。

+

source_b

独立来源 B。

+

source_c

独立来源 C。

+
+
+
Main Agent 应输出
+
{
+  "strategy": "parallel",
+  "nodes": [
+    {"node_id": "source_a", "task": "查官方公告"},
+    {"node_id": "source_b", "task": "查媒体报道"},
+    {"node_id": "source_c", "task": "查数据网站"}
+  ]
+}
+
+
+ +
+

类型 3:先 collect,再并行分析

最常见
+
+

collect

统一事实池。

+
+
+

analyze_a

角度 A。

+

analyze_b

角度 B。

+

analyze_c

角度 C。

+
+
+

synthesize

合并。

+
+
+
Main Agent 应输出
+
{
+  "strategy": "dag",
+  "nodes": [
+    {"node_id": "collect", "task": "收集事实材料"},
+    {"node_id": "analyze_a", "depends_on": ["collect"]},
+    {"node_id": "analyze_b", "depends_on": ["collect"]},
+    {"node_id": "analyze_c", "depends_on": ["collect"]},
+    {"node_id": "synthesize", "depends_on": ["analyze_a", "analyze_b", "analyze_c"]}
+  ]
+}
+
+
+ +
+

类型 4:带校验门的报告

finance 常用
+
+

collect

来源。

+

extract

指标。

+

validate

校验。

+

report

报告。

+
+
+
Main Agent 应输出
+
{
+  "strategy": "dag",
+  "nodes": [
+    {"node_id": "collect", "required_evidence": ["tool_result", "url"], "block_downstream_on_partial": true},
+    {"node_id": "extract", "depends_on": ["collect"], "block_downstream_on_partial": true},
+    {"node_id": "validate", "depends_on": ["extract"], "requested_tools": []},
+    {"node_id": "report", "depends_on": ["validate"], "requested_tools": []}
+  ]
+}
+
+
+ +
+

输出规则:不要输出这些

错误格式
+
+
×

不要输出 agent.role = researcher / writer / reviewer

+
×

不要把“基于搜索结果”的节点放到 parallel 里。

+
×

不要让最终节点再调用工具,除非明确需要。

+
+
+
+
+ +
+
+
+
+

Skill 里面的 demo 长什么样

+ 简化版 +
+
+
mgm-galaxy-financial-chart-report-safe.SKILL.md
+
---
+name: mgm-galaxy-financial-chart-report-safe
+tools:
+  - web_search
+  - web_fetch
+---
+
+```beaver-team-template
+{
+  "strategy": "dag",
+  "nodes": [
+    {
+      "node_id": "collect_official_sources",
+      "task": "Collect official MGM and Galaxy financial disclosures.",
+      "use_skill": "web-operation",
+      "skill_query": "official financial disclosure web research",
+      "requested_tools": ["web_search", "web_fetch"],
+      "required_evidence": ["tool_result", "url"],
+      "block_downstream_on_partial": true
+    },
+    {
+      "node_id": "extract_financial_metrics",
+      "task": "Extract comparable metrics from collected official sources.",
+      "depends_on": ["collect_official_sources"],
+      "requested_tools": ["web_fetch"],
+      "block_downstream_on_partial": true
+    },
+    {
+      "node_id": "validate_metrics",
+      "task": "Validate period, currency, unit, and source consistency.",
+      "depends_on": ["extract_financial_metrics"],
+      "requested_tools": []
+    },
+    {
+      "node_id": "generate_chart_report",
+      "task": "Generate Markdown table and chart-ready data.",
+      "depends_on": ["validate_metrics"],
+      "requested_tools": []
+    }
+  ]
+}
+```
+
+
+ +
+

加载链路:谁把 Skill 变成 DAG

实际责任人
+
+
1. Skill Catalog
+
+ 读 SKILL.md解析 beaver-team-templateSkillRecord.team_template +
+
+
+
2. Context Builder
+
+ SkillRecordSkillContext.team_template给 Planner / Main Agent 看 +
+
+
+
3. Main Agent
+
+ 看到 primary template选择 run_agent_team传 task-only JSON +
+
+
+
4. Planner
+
+ 校验 JSON构造 ExecutionGraph每个 node 是 generic worker +
+
+
+
5. Scheduler
+
+ 按 depends_on 找 ready node创建 DelegationEnvelopeLocalAgentRunner 执行 +
+
+
+ +
+

每个子 Agent 怎么选 Skill

优先级
+
+
+

第一优先:use_skill

+

精确绑定。比如 use_skill: "web-operation"

+
+
+

第二优先:skill_query

+

exact 找不到,就按 query 动态找。

+
+
+

第三优先:node task

+

query 也不行,就用节点任务文本找。

+
+
+

最后兜底:ephemeral guidance

+

没有合适 Skill,也不 hard fail,继续跑。

+
+
+
+ 为什么 MGM 仍可能不稳? +

如果 Steven 里没有 web-operation,collect 节点不会 exact bind,只能 fallback。DAG 没错,但 Skill 绑定名需要和真实环境对齐。

+
+
+ +
+

复杂 DAG 怎么办:不是只能一条线

多阶段 DAG
+
+
+

collect_base

+

先拿共同材料。

+
+
+
+

analyze_market

市场角度。

+

analyze_finance

财务角度。

+

analyze_risk

风险角度。

+
+
+
+

merge_findings

合并。

+

validate_claims

校验。

+

final_report

输出。

+
+
+
+
复杂 DAG 的关键不是 strategy 名字,而是 depends_on
+
[
+  {"node_id": "collect_base"},
+  {"node_id": "analyze_market", "depends_on": ["collect_base"]},
+  {"node_id": "analyze_finance", "depends_on": ["collect_base"]},
+  {"node_id": "analyze_risk", "depends_on": ["collect_base"]},
+  {"node_id": "merge_findings", "depends_on": ["analyze_market", "analyze_finance", "analyze_risk"]},
+  {"node_id": "validate_claims", "depends_on": ["merge_findings"]},
+  {"node_id": "final_report", "depends_on": ["validate_claims"]}
+]
+
+
+
+
+ +
+
+
+

先别记术语,只看四类错误

从左到右定位
+
+
+

1. 图错了

+

DAG 依赖关系错。

+
韩国队 case
+
+
+

2. Skill 没绑上

+

use_skill 不存在,工具没装出来。

+
web-operation miss
+
+
+

3. 节点输出不合格

+

没证据、raw tool call、模拟搜索。

+
partial / failed
+
+
+

4. 最终总结跑偏

+

Team 已完成,但总结又说“我将启动 Team”。

+
fake final
+
+
+
+ +
+

当前已经有的机制

已有
+
+
+
问题
+
系统怎么处理
+
结果
+
+
+
Planner JSON 格式错
+
repair 一次
+
还错就 fallback single
+
+
+
use_skill 找不到
+
fallback 到 skill_query
+
不直接 hard fail
+
+
+
工具不在 allowed_tool_names
+
AgentLoop 过滤 + ToolExecutor 二次拒绝
+
工具不可用
+
+
+
required_evidence 缺失
+
节点 partial / success=false
+
按 block 设置决定是否阻断
+
+
+
finalized 还是 raw tool call
+
不能标 succeeded
+
节点 failed
+
+
+
+ +
+

还应该补的机制

下一步重点
+
+
+
问题
+
应该怎么处理
+
为什么
+
+
+
“基于搜索结果”的节点没有 depends_on
+
Planner repair DAG
+
坏图不能执行
+
+
+
parallel 用错
+
改成 dag / safe sequence
+
防止分析节点空跑
+
+
+
节点声称搜索但 tool_count=0
+
判 partial 或 retry node
+
防止“模拟搜索”成功
+
+
+
final synthesis 输出“我将启动 Team”
+
判失败并重试一次总结
+
Team 已经跑完,不能再规划
+
+
+
+ +
+

一条错误从哪里冒出来,就在哪里处理

定位图
+
+
DAG 生成阶段
+
+ 字段错repairfallback + 依赖错repair DAG禁止执行坏图 +
+
+
+
节点执行阶段
+
+ Skill missskill_queryephemeral guidance + 工具 deniednode failed +
+
+
+
证据判断阶段
+
+ required_evidence 满足succeeded + 缺证据partial + block=true阻断下游 +
+
+
+
最终输出阶段
+
+ required 节点全成功complete + required 节点有 partial/failedincomplete + 输出又开始规划应该重试总结 +
+
+
+
+
+ +
+
+
+

Swarms 是怎么工作的

先选工作流,再跑 Agent
+
+

1. 先定义 Agent

每个 Agent 有名字、系统提示词、工具、模型。

Agent = worker
+

2. 再选择 Workflow

顺序、并行、图、群聊、层级等。

workflow first
+

3. 明确连接关系

谁先跑、谁依赖谁、谁把结果给谁。

edges / flow
+

4. Runtime 执行

按 workflow 调度,不靠模型临场猜顺序。

orchestration
+
+
+ +
+

Swarms 常见多步形态

workflow shapes
+
+
类型
关系
适合
+
SequentialWorkflow
A → B → C
一步接一步的流水线
+
ConcurrentWorkflow
A ∥ B ∥ C
互不依赖的并行任务
+
AgentRearrange
agent1 → agent2 → agent3
用字符串表达 agent 流向
+
GraphWorkflow
节点 + 边
复杂 DAG,拓扑调度
+
Mixture / Group / Hierarchical
多个 Agent 协作或分层
讨论、投票、管理者分派
+
+
+ +
+

多步怎么表达

不是一句 prompt
+
+

Agent A

收集事实。

+
+
+

Agent B

分析视角 1。

+

Agent C

分析视角 2。

+

Agent D

分析视角 3。

+
+
+

Agent E

合并总结。

+
+
关键点

Swarms 的强项不是“提示词更神”,而是把多 Agent 的关系做成显式 workflow。模型负责单步任务,框架负责调度关系。

+
+ +
+

Swarms 思路对 Beaver 的启发

拿 workflow,不拿固定角色
+
+

保留 Beaver 的 generic worker,不必变成 researcher/writer/reviewer。

+

学习 Swarms 的显式 workflow:sequence / parallel / graph / hierarchy。

+
!

不要让 LLM 自由猜 DAG;让它选择 workflow shape 或填槽。

+

Runtime 根据结构执行,LLM 只负责节点内完成任务。

+
+
+
+
+ +
+
+
+

我们和 Swarms 的差别

现在的问题在这里
+
+
维度
Swarms
Beaver 当前
+
工作流来源
开发者显式选择 workflow
Planner / Main Agent 临场生成 JSON
+
Agent 定义
通常是具名 Agent + 角色/工具
generic worker,role 为空
+
依赖关系
flow / edge 是一等公民
depends_on 只是字段,提示不够强
+
复杂多步
GraphWorkflow / AgentRearrange
让 LLM 自己写 DAG,容易写错
+
Skill
不是核心概念
Skill 是 Beaver 的优势:可提供模板、工具、约束
+
+
+ +
+

Beaver 做对的地方

保留
+
+

Skill-first

用户不用懂 Agent 架构,Skill 可以携带业务模板。

+

Generic worker

不把产品锁死在固定 researcher/writer 角色。

+

工具安全上限

allowed_tool_names 可以限制节点越权。

+

Evidence gate

节点是否完成可以和证据绑定。

+
+
+ +
+

Beaver 当前缺的地方

要补
+
+

Workflow shape 不明确

没有先选 sequence / collect-then-parallel / graph。

+

depends_on 语义没教

prompt 只列字段,没有规则。

+

坏 DAG 没被拦

parallel 用错也会执行。

+

final synthesis 不够硬

Team 已跑完还可能输出“我要启动 Team”。

+
+
+ +
+

正确定位

Beaver ≠ 复制 Swarms
+
+
不要变成
+
固定角色 Agent 库+用户手写 workflow+没有 Skill 语义
+
+
+
应该变成
+
Skill Template+Generic Worker+显式 Workflow Shape+Runtime 校验 DAG
+
+
+
+
+ +
+
+
+

我们现在明确要做什么

本地 MCP Workflow 工具
+
+

1. Main Agent

不再手写复杂 DAG。它只选择一个 workflow 工具。

Sequential / Concurrent / Graph...
+

2. 本地 MCP Server

永远本地加载,专门暴露 Team Workflow tools。

local_team_workflow_mcp
+

3. Workflow Tool

每个 workflow 单独一个 Python 实现文件。

sequential.py / graph.py...
+

4. Beaver Runtime

工具内部生成现有 ExecutionGraph,再交给 Scheduler 跑。

不新增 Team runtime
+
+
+ +
+

最终调用模型

workflow = tool
+
+
Agent 看到
+
SequentialWorkflowConcurrentWorkflowGraphWorkflowMixtureOfAgents
+
+
+
Agent 填写
+
task+agents[]+每个 agent 的 instruction / skill_query
+
+
+
工具负责
+
生成 depends_on校验结构启动 Team
+
+
关键变化

不是一个 run_swarm_workflow(architecture=...) 大工具;而是每种 architecture 自己就是一个工具。这样工具 schema 更短,LLM 更容易选对。

+
+ +
+

建议工具清单

MCP tools
+
+
工具
Agent 必填什么
系统怎么连
+
SequentialWorkflow
agents[]
按顺序 A → B → C
+
ConcurrentWorkflow
agents[]
全部并行,无依赖
+
MixtureOfAgents
agents[], aggregator
并行专家 → 聚合者
+
AgentRearrange
agents[], flow
解析 flow 字符串
+
GraphWorkflow
agents[], edges[], output_agent
严格按 edges 执行
+
GroupChat
agents[], rounds
多轮讨论后汇总
+
HierarchicalSwarm
manager, agents[]
manager 分派和汇总
+
+
+ +
+

完整运行链路

保持 Beaver 现有 runtime
+
+
工具加载
+
config loaderlocal_team_workflow_mcpMCPToolWrapperAgentLoop tools
+
+
+
用户任务
+
Main Agent选择 Workflow tool填写 agentstool 生成 ExecutionGraph
+
+
+
执行
+
TeamServiceSchedulerLocalAgentRunnerEvidence / synthesis
+
+
+
+
+ +
+
+
+

代码放哪里、改哪些文件

implementation map
+
+

配置层

把 workflow MCP 加入永远加载的本地 MCP 列表。

foundation/config/loader.py
+

MCP 层

tools_server 增加 team_workflow category。

interfaces/mcp/tools_server.py
+

Workflow 层

每个 workflow 一个 Python 文件。

beaver/team_workflows/*.py
+

Runtime 层

复用 ExecutionGraph / Scheduler / LocalAgentRunner。

不新增 runtime
+
+
+ +
+

新增目录

workflow implementations
+
+
new package
+
app-instance/backend/beaver/team_workflows/
+  __init__.py
+  base.py
+  mcp_tools.py
+  sequential.py
+  concurrent.py
+  mixture_of_agents.py
+  agent_rearrange.py
+  graph.py
+  group_chat.py
+  hierarchical.py
+
+
设计原则

base.py 放公共 WorkerSpec、schema 解析、ExecutionNode 构造、图校验;每个 workflow 文件只实现自己的连线规则。

+
+ +
+

修改现有文件

integration points
+
+
文件
改动
+
foundation/config/loader.py
新增 local_team_workflow_mcp,永远本地加载
+
interfaces/mcp/tools_server.py
新增 team_workflow category,返回 workflow tools
+
tools/mcp/wrapper.py
原则上不改;继续包装 MCP tools
+
tools/builtins/agent_team.py
原则上不改;workflow tool 复用同一 TeamService 路径
+
coordinator/*
原则上不改;继续执行 ExecutionGraph
+
+
+ +
+

每个 workflow 文件怎么做

file responsibilities
+
+
文件
职责
连线规则
+
sequential.py
SequentialWorkflow
agents[i] 依赖 agents[i-1]
+
concurrent.py
ConcurrentWorkflow
所有 agents 无依赖并行
+
mixture_of_agents.py
MixtureOfAgents
所有专家 → aggregator
+
agent_rearrange.py
AgentRearrange
解析 flow,再生成边
+
graph.py
GraphWorkflow
edges[] 必填,严格校验无环和引用
+
group_chat.py
GroupChat
v1 可先不做,或 rounds → 顺序轮次
+
hierarchical.py
HierarchicalSwarm
v2;manager 分派 workers 后汇总
+
+
+ +
+

本地 MCP Server 包装方式

always loaded
+
+
loading path
+
foundation/config/loader.py
+→ LOCAL_MCP_CATEGORIES["local_team_workflow_mcp"] = {"category": "team_workflow", ...}
+→ python -m beaver.interfaces.mcp.tools_server --category team_workflow
+→ tools_server._category_tools("team_workflow")
+→ beaver.team_workflows.mcp_tools.create_team_workflow_tools()
+→ MCPToolWrapper exposes tools to AgentLoop
+
+
+ +
+

边界范围

明确不做什么
+
+

做:本地 MCP workflow tools、每个 workflow 单独 py、args → ExecutionGraph、复用 TeamService/Scheduler。

+
×

不做:新 Team runtime、固定角色 Agent registry、nested Team、前端改造、chart renderer、高风险审批 UI。

+
+
Planner 的位置

Planner 可以继续作为兼容路径存在,但不再是 Agent Team 成败的核心入口。主路径是 Main Agent 在 AgentLoop 中调用具体 workflow MCP tool。

+
+
+
+ +
+
+
+

最终定稿:每个 Team Workflow 本身就是一个工具

workflow = tool
+
+

Main Agent

只负责选哪个 workflow 工具。

SequentialWorkflow / GraphWorkflow...
+

填 agents

args 主要是 agents / instruction / skill_query。

不是手写自由 DAG
+

Workflow 工具

根据自己的规则生成 ExecutionGraph。

内部生成 depends_on
+

Scheduler

执行 Graph,收集 evidence,最终总结。

runtime owns orchestration
+
+
+ +
+

SequentialWorkflow 工具

agents 顺序就是执行顺序
+
+
tool call
+
{
+  "tool": "SequentialWorkflow",
+  "arguments": {
+    "task": "比较 MGM China 和 Galaxy Entertainment,输出中文财务报告",
+    "agents": [
+      {"name": "source_collector", "instruction": "收集官方财务披露来源"},
+      {"name": "metric_extractor", "instruction": "提取收入、EBITDA、净利润等指标"},
+      {"name": "validator", "instruction": "校验来源、周期、币种、单位"},
+      {"name": "reporter", "instruction": "生成 Markdown 报告和 chart-ready data"}
+    ]
+  }
+}
+
+
工具内部生成

source_collector → metric_extractor → validator → reporter。Main Agent 不需要写 depends_on

+
+ +
+

ConcurrentWorkflow 工具

agents 全部并行
+
+
tool call
+
{
+  "tool": "ConcurrentWorkflow",
+  "arguments": {
+    "task": "从多个独立来源调研同一主题",
+    "agents": [
+      {"name": "official_sources", "instruction": "查官方来源"},
+      {"name": "media_sources", "instruction": "查媒体报道"},
+      {"name": "data_sources", "instruction": "查数据网站"}
+    ]
+  }
+}
+
+
工具内部生成

所有 agents 无依赖并行执行。只有真正互不依赖的任务才用它。

+
+ +
+

GraphWorkflow:高级工具,edges 是必填 args

explicit graph
+
+
GraphWorkflow tool call
+
{
+  "tool": "GraphWorkflow",
+  "arguments": {
+    "task": "分析今天早上韩国队在世界杯上的表现,输出中文结构化报告",
+    "agents": [
+      {"name": "collector", "instruction": "收集比赛事实、比分、关键事件和可靠来源", "skill_query": "multi search sports news"},
+      {"name": "tactics", "instruction": "基于 collector 的结果分析战术表现"},
+      {"name": "players", "instruction": "基于 collector 的结果分析球员表现"},
+      {"name": "media", "instruction": "基于 collector 的结果分析媒体舆论"},
+      {"name": "synthesizer", "instruction": "合并所有分析,输出最终中文报告"}
+    ],
+    "edges": [
+      ["collector", "tactics"],
+      ["collector", "players"],
+      ["collector", "media"],
+      ["tactics", "synthesizer"],
+      ["players", "synthesizer"],
+      ["media", "synthesizer"]
+    ],
+    "output_agent": "synthesizer"
+  }
+}
+
+
精确定义

GraphWorkflow 不是让 Agent 写自然语言 flow,也不是自由 DAG 文本。它是工具,edges 是 args schema 的必填字段,runtime 必须校验无环、节点存在、output_agent 可达。

+
+ +
+

每种 workflow 工具暴露什么 args

schema contract
+
+
工具
必填 args
工具内部怎么连
+
SequentialWorkflow
task, agents[]
按 agents 顺序生成链式依赖
+
ConcurrentWorkflow
task, agents[]
全部并行,无依赖
+
MixtureOfAgents
task, agents[], aggregator
非 aggregator 并行 → aggregator
+
AgentRearrange
task, agents[], flow
flow 是结构参数,按工具规则解析
+
GraphWorkflow
task, agents[], edges[], output_agent
edges 必填,runtime 严格校验
+
GroupChat
task, agents[], moderator/rounds
轮流发言,moderator/last agent 汇总
+
HierarchicalSwarm
task, manager, agents[]
manager 分派,workers 执行,manager 汇总
+
+
+
+
+ +
+
+
+

几种方案对比

LLM 难度越低越稳
+
+
方案
LLM 要做什么
综合判断
+
A. 自由输出 DAG JSON
自己判断类型、节点、依赖、工具、证据
最灵活,最容易错
+
B. 一个 run_swarm_workflow + architecture 参数
选 architecture + 填 agents/flow/edges
比自由 DAG 好,但工具语义不够清晰
+
C. Skill Template 严格驱动
只适配已有模板
业务场景最稳
+
D. 每个 workflow 是一个工具
选工具 + 填 agents/instructions
推荐主线;最接近 swarms architecture
+
E. GraphWorkflow 工具
填 agents + 必填 edges + output_agent
高级路径;显式但受 schema 约束
+
+
+ +
+

10 个从简单到复杂的场景

1 最简单,10 最难
+
+
1

单问答

例如“解释一下 EBITDA”。不需要 Team。最佳:single_agent。

+
2

单工具查询

例如“查今天珠海天气”。最佳:single_agent + tool。

+
3

单来源总结

给一个 URL,让它总结。最佳:single_agent。

+
4

多来源独立搜索

多个来源互不依赖。最佳:ConcurrentWorkflow。

+
5

先收集再多角度分析

韩国队这类。最佳:GraphWorkflow 或 AgentRearrange,edges/flow 必须表达 collector → analyses → synthesizer。

+
6

财务来源→指标→校验→报告

MGM/Galaxy。最佳:SequentialWorkflow,agents 顺序生成依赖。

+
7

多轮修订任务

用户验收失败后重跑部分节点。需要 run version + evidence reuse。

+
8

复杂 DAG:并行后合并再并行

需要显式 dependency。最佳:GraphWorkflow,edges 是必填 args。

+
9

需要判断证据是否充分

需要 LLM judge / evidence contract。不能只看 finish_reason。

+
10

动态协作 / 运行中新增节点

接近 Swarms 高级编排。需要独立设计,不应 v1 直接做。

+
+
+ +
+

推荐落地路线

优先级
+
+

第一步

把 Swarms architecture 做成工具。

+

第二步

Main Agent 选择具体 workflow 工具。

+

第三步

args 主体改成 agents/instructions。

+

第四步

GraphWorkflow 暴露必填 edges/output_agent。

+

第五步

workflow 工具内部生成 ExecutionGraph。

+
+
+
+
+
+
+ + diff --git a/docs/superpowers/examples/install_mgm_skill_metadata.py b/docs/superpowers/examples/install_mgm_skill_metadata.py new file mode 100644 index 0000000..bcef946 --- /dev/null +++ b/docs/superpowers/examples/install_mgm_skill_metadata.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +import hashlib +import json +from datetime import datetime, timezone +from pathlib import Path + + +def main() -> None: + skill_name = "mgm-galaxy-financial-chart-report-safe" + workspace = Path("/root/.beaver/workspace") + skill_dir = workspace / "skills" / skill_name + skill_md = skill_dir / "versions" / "v0001" / "SKILL.md" + content = skill_md.read_text(encoding="utf-8") + digest = "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest() + now = datetime.now(timezone.utc).isoformat() + + (skill_dir / "current.json").write_text( + json.dumps({"current_version": "v0001"}, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + (skill_dir / "skill.json").write_text( + json.dumps( + { + "name": skill_name, + "display_name": "MGM/Galaxy Financial Chart Report Safe", + "description": "Compare MGM China and Galaxy Entertainment using official financial sources, produce chart-ready Markdown, and avoid claiming generated chart image/file artifacts.", + "created_at": now, + "updated_at": now, + "current_version": "v0001", + "status": "active", + "tags": ["finance", "research", "report", "chart-ready-data", "mgm", "galaxy"], + "owners": ["steven"], + "source_kind": "workspace", + "lineage": [], + }, + indent=2, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + (skill_dir / "versions" / "v0001" / "version.json").write_text( + json.dumps( + { + "skill_name": skill_name, + "version": "v0001", + "content_hash": digest, + "summary_hash": digest, + "created_at": now, + "created_by": "steven", + "change_reason": "Add real Skill Team Template example for MGM/Galaxy finance report demo", + "parent_version": None, + "review_state": "published", + "frontmatter": { + "name": skill_name, + "description": "Compare MGM China and Galaxy Entertainment using official financial sources, produce chart-ready Markdown, and avoid claiming generated chart image/file artifacts.", + "tools": ["web_search", "web_fetch"], + }, + "summary": "MGM/Galaxy finance report skill with a task-only Beaver team template for official source collection, metric extraction, validation, and Markdown chart-ready reporting.", + "tool_hints": ["web_search", "web_fetch"], + "provenance": {"source_kind": "manual_demo", "target_instance": "steven"}, + "tree_hash": "", + }, + indent=2, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", + ) + + index_path = workspace / "skills" / "_index" / "published.json" + try: + payload = json.loads(index_path.read_text(encoding="utf-8")) + except FileNotFoundError: + payload = {"items": []} + items = [str(item) for item in payload.get("items", [])] + if skill_name not in items: + items.append(skill_name) + index_path.write_text(json.dumps({"items": items}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + print(f"installed metadata for {skill_name}: {digest}") + + +if __name__ == "__main__": + main() diff --git a/docs/superpowers/examples/mgm-galaxy-financial-chart-report-safe.SKILL.md b/docs/superpowers/examples/mgm-galaxy-financial-chart-report-safe.SKILL.md new file mode 100644 index 0000000..a6c8977 --- /dev/null +++ b/docs/superpowers/examples/mgm-galaxy-financial-chart-report-safe.SKILL.md @@ -0,0 +1,155 @@ +--- +name: mgm-galaxy-financial-chart-report-safe +description: Compare MGM China and Galaxy Entertainment using official financial sources, produce chart-ready Markdown, and avoid claiming generated chart image/file artifacts. +tools: + - web_search + - web_fetch +--- + +# MGM/Galaxy Financial Chart Report Safe + +## Overview + +Use this skill when the user asks for a finance comparison report for MGM China Holdings Limited and Galaxy Entertainment Group, especially when the requested output includes a table, chart-ready data, or a textual chart section. + +The skill intentionally separates source collection, metric extraction, validation, and final reporting. It must not invent chart files or image artifacts. If the runtime does not expose a registered chart-rendering tool, the final output should be Markdown plus chart-ready data only. + +```beaver-team-template +{ + "version": 1, + "strategy": "dag", + "nodes": [ + { + "node_id": "collect_official_sources", + "task": "Collect official MGM China Holdings and Galaxy Entertainment financial disclosure sources for the requested period. Prefer annual reports, interim reports, results announcements, investor relations pages, and exchange filings. Return source URLs with short notes about period coverage.", + "use_skill": "web-operation", + "skill_query": "official financial disclosure web research", + "depends_on": [], + "requested_tools": ["web_search", "web_fetch"], + "required_evidence": ["tool_result", "url"], + "evidence_contract": { + "version": 1, + "entities": ["MGM China Holdings", "Galaxy Entertainment Group"], + "source_types": ["annual_report", "interim_report", "results_announcement", "investor_relations", "exchange_filing"], + "minimum_sources_per_entity": 1 + }, + "validation_rules": [ + "Prefer official company, investor relations, HKEX, or stock exchange sources.", + "Record the reporting period attached to each source.", + "Do not use unsourced market commentary as primary evidence." + ], + "required_for_completion": true, + "block_downstream_on_partial": true, + "max_tool_iterations": 4, + "constraints": [ + "Use only public pages.", + "Do not require login or paid databases." + ] + }, + { + "node_id": "extract_financial_metrics", + "task": "Extract comparable financial metrics for MGM China Holdings and Galaxy Entertainment from the collected official sources. Include revenue or net revenue, adjusted EBITDA where available, net profit/loss where available, period, currency, unit, and source URL for each metric.", + "skill_query": "financial metric extraction from official disclosures", + "depends_on": ["collect_official_sources"], + "requested_tools": ["web_fetch"], + "required_evidence": ["output"], + "evidence_contract": { + "version": 1, + "metrics": ["revenue", "adjusted_ebitda", "net_profit_or_loss"], + "required_fields": ["company", "period", "metric", "value", "currency", "unit", "source_url"] + }, + "validation_rules": [ + "Keep currencies and units explicit.", + "Do not compare different reporting periods without labeling the mismatch.", + "Mark unavailable metrics as unavailable instead of estimating them." + ], + "required_for_completion": true, + "block_downstream_on_partial": true, + "max_tool_iterations": 2, + "constraints": [ + "Use upstream official sources before searching for alternatives." + ] + }, + { + "node_id": "validate_metrics", + "task": "Validate extracted metrics for source consistency, period alignment, currency/unit consistency, and obvious transcription errors. Produce a concise validation note and list any evidence gaps.", + "skill_query": "finance metric validation", + "depends_on": ["extract_financial_metrics"], + "requested_tools": [], + "required_evidence": ["output"], + "evidence_contract": { + "version": 1, + "checks": ["source_consistency", "period_alignment", "currency_unit_consistency", "transcription_sanity"] + }, + "validation_rules": [ + "Do not introduce new unsourced figures.", + "If values are not comparable, explain why and preserve both values with labels." + ], + "required_for_completion": true, + "block_downstream_on_partial": true, + "max_tool_iterations": 0, + "constraints": [ + "No tools in this validation node; use upstream evidence only." + ] + }, + { + "node_id": "generate_chart_report", + "task": "Generate the final Markdown comparison report. Include an executive summary, source-backed comparison table, chart-ready data table, optional Mermaid or text bar chart section, and caveats. Do not claim that a chart image, chart file, or saved artifact was generated.", + "skill_query": "financial markdown report with chart-ready data", + "depends_on": ["validate_metrics"], + "requested_tools": [], + "required_evidence": ["output"], + "evidence_contract": { + "version": 1, + "outputs": ["comparison_table", "chart_ready_data", "markdown_report"], + "forbidden_claims": ["generated_chart_image", "generated_chart_file", "saved_chart_artifact"] + }, + "validation_rules": [ + "Every numeric claim must trace back to a source URL or be marked unavailable.", + "Do not claim a generated image/file unless a registered chart renderer tool was actually used.", + "Prefer Markdown tables and chart-ready data over unsupported rendering claims." + ], + "required_for_completion": true, + "block_downstream_on_partial": false, + "max_tool_iterations": 0, + "constraints": [ + "No chart renderer is assumed.", + "No file/image artifact claims." + ] + } + ] +} +``` + +## When to Use + +- The user asks to compare MGM China and Galaxy Entertainment financial performance. +- The user asks for a chart, chart-ready data, Markdown chart section, or board-style finance report. +- The task requires source-backed public financial data rather than generic market commentary. + +## Required Tools + +- `web_search` +- `web_fetch` + +## Workflow + +1. Collect official sources first: company investor relations pages, annual/interim reports, results announcements, and exchange filings. +2. Extract comparable metrics with period, currency, unit, and source URL. +3. Validate that metrics are comparable before drawing conclusions. +4. Produce a Markdown report with comparison table and chart-ready data. +5. If a real chart renderer tool is unavailable, say so implicitly by providing chart-ready data; do not claim an image or file was created. + +## Validation + +- Confirm each company has at least one official source. +- Confirm all numeric metrics carry period, currency, unit, and source URL. +- Confirm the final report does not contain claims such as “saved chart image”, “generated chart file”, or “attached chart artifact”. + +## Boundaries + +- Do not use private, paid, or login-only sources. +- Do not fabricate unavailable figures. +- Do not use high-risk write, terminal, email, or external-send tools. +- Do not create nested teams or role-based agents. +- Do not claim chart rendering unless the runtime exposes and actually uses a registered chart-renderer tool. diff --git a/docs/superpowers/examples/steven-mgm-galaxy-team-demo.md b/docs/superpowers/examples/steven-mgm-galaxy-team-demo.md new file mode 100644 index 0000000..2bbda32 --- /dev/null +++ b/docs/superpowers/examples/steven-mgm-galaxy-team-demo.md @@ -0,0 +1,257 @@ +# Steven MGM/Galaxy Team Template Demo + +## Target + +Install `mgm-galaxy-financial-chart-report-safe` into Steven's Beaver workspace, then run one task that exercises: + +```text +Planner +→ Skill Template selection +→ ExecutionGraph / ExecutionNode adaptation +→ Node Skill Binding +→ Team execution +→ Tool scope filtering +→ Evidence gate +→ Final synthesis complete/incomplete outcome +``` + +## Current environment status observed by Codex + +The repository contains Steven's instance metadata: + +```text +instance_id: steven +container_name: app-instance-steven +beaver_home: app-instance/runtime/instances/steven/beaver-home +workspace: app-instance/runtime/instances/steven/beaver-home/workspace +public_url: http://steven.172.19.0.245.nip.io:8088 +``` + +Codex could not directly apply the skill to the live Steven instance in this session because: + +```text +docker API: permission denied while connecting to /var/run/docker.sock +Steven workspace/skills parent dir: owned by nobody:nogroup and not writable by current user +local backend .venv: incomplete after uv environment rebuild; missing test/runtime dependencies +``` + +So this runbook is the exact artifact to apply from a shell with Docker or filesystem permission. + +## Install Skill into Steven workspace + +From repository root, run as a user that can write Steven's workspace: + +```bash +SKILL_NAME=mgm-galaxy-financial-chart-report-safe +WORKSPACE=app-instance/runtime/instances/steven/beaver-home/workspace +SKILL_DIR="$WORKSPACE/skills/$SKILL_NAME" + +mkdir -p "$SKILL_DIR/versions/v0001" +cp docs/superpowers/examples/mgm-galaxy-financial-chart-report-safe.SKILL.md \ + "$SKILL_DIR/versions/v0001/SKILL.md" + +python3 - <<'PY' +import hashlib +import json +from pathlib import Path +from datetime import datetime, timezone + +skill_name = "mgm-galaxy-financial-chart-report-safe" +workspace = Path("app-instance/runtime/instances/steven/beaver-home/workspace") +skill_dir = workspace / "skills" / skill_name +skill_md = skill_dir / "versions" / "v0001" / "SKILL.md" +content = skill_md.read_text(encoding="utf-8") +digest = "sha256:" + hashlib.sha256(content.encode("utf-8")).hexdigest() +now = datetime.now(timezone.utc).isoformat() + +(skill_dir / "current.json").write_text( + json.dumps({"current_version": "v0001"}, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", +) + +(skill_dir / "skill.json").write_text( + json.dumps( + { + "name": skill_name, + "display_name": "MGM/Galaxy Financial Chart Report Safe", + "description": "Compare MGM China and Galaxy Entertainment using official financial sources, produce chart-ready Markdown, and avoid claiming generated chart image/file artifacts.", + "created_at": now, + "updated_at": now, + "current_version": "v0001", + "status": "active", + "tags": ["finance", "research", "report", "chart-ready-data", "mgm", "galaxy"], + "owners": ["steven"], + "source_kind": "workspace", + "lineage": [], + }, + indent=2, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", +) + +(skill_dir / "versions" / "v0001" / "version.json").write_text( + json.dumps( + { + "skill_name": skill_name, + "version": "v0001", + "content_hash": digest, + "summary_hash": digest, + "created_at": now, + "created_by": "steven", + "change_reason": "Add real Skill Team Template example for MGM/Galaxy finance report demo", + "parent_version": None, + "review_state": "published", + "frontmatter": { + "name": skill_name, + "description": "Compare MGM China and Galaxy Entertainment using official financial sources, produce chart-ready Markdown, and avoid claiming generated chart image/file artifacts.", + "tools": ["web_search", "web_fetch"], + }, + "summary": "MGM/Galaxy finance report skill with a task-only Beaver team template for official source collection, metric extraction, validation, and Markdown chart-ready reporting.", + "tool_hints": ["web_search", "web_fetch"], + "provenance": {"source_kind": "manual_demo", "target_instance": "steven"}, + "tree_hash": "", + }, + indent=2, + ensure_ascii=False, + ) + + "\n", + encoding="utf-8", +) + +index_path = workspace / "skills" / "_index" / "published.json" +index_path.parent.mkdir(parents=True, exist_ok=True) +try: + payload = json.loads(index_path.read_text(encoding="utf-8")) +except FileNotFoundError: + payload = {"items": []} +items = [str(item) for item in payload.get("items", [])] +if skill_name not in items: + items.append(skill_name) +index_path.write_text(json.dumps({"items": items}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") +PY +``` + +## Restart or start Steven container + +If the container already exists: + +```bash +docker restart app-instance-steven +``` + +If it does not exist, use the existing instance metadata and project scripts rather than creating a new instance id. + +## Demo task prompt + +Send this as Steven's user task: + +```text +Use the MGM/Galaxy finance report skill to compare MGM China Holdings and Galaxy Entertainment using official public financial disclosures. Produce a concise board-style Markdown report with source URLs, a comparison table, chart-ready data, and a text/Mermaid chart section. Do not claim a generated image or saved chart file. +``` + +## Expected planning shape + +The planner should produce a team DAG with these task nodes: + +```text +collect_official_sources +→ extract_financial_metrics +→ validate_metrics +→ generate_chart_report +``` + +Expected node constraints: + +```text +collect_official_sources.allowed_tool_names = ["web_search", "web_fetch"] +extract_financial_metrics.allowed_tool_names = ["web_fetch"] +validate_metrics.allowed_tool_names = [] +generate_chart_report.allowed_tool_names = [] +``` + +The created workers should remain generic: + +```text +node.agent.role = "" +node.agent.metadata.sub_agent_kind = "generic_skill_worker" +``` + +## Expected complete outcome + +If source collection and extraction produce required evidence: + +```text +Planner +→ TeamRunResult with required nodes completion_status=succeeded +→ task_outcome=complete +→ tool-free final synthesis +→ final Markdown report +``` + +The final output may include: + +```text +comparison table +chart-ready data +Mermaid +Markdown chart section +text bar chart fallback +final textual report +``` + +It must not claim: + +```text +generated chart image +generated chart file +saved chart artifact +``` + +## Expected incomplete outcome + +If official-source evidence is missing or web tools fail: + +```text +collect_official_sources.completion_status=partial +→ evidence_gaps populated +→ because block_downstream_on_partial=true, downstream nodes are blocked +→ task_outcome=incomplete +→ tool-free final synthesis still runs +→ final answer is prefixed with an incomplete notice +``` + +The final response should explain which required evidence was missing instead of fabricating metrics. + +## Verification queries + +After running the task, inspect Steven's event log: + +```bash +WORKSPACE=app-instance/runtime/instances/steven/beaver-home/workspace +tail -n 200 "$WORKSPACE/tasks/events.jsonl" +``` + +Look for: + +```text +task_execution_planned +task_team_run_completed or task_team_run_failed +task_synthesis_completed +``` + +For `task_execution_planned`, verify: + +```text +planner_adaptation.template_used = true +planner_adaptation.selected_template = mgm-galaxy-financial-chart-report-safe +node_ids include collect_official_sources/extract_financial_metrics/validate_metrics/generate_chart_report +``` + +For `task_synthesis_completed`, verify: + +```text +task_outcome = complete | incomplete +incomplete_node_ids = [] for complete, otherwise populated +``` diff --git a/docs/superpowers/examples/steven_team_demo_harness.py b/docs/superpowers/examples/steven_team_demo_harness.py new file mode 100644 index 0000000..2890249 --- /dev/null +++ b/docs/superpowers/examples/steven_team_demo_harness.py @@ -0,0 +1,316 @@ +from __future__ import annotations + +import asyncio +import json +from dataclasses import asdict +from pathlib import Path +from typing import Any + +from beaver.engine import AgentLoop, EngineLoader +from beaver.engine.context import SkillContext +from beaver.engine.providers.base import LLMProvider, LLMResponse, ToolCallRequest +from beaver.engine.providers.factory import ProviderBundle, build_provider_runtime +from beaver.services.team_service import TeamService +from beaver.skills.catalog.loader import SkillsLoader +from beaver.skills.catalog.utils import strip_frontmatter +from beaver.skills.drafts import DraftService +from beaver.skills.specs import SkillSpecStore +from beaver.tasks.attempt_orchestrator import TaskAttemptOrchestrator +from beaver.tasks.models import TaskRecord +from beaver.tasks.planner import TaskExecutionPlanner +from beaver.tasks.skill_resolver import TaskSkillResolver + + +WORKSPACE = Path("/root/.beaver/workspace") +SKILL_NAME = "mgm-galaxy-financial-chart-report-safe" + + +def _text_from_messages(messages: list[dict[str, Any]]) -> str: + return "\n".join(str(message.get("content") or "") for message in messages) + + +def _tool_names(tools: list[dict[str, Any]] | None) -> list[str]: + names: list[str] = [] + for tool in tools or []: + if "function" in tool: + names.append(str(tool["function"].get("name") or "")) + else: + names.append(str(tool.get("name") or "")) + return [name for name in names if name] + + +class DemoProvider(LLMProvider): + def __init__(self, *, collect_uses_tool: bool) -> None: + super().__init__() + self.collect_uses_tool = collect_uses_tool + self.calls: list[dict[str, Any]] = [] + + async def chat( + self, + messages: list[dict[str, Any]], + tools: list[dict[str, Any]] | None = None, + model: str | None = None, + max_tokens: int | None = None, + temperature: float = 0.0, + thinking_enabled: bool | None = None, + ) -> LLMResponse: + text = _text_from_messages(messages) + names = _tool_names(tools) + self.calls.append( + { + "tool_names": names, + "has_tool_result": any(message.get("role") == "tool" for message in messages), + "text_preview": text[:300], + } + ) + + if "You choose whether an internal Beaver Task attempt" in text: + return LLMResponse( + content=json.dumps(_planner_json(), ensure_ascii=False), + provider_name="demo", + model="demo-model", + ) + + if "You select Beaver skills for a single run" in text: + return LLMResponse(content="[]", provider_name="demo", model="demo-model") + + if "team:generate_chart_report" in text: + return LLMResponse( + content=( + "# MGM China vs Galaxy Entertainment Demo Report\n\n" + "| Company | Metric | Value | Source |\n" + "|---|---:|---:|---|\n" + "| MGM China | Revenue | demo value | upstream source |\n" + "| Galaxy Entertainment | Revenue | demo value | upstream source |\n\n" + "Chart-ready data is provided as Markdown. No image or saved chart file was generated." + ), + provider_name="demo", + model="demo-model", + ) + + if "team:validate_metrics" in text: + return LLMResponse( + content="Validation complete: periods and units are labeled; no generated chart artifact is claimed.", + provider_name="demo", + model="demo-model", + ) + + if "team:extract_financial_metrics" in text: + return LLMResponse( + content=( + "Extracted demo metric table: MGM China revenue: source-backed placeholder; " + "Galaxy Entertainment revenue: source-backed placeholder. Currency, period, and source URL fields are labeled." + ), + provider_name="demo", + model="demo-model", + ) + + if "team:collect_official_sources" in text: + if self.collect_uses_tool and "web_fetch" in names and not any(message.get("role") == "tool" for message in messages): + return LLMResponse( + content=None, + tool_calls=[ + ToolCallRequest( + id="call_collect_fetch", + name="web_fetch", + arguments={ + "url": "https://www.bing.com/search?q=MGM+China+Galaxy+Entertainment+annual+report", + "max_chars": 1000, + }, + ) + ], + finish_reason="tool_calls", + provider_name="demo", + model="demo-model", + ) + return LLMResponse( + content=( + "Collected official-source candidates for MGM China Holdings and Galaxy Entertainment. " + "Demo evidence includes a successful web_fetch tool result with URL captured by Beaver." + ), + provider_name="demo", + model="demo-model", + ) + + return LLMResponse(content="Demo final synthesis.", provider_name="demo", model="demo-model") + + def get_default_model(self) -> str: + return "demo-model" + + +def _planner_json() -> dict[str, Any]: + return { + "mode": "team", + "reason": "finance comparison benefits from staged source collection, extraction, validation, and reporting", + "strategy": "dag", + "nodes": [ + { + "node_id": "collect_official_sources", + "task": "Collect official MGM China Holdings and Galaxy Entertainment financial disclosure sources for the requested period. Prefer annual reports, interim reports, results announcements, investor relations pages, and exchange filings. Return source URLs with short notes about period coverage.", + "use_skill": "web-operation", + "skill_query": "official financial disclosure web research", + "depends_on": [], + "requested_tools": ["web_search", "web_fetch"], + "required_evidence": ["tool_result", "url"], + "evidence_contract": {"version": 1, "entities": ["MGM China Holdings", "Galaxy Entertainment Group"]}, + "required_for_completion": True, + "block_downstream_on_partial": True, + "max_tool_iterations": 2, + }, + { + "node_id": "extract_financial_metrics", + "task": "Extract comparable financial metrics for MGM China Holdings and Galaxy Entertainment from the collected official sources. Include revenue or net revenue, adjusted EBITDA where available, net profit/loss where available, period, currency, unit, and source URL for each metric.", + "use_skill": "web-operation", + "skill_query": "financial metric extraction from official disclosures", + "depends_on": ["collect_official_sources"], + "requested_tools": ["web_fetch"], + "required_evidence": ["output"], + "evidence_contract": {"version": 1, "metrics": ["revenue", "adjusted_ebitda", "net_profit_or_loss"]}, + "required_for_completion": True, + "block_downstream_on_partial": True, + "max_tool_iterations": 1, + }, + { + "node_id": "validate_metrics", + "task": "Validate extracted metrics for source consistency, period alignment, currency/unit consistency, and obvious transcription errors. Produce a concise validation note and list any evidence gaps.", + "use_skill": "utility-tools", + "skill_query": "finance metric validation", + "depends_on": ["extract_financial_metrics"], + "requested_tools": [], + "required_evidence": ["output"], + "evidence_contract": {"version": 1, "checks": ["source_consistency", "period_alignment"]}, + "required_for_completion": True, + "block_downstream_on_partial": True, + "max_tool_iterations": 0, + }, + { + "node_id": "generate_chart_report", + "task": "Generate the final Markdown comparison report. Include an executive summary, source-backed comparison table, chart-ready data table, optional Mermaid or text bar chart section, and caveats. Do not claim that a chart image, chart file, or saved artifact was generated.", + "use_skill": "utility-tools", + "skill_query": "financial markdown report with chart-ready data", + "depends_on": ["validate_metrics"], + "requested_tools": [], + "required_evidence": ["output"], + "evidence_contract": {"version": 1, "outputs": ["comparison_table", "chart_ready_data", "markdown_report"]}, + "required_for_completion": True, + "block_downstream_on_partial": False, + "max_tool_iterations": 0, + }, + ], + "adaptation": {"template_used": True}, + "final_synthesis_instruction": "Synthesize node outputs into a concise Markdown finance report.", + } + + +def _task() -> TaskRecord: + return TaskRecord( + task_id="demo-task-mgm-galaxy", + session_id="web:demo-mgm-galaxy-harness", + description="Compare MGM China and Galaxy Entertainment using official public financial disclosures.", + goal="Compare MGM China and Galaxy Entertainment using official public financial disclosures.", + constraints=[], + priority=0, + status="open", + creator="demo", + created_at="demo", + updated_at="demo", + ) + + +def _finance_skill_context(loader: SkillsLoader) -> SkillContext: + record = loader.get_skill_record(SKILL_NAME) + raw = loader.load_published_skill(SKILL_NAME) + if record is None or raw is None: + raise RuntimeError(f"missing published skill: {SKILL_NAME}") + return SkillContext( + name=record.name, + version=record.version, + content=strip_frontmatter(raw).strip(), + content_hash=record.content_hash or "", + activation_reason="demo_exact_skill", + tool_hints=list(record.tool_hints), + team_template=record.team_template, + team_template_warnings=list(record.team_template_warnings), + ) + + +async def _run_case(*, collect_uses_tool: bool) -> dict[str, Any]: + loader = SkillsLoader(WORKSPACE) + store = SkillSpecStore(WORKSPACE) + runtime = build_provider_runtime(model="demo-model", provider_name="custom", api_key="demo", api_base="http://demo.invalid/v1") + provider = DemoProvider(collect_uses_tool=collect_uses_tool) + bundle = ProviderBundle(main_runtime=runtime, main_provider=provider) + engine_loader = EngineLoader(workspace=WORKSPACE) + loop = AgentLoop(loader=engine_loader) + loaded = loop.boot() + resolver = TaskSkillResolver(skills_loader=loader, draft_service=DraftService(store)) + planner = TaskExecutionPlanner(task_skill_resolver=resolver, tool_registry=loaded.tool_registry) + task = _task() + skill_context = _finance_skill_context(loader) + plan = await planner.plan( + task=task, + user_message=task.description, + attempt_index=1, + provider_bundle=bundle, + activated_skills=[skill_context], + timeout_seconds=5.0, + ) + team_result = None + if plan.is_team: + team_result = await TeamService(loop).run_team( + plan.graph, + parent_task_id=None, + parent_session_id=task.session_id, + provider_bundle_factory=lambda node: bundle, + inherited_pinned_skill_contexts=[skill_context], + ) + context, prefix, metadata = TaskAttemptOrchestrator._team_synthesis_outcome(plan, team_result, prompt_locale="en") + return { + "case": "complete" if collect_uses_tool else "incomplete", + "plan_mode": plan.mode, + "plan_reason": plan.reason, + "planner_adaptation": plan.planner_adaptation, + "node_ids": [node.node_id for node in plan.graph.nodes] if plan.graph else [], + "node_tool_scopes": {node.node_id: node.allowed_tool_names for node in plan.graph.nodes} if plan.graph else {}, + "node_skill_bindings": [ + { + "node_id": node.node_id, + "pinned_skill_names": node.inherited_pinned_skills, + "pinned_skill_contexts": [skill.name for skill in node.inherited_pinned_skill_contexts], + "role": node.agent.role, + "sub_agent_kind": node.agent.metadata.get("sub_agent_kind"), + "exact_binding_used": node.agent.metadata.get("exact_binding_used"), + } + for node in (plan.graph.nodes if plan.graph else []) + ], + "team_success": team_result.success if team_result else None, + "team_summary": team_result.summary if team_result else None, + "team_run_ids": team_result.run_ids if team_result else [], + "node_results": [ + { + "node_id": result.node_id, + "success": result.success, + "completion_status": result.completion_status, + "finish_reason": result.finish_reason, + "evidence_gaps": result.evidence_gaps, + "output_preview": result.output_text[:180], + } + for result in (team_result.node_results if team_result else []) + ], + "synthesis_metadata": metadata, + "incomplete_prefix_present": bool(prefix), + "outcome_context_preview": context[:600], + "provider_calls": provider.calls, + } + + +async def main() -> None: + results = [ + await _run_case(collect_uses_tool=True), + await _run_case(collect_uses_tool=False), + ] + print(json.dumps(results, ensure_ascii=False, indent=2, default=str)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/docs/superpowers/plans/2026-06-24-template-guided-team-routing.md b/docs/superpowers/plans/2026-06-24-template-guided-team-routing.md new file mode 100644 index 0000000..99a7fc6 --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-template-guided-team-routing.md @@ -0,0 +1,531 @@ +# Template-Guided Team Routing Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Let a root Main Agent choose Team execution on its first provider response whenever an activated Skill supplies a valid Team template, while preserving an intentional zero-extra-round single-agent path. + +**Architecture:** Keep `ExecutionGraph`, `ExecutionNode`, `LocalAgentRunner`, and `run_agent_team` unchanged. Add a small Main-Agent routing state inside `AgentLoop`: it selects the first valid activated template, adds compact first-turn guidance, classifies the first provider response as `team` or `single`, persists a structured mode event, and prevents a later mid-run Team switch after single-agent work starts. Project that event into the existing Task process stream; no frontend work is included. + +**Tech Stack:** Python 3.12, asyncio, dataclasses, pytest, existing `AgentLoop`, session event store, process projector, and Team tool runtime. + +--- + +## File Structure + +- `app-instance/backend/beaver/engine/loop.py`: primary-template selection, first-turn guidance, mode classification/lock, tool-call filtering, and persistent routing event. +- `app-instance/backend/beaver/services/process_service.py`: project the routing event into the existing task process stream. +- `app-instance/backend/tests/unit/test_agent_loop.py`: Main-Agent prompt, first-turn Team, first-turn Single, mixed-call, and no-template regression tests. +- `app-instance/backend/tests/unit/test_process_projection.py`: routing-event projection test. + +No changes to Planner, Team scheduler/runtime, ToolAssembler, ToolExecutor, evidence gate, final synthesis gate, frontend, or Skill learning are required. + +### Task 1: Select a Primary Template and Make First-Turn Routing Explicit + +**Files:** + +- Modify: `app-instance/backend/beaver/engine/loop.py` +- Modify: `app-instance/backend/tests/unit/test_agent_loop.py` + +- [ ] **Step 1: Add a sequenced provider and a valid template fixture to the AgentLoop test module** + +Add imports for `SkillContext` and `ToolCall`, then add a provider that captures the system prompt and returns supplied responses in sequence: + +```python +class SequencedProvider(LLMProvider): + def __init__(self, responses: list[LLMResponse]) -> None: + super().__init__() + self.responses = list(responses) + self.calls: list[dict[str, Any]] = [] + + async def chat(self, messages: list[dict], tools: list[dict] | None = None, **_: Any) -> LLMResponse: + self.calls.append({"messages": messages, "tools": tools}) + return self.responses.pop(0) + + def get_default_model(self) -> str: + return "stub-model" + + +def _team_template_skill(name: str = "finance-report") -> SkillContext: + return SkillContext( + name=name, + content="# Finance report", + team_template={ + "version": 1, + "strategy": "dag", + "nodes": [{"node_id": "collect", "task": "Collect official sources"}], + }, + ) +``` + +- [ ] **Step 2: Write failing first-turn guidance and deterministic-primary tests** + +```python +def test_root_task_with_template_adds_first_turn_team_routing_guidance(tmp_path) -> None: + provider = RecordingProvider() + loop = AgentLoop(loader=EngineLoader(workspace=tmp_path)) + + asyncio.run(loop.process_direct( + "compare financial reports", + session_id="session", + task_id="task-1", + task_mode=True, + pinned_skill_contexts=[_team_template_skill(), _team_template_skill("ignored")], + provider_bundle=_bundle(provider), + )) + + system_content = "\n".join( + str(message["content"]) + for message in provider.messages_by_call[0] + if message["role"] == "system" + ) + assert "choose one execution path in this first response" in system_content + assert "run_agent_team" in system_content + assert '"skill_name":"finance-report"' in system_content + assert "ignored" not in system_content + + +def test_empty_template_nodes_do_not_enable_first_turn_team_routing(tmp_path) -> None: + provider = RecordingProvider() + loop = AgentLoop(loader=EngineLoader(workspace=tmp_path)) + empty = SkillContext(name="empty", content="# Empty", team_template={"nodes": []}) + + asyncio.run(loop.process_direct( + "single lookup", + session_id="session", + task_id="task-1", + task_mode=True, + pinned_skill_contexts=[empty], + provider_bundle=_bundle(provider), + )) + + assert "choose one execution path in this first response" not in provider.system_prompts[0] +``` + +Extend `RecordingProvider` to retain `messages_by_call` and `system_prompts`, instead of creating a second nearly-identical fixture. + +- [ ] **Step 3: Run the focused tests to verify failure** + +Run: + +```bash +cd app-instance/backend && uv run pytest tests/unit/test_agent_loop.py -q +``` + +Expected: FAIL because no Main-Agent template selector or first-turn routing guidance exists. + +- [ ] **Step 4: Add a private, immutable routing-selection value and selector in `loop.py`** + +Place this near `AgentRunResult`: + +```python +@dataclass(frozen=True, slots=True) +class _TeamTemplateRouting: + skill_name: str + template: dict[str, Any] + ignored_skill_names: tuple[str, ...] = () + + +def _select_main_agent_team_template( + activated_skills: list[SkillContext], +) -> _TeamTemplateRouting | None: + candidates = [ + skill + for skill in activated_skills + if isinstance(skill.team_template, dict) + and isinstance(skill.team_template.get("nodes"), list) + and bool(skill.team_template["nodes"]) + ] + if not candidates: + return None + return _TeamTemplateRouting( + skill_name=candidates[0].name, + template=dict(candidates[0].team_template or {}), + ignored_skill_names=tuple(skill.name for skill in candidates[1:]), + ) +``` + +This intentionally mirrors, but does not alter, `TaskExecutionPlanner._select_team_template()`: planner adaptation metadata and Main-Agent first-turn routing have different lifecycles. Do not move the helper into Planner or use Planner as a runtime dependency. + +- [ ] **Step 5: Build compact guidance only when a root Task can actually invoke the Team tool** + +Replace the static-only Team section with a helper that accepts the routing value: + +```python +@staticmethod +def _team_template_routing_prompt(routing: _TeamTemplateRouting) -> str: + template_payload = json.dumps( + {"skill_name": routing.skill_name, "template": routing.template}, + ensure_ascii=False, + separators=(",", ":"), + ) + return ( + "# Task Agent Team Routing\n\n" + "An active Skill provides this primary Team template:\n" + f"{template_payload}\n\n" + "Before beginning ordinary work, choose one execution path in this first response. " + "For staged collection, extraction, validation, comparison, research, or reporting represented " + "by this template, call `run_agent_team` now using task-only nodes derived from it. " + "Choose single-agent execution only for a plainly one-step request, an explicit request not to " + "delegate, or a template that does not fit the immediate request. Do not call ordinary tools " + "before this choice. If choosing single-agent execution, call ordinary tools or answer normally " + "without explaining the routing choice." + ) +``` + +In `_process_direct_impl()`, calculate the value after activated Skills are resolved. Pass it into `_extra_guidance_sections()` only when all are true: + +```python +is_root_task = task_mode and not parent_session_id and not str(source or "").startswith("team:") +team_tool_available = any(spec.name == AGENT_TEAM_TOOL_NAME for spec in selected_tool_specs) +routing_template = _select_main_agent_team_template(activated_skills) +routing_enabled = is_root_task and team_tool_available and routing_template is not None +``` + +Keep `TASK_AGENT_TEAM_CAPABILITY_PROMPT` for ordinary root Task capability exposure. Do not add guidance for empty/invalid templates, child Team nodes, non-Task runs, or when `run_agent_team` is absent. + +- [ ] **Step 6: Run the focused tests to verify they pass** + +Run: + +```bash +cd app-instance/backend && uv run pytest tests/unit/test_agent_loop.py -q +``` + +Expected: PASS, including existing root-Team-tool visibility coverage. + +### Task 2: Lock First-Turn Mode and Persist the Machine-Readable Decision + +**Files:** + +- Modify: `app-instance/backend/beaver/engine/loop.py` +- Modify: `app-instance/backend/tests/unit/test_agent_loop.py` + +- [ ] **Step 1: Write failing Team, Single, mixed-call, and legacy behavior tests** + +Use `ToolCall` objects in a `SequencedProvider`; use the normal registered `run_agent_team` only with a `tool_executor_override` stub so the test checks AgentLoop routing without starting a real Team. + +```python +def test_first_turn_agent_team_call_records_team_mode_and_executes_only_team(tmp_path) -> None: + provider = SequencedProvider([ + LLMResponse( + content="", + tool_calls=[ + ToolCall(id="team", name="run_agent_team", arguments={"nodes": [{"node_id": "collect", "task": "Collect"}]}), + ToolCall(id="search", name="web_search", arguments={"query": "must not run"}), + ], + provider_name="stub", + model="stub-model", + ), + LLMResponse(content="done", provider_name="stub", model="stub-model"), + ]) + executor = CapturingToolExecutor() + loop = AgentLoop(loader=EngineLoader(workspace=tmp_path)) + + asyncio.run(loop.process_direct( + "compare finance reports", + session_id="session", + task_id="task-1", + task_mode=True, + pinned_skill_contexts=[_team_template_skill()], + provider_bundle=_bundle(provider), + tool_executor_override=executor, + )) + + assert [call.name for call in executor.calls] == ["run_agent_team"] + decision = _event_payload(loop, "session", "execution_mode_selected") + assert decision == { + "task_id": "task-1", + "execution_mode": "team", + "routing_source": "main_agent_first_turn", + "primary_template_skill": "finance-report", + "ignored_template_skills": [], + } + + +def test_first_turn_ordinary_tool_records_single_and_blocks_later_team_call(tmp_path) -> None: + provider = SequencedProvider([ + LLMResponse( + content="", + tool_calls=[ToolCall(id="search", name="web_search", arguments={"query": "one step"})], + provider_name="stub", + model="stub-model", + ), + LLMResponse( + content="", + tool_calls=[ToolCall(id="team", name="run_agent_team", arguments={"nodes": [{"node_id": "late", "task": "Late"}]})], + provider_name="stub", + model="stub-model", + ), + LLMResponse(content="done", provider_name="stub", model="stub-model"), + ]) + executor = CapturingToolExecutor() + loop = AgentLoop(loader=EngineLoader(workspace=tmp_path)) + + asyncio.run(loop.process_direct( + "one-step lookup", + session_id="session", + task_id="task-1", + task_mode=True, + pinned_skill_contexts=[_team_template_skill()], + provider_bundle=_bundle(provider), + tool_executor_override=executor, + )) + + assert [call.name for call in executor.calls] == ["web_search"] + assert "run_agent_team" not in provider.tool_names_by_call[1] + late_result = _tool_result_by_call_id(loop, "session", "team") + assert late_result["error"] == "execution_mode_locked_single" + assert _event_payload(loop, "session", "execution_mode_selected")["execution_mode"] == "single" +``` + +Also assert that a root Task with no template keeps `run_agent_team` in every provider schema, preserving legacy behavior. + +- [ ] **Step 2: Run the test module to verify failure** + +Run: + +```bash +cd app-instance/backend && uv run pytest tests/unit/test_agent_loop.py -q +``` + +Expected: FAIL because AgentLoop has no decision event, no per-run mode state, and executes mixed/later Team calls normally. + +- [ ] **Step 3: Add mode state and first-response classification immediately after the provider response** + +Before the `while True` loop set: + +```python +routing_mode: str | None = None +``` + +After `response = await provider.chat(**chat_kwargs)` and before serializing/appending the assistant message, classify only once when `routing_enabled` is true: + +```python +if routing_enabled and routing_mode is None: + tool_names = {self._tool_call_name(tool_call) for tool_call in response.tool_calls} + routing_mode = "team" if AGENT_TEAM_TOOL_NAME in tool_names else "single" + append_message( + resolved_session_id, + run_id=resolved_run_id, + role="system", + event_type="execution_mode_selected", + event_payload={ + "task_id": task_id, + "attempt_index": attempt_index, + "execution_mode": routing_mode, + "routing_source": "main_agent_first_turn", + "primary_template_skill": routing_template.skill_name, + "ignored_template_skills": list(routing_template.ignored_skill_names), + }, + content=None, + context_visible=False, + source=source, + title=title, + model=final_model, + user_id=user_id, + ) +``` + +Do not write this event for runs without `routing_enabled`. A no-tool first response selects `single` before the normal final-answer branch. + +- [ ] **Step 4: Apply the no-mixed-mode and single-lock behavior at the call boundary** + +Add two private helpers: + +```python +@staticmethod +def _calls_for_execution_mode(tool_calls: list[Any], routing_mode: str | None) -> list[Any]: + if routing_mode != "team": + return list(tool_calls) + return [call for call in tool_calls if AgentLoop._tool_call_name(call) == AGENT_TEAM_TOOL_NAME] + + +@staticmethod +def _team_locked_result(tool_call: Any) -> ToolResult: + return ToolResult( + success=False, + content="Agent Team can only be selected in the first response of this Task run.", + tool_name=AGENT_TEAM_TOOL_NAME, + error="execution_mode_locked_single", + ) +``` + +Then use these rules in the loop: + +1. If first response selected `team`, serialize and execute only `run_agent_team`; ordinary calls from that response receive no execution. +2. If `routing_mode == "single"` and the current iteration is after the first response, remove `run_agent_team` from `chat_kwargs["tools"]` before calling the provider. +3. If a later response nevertheless emits `run_agent_team`, do not call the executor. Add `_team_locked_result()` through the same `tool_result_recorded` and context-builder paths as ordinary tool failures. +4. Preserve the normal concurrent-execution decision for the remaining executable calls. + +Keep original tool schemas and ToolExecutor behavior unchanged for no-template runs. Do not alter `allowed_tool_names` behavior or use it as a source of tools. + +- [ ] **Step 5: Run focused AgentLoop tests** + +Run: + +```bash +cd app-instance/backend && uv run pytest tests/unit/test_agent_loop.py -q +``` + +Expected: PASS. The test verifies no extra provider call is made solely for mode selection, mixed first-turn calls execute only Team, and late Team calls are rejected after Single mode. + +### Task 3: Project Routing Decisions into the Task Process Stream + +**Files:** + +- Modify: `app-instance/backend/beaver/services/process_service.py` +- Modify: `app-instance/backend/tests/unit/test_process_projection.py` + +- [ ] **Step 1: Write a failing process-projection test** + +```python +def test_process_projection_maps_main_agent_execution_mode_selection(tmp_path: Path) -> None: + session = SessionManager(tmp_path) + run_store = RunMemoryStore(tmp_path / "memory" / "runs") + session.append_message( + "web:test", + run_id="main-run", + role="system", + event_type="execution_mode_selected", + event_payload={ + "task_id": "task-1", + "attempt_index": 1, + "execution_mode": "team", + "routing_source": "main_agent_first_turn", + "primary_template_skill": "finance-report", + "ignored_template_skills": ["secondary-template"], + }, + context_visible=False, + ) + + projection = SessionProcessProjector(session, run_store).project("web:test") + + event = next(item for item in projection["events"] if item["kind"] == "execution_mode_selected") + assert event["status"] == "done" + assert event["metadata"]["execution_mode"] == "team" + assert event["metadata"]["primary_template_skill"] == "finance-report" + assert event["metadata"]["ignored_template_skills"] == ["secondary-template"] +``` + +- [ ] **Step 2: Run it to verify failure** + +Run: + +```bash +cd app-instance/backend && uv run pytest tests/unit/test_process_projection.py -q +``` + +Expected: FAIL with `StopIteration`, because the projector ignores `execution_mode_selected`. + +- [ ] **Step 3: Add a narrow event branch in `SessionProcessProjector.project()`** + +Place the branch after `skill_activation_snapshotted` and before Team-completion handling: + +```python +elif record.event_type == "execution_mode_selected": + run_id = record.run_id or root_run_id + parent_run_id = root_run_id if run_id != root_run_id else None + mode = str(payload.get("execution_mode") or "single") + add_event( + event_id=_event_id(record, "execution-mode"), + run_id=str(run_id), + parent_run_id=parent_run_id, + kind="execution_mode_selected", + actor_type="system", + actor_id="main-agent-router", + actor_name="Main Agent", + text="Main Agent selected Team execution." if mode == "team" else "Main Agent selected single-agent execution.", + created_at=created_at, + status="done", + metadata={ + **dict(payload), + "task_id": task_id, + "attempt_index": attempt_index, + "timeline_type": "execution_mode", + }, + ) +``` + +Do not add frontend rendering in this task. The projected event is enough for the existing API/process payload and future UI work. + +- [ ] **Step 4: Run focused projection tests** + +Run: + +```bash +cd app-instance/backend && uv run pytest tests/unit/test_process_projection.py -q +``` + +Expected: PASS. + +### Task 4: Regression Verification and Steven Docker Acceptance + +**Files:** + +- No new production files. +- Modify only test fixtures/assertions from Tasks 1–3 if a compatibility issue is exposed. + +- [ ] **Step 1: Run all directly affected unit tests** + +Run: + +```bash +cd app-instance/backend && uv run pytest \ + tests/unit/test_agent_loop.py \ + tests/unit/test_process_projection.py \ + tests/unit/test_team_node_tool_policy.py \ + tests/unit/test_task_execution_planner.py \ + tests/unit/test_task_team_synthesis_outcome.py \ + -q +``` + +Expected: PASS. Do not change tests outside this feature to accommodate unrelated Python/TestClient cleanup behavior. + +- [ ] **Step 2: Verify static quality for the scoped diff** + +Run: + +```bash +git diff --check -- \ + app-instance/backend/beaver/engine/loop.py \ + app-instance/backend/beaver/services/process_service.py \ + app-instance/backend/tests/unit/test_agent_loop.py \ + app-instance/backend/tests/unit/test_process_projection.py +``` + +Expected: no output and exit status 0. + +- [ ] **Step 3: Deploy only after local tests pass and verify the real MGM/Galaxy route** + +Run the established Steven deployment procedure: + +```bash +docker cp app-instance/backend/beaver app-instance-steven:/opt/app/backend/ +docker cp app-instance/backend/pyproject.toml app-instance-steven:/opt/app/backend/pyproject.toml +docker exec app-instance-steven sh -lc 'cd /opt/app/backend && uv pip install --system --no-deps -e .' +docker restart app-instance-steven +curl -fsS http://127.0.0.1:20000/api/ping +``` + +Create a fresh MGM/Galaxy finance-report Task and inspect its session/task process events. Acceptance requires this ordering: + +```text +skill_activation_snapshotted +→ execution_mode_selected {execution_mode: team, primary_template_skill: mgm-galaxy-financial-chart-report-safe} +→ tool_call_started: run_agent_team +→ run_agent_team_debug: invoke_started +→ task_team_run_completed or task_team_run_failed +``` + +The first ordinary `web_search` must be emitted by a Team node, never by the root Main Agent. If the model intentionally selects Single for this known staged finance template, stop and inspect the captured first-turn system prompt/tool call before changing code. + +- [ ] **Step 4: Report and stop** + +Report modified files, focused test outputs, Docker health, real-task event ordering, `git diff --stat`, and remaining model-mediated routing risk. Do not stage or commit unless the user explicitly asks. + +## Plan Self-Review + +- Scope coverage: primary template selection, first-turn guidance, mode selection without extra LLM round/reason text, mode lock, raw event persistence, process projection, and real MGM/Galaxy verification are covered. +- Compatibility: no-template runs keep existing Team-tool exposure; child Team nodes still cannot see the tool; graph/runtime/tool scope/evidence/synthesis behavior is untouched. +- Out-of-scope guard: no Planner heuristic change, no frontend, no fixed roles, no nested Team, and no new Team model appear in the implementation tasks. + diff --git a/docs/superpowers/specs/2026-06-24-template-guided-team-routing-design.md b/docs/superpowers/specs/2026-06-24-template-guided-team-routing-design.md new file mode 100644 index 0000000..7baec00 --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-template-guided-team-routing-design.md @@ -0,0 +1,150 @@ +# Template-Guided Team Routing Design + +## Goal + +Make an activated Skill with a valid `beaver-team-template` a first-class execution option for the Main Agent. The Main Agent makes the execution-mode choice during its first normal model turn: it either calls `run_agent_team` or proceeds as a single agent. No new model round, natural-language decision reason, fixed-role agent, parallel Team model, or nested Team is introduced. + +## Problem + +The MGM/Galaxy financial-report task activated `mgm-galaxy-financial-chart-report-safe`, whose template describes source collection, metric extraction, validation, and report generation. The initial task planner nevertheless returned `planner_skipped_simple_task`, because its keyword prefilter did not recognize the request. The Main Agent had `run_agent_team` in its tool schema and the template in its Skill guidance, but the existing prompt only said that it "may" or should "prefer" the Team tool. It selected `web_search` directly and the run stopped at the web-search low-quality budget before any Team was created. + +The issue is not missing tool registration or a broken Team runtime. The decision to use an active template is currently an unconstrained, implicit LLM preference. + +## Scope + +In scope: + +- Main-Agent first-turn routing when one or more activated Skills provide a valid Team template. +- A compact, structured representation of the selected primary template in Main-Agent guidance. +- Explicit first-turn mode semantics inferred from normal execution: + - calling `run_agent_team` selects Team mode; + - calling another tool, or replying without a tool call, selects single-agent mode. +- Structured lifecycle observability for the chosen mode without an LLM-authored reason. +- Tests for template-present Team routing, single-agent opt-out, and legacy no-template behavior. + +Out of scope: + +- A separate mode-selection provider call or a `select_execution_mode` tool. +- Requiring a natural-language explanation for single-agent execution. +- Changing `ExecutionGraph`, `ExecutionNode`, `LocalAgentRunner`, scheduler/evidence/synthesis semantics, node Skill binding, or tool scopes. +- Fixed-role agents, nested Teams, a parallel Team runtime, frontend work, or chart-renderer tools. +- Changing the existing pre-execution `TaskExecutionPlanner` heuristic; this design makes Main-Agent routing reliable even when that planner returns `single`. + +## Existing Runtime Boundary + +```text +TaskAttemptOrchestrator +→ TaskExecutionPlanner.plan() +→ Main Agent AgentLoop.process_direct() +→ Skill activation + ToolAssembler +→ provider first response +→ selected tool execution +``` + +Today, `AgentLoop` registers `run_agent_team` for a root Task run and adds `TASK_AGENT_TEAM_CAPABILITY_PROMPT`. `SkillContext.team_template` is already populated from a Skill's `beaver-team-template` block. Therefore the implementation belongs at the Main-Agent prompt/tool boundary, not in a new Team runtime. + +## Design + +### 1. Template Eligibility + +The normal activated Skill list remains authoritative. The routing helper considers only Skills where `SkillContext.team_template` is a non-empty mapping with a non-empty `nodes` list. + +At most one template is primary. Selection is deterministic and preserves activation order: the first eligible activated Skill is primary; later eligible templates are guidance only. The primary Skill name and ignored template Skill names are included in structured runtime metadata, not in an LLM decision response. + +No template is synthesized from Skill prose. Invalid/missing templates retain existing single-agent behavior. + +### 2. First-Turn Routing Contract + +When a root Task run has an eligible primary template and `run_agent_team` is exposed, the system guidance tells the Main Agent: + +```text +Before beginning ordinary work, choose one execution path in this first response. + +- For staged collection, extraction, validation, comparison, research, or reporting represented by the active template, call run_agent_team now using task-only nodes derived from the primary template. +- Choose single-agent execution only when the user's request is plainly a one-step request, explicitly asks not to delegate, or the template does not fit the immediate request. +- Do not call ordinary tools before this choice. +- If you choose single-agent execution, call ordinary tools or answer normally; do not explain the routing choice. +``` + +The template is included as compact JSON in this guidance, along with its Skill name. This prevents the model having to reconstruct the graph from prose. The existing template and task-only schema restrictions remain authoritative: no `agent` or `role` fields, no nested Team. + +This is a prompting constraint, not a second classifier or a forced Team decision. A model can intentionally opt out by beginning normal single-agent work. + +### 3. Mode Lock and Fallback + +The first provider response locks the root run's mode: + +| First response | Mode | Subsequent behavior | +| --- | --- | --- | +| contains `run_agent_team` | `team` | Execute the Team tool and retain existing Team outcome/synthesis behavior. | +| contains another tool call | `single` | Execute the ordinary tool call. Hide/reject later `run_agent_team` calls for this root run. | +| contains no tool call | `single` | Return the answer normally. | +| Team tool returns an error | `team` | Preserve its tool result and allow the Main Agent's normal post-tool response; do not silently start a second Team. | + +The lock prevents a half-completed single-agent search from later creating a Team with overlapping evidence and ambiguous timeline ownership. A new Task attempt starts a fresh first-turn routing decision. + +The existing pre-execution planner may still create a Team before Main Agent execution. If it does, no Main-Agent route decision is needed: its mode is already `team`. The new behavior applies to the current `single` pre-plan path, where the Main Agent otherwise owns execution. + +### 4. Observability + +Persist a machine-readable event immediately after the first provider response is classified: + +```json +{ + "execution_mode": "team", + "routing_source": "main_agent_first_turn", + "primary_template_skill": "mgm-galaxy-financial-chart-report-safe", + "ignored_template_skills": [] +} +``` + +For a no-template run, no new event is necessary. For a template run that selects single mode, the same event is written with `execution_mode: "single"`. There is deliberately no `reason` field and no user-visible decision text. + +The event makes future task detail/process projections able to distinguish “Team was not available” from “Team was available and the Main Agent selected single execution,” without inflating token usage. + +### 5. Failure Semantics + +- Template parse/selection failure: retain existing tool selection and single-agent execution. +- `run_agent_team` unavailable: do not claim Team routing; retain normal single-agent execution. +- Invalid Team arguments: current Team tool error path remains intact; it is visible as a tool result. +- First response tries both `run_agent_team` and ordinary tools: reject/defer ordinary tools for that turn and execute only Team, because Team mode owns the execution plan. This is a defensive runtime rule to preserve the no-mixed-mode invariant. + +## Test Strategy + +Unit tests cover the smallest boundaries: + +1. Eligible activated template produces the first-turn route guidance containing the primary template and task-only Team instruction. +2. A first response containing `run_agent_team` records `team` mode and still follows the current Team tool call path. +3. A first response containing `web_search` records `single` mode; a later `run_agent_team` call is not exposed/executed in that run. +4. A valid template does not make ordinary no-template runs change their tool list or behavior. +5. Multiple templates select the first activated template and report the others as ignored metadata. +6. Existing agent-loop and Task/Team regression tests stay green. + +## Acceptance Criteria + +For a request that activates the MGM/Galaxy Skill: + +```text +Skill activation +→ primary template is available to Main Agent first turn +→ Main Agent calls run_agent_team before web_search +→ Team graph and child nodes are created +``` + +For a plainly one-step request with an activated but non-fitting template: + +```text +Skill activation +→ Main Agent begins normal execution directly +→ execution_mode=single is recorded +→ no additional model round and no reason text are generated +``` + +All existing semantics for generic workers, node tool scopes, Skill-based tool assembly, evidence gates, final synthesis, and no nested Teams remain unchanged. + +## Risks + +- This relies on the Main Agent following a stronger first-turn instruction; it is materially more reliable than `may/prefer`, but still model-mediated rather than a hard classifier. +- A template can be activated for a broad Skill while not fitting a narrow user follow-up. The explicit single-agent route is retained for that case. +- Hiding `run_agent_team` after first-turn single selection changes a root run's available tool list over iterations. The event and tests must make that state transition explicit. +- Existing pre-planner keyword routing remains a separate heuristic. It can still choose a Team early for known complex tasks; it is no longer the sole mechanism for template-driven Team execution. diff --git a/docs/superpowers/specs/2026-06-26-local-mcp-team-workflows-design.md b/docs/superpowers/specs/2026-06-26-local-mcp-team-workflows-design.md new file mode 100644 index 0000000..1707fe1 --- /dev/null +++ b/docs/superpowers/specs/2026-06-26-local-mcp-team-workflows-design.md @@ -0,0 +1,1259 @@ +# Local MCP Team Workflow Tools Clean Architecture + +## 0. 结论 + +Agent Team 重新收口成一个干净模型: + +```text +用户任务 +→ Root AgentLoop +→ 选择一个本地常驻 Workflow MCP Tool +→ Workflow Tool 接收 agents / instructions / 必要结构参数 +→ Workflow Tool 内部生成 ExecutionGraph +→ Scheduler 启动多个平级 AgentLoop +→ 汇总 Team 结果 +``` + +不再让 Planner 或 Agent 自由生成复杂 DAG。 + +不再把 `run_agent_team` 作为主入口。 + +不再把 Skill 里的 `beaver-team-template` 当成执行图模板。 + +最终目标是: + +```text +Workflow 是工具 +Agent 只填槽 +结构由代码生成 +ExecutionGraph 是内部 IR +所有 agent 都是平级 AgentLoop +``` + +--- + +## 1. 架构原则 + +### 1.1 没有特殊 Main Agent + +系统里只有 `AgentLoop`。 + +产品文案上可以说“当前会话 agent”或“root agent”,但代码和架构里不再把它设计成特殊 Main Agent。 + +```text +Root AgentLoop +Worker AgentLoop +Aggregator AgentLoop +Validator AgentLoop +``` + +这些都是同一种执行单元。 + +区别只来自: + +```text +输入任务不同 +上下文不同 +可见 Skill 不同 +可见工具不同 +dependency_outputs 不同 +``` + +### 1.2 Planner 不再负责 Team DAG + +`TaskExecutionPlanner` 不再是 Agent Team 的入口。 + +它不再负责输出: + +```text +mode = team +strategy +nodes +depends_on +ExecutionGraph +``` + +如果还保留 Planner,它只能做非 Team 核心逻辑,例如: + +```text +创建 Task 前的轻量判断 +任务标题 / 摘要 +上下文提示 +legacy 迁移期兼容 +``` + +但它不能再是“Team 是否启动”和“Team 图怎么连”的权威来源。 + +### 1.3 `run_agent_team` 不再是主入口 + +旧路径: + +```text +AgentLoop +→ run_agent_team({strategy, nodes, depends_on}) +→ Planner.from_json() +→ ExecutionGraph +``` + +新路径: + +```text +AgentLoop +→ SequentialWorkflow(...) +→ workflow code builds ExecutionGraph +``` + +或: + +```text +AgentLoop +→ GraphWorkflow(...) +→ graph.py validates edges +→ workflow code builds ExecutionGraph +``` + +`run_agent_team` 可以在迁移期保留为隐藏兼容工具,但不进入新设计目标,不应该继续作为 root AgentLoop 的推荐工具。 + +### 1.4 Skill 不再携带可执行 DAG + +旧设计: + +```text +SKILL.md +→ beaver-team-template +→ Planner adapts template +→ ExecutionGraph +``` + +新设计: + +```text +SKILL.md +→ workflow guidance +→ Root AgentLoop reads guidance +→ Root AgentLoop calls Workflow Tool +``` + +Skill 的职责是指导 Agent: + +```text +应该选哪个 workflow tool +应该创建哪些 agents +每个 agent 应该做什么 +哪些 agent 需要 use_skill / skill_query +输出边界是什么 +``` + +Skill 不再直接定义 runtime DAG。 + +### 1.5 ExecutionGraph 是内部 IR + +`ExecutionGraph / ExecutionNode` 继续保留。 + +但它们的定位变成: + +```text +runtime intermediate representation +``` + +也就是: + +```text +LLM 不直接写 ExecutionGraph +Skill 不直接写 ExecutionGraph +前端不直接编辑 ExecutionGraph +Workflow tool 内部生成 ExecutionGraph +Scheduler 执行 ExecutionGraph +``` + +这样可以保留已有调度器和 evidence gate,同时把“图的正确性”从 LLM 输出转移到代码。 + +--- + +## 2. 新执行链路 + +### 2.1 单 Agent 任务 + +```text +用户输入 +→ 创建 Task +→ Root AgentLoop +→ Skill / ToolAssembler 装载普通工具 +→ Agent 直接调用普通工具或直接回答 +→ Task 完成 +``` + +这种场景不进入 Team。 + +### 2.2 Team 任务 + +```text +用户输入 +→ 创建 Task +→ Root AgentLoop +→ Skill guidance 告诉它适合哪个 workflow +→ Root AgentLoop 调用本地 Workflow MCP Tool +→ Workflow Tool 解析 agents / instructions +→ Workflow Tool 生成 ExecutionGraph +→ TeamService / Scheduler 执行 graph +→ 每个 node 启动一个 Worker AgentLoop +→ Worker AgentLoop 选择或绑定 Skill +→ Worker AgentLoop 调用工具并产出 evidence +→ Scheduler 根据 graph / evidence 决定继续或阻断 +→ Aggregator / final synthesis 汇总 +→ Task 完成或 incomplete +``` + +### 2.3 多 Agent 不是多角色 + +不要把 workflow agents 理解成固定角色 Agent。 + +它们不是: + +```text +researcher +writer +reviewer +analyst +``` + +它们只是当前 workflow 的工作节点: + +```text +source_collector +metric_extractor +validator +reporter +``` + +每个节点仍然是: + +```text +AgentDescriptor(role="") +metadata["sub_agent_kind"] = "generic_skill_worker" +``` + +`role=""` 只是为了兼容现有 dataclass 字段,不代表固定角色。 + +--- + +## 3. Root AgentLoop 能看到什么 + +Root AgentLoop 看到的是: + +```text +用户任务 +会话上下文 +已激活 Skill guidance +普通工具 +本地常驻 Team Workflow MCP tools +``` + +示例工具: + +```text +web_search +web_fetch +skill_view +... +SequentialWorkflow +ConcurrentWorkflow +MixtureOfAgents +AgentRearrange +GraphWorkflow +``` + +如果当前 MCP wrapper 仍带 server 前缀,实际名字可能是: + +```text +mcp_local_team_workflow_mcp_SequentialWorkflow +mcp_local_team_workflow_mcp_GraphWorkflow +``` + +但产品目标应该是让 Agent 看到短名字: + +```text +SequentialWorkflow +GraphWorkflow +``` + +Root AgentLoop 不应该看到或输出: + +```text +TaskExecutionPlanner JSON +ExecutionGraph JSON +free-form depends_on DAG +researcher / writer / reviewer role agents +``` + +--- + +## 4. Worker AgentLoop 能看到什么 + +Workflow Tool 生成 `ExecutionGraph` 后,Scheduler 会按 node 启动 Worker AgentLoop。 + +每个 Worker AgentLoop 看到: + +```text +当前 node instruction +上游 dependency outputs +当前 node 的 pinned SkillContext +当前 node 的 skill_query fallback +Skill / ToolAssembler 装载出的工具 +allowed_tool_names safety scope +required_evidence / validation_rules / evidence_contract +``` + +Worker AgentLoop 默认不应该看到 Team Workflow tools。 + +原因: + +```text +避免 nested Team +避免一个 worker 再启动另一个 workflow +避免 timeline / evidence ownership 混乱 +``` + +如果未来要支持 nested workflow,需要独立设计,不进入 v1。 + +--- + +## 5. allowed_tool_names 的准确语义 + +`allowed_tool_names` 不是工具来源。 + +工具来源仍然是: + +```text +Skill +ToolAssembler +AgentLoop 的正常工具装载机制 +``` + +`allowed_tool_names` 只是安全上限: + +```text +effective_tools = assembled_tools ∩ allowed_tool_names +``` + +三态: + +```text +None +→ 不启用 node-level scope +→ 保留原工具装载行为 + +[] +→ 显式禁止该 node 使用任何工具 + +["web_search", "web_fetch"] +→ 只允许已装载工具里的 web_search / web_fetch 通过 +``` + +Workflow Tool 可以把这个字段写进 `ExecutionNode`,但不能用它凭空暴露工具。 + +--- + +## 6. Workflow Tools + +### 6.1 设计原则 + +每种 workflow 是一个工具。 + +不是: + +```text +run_swarm_workflow({architecture: "sequential"}) +``` + +而是: + +```text +SequentialWorkflow(...) +ConcurrentWorkflow(...) +MixtureOfAgents(...) +GraphWorkflow(...) +``` + +这样 LLM 的选择难度更低: + +```text +先选工具 +再填工具 schema +``` + +而不是: + +```text +先选 architecture 字符串 +再猜不同 architecture 需要哪些字段 +``` + +### 6.2 v1 实现范围 + +v1 实现: + +```text +SequentialWorkflow +ConcurrentWorkflow +MixtureOfAgents +AgentRearrange +GraphWorkflow +``` + +v1 不实现或只占位: + +```text +GroupChat +HierarchicalSwarm +ForestSwarm +dynamic team expansion +``` + +原因: + +```text +GroupChat 需要多轮 conversation state +HierarchicalSwarm 需要 manager 动态拆分任务 +ForestSwarm 接近动态多图执行 +``` + +这些不适合和第一版稳定 workflow tool 混在一起。 + +### 6.3 SequentialWorkflow + +输入: + +```json +{ + "task": "比较 MGM China 和 Galaxy Entertainment,输出中文财务报告", + "agents": [ + {"name": "source_collector", "instruction": "收集官方财务披露来源"}, + {"name": "metric_extractor", "instruction": "提取可比财务指标"}, + {"name": "validator", "instruction": "校验周期、币种、单位、来源一致性"}, + {"name": "reporter", "instruction": "生成中文报告、对比表和 chart-ready data"} + ] +} +``` + +内部生成: + +```text +source_collector +→ metric_extractor +→ validator +→ reporter +``` + +### 6.4 ConcurrentWorkflow + +输入: + +```json +{ + "task": "从多个独立角度调研同一主题", + "agents": [ + {"name": "official_sources", "instruction": "查官方资料"}, + {"name": "media_sources", "instruction": "查媒体报道"}, + {"name": "data_sources", "instruction": "查数据来源"} + ] +} +``` + +内部生成: + +```text +official_sources ∥ media_sources ∥ data_sources +``` + +只能用于真正互不依赖的工作。 + +不能用于: + +```text +先搜索事实,再基于事实做多个分析 +``` + +这种要用 `GraphWorkflow` 或 `MixtureOfAgents`。 + +### 6.5 MixtureOfAgents + +输入: + +```json +{ + "task": "对一个问题做多角度分析并汇总", + "agents": [ + {"name": "tactics", "instruction": "分析战术"}, + {"name": "players", "instruction": "分析球员表现"}, + {"name": "media", "instruction": "分析媒体舆论"} + ], + "aggregator": { + "name": "synthesizer", + "instruction": "合并所有分析,输出中文结构化报告" + } +} +``` + +内部生成: + +```text +tactics ┐ +players ├→ synthesizer +media ┘ +``` + +### 6.6 AgentRearrange + +输入: + +```json +{ + "task": "先收集,再多角度分析,再汇总", + "agents": [ + {"name": "collector", "instruction": "收集事实"}, + {"name": "tactics", "instruction": "分析战术"}, + {"name": "players", "instruction": "分析球员"}, + {"name": "media", "instruction": "分析舆论"}, + {"name": "synthesizer", "instruction": "汇总报告"} + ], + "flow": "collector -> tactics, players, media -> synthesizer" +} +``` + +`flow` 是结构参数,不是自然语言说明。 + +必须严格解析: + +```text +节点必须存在 +不允许未知节点 +不允许环 +不允许无法到达输出节点 +``` + +### 6.7 GraphWorkflow + +输入: + +```json +{ + "task": "分析韩国队今早世界杯比赛表现", + "agents": [ + {"name": "collector", "instruction": "收集比赛事实、比分、关键事件和来源"}, + {"name": "tactics", "instruction": "基于 collector 结果分析战术"}, + {"name": "players", "instruction": "基于 collector 结果分析球员表现"}, + {"name": "media", "instruction": "基于 collector 结果分析媒体舆论"}, + {"name": "synthesizer", "instruction": "合并所有分析,输出中文报告"} + ], + "edges": [ + ["collector", "tactics"], + ["collector", "players"], + ["collector", "media"], + ["tactics", "synthesizer"], + ["players", "synthesizer"], + ["media", "synthesizer"] + ], + "output_agent": "synthesizer" +} +``` + +这是唯一 v1 允许直接暴露 edges 的工具。 + +必须校验: + +```text +edges 必填 +edge 两端 agent 必须存在 +不允许 cycle +output_agent 必须存在 +output_agent 必须可达 +不能有无意义孤岛,除非显式 allow_disconnected=true +``` + +--- + +## 7. Skill 里的 Team 指导怎么写 + +### 7.1 Skill 的新职责 + +Skill 不再写执行 DAG。 + +Skill 写的是: + +```text +什么时候使用 Team +推荐哪个 Workflow Tool +每个 agent slot 怎么填 +每个 agent 需要什么 Skill +每个 agent 的工具安全上限 +输出边界 +``` + +### 7.2 示例:MGM / Galaxy finance skill + +```markdown +# MGM / Galaxy Financial Comparison Report + +Use this skill when the user asks for a financial comparison report comparing +MGM China and Galaxy Entertainment. + +Recommended workflow: + +- Use `SequentialWorkflow` for ordinary finance comparison reports. +- Use `GraphWorkflow` only if the user asks for multiple independent analysis branches. + +When using `SequentialWorkflow`, create agents in this order: + +1. `source_collector` + - instruction: Collect official MGM China and Galaxy Entertainment disclosures. + - skill_query: official web search and web fetch. + - allowed_tool_names: web_search, web_fetch. + +2. `metric_extractor` + - instruction: Extract comparable financial metrics from collected sources. + +3. `validator` + - instruction: Validate period, currency, unit, source, and metric definitions. + +4. `reporter` + - instruction: Generate Chinese Markdown report, comparison table, and chart-ready data. + +Output boundary: + +- Allowed: Markdown table, chart-ready data, Mermaid, text bar chart fallback. +- Not allowed: generated chart image, generated chart file, saved chart artifact. +``` + +### 7.3 可选结构化 guidance block + +建议新 block: + +````markdown +```beaver-team-workflow +{ + "version": 1, + "preferred_tool": "SequentialWorkflow", + "agents": [ + { + "name": "source_collector", + "instruction": "Collect official MGM China and Galaxy Entertainment disclosures.", + "skill_query": "official web search financial filings", + "allowed_tool_names": ["web_search", "web_fetch"] + }, + { + "name": "metric_extractor", + "instruction": "Extract comparable financial metrics from collected sources." + }, + { + "name": "validator", + "instruction": "Validate period, currency, unit, source, and metric definitions." + }, + { + "name": "reporter", + "instruction": "Generate Chinese Markdown report, comparison table, and chart-ready data." + } + ], + "output_boundary": { + "allow": ["markdown_table", "chart_ready_data", "mermaid", "text_bar_chart"], + "deny": ["generated_chart_image", "generated_chart_file", "saved_chart_artifact"] + } +} +``` +```` + +这个 block 不是执行图。 + +它只是 root AgentLoop 的 workflow tool-call guidance。 + +--- + +## 8. 代码存放位置 + +### 8.1 新增目录 + +```text +app-instance/backend/beaver/team_workflows/ + __init__.py + base.py + mcp_tools.py + sequential.py + concurrent.py + mixture_of_agents.py + agent_rearrange.py + graph.py +``` + +v2 再考虑: + +```text + group_chat.py + hierarchical.py + forest.py +``` + +### 8.2 `base.py` + +负责公共结构: + +```text +WorkflowAgentSpec +WorkflowGraphBuilder +WorkflowExecutionRequest +WorkflowExecutionResult +agent name validation +common schema fragments +ExecutionNode construction helper +ExecutionGraph validation helper +``` + +`base.py` 生成 node 时统一: + +```python +AgentDescriptor( + name=agent.name, + role="", + system_prompt="", + metadata={ + "sub_agent_kind": "generic_skill_worker", + "workflow_tool": workflow_name, + "workflow_agent_name": agent.name, + }, +) +``` + +### 8.3 `sequential.py` + +负责: + +```text +SequentialWorkflow schema +SequentialWorkflow args validation +agents order → chain dependencies +``` + +### 8.4 `concurrent.py` + +负责: + +```text +ConcurrentWorkflow schema +ConcurrentWorkflow args validation +all agents → no dependencies +``` + +### 8.5 `mixture_of_agents.py` + +负责: + +```text +MixtureOfAgents schema +expert agents validation +aggregator validation +experts → aggregator dependencies +``` + +### 8.6 `agent_rearrange.py` + +负责: + +```text +AgentRearrange schema +flow parser +flow → edges +edges validation +``` + +### 8.7 `graph.py` + +负责: + +```text +GraphWorkflow schema +required edges +required output_agent +cycle detection +unknown node detection +reachability validation +edges → depends_on +``` + +### 8.8 `mcp_tools.py` + +负责把 workflow 实现暴露成 MCP tools: + +```text +create_team_workflow_tools() +→ SequentialWorkflowTool +→ ConcurrentWorkflowTool +→ MixtureOfAgentsTool +→ AgentRearrangeTool +→ GraphWorkflowTool +``` + +--- + +## 9. 本地 MCP 常加载 + +### 9.1 修改 config loader + +文件: + +```text +app-instance/backend/beaver/foundation/config/loader.py +``` + +在 `LOCAL_MCP_CATEGORIES` 加: + +```python +"local_team_workflow_mcp": { + "category": "team_workflow", + "display_name": "本地 Agent Team Workflow 工具", +} +``` + +这样 Beaver 默认启动时就会加载: + +```text +python -m beaver.interfaces.mcp.tools_server --category team_workflow +``` + +### 9.2 修改 MCP tools server + +文件: + +```text +app-instance/backend/beaver/interfaces/mcp/tools_server.py +``` + +加 category: + +```python +LOCAL_TOOL_CATEGORIES = { + ... + "team_workflow": "Beaver Local Team Workflow Tools", +} +``` + +加工具分发: + +```python +elif category == "team_workflow": + from beaver.team_workflows.mcp_tools import create_team_workflow_tools + tools = create_team_workflow_tools() +``` + +--- + +## 10. Runtime bridge 必须干净处理 + +这里是实现关键点。 + +当前普通 MCP wrapper 的问题: + +```text +MCPToolWrapper.invoke(arguments, context) +→ 只把 arguments 发给 MCP server +→ MCP server 不知道当前 task_id / session_id / loaded runtime +``` + +但 Workflow Tool 要启动 Team,必须知道: + +```text +task_id +session_id +loaded runtime +TeamService +workspace +provider context +``` + +所以设计上要明确: + +```text +Team Workflow MCP tools 是本地托管工具,不是普通远端 MCP 工具。 +``` + +推荐实现: + +```text +MCP server 负责暴露 schema +AgentLoop 通过 tool schema 让模型选择工具 +执行时 Beaver 识别 category=team_workflow +在当前进程内调用 TeamWorkflowExecutor +TeamWorkflowExecutor 使用当前 ToolContext 调 TeamService.run_team() +``` + +也就是: + +```text +MCP as schema surface +current process as execution owner +``` + +不要让 MCP 子进程自己 boot 一套 Beaver runtime。 + +否则会带来: + +```text +session 写入归属不清 +task run 绑定不稳 +provider config 重复初始化 +日志难追踪 +并发锁更复杂 +``` + +--- + +## 11. 要修改的代码 + +### 11.1 新增 + +```text +app-instance/backend/beaver/team_workflows/__init__.py +app-instance/backend/beaver/team_workflows/base.py +app-instance/backend/beaver/team_workflows/mcp_tools.py +app-instance/backend/beaver/team_workflows/sequential.py +app-instance/backend/beaver/team_workflows/concurrent.py +app-instance/backend/beaver/team_workflows/mixture_of_agents.py +app-instance/backend/beaver/team_workflows/agent_rearrange.py +app-instance/backend/beaver/team_workflows/graph.py +app-instance/backend/beaver/team_workflows/executor.py +``` + +### 11.2 修改 + +```text +app-instance/backend/beaver/foundation/config/loader.py +``` + +新增 `local_team_workflow_mcp`。 + +```text +app-instance/backend/beaver/interfaces/mcp/tools_server.py +``` + +新增 `team_workflow` category。 + +```text +app-instance/backend/beaver/tools/mcp/wrapper.py +``` + +增加本地 team workflow bridge,避免普通 MCP 子进程执行 Team runtime。 + +```text +app-instance/backend/beaver/engine/loop.py +``` + +调整: + +- root task AgentLoop 可以看到 Workflow tools; +- worker AgentLoop 默认看不到 Workflow tools; +- 去掉 “Main Agent” 概念文案; +- 不再鼓励 `run_agent_team`。 + +```text +app-instance/backend/beaver/tasks/attempt_orchestrator.py +``` + +调整: + +- Task 执行主路径进入 Root AgentLoop; +- 不再要求 Planner 先判断 team; +- Planner 生成 team DAG 的路径降级或删除。 + +```text +app-instance/backend/beaver/skills/catalog/utils.py +app-instance/backend/beaver/skills/catalog/loader.py +app-instance/backend/beaver/engine/context/builder.py +app-instance/backend/beaver/skills/assembler/task_assembler.py +``` + +调整: + +- 解析 `beaver-team-workflow` guidance; +- 不再把 `beaver-team-template` 作为新执行图入口; +- 旧字段如保留,仅作为 migration metadata,不进入新主路径。 + +### 11.3 废弃或删除 + +```text +app-instance/backend/beaver/tools/builtins/agent_team.py +``` + +废弃为主入口。 + +迁移期可隐藏保留,但不再暴露给 root AgentLoop。 + +```text +app-instance/backend/beaver/tasks/planner.py +``` + +删除或废弃 Team DAG generation 责任。 + +不再让它输出 `ExecutionGraph`。 + +--- + +## 12. 测试计划 + +### 12.1 Workflow graph builder + +新增: + +```text +app-instance/backend/tests/unit/test_team_workflow_graph.py +``` + +覆盖: + +```text +SequentialWorkflow builds A → B → C +ConcurrentWorkflow builds independent nodes +MixtureOfAgents builds experts → aggregator +AgentRearrange parses flow +GraphWorkflow requires edges +GraphWorkflow rejects unknown nodes +GraphWorkflow rejects cycles +GraphWorkflow requires reachable output_agent +``` + +### 12.2 MCP loading + +修改: + +```text +app-instance/backend/tests/unit/test_config_loader.py +``` + +覆盖: + +```text +load_config injects local_team_workflow_mcp +category = team_workflow +managed = true +``` + +新增: + +```text +app-instance/backend/tests/unit/test_team_workflow_mcp.py +``` + +覆盖: + +```text +tools_server category team_workflow lists workflow tools +SequentialWorkflow schema has task + agents +GraphWorkflow schema has task + agents + edges + output_agent +``` + +### 12.3 Runtime bridge + +新增: + +```text +app-instance/backend/tests/unit/test_team_workflow_runtime_bridge.py +``` + +覆盖: + +```text +workflow tool call keeps current task_id/session_id +workflow tool calls TeamService.run_team() +workflow result is attached to parent task +ordinary MCP tools still use normal MCP execution +``` + +### 12.4 AgentLoop visibility + +修改: + +```text +app-instance/backend/tests/unit/test_agent_loop.py +``` + +覆盖: + +```text +root task AgentLoop sees workflow tools +worker AgentLoop does not see workflow tools +run_agent_team is not exposed as preferred root tool +allowed_tool_names remains safety filter only +``` + +### 12.5 Skill guidance + +新增或修改: + +```text +app-instance/backend/tests/unit/test_skill_team_workflow_guidance.py +``` + +覆盖: + +```text +beaver-team-workflow block parses +preferred_tool preserved +agents guidance preserved +output_boundary preserved +guidance reaches SkillContext +``` + +--- + +## 13. 迁移计划 + +### Phase 1:建立 workflow builders + +只做: + +```text +beaver/team_workflows/base.py +sequential.py +concurrent.py +mixture_of_agents.py +agent_rearrange.py +graph.py +``` + +不接 AgentLoop。 + +验收: + +```text +workflow args 能稳定生成 ExecutionGraph +``` + +### Phase 2:接本地 MCP schema + +做: + +```text +local_team_workflow_mcp +team_workflow category +mcp_tools.py +``` + +验收: + +```text +AgentLoop tool registry 能看到 Workflow tools +``` + +### Phase 3:接 runtime bridge + +做: + +```text +team_workflows/executor.py +MCP wrapper category bridge +TeamService.run_team integration +``` + +验收: + +```text +调用 SequentialWorkflow 真的启动 Team +``` + +### Phase 4:移除旧主入口 + +做: + +```text +Root AgentLoop 不再暴露 run_agent_team +TaskExecutionPlanner 不再生成 team graph +beaver-team-template 不再进入新主路径 +``` + +验收: + +```text +Team 只能通过 Workflow tools 启动 +``` + +### Phase 5:Skill guidance 升级 + +做: + +```text +支持 beaver-team-workflow +更新 MGM/Galaxy Skill +更新其他 Team Skill 示例 +``` + +验收: + +```text +Skill 指导 Agent 调 SequentialWorkflow / GraphWorkflow +而不是写 DAG +``` + +--- + +## 14. 明确不做 + +v1 不做: + +```text +固定角色 Agent registry +Planner-generated Team DAG +Main Agent 特殊类 +nested Team +运行中动态新增节点 +GroupChat 真多轮状态机 +HierarchicalSwarm manager 动态派工 +ForestSwarm +chart renderer +高风险审批 UI +前端大改 +Skill learning / eval / publish / rollback 改造 +``` + +--- + +## 15. 最终目标图 + +```text +User Task + ↓ +Root AgentLoop + ↓ sees +Skill workflow guidance +ordinary tools +Workflow MCP tools + ↓ calls +SequentialWorkflow / ConcurrentWorkflow / MixtureOfAgents / AgentRearrange / GraphWorkflow + ↓ +workflow Python implementation + ↓ builds +ExecutionGraph / ExecutionNode + ↓ +TeamService + ↓ +Scheduler + ↓ +Worker AgentLoops + ↓ each sees +node instruction +dependency outputs +pinned/dynamic Skill +assembled tools +allowed_tool_names safety scope + ↓ +NodeRunResult + evidence + ↓ +TeamRunResult complete / incomplete + ↓ +Task final result +``` + +这是干净版本的架构边界: + +```text +Workflow Tool 是 Team 的唯一新入口。 +ExecutionGraph 是内部 IR。 +AgentLoop 是唯一 agent 执行单元。 +Skill 只提供 workflow guidance。 +``` diff --git a/docs/ui-ux/pages/ig_045ac706dce1f1bb016a3b37307e14819199cf4a038b6379f7.png b/docs/ui-ux/pages/ig_045ac706dce1f1bb016a3b37307e14819199cf4a038b6379f7.png new file mode 100644 index 0000000..a60a626 Binary files /dev/null and b/docs/ui-ux/pages/ig_045ac706dce1f1bb016a3b37307e14819199cf4a038b6379f7.png differ diff --git a/docs/ui-ux/pages/ig_045ac706dce1f1bb016a3b3775aec8819193685823f5893c76.png b/docs/ui-ux/pages/ig_045ac706dce1f1bb016a3b3775aec8819193685823f5893c76.png new file mode 100644 index 0000000..a2bdbc8 Binary files /dev/null and b/docs/ui-ux/pages/ig_045ac706dce1f1bb016a3b3775aec8819193685823f5893c76.png differ diff --git a/docs/ui-ux/pages/skill-draft-review-demo.png b/docs/ui-ux/pages/skill-draft-review-demo.png new file mode 100644 index 0000000..273b0ac Binary files /dev/null and b/docs/ui-ux/pages/skill-draft-review-demo.png differ diff --git a/test-results/.last-run.json b/test-results/.last-run.json index cbcc1fb..5fca3f8 100644 --- a/test-results/.last-run.json +++ b/test-results/.last-run.json @@ -1,4 +1,4 @@ { - "status": "passed", + "status": "failed", "failedTests": [] } \ No newline at end of file