From 46a834186416733bf0d8d342ba66cda57f667100 Mon Sep 17 00:00:00 2001 From: Y1fe1Zh0u Date: Tue, 25 Aug 2026 16:24:27 +0800 Subject: [PATCH 1/3] Make repository guidance actionable across agent work Replace stale topology and workflow prose with concise ownership, validation, type-safety, verification, communication, and instruction-editing contracts for repository-wide agent work. Constraint: Commit only the root AGENTS.md; nested guidance and placeholder documentation remain out of scope Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep path-specific rules in the nearest nested AGENTS.md and avoid duplicating root guidance Tested: git diff --check; git diff --cached --check Not-tested: Linked placeholder documentation and ignored pre-push Skill are not included in this commit --- AGENTS.md | 155 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 58 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fa33cfd31..80db9757a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,81 +1,120 @@ # AGENTS.md — Clawith Agent Governance & Architecture Guidelines ---- - ## 1. Project Identity -**Clawith** — Multi-tenant Enterprise Agent Application Platform. -Repository architecture and invariants defined in [`ARCHITECTURE_SPEC_EN.md`](file:///Users/alex/Documents/Code/dataelem/Clawith/ARCHITECTURE_SPEC_EN.md). - -### Core Stack & Layout -| Path | Component | Stack | Responsibilities | -|---|---|---|---| -| `backend/` | Product API & Runtime | Python 3.11+, FastAPI, SQLModel (PostgreSQL), LangGraph, Celery/Worker | API adapters, tenant isolation, durable execution state, message delivery | -| `frontend/` | Web Interface | React 18, TypeScript, Vite, Tailwind CSS, shadcn/ui | End-user agent interaction, workspace, chat, session management | +Clawith is an enterprise agent harness for durable single-agent and multi-agent execution. It enables agents to use tools, maintain execution state, and operate across long-running workflows. This repository contains the agent runtime, execution contracts, product APIs, integrations, and web interface used to build and operate those agents. -### Separation of Four Kinds of Facts (Separation Principle) -1. **Product Records**: Owner = Clawith product tables (Tenant, User, Agent, Session, Group, Permissions). -2. **Accepted Command Inbox**: Owner = `agent_run_commands` table (Accepted start, resume, cancel inputs). -3. **Execution Lifecycle**: Owner = LangGraph Checkpoint (PostgreSQL durable checkpoint). -4. **User Delivery**: Owner = Product-side idempotent reconciliation and delivery. +### Repository layout -> **CRITICAL INVARIANT (C1)**: Product projections must **NEVER** become a second Agent execution state machine. API endpoints and product services must not mutate checkpoint lifecycle fields directly or implement private execution control loops. +```text +backend/ Backend application and agent runtime. + Internal structure and rules: backend/AGENTS.md +frontend/ Web application for configuring, operating, and observing agents. + Internal structure and rules: frontend/AGENTS.md +docs/ Durable project documentation and current sources of truth. +specs/ Feature specifications and implementation artifacts. +scripts/ Repository-wide development, validation, and maintenance tooling. +deploy/ Production deployment documentation and deployment-specific assets. +helm/ Kubernetes deployment charts. +.github/ GitHub workflows and CI support scripts. +.specify/ Specification workflow templates and generators. +docker-compose*.yml Local, CI, and production-oriented container topology. +``` ---- +Internal refactors update the nearest path-specific `AGENTS.md`. The root `AGENTS.md` changes only when a top-level repository boundary changes. + +## 2. Conventions + +Each behavior-driving fact has one authoritative owner. Other layers may submit commands, record outcomes, cache data, or build projections, but they must not independently redefine that fact or become a second authority for it. + +- **Lifecycle ownership is explicit.** Every registration, task, subscription, connection, or resource that outlives the current operation has one owner, defined termination conditions, and cleanup paths for success, failure, and cancellation. +- **Runtime responsibilities are documented.** Every capability or subsystem with an independent runtime responsibility must document the authoritative facts and relationships it owns, how those facts change, and how their correctness is verified. Do not infer runtime health from the presence of code, configuration, services, or UI state. +- **State and protocol variants are explicit.** Treat internal lifecycle states and shared contracts as closed unless they are deliberately designed for extension. Update every producer and consumer when a closed set changes, and define explicit unknown-value behavior for extensible inputs. +- **Model-visible inputs are traceable.** Every input that can affect a model decision must have an identifiable source and be attributable to the corresponding Run. Do not inject transient context that cannot later be inspected or reconstructed. See [`docs/model-visible-inputs.md`](docs/model-visible-inputs.md). +- **Keep the Runtime core generic.** The Agent Runtime core may change while its execution model is being completed, but core changes must define general execution semantics rather than product-, integration-, UI-, or capability-specific behavior. Add specialized behavior through its owning Tool, Skill, Provider, Channel, Hook, or service boundary. Document and test every change to the execution model. +- **New state machines require an independent owner and need.** Do not introduce a state machine merely to represent workflow steps, UI progress, or a lifecycle already owned elsewhere. A new state machine must correspond to an independently identified object with authoritative transitions and a current behavioral consumer. +- **Capability boundaries require real participants.** Introduce a shared capability contract only when it has a current provider and consumer. Keep roles together when they change for the same reason; separate them only when their responsibilities and evolution are genuinely independent. +- **Resolve policy before execution.** Defaults, configuration precedence, and policy choices must be resolved explicitly by their owning layer before an operation executes. Execution code consumes resolved inputs and must not hide additional policy decisions in fallbacks. +- **Misconfiguration fails at the earliest authoritative point.** Reject an invalid or missing configuration as soon as its owning layer has enough information to determine the error. Do not silently skip the configured behavior, invent a fallback, or defer a known failure into execution. +- **Validate at trust boundaries.** Use static types for same-process internal contracts and avoid duplicating runtime validation between already typed layers. Validate data when it enters from configuration, HTTP or WebSocket requests, model or Tool JSON, persistence, files, workers, processes, and external integrations. +- **Data access is bounded and evidence-driven.** Query and loading paths must + define their expected cardinality and enforce filtering, pagination, batching, + and result limits at the layer that owns the complete data operation. Avoid + per-item queries, repeated full materialization, and loading unbounded data + for downstream filtering. +- **Caches require ownership and measured need.** Introduce caching only after + identifying repeated expensive work on a real access path. Every cache must + define its authoritative source, owner, key scope, invalidation rule, capacity + bound, and freshness behavior. +- **Ignored failures are narrow and explained.** Catch only the single operation whose specific failure may be ignored, and state what is being ignored and why the primary outcome remains safe. Never use an empty or broad catch to hide unrelated failures. +- **Tests enforce behavior, not product truth.** A passing test proves that the implementation matches its asserted behavior; it does not prove that the asserted behavior matches the current product or architecture contract. Update obsolete tests together with an explicitly approved contract change, and never change an expectation merely to make a failure disappear. +- **Non-trivial changes keep code, Agent Notes, and commit history aligned.** Any change to behavior, architecture, a shared contract, Runtime semantics, persistence, security, permissions, compatibility, or engineering process must add or update its owning Agent Note in the same change. The code implements the decision, the Agent Note owns its durable rationale and current contract, and the commit message records the intent, scope, and verification of this change. These three records must not contradict one another. Update an existing owning note instead of creating a duplicate; only mechanical or strictly local changes are exempt. + +## 3. Type Checking + +Everything compiles under `strict: true` with `noImplicitAny`; every remaining `any` explains why narrowing is infeasible. + +Public interfaces must be usable without reading their implementation. Types +define structure; owning documentation defines non-obvious behavior, failure, +side effects, ownership, timing, cancellation, and durability. + +Every new or changed automated rule must include positive and negative coverage: +valid cases pass, and representative invalid cases fail for the intended +reason. + +## 4. Quick Command Reference -## 2. P0 Architectural Constitution Rules +Dev and test commands live in sub-project instruction files: -The single source of truth for architectural laws is [`docs/constitution.md`](file:///Users/alex/Documents/Code/dataelem/Clawith/docs/constitution.md) (enforced by `scripts/arch-guard.sh`). Do not copy these laws here — link to them: +- Backend: `backend/AGENTS.md` (Server start, Alembic migrations, Pytest, Ruff) +- Frontend: `frontend/AGENTS.md` (Vite dev server, type-check, lint, build) -- **C1: Runtime Boundary Isolation** → [`docs/constitution.md#C1`](file:///Users/alex/Documents/Code/dataelem/Clawith/docs/constitution.md#c1-runtime-boundary-isolation-fact-separation) -- **C2: Strict Multi-Tenant Data Scope** → [`docs/constitution.md#C2`](file:///Users/alex/Documents/Code/dataelem/Clawith/docs/constitution.md#c2-strict-multi-tenant-data-scope--auto-injected--explicit-filters) -- **C3: Idempotent Side Effects & Reconciliation** → [`docs/constitution.md#C3`](file:///Users/alex/Documents/Code/dataelem/Clawith/docs/constitution.md#c3-idempotent-side-effects--reconciliation) -- **C4: Client & Gateway Wrapper Enforcement** → [`docs/constitution.md#C4`](file:///Users/alex/Documents/Code/dataelem/Clawith/docs/constitution.md#c4-client--gateway-wrapper-enforcement) -- **C5: Database & Performance Standards** → [`docs/constitution.md#C5`](file:///Users/alex/Documents/Code/dataelem/Clawith/docs/constitution.md#c5-database--performance-standards-no-foreign-keys--n1-prevention) -- **C6: Code Modularity & Reusability** → [`docs/constitution.md#C6`](file:///Users/alex/Documents/Code/dataelem/Clawith/docs/constitution.md#c6-code-modularity--reusability-recommended-size-thresholds--helper-layer) +## 5. Failure Diagnosis and Handling ---- +When a command fails: -## 3. Quick Command Reference +```text +Command fails + ↓ +Identify the failing layer + ↓ +Collect evidence from that layer + ↓ +Fix the layer that owns the failure + ↓ +Run the original command again +``` -Dev and test commands live in sub-project instruction files: -- Backend: `backend/AGENTS.md` (Server start, Alembic migrations, Pytest, Ruff) -- Frontend: `frontend/AGENTS.md` (Vite dev server, type-check, lint, build) +Do not: ---- +- Modify product code to accommodate the current machine before evidence shows that the environment is the failing layer and that a product-level portability change is required. +- Dismiss a test failure as an environment problem before collecting environment evidence and ruling out a product-code regression. -## 4. SDD Workflow (Specification-Driven Development) +## 6. Verification -For non-trivial features or architecture refactoring, follow this workflow: +After code changes, verification scope is determined by the affected contracts and consumers, not by the number of modified files. Cross-layer changes must follow the real execution path and update and verify every affected layer; local changes require only local evidence. -```text -1. Spec Discovery → ★ User Confirms -2. spec.md → /sdd-review spec → ★ User Confirms -3. design.md → /sdd-review design → ★ User Confirms (Constitution Check) -4. tasks.md → /sdd-review tasks -5. Branch feat/{NNN}-{name} -6. Implement Wave-by-Wave & Run unit tests → /task-review -7. Run scripts/arch-guard.sh & test suite -8. /code-review --base main -``` -*Note: ★ indicates mandatory user confirmation gates.* +Match evidence to the surface. ---- +Use [`docs/testing.md`](docs/testing.md) to select verification by changed contract. Start with focused checks and expand only when the change crosses a documented boundary. -## 5. Instruction File Mapping (AGENTS.md Hierarchy) +Run checks before pushes via [`clawith-pre-push-checks`](.agents/skills/clawith-pre-push-checks/SKILL.md) and report the exact commands and results. After rebasing, merging, resolving conflicts, or otherwise synchronizing a branch, immediately rerun the checks affected by the resulting diff. Do not merge while required checks are failing. -- **Root `AGENTS.md`** (This file): Single source of truth for global constitution, architecture topology, SDD workflow, and P0 rules. -- **[`backend/AGENTS.md`](file:///Users/alex/Documents/Code/dataelem/Clawith/backend/AGENTS.md)**: Backend-specific coding standards, Python import rules, database access guidelines. -- **[`backend/alembic/AGENTS.md`](file:///Users/alex/Documents/Code/dataelem/Clawith/backend/alembic/AGENTS.md)**: Database migration standards, timestamp conventions, lock safety. -- **[`frontend/AGENTS.md`](file:///Users/alex/Documents/Code/dataelem/Clawith/frontend/AGENTS.md)**: Frontend-specific coding standards, React/TS guidelines, HTTP wrapper usage. +## Communication -> **RULE**: Sub-directory `AGENTS.md` files extend root guidelines. Never duplicate root rules in sub-files. If a rule spans multiple components, put it here. +- Lead with the conclusion, result, or blocker. +- Use direct, concrete language and name the actual actor, fact, file, command, + API, state, or behavior. +- Separate verified repository facts, inference, and unverified live behavior. +- Do not narrate internal reasoning, tool choreography, or review history. +- Report only commands and checks actually run, together with relevant + verification gaps. +- Keep responses concise unless risk, ambiguity, or the user requests more + detail. -## Active Technologies -- Python 3.11+ + FastAPI, SQLAlchemy 2.x async ORM, PostgreSQL, LangGraph checkpoint, Pydantic, httpx (002-tool-runtime-contract) -- PostgreSQL `agent_tool_executions` + LangGraph PostgreSQL checkpoint;不新增第二套 Run 生命周期状态机 (002-tool-runtime-contract) -- Python 3.12 deployment baseline(package metadata >=3.11);React 19 / strict TypeScript + FastAPI、SQLAlchemy async ORM、Pydantic、LangGraph Runtime、React Query、Vite;不新增依赖 (003-feishu-group-proactive) -- PostgreSQL 15;复用 `chat_sessions` 与 `channel_deliveries`,为 Schedule/Trigger 增加可选目标 Session UUID (003-feishu-group-proactive) +## Editing these instructions -## Recent Changes -- 002-tool-runtime-contract: Added Python 3.11+ + FastAPI, SQLAlchemy 2.x async ORM, PostgreSQL, LangGraph checkpoint, Pydantic, httpx +Keep repository-wide instructions concise, self-contained, and linked to their +owning documentation. Put path-specific rules in the nearest nested +`AGENTS.md`, and do not duplicate rules across instruction files. Add or expand +a root rule only when it must remain available across the repository. From 02ce62b0d0593ffc9972aaecdb772844f5179d89 Mon Sep 17 00:00:00 2001 From: Y1fe1Zh0u Date: Tue, 25 Aug 2026 17:02:41 +0800 Subject: [PATCH 2/3] Clear obsolete documentation before rebuilding its ownership model Remove the legacy specs corpus, stale design exports, regression case archive, obsolete documentation tree, and repository Skill lock so the replacement documentation system can start from an explicit empty baseline. Constraint: Preserve .specify templates and repository Speckit Skills for future specification work Rejected: Incrementally reorganize the legacy corpus | user chose a zero-history documentation reset Confidence: high Scope-risk: moderate Reversibility: clean through Git for tracked files; ignored local artifacts are not Git-recoverable Directive: Rebuild documentation ownership and navigation from the DSH model before adding new project documents Tested: git diff --cached --check; staged scope inspection Not-tested: Documentation links may remain unresolved until the replacement hierarchy is created --- .../CONTEXT_COMPACT_DESIGN.md | 355 --------- .../LONG_RUNNING_CHECKPOINT_DESIGN.md | 85 --- .../context-compact-feishu-doc.xml | 91 --- .../original-doc-links-append.xml | 7 - V1.11.4_REGRESSION_TEST_CASES.md | 609 ---------------- docs/README.md | 38 - docs/SDD-Guide.md | 56 -- docs/architecture/01-architecture-overview.md | 45 -- .../02-backend-runtime-boundary.md | 32 - .../03-multi-tenant-data-model.md | 18 - docs/constitution.md | 79 -- .../design.md | 684 ------------------ .../spec.md | 315 -------- .../default-agent-seeding-technical-design.md | 280 ------- .../20260728-dao-migration-plan.md | 149 ---- ...0728-private-chat-finish-migration-plan.md | 420 ----------- skills-lock.json | 11 - .../checklists/requirements.md | 34 - .../contracts/vercel-async-operation.md | 52 -- specs/001-fix-vercel-async-wait/data-model.md | 39 - specs/001-fix-vercel-async-wait/plan.md | 81 --- specs/001-fix-vercel-async-wait/quickstart.md | 39 - specs/001-fix-vercel-async-wait/research.md | 57 -- specs/001-fix-vercel-async-wait/spec.md | 130 ---- specs/001-fix-vercel-async-wait/tasks.md | 80 -- .../checklists/requirements.md | 36 - .../contracts/repair-and-lifecycle.md | 33 - .../contracts/step-tool-context.md | 37 - .../contracts/tool-result.md | 40 - specs/002-tool-runtime-contract/data-model.md | 121 ---- specs/002-tool-runtime-contract/plan.md | 130 ---- specs/002-tool-runtime-contract/quickstart.md | 175 ----- specs/002-tool-runtime-contract/research.md | 89 --- specs/002-tool-runtime-contract/spec.md | 202 ------ specs/002-tool-runtime-contract/tasks.md | 249 ------- .../checklists/requirements.md | 37 - .../contracts/automation-delivery.md | 22 - .../contracts/directory-and-tool.md | 52 -- .../003-feishu-group-proactive/data-model.md | 59 -- specs/003-feishu-group-proactive/plan.md | 93 --- .../003-feishu-group-proactive/quickstart.md | 19 - specs/003-feishu-group-proactive/research.md | 53 -- specs/003-feishu-group-proactive/spec.md | 152 ---- specs/003-feishu-group-proactive/tasks.md | 60 -- .../checklists/requirements.md | 36 - .../contracts/feishu-passive-listening.md | 85 --- .../data-model.md | 83 --- specs/004-feishu-passive-listening/design.md | 30 - specs/004-feishu-passive-listening/plan.md | 147 ---- .../quickstart.md | 58 -- .../004-feishu-passive-listening/research.md | 73 -- specs/004-feishu-passive-listening/spec.md | 156 ---- 52 files changed, 6113 deletions(-) delete mode 100644 .clawith-local-designs/CONTEXT_COMPACT_DESIGN.md delete mode 100644 .clawith-local-designs/LONG_RUNNING_CHECKPOINT_DESIGN.md delete mode 100644 .clawith-local-designs/context-compact-feishu-doc.xml delete mode 100644 .clawith-local-designs/original-doc-links-append.xml delete mode 100644 V1.11.4_REGRESSION_TEST_CASES.md delete mode 100644 docs/README.md delete mode 100644 docs/SDD-Guide.md delete mode 100644 docs/architecture/01-architecture-overview.md delete mode 100644 docs/architecture/02-backend-runtime-boundary.md delete mode 100644 docs/architecture/03-multi-tenant-data-model.md delete mode 100644 docs/constitution.md delete mode 100644 docs/features/v1.12.0/001-session-isolated-sandbox-output/design.md delete mode 100644 docs/features/v1.12.0/001-session-isolated-sandbox-output/spec.md delete mode 100644 docs/prd/features/agent-directory/default-agent-seeding-technical-design.md delete mode 100644 docs/technical-plans/20260728-dao-migration-plan.md delete mode 100644 docs/technical-plans/20260728-private-chat-finish-migration-plan.md delete mode 100644 skills-lock.json delete mode 100644 specs/001-fix-vercel-async-wait/checklists/requirements.md delete mode 100644 specs/001-fix-vercel-async-wait/contracts/vercel-async-operation.md delete mode 100644 specs/001-fix-vercel-async-wait/data-model.md delete mode 100644 specs/001-fix-vercel-async-wait/plan.md delete mode 100644 specs/001-fix-vercel-async-wait/quickstart.md delete mode 100644 specs/001-fix-vercel-async-wait/research.md delete mode 100644 specs/001-fix-vercel-async-wait/spec.md delete mode 100644 specs/001-fix-vercel-async-wait/tasks.md delete mode 100644 specs/002-tool-runtime-contract/checklists/requirements.md delete mode 100644 specs/002-tool-runtime-contract/contracts/repair-and-lifecycle.md delete mode 100644 specs/002-tool-runtime-contract/contracts/step-tool-context.md delete mode 100644 specs/002-tool-runtime-contract/contracts/tool-result.md delete mode 100644 specs/002-tool-runtime-contract/data-model.md delete mode 100644 specs/002-tool-runtime-contract/plan.md delete mode 100644 specs/002-tool-runtime-contract/quickstart.md delete mode 100644 specs/002-tool-runtime-contract/research.md delete mode 100644 specs/002-tool-runtime-contract/spec.md delete mode 100644 specs/002-tool-runtime-contract/tasks.md delete mode 100644 specs/003-feishu-group-proactive/checklists/requirements.md delete mode 100644 specs/003-feishu-group-proactive/contracts/automation-delivery.md delete mode 100644 specs/003-feishu-group-proactive/contracts/directory-and-tool.md delete mode 100644 specs/003-feishu-group-proactive/data-model.md delete mode 100644 specs/003-feishu-group-proactive/plan.md delete mode 100644 specs/003-feishu-group-proactive/quickstart.md delete mode 100644 specs/003-feishu-group-proactive/research.md delete mode 100644 specs/003-feishu-group-proactive/spec.md delete mode 100644 specs/003-feishu-group-proactive/tasks.md delete mode 100644 specs/004-feishu-passive-listening/checklists/requirements.md delete mode 100644 specs/004-feishu-passive-listening/contracts/feishu-passive-listening.md delete mode 100644 specs/004-feishu-passive-listening/data-model.md delete mode 100644 specs/004-feishu-passive-listening/design.md delete mode 100644 specs/004-feishu-passive-listening/plan.md delete mode 100644 specs/004-feishu-passive-listening/quickstart.md delete mode 100644 specs/004-feishu-passive-listening/research.md delete mode 100644 specs/004-feishu-passive-listening/spec.md diff --git a/.clawith-local-designs/CONTEXT_COMPACT_DESIGN.md b/.clawith-local-designs/CONTEXT_COMPACT_DESIGN.md deleted file mode 100644 index 3b57c91de..000000000 --- a/.clawith-local-designs/CONTEXT_COMPACT_DESIGN.md +++ /dev/null @@ -1,355 +0,0 @@ -# Context Compact Design - -This document captures the agreed code design for the next compact PR stacked -after PR #572. It only covers the first production-ready compact iteration. - -## Background - -The current history work is split into two prior layers: - -- PR #564 preserves API-valid tool-call history blocks during message-count - truncation. -- PR #572 adds an 80% token-budget fallback based on backend model - `context_window_tokens`, with no provider/model-name hardcoding. - -The next layer should add summary compaction before falling back to drop-only -truncation. - -## Goals - -- Keep the existing 80% trigger from PR #572. -- Use the model window from backend model metadata/config only. -- Prepare request messages against the model that will actually be called, - including fallback models. -- Preserve recent user intent and API-safe recent blocks so long-running tasks - can continue. -- Keep the first compact implementation close to Codex/Claude Code mainstream - behavior before adding Clawith-specific long-running task memory. - -## Non-Goals For The First Compact PR - -- No workspace checkpoint files. -- No persistent compact entries in the database. -- No frontend controls. -- No dedicated compact model setting. -- No Claude Code-style context collapse, session memory compact, or - microcompact. -- No synthetic tool results. - -## Reference Shape - -The implementation should follow the simple mainstream compact pattern: - -- Codex: compact is triggered from the current model metadata, produces a - continuation summary, and replaces the prompt history before the next model - call. -- Claude Code: compact summary is inserted as a conversation continuation - message, while recent messages can remain verbatim. -- Pi: compact keeps a recent raw tail and summarizes older history, avoiding - cuts at tool results. - -For Clawith's first PR, use this shape: - -```text -old safe blocks -> compact summary -recent safe blocks -> preserved verbatim -final request -> compact summary + recent safe blocks -``` - -## Trigger - -Compaction uses the same budget as PR #572: - -```text -token_budget = context_window_tokens * 0.8 -trigger when estimated_conversation_tokens > token_budget -``` - -If `context_window_tokens` is missing, do not compact and do not guess a window -from provider or model names. Keep the existing message-count truncation path. - -The token estimate should use the same local estimator introduced for PR #572. -The threshold is deliberately 80%, not Codex's 90%, because Clawith must support -large-window but lower-capability models that degrade before the advertised -context limit. - -## Per-Model Preparation - -Message preparation must run for the model that is about to be called. - -This is required for primary/fallback safety: - -1. Prepare messages with the primary model's `context_window_tokens`. -2. If primary fails and fallback is used, prepare messages again with the - fallback model's `context_window_tokens`. - -This mirrors Codex's approach of evaluating context limits at turn/sampling -boundaries from the current model metadata. - -Implementation should prefer a per-model preparation callback in the failover -path rather than preparing once outside `call_llm_with_failover`. - -Required behavior: - -```text -primary call: - prepare_messages(primary_model) - call primary - -fallback call: - prepare_messages(fallback_model) - call fallback -``` - -It is not acceptable to prepare once with the primary model and reuse that -prompt for a smaller fallback model. - -## Compact Scope - -Use the existing safe-block strategy from PR #564/#572: - -- Assistant `tool_calls` and matching `role="tool"` results are atomic. -- Blocks are kept whole or dropped whole. -- Orphan or incomplete tool blocks are dropped. -- No fake/synthetic tool results are generated. - -When compact triggers: - -```text -messages_to_keep = recent API-safe blocks within recent_keep_tokens -messages_to_summarize = API-safe blocks before messages_to_keep -``` - -The latest user message must be kept even when it exceeds the recent budget. - -The preserved recent tail must include enough raw context to keep the current -task moving even if the compact summary is lossy. This means the compact summary -must not be the only carrier of the latest user intent. - -Recent budget: - -```text -recent_keep_tokens = min(20_000, token_budget * 0.25) -``` - -This matches the practical shape used by Codex/Pi: keep recent raw context -around the 20k-token range on large-window models while scaling down for -smaller windows. - -Selection algorithm: - -1. Convert the conversation into API-safe blocks. -2. Walk from the tail toward the head. -3. Keep whole blocks while the estimated block tokens fit - `recent_keep_tokens`. -4. Always keep the latest user block even if it exceeds the recent budget. -5. Everything before the kept tail becomes `messages_to_summarize`. - -If there are no old blocks to summarize, do not compact. Use the existing -truncate path if a budget still needs to be enforced. - -## Summary Generation - -Use the current effective model for summary generation. Do not introduce a -separate compact model in the first PR. - -Summary max output: - -```text -max_summary_tokens = min(model.max_output_tokens if set, 4096) -``` - -If the model has no configured output cap, use 4096. - -The prompt should ask for a concise structured handoff summary, but should not -require rigid section names. The content must cover: - -- Original task and current objective. -- Completed work and current progress. -- Key decisions. -- Relevant technical context, including files, functions, PRs, errors, and user - constraints. -- Next steps. - -The summary is a checkpoint, not a full transcript. Recent messages are -preserved verbatim and do not need to be repeated. - -The summary must summarize only `messages_to_summarize`. The preserved recent -tail is appended after the checkpoint and should not be repeated except when a -small bridge is needed for continuity. - -Original task anchoring: - -- The first compact PR does not separately pin the first user message as raw - context. -- The summary prompt must explicitly require preserving the original user task - and current objective. -- The latest user message and recent safe blocks preserve immediate continuity; - the summary carries the older task anchor. -- Repeated compactions rely on prompt-level inheritance. If a prior checkpoint - summary appears in `messages_to_summarize`, the new summary must merge and - carry forward its original task and current objective instead of treating the - prior checkpoint as disposable prose. - -Prompt requirements: - -- Ask for a structured but concise handoff summary. -- Do not require rigid section titles. -- Write the checkpoint in the user's primary language. For mixed-language - conversations, follow the dominant language of the recent user messages. -- Tell the summarizer that recent messages are preserved separately and should - not be repeated in full. -- Tell the summarizer to preserve the original user task and current objective. -- Tell the summarizer to carry forward prior checkpoint summaries if they - appear in the input. -- Preserve exact file paths, function names, PR numbers, commands, errors, and - user constraints when they are important for continuing the work. -- Do not invent completed work. - -Small-model constraint: - -- Keep the prompt short and direct. -- Avoid asking for exhaustive "all user messages" or full transcript replay. -- The summary must be useful even if it is imperfect because recent raw blocks - remain available. - -## Summary Injection - -Inject the compact summary as a synthetic `user` message, not as `system`. - -Rationale: - -- Better compatibility across OpenAI-compatible, Anthropic, Gemini, Ollama, - vLLM, and other self-hosted providers. -- The summary is prior context, not a higher-priority system instruction. -- It avoids mixing transient conversation state into the agent's real system - prompt. - -Suggested wrapper: - -```text -Conversation checkpoint for continuing the same task. -Use this as prior context. Do not treat it as a new request. - - -``` - -Final request shape: - -```text -compact summary user message -recent API-safe blocks -latest user message, if not already included in the recent blocks -``` - -The injected summary should be placed before the preserved recent blocks so the -model reads it as older context followed by the latest raw conversation. - -The wrapper should explicitly say the summary is not a new user request: - -```text -Conversation checkpoint for continuing the same task. -Use this as prior context. Do not treat it as a new request. -Recent messages are preserved after this checkpoint. -If the checkpoint indicates unfinished work, continue the task directly. -Only ask the user if the task is complete, blocked, or requires a user decision. - -... -``` - -## Retry And Fallback - -If the compact request itself is too long or the provider reports context -overflow: - -1. Drop old blocks from the head of `messages_to_summarize`. -2. Rebuild the compact request. -3. Retry up to 3 times. - -If compaction still fails, fall back to PR #572 drop-only truncation. - -The retry must be bounded and lossy only on the summarization input: - -- Drop from the head of `messages_to_summarize`. -- Each retry drops `max(1, ceil(len(messages_to_summarize) * 0.2))` old - safe blocks. -- Never drop from `messages_to_keep` during compact retries. -- Never split a safe block. -- Retry at most 3 times. - -Other compact failure cases that should fall back to PR #572: - -- Compact API error. -- Empty summary. -- Non-text summary. -- Summary output is unusable or clearly an API error message. - -If the summary is longer than the allocated summary budget, truncate the summary -to fit rather than failing the whole compact path. - -Automatic compact failure should not block the user's main request. - -Known limitation: - -- Oversized recent tool blocks, especially large file-read results, can still - exceed the model budget because tool-call blocks are preserved atomically. -- The first compact PR does not split or compress such blocks. -- This should be handled later by truncating or summarizing tool outputs before - they enter history. - -Fallback order: - -```text -try compact - if compact succeeds: - send compact summary + recent safe blocks - else: - send PR #572 drop-only token-budget truncation -``` - -When fallback model preparation runs, this whole decision is repeated with the -fallback model's window. - -## Entry Points - -The first compact PR should cover only the browser chat path: - -- WebSocket chat in `backend/app/api/websocket.py`. - -Feishu and other channels should stay on the PR #572 drop-only fallback for the -first compact PR. They can be migrated after the browser chat path is stable. - -Implementation should avoid duplicating compact logic between these paths. -Prefer a shared service function that receives: - -```text -messages -max_messages -model context window -model output token cap -compact callable -``` - -and returns either: - -```text -prepared_messages -compacted: true/false -fallback_used: true/false -``` - -## Test Plan - -Add focused tests for: - -- Compaction triggers above 80%. -- No compaction below 80%. -- No compaction when `context_window_tokens` is missing. -- Latest user message is preserved. -- Tool-call blocks remain atomic. -- Compact failure falls back to drop-only truncation. -- Compact context overflow retries from the head up to 3 times. -- Fallback model preparation recomputes budgets with the fallback model window. -- Summary is inserted as a synthetic user message before preserved recent - blocks. -- Missing `context_window_tokens` keeps the old message-count behavior. diff --git a/.clawith-local-designs/LONG_RUNNING_CHECKPOINT_DESIGN.md b/.clawith-local-designs/LONG_RUNNING_CHECKPOINT_DESIGN.md deleted file mode 100644 index c9edaf758..000000000 --- a/.clawith-local-designs/LONG_RUNNING_CHECKPOINT_DESIGN.md +++ /dev/null @@ -1,85 +0,0 @@ -# Long-Running Checkpoint Design - -This document captures a later Clawith-specific optimization that is not part -of the first compact PR. - -## Idea - -Base compact should keep short summaries in the model context. Long-running -tasks may also need a detailed external checkpoint that can preserve exact task -details without bloating every prompt. - -The proposed second layer is: - -```text -compact summary -> short, included in context -workspace checkpoint -> detailed, written to a workspace file, read on demand -``` - -## Motivation - -Clawith agents can run long tasks where the original user task, detailed -progress, decisions, files, tests, and errors should not disappear after several -compactions. - -A short compact summary should not carry every detail. A workspace checkpoint -can preserve richer information while keeping normal prompts small. - -## Candidate File Layout - -```text -.clawith/checkpoints///latest.md -``` - -The path must isolate agents and conversations to avoid concurrent overwrite. - -## Candidate Checkpoint Content - -```markdown -# Clawith Task Checkpoint - -## Original Task -## Current Objective -## Detailed Progress -## Decisions -## Files Read -## Files Modified -## Commands And Tests -## Errors And Fixes -## Open Questions -## Next Steps -``` - -## Runtime Behavior - -When compact runs: - -1. Generate the short compact summary for context. -2. Generate or update a detailed checkpoint file if a writable workspace exists. -3. Add only a short path hint to the model context. -4. Do not read the checkpoint by default. -5. The agent reads the checkpoint only when exact older details are needed. - -Checkpoint write failure should not block compact. - -## Risks To Resolve Before Implementation - -- Workspace write policy and user visibility. -- `.gitignore` and repository pollution. -- Sensitive value filtering for API keys, tokens, and credentials. -- Multi-agent and multi-session concurrency. -- Cleanup/retention strategy. -- Behavior for Feishu/Slack/Teams channels without a clear workspace. -- Consistency between DB compact summary and workspace checkpoint. - -## Relationship To The First Compact PR - -The first compact PR should not implement this layer. It should only provide: - -- 80% trigger. -- model-metadata-based budget. -- short summary compaction. -- recent safe-block preservation. -- drop-only fallback. - -This checkpoint layer can be added after the base compact path is stable. diff --git a/.clawith-local-designs/context-compact-feishu-doc.xml b/.clawith-local-designs/context-compact-feishu-doc.xml deleted file mode 100644 index 8aa8ec441..000000000 --- a/.clawith-local-designs/context-compact-feishu-doc.xml +++ /dev/null @@ -1,91 +0,0 @@ -Clawith 上下文压缩策略设计讨论 - - -

这份文档用于讨论 #572 之后的下一步 compact 策略。第一版目标是先对齐 Codex / Claude Code 的主流处理方式:按当前模型窗口触发、用 summary compact 保留任务连续性、失败时回退到已有的 80% 丢弃兜底。

-
- -

一、背景和已完成基础

-
    -
  • #564 已补齐 tool call / tool result 成对处理,历史裁剪不会破坏工具调用协议。
  • -
  • #572 已实现 80% token-budget 的 drop-only fallback,窗口来自后端模型 metadata / config,不按模型名写死。
  • -
  • 下一步 compact 不替代 #572,而是在它前面加一个更优先的 summary compact;compact 失败时仍走 #572 的保守兜底。
  • -
- -

二、第一版范围

- - - - - - - - - - -
项目决策
入口只做浏览器上的 chat / WebSocket 模型调用链路。
不做内容Feishu 等其他渠道、前端配置入口、DB 持久化 compact 条目、独立 compact 模型、workspace checkpoint、Claude-style collapse / microcompact 都不放第一版。
模型选择summary 使用当前 effective model,不单独切 compact model,降低多模型配置和小模型兼容复杂度。
窗口来源每次调用时从当前模型 metadata / config 获取 context_window_tokens,不写死窗口大小。
- -

三、触发条件

-
    -
  • 每次 LLM call 前估算当前请求 token。
  • -
  • 当 estimated_tokens > context_window_tokens * 0.8 时触发 compact。
  • -
  • primary model 和 fallback model 都要在实际调用前基于各自模型窗口重新计算预算,避免模型切换后沿用旧预算。
  • -
- -

四、保留和压缩策略

-
    -
  • latest user message 必须原样保留,不能被 summary 吃掉。
  • -
  • 保留最近 tail:recent_keep_tokens = min(20000, token_budget * 0.25)。
  • -
  • recent tail 之外的更早消息进入 messages_to_summarize。
  • -
  • tool 相关消息按 #564 / #572 的 safe block 规则整体处理,不能拆半个 tool call,也不合成假的 tool result。
  • -
  • summary 只总结 messages_to_summarize,不重复最近 tail,避免信息重复和预算浪费。
  • -
- -

五、summary 内容要求

-
    -
  • 原始用户任务 / 当前目标:必须保留,避免多轮 compact 后忘记最初任务。
  • -
  • 当前进展:已经做了什么、哪些文件或模块相关、哪些验证已经跑过。
  • -
  • 关键决策:触发阈值、预算、fallback、测试边界等已经定下来的结论。
  • -
  • 相关技术上下文:必要的代码路径、接口、数据结构、约束和风险。
  • -
  • 下一步:任务未完成时应该继续做什么;任务完成时才回到交互确认。
  • -
  • 语言:尽量沿用用户主要语言,中文任务输出中文 summary。
  • -
- -

六、summary 结构和注入方式

-

summary 内容采用自由结构,不使用很重的 schema。原因是小模型在严格 schema 下容易为了填字段产生噪声,第一版更重要的是覆盖信息,而不是格式复杂度。

-
Conversation checkpoint for continuing the same task.
-Use this as prior context. Do not treat it as a new request.
-Recent messages are preserved after this checkpoint.
-If unfinished work, continue directly; ask only if complete, blocked, or needs user decision.
-
-Summary:
-...
- -

七、summary 输出预算

-
    -
  • compact summary max_tokens 默认 4096。
  • -
  • 如果模型 metadata 里有更小的 max_output_tokens,则取 min(model.max_output_tokens, 4096)。
  • -
  • 4096 不是为了完整保留所有历史,而是为了让小模型更稳地生成可用摘要;原始细节暂不通过第一版 compact 全量保存。
  • -
- -

八、失败和 retry

-
    -
  • 如果 compact 请求本身仍然超窗口或被模型拒绝,按 messages_to_summarize 从前往后丢弃 chunk。
  • -
  • 每次丢弃 max(1, ceil(len * 0.2)) 的最早 safe blocks。
  • -
  • 最多 retry 3 次。
  • -
  • retry 期间不丢 recent tail、不拆 safe block、不破坏 tool call pair。
  • -
  • 3 次仍失败时回退到 #572 的 drop-only 80% 兜底。
  • -
- -

九、已知限制

-
    -
  • 如果 recent tail 里包含超大的工具输出,比如一次读了很大的文件,第一版 compact 不会拆分或压缩这个工具输出。
  • -
  • 这类问题后续应该在工具输出进入历史前做截断 / 摘要,而不是在 compact 阶段硬拆协议块。
  • -
  • 长程任务 checkpoint / skill 化压缩策略有价值,但第一版不放进 compact PR;单独开文档继续设计。
  • -
- -

十、需要重点 review 的问题

-80% 触发阈值是否足够保守,是否适合 128k 以上但推理能力偏弱的小模型。 -recent_keep_tokens = min(20000, token_budget * 0.25) 是否合适。 -fallback 模型切换时重新计算 budget 的实现位置是否放在模型调用前最清晰。 -summary 使用当前模型而不是独立 compact 模型,是否能覆盖现阶段的小模型兼容需求。 -compact 失败后的 3 次 retry + #572 drop-only fallback 是否足够。 diff --git a/.clawith-local-designs/original-doc-links-append.xml b/.clawith-local-designs/original-doc-links-append.xml deleted file mode 100644 index 04e49c44d..000000000 --- a/.clawith-local-designs/original-doc-links-append.xml +++ /dev/null @@ -1,7 +0,0 @@ -
-

十一、原始技术文件

-

下面两份是本次讨论过程中沉淀的原始技术材料,保留更完整的方案细节,后续实现时优先对照这些文档。

- diff --git a/V1.11.4_REGRESSION_TEST_CASES.md b/V1.11.4_REGRESSION_TEST_CASES.md deleted file mode 100644 index 703348409..000000000 --- a/V1.11.4_REGRESSION_TEST_CASES.md +++ /dev/null @@ -1,609 +0,0 @@ -# Clawith v1.11.4 版本回归与测试文档 - -## 1. 文档目的 - -本文档用于执行 Clawith v1.11.4 候选版本的专项回归、全量回归和发布前验收。 - -测试范围由三类事实合并生成: - -1. CoAligne 中的候选修复对话及版本聚合记录,用于还原用户问题、验收路径和人工测试重点。 -2. `upstream/main...v1.11.4` 中的 Lore commit message,用于提取约束、被拒绝方案、不可回归指令和历史测试缺口。 -3. 当前聚合分支的代码、测试和本地验证结果,用于确认本轮真实待测范围。 - -本文档中的“历史证据”不能替代本轮聚合版本回归。只有在目标部署上执行并记录证据的 Case,才可标记为本轮通过。 - -## 2. 版本基准 - -### 2.1 Git 基准 - -| 项目 | 当前值 | -| --- | --- | -| 聚合分支 | `v1.11.4` | -| 基线 | `upstream/main` / `251aeba8` | -| 当前候选代码提交 | `9a3e291b` | -| PR #827 | `fa58883d`:Vercel 异步部署等待 | -| PR #833 | `ae3eb9c9`:Trigger 时区与 occurrence 修复 | -| PR #837 | `d5525db4`:默认 Agent 只初始化一次 | -| PR #927 | `328eaffa`:群聊 `@` 候选列表滚动 | -| PR #928 | `535cb539`(源提交 `e00b05e3`):Direct Chat Session 运行态隔离 | -| 最新 main | PR #826 / #836 / #842:OAuth/SSO 浏览器绑定、图片超时对账、Feishu/Teams webhook 认证 | - -执行测试前必须重新记录:待测 commit、前后端镜像 ID、部署时间、数据库 migration head、浏览器加载的前端资源版本。不能只依赖 `/api/version` 判断部署是否为本候选版本。 - -### 2.2 当前版本号风险 - -当前候选分支的 `backend/VERSION` 和 `frontend/VERSION` 仍为 `1.11.3`。正式发布 v1.11.4 前必须确认版本号更新策略,并验证前端、后端、镜像标签和发布说明一致。该项未确认前,A2 Case 不得通过。 - -### 2.3 改动范围与核心风险 - -| 模块 | 用户问题 / 改动 | 核心回归风险 | -| --- | --- | --- | -| Vercel Tool | 部署已被接受但仍在构建时,Runtime 不能把它当成功;需要等待精确 deployment 终态 | 重复创建项目、重复上传/部署;等待不结束;错误 deployment 被结算 | -| Trigger | 修复时区写入、有效时区、Cron occurrence、dispatch、幂等与失败重试链路 | 错时区、漂移、重复触发、漏触发、失败 occurrence 被提前消费 | -| 默认 Agent | Morty/Meeseeks 删除、停止或重命名后不得重新创建 | 重启后复活;多实例重复创建;存储自愈被错误禁用 | -| 群聊 `@` | 超过 8 个候选时全部成员可通过鼠标、触控板和键盘访问 | 第 9 个以后不可达;高亮项跑出可视区;mention identity 或 IME 回归 | -| Direct Chat Session | 一个 Session 运行中切换到另一个 Session,输入框与停止按钮必须只反映当前 Session | 旧 Session 状态串入新 Session;后台补发污染当前消息;停止错 Run | -| 最新 main 兼容 | OAuth/SSO 浏览器绑定、Feishu/Teams webhook 认证、图片超时 unknown-result 对账 | 跨浏览器换码;伪造渠道入口;错误 serviceUrl;图片请求被自动重放 | - -### 2.4 当前 P0 Migration 修复状态 - -`upstream/main@251aeba8` 原本存在 `f061_default_tenant_timezone` 和 `f061_enterprise_info_tenant_id` 两个 Alembic head;Drone build #486 从空数据库执行 migration 时,后者对初始 schema 已存在的 `enterprise_info.tenant_id` 再次 `ADD COLUMN`,触发 `DuplicateColumnError`。 - -PR #945 已将 `f061_enterprise_info_tenant_id` 改为按实际 schema 幂等执行;v1.11.4 聚合分支另以 `f063_merge_v1_11_4_heads` 合并时区与 Tool Runtime 两条迁移链。本地 `alembic heads` 已收敛为该唯一 head,Drone build #489 的 fresh DB migration 已通过。上一版本数据库升级路径尚未执行,因此 A3 的 migration 阻断已解除,但 A4 和数据库升级 P0 仍待 upgrade DB 实证。 - -## 3. Lore 决策转成的发布守则 - -以下规则是本版本的不可回归项。任何一项被违反,都应判定对应模块失败: - -1. Vercel 接受部署不等于部署成功;仅 `READY` 成功,`ERROR`/`CANCELED` 失败,非终态继续等待。 -2. Vercel poll 必须按精确 `deployment_id` 查询,不得查询 deployment list,也不得重放创建项目、上传、仓库关联或 deployment POST。 -3. Trigger 有效时区固定为 `Agent.timezone -> Tenant.timezone -> Asia/Shanghai`;不得静默退回 UTC。 -4. Cron occurrence 只能由 evaluator 计算一次,daemon、dispatch、幂等键、queue 和 Runtime intake 必须原样传递同一个 `scheduled_at`。 -5. 不得从 `last_fired_at` 重新推导 occurrence;保留 30 秒 grace,不做历史补偿触发。 -6. scheduled Trigger intake 失败时不得消费 occurrence identity;应在 grace 内由普通扫描再次尝试。Webhook 的同步失败回执语义保持不变。 -7. 无效 Cron 必须在 REST 和 Agent Tool 更新边界、数据库 mutation/commit 之前被拒绝。 -8. 默认 Agent 是否初始化不得由名称或运行状态判断;删除、停止、重命名都不得触发补建。 -9. 默认 Agent 初始化与未删除 Agent 的非覆盖式 storage repair 是两个独立判断。 -10. 群聊 mention 不得恢复 8 人上限;键盘高亮变化必须同步候选弹层自身的滚动位置,同时保留结构化 participant ID 和 IME 行为。 -11. Direct Chat 的停止、等待、对账和输入限制只能由当前选中的 Agent/Session 运行态驱动;后台 Session 继续运行,但不得改写可见聊天 UI。 -12. OAuth state 与 QR SSO session 必须绑定创建它们的浏览器,跨浏览器交换不得获得登录结果。 -13. Feishu webhook 必须验证 token、签名并处理加密 envelope;Teams 必须验证 Bot Framework JWT audience,并只信任与 claim 绑定的 serviceUrl。 -14. 图片生成超时必须保留为 unknown Tool receipt,经用户明确确认后结算;不得自动重放外部图片生成请求。 - -## 4. 测试环境与数据准备 - -### 4.1 环境 - -- 一套可重启的完整 Clawith 环境:Frontend、Backend、Runtime worker、PostgreSQL、Redis。 -- 可查看 Backend、Trigger daemon、Runtime intake 和 Tool execution 日志。 -- 可查询测试数据库中的 `agents`、`tenant_settings`、Trigger execution/queue/receipt 相关记录。 -- 一个隔离的 Vercel 测试账号和项目,允许真实创建、查询和取消 deployment。 -- Chrome 或 Chromium,支持检查 DOM、WebSocket、Network 和滚动位置。 -- 两个相互隔离的浏览器 profile,用于 OAuth/QR SSO 跨浏览器负向验证。 -- Feishu/Teams webhook 签名与 JWT 测试材料,以及可控制超时的图片生成 Provider stub。 - -### 4.2 测试数据 - -1. 新租户 T-New:从未执行默认 Agent 初始化。 -2. 兼容租户 T-Legacy:存在旧 `_bootstrap/.seeded` 或历史 Morty/Meeseeks 行,但不存在 `bootstrap:default_agents:v1`。 -3. 两个时区不同的租户/Agent:例如 Tenant=`Asia/Shanghai`,Agent=`America/Los_Angeles`。 -4. 一个 Agent 时区为空的继承场景,以及 Agent/Tenant 时区都为空的平台兜底场景。 -5. 至少三个 Cron Trigger:正常触发、故意制造 intake 失败、用于修改为非法表达式。 -6. 一个包含至少 12 个可 `@` 成员的群聊,成员名需要能通过关键字筛选。 -7. 一个可以稳定进入 `BUILDING` 后再进入 `READY` 的 Vercel deployment;另准备可观察 `ERROR` 或 `CANCELED` 的 deployment。 -8. 一个可创建至少两个 Direct Chat Session 的 Agent:Session A 可保持运行/等待,Session B 保持空闲并可独立发消息。 - -统一记录 Tenant、Agent、Group、Session、Run、Trigger、execution、deployment ID,以及执行时间和时区。 - -## 5. 发布门禁 - -| 优先级 | 门禁 | 通过条件 | -| --- | --- | --- | -| P0 | 候选版本身份 | commit、镜像、migration head、前端资源可互相对应;版本号策略已确认 | -| P0 | 数据库升级 | 全新数据库与上一版本数据库均可升级到修复后的唯一 head;本地唯一 head 与 Drone #489 fresh DB 已通过,上一版本 upgrade DB 尚待验证,状态仍为阻断 | -| P0 | 专项主链 | B1–B4、C1–C8、D1–D6、E1–E5、F1–F6、G1–G6 全部通过 | -| P0 | 自动化 | 后端专项、前端测试、前端构建、架构守卫均通过 | -| P1 | 全量回归 | Backend 全量 pytest 通过,或所有失败均有确认的基线证据和放行记录 | -| P1 | 真实环境 | Vercel、PostgreSQL 重启/多实例、真实群聊与 Direct Chat WebSocket、隔离浏览器 OAuth/SSO、真实渠道 webhook 验证完成 | - -P0 失败不得发布。P1 未完成必须登记为明确的发布风险,由版本负责人书面放行,不能静默标记为通过。 - -## 6. 基线与升级 Case - -### A1 候选提交与改动范围 - -步骤: - -1. 记录 `git rev-parse HEAD` 和实际部署镜像 digest。 -2. 核对候选分支包含 PR #827、#833、#837、#927、#928,并已合入 `upstream/main@251aeba8`。 -3. 核对 `git diff upstream/main...HEAD` 不包含计划外业务改动。 - -预期:候选代码与本文件 2.1 的基准一致;如后续追加 commit,必须补充变更说明和受影响 Case。 - -### A2 版本标识一致性 - -步骤:检查 `backend/VERSION`、`frontend/VERSION`、镜像标签、UI 版本信息和发布说明。 - -预期:正式发布物统一标识为 v1.11.4;不得出现新代码被标识为 v1.11.3 的情况。 - -### A3 全新数据库启动 - -步骤: - -1. 使用空 PostgreSQL 数据库启动完整服务。 -2. 等待 entrypoint 自动执行 migration。 -3. 执行 `alembic current --check-heads`。 -4. 创建首个租户和管理员,完成基础登录。 - -预期:启动成功;migration 收敛为一个经过 fresh/upgrade 双路径验证的 head;新 Tenant 默认时区为 `Asia/Shanghai`;`enterprise_info.tenant_id` 不被重复添加。 - -### A4 从上一版本升级 - -步骤: - -1. 使用 v1.11.3 数据库快照启动上一版本,确认可用。 -2. 保留数据库并切换到 v1.11.4 候选镜像。 -3. 等待自动 migration,检查 Backend、worker 和 Trigger daemon。 -4. 抽查原有 Tenant、Agent、Trigger、Session 和默认 Agent 数据。 - -预期:升级成功且无数据丢失;Tenant 时区回填符合 migration 设计;默认 Agent 只建立初始化事实,不错误创建或删除 Agent。 - -## 7. PR #827:Vercel 异步部署回归 - -### B1 BUILDING 返回 durable pending - -步骤:发起真实或受控的 `vercel_deploy`,让 provider 首次返回 `INITIALIZING`、`QUEUED`、`BUILDING` 或 `PENDING`。 - -预期: - -- Tool execution 进入异步 pending/waiting 状态,不返回最终成功。 -- metadata 包含相同的 `deployment_id`、`runtime_async_pending=true`。 -- `operation_key` 为 `vercel:deployment:{deployment_id}`。 -- 不需要模型自行调用等待或 deployment list Tool。 - -### B2 精确 deployment poll - -步骤:观察后续 poll 的 provider 请求与 Tool 参数。 - -预期:只查询 `GET /v13/deployments/{deployment_id}`;poll 参数为 `operation=poll` 和原始 `deployment_id`;不得查询项目 deployment list。 - -### B3 READY 正常结算 - -步骤:让 B1 的 deployment 最终进入 `READY`。 - -预期:原 execution/receipt 被结算为 succeeded;`runtime_async_pending=false`;operation key 不变;最终 URL 与精确 deployment 对应;Run 能继续并结束。 - -### B4 ERROR/CANCELED 失败结算 - -分别验证 provider 进入 `ERROR` 和 `CANCELED`。 - -预期:原 execution 被结算为 failed,不得显示成功;错误信息保留 deployment ID 和可诊断 provider 状态;Run 不应无限等待。 - -### B5 poll 不重放外部写入 - -步骤:统计同一 operation 从首次部署到多轮 poll 的 provider 调用。 - -预期:poll 期间项目创建、blob 上传、GitHub repository link 和 deployment POST 的调用次数均不增加;首次已确认 receipt 被复用。 - -### B6 错配、未知和瞬时读取失败 - -分别模拟 deployment ID 错配、缺失/未知状态、读取超时。 - -预期:不得结算为成功;错配或无法确认的 observation 保持 unknown/pending 或按合同失败;瞬时读取超时不得触发重复 launch 写入。 - -### B7 兼容与范围边界 - -预期: - -- `vercel_list_deployments` 仍是普通只读 Tool,不具备等待语义。 -- upload 模式和现有 GitHub repo 模式均可发起部署。 -- 该修复不承诺恢复上线前已卡住的旧 Run,也不包含通用 poll deadline/backoff 策略;如发现旧 Run,单独登记,不误判为新路径回归。 - -## 8. PR #833:Trigger 时区与触发链路回归 - -### C1 Tenant/Agent 时区写入校验 - -分别通过 REST/API 更新 Tenant 和 Agent: - -- 合法完整 IANA 时区:`Asia/Shanghai`、`America/Los_Angeles`。 -- 非法值、空白值、拼写错误。 -- Agent 时区设为 `null`,使用 Tenant 继承。 - -预期:合法值保存;非法值在 mutation/commit 前返回校验错误;Agent `null` 保持继承语义。 - -### C2 有效时区优先级与展示一致 - -组合验证: - -1. Agent 和 Tenant 都有值,使用 Agent。 -2. Agent 为空、Tenant 有值,使用 Tenant。 -3. Agent 和 Tenant 都为空,使用 `Asia/Shanghai`。 - -预期:Agent 详情展示的 effective timezone 与 evaluator 实际使用时区一致,不出现静默 UTC。 - -### C3 evaluator 计算唯一 occurrence - -步骤:创建 Cron Trigger,记录 evaluator 输出的 `scheduled_at`,沿 daemon、dispatch、execution key、queue 和 Runtime source 追踪。 - -预期:全链路使用完全相同的 UTC instant;下游不按当前时间或 `last_fired_at` 再计算。 - -### C4 不受 last_fired_at 漂移影响 - -步骤:构造与当前计划不一致的旧 `last_fired_at`,执行 evaluator。 - -预期:当前 occurrence 由 Cron、有效时区和当前扫描窗口确定,不因历史完成时间逐轮漂移。 - -### C5 30 秒 grace 与创建时间下界 - -分别在 occurrence 后 0–30 秒和超过 30 秒扫描,并创建一个刚建立、但上一 occurrence 早于 Trigger `created_at` 的 Trigger。 - -预期:grace 内可注册;超过 grace 不做历史 catch-up;早于创建时间的 occurrence 不执行。 - -### C6 intake 失败后 grace 内可重试 - -步骤:第一次 scheduled Runtime intake 强制失败,下一次 15 秒 daemon 扫描恢复成功。 - -预期:第一次失败回滚 execution/receipt,不消费 occurrence identity;同一 `scheduled_at` 在 30 秒 grace 内可再次注册且最终只成功执行一次。 - -### C7 并发注册与幂等 - -步骤:让两个 daemon/worker 同时尝试注册同一 Trigger occurrence。 - -预期:幂等键包含 evaluator 提供的 occurrence;数据库最终只有一次有效执行,不重复启动 Runtime。 - -### C8 非法 Cron 更新 - -分别通过 REST update 和 Agent Tool partial update 写入非法 Cron,再验证合法更新和未修改 Cron 的 partial update。 - -预期:非法值在 commit 前拒绝,数据库保留旧配置;合法值可更新;partial update 不破坏已有合法 Cron。 - -### C9 Webhook 与非 Cron 兼容 - -预期:Webhook intake 失败仍保留原有同步失败 receipt;`cooldown_seconds` 和非 Cron Trigger 行为不因本修复改变。 - -### C10 已知范围外边界 - -DST 切换边界、秒级 Cron、历史 backfill 和新增 `next_run_at` 状态不在本修复承诺内。如版本需要支持这些能力,应新增独立 Case,不能从当前自动化结果推断已支持。 - -## 9. PR #837:默认 Agent 一次性初始化回归 - -### D1 新租户首次创建 - -步骤:为 T-New 完成首个用户注册或首次启动初始化,记录 Morty/Meeseeks ID 和 `tenant_settings`。 - -预期:各创建一个 Agent;设置 key 为 `bootstrap:default_agents:v1`,保存稳定 ID;Agent 与设置在同一数据库事务内成立。 - -### D2 重启不重复创建 - -步骤:连续重启 Backend 两次,并再次触发首用户初始化入口。 - -预期:Morty/Meeseeks 数量和 ID 不变;不得因重复调用 seeder 创建副本。 - -### D3 删除后不复活 - -步骤:逻辑删除 Morty,重启 Backend 并再次运行 seeder。 - -预期:已删除 Morty 不恢复、不补建、不执行 storage repair;Meeseeks 保持原 ID。 - -### D4 停止和重命名后不补建 - -分别停止 Meeseeks、重命名 Morty,然后重启。 - -预期:不创建同名副本;`status` 和 `name` 不参与“是否初始化”的判断。 - -### D5 未删除 Agent 的 storage repair - -步骤:删除仍存活默认 Agent 的 workspace 根目录或 Skills 目录,保留用户自建文件,再重启。 - -预期:只补齐缺失目录/内置内容,不覆盖用户文件;已删除 Agent 不 repair。 - -### D6 旧部署懒回填 - -分别验证: - -1. legacy marker 指向 stopped/renamed/已删除历史 Agent。 -2. marker 缺失,但数据库存在 canonical 历史行。 -3. marker value 损坏。 - -预期:发现可信历史事实时写入 tenant setting,但不补建缺失 Agent;损坏的数据库初始化标记采取保守策略,不重新创建。 - -### D7 marker 合并兼容 - -步骤:在 `_bootstrap/.seeded` 预置其他 seeder 条目,例如 `okr_agent`,再执行默认 Agent 初始化。 - -预期:默认 Agent 条目以追加/合并方式写入,其他 marker 不被覆盖。 - -### D8 多实例并发初始化 - -步骤:使用真实 PostgreSQL,同时启动两个 Backend 实例为同一新租户执行初始化。 - -预期:transaction advisory lock 生效;最终每个默认 Agent 只有一个,tenant setting 完整且 ID 对应正确。 - -### D9 旧逻辑已创建副本的处理边界 - -预期:升级过程不自动删除旧逻辑已经创建且仍活跃的 Agent;这是保守兼容策略。人工清理后再次重启,不得继续补建。 - -## 10. PR #927:群聊 `@` 候选列表回归 - -### E1 超过 8 个候选全部可达 - -步骤:在 12 人群聊输入 `@`,检查候选 DOM 和列表总数。 - -预期:至少 12 个匹配成员全部渲染在有最大高度的弹层中;不得只保留前 8 个。 - -### E2 鼠标、触控板与触摸滚动 - -步骤:分别用鼠标滚轮、触控板和触摸手势滚动候选弹层。 - -预期:弹层 `scrollTop` 变化;第 9 个以后成员可见、可点击;页面或外层聊天区不被错误带动。 - -### E3 键盘高亮自动跟随 - -步骤:连续按 `ArrowDown` 到第 9–12 个候选,再按 `ArrowUp` 返回;验证首尾 wrap。 - -预期:高亮项始终进入弹层可视区;弹层局部滚动同步;上下键和首尾循环行为正常。 - -### E4 筛选、取消和提交 - -步骤:输入 `@` 后继续输入关键字,验证候选缩小;再验证 `Escape`、`Enter` 和 `Tab`。 - -预期:筛选后高亮与滚动位置合理重置;Escape 关闭;Enter/Tab 选择当前高亮项;输入框文本正确。 - -### E5 结构化 mention identity - -步骤:选择第 9 个以后、存在重名可能的成员并发送消息,检查前端 payload、后端消息和路由结果。 - -预期:使用被选成员的真实 `participant_id`,不能只靠显示名;消息被正确路由。 - -### E6 IME 与编辑回归 - -步骤:使用中文输入法组合输入 mention 查询词,在 composition 未结束时按 Enter,再完成输入并选择候选。 - -预期:composition 期间不误提交消息;mention 候选和最终消息正常。 - -### E7 边界布局 - -在窄窗口、弹层靠近视口底部、长成员名和快速筛选场景下重复 E1–E4。 - -预期:弹层不超出可操作区域,不抖动、不遮挡当前高亮项,无控制台错误。 - -## 11. PR #928:Direct Chat Session 运行态隔离回归 - -### F1 运行中切换到空闲 Session - -步骤:在同一 Agent 的 Session A 发起长任务,确认出现运行指示和停止按钮;不停止任务,立即切换到空闲 Session B。 - -预期:Session B 输入框可输入和发送新消息;不显示 Session A 的停止按钮、等待提示或 Tool 对账卡;Session A 在后台继续运行。 - -### F2 切回运行中的 Session - -步骤:完成 F1 后切回 Session A,再次切到 Session B。 - -预期:Session A 恢复自己的运行进度和精确停止按钮;Session B 仍保持独立可输入状态;多次切换不产生闪烁或串态。 - -### F3 两个 Session 独立执行 - -步骤:Session A 运行期间,在 Session B 发送新任务;观察两条 WebSocket、消息列表和 Runtime state 请求。 - -预期:两条 Session lane 独立;每个 Session 只展示自己的消息、运行指示与终态;任一 Session 完成都不清空或覆盖另一 Session。 - -### F4 后台连接延迟恢复 - -步骤:让 Session A 在 WebSocket 断开/重连期间产生待发消息,随后切换到 Session B,再恢复 Session A 连接。 - -预期:待发消息只发送到 Session A;Session B 的输入框、消息列表、等待状态和停止按钮不被后台补发修改。 - -### F5 waiting_user 与 Tool 对账隔离 - -步骤:让 Session A 进入 `waiting_user` 或产生 unknown Tool receipt,再切到 Session B。 - -预期:继续/对账限制和确认卡只出现在 Session A;Session B 不被禁用,可正常创建独立 Run。 - -### F6 精确停止当前 Session Run - -步骤:两个 Session 均存在活动 Run 时,分别打开 Session A、Session B 并点击停止,记录发送的 `session_id` 和 `run_id`。 - -预期:停止按钮只针对当前 Session 的可取消 Run;不得停止后台另一个 Session;终态刷新后按钮及时消失。 - -## 12. 最新 main 安全与对账兼容回归 - -### G1 OAuth state 浏览器绑定 - -步骤:浏览器 A 发起 OAuth;分别在 A 和隔离浏览器 B 使用回调 state/code,并测试缺失、过期、篡改 state。 - -预期:只有创建 state 的浏览器 A 可完成换码;B 及所有无效 state 均被拒绝,且不创建登录会话。 - -### G2 QR SSO session 浏览器绑定 - -步骤:浏览器 A 创建扫码会话,完成扫码后分别由 A、B 查询 token;再验证过期和不存在的 session。 - -预期:只有 A 可读取结果;B 无法借用 session 获得 token;失败路径不泄露登录状态。 - -### G3 Feishu webhook 认证 - -分别发送合法 verification token、合法签名、加密 envelope,以及错误 token、错误签名、篡改密文和缺少必要头的请求。 - -预期:合法请求被解密并处理一次;非法请求在进入业务处理前拒绝;不得产生伪造消息或 Runtime Run。 - -### G4 Teams JWT 与 serviceUrl 绑定 - -分别验证正确 Bot Framework JWT/audience、错误 audience、无 token、过期 token,以及 body 中 serviceUrl 与 JWT claim 不一致。 - -预期:仅合法 JWT 被接受;回复 endpoint 只保存 claim 允许的 serviceUrl;不得向攻击者提供的地址转发 bearer token。 - -### G5 图片超时 unknown-result 对账 - -步骤:让图片生成 Provider 在可能已受理请求后超时,打开 Direct Chat runtime-state,并分别确认“已生效”和“未生效”。 - -预期:execution 保持 unknown 并显示现有对账控件;确认只结算原 receipt,不自动重放图片请求;后续重试必须是新 Tool call。 - -### G6 作用域与兼容负向验证 - -步骤:尝试跨 Tenant、跨 Session 读取/结算图片 unknown receipt,并回归普通成功/明确失败的图片调用及现有 Feishu/Teams 正常消息。 - -预期:跨作用域操作失败关闭;非超时 Tool 语义不变;渠道正常消息与回复地址持久化不回归。 - -## 13. 组合回归 - -### H1 Agent Tool schema 合并完整性 - -本分支合并 PR #833 时在 `backend/app/schemas/schemas.py` 发生过冲突。 - -预期:时区 `field_validator`、既有 `field_serializer` 和 `validate_timezone_name` 均保留;应用可正常 import/start;Vercel Tool 与 Trigger Tool schema 同时可用。 - -### H2 Trigger 启动的 Agent 使用正确配置 - -让 Cron Trigger 启动一个可调用普通 Tool 的 Agent。 - -预期:按正确时区和 occurrence 启动一次;Runtime intake、Agent 配置读取和 Tool schema 正常,无跨 PR 合并导致的序列化/校验错误。 - -### H3 默认 Agent 与 Trigger 共存 - -为默认 Agent 创建 Trigger,随后重启 Backend。 - -预期:Trigger 不重复执行;默认 Agent 不重复创建;原 Trigger 仍关联同一 Agent ID。 - -### H4 群聊与 Direct Chat 基础回归 - -除 `@` 列表外,验证群消息发送、普通成员选择、历史消息、active Run 指示、输入法和 WebSocket 重连;同时执行 F1–F6 的多 Session 切换。 - -预期:现有群聊主链不因 mention 局部修复退化;Direct Chat 后台运行能力与当前 Session UI 隔离同时成立。 - -### H5 服务重启恢复 - -在 Vercel deployment pending、Trigger 即将触发和群聊已连接三个状态下分别重启相关服务。 - -预期:Vercel operation 可由既有 Runtime 恢复机制继续结算;Trigger occurrence 不重复;前端可重连。若旧 Run 恢复不在修复范围,必须区分新旧 operation 并记录事实。 - -## 14. 自动化执行清单 - -### 14.1 Backend 专项回归 - -```bash -cd backend -.venv/bin/python -m pytest \ - tests/test_agent_tools_typed_vercel_deploy.py \ - tests/test_agent_runtime_async_tool_poll.py \ - tests/test_agent_runtime_tool_step_service.py \ - tests/test_timezone_validation.py \ - tests/test_trigger_config_updates.py \ - tests/test_trigger_runtime_scheduling.py \ - tests/test_trigger_runtime_queue.py \ - tests/test_trigger_runtime_intake.py \ - tests/test_agent_runtime_trigger_completion.py \ - tests/test_a2a_trigger_eval.py \ - tests/test_agent_seeder_storage_repair.py \ - tests/test_auth.py \ - tests/test_chat_session_runtime_state.py \ - tests/test_feishu_channel_runtime.py \ - tests/test_http_channel_runtime.py \ - tests/test_tool_execution.py \ - tests/test_agent_runtime_channel_provider_delivery.py -``` - -### 14.2 Backend 全量与静态检查 - -```bash -cd backend -.venv/bin/python -m pytest -.venv/bin/ruff check \ - app/services/agent_tools.py \ - app/services/agent_seeder.py \ - app/services/timezone_utils.py \ - app/services/trigger_daemon.py \ - app/services/trigger_runtime \ - tests/test_agent_tools_typed_vercel_deploy.py \ - tests/test_agent_seeder_storage_repair.py \ - tests/test_timezone_validation.py \ - tests/test_trigger_config_updates.py \ - tests/test_trigger_runtime_scheduling.py \ - tests/test_trigger_runtime_queue.py -``` - -如果全量 Ruff 命中 `main` 已存在的问题,必须保存基线分支与候选分支的同命令对比;只有候选新增问题才判定为本版本回归。 - -### 14.3 Frontend - -```bash -cd frontend -npm test -npm run build -``` - -自动化通过后仍需在真实浏览器执行 E1–E7、F1–F6,并用隔离浏览器执行 G1–G2;Node contract test 不能替代滚动、输入法、Session 切换或浏览器绑定手工验证。 - -### 14.4 Migration 与架构守卫 - -```bash -cd backend -.venv/bin/alembic heads -.venv/bin/alembic current --check-heads -cd .. -bash scripts/arch-guard.sh -git diff --check upstream/main...HEAD -``` - -## 15. 当前聚合分支证据(2026-08-10) - -以下是当前本地 `v1.11.4` / `9a3e291b` 已读取到的证据,不代表目标测试环境已验收: - -| 检查 | 当前结果 | 证据性质 | -| --- | --- | --- | -| 原候选 Backend 专项 | 主线合入前 155 passed,5 warnings;合入后尚未重跑该整组 | 先前候选结果,需重跑 | -| 最新 main 专项 | 75 passed,5 warnings(1 个项目内既有 Pydantic warning,4 个来自当前复用环境依赖) | 当前聚合分支本地结果 | -| Frontend `npm test` | 91 passed | 当前聚合分支本地结果 | -| Frontend `npm run build` | passed | 当前聚合分支本地结果 | -| `scripts/arch-guard.sh` | passed,只有 warning | 当前聚合分支本地结果 | -| 变更 Python 文件 Ruff critical rules | passed | 当前聚合分支本地结果 | -| 全量 Ruff | 未通过;命中 `main` 已存在的 module docstring/import 顺序及 unused import | 已完成基线归因,不是候选新增结论 | -| Backend 全量 pytest | 本轮聚合分支尚未完成 | 待执行 | -| Alembic heads | passed:`f063_merge_v1_11_4_heads` 为唯一 head | 当前聚合分支本地结果 | -| 新库 Drone #489 | passed:PR #945 fresh DB migration 完整通过 | 实际 CI;`f061` 重复列阻断已解除 | -| 升级库 Docker 测试 | 本轮聚合分支尚未执行 | 待执行 | -| 真实 Vercel E2E | 尚未执行 | 待执行 | -| 真实 PostgreSQL 多实例 seeding | 尚未执行 | 待执行 | -| 真实群聊 WebSocket | 尚未执行 | 待执行 | - -历史单 PR 证据:PR #827 曾记录 102 个专项与 172 个回归测试;PR #833 曾记录 2173 个 Backend 测试、33 个专项测试及新库/升级库 CI;PR #837 曾记录 38 个专项测试;PR #927 曾记录 89 个前端测试、build 和 12 人 Playwright。它们仅用于说明设计曾被验证,不得直接填入本轮执行结果。 - -## 16. 执行记录模板 - -### 16.1 环境记录 - -| 字段 | 实际值 | -| --- | --- | -| 执行日期 / 执行人 | | -| 待测 commit | | -| Frontend / Backend 镜像 digest | | -| 数据库来源与 migration head | | -| 浏览器与版本 | | -| Vercel 测试账号/项目 | | -| Tenant / Agent / Group / Session / Run ID | | - -### 16.2 Case 结果 - -| Case | 结果(Pass/Fail/Blocked/Not Run) | 实际结果 | 证据链接/日志/截图 | 缺陷编号 | -| --- | --- | --- | --- | --- | -| A1–A4 | | | | | -| B1–B7 | | | | | -| C1–C10 | | | | | -| D1–D9 | | | | | -| E1–E7 | | | | | -| F1–F6 | | | | | -| G1–G6 | | | | | -| H1–H5 | | | | | - -### 16.3 发布结论 - -- P0 是否全部通过: -- P1 未完成/失败项: -- 已知缺陷与影响: -- 回滚条件: -- 版本负责人结论:发布 / 有条件发布 / 阻断 - -## 17. 追溯来源 - -- CoAligne:Vercel deployment wait、Trigger 时区与触发主链、默认 Morty/Meeseeks 删除后重建、群聊 `@` 候选滚动、Direct Chat Session 状态串线,以及候选修复聚合到 v1.11.4 的对话记录。 -- Lore commits:`fa58883d`、`3ce7cfc4`、`ae867114`、`d576c582`、`2c3b3ecd`、`bd98901b`、`ae3eb9c9`、`d5525db4`、`328eaffa`、`535cb539`、`9a3e291b`,以及本地聚合 merge commits。 -- 最新 main:PR #826(浏览器绑定 OAuth/SSO)、PR #836(图片超时对账)、PR #842(Feishu/Teams webhook 认证)。 -- 当前实现与测试:`upstream/main...9a3e291b` 的 source diff、测试文件、Spec Kit Vercel contract 和默认 Agent 技术方案。 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 90511d947..000000000 --- a/docs/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Clawith 文档导航 (Documentation Navigation Hub) - -> 找文档从这里开始。原则:**规范看根目录与 `constitution.md`、架构看 `architecture/`、需求交付看 `features/`、重构计划看 `technical-plans/`**。 - ---- - -## 1. 规范与流程 (开发前必读) - -| 文档 | 内容 | -|---|---| -| [`constitution.md`](constitution.md) | 架构宪法铁律 C1–C4(运行时隔离 / 多租户 / 副作用幂等 / HTTP 客户端包装) | -| [`SDD-Guide.md`](SDD-Guide.md) | 开发流程与文档归档指南:流程分级 (Hotfix vs Full SDD)、★ 暂停点、已知坑记录机制 | -| [`../AGENTS.md`](../AGENTS.md) | 全局 AI Agent 约束与指令总入口 | - ---- - -## 2. 系统架构 (子系统深度) - -[`architecture/`](architecture/) — 核心架构基线(最新状态快照): - -- [`01-architecture-overview.md`](architecture/01-architecture-overview.md):系统整体拓扑与四类事实隔离原则 -- [`02-backend-runtime-boundary.md`](architecture/02-backend-runtime-boundary.md):FastAPI、RuntimeCommandIntake 与 Command Worker 运行时隔离 -- [`03-multi-tenant-data-model.md`](architecture/03-multi-tenant-data-model.md):多租户数据隔离模型与 SQLModel 表结构 - ---- - -## 3. 功能交付归档 (SDD 产出) - -`docs/features/` — 按 `v{X.Y.Z}/{NNN}-{name}/` 组织。每个需求包含 `spec.md` (需求与验收标准)、`design.md` (架构设计与已知坑)、`tasks.md` (任务日志)。 - ---- - -## 4. 重大技术方案与迁移计划 - -[`technical-plans/`](technical-plans/) — 重大技术重构与迁移方案归档: - -- [`20260728-private-chat-finish-migration-plan.md`](technical-plans/20260728-private-chat-finish-migration-plan.md):私有会话结束逻辑迁移方案 -- [`20260728-dao-migration-plan.md`](technical-plans/20260728-dao-migration-plan.md):DAO 重构与数据库迁移方案 diff --git a/docs/SDD-Guide.md b/docs/SDD-Guide.md deleted file mode 100644 index 88529b0af..000000000 --- a/docs/SDD-Guide.md +++ /dev/null @@ -1,56 +0,0 @@ -# SDD Guide — Spec-Driven Development Workflow - -> Authoritative guide for feature development and document archiving in Clawith. -> Root `AGENTS.md §4` contains the quick-reference flow; this document is the full specification. -> Architecture laws every feature must obey → [`docs/constitution.md`](constitution.md). - ---- - -## 1. Pick the Track (流程分级) - -Not every change requires the full SDD pipeline. Match track to scope: - -| Track | When to Use | Required Steps | -|---|---|---| -| **Hotfix / Trivial** | Bug fix, copy/text change, dep bump, ≲ 1 file of logic, **no contract change** | Branch → fix → `arch-guard.sh` pass → unit test → Code Review → merge. No spec/design docs. | -| **Small Feature** | Single module, no cross-feature contract change, low uncertainty | Lightweight `spec.md` (Acceptance criteria) → implement → test → review. | -| **Full SDD Track** | New capability, cross-module change, new/changed API contract, or touches a constitution clause | Full pipeline (§2) with mandatory ★ pause points. | - ---- - -## 2. Full SDD Pipeline - -```text -1. Spec Discovery (explore codebase, clarify intent) → ★ User Confirms -2. spec.md (What & Acceptance Criteria) → ★ User Confirms -3. design.md (How & Gotchas & Constitution Check) → ★ User Confirms -4. tasks.md (Task breakdown & execution log) -5. Branch feat/{NNN}-{name} -6. Implement wave-by-wave & run tests -7. Run scripts/arch-guard.sh & test suite -8. Code Review & Merge -``` - ---- - -## 3. Pause Points (★) — Human-in-the-Loop - -★ represents a **mandatory stop where the agent must pause and wait for user confirmation**. - -- **Fixed ★**: After Spec Discovery, after `spec.md`, after `design.md`. -- **Dynamic ★ (Deviation Re-confirmation)**: During implementation, if a technical discovery invalidates a previously agreed-upon spec or design decision, **stop and re-confirm with the user**. - ---- - -## 4. Document Roles & Archiving Principles - -| Document | Purpose | Location / Update Rule | -|---|---|---| -| **`spec.md`** | What & acceptance criteria | Archived under `docs/features/v{X.Y.Z}/{NNN}-{name}/` | -| **`design.md`** | Why this How + today's-state snapshot + Known Gotchas | Overwrite in-place for current state; keep decision reasons and gotchas | -| **`tasks.md`** | What was done, in what order | Appended running log during feature execution | - -### Key Archiving Rules: -1. **Single Source of Truth**: Laws in `constitution.md`, subsystem architecture in `docs/architecture/`, feature deliverables in `docs/features/`. -2. **Overwrite-in-Place for Architecture**: `docs/architecture/` files always reflect today's latest system snapshot. -3. **Keep Decision Reasons & Gotchas**: Record *why* alternatives were rejected and known traps in `design.md` so future developers do not repeat failed technical attempts. diff --git a/docs/architecture/01-architecture-overview.md b/docs/architecture/01-architecture-overview.md deleted file mode 100644 index 8e47be4d6..000000000 --- a/docs/architecture/01-architecture-overview.md +++ /dev/null @@ -1,45 +0,0 @@ -# 01 - Clawith Architecture Overview - -> Status: Current implementation baseline. -> Scope: System topology, boundary principles, and core components. - ---- - -## 1. System Purpose & Topology - -Clawith is a multi-tenant enterprise Agent application platform. It exposes direct chat, group chat, tasks, triggers, heartbeats, and Agent-to-Agent entry points while executing all durable Agent logic through a shared, isolated runtime. - -```text -Web / Channel / Task / Trigger / Heartbeat / A2A - │ - ▼ - RuntimeCommandIntake - AgentRun + AgentRunCommand (DB) - │ - ▼ - Command Worker - thread-serialized execution - │ - ▼ - Clawith Agent Kernel - (context -> model -> tool -> verify) - │ - ▼ - LangGraph - PostgreSQL Durable Checkpoint -``` - ---- - -## 2. Separation of Four Kinds of Facts - -To maintain durable execution stability, Clawith strictly decouples four distinct concerns: - -| Fact Type | Owner | Description | -|---|---|---| -| **Product Records** | Product DB Tables | Tenants, Users, Agents, Sessions, Groups, Permissions. | -| **Accepted Command Inbox** | `agent_run_commands` Table | Accepted `start`, `resume`, and `cancel` inputs. | -| **Execution Lifecycle** | LangGraph Checkpoint | PostgreSQL durable checkpoint state. | -| **User Delivery** | Product Reconciler | Idempotent message delivery & external notifications. | - -> **INVARIANT (C1)**: Product projections must **NEVER** become a second Agent execution state machine. API endpoints and product services must not mutate checkpoint lifecycle fields directly. diff --git a/docs/architecture/02-backend-runtime-boundary.md b/docs/architecture/02-backend-runtime-boundary.md deleted file mode 100644 index 10d50c95d..000000000 --- a/docs/architecture/02-backend-runtime-boundary.md +++ /dev/null @@ -1,32 +0,0 @@ -# 02 - Backend & Runtime Boundary Isolation - -> Status: Current implementation baseline. -> Scope: Execution intake, Command Worker, and LangGraph Checkpoint boundaries. - ---- - -## 1. API & Channel Adapters (`backend/app/api/`) - -HTTP, WebSocket, webhook, and channel adapters perform authentication, tenant authorization, payload validation, and request persistence. - -**Rules**: -- Adapters must convert valid requests into durable commands via `RuntimeCommandIntake`. -- Adapters MUST NOT invoke graph nodes directly, advance graph node execution status, or modify checkpoint tables. - ---- - -## 2. Runtime Command Intake (`backend/app/services/agent_runtime/`) - -Shared execution boundary that atomically records: -- The immutable `AgentRun` registry identity. -- A durable `AgentRunCommand` for `start`, `resume`, or `cancel`. -- Stable idempotency and correlation facts. - ---- - -## 3. Command Worker (`command_worker.py`) - -The Command Worker claims durable commands from `agent_run_commands`, serializes execution per thread, invokes the LangGraph topology, and handles post-checkpoint reconciliation. - -- **Checkpoints are Authoritative**: A committed checkpoint remains authoritative even if product synchronization fails. -- **Reconciliation is Idempotent**: Side-effect synchronization and notification delivery are retryable and idempotent. diff --git a/docs/architecture/03-multi-tenant-data-model.md b/docs/architecture/03-multi-tenant-data-model.md deleted file mode 100644 index c0fcd39c6..000000000 --- a/docs/architecture/03-multi-tenant-data-model.md +++ /dev/null @@ -1,18 +0,0 @@ -# 03 - Multi-Tenant Data Model & Isolation - -> Status: Current implementation baseline. -> Scope: Tenant scoping, SQLModel data models, and cache key rules. - ---- - -## 1. Multi-Tenant Principle - -Clawith is a strictly multi-tenant enterprise system. No operation or query may access data outside the authorized `tenant_id` scope. - ---- - -## 2. Enforcement Rules - -1. **Database Queries**: Every SQLModel / SQLAlchemy query MUST explicitly include `.where(Model.tenant_id == tenant_id)` or use auto-injected ContextVar filters. -2. **Redis Cache Keys**: Cache keys must follow the format `tenant:{tenant_id}:{key_name}`. -3. **Background Worker Tasks**: Worker tasks must validate the tenant scope of the target `AgentRun` before executing commands. diff --git a/docs/constitution.md b/docs/constitution.md deleted file mode 100644 index 6c703e2cc..000000000 --- a/docs/constitution.md +++ /dev/null @@ -1,79 +0,0 @@ -# Clawith Architecture Constitution - -> **The single source of truth for Clawith's architectural laws — invariant across all features, never to be violated.** -> -> - `AGENTS.md` and every feature's `design.md` **reference this file; they never copy it.** Changing an implementation never requires editing this file (they point here). -> - `scripts/arch-guard.sh` is the **machine-enforcement arm** of this document: each RULE maps to a clause below. -> - Violations are reported as **BLOCKER** during design/code reviews. - ---- - -## Anchor Table (Clause ↔ arch-guard RULE) - -| Clause | Law | arch-guard RULE | Severity | -|---|---|---|---| -| **C1** | Runtime Boundary Isolation (Fact Separation) | `C1-RuntimeIsolation` | VIOLATION | -| **C2** | Strict Multi-Tenant Data Scope | `C2-MultiTenantScope` | VIOLATION | -| **C3** | Idempotent Side Effects & Reconciliation | `C3-IdempotentSideEffects` | VIOLATION | -| **C4** | Client & Gateway Wrapper Enforcement | `C4-NoDirectAxios` | VIOLATION | - ---- - -## C1. Runtime Boundary Isolation (Fact Separation) - -Clawith separates four distinct kinds of facts: - -1. **Product Records**: Clawith product SQLModel tables (`Tenant`, `User`, `Agent`, `Session`, `Group`, `Permissions`). -2. **Accepted Command Inbox**: `agent_run_commands` table (Accepted `start`, `resume`, `cancel` inputs). -3. **Execution Lifecycle**: LangGraph Checkpoint (PostgreSQL durable checkpoint). -4. **User Delivery**: Product-side idempotent reconciliation and delivery. - -### Invariants: -- `backend/app/api/` and channel adapters must only create durable commands via `RuntimeCommandIntake`. -- API endpoints and product services **MUST NOT** invoke graph nodes directly, advance node execution status, or modify checkpoint tables. -- Product projections must **NEVER** become a second Agent execution state machine. - ---- - -## C2. Strict Multi-Tenant Data Scope — Auto-Injected & Explicit Filters - -Every database query, Redis cache key, and background worker task MUST explicitly enforce `tenant_id` scoping to prevent cross-tenant data leaks. - -- **SQLModel / SQLAlchemy**: Always include `.where(Model.tenant_id == tenant_id)` or ensure tenant context injection via ContextVar. -- **Cache Keys**: Redis keys must be prefixed with `tenant:{tenant_id}:`. -- **Worker Tasks**: Celery/Command Worker tasks must validate `tenant_id` before processing commands. - ---- - -## C3. Idempotent Side Effects & Reconciliation - -LangGraph checkpoint commitment is authoritative. - -- Command application and product synchronization are distinct facts. -- A committed checkpoint remains authoritative even if product synchronization temporarily fails. -- Product-side projections, notifications, and message delivery MUST be distinct, retryable, and idempotent. - ---- - -## C4. Client & Gateway Wrapper Enforcement - -- **Frontend**: Components and pages MUST NEVER `import axios` directly. All HTTP requests must go through the central request wrapper (`src/api/request.ts`). -- **Backend**: Backend code must access external LLM/tools through unified proxy & sandboxed execution environments. - ---- - -## C5. Database & Performance Standards (No Foreign Keys & N+1 Prevention) - -- **No Physical Foreign Keys**: Database tables MUST NOT create physical `FOREIGN KEY` constraints at the DB layer. Maintain relationship integrity at the application/SQLModel layer to prevent lock contention and migration deadlocks. -- **Minimize DB JOINs**: Avoid multi-table complex JOINs. Prefer application-level batch querying or indexed lookup tables. -- **N+1 Prevention via Batching**: Eliminate N+1 loop queries. Use batch query APIs (`in_()` clauses, batch load interfaces) or `selectinload` for batch fetching. - ---- - -## C6. Code Modularity & Reusability (Recommended Size Thresholds & Helper Layer) - -- **Recommended Size Thresholds (Flexible Guidelines)**: - - Functions: Recommended ~100 lines. Treat exceeding lines as a signal for refactoring into sub-functions. - - Backend files: Recommended ~1000 lines (Frontend ~600 lines). Allow flexibility based on context, treating large files as candidates for module splitting. -- **No Wheel Reinvention**: Search existing `app/core/`, `app/utils/`, and `app/helpers/` utilities before writing custom helper code. Extract common logic into reusable `utils/helpers` modules. - diff --git a/docs/features/v1.12.0/001-session-isolated-sandbox-output/design.md b/docs/features/v1.12.0/001-session-isolated-sandbox-output/design.md deleted file mode 100644 index a4b82d5f0..000000000 --- a/docs/features/v1.12.0/001-session-isolated-sandbox-output/design.md +++ /dev/null @@ -1,684 +0,0 @@ -# Session-Isolated Sandbox Output Design - -## 1. Status - -- Feature: Session-isolated local code execution output -- Spec: [`spec.md`](spec.md) -- Status: Draft for user confirmation -- Constitution: [`docs/constitution.md`](../../../constitution.md) - -## 2. Design Summary - -This design intentionally does not route a Session to one Runtime Worker. Every Runtime Worker continues to claim durable commands through the existing PostgreSQL Command Inbox. - -Local Session code execution adds one narrow distributed coordination primitive: - -```text -Redis execution lease - covers materialize -> bwrap execute -> mode-specific Workspace publish -``` - -One bubblewrap child and its writable working copy remain active for the duration -of one Agent loop. The process is closed when that loop settles. Durable -cross-loop continuity still comes only from files under: - -```text -workspace/output/{session_id} -``` - -Guest paths follow the same logical names exposed by Workspace tools. In -particular, `workspace/` maps to `/workspace/`; the Sandbox does not -add a second `workspace` segment. The persistent output path is therefore: - -```text -/workspace/output/{session_id} -``` - -## 3. Current-State Snapshot - -### 3.1 Runtime execution - -- `RuntimeCommandDaemon` concurrently calls `RuntimeCommandWorker.run_once()`. -- Commands are claimed from PostgreSQL with `FOR UPDATE SKIP LOCKED`. -- The existing PostgreSQL advisory Thread lock serializes one LangGraph Thread. -- `RuntimeToolStepService` executes a model-proposed Tool batch sequentially. -- Different Threads/Runs and legacy/direct Tool entry points can still execute concurrently. - -This means Redis is not a new Agent scheduler. It is a defense-in-depth mutex around one shared Session output prefix. - -### 3.2 Code execution - -The typed Runtime path is currently: - -```text -RuntimeToolStepService - -> execute_builtin_tool_outcome - -> _run_with_temp_workspace_outcome - -> _execute_code_outcome - -> SandboxBackend.execute -``` - -The legacy path calls the same backend through `execute_tool`. Approved legacy actions can call `_execute_tool_direct`. - -### 3.3 Workspace publication - -`_prepare_temp_workspace` materializes durable Storage into a temporary Agent root. In `merge` mode, `flush_temp_workspace` uses Storage version tokens and conditional writes to publish changes. In `isolated_output`, the exact Session-owned output prefix is serialized by the tenant-scoped execution lease and is published with replacement semantics; it does not participate in shared-Workspace CAS conflict decisions. Redis Workspace path locks remain an additional short-lived guard for the physical write window. - -The local subprocess backend currently creates another staging tree, runs bubblewrap over that staging tree, validates generated files, and copies accepted changes back into the temporary Agent root. The outer temporary Workspace adapter then publishes to durable Storage. - -### 3.4 Dirty-worktree integration constraint - -At design time, `subprocess_backend.py` and its tests already contain user-owned staged and unstaged changes for staging, pip proxying, output validation, and process reaping. Implementation MUST preserve those changes and layer this feature onto their resulting behavior. It MUST NOT reset or replace the files wholesale. - -## 4. Component Design - -```text -agent_tools code-execution orchestrator - ├─ SandboxExecutionLeaseStore Redis acquire/renew/release - ├─ SandboxExecutionLease one held lease + heartbeat - ├─ SandboxWorkspacePolicy mode/session/path validation - ├─ TempWorkspace materialize roots + publish roots - ├─ SubprocessBackend Agent-loop bwrap + publish enforcement - └─ flush_temp_workspace durable mode-specific publication -``` - -### 4.1 `SandboxExecutionLeaseStore` - -New module: - -```text -backend/app/services/sandbox/execution_lease.py -``` - -Public responsibilities: - -```python -class SandboxExecutionLeaseStore: - async def acquire(scope, *, ttl_seconds) -> SandboxExecutionLease | None: ... - -class SandboxExecutionLease: - async def start_heartbeat() -> None: ... - async def ensure_publication_window(seconds: int) -> bool: ... - async def release() -> None: ... -``` - -The module owns Redis scripts and does not know about LangGraph, Tool receipts, or Workspace files. - -### 4.2 `SandboxWorkspacePolicy` - -New module: - -```text -backend/app/services/sandbox/workspace_policy.py -``` - -Responsibilities: - -- parse `workspace_mode`; -- validate canonical Session UUID; -- derive the durable relative prefix `workspace/output/{session_id}`; -- derive the guest prefix `/workspace/output/{session_id}`; -- declare materialization and publication roots; -- reject unsupported backend/mode combinations. - -The model never provides these values. They come from Tool configuration and trusted Runtime/session context. - -### 4.3 Code-execution orchestrator - -The common orchestration boundary remains in `agent_tools` initially, but the new logic is extracted into small helpers rather than enlarging backend-specific code. - -The orchestrator resolves one immutable execution plan before any side effect: - -```python -@dataclass(frozen=True) -class ExecuteCodePlan: - sandbox_config: SandboxConfig - workspace_mode: Literal["merge", "isolated_output"] - publication_owner: Literal["gateway", "workspace_cas"] - tenant_id: UUID | None - agent_id: UUID - session_id: UUID | None - effective_timeout_seconds: int -``` - -`publication_owner` is a trusted Executor configuration value, not a Tool argument. One Executor resolves exactly one publication owner for the whole invocation: - -- `gateway`: the Sandbox gateway performs the durable Workspace mutation and revision recording; the outer temporary Workspace adapter does not publish those files; -- `workspace_cas`: the gateway only validates and copies into `TempWorkspace`; the outer adapter performs mode-specific publication and any post-commit revision recording. The historical name is retained even though `isolated_output` uses replacement semantics rather than shared-Workspace CAS. - -The two branches are mutually exclusive. Startup/configuration validation rejects an Executor definition that enables both or neither publication paths. The same resolved plan is used for lease TTL, Workspace materialization, backend selection, mount policy, and publication ownership. Configuration is not fetched independently at multiple stages. - -For an invocation carrying a Session, the orchestrator also resolves a validated scope: - -```python -@dataclass(frozen=True) -class SandboxExecutionScope: - tenant_id: UUID - agent_id: UUID - session_id: UUID -``` - -The scope resolver parses canonical UUIDs and verifies that the Chat Session belongs to the exact Agent and tenant using an explicitly tenant-filtered DAO query. A missing tenant, missing Session, or ownership mismatch fails closed before lease acquisition, materialization, or code execution. `tenant_id=None` is permitted only for legacy non-Session `merge` invocations that do not create a lease key. - -## 5. Redis Lease Contract - -### 5.1 Key - -```text -tenant:{tenant_id}:sandbox-execution:{agent_id}:{session_id} -``` - -This is explicit tenant scoping under Constitution C2. All IDs are canonical UUID text. - -Only a validated `SandboxExecutionScope` may construct this key. The implementation must never serialize a missing tenant as `tenant:None` and must not accept an arbitrary well-formed Session UUID without verifying tenant and Agent ownership. - -### 5.2 Value - -The value is an opaque exact-match string: - -```text -v1|{executor_instance_id}|{random_lease_token} -``` - -`executor_instance_id` is process-unique and generated once at process startup from hostname, PID, and a random UUID. The full value is never logged. Logs may include a short SHA-256 correlation hash. - -### 5.3 Acquire - -Acquire uses one Redis command: - -```text -SET key value NX PX 60000 -``` - -The steady-state lease TTL is 60 seconds. An invocation that cannot acquire returns: - -```text -error_code = sandbox_session_busy -retryable = true -``` - -The caller does not block or poll while occupying a Runtime command slot. - -### 5.4 Heartbeat - -While materialization, execution, or pre-publication processing is active, a background task renews every 20 seconds with an atomic Lua script: - -```lua -if redis.call('get', KEYS[1]) == ARGV[1] then - return redis.call('pexpire', KEYS[1], ARGV[2]) -end -return 0 -``` - -Renewal returning `0` means ownership was lost. Redis exceptions mean ownership is unverifiable. Both states are latched on the lease handle. - -### 5.5 Publication window - -Immediately before durable Workspace publication, the owner runs the same compare-and-renew script with a 120-second TTL and then publishes under a 60-second application timeout. - -This ordering closes the check-then-expire race: - -```text -atomic owner check + extend to 120 s - -> publish with 60 s deadline - -> release -``` - -Even if the normal heartbeat fails after the extension, another Worker cannot acquire the lease during the bounded publication window. - -If the publication-window extension fails, publication does not start and the Tool settles `unknown` if code may already have run. - -The 60-second publication timeout covers path-lock acquisition, candidate validation, durable writes/deletes, revision recording owned by the selected publication path, and result collection. Publication is a multi-file operation and is not transactionally atomic. If timeout, cancellation, Redis uncertainty, or an exception occurs after the first durable mutation may have started, the Tool settles `unknown`, never automatically retries code, and records the known committed, deleted, conflicted, and unverified paths/counts in outcome metadata for reconciliation. Cancellation of the caller does not convert a possibly partial publication into a normal failure. - -Workspace path locks used inside publication are updated to accept `tenant_id` and use keys beginning with: - -```text -tenant:{tenant_id}:workspace-lock:{agent_id}:{normalized_path} -``` - -Session-scoped execution must not call the existing unscoped key builder. Owner-only lease release remains required. Storage CAS remains required for `merge`; exact-prefix replacement in `isolated_output` requires verified lease ownership. The path lock is not a substitute for either mode's authority check. - -### 5.6 Release - -Release uses compare-and-delete: - -```lua -if redis.call('get', KEYS[1]) == ARGV[1] then - return redis.call('del', KEYS[1]) -end -return 0 -``` - -Release is shielded during cancellation cleanup. Failure to release after a fully settled publication is logged; TTL provides recovery and does not change the committed Tool outcome. - -### 5.7 Why no fencing counter - -A monotonic fencing counter is unnecessary in V1 because: - -- the lease is renewed immediately before a bounded publication window; -- a second execution cannot acquire during that window; -- whichever publication owner is selected must use conditional version-token writes/deletes for `merge`, and exact-prefix replacement only for `isolated_output` while the Session lease is valid; -- no long-lived Sandbox resource accepts commands after the invocation. - -If publication cannot be bounded or future backends maintain long-lived mutable state, the contract must add durable fencing before supporting that behavior. - -## 6. Workspace Mode Configuration - -### 6.1 Configuration schema - -Add to the `execute_code` built-in definition: - -```json -{ - "key": "workspace_mode", - "label": "Workspace Write Mode", - "type": "select", - "default": "merge", - "options": [ - {"label": "Merge workspace changes", "value": "merge"}, - {"label": "Session output only", "value": "isolated_output"} - ] -} -``` - -Add `workspace_mode` to `SandboxConfig` with validation restricted to the two values. `SandboxConfig.from_dict` preserves the existing field-level fallback behavior. - -Add the internal trusted `publication_owner` setting to the Executor definition. It is resolved from stored Executor configuration and is not exposed as a model-call argument. The local Executor definition must select exactly one of `gateway` or `workspace_cas`; validation rejects ambiguous values. Seeder/default handling must choose the owner matching the deployed Executor implementation so upgrades never activate two publication paths. - -Existing Tools receive `merge` when the field is absent, so no data migration is required. Seeder schema/default merging exposes the option to enterprise and per-Agent configuration UIs, which already render `select` fields generically. - -### 6.2 Supported backends - -| Backend | `merge` | `isolated_output` | -|---|---:|---:| -| Local subprocess + bubblewrap | Supported | Supported | -| Unsafe local fallback without bubblewrap | Existing development behavior | Rejected | -| Docker | Existing behavior | Rejected in V1 | -| E2B / remote API backends | Existing behavior | Rejected in V1 | - -Stable rejection: - -```text -error_code = sandbox_workspace_mode_unsupported -retryable = false -``` - -`execute_code_e2b` does not gain a `workspace_mode` control in V1. - -For `isolated_output`, rejection of an unavailable bubblewrap backend takes precedence over `allow_unsafe_fallback_when_bwrap_missing`. Enabling the development fallback cannot weaken the isolated mount contract. - -## 7. Temporary Workspace and Publication Roots - -### 7.1 Split materialization from publication - -`TempWorkspace` currently uses one `selected_paths` collection for both materialization and sync-back. `isolated_output` needs all standard Agent paths readable but only one prefix publishable. - -Refactor the object to hold: - -```python -materialized_paths: list[str] -publish_paths: list[str] -``` - -For `merge`: - -```text -materialized_paths = existing defaults -publish_paths = existing defaults -``` - -For `isolated_output`: - -```text -materialized_paths = existing defaults -publish_paths = ["workspace/output/{session_id}"] -``` - -The materialization manifest may include all readable files. `flush_temp_workspace` filters both local-file collection and manifest deletion checks to `publish_paths`. A file outside those roots is never written or deleted. - -### 7.2 Session output preparation - -The exact output directory is created inside the temporary Agent root even when it does not yet exist in Storage. Existing files under the prefix are materialized with Storage version tokens. - -The temporary Agent root is Run-scoped. The first local sandbox use in one -Agent loop materializes it once, and later `execute_code` calls reuse the same -root and manifest. Each successful publication refreshes that manifest with the -new durable version/hash (or removes a deleted entry), so a later call compares -against the immediately preceding publication instead of the loop-entry -snapshot. The command boundary discards the root only after the loop settles. - -Path construction goes through `normalize_workspace_path` plus an exact UUID-derived suffix. No model-provided relative path participates in the writable mount root. - -### 7.3 Durable artifact references - -Successful writes under the Session output prefix produce the existing form: - -```text -workspace://{agent_id}/workspace/output/{session_id}/{relative_file} -``` - -Deleted files do not produce artifact references. Conflict and unknown outcomes preserve existing Tool ledger semantics. - -## 8. Bubblewrap Mount Design - -### 8.1 Merge mode - -Preserve the current staging behavior: - -```text -bind staging-root /workspace -``` - -The existing output validation/gateway remains responsible for selecting acceptable staged changes before the outer Storage publication. - -### 8.2 Isolated output mode - -Prepare three host paths: - -```text -staging_root/ full materialized Agent root -staging_root/workspace/.tmp/ runtime scripts and pip proxy files -staging_root/workspace/output/{session_id}/ Session output staging -``` - -Build bubblewrap mounts in this order: - -```text ---bind staging_root /clawith ---bind staging_root/workspace /workspace ---bind staging_root/memory /memory ---bind staging_root/skills /skills ---ro-bind persistent_venv /opt/clawith/venv -``` - -Destination directories are created before bubblewrap starts. The staging tree -is a writable loop-local copy; publication filtering, not a nested read-only -mount, defines persistence. - -The execution script moves to: - -```text -/workspace/.tmp/_exec_tmp.{py|sh|js} -``` - -The working directory is `/`, so relative logical Workspace-tool paths map -directly: `workspace/...` is `/workspace/...`, `skills/...` is `/skills/...`, -and `memory/...` is `/memory/...`. Writes outside Session output are allowed in -the loop copy but are discarded at loop settlement. - -### 8.3 Symlink policy - -Materialization and staging MUST not allow a symlink under the writable prefix to target outside that prefix. Before mounting and before publication: - -- resolve the host output directory beneath the staging root; -- reject symlink components in the output root; -- skip or reject staged symlink files; -- verify every publication candidate remains beneath the resolved output root. - -The working-copy tree may retain safe readable symlinks only where current -Workspace path rules already permit them; no symlink may turn into a -publication escape. - -## 9. Sandbox Output Gateway and Publication Ownership - -The backend staging gateway is refactored into preparation and publication phases. Preparation accepts explicit allowed publication roots and produces an immutable validated candidate set without durable mutation: - -```python -candidates = await gateway.prepare( - staging_root, - temp_workspace_root, - publish_paths=policy.publish_paths, -) -``` - -For `isolated_output`, all materialized directories in the loop working copy are -readable and writable, but publication scans only -`workspace/output/{session_id}` for creates, modifications, and deletions. It -preserves existing quotas and HTML/SVG sanitization. Other writes remain -ephemeral and are discarded when the Agent loop closes. - -In `merge` mode, output quotas apply only to the publication delta: newly -created or modified candidate files count toward the 100-file, 50-MB total, -and 10-MB single-file limits. Unchanged materialized files do not consume the -quota, and deletions use a separate 100-file limit. In `isolated_output` mode, -the Session-owned output directory is excluded from the changed-file and -deleted-file count limits. The 50-MB total, 10-MB single-file, path-boundary, -symlink, extension, and HTML/SVG safety checks still apply. - -Publication behavior is exclusive: - -- after the publication lease is extended, `publication_owner=gateway` passes the prepared candidate set to the gateway's durable publisher and disables outer sync-back for those roots; -- after the publication lease is extended, `publication_owner=workspace_cas` copies the prepared candidates into `TempWorkspace` without database revision or durable Storage mutation, then the outer adapter owns durable publication and records revisions only after the corresponding mutation is confirmed. - -Both branches preserve path filtering, artifact reporting, and `unknown` settlement. `merge` preserves conditional conflict handling. `isolated_output` replaces files only inside its exact Session output prefix after lease ownership is revalidated, so a previously published file in that prefix does not produce `workspace_sync_conflict`. No invocation may execute both branches. Tests must spy on both publication interfaces and prove exactly one receives durable mutation calls for each configuration. - -## 10. End-to-End Execution Flow - -```text -1. AgentToolExecution receipt is reserved by the existing Runtime Tool service. -2. Resolve ExecuteCodePlan once. -3. Validate backend + workspace_mode + publication_owner and resolve the exact tenant/Agent/Session ownership. -4. For every local Session-scoped `execute_code`, in either `merge` or `isolated_output`, acquire the shared Redis execution lease. -5. Start lease heartbeat. -6. Create or reuse the Run-scoped materialized Agent paths and Session output manifest. -7. Create or reuse the Run-scoped backend and bwrap process. -8. Run code and capture exit/output. -9. Gateway validates only policy-allowed staged changes and freezes the publication candidate set without durable mutation. -10. Atomically extend the lease for the bounded publication window. -11. Invoke exactly one selected publication branch using the prepared candidates: gateway-owned publication or outer publication of only `TempWorkspace.publish_paths`, using the workspace mode's conflict policy. -12. Compose code status, publication status, and artifact references. -13. Stop heartbeat and compare-delete the lease. -14. Existing Runtime Tool service settles the durable receipt/checkpoint. -``` - -The lease covers steps 4 through 13. The first local sandbox call materializes -under the lease; subsequent calls access the same Run-scoped working copy only -after acquiring that lease, and the preceding call publishes before releasing -it. A local `merge` call and a local `isolated_output` call with the same -validated scope therefore contend on the same key. Only a non-Session `merge` -compatibility call omits the lease. - -## 11. Entry-Point Coverage - -### 11.1 Typed Runtime - -`execute_builtin_tool_outcome` uses the new common code-execution orchestrator. Its Runtime `session_id` is resolved through the tenant-filtered scope resolver rather than being trusted solely because it is present. This is the primary supported path. - -### 11.2 Legacy caller - -The legacy `execute_tool` branch calls the same orchestrator and passes its contextual `session_id` to the same scope resolver. It receives a text rendering only after typed execution semantics are decided. - -### 11.3 Approved action - -`_execute_tool_direct` gains an optional contextual `session_id`. `AutonomyService` passes the Session from stored `runtime_scope` where available, but the common scope resolver still verifies tenant and Agent ownership before treating it as trusted. - -An approved action configured for `isolated_output` but lacking an exact Session fails with `sandbox_session_required`; it never writes to a shared Agent output directory. - -### 11.4 Non-Session sources - -`merge` remains available without a Session and does not invent an execution lease scope. `isolated_output` requires a Session and fails closed. - -## 12. Outcome and Error Contract - -| Situation | Status | Error code | Retryable | -|---|---|---|---:| -| Execution lease occupied | failed | `sandbox_session_busy` | true | -| Redis unavailable before code | failed | `sandbox_coordination_unavailable` | true | -| Missing tenant for Session execution | failed | `sandbox_execution_scope_invalid` | false | -| Session does not belong to tenant/Agent | failed | `sandbox_execution_scope_invalid` | false | -| Isolated mode without Session | failed | `sandbox_session_required` | false | -| Unsupported backend/mode | failed | `sandbox_workspace_mode_unsupported` | false | -| Lease lost before code starts | failed | `sandbox_execution_lease_lost` | true | -| Lease lost/unverifiable after code may run | unknown | `sandbox_execution_lease_lost` | false | -| Code exits non-zero, publication succeeds | failed | `sandbox_execution_failed` | existing policy | -| Code ran, merge publication conflicts or any publication is unprovable | unknown | existing `workspace_sync_conflict` / `workspace_sync_outcome_unknown` | false | -| Publication timed out or may be partial | unknown | `workspace_sync_outcome_unknown` | false | - -For a failed code invocation whose isolated artifacts publish successfully, the returned `ToolExecutionOutcome` remains failed but includes those artifact references and publication counts in metadata. - -## 13. Cancellation and Cleanup - -- bubblewrap remains `--die-with-parent` and is launched in a new process session; -- timeout/cancellation terminates and reaps the process group using the existing in-progress process-reaping changes; -- heartbeat shutdown and owner-only lease release run in `finally`; -- temporary Workspace cleanup occurs only after publication has settled or been abandoned; -- no WebSocket disconnect handler writes Workspace files; -- no Worker-local Session timer owns durable output. - -## 14. Seven-Day Retention Decision - -V1 establishes the durable path but does not implement physical seven-day deletion. - -A correct physical TTL requires at least: - -- a durable `sandbox_output_expires_at` product fact scoped to the Chat Session; -- a tenant-aware cleanup scanner; -- acquisition of the same Session execution lease before deletion; -- per-file conditional deletion and conflict recovery for `merge`, plus exact-prefix replacement for `isolated_output`; -- audit/version behavior for automated deletion. - -Using Redis expiry or a Worker-local timer would make deletion non-durable and inconsistent across restarts, violating the requested architecture boundary. Therefore V1 leaves output durable until the retention feature is separately implemented. No UI will claim that automatic deletion is active. - -## 15. Rollout - -1. Add `workspace_mode` with default `merge`. -2. Add a validated internal `publication_owner` default matching the deployed local Executor's single durable publication implementation. -3. Keep all existing Agents on `merge` after seeding. -4. Enable `isolated_output` per Executor Code configuration. -5. Reject unsupported backends and ambiguous publication-owner configuration explicitly. -6. No Runtime graph version or checkpoint migration is required. -7. Rollback consists of selecting/defaulting to `merge` while retaining one valid publication owner; no durable command or file migration is needed. - -## 16. Constitution Check - -### C1 — Runtime Boundary Isolation - -Pass. - -- PostgreSQL `agent_run_commands` remains the accepted command authority. -- LangGraph checkpoint remains the execution lifecycle authority. -- Redis lease is only a volatile Tool concurrency guard. -- No product projection or Redis state advances Agent lifecycle. - -### C2 — Strict Multi-Tenant Scope - -Pass with required implementation checks. - -- Redis lease keys explicitly begin with `tenant:{tenant_id}:`. -- Tenant, Agent, and Session identities are resolved into a non-null scope before Session execution. -- The Session lookup must match tenant, Agent, and Session together and include an explicit `tenant_id` filter. -- Session-scoped Workspace lock keys also begin with `tenant:{tenant_id}:`; the legacy unscoped helper is not used by this flow. -- Workspace storage remains Agent-prefixed and output is further Session-prefixed. -- Automatic ORM tenant filtering remains the default for `User`. Identity membership discovery is a narrow DAO-only exception: it must include an exact `identity_id`, and tenant switching must additionally include the requested `tenant_id`, so `/auth/my-tenants` and `/auth/switch-tenant` can authorize memberships across the current JWT tenant without exposing unrelated users. - -### C3 — Idempotent Side Effects and Reconciliation - -Pass. - -- `AgentToolExecution` remains the durable side-effect receipt. -- Redis does not decide whether a Tool succeeded. -- Trusted Executor configuration selects exactly one durable publication owner for an invocation; gateway and outer Workspace CAS publication cannot both run. -- In `merge`, the selected owner preserves conditional conflict handling and never silently overwrites a newer durable file. -- In `isolated_output`, the selected owner may replace only the exact Session-owned output prefix while holding the validated Session execution lease; shared Workspace paths remain unreachable by that publication. -- Partial or unprovable multi-file publication settles `unknown` with reconciliation metadata. -- Post-dispatch uncertainty settles as `unknown` rather than automatic replay. - -### C4 — Gateway Wrapper Enforcement - -Pass. - -- Code continues through the unified Sandbox backend. -- `isolated_output` strengthens the local filesystem gateway. -- No new direct external provider client is introduced. - -### C5 — Database and Performance - -Pass for V1. - -- No database schema or query is added for the deferred retention feature. -- Materialization remains bounded by existing file/total-size limits. -- No physical foreign key is introduced. - -### C6 — Modularity and Reuse - -Pass with implementation constraint. - -- Redis ownership logic is isolated in `execution_lease.py`. -- Workspace policy/path logic is isolated in `workspace_policy.py`. -- Existing Storage CAS and Workspace helpers are reused. -- Backend staging validation is parameterized instead of adding another publication implementation. - -## 17. Known Gotchas - -1. **Workspace path identity:** Workspace-tool `workspace/` maps directly to guest `/workspace/`. -2. **Two publication implementations:** trusted Executor configuration must select exactly one durable owner; runtime assertions prevent gateway and outer Workspace publication from both running. -3. **Manifest deletion filtering:** filtering only newly collected files is insufficient; deletion checks must also filter the manifest by `publish_paths`. -4. **Loop cleanup:** the command boundary must reap the bwrap process and discard non-published working-copy changes. - The materialized `TempWorkspace`, its manifest, and the bwrap staging tree - share this boundary; a later code call must not re-materialize or re-clone - the full tree while the loop remains active. -5. **Pip proxy:** runtime `.tmp` remains under `/workspace/.tmp` in the loop copy. -6. **Lease timing:** checking a 60-second lease and then starting publication is racy; publication requires atomic extension plus a shorter bounded deadline. -7. **Legacy approvals:** approved execution may lack Session context; isolated mode must fail closed rather than guess. -8. **Remote backends:** their filesystem contract cannot be inferred from the local bubblewrap interface. -9. **Dirty worktree:** process-reaping and staging changes already in progress must be preserved during implementation. -10. **Scope validation:** canonical UUID syntax is insufficient; tenant, Agent, and Session ownership must be verified together before any Session lease or output path is constructed. -11. **Partial publication:** a timeout after the first durable mutation is an `unknown` outcome with reconciliation metadata, not a retryable failure. - -## 18. Verification Design - -### Lease tests - -- acquire succeeds once and contending executor receives busy; -- different tenant/Agent/Session scopes do not collide; -- missing tenant and cross-tenant/cross-Agent Session identities fail before Redis or code execution; -- foreign token cannot renew or release; -- heartbeat latches renewal failure; -- publication extension uses owner comparison; -- Redis exceptions fail closed; -- cancellation performs owner-only release; -- `merge` and `isolated_output` calls for the same Session contend on the same lease key. - -### Workspace policy tests - -- valid UUID Session derives exact relative and guest paths; -- missing/invalid Session is rejected for isolated mode; -- traversal and separator variants cannot alter the prefix; -- backend/mode compatibility is explicit; -- `isolated_output` rejects missing bubblewrap even when unsafe fallback is enabled. - -### Bubblewrap tests - -- logical `workspace/` maps to guest `/workspace/`; -- logical `skills/` maps to guest `/skills/`; -- materialized directories are writable in the loop copy; -- one bwrap process is reused across code calls in one Agent loop; -- one materialized `TempWorkspace` and refreshed manifest are reused across those calls; -- bwrap reuse does not copy the full materialized tree again; -- loop settlement reaps that process; -- script runs from `.tmp`; -- writes outside output remain available during the loop but are not published; -- writes inside output succeed. - -### Publication tests - -- materialize all default readable roots but publish only Session output; -- creates, modifications, and deletions under Session output use replacement semantics after Session lease validation; -- changes outside prefix are ignored and not deleted; -- failed code can publish diagnostic output while remaining failed; -- merge-mode conflict produces unknown and no silent overwrite; -- an existing file under the exact isolated Session output prefix is replaced without a Workspace CAS conflict; -- candidate preparation performs no durable mutation before the publication-window lease extension succeeds; -- publication timeout after a simulated first-file commit produces unknown with partial/unverified metadata and never re-executes code; -- Session-scoped Workspace locks use tenant-prefixed keys; -- each publication-owner configuration invokes exactly one durable mutation interface and never both; -- Worker A publish followed by logical Worker B materialization reads the same durable file. - -### Regression tests - -- existing `merge` behavior; -- current Sandbox process timeout/cancellation tests; -- typed E2B outcome tests; -- Runtime Tool ledger and unknown-outcome tests; -- `scripts/arch-guard.sh`; -- targeted Ruff checks for changed modules. diff --git a/docs/features/v1.12.0/001-session-isolated-sandbox-output/spec.md b/docs/features/v1.12.0/001-session-isolated-sandbox-output/spec.md deleted file mode 100644 index 11fb59738..000000000 --- a/docs/features/v1.12.0/001-session-isolated-sandbox-output/spec.md +++ /dev/null @@ -1,315 +0,0 @@ -# Session-Isolated Sandbox Output Specification - -## 1. Status - -- Feature: Session-isolated local code execution output -- Track: Full SDD -- Target release: v1.12.0 -- Status: Draft for user confirmation -- Constitution: [`docs/constitution.md`](../../../constitution.md) - -## 2. Problem Statement - -Clawith Runtime Workers already claim durable Agent commands from PostgreSQL and execute Tool steps in the claiming process. Local `execute_code` now reuses one bubblewrap process for code calls within the same Agent loop. - -The loop-scoped process preserves temporary working-copy state between code calls in that loop. State that must survive later loops still belongs in the durable Agent Workspace. - -The requested behavior is therefore: - -1. any eligible Runtime Worker may execute a Session command through the existing Command Inbox; -2. concurrent `execute_code` calls for the same Session are prevented with a Redis execution lease; -3. one Agent loop reuses one bubblewrap process and closes it at settlement; -4. `isolated_output` permits working-copy writes but publishes only a fixed Session output directory; -5. output files are conditionally published to durable Workspace storage and can be rematerialized by any later Worker; -6. no new Runtime Worker affinity or owner-specific queue is introduced. - -## 3. Goals - -### G1. Preserve the existing Runtime Worker model - -Runtime Workers MUST continue claiming commands through the existing PostgreSQL Command Inbox. The feature MUST NOT introduce Session-to-Worker routing, Worker-specific command queues, or a second command scheduler. - -### G2. Redis execution ownership - -Redis MUST store a short-lived, tenant-scoped execution lease for an exact `(tenant_id, agent_id, session_id)` while local `execute_code` is active. The lease prevents overlapping code executions for one Session; it does not own the Agent Run or Session lifecycle. - -### G3. Loop-scoped bubblewrap - -Every Agent loop MUST own at most one local bubblewrap process. Code calls in the loop reuse that process and its writable working copy. The process MUST be closed when the loop settles; later loops do not inherit interpreter memory or background processes. - -### G4. Fixed writable Session output - -In `isolated_output` mode, code MUST see Workspace-tool-compatible paths and a -separate publication boundary: - -```text -/workspace/ loop-scoped writable copy -/workspace/output/{session_id}/ published read-write output -``` - -Files in the fixed directory MUST be conditionally published to the matching Agent Workspace path and MUST be available to later executions regardless of which Runtime Worker claims them. - -### G5. Preserve current merge behavior - -The existing temporary Workspace materialization, conditional writes, conflict detection, and per-invocation settlement remain the basis of `merge` mode. - -### G6. Durable facts stay durable - -PostgreSQL Command Inbox, LangGraph checkpoints, `AgentToolExecution` receipts, and Workspace storage remain authoritative. Redis loss MUST NOT erase accepted commands, execution outcomes, or files. - -## 4. Non-Goals - -This version does not introduce: - -- Runtime Worker affinity; -- Worker registration or heartbeat for Sandbox routing; -- Worker-specific Redis queues; -- a persistent bubblewrap daemon or long-lived namespace; -- preservation or migration of in-memory interpreter state; -- a standalone Sandbox Control Plane; -- a general Tool Worker architecture; -- migration of Command, checkpoint, or Tool receipt facts into Redis; -- automatic migration of local temporary files between Workers; -- strict multi-file atomic publication beyond the existing conditional-write contract; -- automatic seven-day physical deletion unless a suitable durable retention owner already exists. - -## 5. Identity and Terminology - -| Term | Meaning | -|---|---| -| Runtime Worker | Existing process that claims durable commands and drives LangGraph model/tool execution. | -| Execution scope | Exact tuple `(tenant_id, agent_id, session_id)`. | -| Execution lease | Expiring Redis mutex that authorizes one local `execute_code` invocation for the scope. | -| Lease token | Unguessable value used for compare-and-renew and compare-and-delete. | -| Sandbox invocation | One fresh backend execution and bubblewrap child process. | -| Session output | Durable Workspace path `output/{session_id}`. | - -The execution scope MUST be derived from trusted Runtime context. Model or client input MUST NOT select Redis keys, lease tokens, or arbitrary output prefixes. - -## 6. Functional Requirements - -### FR1. Runtime command execution - -1. Runtime Workers MUST continue using the existing database claim algorithm, Thread lock, scheduling lane, checkpoint driver, and Tool ledger. -2. Commands for the same Session MAY be claimed by different Runtime Worker processes at different times. -3. No Session ownership record is needed outside an active local code invocation. -4. Runs with no exact Session identity remain unchanged. - -### FR2. Execution lease acquisition - -1. Before starting local `execute_code` for an exact Session, the executing Runtime Worker MUST atomically acquire a Redis lease. -2. The Redis key MUST be explicitly tenant-scoped as required by Constitution C2. -3. The value MUST include an unguessable `lease_token` and process-unique executor identity for diagnostics. -4. Acquisition MUST not replace an unexpired lease. -5. Lease renewal MUST compare the current token before extending expiry. -6. Lease release MUST compare the current token before deletion. -7. The lease expiry MUST exceed the code execution deadline plus bounded publication cleanup, or the lease MUST be safely renewed while work remains active. -8. Failure to acquire the lease MUST return a stable retryable busy outcome without starting code. - -### FR3. Lease loss and Redis outage - -1. If Redis ownership cannot be established before execution, code MUST not start. -2. If lease renewal becomes unverifiable during execution, no unguarded Workspace publication may occur. -3. If code may have run but publication safety cannot be proven, the existing Tool receipt MUST settle as `unknown`, not automatically retry the side effect. -4. Redis recovery permits later invocations after the lease expires; no durable execution fact is reconstructed from Redis. -5. Non-code Tools and non-Session Runs do not acquire this lease and retain their existing behavior. - -### FR4. Executor Code Workspace modes - -Executor Code configuration MUST expose: - -```text -workspace_mode = merge | isolated_output -``` - -#### `merge` - -1. Preserve current temporary Workspace materialization. -2. The local Sandbox may modify the materialized copy according to the existing Sandbox contract. -3. At invocation settlement, calculate and conditionally write changed files back to Workspace storage. -4. Existing version/hash checks remain authoritative. -5. Publication completes before the Tool step is reported as successfully settled. - -#### `isolated_output` - -1. An exact canonical `session_id` is mandatory. -2. Materialize the Workspace so code can read the current durable contents. -3. Materialized Sandbox directories are writable within the current Agent loop; - only `/workspace/output/{session_id}` is eligible for host publication. -4. The output directory MUST be included in materialization for every later invocation in that Session. -5. At invocation settlement, collect changes only under `output/{session_id}`. -6. Conditionally write those changes to the corresponding durable Agent Workspace path. -7. Never publish modifications outside the Session output prefix. -8. A later invocation on any Runtime Worker can rematerialize, read, edit, and republish those files. -9. Missing or invalid Session identity MUST fail closed rather than use a shared Agent output directory. - -### FR5. Bubblewrap mount contract - -In `isolated_output` mode, the local backend MUST enforce the equivalent of: - -```text -ro-bind /workspace -bind /workspace -``` - -The implementation MAY use a safe equivalent mount topology, but tests MUST demonstrate that writes outside the fixed prefix fail and writes inside it succeed. - -The virtual environment and platform runtime paths remain governed by the existing Sandbox backend contract and are not Session output. - -### FR6. Publication semantics - -1. Workspace publication MUST happen during each `execute_code` Tool settlement, before releasing the execution lease. -2. WebSocket disconnect, Agent Run completion, Session idle, and bubblewrap process exit MUST NOT independently trigger a second implicit publication. -3. Successful and failed code invocations MAY both leave files under `isolated_output`. -4. Code status and publication status MUST remain distinguishable. -5. If code exits non-zero but output publication succeeds, the Tool remains a code-execution failure while reporting any published artifact references. -6. If publication conflicts or becomes unprovable after code ran, use existing conflict/unknown Tool semantics and do not silently overwrite. -7. Temporary execution scripts, `.tmp`, pip proxy files, virtual environments, caches, and platform internals MUST not be published. - -### FR7. Execution serialization boundary - -1. The execution lease serializes `execute_code` for one exact Session across Runtime Worker processes. -2. Different Sessions for the same Agent use different lease keys and MAY execute concurrently. -3. `merge` and `isolated_output` invocations for the same Session share the same execution lease to prevent mixed-mode overlap. -4. Existing Workspace path locks and conditional writes remain required; the execution lease does not replace them. - -### FR8. Output retention - -1. The intended default retention policy for `output/{session_id}` is seven days after the latest qualifying output activity. -2. Retention metadata, when implemented, MUST have a durable owner and MUST not exist only in Redis. -3. Cleanup MUST not delete files while the Session execution lease is held. -4. If current Workspace storage has no suitable durable retention record, V1 MUST leave physical deletion disabled rather than implement an unsafe Worker-local timer. -5. Adding a durable retention model and cleanup daemon requires an explicit design decision and migration in `design.md`/`tasks.md`. - -### FR9. Rollout and compatibility - -1. `workspace_mode` MUST default so existing Agents preserve their current `merge` behavior. -2. Existing active LangGraph checkpoints require no rewrite. -3. Existing `AgentToolExecution` rows and durable Tool recovery leases remain compatible. -4. Remote Sandbox backends MUST not be forced into a local bubblewrap mount contract they cannot enforce. -5. The feature MUST explicitly define which Sandbox backend types support `isolated_output`; unsupported combinations fail configuration validation rather than silently behaving as `merge`. - -## 7. Failure and Recovery Requirements - -| Failure | Required behavior | -|---|---| -| Runtime Worker exits before code starts | Durable Tool/Command recovery applies; execution lease eventually expires. | -| Runtime Worker exits while code runs | Process-local bwrap dies with its parent where supported; lease expires; durable Tool receipt governs recovery. | -| Redis unavailable before acquisition | Code does not start. | -| Redis unavailable after code starts | Publication is blocked unless lease ownership can be safely revalidated; uncertain outcome uses `unknown`. | -| Duplicate Tool attempt | Existing `AgentToolExecution` reservation prevents unsafe duplicate execution; Redis lease is only an additional concurrency guard. | -| Another Worker attempts same Session | It cannot start code while the first execution lease is valid. | -| Workspace file changes after materialization | Conditional write reports conflict/unknown; newer durable file is not silently overwritten. | -| Later invocation lands on another Worker | It rematerializes Session output from durable Workspace and does not need prior Worker state. | - -## 8. Security and Tenant Isolation - -1. Redis lease keys MUST begin with or contain explicit `tenant:{tenant_id}:` scope. -2. `agent_id` and `session_id` MUST be derived from trusted Runtime context and validated before key/path construction. -3. Output paths MUST be normalized and proven beneath both the Agent Workspace root and exact `output/{session_id}` prefix. -4. Session IDs used as path components MUST be canonical identifiers without path separators or traversal. -5. Symlinks and bind-mount targets MUST not escape the materialized Workspace or Session output staging root. -6. Redis values MUST not contain source code, Workspace contents, credentials, model messages, or Tool results. -7. Logs MUST not expose lease tokens; a non-reversible short hash MAY be used for diagnostics. - -## 9. Observability Requirements - -Structured logs and metrics MUST expose: - -- execution lease acquisition, contention, renewal failure, expiry, and release; -- executor process identity and Session scope without leaking secret tokens; -- selected Workspace mode and Sandbox backend; -- bubblewrap invocation start/finish and timeout; -- Session output publication updated/deleted/conflicted/skipped counts; -- publication success, failure, and unknown outcomes; -- rejected unsupported `workspace_mode`/backend combinations. - -## 10. Acceptance Criteria - -### AC1. Runtime Worker affinity is absent - -Given two eligible Runtime Workers, consecutive commands for one Session may be claimed by different Workers through the existing database algorithm; no Worker-specific queue or Session owner lease is created. - -### AC2. Same-Session code serialization - -Given two concurrent `execute_code` attempts for the same tenant, Agent, and Session, only the Redis execution-lease holder starts code. The other returns a stable retryable busy outcome. - -### AC3. Different Sessions execute independently - -Given two Sessions belonging to one Agent, they use different tenant-scoped lease keys and may run concurrently. - -### AC4. Owner-only renewal and release - -Given a stale or foreign lease token, it cannot renew or delete the active execution lease. - -### AC5. Redis outage fails closed for Session code - -Given Redis is unavailable before execution, local Session `execute_code` does not start and durable Runtime state remains recoverable. - -### AC6. Loop-scoped bwrap reuse - -Given two code calls in one Agent loop, they reuse one bubblewrap child and its -working copy. The child is reaped when the loop settles; continuity across -later loops is promised only for files published to durable Workspace. - -### AC7. Isolated output permissions - -Given `workspace_mode=isolated_output`, code can read and modify the materialized -working copy during one Agent loop, while only changes beneath -`/workspace/output/{session_id}` are published to the host Workspace. - -### AC8. Output survives Worker change - -Given Worker A publishes `output/{session_id}/report.csv`, a later invocation on Worker B rematerializes, reads, updates, and republishes that file. - -### AC9. No cross-Session output writes - -Given Session A, code cannot publish into Session B's output directory or a shared `output` root. - -### AC10. Merge regression - -Given `workspace_mode=merge`, existing materialization, sync-back, version conflict, and Tool outcome behavior remains unchanged apart from the execution lease around local Session code. - -### AC11. Failed execution can publish isolated artifacts - -Given code writes a diagnostic file under its Session output and then exits non-zero, the diagnostic file may be conditionally published and referenced while the Tool result remains failed. - -### AC12. Conflict does not overwrite - -Given a durable output file changes after materialization, sync-back does not silently overwrite it and settles through the existing conflict/unknown contract. - -### AC13. Unsupported backend fails explicitly - -Given a backend that cannot enforce `isolated_output`, configuration or execution returns a stable unsupported-mode error and does not silently grant broader writes. - -### AC14. Checkpoint compatibility - -Given a pre-feature active checkpoint, it resumes without checkpoint schema rewriting because the execution lease and Workspace mode are outside mutable LangGraph lifecycle state. - -## 11. Required Verification Scope - -Implementation verification MUST include: - -- Redis lease acquire, contention, renew, expiry, and owner-only release tests; -- two logical Runtime Worker identities contending for one Session execution; -- Redis unavailable fail-closed tests; -- one-bubblewrap-per-Agent-loop lifecycle tests; -- `isolated_output` read/write mount-boundary tests; -- path traversal, symlink, and cross-Session isolation tests; -- materialize/publish/rematerialize tests across different logical Workers; -- failed execution with successfully published diagnostic output; -- Workspace conflict and unknown-outcome tests; -- current `merge` regression tests; -- existing Agent Runtime Tool ledger and command recovery tests; -- `scripts/arch-guard.sh`. - -## 12. Open Design Decisions - -The following are deferred to `design.md`: - -1. Exact Redis key/value layout and Lua scripts. -2. Lease TTL and renewal interval relative to configured code timeout. -3. How `workspace_mode` is added to the existing Executor Code configuration schema. -4. The smallest safe change to current temporary Workspace materialization and bubblewrap mount construction. -5. Exact typed busy/unsupported/publication error codes. -6. Whether V1 adds durable seven-day retention metadata or explicitly defers physical cleanup. diff --git a/docs/prd/features/agent-directory/default-agent-seeding-technical-design.md b/docs/prd/features/agent-directory/default-agent-seeding-technical-design.md deleted file mode 100644 index 82da0b852..000000000 --- a/docs/prd/features/agent-directory/default-agent-seeding-technical-design.md +++ /dev/null @@ -1,280 +0,0 @@ -# 默认 Agent 一次性初始化技术方案 - -> 状态:待实现 -> -> 范围:Morty、Meeseeks 的首次创建、升级兼容和存储自愈 - -## 1. 业务语义 - -Morty 和 Meeseeks 是租户首次完成平台初始化时创建的默认 Agent。 - -初始化成功后,平台必须尊重用户对这两个 Agent 的生命周期操作: - -- 用户删除后,后续启动、重启和升级不得重新创建。 -- 用户重命名后,不得因为默认名称消失而创建同名副本。 -- 用户仅停止 Agent 时,不得创建副本。 -- 未删除的默认 Agent 如果 workspace 或 Skills 存储损坏,启动时仍可执行非覆盖式修复。 - -因此,“是否创建默认 Agent”与“是否修复默认 Agent 存储”必须是两项独立判断。 - -## 2. 当前实现与问题 - -### 2.1 当前调用链 - -`seed_default_agents()` 在两个入口运行: - -- 后端启动流程:`backend/app/main.py` -- 首个平台注册用户创建完成后:`backend/app/api/auth.py` - -重复调用本身是允许的,前提是 seeder 具备可靠的一次性语义。 - -### 2.2 当前创建判据 - -当前 seeder 按以下条件查找已有默认 Agent: - -```python -Agent.tenant_id == admin.tenant_id -Agent.name.in_(["Morty", "Meeseeks"]) -Agent.agent_type == "native" -Agent.status != "stopped" -``` - -如果对应名称不在查询结果中,就创建新的 Agent。 - -这个判据把可变运行状态当成了初始化事实: - -- 删除接口会保留 Agent 行,同时设置 `deleted_at` 和 `status="stopped"`。 -- 停止接口也会设置 `status="stopped"`。 -- 重命名会改变 `name`。 - -因此删除、停止和重命名都可能被错误解释为“从未初始化”。 - -### 2.3 现有 seed marker - -存储中已有 `_bootstrap/.seeded`,但默认 Agent seeder 当前只写入、不读取该标记。该文件不能作为新的唯一事实源:部署可能更换或丢失存储,而数据库仍然保留。 - -### 2.4 必须保留的存储自愈 - -现有 `_repair_default_agent_storage()` 会为仍存在的默认 Agent 修复缺失的根目录和 Skills 目录,并避免覆盖用户文件。这个能力必须保留,不能恢复成“发现 seed marker 后整段 seeder 直接返回”。 - -## 3. 技术目标 - -1. 使用租户级、持久、与名称和运行状态无关的初始化事实。 -2. 默认 Agent 每个租户最多自动初始化一次。 -3. 删除、停止、重命名均不触发重新创建。 -4. 对未删除的默认 Agent 保留存储自愈。 -5. 兼容没有数据库初始化标记的现有部署。 -6. 多实例同时启动时不得重复创建。 -7. 不增加依赖,优先复用现有表和数据库锁模式。 - -## 4. 数据事实源 - -### 4.1 新的规范事实 - -复用现有 `tenant_settings` 表,不新增表和 Alembic migration。 - -建议设置项: - -```text -key = "bootstrap:default_agents:v1" -``` - -建议 value: - -```json -{ - "initialized": true, - "agents": { - "morty": "", - "meeseeks": "" - }, - "source": "created|legacy_marker|database_history" -} -``` - -语义: - -- 设置项存在即表示该租户已经完成过默认 Agent 初始化;`initialized=true` 用于校验和诊断。即使 value 损坏,也必须保守地停止自动创建并记录告警。 -- Agent ID 是稳定身份,用于后续存储修复;不再通过名称反查身份。 -- ID 对应 Agent 已删除或物理不存在时,也不得重新创建。 -- `source` 仅用于诊断和升级审计,不参与业务判断。 - -### 4.2 删除事实 - -`Agent.deleted_at` 是 Agent 是否被用户逻辑删除的事实源。 - -- `deleted_at is None`:Agent 仍存在,可以检查和修复存储。 -- `deleted_at is not None`:Agent 已删除,跳过存储修复,也不得补建。 -- `status` 只描述运行状态,不参与初始化或删除判断。 - -### 4.3 legacy marker 的角色 - -`_bootstrap/.seeded` 只用于现有部署的兼容识别和运维诊断,不再作为长期唯一事实源。 - -后续如仍需写入 legacy marker,必须使用追加/合并方式,不能覆盖 `okr_agent` 等其他 seed 信息。 - -## 5. 核心流程 - -### 5.1 并发边界 - -进入租户默认 Agent 初始化流程后,先获取租户级 PostgreSQL transaction advisory lock。锁键建议包含租户 ID和固定命名空间: - -```text -default-agent-bootstrap: -``` - -锁内重新读取 `tenant_settings`,避免多个后端实例同时判断“未初始化”并重复创建。 - -### 5.2 已有数据库标记 - -如果 `bootstrap:default_agents:v1` 已存在: - -1. 不执行任何默认 Agent 创建。 -2. 按设置中保存的 Agent ID 查询数据库,查询必须包含 stopped 和逻辑删除行。 -3. 对 `deleted_at is None` 的 Agent 调用 `_repair_default_agent_storage()`。 -4. 对已删除或不存在的 Agent 直接跳过。 - -### 5.3 新租户首次初始化 - -如果数据库标记不存在,并且兼容识别没有发现历史初始化事实: - -1. 创建 Morty 和 Meeseeks。 -2. 创建 Participant、权限、默认工具和相互关系。 -3. 初始化 workspace 和 Skills。 -4. 在同一数据库事务中写入 `bootstrap:default_agents:v1`,保存两个 Agent ID。 -5. 提交事务。 -6. 数据库提交成功后,以追加方式更新 legacy marker;marker 写入失败只记录告警,不回滚已经成立的数据库事实。 - -数据库中的 Agent 和初始化设置必须一起提交,避免出现“Agent 已创建但初始化设置缺失”的中间状态。 - -## 6. 现有部署兼容 - -### 6.1 是否必须回填 - -如果采用 `tenant_settings` 作为新的规范事实,现有租户必须建立这个事实,否则“数据库标记不存在”仍可能被错误理解为全新租户。 - -但不需要: - -- 新增 Alembic 数据迁移; -- 单独执行离线回填脚本; -- 人工逐租户处理。 - -采用 seeder 首次运行时的懒回填即可。也就是说,兼容回填是逻辑上必须的,但不需要独立发布步骤。 - -### 6.2 懒回填顺序 - -数据库标记不存在时,按以下顺序识别历史初始化: - -1. 读取 legacy marker 中的 `morty`、`meeseeks` ID。 -2. 校验 marker 指向的 Agent 是否属于当前租户;查询包含已删除和 stopped 行。 -3. 如果 marker 无法使用,则查询当前租户所有历史 Agent 行,包括已删除和 stopped 行,查找曾存在的 canonical 名称 Morty/Meeseeks。 -4. 发现任一可信历史证据,就写入 `bootstrap:default_agents:v1`,`source` 分别记录为 `legacy_marker` 或 `database_history`,不创建缺失 Agent。 -5. 只有完全没有数据库标记、有效 legacy marker 和历史 Agent 证据时,才执行首次创建。 - -这里采用保守策略:有历史证据时宁可不自动创建,也不能覆盖用户删除意图。 - -### 6.3 无法完全恢复的历史状态 - -如果现有部署同时满足以下条件: - -- legacy marker 已丢失; -- 默认 Agent 已被重命名; -- 数据库中没有可识别的 canonical 名称历史; -- 数据库初始化标记尚未建立; - -系统无法只根据现有数据可靠证明该 Agent 曾由默认 seeder 创建。不得通过角色描述、Bio 或 workspace 内容做模糊猜测。 - -该极端状态只能通过运维确认后补写租户设置。修复上线后,新数据库标记会消除后续同类歧义。 - -### 6.4 已被旧逻辑重新创建的 Agent - -升级兼容过程不自动删除当前活跃 Agent。系统无法可靠判断用户是否已经开始使用旧逻辑重新创建出的对象。 - -用户可在修复上线后再次删除该 Agent;数据库初始化事实已经建立,后续不会再次创建。 - -## 7. 代码改动范围 - -### 7.1 `backend/app/services/agent_seeder.py` - -- 引入 `TenantSetting`。 -- 增加默认 Agent 设置 key 和 value 解析函数。 -- 增加 legacy marker 解析和懒回填函数。 -- 增加租户级 transaction advisory lock。 -- 将 `seed_default_agents()` 拆为: - - 初始化事实解析; - - 首次创建; - - 现存 Agent 存储修复。 -- 移除以 `name + status != stopped` 作为创建判据的逻辑。 -- 保留 `_repair_default_agent_storage()` 的非覆盖语义。 -- legacy marker 改为追加/合并写入,避免覆盖其他 seeder 条目。 - -### 7.2 `backend/tests/test_agent_seeder_storage_repair.py` - -扩展现有测试覆盖初始化状态、兼容回填和删除语义。 - -不需要修改前端、Agent 删除接口或数据库结构。 - -## 8. 测试设计 - -### 8.1 首次创建 - -- 没有设置、marker 和历史 Agent 时创建两个默认 Agent。 -- 创建与租户初始化设置在同一事务提交。 -- 初始化失败时不留下 `initialized=true`。 - -### 8.2 已初始化 - -- 两个 Agent 都存在:不创建,继续执行存储健康检查。 -- Morty 已删除:不创建 Morty,只检查未删除的 Meeseeks。 -- 两个都已删除:不创建,也不修复存储。 -- Agent 仅 stopped、未删除:不创建副本,仍允许存储修复。 -- Agent 已重命名:按 ID 识别,不创建 canonical 名称副本。 -- 设置中的 Agent ID 已不存在:不创建。 - -### 8.3 兼容回填 - -- legacy marker 有效:写入租户设置,不创建。 -- marker 指向已删除 Agent:仍视为已初始化,不创建。 -- marker 缺失但数据库存在历史 canonical Agent:写入租户设置,不创建。 -- marker 来自其他租户或格式损坏:忽略 marker,继续数据库历史判断。 -- 完全没有历史证据:执行首次创建。 - -### 8.4 并发 - -- 两个 seeder 并发进入时,只有锁内第一个流程可以创建。 -- 第二个流程取得锁后重新读取设置并进入已初始化分支。 - -### 8.5 回归验证 - -- 运行 `backend/tests/test_agent_seeder_storage_repair.py`。 -- 运行与 Agent 删除、列表可见性相关的 scoped tests。 -- 对修改文件运行 Ruff。 -- 验证现有存储漂移修复测试继续通过。 - -## 9. 验收标准 - -- 新租户仍自动获得 Morty 和 Meeseeks。 -- 删除任一默认 Agent 后,连续重启两次均不出现新副本。 -- 重命名任一默认 Agent 后,连续重启两次均不出现 canonical 名称副本。 -- stop 后重启不产生副本。 -- 未删除默认 Agent 的 workspace/Skills 丢失后仍能被修复。 -- 现有部署无需人工脚本即可自动建立数据库初始化事实。 -- 多实例同时启动不会重复创建默认 Agent。 - -## 10. 非目标与风险 - -- 本次不自动清理旧版本已经创建的重复 Agent。 -- 本次不改变普通 Agent 的删除、停止或重命名接口。 -- 本次不把 Morty/Meeseeks 改成不可删除的 system Agent。 -- 本次不以名称、Bio、角色描述等可变内容作为长期身份。 -- legacy marker 丢失且历史 Agent 已重命名的极端部署,需要运维确认;不做推测性自动修复。 - -## 11. 实施顺序 - -1. 先补删除、停止、重命名和 legacy 回填的失败测试。 -2. 增加租户初始化设置和兼容解析函数。 -3. 加入租户级并发锁。 -4. 拆分首次创建与存储修复路径。 -5. 运行 scoped tests 和 Ruff。 -6. 使用本地数据库验证首次初始化与删除后重启。 -7. 部署前检查目标环境当前 marker、历史默认 Agent 行和重复 Agent 状态,不自动清理数据。 diff --git a/docs/technical-plans/20260728-dao-migration-plan.md b/docs/technical-plans/20260728-dao-migration-plan.md deleted file mode 100644 index 2c34ab7ea..000000000 --- a/docs/technical-plans/20260728-dao-migration-plan.md +++ /dev/null @@ -1,149 +0,0 @@ -# DAO 层改造迁移计划 - -> 状态:进行中(基础设施 + auth 域已完成,其余业务待迁移) -> 起始提交:`60ffcb0` refactor(db): introduce ContextVar DAO layer (#678) - -## 一、现状 - -**已完成的基础设施**(`60ffcb0` 引入,可作为标准范式) - -- `app/dao/base.py` — `BaseDAO`,基于 `ContextVar` 的 `session()` 上下文管理,内置 CRUD -- `app/database.py` — `_session_ctx`、`transaction()` 事务边界工具、`get_db()` 依赖 -- 8 个 DAO 单例:`user / identity / identity_provider / invitation_code / org_member / participant / system_setting / tenant` - -**完全改造完成的业务** - -- `auth.py`(0 处 `get_db` 残留) -- 相关 service:registration / password_reset / platform / system_email / email_service - -**未完成的工作量(量化)** - -| 层 | 指标 | 数量 | -|---|---|---| -| API 层 | 残留 `Depends(get_db)` | 231 处,分布在 ~38 个路由文件 | -| API 层 | 混合状态(部分改造) | `agents.py` 16 处残留 | -| Service 层 | 直接 `async_session`/`get_db` | 29 个文件 | -| DAO 单例 | 已建 / 模型总数 | 8 / ~30 个模型 | - ---- - -## 二、目标与原则 - -1. **数据库访问收敛到 DAO**:API / Service 不再直接 `Depends(get_db)` 或 `async_session()`,只调用 DAO 方法或 `transaction()`。 -2. **事务按需、不默认**:`transaction()` 仅在「多步写需要原子性」时使用;单条读 / 单条写走 DAO 即可(见决策点 1)。 -3. **多租户隔离不破**:每个自定义查询方法必须过滤 `tenant_id`(见 `.agents/rules/design_and_dev.md`)。 -4. **风格统一**:每个 DAO 一个 `XxxDAO(BaseDAO[Model])` 类 + 模块级单例 `xxx_dao`,在 `app/dao/__init__.py` 汇总导出。 -5. **可增量、可回滚**:一次只动一组相关模型,每个 PR 自洽、可独立合并、有测试。 - ---- - -## 三、迁移标准步骤(每个模型/模块套用) - -1. 新建 `app/dao/xxx_dao.py`,继承 `BaseDAO[Model]`,把该路由/service 里所有原生 SQL 查询搬成具名方法。 -2. 查询方法默认走 `async with self.session()`(自动复用 context session 或新建)。 -3. 需要跨多个 DAO 写一致的操作,外层用 `async with transaction():` 包裹,DAO 内部 `flush()` 而非 `commit()`。 -4. 在 `__init__.py` 注册单例。 -5. 改造调用方:路由去掉 `db: AsyncSession = Depends(get_db)`,service 去掉 `async_session()`。 -6. 补/改单元测试(mock DAO 或用现有测试 DB fixture)。 -7. Ruff(line 120 / py3.11)+ `grep get_db` 清零校验。 - ---- - -## 四、关键设计决策 - -### 决策点 1 · Service 层(含守护任务)的事务策略 ✅ 已对齐 - -> Service 层(含守护任务)强制走 **DAO**;事务只在「多步写需要原子性」时用 `transaction()` 显式包裹,**按需而非默认**。 - -`transaction()` 对守护任务的本质作用不是"开事务",而是"建一个 session 并注入 ContextVar"。 -因为守护任务在请求外运行、`_session_ctx` 为 None,会走 `transaction()` 的最后一条分支(新建 session + commit)。 -因此判断标准与请求内一致——看是否需要原子性,而不是看是否在请求外。 - -| 操作 | 推荐做法 | -|---|---| -| 单条读 | DAO 方法即可,DAO 内部 `self.session()` 自己建 session | -| 单条写 | DAO `create/update/delete`,内部 `flush()`,session 由 `self.session()` 退出时 commit | -| 多条写、要原子 | `async with transaction():` 框住,内部 DAO 只 `flush()`,最外层 commit 一次 | - -**关键坑**:`BaseDAO.session()` 自建的 session 退出时会 commit。所以多次 DAO 调用各自 commit、没有原子性;要原子性**必须**外层 `transaction()`,此时各 DAO 复用同一 context session。 - -### 决策点 2 · 读操作 commit 开销(待定) - -当前 `BaseDAO.session()` 对自建 session 一律 commit,读操作 commit 无副作用但略浪费。 -可选:给 `BaseDAO` 加 `readonly` 路径只 flush / 不 commit。 - -### 决策点 3 · 跨 DAO 组合查询放哪(建议) - -放进调用方 service 用 `transaction()` 编排,而不是在某个 DAO 里写跨表 join,保持 DAO 单模型职责。 - ---- - -## 五、分阶段计划(按优先级 + 耦合度排序) - -> 每个 Phase = 一个或多个独立 PR。优先级依据:核心域 > 业务频次 > 渠道适配器。 - -### Phase 0 · 收尾已动工模块 ⭐ 最高优先级 - -- `agents.py`(16 处残留):已是混合状态,风险最高。补齐 `agent_dao`(含 `agent_credential` 关联),清掉全部 `get_db`。 -- **目标**:让"改造中"文件归零,消除双范式并存。 - -### Phase 1 · 核心域(高频 + 高耦合) - -| 文件 | get_db | 待建 DAO(模型) | -|---|---|---| -| `tools.py` | 18 | `tool_dao`(Tool) | -| `enterprise.py` | 36 | `audit_dao`、`org_dao`(Org 已部分有 org_member)、`tenant_setting_dao` | -| `tenants.py` | 14 | `tenant_setting_dao`(tenant_dao 已有) | -| `chat_sessions.py` | 6 | `chat_session_dao` | -| `tasks.py` | 7 | `task_dao` | -| `users.py` | 4 | 复用 user_dao | -| `focus.py` | 4 | `focus_dao` | -| `notification.py` | 6 | `notification_dao` | -| `schedules.py` | 7 | `schedule_dao` | - -### Phase 2 · 组织 / 关系 / 治理 - -| 文件 | get_db | 待建 DAO | -|---|---|---| -| `relationships.py` | 10 | 复用 org_member / 新建关系查询方法 | -| `organization.py` | 3 | 补 org_member_dao | -| `advanced.py` | 10 | 多模型,逐方法迁移 | -| `admin.py` | 9 | 复用 system_setting / audit | -| `activity.py` | 4 | `activity_log_dao` | -| `onboarding.py` | 5 | `onboarding_dao` | -| `agent_credentials.py` | 5 | `agent_credential_dao` | -| `agentbay_control.py` | 9 | 评估是否纯转发 | -| `pages.py` / `plaza.py` / `skills.py` / `okr.py` | 0~3 | `published_page_dao`、`plaza_dao`、`skill_dao`、`okr_dao` | - -### Phase 3 · 渠道适配器(量大但模式重复,可并行) - -`feishu / dingtalk / wecom / wechat / teams / slack / whatsapp / discord_bot / google_workspace / atlassian / sso` —— 这些大多只是查 `channel_config` / `participant`,模式高度雷同。 - -- **建议**:先沉淀 `channel_config_dao`,再做一次性批量迁移模板,渠道逐个套用。 -- 含 `gateway.py`(6) / `messages.py`(3)。 - -### Phase 4 · Service 层下沉(29 个文件) - -事务策略按决策点 1 处理——**按需 `transaction()`,不默认包事务**。按依赖深度分两批: - -1. **浅依赖**(2-3 处,纯查询):`audit_logger / activity_logger / chat_session_service / channel_user_service / token_tracker / template_seeder / feishu_ws / dingtalk_stream / timezone_utils` → 直接换 DAO 调用。 -2. **深依赖 / 后台守护**(`agent_tools` 75 处、`heartbeat`、`okr_*`、`trigger_daemon`、`scheduler`、`quota_guard`、`task_executor`、`resource_discovery`、`agent_context`、`wechat_channel`、`wecom_stream`、`agent_seeder`、`agentbay_client`)→ 逐方法判断:单步写走 DAO;多步原子写用 `transaction()` 框住。 - ---- - -## 六、每个 PR 的验收清单 - -- [ ] 目标文件 `grep -E "Depends\(get_db\)|async_session"` 归零(守护类按决策点 1 处理,多步写处可见 `transaction()`) -- [ ] 新 DAO 方法均过滤 `tenant_id`(适用时) -- [ ] `app/dao/__init__.py` 已注册新单例 -- [ ] 相关单测通过;Ruff 通过 -- [ ] 无 `DetachedInstanceError`(参考 #686:session 关闭后不要再访问关系字段,必要时 `selectinload`) - ---- - -## 七、推进节奏 - -- **本周**:Phase 0(agents 收尾)单独出一个 PR,跑通"收尾混合文件"的流程。 -- **接下来 2-3 周**:Phase 1 按文件拆 PR(每个文件 1 PR,便于 review)。 -- **并行**:Phase 3 渠道迁移可交给多人/多 agent 并行套模板。 -- **最后**:Phase 4 service 下沉收尾,重点处理守护进程的上下文与原子性判断。 diff --git a/docs/technical-plans/20260728-private-chat-finish-migration-plan.md b/docs/technical-plans/20260728-private-chat-finish-migration-plan.md deleted file mode 100644 index 0848a63da..000000000 --- a/docs/technical-plans/20260728-private-chat-finish-migration-plan.md +++ /dev/null @@ -1,420 +0,0 @@ -# Finish 协议迁移方案:私信自然结束与群聊 at - -状态:已按方案实施并通过本地回归,待最终审查与合并。 - -> 本文同时记录私信和群聊的完成协议。私信使用自然停止;群聊在此基础上使用独立的 `at` Tool 表达结构化 Agent mention。 - -## 第一部分:私信自然结束方案 - -决策:私信与主流 Agent Loop 保持一致,不再要求模型调用带完整正文的 `finish(content=...)`。最终回答使用普通 Assistant content,Runtime 根据 Provider 的原生停止原因和是否存在 Tool Call 判断本轮是否完成。不要改成 `` 等正文结束标记;文本标记仍可能被遗漏、重复、截断或与用户内容冲突。 - -私信阶段先独立落地,群聊现有结构化 mention、`group_handoff`、child Run 和同 Session 公开回复在第一阶段保持不变。 - -### 目标执行语义 - -1. 响应包含 Tool Call:执行工具、写入 Tool Result,并继续模型循环;正文不能绕过仍待执行的工具直接完成 Run。 -2. 响应不包含 Tool Call,停止原因为自然结束且正文非空:把普通 Assistant content 作为内部完成候选,继续走现有 verify、finalize、checkpoint 和投递链路。 -3. 停止原因为输出长度上限:视为截断,不得把已有半段正文当作完整答案发布;进行一次有界的“重新生成完整答案”修复,重复截断后以明确的 `model_incomplete_output` 失败。 -4. 停止原因为安全过滤、拒绝或未知异常:进入对应的结构化非成功结果,不得伪装成已验证完成。 -5. 自然停止但正文为空:进行一次有界空响应修复;重复为空后失败,不再提示模型调用 `finish`。 - -### 停止原因归一化 - -当前 `LLMResponse.finish_reason` 已在 Provider Client 层存在,但 `backend/app/services/llm/single_step.py` 的 `LLMCompletionStep` 没有该字段,`complete_llm_once()` 返回时会丢失停止原因。第一步应增加并透传规范化的 `finish_reason`: - -| Provider 原始值 | Runtime 规范值 | 私信处理 | -| --- | --- | --- | -| `stop`、`end_turn`、`stop_sequence` | `stop` | 无 Tool Call且正文非空时进入验证 | -| `tool_calls`、`tool_use`,或响应实际包含 Tool Call | `tool_calls` | 执行工具并继续 | -| `length`、`max_tokens` | `length` | 截断修复,不得投递 | -| `content_filter`、`safety`、`recitation` | `content_filter` | 结构化非成功结果 | -| `refusal` | `refusal` | 结构化拒绝结果 | -| 未识别值 | `unknown` | 不得直接判定完成 | - -兼容期可以允许旧 OpenAI-compatible 模型的“`finish_reason=None`、无 Tool Call、正文非空”按自然结束处理并记录诊断日志,避免本地模型立即回归;显式的 `length`、过滤或拒绝不能进入该兼容分支。 - -### 代码改动范围 - -1. `backend/app/services/llm/single_step.py` - - 为 `LLMCompletionStep` 增加规范化的 `finish_reason`。 - - 从 Provider `LLMResponse` 透传该字段,Tool Call 存在时优先归一为 `tool_calls`。 -2. `backend/app/services/agent_runtime/model_step_service.py` - - 私信工具集合移除并过滤模型可见的 `finish`,停止通过 `_with_runtime_tools()` 为私信强制注入它。 - - `_parse_step()` 按停止原因区分自然完成、工具执行、截断、过滤、拒绝和空响应。 - - 保留内部 `ModelStepResult(intent="finish")`;它只是 Runtime 状态名,不再代表模型必须调用同名 Tool。 -3. `backend/app/services/agent_runtime/node_executor.py` - - 继续复用现有 `verifying -> completed`、`final_answer`、verification 和 finalization 主链。 - - 把私信的 `missing_finish` / `FINISH_PROTOCOL_REMINDER` 语义改为空响应或不完整输出修复,错误信息不再声称模型必须调用 `finish`。 -4. `backend/app/services/llm/caller.py` - - 旧调用入口同步接受自然停止的普通正文,删除私信的 `FINISH_PROTOCOL_REMINDER` 循环。 - - 发送 Provider 请求前过滤 `finish`;`skip_tools=True` 时发送空工具集合。 - - 最终正文确认完成后再交给用户输出回调,避免中间工具轮的普通文字被误投递为最终答案。 -5. `backend/app/api/enterprise.py` - - 模型工具调用能力探针不再要求 `finish(content="ok")`,改用无副作用的 `capability_probe(value="ok")`。 - - 探针只判断原生工具调用、工具名和参数 JSON 是否正确,不再把 Clawith 私有收尾协议当作通用工具能力。 -6. `backend/app/services/agent_tools.py` 与 builtin 定义 - - 当前实际数据库已经确认不存在 `finish` Tool row,因此不需要数据库清理或数据迁移。 - - 第一阶段直接删除 `FINISH_TOOL_SEED` 及 `SYNC_IS_DEFAULT_TOOL_NAMES` 中的 `finish`,防止后续 bootstrap Seeder 创建该 row。 - - 暂时保留旧 parser 和 `execute_tool("finish")` no-op,仅用于部署切换时恢复旧 checkpoint;这项兼容不依赖数据库 Tool row。 - - 稳定一个版本并确认没有旧调用后,再单独删除遗留 parser、no-op executor、Prompt 和旧协议测试。 - -### 回归测试与验收标准 - -1. 私信模型请求的 Tool Schema 不再包含 `finish`。 -2. `finish_reason=stop`、无 Tool Call、正文非空时,一次模型响应即可进入验证和完成,不产生 `FINISH_PROTOCOL_REMINDER`。 -3. 长最终回答始终保存在普通 Assistant content 中,不进入任何 Tool arguments JSON。 -4. `finish_reason=length` 即使带非空正文也不得完成或投递;一次有界重生成后仍截断则结构化失败。 -5. `content_filter`、`refusal`、未知停止原因和重复空响应不得被误记为成功 Run。 -6. 普通应用 Tool Call 仍按原顺序执行并继续模型循环;同时存在正文时也不能提前完成。 -7. `skip_tools=True` 的私信可以在没有任何 Tool Schema 的情况下自然完成。 -8. OpenAI、Anthropic、Gemini以及缺少停止原因的 OpenAI-compatible 模型均有停止原因归一化回归。 -9. 旧的合法 `finish` 响应保留一条过渡兼容测试,但不再作为正常私信成功路径或工具能力标准。 -10. 现有群聊结构化 mention、预检、checkpoint handoff、公开投递和 child Run 回归在第一阶段必须保持不变。 - -### 实施顺序 - -1. 先增加 `finish_reason` 透传和停止原因单元测试。 -2. 再移除私信模型可见 `finish`,启用自然正文完成。 -3. 同步旧 caller 和 Enterprise capability probe。 -4. 运行私信 Runtime 定向回归、LLM Client 测试以及后端全量测试和静态检查。 -5. 私信阶段稳定后,再实施本文第二部分的群聊 `at` 协议;两个阶段保持独立提交和验证。 - ---- - -## 第二部分:群聊 at 协议 - -### 决策 - -群聊把“说什么”“@谁”“是否自然结束”和“最终路由副作用”拆成四个概念: - -| 概念 | 表达方式 | 职责 | -| --- | --- | --- | -| 最终公开回复 | 普通 Assistant content | 只包含群成员应该看到的业务正文 | -| 结构化 Agent mention | `at` Tool | 只设置下一条最终回复需要唤醒的 Agent | -| 模型结束 | Provider `finish_reason` | 区分自然停止、Tool Call、截断和异常停止 | -| child Run 路由 | Runtime `group_handoff` | 预检通过后冻结并在投递事务中执行 | - -模型不再调用 `finish`,也不再把公开正文放入 Tool arguments。 - -### 模型可见的 at Tool - -`at` 只在 Group Agent Run 中注入: - -```json -{ - "type": "function", - "function": { - "name": "at", - "description": "Set the complete list of Group Agents that must be visibly mentioned and woken by the next final public reply. This only stages routing and does not send a message or finish the Run.", - "parameters": { - "type": "object", - "properties": { - "participant_ids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - }, - "maxItems": 100, - "uniqueItems": true - } - }, - "required": ["participant_ids"], - "additionalProperties": false - } - } -} -``` - -调用约定: - -1. `participant_ids` 是下一条最终公开回复需要唤醒的完整 Agent 集合,不是增量列表。 -2. 模型必须先通过 `group_query_members` 获取稳定 participant UUID,不能根据显示名猜测 ID。 -3. 后一次成功的 `at` 调用覆盖此前暂存集合。 -4. `at([])` 清除当前暂存集合。 -5. `at` 可以和普通 Tool Call 共存,不要求成为本轮唯一 Tool Call。 -6. `at` 不包含公开正文,不表示 Run 已完成,也不立即创建 child Run。 - -### 标准两步 Tool Loop - -使用 Provider 通用的 Tool Call → Tool Result → Final Assistant content 流程: - -```text -group_query_members - ↓ -取得 participant_id - ↓ -at(participant_ids) - ↓ -Runtime 暂存目标,不发送消息、不创建 child Run - ↓ -返回 Tool Result - ↓ -模型输出普通最终 Assistant content - ↓ -finish_reason=stop - ↓ -正文与结构化目标双向校验 - ↓ -preflight → verify → finalize - ↓ -原子发布公开消息并创建 child Run -``` - -如果 Provider 在 `at` Tool Call响应中同时返回 Assistant content,该 content 只作为工具轮草稿进入历史,不能直接作为公开最终回复。Runtime 仍然返回 Tool Result,并等待下一轮自然最终正文。 - -### 分层结构 - -#### 1. 模型输出层 - -模型只输出: - -- 普通 Assistant content; -- 真实业务 Tool Call; -- Group Run 中可选的 `at` Tool Call。 - -模型不再看到 `finish`、`finish.content`、`FINISH_PROTOCOL_REMINDER` 或文本结束标记。 - -#### 2. Provider 响应归一化层 - -继续使用第一部分定义的 `LLMCompletionStep`: - -```python -LLMCompletionStep( - content: str | None, - tool_calls: tuple[dict, ...], - finish_reason: str | None, - reasoning_content: str | None, - retry_instruction: str | None, - usage: TokenUsage, -) -``` - -该层只统一 Provider 差异,不执行群聊业务。 - -#### 3. Runtime 响应解释层 - -现有 `model_step_service._parse_step()` 继续承担响应解释,但删除模型可见 `finish` 的特殊分支: - -```python -if step.tool_calls: - return tool_calls_route() - -if step.finish_reason == "stop" and step.content: - return final_candidate(step.content) - -if step.finish_reason == "length": - return incomplete_output_repair() - -return abnormal_completion() -``` - -这不是新增 Runtime 层,而是收敛现有职责:Tool Call进入 Tool Node,自然正文进入 Verify Node,截断和异常停止不得误判完成。 - -为了降低第一阶段改动风险,内部 `intent="finish"` 和 `finish_content` 可以暂时作为兼容命名保留;它们只代表内部最终候选,不再对应模型 Tool。后续再机械重命名为 `intent="final"` 和 `final_content`。 - -#### 4. at 暂存状态层 - -`at` 进入标准 Tool Node,但只更新 checkpoint lifecycle: - -```json -{ - "pending_group_at": { - "participant_ids": [ - "participant-uuid" - ], - "tool_call_id": "call_xxx", - "staged_at_model_step": 4 - } -} -``` - -Runtime 同一次状态更新写入: - -- `pending_group_at`; -- 对应的 `role=tool` Tool Result。 - -Tool Result建议保持简短: - -```json -{ - "status": "staged", - "participant_count": 1 -} -``` - -`pending_group_at` 的生命周期: - -- `at` 成功后写入 checkpoint; -- 后续模型和工具轮次继续保留; -- 新 `at` 调用覆盖,`at([])` 清除; -- 最终预检通过并冻结正式 handoff 后清除; -- Run 失败或取消时丢弃; -- 不直接写入业务数据库表。 - -#### 5. 最终正文与路由预检层 - -模型自然停止并输出最终正文时,Runtime 同时读取: - -```text -final_content -pending_group_at -``` - -执行双向一致性校验: - -1. 正文没有 Agent `@名字`,也没有 `pending_group_at`:普通群聊回复。 -2. 正文包含 Agent `@名字`,但没有匹配的结构化 ID:不发布,要求模型查询成员并调用 `at`。 -3. `pending_group_at` 包含目标,但正文没有相应可见 `@名字`:不发布,防止后台唤醒用户看不到的 Agent。 -4. 正文目标与结构化 ID 不一致:不发布。 -5. 双向匹配后调用现有 `preflight_group_agent_handoff()`。 - -完整预检继续校验: - -- Group 和 Session; -- source Run、parent/root lineage; -- sender participant; -- 目标仍是当前群成员; -- 目标是可运行的 Agent participant; -- 目标模型、预算和 rollout; -- cycle guard; -- cutoff 和 idempotency key。 - -通过后生成现有 `GroupAgentHandoffIntent`,并冻结为 `group_handoff_intent`。 - -如果正文或预检需要修复: - -- 不发送公开消息; -- 不创建 child Run; -- 保留 `pending_group_at`,允许模型修改正文; -- 模型可以重新调用 `at` 覆盖或清除目标。 - -#### 6. Verify、Finalize 与 Delivery 层 - -继续复用现有终态主链: - -```text -final content - ↓ -business verification - ↓ -terminal checkpoint - ↓ -delivery revalidation - ↓ -同一事务: -- 创建公开 ChatMessage -- 创建结构化 mentions -- 创建目标 child Runs -- 创建 Start Commands -- 写入 delivery event/receipt -``` - -正式 checkpoint 保持现有业务语义: - -```json -{ - "final_answer": "公开回复正文", - "delivery_request": { - "content": "公开回复正文", - "group_handoff": { - "mention_participant_ids": [ - "participant-uuid" - ] - } - } -} -``` - -模型面对的是 `at`;Runtime 和 checkpoint 继续使用 `group_handoff`,因为它表达的是经过预检的 child Run 路由事实。 - -### 数据库影响 - -核心方案不需要数据库 Schema 变化: - -- 不新增表、列、索引或外键; -- `pending_group_at` 存在现有 LangGraph checkpoint JSON 中; -- 正式 `group_handoff` 继续使用现有 checkpoint/delivery 结构; -- ChatMessage、mentions、AgentRun 和 Start Command 继续写入现有表。 - -`at` 是 Runtime 专用 Tool,由代码注入,不进入可配置 Tool 数据库。 - -2026-07-21 已直接连接当前项目配置指向的实际 `clawith` 数据库核对: - -```text -tools_table_exists=True -finish_rows=[] -matching_columns=[] -``` - -确认当前数据库: - -- `public.tools` 表存在,但没有 `name='finish'` 的 row; -- 没有 `finish_content`、`finish_delivery_intent`、`final_answer`、`group_handoff` 或 `pending_group_at` 独立列; -- 不需要清理旧 row; -- 不需要 Alembic 数据迁移或 Schema migration。 - -代码中仍有 `FINISH_TOOL_SEED`,bootstrap 的 `seed_builtin_tools()` 未来可能创建 `finish` row。因此协议迁移必须同时删除该 seed 和 `SYNC_IS_DEFAULT_TOOL_NAMES` 中的 `finish`,从源头防止以后写入数据库。 - -### 新风险与控制 - -| 风险 | 级别 | 控制方式 | -| --- | --- | --- | -| Provider `finish_reason` 缺失或不一致 | 高 | 统一归一化;兼容 `None + 非空正文`;显式截断和过滤不得完成 | -| `pending_group_at` 与 Tool Result写入不一致 | 高 | 在同一次 LangGraph state update 中原子写入 | -| 旧 checkpoint 仍包含 `finish` | 高 | 保留一版旧 parser/no-op,只是不再向新请求暴露 | -| at目标与最终正文不一致 | 高 | 最终发布前双向校验,不一致时零消息、零 child Run | -| `at` Tool Call重放 | 中 | Tool Call ID 幂等;重复执行不能产生外部副作用 | -| 多一次模型调用带来延迟和 Token | 中 | 接受标准两步 Tool Loop,换取跨 Provider 兼容性 | -| 目标在 at后退出群聊或失效 | 低 | 最终 preflight 和 delivery 二次校验 | -| Bootstrap Seeder 未来创建 finish row | 低 | 与协议迁移同时删除 `FINISH_TOOL_SEED` 和默认同步名单中的 `finish` | - -关键安全约束: - -1. `at` 永远不能直接发送公开消息或创建 child Run。 -2. `pending_group_at` 与 Tool Result必须原子进入 checkpoint。 -3. Tool Call存在时永远优先进入 Tool Node,同轮 content不能提前完成。 -4. 最终正文和结构化目标必须双向匹配。 -5. child Run 只允许在 verify 通过后的 terminal delivery 中创建。 -6. delivery retry 继续使用现有 idempotency key,不能重复发送或重复创建 child Run。 - -### 对 BUGS_TO_FIX.md 的影响 - -采用私信自然结束和群聊 `at` 后: - -1. 第 14 条中的 `invalid_finish` / `missing_finish` 主路径消失,不需要按原来的 `partial_answer` 方案修复。若未来仍要展示失败草稿,应作为通用失败恢复体验重新设计。 -2. 第 15 条的 finish开关主体问题被协议迁移取代:`finish` 不再是模型工具,也不再需要可配置开关。 -3. Enterprise 使用 finish探测工具能力的问题仍需修复,已纳入第一部分的 `capability_probe`。 -4. `repair_draft` 被当作普通流式 Assistant 内容展示仍是独立 Bug,不能因为移除 finish而忽略。 -5. “Native tool calling is not working”错误分类过宽仍需修复,应该区分 Tool JSON、at路由、Provider 能力和停止原因错误。 -6. 第 6 条模型验证和分配门禁仍需修复;Agent 仍依赖真实 Tool Calling能力。 -7. Group Planning/Compact 错误复用 Agent Tool Calling门禁的问题仍需独立处理。 -8. 长 `write_file` 参数截断与 finish正文迁移无关,仍是独立可靠性问题。 - -### 群聊回归测试与验收标准 - -1. Group Run 的 Tool Schema 包含 `at`,不包含模型可见 `finish`。 -2. 普通群聊回复无 Tool Call、自然停止后直接完成。 -3. `at` schema不包含正文,只接受 participant UUID 数组。 -4. `at` 成功时只写入 `pending_group_at` 和 Tool Result,不发送消息、不创建 child Run。 -5. checkpoint恢复后 `pending_group_at` 不丢失、不重复执行。 -6. 新 `at` 调用覆盖旧目标,`at([])` 可以清除。 -7. 同一响应含 content和 `at` 时只执行 Tool Loop,不提前发布 content。 -8. 字面 `@Agent` 缺少结构化 ID 时不发布。 -9. 结构化 ID缺少对应可见 `@Agent` 时不发布。 -10. 无效、离群、非 Agent或不可运行目标预检失败时零消息、零 child Run。 -11. 预检通过后 terminal checkpoint 包含完整 `group_handoff`。 -12. 成功投递时公开消息、mentions、child Runs和 Start Commands同事务创建。 -13. delivery retry 不重复消息、mentions或 child Run。 -14. B child Run 在同一 Group Session中公开回复。 -15. 私信和旧 checkpoint兼容测试保持通过。 - -### 分阶段实施 - -1. 先完成第一部分:停止原因透传和私信自然结束。 -2. 私信稳定后,增加 Group-only `at` Tool和 `pending_group_at`。 -3. 接入 Tool Node原子 checkpoint更新。 -4. 增加最终正文与结构化目标的双向校验。 -5. 复用现有 preflight、verify、terminal checkpoint和原子 delivery。 -6. 保留旧 `finish` 响应兼容一版,但不再向新模型请求暴露。 -7. 完成定向、全量和恢复/幂等测试后,再清理遗留 parser、no-op executor、Prompt、UI兼容文案和旧协议测试。 diff --git a/skills-lock.json b/skills-lock.json deleted file mode 100644 index eb92a4946..000000000 --- a/skills-lock.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": 1, - "skills": { - "guizang-ppt-skill": { - "source": "op7418/guizang-ppt-skill", - "sourceType": "github", - "skillPath": "SKILL.md", - "computedHash": "d3eb8db2dfc7faa08d7e837187a8935fcd9dfa8b540a9f03e7cd30ab4e4412e6" - } - } -} diff --git a/specs/001-fix-vercel-async-wait/checklists/requirements.md b/specs/001-fix-vercel-async-wait/checklists/requirements.md deleted file mode 100644 index ffb6dbe45..000000000 --- a/specs/001-fix-vercel-async-wait/checklists/requirements.md +++ /dev/null @@ -1,34 +0,0 @@ -# Specification Quality Checklist: Vercel Async Deployment Wait Recovery - -**Purpose**: Validate specification completeness and quality before proceeding to planning -**Created**: 2026-08-05 -**Feature**: [spec.md](../spec.md) - -## Content Quality - -- [x] No implementation details beyond named external business states and existing system boundaries -- [x] Focused on user value and business needs -- [x] Written for non-technical stakeholders -- [x] All mandatory sections completed - -## Requirement Completeness - -- [x] No NEEDS CLARIFICATION markers remain -- [x] Requirements are testable and unambiguous -- [x] Success criteria are measurable -- [x] Success criteria are technology-agnostic -- [x] All acceptance scenarios are defined -- [x] Edge cases are identified -- [x] Scope is clearly bounded -- [x] Dependencies and assumptions identified - -## Feature Readiness - -- [x] All functional requirements have clear acceptance criteria -- [x] User scenarios cover primary flows -- [x] Feature meets measurable outcomes defined in Success Criteria -- [x] No implementation details leak into the specification beyond necessary domain terminology - -## Notes - -- Validation passed in one review iteration. The feature is ready for implementation planning. diff --git a/specs/001-fix-vercel-async-wait/contracts/vercel-async-operation.md b/specs/001-fix-vercel-async-wait/contracts/vercel-async-operation.md deleted file mode 100644 index 61ee84ef5..000000000 --- a/specs/001-fix-vercel-async-wait/contracts/vercel-async-operation.md +++ /dev/null @@ -1,52 +0,0 @@ -# Contract: Vercel Declared Async Operation - -## Pending outcome - -```json -{ - "status": "pending", - "result_ref": null, - "metadata": { - "provider": "vercel", - "deployment_id": "dpl_xxx", - "deployment_state": "BUILDING", - "runtime_async_pending": true, - "async_operation": { - "version": 1, - "operation_id": "dpl_xxx", - "operation_key": "vercel:deployment:dpl_xxx", - "state": "BUILDING", - "poll": { - "tool": "vercel_deploy", - "arguments": { - "operation": "poll", - "deployment_id": "dpl_xxx", - "poll_failure_count": 0 - }, - "interval_ms": 2000 - } - } - } -} -``` - -## Terminal outcome - -The terminal result MUST retain the same operation key and set `runtime_async_pending` to `false`. - -| Provider state | Tool status | -| --- | --- | -| `READY` | `succeeded` | -| `ERROR` | `failed` | -| `CANCELED` | `failed` | -| missing, unknown, or mismatched observation | `unknown` | - -## Prohibited behavior - -- A poll MUST NOT call any project-create, file-upload, repository-link, or deployment-create endpoint. -- A poll MUST NOT use project deployment-list results for settlement. -- A non-terminal state or transient status-read timeout MUST NOT produce `succeeded`. -- Consecutive transient status-read failures MUST retry at most 10 times. A - successful provider observation resets the counter. Exhaustion MUST produce - `unknown`/reconciliation because the deployment's terminal state is unproven. -- The contract MUST NOT require a Model-generated wait or polling Tool call. diff --git a/specs/001-fix-vercel-async-wait/data-model.md b/specs/001-fix-vercel-async-wait/data-model.md deleted file mode 100644 index cbcd49c80..000000000 --- a/specs/001-fix-vercel-async-wait/data-model.md +++ /dev/null @@ -1,39 +0,0 @@ -# Data Model: Vercel Async Deployment Wait Recovery - -No schema migration or new persistent entity is required. The feature uses the existing Tool execution -receipt and Runtime checkpoint. - -## Deployment Operation Receipt - -Represents the original Vercel deployment and every subsequent status observation. - -| Field | Meaning | Validation | -| --- | --- | --- | -| `operation_id` | Vercel deployment ID | Non-empty, stable across polls | -| `operation_key` | Runtime settlement correlation | Non-empty, stable for one deployment | -| `state` | Latest normalized Vercel `readyState` | Known non-terminal or terminal state | -| `poll.tool` | Internal Tool continuation | `vercel_deploy` | -| `poll.arguments.operation` | Execution mode | `poll` | -| `poll.arguments.deployment_id` | Exact deployment to read | Equals `operation_id` | -| `poll.interval_ms` | Next scheduled observation | `2000` | -| `runtime_async_pending` | Whether more observations are required | `true` for non-terminal, `false` for terminal | - -## State Transitions - -```text -INITIALIZING ─┐ -QUEUED ─┼─> pending ─> exact poll ─> pending or terminal -BUILDING ─┘ - -READY -> succeeded -ERROR -> failed -CANCELED -> failed -unknown or invalid observation -> unknown, never succeeded -``` - -## Relationships - -- One Agent Run contains the original deployment Tool execution. -- Each scheduled poll creates or consumes a Tool execution associated with the same Run. -- All receipts for one deployment share `operation_key`. -- A terminal poll atomically settles the current poll and prior pending receipts with that key. diff --git a/specs/001-fix-vercel-async-wait/plan.md b/specs/001-fix-vercel-async-wait/plan.md deleted file mode 100644 index 8584dcba1..000000000 --- a/specs/001-fix-vercel-async-wait/plan.md +++ /dev/null @@ -1,81 +0,0 @@ -# Implementation Plan: Vercel Async Deployment Wait Recovery - -**Branch**: `001-fix-vercel-async-wait` | **Date**: 2026-08-05 | **Spec**: [spec.md](spec.md) -**Input**: Feature specification from `/specs/001-fix-vercel-async-wait/spec.md` - -## Summary - -Change the existing Vercel Tool Adapter so accepted deployments in INITIALIZING, QUEUED, or BUILDING -return the Runtime's existing declared asynchronous-operation contract instead of a successful Tool -receipt. Add an internal poll mode that performs one exact deployment status read and reuses the same -operation identity until READY, ERROR, or CANCELED. Reuse all current Runtime scheduling, resume, -waiting, and terminal-settlement code without modification. - -## Technical Context - -**Language/Version**: Python 3.11 -**Primary Dependencies**: FastAPI service stack, httpx, SQLAlchemy async ORM, existing Agent Runtime -**Storage**: Existing PostgreSQL-backed `AgentToolExecution.result_metadata`; no migration -**Testing**: pytest, pytest-asyncio, existing scripted Vercel provider fixtures -**Target Platform**: Clawith backend service and Runtime worker -**Project Type**: Existing web-service backend -**Performance Goals**: One status GET per scheduled poll; no blocking sleep inside Tool execution -**Constraints**: Exactly one deployment POST; fixed 2-second interval; no new dependency; no generic -Runtime changes; no public Tool behavior expansion -**Scale/Scope**: One Vercel Tool Adapter, its typed-outcome tests, and one existing Runtime-contract -integration path - -## Constitution Check - -*GATE: Passed before research and re-checked after design.* - -- **Evidence Before Claims**: Current Vercel and Runtime code paths were inspected; the defect is the - Vercel outcome mapping and missing internal poll mode. -- **Minimal Scoped Changes**: Source changes are limited to `backend/app/services/agent_tools.py` and - scoped tests. Generic Runtime files are prohibited unless a failing contract test proves otherwise. -- **Contract and State Ownership**: Vercel maps `readyState`; Runtime consumes typed pending and - terminal outcomes. Model prose is not used for settlement. -- **Tests Prove Behavior**: Tests cover non-terminal mapping, repeated exact polling, terminal mapping, - original receipt settlement, and absence of duplicate deployment POSTs. -- **Preserve Existing Work**: Existing dirty files and ignored documentation remain untouched outside - the approved Spec Kit artifacts and Vercel bug-fix scope. - -Post-design re-check: PASS. The design adds no database entity, dependency, generic state machine, or -second scheduler. - -## Project Structure - -### Documentation (this feature) - -```text -specs/001-fix-vercel-async-wait/ -├── spec.md -├── plan.md -├── research.md -├── data-model.md -├── quickstart.md -├── contracts/ -│ └── vercel-async-operation.md -├── checklists/ -│ └── requirements.md -└── tasks.md -``` - -### Source Code - -```text -backend/ -├── app/services/agent_tools.py -└── tests/ - ├── test_agent_tools_typed_vercel_deploy.py - ├── test_agent_runtime_tool_step_service.py - └── test_agent_runtime_async_tool_poll.py -``` - -**Structure Decision**: Keep implementation inside the existing monolithic built-in Tool Adapter. -Reuse existing Runtime tests where possible; add only the smallest Vercel-specific integration -coverage needed to prove the generic contract consumes the new outcome. - -## Complexity Tracking - -No constitution violations or added architectural complexity. diff --git a/specs/001-fix-vercel-async-wait/quickstart.md b/specs/001-fix-vercel-async-wait/quickstart.md deleted file mode 100644 index 10012b68e..000000000 --- a/specs/001-fix-vercel-async-wait/quickstart.md +++ /dev/null @@ -1,39 +0,0 @@ -# Quickstart: Verify the Vercel Async Wait Stopgap - -## 1. Run Vercel Adapter tests - -```bash -cd backend -.venv/bin/python -m pytest tests/test_agent_tools_typed_vercel_deploy.py -``` - -Verify that BUILDING produces a pending asynchronous operation, the internal poll path issues only an -exact deployment GET, READY succeeds, ERROR/CANCELED fail, and no poll repeats a deployment POST. - -## 2. Run Runtime contract tests - -```bash -cd backend -.venv/bin/python -m pytest \ - tests/test_agent_runtime_async_tool_poll.py \ - tests/test_agent_runtime_tool_step_service.py -``` - -Verify that the existing Runtime schedules the pending outcome and terminal settlement closes the -original receipt without changes to generic Runtime code. - -## 3. Run scoped static checks - -```bash -cd backend -.venv/bin/ruff check \ - app/services/agent_tools.py \ - tests/test_agent_tools_typed_vercel_deploy.py \ - tests/test_agent_runtime_async_tool_poll.py \ - tests/test_agent_runtime_tool_step_service.py -``` - -## 4. Diff boundary - -Confirm that production code changes are limited to the Vercel Tool Adapter. Generic Scheduler, -Resume, LangGraph wait, terminal settlement, and other Tool files must remain unchanged. diff --git a/specs/001-fix-vercel-async-wait/research.md b/specs/001-fix-vercel-async-wait/research.md deleted file mode 100644 index 010915634..000000000 --- a/specs/001-fix-vercel-async-wait/research.md +++ /dev/null @@ -1,57 +0,0 @@ -# Research: Vercel Async Deployment Wait Recovery - -## Decision 1: Reuse the declared asynchronous Tool contract - -**Decision**: Emit the existing `runtime_async_pending + async_operation` metadata from -`vercel_deploy` for non-terminal provider states. - -**Rationale**: The Runtime already persists due times, schedules idempotent timer resumes, reconstructs -poll calls, and atomically settles all same-Run receipts sharing an operation key. - -**Alternatives considered**: - -- Restore a blocking `while + sleep` loop: rejected because it occupies a Tool worker and loses the - durable restart behavior introduced by the Runtime. -- Add a Vercel-specific scheduler: rejected because it duplicates an existing generic mechanism. -- Let the Model call `wait(external)`: rejected because that wait has no guaranteed pending operation - or resume producer. - -## Decision 2: Use the exact deployment identifier - -**Decision**: Poll `GET /v13/deployments/{deployment_id}` and keep one stable operation key derived -from that deployment identity. - -**Rationale**: The create response already supplies the identity. Project deployment lists cannot -prove which deployment belongs to the original Tool operation. - -**Alternatives considered**: - -- `vercel_list_deployments`: rejected because list ordering and concurrent deployments make the - correlation ambiguous. - -## Decision 3: Keep polling internal and fixed-interval - -**Decision**: The Runtime-generated poll invokes an internal `vercel_deploy` mode with the existing -two-second interval. Known provider-pending states may continue polling, while consecutive status-read -failures are capped at the Runtime safe-read limit of 10 and reset after a successful observation. The -public Model-facing deployment request remains unchanged. - -**Rationale**: This is the smallest compatible change and prevents Model turns between polls. - -**Alternatives considered**: - -- Publicly expose a new status Tool or operation discriminator: rejected as unnecessary API expansion. -- Add adaptive backoff or an overall provider-pending deadline: deferred because the approved scope is - production stopgap, not Runtime redesign. Exhausted status-read failures enter reconciliation rather - than declaring the external deployment failed without provider proof. - -## Decision 4: Preserve provider truth at terminal settlement - -**Decision**: READY succeeds; ERROR and CANCELED fail; known non-terminal states remain pending. -Missing, unknown, or mismatched state never succeeds. - -**Rationale**: Acceptance of a deployment request is not proof that the deployment completed. - -**Alternatives considered**: - -- Treat accepted or BUILDING as success: rejected because it caused the reported stuck Run. diff --git a/specs/001-fix-vercel-async-wait/spec.md b/specs/001-fix-vercel-async-wait/spec.md deleted file mode 100644 index 42fa1e801..000000000 --- a/specs/001-fix-vercel-async-wait/spec.md +++ /dev/null @@ -1,130 +0,0 @@ -# Feature Specification: Vercel Async Deployment Wait Recovery - -**Feature Branch**: `001-fix-vercel-async-wait` -**Created**: 2026-08-05 -**Status**: Draft -**Input**: User description: "Fix the Vercel asynchronous deployment wait bug with the smallest -possible change. Keep non-terminal deployments pending, poll the exact deployment through the -existing durable Runtime, settle only on a provider terminal state, and never create a duplicate -deployment." - -## User Scenarios & Testing *(mandatory)* - -### User Story 1 - Receive the Final Deployment Result (Priority: P1) - -As a user who asks an Agent to deploy a project to Vercel, I receive a final response after the -specific deployment reaches a terminal state instead of seeing the Agent remain stuck waiting after -Vercel has completed the deployment. - -**Why this priority**: This is the reported production failure. A deployment can finish successfully -while the user never receives a completion response. - -**Independent Test**: Start one deployment that reports BUILDING before READY. The system must keep -the operation pending, check the same deployment again, settle it as successful, resume the same -Run, and make the final result available for the Agent response. - -**Acceptance Scenarios**: - -1. **Given** Vercel accepts a deployment and reports INITIALIZING, QUEUED, or BUILDING, **When** the - initial deployment call completes, **Then** the operation remains pending and the Run waits for - the existing Runtime polling mechanism. -2. **Given** the tracked deployment is pending, **When** Vercel later reports READY, **Then** the - original deployment operation succeeds and the same Run continues to its final response. -3. **Given** the tracked deployment is pending, **When** Vercel later reports ERROR or CANCELED, - **Then** the original deployment operation fails and the same Run continues through existing - failure handling. - ---- - -### User Story 2 - Avoid Duplicate Deployments (Priority: P2) - -As a user waiting for a deployment, I expect status checks to observe the deployment already created -for my request and never create additional deployments. - -**Why this priority**: Repeating an external write while polling can deploy stale or duplicate -versions and violates the existing exactly-once Tool contract. - -**Independent Test**: Exercise multiple non-terminal status checks followed by a terminal status and -verify that the provider receives exactly one create request while every status check uses the -original deployment identifier. - -**Acceptance Scenarios**: - -1. **Given** a deployment has already been created, **When** one or more Runtime polls execute, - **Then** each poll performs only an exact status read for the original deployment. -2. **Given** a poll is resumed after a process restart, **When** it executes, **Then** it uses the - persisted deployment identifier and does not repeat project creation, upload, repository linking, - or deployment creation. - -### Edge Cases - -- A status read times out after a stable deployment identifier has already been received; the - deployment must not be reported as successful solely because the create request was accepted. -- A successful status response omits a usable state, reports an unknown state, identifies a - different deployment, or lacks a valid deployment URL; the system must not fabricate success. -- Vercel reports READY immediately in the create response; the operation completes without entering - the asynchronous wait path. -- Vercel reports ERROR or CANCELED immediately; the operation fails without scheduling another poll. -- A project contains multiple deployments; list results must not settle the deployment operation - because only the exact deployment identifier is authoritative. - -## Requirements *(mandatory)* - -### Functional Requirements - -- **FR-001**: The system MUST treat INITIALIZING, QUEUED, and BUILDING as non-terminal deployment - states. -- **FR-002**: A non-terminal deployment MUST remain pending and include a stable operation identity, - exact deployment identity, and instructions for the existing Runtime polling mechanism. -- **FR-003**: Every status check MUST query the exact deployment created by the original request. -- **FR-004**: A status check MUST NOT create a project, upload files, link a repository, or create a - deployment. -- **FR-005**: READY MUST settle the original operation as successful. -- **FR-006**: ERROR and CANCELED MUST settle the original operation as failed. -- **FR-007**: A status timeout after receipt of a stable deployment identity MUST NOT settle the - operation as successful. -- **FR-008**: A missing, unknown, or mismatched provider state MUST NOT settle the operation as - successful. -- **FR-009**: All non-terminal checks for one deployment MUST retain the same operation identity so - the existing Runtime can settle the original operation at terminal completion. -- **FR-010**: Deployment list results MUST NOT settle or resume the original deployment operation. -- **FR-011**: The final terminal result MUST allow the same Run to continue and produce its existing - user-facing completion or failure response. -- **FR-012**: The change MUST reuse the existing asynchronous Runtime scheduling, waiting, resume, and - settlement behavior without changing generic wait behavior or other Tool contracts. - -### Key Entities - -- **Deployment Operation**: The single external deployment created for the user request, identified - by a stable provider deployment identity and a stable operation identity. -- **Deployment State Observation**: One exact observation of the tracked deployment, containing its - provider state and usable URL when available. -- **Agent Run**: The existing execution that initiated the deployment, waits while the operation is - pending, and continues after terminal settlement. - -## Success Criteria *(mandatory)* - -### Measurable Outcomes - -- **SC-001**: In all automated scenarios where a deployment progresses through one or more - non-terminal states to READY, the initiating Run resumes and reaches its final response. -- **SC-002**: Every tested deployment operation issues exactly one provider deployment-create - request, regardless of the number of status checks. -- **SC-003**: All tested provider terminal states map deterministically: READY succeeds, while ERROR - and CANCELED fail. -- **SC-004**: No tested non-terminal, missing, unknown, timed-out, or mismatched status is recorded as - successful. -- **SC-005**: Existing asynchronous Runtime regression tests continue to pass without behavior - changes outside the Vercel deployment path. - -## Assumptions - -- The existing durable Runtime scheduler, timer resume, waiting checkpoint, and operation settlement - contracts remain the authoritative implementation and already function for declared asynchronous - Tool operations. -- Polling uses the existing fixed two-second interval for this stopgap fix. -- General retry backoff, maximum retry counts, total operation deadlines, provider cancellation, - rejected-resume reclamation, and generic Model/Runtime wait conflicts remain out of scope. -- The public deployment request remains unchanged; polling is an internal continuation of an already - accepted operation. -- No other Tool behavior is changed unless a separate reproducible failure is established. diff --git a/specs/001-fix-vercel-async-wait/tasks.md b/specs/001-fix-vercel-async-wait/tasks.md deleted file mode 100644 index d748d390b..000000000 --- a/specs/001-fix-vercel-async-wait/tasks.md +++ /dev/null @@ -1,80 +0,0 @@ -# Tasks: Vercel Async Deployment Wait Recovery - -**Input**: Design documents from `/specs/001-fix-vercel-async-wait/` -**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/ - -**Tests**: Regression tests are required by the feature specification and Constitution. - -## Phase 1: Baseline - -**Purpose**: Confirm the existing Vercel and generic Runtime contracts before editing production code. - -- [x] T001 Run the current scoped baseline in `backend/tests/test_agent_tools_typed_vercel_deploy.py`, `backend/tests/test_agent_runtime_async_tool_poll.py`, and `backend/tests/test_agent_runtime_tool_step_service.py` - ---- - -## Phase 2: User Story 1 - Receive the Final Deployment Result (Priority: P1) 🎯 MVP - -**Goal**: Keep non-terminal Vercel deployments pending and settle the original operation when the -exact deployment reaches READY, ERROR, or CANCELED. - -**Independent Test**: A scripted deployment progresses BUILDING → READY; the initial result is -pending, the internal poll reads the exact deployment, and the terminal result carries the same -operation key for existing Runtime settlement. - -### Tests for User Story 1 - -- [x] T002 [US1] Replace the accepted-BUILDING success expectation with pending-contract and internal-poll terminal cases in `backend/tests/test_agent_tools_typed_vercel_deploy.py` - -### Implementation for User Story 1 - -- [x] T003 [US1] Add the minimal Vercel provider-state helper, internal poll branch, pending outcome, and terminal operation metadata in `backend/app/services/agent_tools.py` -- [x] T004 [US1] Prove the existing Runtime consumes the Vercel pending and terminal contracts using scoped coverage in `backend/tests/test_agent_runtime_tool_step_service.py` or an existing equivalent test - -**Checkpoint**: BUILDING remains pending, READY succeeds, ERROR/CANCELED fail, and the original Run -can continue through the existing Runtime. - ---- - -## Phase 3: User Story 2 - Avoid Duplicate Deployments (Priority: P2) - -**Goal**: Ensure every continuation performs only an exact status read for the original deployment. - -**Independent Test**: Multiple internal polls issue zero project creates, uploads, repository links, -or deployment POSTs and always use the original deployment ID. - -### Tests for User Story 2 - -- [x] T005 [US2] Add assertions that internal polls perform only exact deployment GET requests and never repeat external writes in `backend/tests/test_agent_tools_typed_vercel_deploy.py` - -### Implementation for User Story 2 - -- [x] T006 [US2] Verify the internal poll discriminator branches before launch validation and all external write stages in `backend/app/services/agent_tools.py` - -**Checkpoint**: One user request produces exactly one Vercel deployment POST regardless of poll count. - ---- - -## Phase 4: Validation - -**Purpose**: Prove the stopgap and enforce the approved diff boundary. - -- [x] T007 Run scoped pytest for `backend/tests/test_agent_tools_typed_vercel_deploy.py`, `backend/tests/test_agent_runtime_async_tool_poll.py`, and `backend/tests/test_agent_runtime_tool_step_service.py` -- [x] T008 Run scoped Ruff on `backend/app/services/agent_tools.py` and modified test files, then verify generic Runtime production files are unchanged - ---- - -## Dependencies & Execution Order - -- T001 establishes the baseline. -- T002 must precede T003 so the regression is observable before implementation. -- T003 enables T004 and T005. -- T005 validates T006; both use the same source and test files, so they run sequentially. -- T007 and T008 run after all implementation tasks. - -## Implementation Strategy - -Implement only User Story 1 and User Story 2 as one minimal stopgap. Do not add a new scheduler, -deadline, backoff policy, cancellation path, public Tool, or generic wait rule. Stop if the existing -Runtime contract cannot consume the declared async outcome without production Runtime changes and -report that evidence before expanding scope. diff --git a/specs/002-tool-runtime-contract/checklists/requirements.md b/specs/002-tool-runtime-contract/checklists/requirements.md deleted file mode 100644 index 9c4a731f5..000000000 --- a/specs/002-tool-runtime-contract/checklists/requirements.md +++ /dev/null @@ -1,36 +0,0 @@ -# Specification Quality Checklist: Tool Runtime 契约与执行链路修复 - -**Purpose**: 在进入规划阶段前验证需求规格的完整性和质量 -**Created**: 2026-08-10 -**Feature**: [spec.md](../spec.md) - -## Content Quality - -- [x] No implementation details (languages, frameworks, APIs) -- [x] Focused on user value and business needs -- [x] Written for non-technical stakeholders -- [x] All mandatory sections completed - -## Requirement Completeness - -- [x] No [NEEDS CLARIFICATION] markers remain -- [x] Requirements are testable and unambiguous -- [x] Success criteria are measurable -- [x] Success criteria are technology-agnostic (no implementation details) -- [x] All acceptance scenarios are defined -- [x] Edge cases are identified -- [x] Scope is clearly bounded -- [x] Dependencies and assumptions identified - -## Feature Readiness - -- [x] All functional requirements have clear acceptance criteria -- [x] User scenarios cover primary flows -- [x] Feature meets measurable outcomes defined in Success Criteria -- [x] No implementation details leak into specification - -## Notes - -- 第一次校验即通过,无 `[NEEDS CLARIFICATION]` 项。 -- `Tool Call`、`Run`、`Receipt`、`checkpoint` 等词是本产品领域对象,不是具体实现方案;具体数据结构、文件和迁移步骤将在 Plan 阶段定义。 -- Spec 已覆盖用户确认的 Tool repair/retry 上限统一为 10、模型可见错误反馈、unknown write 禁止自动重放和旧 checkpoint 兼容边界;计数结构统一重构已明确延期。 diff --git a/specs/002-tool-runtime-contract/contracts/repair-and-lifecycle.md b/specs/002-tool-runtime-contract/contracts/repair-and-lifecycle.md deleted file mode 100644 index dc6db0791..000000000 --- a/specs/002-tool-runtime-contract/contracts/repair-and-lifecycle.md +++ /dev/null @@ -1,33 +0,0 @@ -# Contract: Repair Budget and Execution Lifecycle - -## Tool Repair Episode - -- `same_fingerprint_failures` reaches 10: pause immediately after recording the 10th failure; do not invoke model step 11 for that loop. -- `total_failures` reaches 10 for the same Tool episode: pause immediately; do not invoke the next model step. -- Generic Tool protocol repair, `write_file` protocol repair, and safe-read replay retain their current independent counters but each uses a limit of 10; counter unification is deferred. -- Changing fingerprint resets only the consecutive counter. -- Success of the same Tool, new Run, or explicit user correction resets the Tool episode. -- Success of another Tool does not reset it. - -Global `model_turn_limit`, Provider transport retry, Command retry, Receipt safe-read attempt and Verifier episode are independent budgets with independent stop reasons. - -## Verifier Episode - -Verifier attempts belong to a fingerprinted current issue. A passing verification closes the episode. A materially new issue begins at zero; historical repair attempts do not consume its budget. - -## Deadline / Cancel / Lease - -| Control | Meaning | Must not imply | -|---|---|---| -| Operation deadline | Maximum wait for one handler/provider operation | Receipt ownership loss or proof no write occurred | -| Durable cancel | User/platform intent to stop the Run | Automatic rollback of an external write | -| Receipt lease | Which Worker may execute/settle the Receipt | Handler completion deadline | - -Rules: - -1. deadline precedence is explicit call value, then Tool policy default, capped by Tool policy maximum; -2. cancel propagates to supported subprocess/network/SDK operations and otherwise stops waiting with capability telemetry; -3. long Handler renews lease while owning it and fences before side effect/settlement; -4. lease loss prevents stale owner settlement; -5. deadline/cancel/disconnect after a possible write yields unknown/reconcile unless a stable provider/business receipt proves outcome; -6. unknown write cannot be automatically replayed by model, Command retry or Worker restart. diff --git a/specs/002-tool-runtime-contract/contracts/step-tool-context.md b/specs/002-tool-runtime-contract/contracts/step-tool-context.md deleted file mode 100644 index c6dc5e234..000000000 --- a/specs/002-tool-runtime-contract/contracts/step-tool-context.md +++ /dev/null @@ -1,37 +0,0 @@ -# Contract: Step Tool Context - -## Producer - -`RuntimeModelStepService` produces version 1 context only after the actual primary/fallback Provider response has been accepted. The context must describe the exact Workset sent to that Provider call. - -## Consumer - -`RuntimeToolStepService` consumes the context before validation, authorization or Receipt reservation. - -## Rules - -1. `assistant_message_id`, pending calls and accepted call entries must match exactly. -2. Tool name, schema, contract version, effect/retry policy and binding come from accepted context. -3. New-format Tool Step must not call ToolProvider or re-evaluate assignment/enabled/channel/readiness. -4. Current tenant, actor, resource, credential, approval and cancel checks remain mandatory. -5. Binding mismatch/corruption fails before Receipt; it is not guessed or rebuilt. -6. Legacy checkpoint may resolve a batch once and must emit compatibility telemetry. - -## Identity - -- `provider_call_id`: optional original wire ID; -- `call_instance_id`: required Clawith ID placed in checkpoint `tool_calls[].id` and DB `tool_call_id`; -- `execution_id`: created/resolved by Receipt reservation. - -Provider output returned to the Provider must use its expected Provider call identity. Internal projections and idempotency use Call Instance/Execution identity. - -## Binding Validity - -Ordinary visibility changes affect the next Model Step only. Hard safety invalidators for an accepted Call are: - -- tenant/actor mismatch; -- resource authorization revoked; -- credential revoked/unavailable; -- exact registered handler/provider target removed without compatible resolver; -- durable Run cancellation; -- corrupted context or contract version unsupported. diff --git a/specs/002-tool-runtime-contract/contracts/tool-result.md b/specs/002-tool-runtime-contract/contracts/tool-result.md deleted file mode 100644 index 29e023946..000000000 --- a/specs/002-tool-runtime-contract/contracts/tool-result.md +++ /dev/null @@ -1,40 +0,0 @@ -# Contract: Tool Result and Failure Feedback - -## Model-visible Envelope - -```json -{ - "role": "tool", - "tool_call_id": "call_instance_id", - "name": "tool_name", - "execution_status": "succeeded|failed|pending|unknown", - "error_code": "stable_optional_code", - "content": "bounded sanitized summary", - "model_action": "continue|repair_arguments|choose_other_tool|ask_user|wait|reconcile", - "side_effect_state": "none|confirmed|possible|unknown", - "safe_remediation": "optional bounded instruction", - "result_ref": "optional opaque reference" -} -``` - -## Exactly-once Feedback - -- A valid Call Instance with a repairable deterministic failure receives one Tool Result. -- Checkpoint replay reuses the deterministic result message ID and Receipt result. -- Invalid/missing Call identity is protocol corruption and cannot invent a Tool Result pairing. - -## Classification - -| Situation | Runtime state | Count repair? | Automatic replay? | -|---|---|---:|---:| -| Schema/argument failure | failed Tool Result | yes | model decides | -| Deterministic business rejection | failed Tool Result | yes when repairable | model decides | -| Permission/confirmation | waiting | no | no | -| Async operation | pending | no | poll only | -| Durable cancel | cancelled terminal | no | no | -| Possible external write | unknown/reconcile | no | no | -| Provider transport retry | internal | no | bounded safe retry only | - -## Sanitization - -Never include secrets, plaintext credential/config, complete sensitive arguments, stack traces, unbounded provider bodies or raw exception strings. Error codes are stable product vocabulary; summary and remediation have byte limits. diff --git a/specs/002-tool-runtime-contract/data-model.md b/specs/002-tool-runtime-contract/data-model.md deleted file mode 100644 index 3a328ddbe..000000000 --- a/specs/002-tool-runtime-contract/data-model.md +++ /dev/null @@ -1,121 +0,0 @@ -# Data Model: Tool Runtime Contract - -## Identity Model - -| Identity | Scope | Authority | Purpose | -|---|---|---|---| -| `provider_call_id` | Provider assistant response | Provider wire protocol | Assistant/tool response pairing and diagnostics only | -| `call_instance_id` | One accepted Assistant Tool Call inside a Run | Clawith Model Step | Checkpoint, message, Activity, Chat and A2A correlation | -| `execution_id` | One durable Receipt row | PostgreSQL `AgentToolExecution.id` | Lease, attempt, result archive, async poll and reconciliation | -| `business_idempotency_key` | Provider/business operation | Tool adapter/provider | External side-effect deduplication when supported | - -Compatibility mapping: current DB column `tool_call_id` stores `call_instance_id`. It is not renamed in the first migration. - -## Checkpoint Entities - -### StepToolContext - -```json -{ - "version": 1, - "assistant_message_id": "...", - "model_step": 3, - "workset_version": "sha256:...", - "accepted_calls": [ - { - "call_instance_id": "...", - "provider_call_id": "...", - "tool_name": "read_document", - "contract_version": "builtin:read_document:v2", - "schema": {}, - "binding": {}, - "effect": "read", - "retry_policy": "safe" - } - ] -} -``` - -Invariants: - -- one context belongs to exactly one Assistant message; -- `call_instance_id` is unique inside the Run and stable across replay; -- `provider_call_id` may be null for legacy/provider compatibility; -- schema/binding are JSON serializable, bounded and secret-free; -- new checkpoint pending calls must have a matching accepted call entry. - -### ToolWorksetEntry - -Fields: - -- `tool_name`: model-visible name; -- `contract_version`: immutable schema/behavior version; -- `parameters_schema`: accepted model schema; -- `binding`: stable handler/provider target; -- `effect`: `read | write | external_write`; -- `retry_policy`: `safe | conditional | never`; -- `authorization_policy`: stable policy key; -- `deadline_policy`: stable policy key; -- `recovery_policy`: stable policy key. - -### ExecutionBinding - -Allowed forms: - -- builtin: `{kind: "builtin", handler_key: "read_document"}`; -- MCP: `{kind: "mcp", server_id, mcp_tool_name, credential_ref}`; -- group/A2A/AgentBay: stable adapter key plus resource reference. - -Forbidden fields: plaintext credentials, bearer tokens, decrypted config, live client objects, Python callable names that are not registry keys. - -### RepairEpisode - -```json -{ - "tool_name": "read_document", - "episode_id": "...", - "total_failures": 7, - "last_fingerprint": "schema_validation:missing:path", - "same_fingerprint_failures": 3, - "last_call_instance_id": "...", - "updated_at_model_step": 8 -} -``` - -Transitions: - -- count: model-visible and repairable `failed` result; -- reset all for tool: same Tool succeeds or user explicitly corrects the request; -- reset new Run: checkpoint starts empty; -- fingerprint change: reset only `same_fingerprint_failures` to 1; -- exclude: provider retry, safe replay, approval wait, pending, cancel, unknown. - -## PostgreSQL Changes - -### `agent_tool_executions` - -Add nullable columns: - -- `provider_call_id VARCHAR(255)`; -- `contract_version VARCHAR(255)`. - -Keep: - -- primary key `id` as `execution_id`; -- unique `(run_id, tool_call_id)` as Call Instance uniqueness; -- existing attempt, effect, retry, status, result and lease columns. - -No physical foreign keys are added. A non-unique tenant/run/provider index is optional only if observed diagnostics require it; first migration omits it to minimize write cost. - -## State Ownership - -- Workset/context/repair episode: LangGraph checkpoint, because they control execution transition. -- Receipt/result/lease: `AgentToolExecution`, because they are durable side-effect facts. -- Activity/Chat: idempotent projections, never authority for resume or repair counts. - -## Compatibility - -- Legacy Receipt row with null new fields remains readable. -- Legacy checkpoint without `StepToolContext` enters one-batch resolver and marks telemetry. -- New checkpoint with missing/mismatched context is corruption; it cannot silently fall back. -- Deletion of compatibility code requires zero observed uses across retention and rollback windows plus restore fixtures. diff --git a/specs/002-tool-runtime-contract/plan.md b/specs/002-tool-runtime-contract/plan.md deleted file mode 100644 index 50d3fef64..000000000 --- a/specs/002-tool-runtime-contract/plan.md +++ /dev/null @@ -1,130 +0,0 @@ -# Implementation Plan: Tool Runtime 契约与执行链路修复 - -**Branch**: `002-tool-runtime-contract` | **Date**: 2026-08-10 | **Spec**: [spec.md](./spec.md) -**Input**: Feature specification from `/specs/002-tool-runtime-contract/spec.md` - -## Summary - -在现有 Durable Runtime、`AgentToolExecution` Receipt、safe-read replay 和 unknown/reconcile 机制之上,增加一次 Model Step 固化、checkpoint 可恢复的 `StepToolContext`。新 Tool Step 只使用已接受的 Tool Contract/Execution Binding,不再调用 ToolProvider 重建 Workset;同时把 Provider Call ID、Runtime Call Instance 和 Execution Receipt 分离,统一 schema validation、authorization/approval、模型可见失败反馈,并将现有独立 Tool repair/retry 上限统一为 10。操作 deadline、取消传播和 Receipt lease 继续保持三个独立控制面。长期通过可渐进迁移的 RegisteredTool 收敛模型定义与执行能力,不一次性替换现有 Handler。 - -## Technical Context - -**Language/Version**: Python 3.11+ -**Primary Dependencies**: FastAPI, SQLAlchemy 2.x async ORM, PostgreSQL, LangGraph checkpoint, Pydantic, httpx -**Storage**: PostgreSQL `agent_tool_executions` + LangGraph PostgreSQL checkpoint;不新增第二套 Run 生命周期状态机 -**Testing**: pytest, pytest-asyncio, Ruff, Alembic heads/upgrade/downgrade, `scripts/arch-guard.sh` -**Target Platform**: Linux backend workers and API processes -**Project Type**: Multi-tenant web-service backend;本功能无必需前端改动 -**Performance Goals**: 新格式每个 Model Step 最多一次 ToolProvider 查询;Tool Step 为 0 次;checkpoint replay 不增加 ToolProvider 查询或副作用次数 -**Constraints**: 保持旧 checkpoint 可恢复;不新增依赖;所有查询 tenant-scoped;unknown write 禁止自动重放;不弱化 Receipt fence -**Scale/Scope**: Agent Runtime 核心链路、一个兼容型 Alembic 迁移、定向 Runtime/Tool tests;长期 Registry 只建立接口和迁移门槛,不在首轮搬迁全部工具 - -## Constitution Check - -*GATE: Phase 0 前与 Phase 1 后均通过。* - -- **C1 Runtime Boundary Isolation — PASS**:`StepToolContext` 和 repair episode 属于 LangGraph checkpoint 的执行生命周期;`AgentToolExecution` 继续只保存 Receipt/结果事实,API 和产品投影不推进 Runtime 状态。 -- **C2 Strict Multi-Tenant Scope — PASS**:所有 execution/authorization/binding lookup 必须携带 `tenant_id`;binding 不能成为跨 tenant 的可执行引用。 -- **C3 Idempotent Side Effects — PASS**:保留 `AgentToolExecution.id`、lease owner/fence、unknown/reconcile 和 safe-read bounded retry;Call Instance 只增强身份,不绕过 Receipt。 -- **C4 Client/Gateway Wrapper — PASS**:Provider/Tool 调用仍经统一 Runtime/Tool executor;不引入直接外部访问旁路。 -- **C5 Database/Performance — PASS**:迁移不新增物理外键;使用单列/组合索引,不在 Tool loop 内引入 N+1;新 Tool Step 删除一次 Workset 查询。 -- **C6 Modularity/Reusability — PASS**:新增 contract/validation/repair 小模块,避免继续扩张已超过建议尺寸的 `tool_step_service.py` 和 `agent_tools.py`。 - -## Delivery Phases - -### Phase A — Stable Step Tool Context and identity - -1. 定义 `StepToolContext`、`ToolWorksetEntry`、`AcceptedToolCall` 的 checkpoint JSON contract。 -2. Model Step 在 Provider 调用前构建 Workset,在接受 Tool Call 时生成稳定 `call_instance_id` 并保留 `provider_call_id`。 -3. Tool Step 校验 context 与 Assistant Turn 一致,只从保存 binding 执行;新 checkpoint 路径禁止调用 ToolProvider。 -4. 旧 checkpoint 进入单次、可观测的 legacy resolver;同一 pending batch 只解析一次。 -5. `AgentToolExecution` 增加 nullable `provider_call_id` 和 `contract_version`;现有 `tool_call_id` 语义收敛为 Call Instance,`id` 继续是 Execution/Receipt ID。 - -### Phase B — Shared validation, authorization and failure feedback - -1. 在 Receipt reservation 前按已接受 schema 统一校验 object/required/type/enum/additional properties。 -2. 将 actor/tenant/resource/credential/approval 检查收敛为不可绕过的 authorization decision。 -3. 所有具备有效 Call Instance 的可修复失败生成一个 call-linked Tool Result,包含稳定 code、bounded summary、model action、side-effect state 和安全 remediation。 -4. Permission/confirmation、pending、cancel、unknown 和 protocol corruption 继续使用独立控制状态。 - -### Phase C — Repair budgets - -1. checkpoint 保存 per-tool repair episode、连续 fingerprint 计数和总计数。 -2. 第 10 次连续相同失败或第 10 次同 Tool episode 失败后暂停,且不发起下一次模型调用;普通 Tool JSON repair、`write_file` JSON repair 和 safe-read replay 也只把现有独立上限改为 10,不在本轮重构计数结构。 -3. Tool 成功、新 Run、用户明确纠正按 contract 重置;Provider retry、safe internal replay、permission/confirmation、pending、cancel、unknown 不计数。 -4. Verifier repair 改为当前 issue episode 计数,保留全局 `model_turn_limit` 独立语义。 - -### Phase D — Deadlines, cancellation and lease hardening - -1. 为 IMAP、DNS、AgentBay read/code 和本地 code 定义 operation-specific deadline 优先级。 -2. 将 durable cancel 传播到支持的进程/网络/SDK;无法 hard-cancel 的调用停止等待并记录能力限制。 -3. 长任务在 ownership 有效时 renew lease;settlement 前执行 fence;deadline/cancel 后不确定写转 unknown/reconcile。 - -### Phase E — RegisteredTool migration boundary - -建立不影响现有工具的 `RegisteredTool` contract,要求模型 schema、handler binding、effect/retry、authorization、recovery、deadline/cancel capability 完整后才进入 Workset。首轮只迁移代表性 builtin、MCP 和 AgentBay read;其余 legacy adapter 保持隐藏或走兼容层。 - -## Project Structure - -### Documentation - -```text -specs/002-tool-runtime-contract/ -├── spec.md -├── plan.md -├── research.md -├── data-model.md -├── quickstart.md -├── contracts/ -│ ├── step-tool-context.md -│ ├── tool-result.md -│ └── repair-and-lifecycle.md -└── tasks.md -``` - -### Source Code - -```text -backend/ -├── app/models/agent_tool_execution.py -├── app/services/agent_runtime/ -│ ├── state.py -│ ├── model_step_service.py -│ ├── tool_step_service.py -│ ├── tool_execution.py -│ ├── tool_contracts.py # new: checkpoint-safe contracts/bindings -│ ├── tool_validation.py # new: accepted-schema validation -│ ├── tool_authorization.py # new: shared decision envelope -│ ├── tool_repair_budget.py # new: episode transitions -│ └── cancel_source.py -├── app/services/agent_tools.py -├── alembic/versions/ -└── tests/ - ├── test_agent_runtime_tool_contracts.py - ├── test_agent_runtime_tool_step_service.py - ├── test_agent_runtime_tool_execution.py - ├── test_agent_runtime_tool_repair_budget.py - └── test_agent_tools_deadlines.py -``` - -**Structure Decision**: 只扩展现有 backend Runtime 边界。checkpoint contract、validation、authorization 和 repair budget 拆为小模块;Receipt persistence 继续由现有 model/service 所有,不增加平行 Runtime。 - -## Migration and Compatibility Strategy - -- Alembic 采用 add-only、nullable staged migration;`provider_call_id` 和 `contract_version` 不参与首期唯一键。 -- 现有 `(run_id, tool_call_id)` 唯一键保留;新代码把 `tool_call_id` 当作 Call Instance,旧行保持合法。 -- 旧 checkpoint 无 `step_tool_context` 时,Tool Step 仅为整个 pending batch 调用一次 legacy resolver,并记录 `legacy_tool_context_resolved`;新 checkpoint 缺 context 直接视为 corruption。 -- mixed-version Worker 期间,新字段写入必须向旧 Reader 兼容;删除 legacy path 需要完整保留周期、回滚窗口和使用量为零。 - -## Verification Strategy - -1. Contract unit tests:序列化、版本、Call identity、schema validation、failure redaction、repair transitions。 -2. Runtime integration tests:Model Step → checkpoint → 新 Worker Tool Step;普通 availability 变化不影响已接受 Call;安全状态变化仍阻断。 -3. Receipt tests:replay 复用同一 execution;lease renewal/loss/fence;unknown write no replay;safe read bounded retry。 -4. Compatibility tests:旧 checkpoint 单次 resolver、新 checkpoint 禁止 resolver、mixed-version nullable fields。 -5. Lifecycle tests:统一上限 10 的 off-by-one、reset/exclusion、operation deadline、cancel propagation。 -6. Static gates:scoped Ruff、pytest、Alembic single head + upgrade/downgrade、`scripts/arch-guard.sh`。 - -## Complexity Tracking - -无 Constitution 违规。长期 Registry 和 deadline/cancel 能力表放在后续 phase,避免首个安全修复同时搬迁全部工具。 diff --git a/specs/002-tool-runtime-contract/quickstart.md b/specs/002-tool-runtime-contract/quickstart.md deleted file mode 100644 index 40c60830f..000000000 --- a/specs/002-tool-runtime-contract/quickstart.md +++ /dev/null @@ -1,175 +0,0 @@ -# Quickstart: Tool Runtime Contract Implementation - -## Checkout - -```bash -cd /Users/zhou/Code/clawith-worktrees/tool-runtime-contract-repair -git branch --show-current -git log -1 --oneline -``` - -Expected branch: `002-tool-runtime-contract`; base contains `upstream/main@251aeba8` or a later explicitly rebased upstream main. - -## Baseline Evidence (2026-08-10) - -- Branch: `002-tool-runtime-contract` -- Base: `251aeba8c36513bcab11b1538ecfd758bdf2cbe4` (`upstream/main`) -- Pre-implementation changes: only SpecKit artifacts and its generated `AGENTS.md` technology context; original checkout changes remain isolated. -- Alembic: one head, `f061_enterprise_info_tenant_id`. -- Architecture guard: passed all P0 checks; repository-wide legacy warnings were present before implementation (direct service selects, physical FKs and oversized files). -- Existing directed coverage includes model/tool step, tool outcome, checkpoint side effects, cancel source, async poll, A2A, command worker and `test_tool_execution.py`. - -## Implementation Order - -1. Add contract and identity tests before production edits. -2. Add checkpoint `StepToolContext` and stable Call Instance creation. -3. Remove ToolProvider access from new-format Tool Step; add legacy batch resolver. -4. Add DB columns/migration and projection metadata. -5. Add shared validation/authorization/failure envelope. -6. Add repair episode state and uniform Tool repair/retry limit 10 gates. -7. Harden operation deadlines/cancel/lease tests. -8. Add RegisteredTool boundary and migrate representative tools only. - -## Scoped Verification - -```bash -cd backend -.venv/bin/python -m pytest \ - tests/test_agent_runtime_model_step_service.py \ - tests/test_agent_runtime_tool_step_service.py \ - tests/test_agent_runtime_tool_execution.py \ - tests/test_agent_runtime_tool_contracts.py \ - tests/test_agent_runtime_tool_repair_budget.py -.venv/bin/ruff check \ - app/models/agent_tool_execution.py \ - app/services/agent_runtime \ - tests/test_agent_runtime_tool_contracts.py \ - tests/test_agent_runtime_tool_repair_budget.py -.venv/bin/alembic heads -``` - -Before completion: - -```bash -cd /Users/zhou/Code/clawith-worktrees/tool-runtime-contract-repair -bash scripts/arch-guard.sh -cd backend -.venv/bin/python -m pytest tests/test_agent_runtime_*.py -.venv/bin/alembic downgrade -1 -.venv/bin/alembic upgrade head -``` - -## Proof Scenarios - -- accepted call survives assignment/enabled/readiness change; -- current actor/resource/credential revocation still blocks before side effect; -- checkpoint restart on another Worker uses the same binding and execution row; -- repeated Provider-local ID in another Assistant Turn does not collide; -- schema failure returns exactly one sanitized Tool Result; -- the 10th repair failure pauses before the next model invocation; -- provider retry, safe replay, pending, cancel and unknown do not increment repair budget; -- lease loss blocks stale settlement; uncertain write is never auto-replayed; -- legacy checkpoint resolves once per pending batch, new checkpoint never uses legacy fallback. - -## Completion Evidence (2026-08-11) - -### Runtime and Tool regression - -```bash -backend/.venv/bin/python -m pytest -q \ - backend/tests/test_agent_runtime_*.py \ - backend/tests/test_tool_execution.py \ - backend/tests/test_builtin_tool_contracts.py \ - backend/tests/test_agent_tools_legacy_contract_compatibility.py \ - backend/tests/test_agent_tools_remaining_typed_outcomes.py \ - backend/tests/test_agent_tools_typed_content_outcomes.py \ - backend/tests/test_agent_tools_deadlines.py \ - backend/tests/test_llm_single_step.py -``` - -Result: `834 passed, 3 warnings`. The warnings are existing Pydantic/Lark -deprecations and no test failed. - -### Static and architecture checks - -```bash -backend/.venv/bin/ruff check --select E9,F63,F7,F82 -backend/.venv/bin/ruff check \ - backend/app/services/agent_runtime/tool_contracts.py \ - backend/app/services/agent_runtime/tool_registry.py \ - backend/app/services/agent_runtime/tool_repair_budget.py \ - backend/app/services/agent_runtime/tool_validation.py \ - backend/tests/test_agent_runtime_tool_contracts.py \ - backend/tests/test_agent_runtime_tool_execution_migration.py \ - backend/tests/test_agent_runtime_tool_repair_budget.py \ - backend/tests/test_agent_runtime_tool_validation.py \ - backend/alembic/versions/v1_11_3_f062_tool_execution_identity.py -bash scripts/arch-guard.sh -git diff --check -``` - -Results: - -- fatal Ruff checks passed across every changed Python scope; -- full Ruff passed for the new contract/registry/repair/validation modules, - their focused tests, and migration; -- Architecture Guard passed all P0 checks; -- `git diff --check` passed; -- repository-existing broad Ruff/style debt and Architecture Guard warnings - remain (import/style findings in legacy large files, direct selects, physical - foreign keys, and oversized files). They are not introduced as part of this - contract repair and were not mass-formatted in this focused branch. - -### Migration verification - -```bash -cd backend -.venv/bin/alembic heads -.venv/bin/python -m pytest -q \ - tests/test_agent_runtime_tool_execution_migration.py \ - tests/test_agent_runtime_tool_contracts.py -.venv/bin/alembic upgrade \ - f061_enterprise_info_tenant_id:f062_tool_execution_identity --sql -.venv/bin/alembic downgrade \ - f062_tool_execution_identity:f061_enterprise_info_tenant_id --sql -``` - -Results: - -- exactly one Alembic head: `f062_tool_execution_identity`; -- migration/contract tests: `9 passed`; -- forward SQL adds nullable `provider_call_id` and `contract_version`; -- reverse SQL drops the two fields in reverse order; -- the local PostgreSQL role cannot create an isolated verification database, - while the existing `clawith` database is behind current main. Therefore no - destructive online upgrade/downgrade was run against user data. Both online - schema-introspection behavior and old-row compatibility are covered by the - migration tests; both directions also pass Alembic's offline migration path. - -### Final consistency checks - -- branch: `002-tool-runtime-contract`; -- base: `upstream/main@251aeba8c36513bcab11b1538ecfd758bdf2cbe4`; -- all SpecKit files under `specs/002-tool-runtime-contract/` exist; -- accepted calls persist `contract_version`; legacy calls use an explicit - `legacy::` contract version and emit - `legacy_tool_context_resolved` compatibility telemetry; -- legacy deletion remains gated by zero observed legacy batches, one complete - supported-release interval, and a closed rollback window; -- the original dirty checkout remains separate from this worktree; -- no commit or push was performed. - -## Remaining Risks - -- Production/provider validation is not part of this local run. Deadline, - cancellation, unknown-write, credential revocation, and Provider Tool payload - behavior are covered by deterministic unit/integration doubles, not live - provider credentials. -- The current Runtime still settles accepted Tool Calls sequentially. The new - provider `parallel_tool_calls` capability only controls whether a Provider may - emit more than one call in a response; it does not authorize concurrent - business execution. `parallel_safe` remains a separate execution-policy fact. -- RegisteredTool migration is intentionally incremental. One builtin read, one - AgentBay read, and exact-name dynamic MCP contracts use the completeness gate; - remaining legacy adapters stay observable and hidden when incomplete until - their contracts are migrated and the deletion gate is satisfied. diff --git a/specs/002-tool-runtime-contract/research.md b/specs/002-tool-runtime-contract/research.md deleted file mode 100644 index d8c1591f2..000000000 --- a/specs/002-tool-runtime-contract/research.md +++ /dev/null @@ -1,89 +0,0 @@ -# Phase 0 Research: Tool Runtime 契约与执行链路 - -## Baseline - -研究基线为 `upstream/main@251aeba8`。个人 fork 的 `origin/main@5aef9da4` 停留在 v1.10.1,不包含 Durable Runtime,因此本功能分支已无损快进到仓库上游主线。 - -当前已具备:LangGraph checkpoint、`AgentToolExecution` Receipt、`started/succeeded/failed/unknown`、safe-read bounded retry、lease owner/fence、async poll、unknown/reconcile、typed `ToolExecutionOutcome`、Provider transport retry 与全局 model turn limit。 - -当前缺口:Tool Step 再次调用 ToolProvider;checkpoint 无 Workset/Contract/Binding;Provider Call ID 直接作为 Runtime `tool_call_id`;参数只验证为 JSON object;failure envelope 和 repair episode 不完整;部分 IMAP/DNS/AgentBay 路径缺 deadline/cancel。 - -## Decisions - -### D1. Workset 在 Model Step 固化,Tool Step 不重建 - -**Decision**: Model Step 构建一次 Workset,并把已接受 Call 所需的最小 Tool Contract/Execution Binding 存入 checkpoint。 - -**Rationale**: `tool_step_service.py` 当前在执行时再次调用 `get_runtime_agent_tools_for_llm`,会让 assignment/enabled/readiness 的普通变化改变已经接受的调用。稳定 binding 可以跨 Worker 恢复,同时保留当前 actor/resource/credential/cancel 安全检查。 - -**Rejected**: 在 Tool Step 再调用 Provider 并比较两次结果。比较仍无法证明旧 endpoint/target,且会把普通 availability 当 hard revoke。 - -### D2. 保留现有 `tool_call_id` 为 Call Instance,另存 Provider ID - -**Decision**: 现有 `tool_call_id` 从 wire identity 收敛为 Clawith Call Instance;新增 nullable `provider_call_id`;`AgentToolExecution.id` 继续是 Execution/Receipt ID。 - -**Rationale**: 当前所有 Receipt、Activity、Chat、A2A 和 async poll 已围绕 `(run_id, tool_call_id)` 稳定工作。替换主键风险大,新增 Provider correlation 可兼容旧数据并允许不同 Assistant Turn 重复 Provider-local ID。 - -**Rejected**: 让 Provider ID 继续承担持久身份。Gemini/兼容 Provider 可能合成、缺失或重复 ID,跨 replay 不稳定。 - -### D3. Binding 保存引用和不可变目标,不保存秘密或可执行代码 - -**Decision**: Binding 保存 tool kind、registry key、handler key、MCP server/tool target、contract version 和 credential reference;执行时再读取当前 credential 并做安全授权。 - -**Rationale**: 既防止 endpoint/name 漂移,也允许 credential rotation/revocation 立即生效,不把秘密写入 checkpoint。 - -**Rejected**: checkpoint 保存解密 credential 或 Python callable。安全风险高且不具备跨版本可恢复性。 - -### D4. schema validation 位于 Receipt reservation 前 - -**Decision**: 使用 accepted schema 做通用结构校验,失败产生一个 Tool Result,但不创建执行 Receipt。 - -**Rationale**: 无效参数没有执行资格,不应消耗 provider attempt;同时必须返回模型可修复的、call-linked 反馈。 - -**Rejected**: 只依赖各 Handler 自行校验。错误格式不一致、授权/副作用前后顺序不可证明。 - -### D5. authorization 是统一 decision envelope,资源级检查可留在 adapter - -**Decision**: 所有工具经过同一 authorization/approval orchestration;只有必须读取真实对象的资源级判定可由 adapter 执行,但必须返回统一结果。 - -**Rationale**: 不强迫所有 provider 使用同一权限实现,同时保证结果语义和 Receipt 前门不可绕过。 - -### D6. 可修复失败返回模型,控制状态不伪装失败 - -**Decision**: 参数、binding 和确定性业务失败生成一个 sanitized Tool Result。Permission/confirmation、pending、cancel、unknown 和 checkpoint corruption 保持独立状态。 - -**Rationale**: 模型需要知道“为什么失败”和“可采取什么动作”,但 unknown write 绝不能诱导自动重试。 - -### D7. Repair budget 是 Tool episode,不是 Provider/Receipt retry - -**Decision**: 连续同 fingerprint 第 10 次、同 Tool episode 第 10 次暂停;只计模型可见、可修复失败。普通 Tool protocol repair、`write_file` protocol repair 和 safe-read replay 继续使用各自现有计数入口,但上限统一为 10,状态结构后续再整体重构。 - -**Rationale**: Provider transport retry 和 Receipt safe replay 都不代表模型做了错误决策;混计会过早停机或掩盖循环。 - -### D8. Deadline、cancel、lease 是三个控制面 - -**Decision**: 每个 operation 有 deadline;Run cancel 尽量传播到底层;lease 只证明 ownership,并通过 renew/fence 保护结算。 - -**Rationale**: lease 过期不等于 Handler timeout,timeout 也不证明外部写未发生。 - -### D9. Registry 渐进迁移 - -**Decision**: 先定义完整 RegisteredTool contract 和代表性 adapter,未完整声明能力的工具不进入新 Workset;不一次性搬迁 `agent_tools.py` 全部 Handler。 - -**Rationale**: 先消除执行漂移和失败盲区,再逐 family 收敛,降低回归面。 - -## Source Evidence - -- `model_step_service.py` 在每个模型轮构建 Runtime Workset并校验名称。 -- `tool_step_service.py` 在执行 pending calls 时再次调用 ToolProvider,是本次最直接的漂移来源。 -- `AgentToolExecution` 已提供 durable receipt、attempt、lease 和 unknown 状态,应该扩展而不是替换。 -- `tool_execution.py` 已提供 exact request comparison、safe-read retry、lease renewal/fence 和 reconciliation。 -- `node_executor.py` 现有 repair 主要按 protocol code 与 verifier 累计,不等于新的 Tool repair episode。 - -## Open Risks Resolved by Tests - -- Provider fallback 使用不同 capability Workset:最终接受 Call 必须绑定实际调用的 fallback Workset。 -- 同一 response 多 Call:每个 Call 独立 identity/binding,batch context 共享 Workset version。 -- Group/legacy hidden tools:只允许明确 compatibility path,不能让新模型轮重新暴露。 -- mixed Worker:nullable DB 字段和 checkpoint version discriminator 保证旧 Reader 不崩溃。 -- write 后断链:任何无法证明 outcome 的路径统一 unknown,不因 retryable 标记自动重放。 diff --git a/specs/002-tool-runtime-contract/spec.md b/specs/002-tool-runtime-contract/spec.md deleted file mode 100644 index 807f362de..000000000 --- a/specs/002-tool-runtime-contract/spec.md +++ /dev/null @@ -1,202 +0,0 @@ -# Feature Specification: Tool Runtime 契约与执行链路修复 - -**Feature Branch**: `002-tool-runtime-contract` -**Created**: 2026-08-10 -**Status**: Draft -**Input**: 修复 Tool Runtime 中的工具集漂移、调用身份冲突、失败反馈缺失、修复次数混用,以及执行时限、取消和 Receipt lease 不协调的问题,并为长期统一 Tool Registry 建立兼容边界。 - -## User Scenarios & Testing *(mandatory)* - -### User Story 1 - 已接受的 Tool Call 稳定执行 (Priority: P1) - -作为使用 Agent 完成任务的用户,我希望模型已经基于本轮可见 Tool 做出的合法调用不会因为执行前工具配置被再次解析而意外失败,从而避免 Agent 在正确决策后仍中断任务。 - -**Why this priority**: 这是 CoAligne 评审确认的主问题。模型可见条件与执行条件不一致会直接降低 Agent 的任务完成率,并产生没有业务价值的额外修复轮次。 - -**Independent Test**: 模型产生一个当轮合法 Tool Call 后,在执行前修改该 Tool 的普通可见性配置;当前 Call 仍按原绑定执行,下一次模型决策则使用更新后的可见工具集合。 - -**Acceptance Scenarios**: - -1. **Given** 模型已收到当前可用 Tool 并返回一个名称合法的调用,**When** 调用进入执行阶段,**Then** 系统使用模型决策时已经确认的 Tool 绑定,不重新计算整套可见工具集合。 -2. **Given** 一个 Tool Call 已被当前模型轮次接受,**When** 管理员随后修改该 Tool 的普通 assignment、enabled 或 readiness 状态,**Then** 当前调用不因该普通可见性变化被拒绝,变化从下一次模型轮次生效。 -3. **Given** 当前 actor 已失去目标资源权限、凭证已撤销、精确 Tool 绑定已删除,或用户取消 Run,**When** 调用准备产生副作用,**Then** 系统按当前安全状态拒绝或取消执行,并给出明确结果,而不是继续执行或重新计算 Workset。 -4. **Given** Run 从 checkpoint 恢复,**When** 继续执行尚未完成的调用,**Then** 系统使用与原模型决策一致的调用绑定,不因恢复到另一 Worker 而改变 Tool 目标。 - ---- - -### User Story 2 - Tool 失败能够驱动模型修正 (Priority: P1) - -作为用户,我希望参数错误、明确业务拒绝或 Tool 绑定失效能够返回给模型,让模型修改参数、改选 Tool 或向我提问,而不是直接终止整个 Run。 - -**Why this priority**: 当前部分失败只保留普通文本,部分执行前失败直接终止 Run。没有统一、可操作的失败反馈,就无法建立可靠的 Agent 自主修复循环。 - -**Independent Test**: 让模型提交一个带有效调用身份但参数不合法的 Tool Call;系统返回一个安全、结构化、与原调用关联的失败;模型修正参数后再次调用并成功完成任务。 - -**Acceptance Scenarios**: - -1. **Given** Tool Call 具有有效身份但参数不符合 Tool 要求,**When** 系统在 Handler 前发现错误,**Then** 模型恰好收到一个与原调用关联的失败结果,包含稳定错误码、可操作摘要和建议动作。 -2. **Given** Tool 或外部服务明确拒绝请求且确认未产生不确定副作用,**When** 系统处理结果,**Then** 模型可以看到经过清洗的拒绝原因并决定修正或改选 Tool。 -3. **Given** 失败信息包含密钥、完整敏感参数、原始异常或大段 Provider 响应,**When** 生成模型反馈,**Then** 敏感内容被删除或脱敏,只保留有界、可操作的信息。 -4. **Given** 外部写操作可能已经发生但结果无法确认,**When** 系统处理该结果,**Then** Run 进入等待协调状态,模型和 Runtime 都不得将其当作普通可重试失败。 -5. **Given** Tool Call 身份缺失、在同一模型响应中重复,或消息交换关系不可能成立,**When** 系统校验调用,**Then** 系统不得伪造调用身份或执行 Handler,并以协议错误结束该路径。 - ---- - -### User Story 3 - 多轮 Tool Call 身份不会碰撞 (Priority: P1) - -作为连续多轮使用 Tool 的用户,我希望不同模型轮次即使收到 Provider 重复使用的局部 Call ID,也不会复用错误的执行记录、覆盖结果或中断 Run。 - -**Why this priority**: 已确认 Gemini 会在每次响应中从 `call_1` 开始编号,而当前耐久执行记录把该值当作 Run 内全局身份。这是一条确定性的多轮失败路径。 - -**Independent Test**: 在同一个 Run 中连续两个模型轮次分别调用不同 Tool,但 Provider 都返回 `call_1`;两个调用独立执行、独立记录,并分别与正确的 Tool Result 配对。 - -**Acceptance Scenarios**: - -1. **Given** 同一 Run 的两个不同 Assistant Turn 都包含 Provider-local `call_1`,**When** 系统执行它们,**Then** 两次调用拥有不同的 Clawith 调用实例和执行记录。 -2. **Given** 同一个 Assistant Tool Call 因 checkpoint replay 再次进入执行,**When** Runtime 恢复,**Then** 它复用原执行记录,不重复产生副作用。 -3. **Given** 同一 Assistant Turn 中存在重复 Call ID,**When** 系统准备执行,**Then** 在任何 Receipt 或副作用产生前拒绝整个非法交换。 -4. **Given** 多轮历史包含相同 Provider-local Call ID,**When** 生成 Tool Result、Activity、Chat、A2A correlation 或下一次 Provider 请求,**Then** 每个结果仍与正确调用实例关联。 - ---- - -### User Story 4 - 修复循环有独立且可理解的预算 (Priority: P2) - -作为用户,我希望 Agent 可以多次修正真正可修复的 Tool 错误,但在持续重复同一错误或围绕同一个 Tool 打转时及时暂停,并允许我纠正后重新开始计数。 - -**Why this priority**: 当前全局模型轮次、Provider retry、Tool replay、Verifier repair 和模型修复容易被混为一种“重试”。独立预算可以兼顾自主完成率、成本和安全。 - -**Independent Test**: 分别制造连续相同错误、同 Tool 不同错误、成功后再次失败、用户纠正后恢复,以及不应计数的 pending/permission/unknown-write 事件,验证每种计数和重置边界。 - -**Acceptance Scenarios**: - -1. **Given** 同一稳定错误已经连续作为模型可见失败出现 9 次,**When** 第 10 次相同失败被记录,**Then** 系统保存该失败并暂停,不能开始第 11 次模型调用。 -2. **Given** 同一个 Tool 在当前 episode 中出现 9 次可计数失败,错误指纹可以变化,**When** 第 10 次失败被记录,**Then** 系统暂停,不能开始下一次模型调用。 -3. **Given** 失败指纹变化但 Tool 相同,**When** 记录新失败,**Then** 连续相同错误计数重新开始,但同 Tool episode 总数保留。 -4. **Given** 被跟踪 Tool 成功、新 Run 开始,或用户明确纠正后恢复,**When** 后续再发生失败,**Then** 按对应规则开启新的 repair episode。 -5. **Given** 事件属于 Provider transport retry、安全内部 replay、permission/confirmation wait、async pending、cancel 或 unknown external write,**When** 系统处理事件,**Then** 不增加模型修复计数。 -6. **Given** 全局 Run 模型轮次已经达到上限,**When** 本地 Tool repair budget 尚未耗尽,**Then** 全局上限仍独立生效并展示不同的停止原因。 -7. **Given** 普通 Tool 或 `write_file` 的 arguments JSON 无效或截断,**When** Runtime 请求模型修复,**Then** 两类 Tool 都分别最多提供 10 次重写机会;safe-read Runtime replay 最多执行同一调用 10 次。本轮只统一上限数值,不重构这些独立计数器。 - ---- - -### User Story 5 - 长时间 Tool 执行可控且可恢复 (Priority: P2) - -作为用户,我希望网络、邮箱、云桌面和代码执行不会无限等待;取消 Run 能尽可能停止正在进行的操作;Worker lease 变化也不会被误认为 Handler 已超时或已取消。 - -**Why this priority**: 执行时限、用户取消和 Receipt lease 是三种不同机制。混用它们会造成无法终止的操作、错误重试或执行完成后无法结算。 - -**Independent Test**: 对选定的网络读取、邮箱读取、云桌面读取和长时间代码执行分别制造超时、取消、lease renewal 和 lease loss,验证底层操作、结果分类和副作用安全。 - -**Acceptance Scenarios**: - -1. **Given** 一个外部读取操作超过该操作允许的最长等待,**When** deadline 到达,**Then** Agent loop 停止等待,并在底层能力支持时终止对应网络、进程或 SDK 操作。 -2. **Given** 用户取消正在执行的 Run,**When** Handler 或 backend 支持取消,**Then** 取消信号传递到底层操作,并停止继续续租。 -3. **Given** 一个合法 Handler 的运行时间超过默认 Receipt lease,**When** 当前 Worker 仍然健康且拥有执行权,**Then** lease 被续期,其他 Worker 不能并发接管同一执行。 -4. **Given** Handler 在可能产生外部写之后发生 deadline、取消或连接中断,**When** 无法证明最终结果,**Then** 状态为 unknown/reconcile,系统不得自动重放。 -5. **Given** Tool 有显式时限、配置默认时限和最大时限,**When** 用户省略或提供时限,**Then** 系统按“显式值优先、否则配置默认值、最后受最大值限制”的规则执行。 - ---- - -### User Story 6 - 旧 Run 可兼容,长期 Tool 注册可渐进迁移 (Priority: P3) - -作为平台维护者,我希望升级后仍能恢复受支持的旧 checkpoint,同时新的 Tool 定义、Handler、授权和恢复能力逐步收敛到同一注册来源,避免再次发生能力发布遗漏。 - -**Why this priority**: 直接删除旧路径会影响运行中的 Run;一次性迁移全部 Tool 又风险过高。需要可观测、可删除的兼容层和分批迁移边界。 - -**Independent Test**: 使用没有新 Tool context 的历史 checkpoint 恢复执行,并验证兼容路径、日志、结果语义和清理条件;同时验证新注册项缺少 Schema、Handler 或安全声明时无法对模型开放。 - -**Acceptance Scenarios**: - -1. **Given** 旧 checkpoint 只有 pending Tool Calls,**When** 新版本恢复它,**Then** 使用明确标识的 legacy compatibility path,并且同一 pending batch 不为每个 Call 重建一次 Tool 环境。 -2. **Given** 旧 checkpoint 中的合法 Call 已无法执行,**When** 统一失败反馈能力启用后,**Then** 模型收到一个 legacy binding unavailable 结果,而不是无原因 terminal。 -3. **Given** 新 checkpoint 的 Tool context 与 pending Call 不匹配,**When** 准备执行,**Then** 在 Receipt 前按 context corruption 拒绝,不能猜测绑定。 -4. **Given** 一个 Tool 注册项缺少模型定义、可执行 Handler 或必要安全属性,**When** 系统准备将其加入 Workset,**Then** 该注册项被拒绝并给出可诊断原因。 -5. **Given** legacy compatibility 使用量在完整保留周期和回滚窗口内持续为零,**When** restore 测试也证明没有依赖,**Then** 兼容路径才可以被删除。 - -### Edge Cases - -- 同一个模型响应中包含多个 Tool Call,其中前一个已成功产生副作用,后一个发生参数、授权或 binding 失败。 -- 模型响应中的 Call ID 为空、重复、长度异常,或 Tool name 不在当轮可见集合。 -- Model Step 完成后进程崩溃,Tool Step 在另一 Worker 上从 checkpoint 恢复。 -- MCP Tool 在 Model Step 后被重命名、删除、迁移到其他 tenant、修改 endpoint,或只轮换 credential。 -- 管理员关闭 Tool,但当前已接受调用仍在等待人工确认;用户随后拒绝、接受或取消。 -- Safe-read 内部 retry 已耗尽,最终只应产生一次模型可见失败和一次 repair 计数。 -- Tool Result 已写入 checkpoint,但节点被重新调度;结果消息和 repair counter 不能重复追加。 -- 同一 Tool 在不同错误之间交替,连续相同错误计数不断重置,但同 Tool episode 最终达到 10。 -- Unknown external write 在重启、重连、用户输入或模型继续推理时仍不得自动重放。 -- Handler 完成时 lease 已丢失;旧 owner 不能覆盖新 owner 或绕过 fence 结算。 -- 底层线程调用无法真正取消;系统必须停止等待并明确记录底层取消能力限制。 -- 旧 Worker 与新 Worker 同时运行时,新的调用实例身份不能提前允许 Provider-local ID 重复。 - -## Requirements *(mandatory)* - -### Functional Requirements - -- **FR-001**: 系统 MUST 在每个模型决策轮次只构建一次模型可见 Tool 集合,并用同一集合完成 Tool name 校验。 -- **FR-002**: 系统 MUST 为每个已接受 Tool Call 保存其所属 Assistant Turn、Provider Call ID、Tool 名称、Tool Contract 版本以及可恢复的执行绑定。 -- **FR-003**: 新格式 checkpoint 的 Tool 执行 MUST 使用已保存绑定,且 MUST NOT 通过重新计算 Agent assignment、enabled、channel 或 readiness 来决定当前 Call 是否可执行。 -- **FR-004**: 普通 Tool availability 变化 MUST 从下一次模型轮次生效;当前已接受 Call 仍 MUST 接受当前 actor、tenant、目标资源、credential 和 cancel 状态检查。 -- **FR-005**: 系统 MUST 为 Provider Call、Clawith 调用实例、执行记录和业务幂等分别维护不会混用的身份。 -- **FR-006**: 同一 Assistant Turn 内 Provider Call ID MUST 唯一;不同 Assistant Turn MAY 使用相同 Provider-local ID,且不得产生内部碰撞。 -- **FR-007**: 同一调用实例的 checkpoint replay MUST 复用原执行记录;不同调用实例 MUST NOT 复用执行结果或副作用记录。 -- **FR-008**: 所有内部结果消息、Activity、Chat、异步操作和 A2A correlation MUST 使用调用实例身份关联;Provider wire output MUST 保留原 Provider Call ID。 -- **FR-009**: 系统 MUST 在 Handler 前验证输入是合法对象,并符合该 Call 已接受的 Tool 参数要求。 -- **FR-010**: Typed builtin、legacy adapter、MCP、A2A、group 和 AgentBay 调用 MUST 经过同一个不可绕过的授权/审批入口后才能预留执行并产生副作用。 -- **FR-011**: 依赖真实资源状态、只能在 Handler 内完成的对象级授权 MUST 返回统一、可分类的 authorization result。 -- **FR-012**: 对具有有效调用身份的可修复失败,系统 MUST 生成恰好一个与原 Call 关联的 Tool Result。 -- **FR-013**: 模型可见失败 MUST 包含执行状态、稳定错误码、有界摘要、模型可采取的动作、副作用确定性,以及可选安全修复提示。 -- **FR-014**: 模型可见失败 MUST 删除或脱敏 secrets、完整敏感参数、stack trace、未清洗异常和无界 Provider payload。 -- **FR-015**: Permission/confirmation、async pending、cancel、unknown external write 和协议损坏 MUST 使用各自独立状态,不得伪装成普通可修复 Tool failure。 -- **FR-016**: Unknown possible write MUST 阻止自动重放,直到通过外部查询、稳定幂等结果或明确人工处理完成协调。 -- **FR-017**: 系统 MUST 在第 10 次连续相同且模型可见的可修复失败后暂停,并且 MUST NOT 启动第 11 次模型调用。 -- **FR-018**: 系统 MUST 在同一个 Tool repair episode 的第 10 次可计数失败后暂停,并且 MUST NOT 启动下一次模型调用。 -- **FR-019**: 不同错误指纹 MUST 只重置连续相同错误计数,不得清除同 Tool episode 总数。 -- **FR-020**: 对应 Tool 成功、新 Run 或用户明确纠正后恢复 MUST 按定义重置 repair episode;无关 Tool 成功不得清除其他 Tool 的失败 episode。 -- **FR-021**: Provider transport retry、安全内部 replay、permission/confirmation wait、async pending、cancel 和 unknown external write MUST NOT 增加模型修复计数。普通 Tool protocol repair、`write_file` protocol repair 和 safe-read replay 保留独立计数结构,但各自上限 MUST 统一为 10;计数结构重构不属于本轮改动。 -- **FR-022**: 全局模型轮次上限 MUST 与 Tool repair budget、Provider retry、Command retry 和 Verifier repair 保持独立,并报告不同停止原因。 -- **FR-023**: Verifier repair MUST 按当前问题 episode 计数;历史已结束问题不得耗尽新的 verifier episode。 -- **FR-024**: 外部 I/O 和长时间操作 MUST 具有与具体操作匹配的最长等待规则;系统 MUST NOT 用单一固定秒数替代所有 Tool 的时限。 -- **FR-025**: 用户取消 MUST 尽可能传播到底层进程、网络或 SDK 操作;不支持强制取消时 MUST 明确记录该限制。 -- **FR-026**: 长时间执行 MUST 在仍拥有执行权时维护 Receipt ownership;lease 到期 MUST NOT 被解释为 Handler 已超时或已取消。 -- **FR-027**: 发生 deadline、cancel 或连接中断后,只要外部写结果无法证明,系统 MUST 将结果标记为 unknown/reconcile。 -- **FR-028**: Tool 显式时限、配置默认时限和最大时限 MUST 遵循一致且可验证的优先级。 -- **FR-029**: 旧 checkpoint MUST 通过明确、可观测且有删除条件的 compatibility path 恢复;新 checkpoint MUST NOT 使用该路径。 -- **FR-030**: 系统 MUST 记录 Workset/Tool Contract 版本、legacy fallback、失败处置、repair counter transition、执行时长、deadline、cancel 和 lease 事件,且不得记录原始秘密。 -- **FR-031**: 长期 Tool 注册来源 MUST 能够把模型定义、Handler、副作用、retry、authorization、recovery 和执行控制能力关联到同一稳定身份。 -- **FR-032**: 未完整迁移的 Tool MUST 保持隐藏;系统 MUST NOT 仅因为存在 legacy Handler 就将其暴露给模型。 - -### Key Entities - -- **Tool Workset**: 某一模型决策轮次真正允许模型看到和选择的 Tool 集合,包含每个 Tool 的模型定义、稳定绑定引用、版本和可用性决定。 -- **Step Tool Context**: 随 checkpoint 保存的本轮 Tool 上下文,关联 Assistant Turn、Workset 版本、已接受 Call、Tool Contract 和执行绑定。 -- **Provider Call Identity**: Provider 在单个模型响应内提供的 Tool Call 标识,只用于协议配对。 -- **Call Instance**: Clawith 对一次具体 Assistant Tool Call 建立的 Run 内稳定身份,用于跨 checkpoint、消息、Activity、异步操作和 A2A 关联。 -- **Tool Execution Receipt**: 一次调用实例的耐久执行记录,保存执行状态、ownership、尝试次数、副作用分类、结果和协调信息。 -- **Tool Result**: 返回给模型和 Runtime 的标准化执行结果,包含成功、失败、pending 或 unknown 状态及安全反馈。 -- **Repair Episode**: 某个 Tool 或 Verifier 问题的一段连续修复过程,拥有独立计数、错误指纹和重置边界。 -- **Execution Binding**: Model Step 已接受的具体 Tool 执行目标,不包含可执行代码或解密凭证,但足以在恢复时解析相同 Handler/Provider target。 - -## Success Criteria *(mandatory)* - -### Measurable Outcomes - -- **SC-001**: 在覆盖普通 availability 变化、checkpoint restart 和跨 Worker 恢复的测试矩阵中,100% 已接受 Tool Call 使用原模型轮次绑定执行;新格式 Tool Step 的 Workset 二次解析次数为 0。 -- **SC-002**: 在所有受支持 Provider 的多轮 Tool 场景中,重复的 Provider-local Call ID 产生 0 次执行记录、Tool Result、Activity、Chat 或 A2A correlation 碰撞。 -- **SC-003**: 同一调用实例在至少一次 checkpoint replay 后仍只产生一条有效执行记录;未知外部写的自动重放次数为 0。 -- **SC-004**: 100% 带有效身份的可修复参数、binding 和明确业务失败产生恰好一个模型可见 Tool Result;敏感信息泄漏测试通过率为 100%。 -- **SC-005**: 第 10 次连续相同错误和第 10 次同 Tool episode 失败均在规定边界暂停;普通 Tool JSON repair、`write_file` JSON repair 和 safe-read replay 的独立上限均为 10;所有 off-by-one、reset 和 exclusion 测试通过率为 100%。 -- **SC-006**: Permission、confirmation、pending、cancel、unknown write、Provider retry 和全局模型轮次上限均显示独立原因,测试中不存在跨预算误计数。 -- **SC-007**: 所有列入范围的 IMAP、DNS、AgentBay read 和代码执行路径在配置的最长等待内返回结果或明确状态,不产生无限等待测试用例。 -- **SC-008**: 长时间 Handler 的 lease renewal、lease loss 和 cancel 测试均不会产生并发双执行或旧 owner 越权结算。 -- **SC-009**: 所有受支持旧 checkpoint 可以通过 compatibility fixture 恢复;新 checkpoint 使用 legacy fallback 的次数为 0。 -- **SC-010**: Tool Runtime 相关回归测试、静态检查和涉及的前端构建全部通过,且原有 unknown-write、Receipt replay 和 Provider Receipt 安全断言没有被弱化。 - -## Assumptions - -- 现有耐久 Tool Receipt、safe-read bounded retry、pending 和 unknown/reconcile 机制继续作为执行安全基础,不在本功能中删除。 -- 普通 assignment、enabled 和 readiness 是模型侧可用性,不被当作已接受 Call 的紧急撤销信号。 -- 立即停止当前调用依赖耐久 Run cancel,或撤销底层 actor、资源或 credential 权限;通用 Tool hard-revoke 数据模型不属于本功能。 -- 完整 Provider Schema capability matrix、默认 Tool 集合收窄、通用 Tool Search 和通用并行执行不属于本功能。 -- 未迁移的 AgentBay Action 继续保持隐藏,后续按 Tool family 分批迁移。 -- 旧 checkpoint 兼容路径只在有观测证据证明不再使用后删除。 -- 用户已经确定所有 Tool 相关 repair/retry 上限统一为 10,并保留独立的计数结构与全局 Run 模型轮次上限;计数结构后续统一重构。 diff --git a/specs/002-tool-runtime-contract/tasks.md b/specs/002-tool-runtime-contract/tasks.md deleted file mode 100644 index 965a3ab40..000000000 --- a/specs/002-tool-runtime-contract/tasks.md +++ /dev/null @@ -1,249 +0,0 @@ -# Tasks: Tool Runtime 契约与执行链路修复 - -**Input**: Design documents from `/specs/002-tool-runtime-contract/` -**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/, quickstart.md -**Tests**: 规范明确要求 Runtime contract、replay、repair budget、deadline/cancel/lease 与兼容测试;各故事按 test-first 执行。 - -## Phase 1: Setup - -**Purpose**: 固定基线、测试入口和迁移拓扑。 - -- [x] T001 记录 `upstream/main` 基线、branch 和 dirty state 到 `specs/002-tool-runtime-contract/quickstart.md` -- [x] T002 [P] 核对并记录现有 Tool Runtime 定向测试清单到 `specs/002-tool-runtime-contract/quickstart.md` -- [x] T003 [P] 运行 Alembic single-head 与 `scripts/arch-guard.sh` 基线检查并记录结果到 `specs/002-tool-runtime-contract/quickstart.md` - ---- - -## Phase 2: Foundational Contracts - -**Purpose**: 建立所有故事共享的 checkpoint-safe contract 与兼容边界。 - -**⚠️ CRITICAL**: 本阶段完成前不修改 Model/Tool 执行主链。 - -- [x] T004 [P] 为 `StepToolContext`、`ToolWorksetEntry`、`AcceptedToolCall` 编写失败态 contract tests 于 `backend/tests/test_agent_runtime_tool_contracts.py` -- [x] T005 [P] 为三层身份与 legacy JSON compatibility 编写失败态 tests 于 `backend/tests/test_agent_runtime_tool_contracts.py` -- [x] T006 实现 versioned、bounded、secret-free Tool contracts 于 `backend/app/services/agent_runtime/tool_contracts.py` -- [x] T007 将 `step_tool_context` 与 `tool_repair_episodes` 类型接入 `backend/app/services/agent_runtime/state.py` -- [x] T008 在 `backend/tests/test_agent_runtime_contracts.py` 增加 checkpoint 序列化/旧 state 兼容测试 - -**Checkpoint**: Contract 可独立序列化,旧 checkpoint 仍可读取。 - ---- - -## Phase 3: User Story 1 — 已接受的 Tool Call 稳定执行 (Priority: P1) 🎯 MVP - -**Goal**: Model Step 固化 Workset/Binding;新 Tool Step 不再重建 Workset。 - -**Independent Test**: 接受 Tool Call 后修改 assignment/enabled/readiness 并换 Worker 恢复,当前 Call 仍使用原 binding;actor/resource/credential/cancel 变化仍阻断。 - -### Tests - -- [x] T009 [P] [US1] 在 `backend/tests/test_agent_runtime_model_step_service.py` 增加实际 primary/fallback Workset 固化与 stable Call Instance 测试 -- [x] T010 [P] [US1] 在 `backend/tests/test_agent_runtime_tool_step_service.py` 增加新 checkpoint ToolProvider 调用为 0、availability 漂移不影响当前 Call 测试 -- [x] T011 [P] [US1] 在 `backend/tests/test_agent_runtime_tool_step_service.py` 增加 context mismatch/corruption 在 Receipt 前失败测试 -- [x] T012 [P] [US1] 在 `backend/tests/test_agent_runtime_tool_step_service.py` 增加 actor/resource/credential/cancel 仍按当前状态阻断测试 - -### Implementation - -- [x] T013 [US1] 在 `backend/app/services/agent_runtime/model_step_service.py` 生成实际 Provider Workset 的 `StepToolContext` 与稳定 Call Instance -- [x] T014 [US1] 在 `backend/app/services/agent_runtime/node_executor.py` 原子写入 assistant message、pending calls 与 `step_tool_context` -- [x] T015 [US1] 在 `backend/app/services/agent_runtime/tool_step_service.py` 从 context 解析 accepted schema/policy/binding 并移除新格式 ToolProvider 查询 -- [x] T016 [US1] 在 `backend/app/services/agent_runtime/tool_step_service.py` 实现整批一次的 legacy resolver 与可观测 compatibility marker -- [x] T017 [US1] 在 `backend/app/services/agent_runtime/tool_step_service.py` 保留当前安全 authorization/cancel gate 并阻止 context corruption fallback - -**Checkpoint**: US1 测试独立通过,SC-001 达成。 - ---- - -## Phase 4: User Story 2 — Tool 失败能够驱动模型修正 (Priority: P1) - -**Goal**: 在 Receipt 前统一 schema validation;有效 Call 的可修复失败产生一个 sanitized Tool Result。 - -**Independent Test**: 缺 required 字段、类型错误、binding unavailable 和确定性业务拒绝分别返回 call-linked failure envelope;secret/stack/provider body 不可见。 - -### Tests - -- [x] T018 [P] [US2] 在 `backend/tests/test_agent_runtime_tool_validation.py` 增加 object/required/type/enum/additionalProperties contract tests -- [x] T019 [P] [US2] 在 `backend/tests/test_agent_runtime_tool_step_service.py` 增加 schema failure 不创建 Receipt且恰好一个 Tool Result 测试 -- [x] T020 [P] [US2] 在 `backend/tests/test_agent_runtime_tool_outcome_contract.py` 增加 model_action/side_effect_state/remediation 与 redaction 测试 -- [x] T021 [P] [US2] 在 `backend/tests/test_agent_runtime_tool_step_service.py` 增加 permission/confirmation/pending/cancel/unknown 不伪装普通失败测试 - -### Implementation - -- [x] T022 [US2] 实现 accepted-schema validator 于 `backend/app/services/agent_runtime/tool_validation.py` -- [x] T023 [US2] 扩展 bounded `ToolExecutionOutcome`/Tool Result envelope 于 `backend/app/services/agent_runtime/tool_execution.py` -- [x] T024 [US2] 在 `backend/app/services/agent_runtime/tool_step_service.py` 接入 Receipt 前 validation 和 exactly-once failure message -- [x] T025 [US2] 在 `backend/app/services/agent_runtime/checkpoint_side_effects.py` 投影 execution/call/provider identity 与 sanitized failure metadata - -**Checkpoint**: US2 测试独立通过,SC-004 达成。 - ---- - -## Phase 5: User Story 3 — 调用身份与 Receipt 不冲突 (Priority: P1) - -**Goal**: Provider Call、Call Instance、Execution Receipt 和业务幂等身份职责分离。 - -**Independent Test**: 不同 Assistant Turn 使用相同 Provider-local ID 时,产生两个 Call Instance/Receipt;同一 Call replay 只复用原 execution。 - -### Tests - -- [x] T026 [P] [US3] 在 `backend/tests/test_agent_runtime_tool_execution.py` 增加 provider ID 重复与 Call Instance 唯一性测试 -- [x] T027 [P] [US3] 在 `backend/tests/test_agent_runtime_tool_step_service.py` 增加 checkpoint replay 复用 execution 且不重复副作用测试 -- [x] T028 [P] [US3] 在 `backend/tests/test_agent_runtime_checkpoint_side_effects.py` 增加 Activity/Chat/A2A identity projection 测试 -- [x] T029 [P] [US3] 在 `backend/tests/test_agent_runtime_tool_execution_migration.py` 增加 nullable 新字段 upgrade/downgrade 与旧行兼容测试 - -### Implementation - -- [x] T030 [US3] 扩展 `provider_call_id` 与 `contract_version` 字段于 `backend/app/models/agent_tool_execution.py` -- [x] T031 [US3] 创建 single-head、DDL-only、可回滚 migration 于 `backend/alembic/versions/` -- [x] T032 [US3] 在 `backend/app/services/agent_runtime/tool_execution.py` reservation/exact request/outcome 中保存并校验 Provider correlation 和 contract version -- [x] T033 [US3] 在 `backend/app/services/agent_runtime/model_step_service.py`、`backend/app/services/agent_runtime/tool_step_service.py` 保留 Provider wire pairing 并内部使用 Call Instance -- [x] T034 [US3] 在 `backend/app/services/agent_runtime/async_tool_poll.py`、`backend/app/services/agent_runtime/a2a_runtime.py` 和 `backend/app/services/agent_runtime/checkpoint_side_effects.py` 透传三层 identity - -**Checkpoint**: US3 测试独立通过,SC-002/SC-003 达成。 - ---- - -## Phase 6: User Story 4 — 修复次数按问题边界计算 (Priority: P2) - -**Goal**: 实现连续同错 10、同 Tool episode 10,并将现有独立 Tool repair/retry 上限统一为 10;本轮不重构计数结构。 - -**Independent Test**: 统一上限 10 的边界、fingerprint 变化、Tool success、新 Run、用户纠正、无关 Tool success及所有 exclusion 均按 contract 转移。 - -### Tests - -- [x] T035 [P] [US4] 在 `backend/tests/test_agent_runtime_tool_repair_budget.py` 增加统一上限 10 的 off-by-one 与 fingerprint 测试 -- [x] T036 [P] [US4] 在 `backend/tests/test_agent_runtime_tool_repair_budget.py` 增加 success/new Run/user correction/reset scope 测试 -- [x] T037 [P] [US4] 在 `backend/tests/test_agent_runtime_tool_repair_budget.py` 增加 Provider retry/safe replay/approval/pending/cancel/unknown exclusion 测试 -- [x] T038 [P] [US4] 在 `backend/tests/test_agent_runtime_node_executor.py` 增加暂停发生在下一次 Model 调用之前的集成测试 -- [x] T039 [P] [US4] 在 `backend/tests/test_agent_runtime_node_executor.py` 增加 Verifier issue episode 与全局 model turn limit 独立测试 - -### Implementation - -- [x] T040 [US4] 实现纯函数 repair episode transitions 于 `backend/app/services/agent_runtime/tool_repair_budget.py` -- [x] T041 [US4] 在 `backend/app/services/agent_runtime/node_executor.py` 对 Tool Result 应用 episode、暂停与 stop reason -- [x] T042 [US4] 在 `backend/app/services/agent_runtime/node_executor.py` 将 verifier aggregate count 迁为 issue fingerprint episode -- [x] T043 [US4] 在 `backend/app/services/agent_runtime/model_step_service.py` 标识 explicit user correction reset 边界并记录 telemetry - -**Checkpoint**: US4 测试独立通过,SC-005/SC-006 达成。 - ---- - -## Phase 7: User Story 5 — 长时间 Tool 可控且不会被错误重放 (Priority: P2) - -**Goal**: operation deadline、durable cancel 和 Receipt lease 独立且可验证。 - -**Independent Test**: IMAP、DNS、AgentBay read/code、本地 code 在策略时限内结束;取消尽可能传播;lease loss 阻止旧 owner;不确定写不重放。 - -### Tests - -- [x] T044 [P] [US5] 在 `backend/tests/test_agent_tools_deadlines.py` 增加 IMAP/DNS/AgentBay read/code deadline 优先级测试 -- [x] T045 [P] [US5] 在 `backend/tests/test_agent_runtime_cancel_source.py` 增加 cancel token 传播与不支持 hard-cancel telemetry 测试 -- [x] T046 [P] [US5] 在 `backend/tests/test_agent_runtime_tool_execution.py` 增加 lease renew/loss/fence 和 stale settlement 测试 -- [x] T047 [P] [US5] 在 `backend/tests/test_agent_runtime_tool_outcome_contract.py` 增加 deadline/cancel 后 possible write → unknown/no replay 测试 - -### Implementation - -- [x] T048 [US5] 定义 deadline/cancel capability policy 于 `backend/app/services/agent_runtime/tool_contracts.py` -- [x] T049 [US5] 在 `backend/app/services/agent_tools.py` 为 IMAP、DNS、本地 code 接入 bounded deadline 和正确 outcome classification -- [x] T050 [US5] 在 `backend/app/services/agentbay_client.py` 与 `backend/app/services/agent_tools.py` 实际执行 AgentBay read/code timeout -- [x] T051 [US5] 在 `backend/app/services/agent_runtime/tool_step_service.py` 运行长任务 lease renewal 并在 settlement 前 fence -- [x] T052 [US5] 在 `backend/app/services/agent_runtime/cancel_source.py` 和 Tool adapter 接口传播 cancel token/capability telemetry - -**Checkpoint**: US5 测试独立通过,SC-007/SC-008 达成。 - ---- - -## Phase 8: User Story 6 — 旧 Run 兼容与长期 Tool 注册迁移 (Priority: P3) - -**Goal**: 旧 checkpoint 可观测恢复;新 RegisteredTool 不完整时不可暴露。 - -**Independent Test**: legacy fixtures 恢复且每 batch 只解析一次;新 checkpoint 永不 fallback;不完整 Registry entry 被拒绝;代表性 builtin/MCP/AgentBay read 可执行。 - -### Tests - -- [x] T053 [P] [US6] 在 `backend/tests/test_agent_runtime_tool_step_service.py` 增加 legacy batch resolver、telemetry 与 deletion gate fixtures -- [x] T054 [P] [US6] 在 `backend/tests/test_builtin_tool_contracts.py` 增加 RegisteredTool completeness/hidden-by-default tests -- [x] T055 [P] [US6] 在 `backend/tests/test_agent_tools_legacy_contract_compatibility.py` 增加代表性 builtin/MCP/AgentBay adapter tests - -### Implementation - -- [x] T056 [US6] 定义 `RegisteredTool` completeness gate 与 lookup 于 `backend/app/services/agent_runtime/tool_registry.py` -- [x] T057 [US6] 将一个 builtin、一个 MCP 和一个 AgentBay read 注册到 `backend/app/services/agent_runtime/tool_registry.py` -- [x] T058 [US6] 在 `backend/app/services/agent_tools.py` 保留明确 legacy adapter 并隐藏不完整注册项 -- [x] T059 [US6] 在 `backend/app/services/agent_runtime/tool_step_service.py` 增加 legacy usage metric/log 和删除条件 - -**Checkpoint**: US6 测试独立通过,SC-009 达成。 - ---- - -## Phase 9: Polish & Cross-Cutting Verification - -- [x] T060 [P] 修复 `set_trigger` validation、`read_document` truncation 和 invisible Tool description 漂移于 `backend/app/services/agent_tools.py` 及对应 tests -- [x] T061 [P] 将 provider `parallel_tool_calls` capability 与业务并行执行能力分离于 `backend/app/services/llm/` 及对应 tests -- [x] T062 运行并修复 scoped Ruff 与 Tool Runtime pytest,命令记录到 `specs/002-tool-runtime-contract/quickstart.md` -- [x] T063 运行 Alembic heads、upgrade/downgrade 和 migration tests,结果记录到 `specs/002-tool-runtime-contract/quickstart.md` -- [x] T064 运行 `scripts/arch-guard.sh`、全量 `backend/tests/test_agent_runtime_*.py` 并记录剩余风险到 `specs/002-tool-runtime-contract/quickstart.md` -- [x] T065 核对所有 docs path、contract/version、legacy deletion gate 与 `git diff --check`,更新 `specs/002-tool-runtime-contract/quickstart.md` - ---- - -## Dependencies & Execution Order - -### Phase Dependencies - -- Setup → Foundational Contracts → US1。 -- US2 依赖 US1 的 accepted schema/binding。 -- US3 依赖 US1 的 stable Call Instance,但其 DB migration/tests 可与 US2 tests 并行。 -- US4 依赖 US2 的标准 failure envelope。 -- US5 依赖 US1/US3 的 binding/Receipt identity,不依赖 US4。 -- US6 依赖 US1 的 compatibility boundary,代表性 Registry 可在 US4/US5 后独立完成。 -- Polish 依赖所选故事完成。 - -### User Story Completion Order - -```text -US1 stable context -├── US2 validation/failure ──> US4 repair budget -├── US3 identity ────────────> US5 lifecycle hardening -└── US6 registry compatibility -``` - -### Parallel Opportunities - -- 每个故事的 `[P]` tests 可在不同文件并行准备,但必须先失败再实现。 -- US2 outcome contract tests 与 US3 migration tests 可并行。 -- US4 pure transition module与 US5 operation-specific handler tests 可在 US3 完成后并行。 -- T060/T061 彼此独立,最后统一回归。 - -## Parallel Example: User Story 1 - -```text -T009 Model Step context tests -T010 ToolProvider-zero and availability drift tests -T011 corruption tests -T012 live safety revocation tests -``` - -## Implementation Strategy - -### MVP First - -1. 完成 T001–T008 固定 contract。 -2. 完成 T009–T017,交付稳定 Workset/Binding 的 US1。 -3. 独立验证 SC-001 后再进入 failure/identity。 - -### Incremental Delivery - -1. US1 消除已接受 Call 的 Workset 漂移。 -2. US2/US3 补齐模型可修复反馈和身份兼容。 -3. US4 落地统一上限 10 的修复次数,保留现有独立计数结构。 -4. US5 加固长任务生命周期。 -5. US6 建立长期 Registry 迁移边界。 - -## Notes - -- 所有 Runtime 行为改动先写失败测试,再修改生产代码。 -- `Tool Step ToolProvider calls = 0` 只适用于新 checkpoint;legacy batch 允许恰好一次。 -- 不把 Receipt lease、operation deadline 或 durable cancel 合并成单一 timeout。 -- 不把长期 Registry 扩展为一次性迁移全部工具。 diff --git a/specs/003-feishu-group-proactive/checklists/requirements.md b/specs/003-feishu-group-proactive/checklists/requirements.md deleted file mode 100644 index 352b0ffdf..000000000 --- a/specs/003-feishu-group-proactive/checklists/requirements.md +++ /dev/null @@ -1,37 +0,0 @@ -# Specification Quality Checklist: 飞书群主动消息 - -**Purpose**: Validate specification completeness and quality before proceeding to planning -**Created**: 2026-08-18 -**Feature**: [spec.md](../spec.md) - -## Content Quality - -- [x] No implementation details (languages, frameworks, APIs) -- [x] Focused on user value and business needs -- [x] Written for non-technical stakeholders -- [x] All mandatory sections completed - -## Requirement Completeness - -- [x] No [NEEDS CLARIFICATION] markers remain -- [x] Requirements are testable and unambiguous -- [x] Success criteria are measurable -- [x] Success criteria are technology-agnostic (no implementation details) -- [x] All acceptance scenarios are defined -- [x] Edge cases are identified -- [x] Scope is clearly bounded -- [x] Dependencies and assumptions identified - -## Feature Readiness - -- [x] All functional requirements have clear acceptance criteria -- [x] User scenarios cover primary flows -- [x] Feature meets measurable outcomes defined in Success Criteria -- [x] No implementation details leak into specification - -## Notes - -- Validation iteration 1 passed all checklist items. -- Validation iteration 2 incorporated the unified recipient contract: no new Feishu group Tool, one external-channel Tool for human and group recipients, and backward compatibility for existing human calls. -- Validation iteration 3 narrowed delivery to Feishu only; other external providers remain behaviorally unchanged. -- The repository SDD workflow requires explicit user confirmation of this specification before design and planning begin. diff --git a/specs/003-feishu-group-proactive/contracts/automation-delivery.md b/specs/003-feishu-group-proactive/contracts/automation-delivery.md deleted file mode 100644 index a56eadec1..000000000 --- a/specs/003-feishu-group-proactive/contracts/automation-delivery.md +++ /dev/null @@ -1,22 +0,0 @@ -# Contract: Schedule and Trigger Feishu Group Delivery - -## Schedule CRUD - -请求/响应新增可选字段: - -```json -{ "delivery_target_id": "" } -``` - -创建或更新非空目标时必须验证当前用户可管理 Agent,且目标属于该 Agent/租户的有效飞书群 Session。 - -## Trigger API and Tools - -`set_trigger`、`update_trigger` 以及 Trigger 管理 API 增加相同的可选 `delivery_target_id`。旧 config 内容与旧 Trigger 行为保持不变。 - -## Runtime registration - -- 空目标:保持当前行为。 -- 有效目标:注册 Run 时设置 `delivery_status=pending` 并冻结飞书群 `delivery_target`。 -- 无效目标:不注册会产生错误投递的 Run;记录明确 intake/执行失败原因。 -- 相同 Schedule occurrence 或 TriggerExecution 重试:复用现有 source execution/idempotency identity,不产生第二条 terminal delivery。 diff --git a/specs/003-feishu-group-proactive/contracts/directory-and-tool.md b/specs/003-feishu-group-proactive/contracts/directory-and-tool.md deleted file mode 100644 index 05aea793e..000000000 --- a/specs/003-feishu-group-proactive/contracts/directory-and-tool.md +++ /dev/null @@ -1,52 +0,0 @@ -# Contract: Directory and send_channel_message - -## query_directory - -新增过滤值:`member_type = group`。`all` 可包含群。 - -飞书群条目: - -```json -{ - "member_type": "group", - "target_recipient_id": "", - "display_name": "项目交付群", - "provider": { "provider_type": "feishu" }, - "can_contact": true, - "contact_tools": ["send_channel_message"], - "unavailable_reason": null -} -``` - -禁止返回 `chat_id`、`external_conv_id` 或凭证。 - -## send_channel_message - -兼容参数: - -```json -{ - "target_member_id": "", - "message": "内容", - "channel": "feishu" -} -``` - -新增飞书群参数: - -```json -{ - "target_recipient_id": "", - "message": "内容", - "channel": "feishu" -} -``` - -规则: - -- `message` 必填。 -- 人类旧路径仍要求 `target_member_id`。 -- 群路径要求 `target_recipient_id`,且只支持 `channel=feishu` 或由目标自动推导为飞书。 -- 两种目标字段同时出现时拒绝,避免歧义。 -- 群目标找不到、越权、失效或类型不符时,外部调用前返回 typed failure。 -- Provider 明确成功/失败/未知使用现有 ToolExecutionOutcome 分类。 diff --git a/specs/003-feishu-group-proactive/data-model.md b/specs/003-feishu-group-proactive/data-model.md deleted file mode 100644 index 9743c1e33..000000000 --- a/specs/003-feishu-group-proactive/data-model.md +++ /dev/null @@ -1,59 +0,0 @@ -# Data Model: 飞书群主动消息 - -## ChatSession(复用) - -飞书群目标必须满足: - -- `tenant_id` 等于当前 Agent 的 tenant -- `agent_id` 等于当前 Agent -- `session_type = group` -- `is_group = true` -- `source_channel = feishu` -- `external_conv_id` 符合已存在的飞书群会话格式 -- `deleted_at IS NULL` - -目录稳定标识:`target_recipient_id = ChatSession.id`。展示名称优先 `group_name`,再退到 `title`;永不向模型返回 Provider `chat_id`。 - -## AgentSchedule(扩展) - -新增可空字段: - -- `delivery_target_id: UUID | null`:指向当前 Agent 的有效飞书群 ChatSession UUID;不建立物理外键。 - -验证:创建、更新、手动触发和自动 tick 注册 Run 时均校验范围;空值保持原有 `delivery_status=not_required`。 - -## AgentTrigger(扩展) - -新增可空字段: - -- `delivery_target_id: UUID | null`:语义与 Schedule 相同;不建立物理外键。 - -验证:Tool/API 设置时校验,TriggerExecution 注册 Run 时再次校验。旧 Trigger 为 null,不改变原有 origin direct delivery逻辑;显式群目标优先于旧 origin direct 目标。 - -## AgentRun.delivery_target(复用) - -冻结后的飞书群目标: - -```json -{ - "kind": "session", - "session_id": "", - "channel_delivery": { - "version": 1, - "channel": "feishu", - "target": { - "receive_id": "", - "receive_id_type": "chat_id" - } - } -} -``` - -该结构已由现有 external group terminal delivery 和 ChannelDelivery 使用;新功能只负责安全构造,不新增状态字段。 - -## State Rules - -- Session 可用 → 可见、可选、可解析。 -- Session 删除/错租户/错 Agent/非飞书/非群 → 不可见且解析失败。 -- Run 注册成功后目标冻结;群改名不影响该 Run。 -- Provider 成功/失败/未知沿用 ChannelDelivery 既有状态;未知不得自动重放。 diff --git a/specs/003-feishu-group-proactive/plan.md b/specs/003-feishu-group-proactive/plan.md deleted file mode 100644 index c7c439582..000000000 --- a/specs/003-feishu-group-proactive/plan.md +++ /dev/null @@ -1,93 +0,0 @@ -# Implementation Plan: 飞书群主动消息 - -**Branch**: `003-feishu-group-proactive` | **Date**: 2026-08-18 | **Spec**: [spec.md](spec.md) -**Input**: 浏览器 Direct Chat、定时任务和后台 Trigger 均可通过现有 `send_channel_message` 主动向已登记飞书群发送消息;不新增 Tool,不扩展其他 Provider。 - -## Summary - -复用现有飞书群 `ChatSession` 作为稳定群目标,以 Session UUID 作为模型可见的 `target_recipient_id`,不暴露 `chat_id`。`query_directory` 增加只属于当前 Agent/租户的飞书群条目;`send_channel_message` 在保留 `target_member_id` 的同时解析飞书群目标并复用 Durable Runtime 的 ChannelDelivery/Provider 发送链路。Schedule 与 Trigger 保存可选目标 Session UUID,注册 Run 时解析、校验并冻结现有 `delivery_target`,从而复用已存在的幂等终态投递,不创建第二套执行或发送状态机。 - -## Technical Context - -**Language/Version**: Python 3.12 deployment baseline(package metadata >=3.11);React 19 / strict TypeScript -**Primary Dependencies**: FastAPI、SQLAlchemy async ORM、Pydantic、LangGraph Runtime、React Query、Vite;不新增依赖 -**Storage**: PostgreSQL 15;复用 `chat_sessions` 与 `channel_deliveries`,为 Schedule/Trigger 增加可选目标 Session UUID -**Testing**: pytest、Ruff、前端 Vitest/build、Architecture Guard、3010 真实飞书群验证 -**Target Platform**: Docker Compose Linux deployment,目标环境 3010 -**Project Type**: FastAPI + React monorepo web application -**Performance Goals**: 目录查询保持分页且无 N+1;自动化完成后正常条件下 60 秒内投递 -**Constraints**: 只做飞书;不新增 Tool;不暴露 Provider `chat_id`;租户/Agent 双重范围;未知结果不重放;保留现有各渠道 `target_member_id` 行为 -**Scale/Scope**: 一个 Agent 的既有飞书群会话目录;文本消息;浏览器、Schedule、Trigger 三种入口 - -## Constitution Check - -*GATE: Phase 0 前通过;Phase 1 设计后再次通过。* - -- **Evidence Before Claims — PASS**: 当前 `send_channel_message` 只解析人;飞书 Provider 已支持 `chat_id`;飞书入站已持久化 Agent 归属的群 Session;Trigger/Schedule 当前未冻结飞书群目标。 -- **Minimal Scoped Changes — PASS**: 复用 `ChatSession`、`query_directory`、`send_channel_message`、`AgentRun.delivery_target`、`ChannelDelivery`,不新增 Provider、不新增依赖、不新增 Tool。 -- **Contract and State Ownership — PASS**: `ChatSession` 拥有稳定会话目标;Runtime 拥有 Run 与投递编排;`ChannelDelivery` 拥有外部投递事实;飞书 Adapter 拥有 `chat_id` 映射。 -- **Tests Prove Behavior — PASS**: 先覆盖目录/Tool/自动化入口的成功、越权、重复和未知结果,再实施。 -- **Preserve Existing Work — PASS**: 仅修改 feature spec 与相关 backend/frontend 文件,不触碰已有 `docs/` 脏改动。 -- **P0 C1 — PASS**: Schedule/Trigger 只向 Runtime 提交冻结目标,不写 checkpoint 生命周期。 -- **P0 C2 — PASS**: 每个群目标查询同时约束 `tenant_id`、`agent_id`、`source_channel=feishu`、`is_group=true`、未删除。 -- **P0 C3/C4 — PASS**: 外部写复用幂等 ChannelDelivery 和飞书 service wrapper。 -- **P0 C5 — PASS**: 不新增物理外键;分页查询;不引入循环查询。 -- **P0 C6 — PASS**: 目标解析抽成小型共享服务,不继续把飞书群分支堆在 `agent_tools.py`。 - -## Project Structure - -### Documentation - -```text -specs/003-feishu-group-proactive/ -├── spec.md -├── plan.md -├── research.md -├── data-model.md -├── quickstart.md -├── contracts/ -│ ├── directory-and-tool.md -│ └── automation-delivery.md -└── tasks.md -``` - -### Source Code - -```text -backend/ -├── alembic/versions/ # Schedule/Trigger target columns -├── app/models/{schedule,trigger}.py -├── app/services/ -│ ├── agent_directory.py # Feishu group directory projection -│ ├── feishu_group_targets.py # scoped target resolve + route builder -│ ├── agent_tools.py # existing Tool delegates to resolver -│ ├── heartbeat_runtime.py # Schedule target freeze -│ └── trigger_runtime/intake.py # Trigger target freeze -├── app/api/{directory,schedules,triggers}.py -└── tests/ # contract/runtime/regression coverage - -frontend/ -├── src/services/api.ts -├── src/pages/agent-detail/AgentDetailPage.tsx -└── src/i18n/{zh,en}.json -``` - -**Structure Decision**: 使用现有 backend/frontend 布局;新增一个窄的后端飞书群目标服务,保持 Tool handler、API 和 Runtime intake 共享同一校验与 route 构造逻辑。 - -## Design Sequence - -1. 用回归测试锁定现有用户私聊和其他 Provider 的 `target_member_id` 行为。 -2. 让可信飞书群入站继续创建 `ChatSession`,目录将符合约束的 Session 投影为 `member_type=group`、`provider_type=feishu`、`target_recipient_id=`。 -3. `send_channel_message` 优先识别 `target_recipient_id`,交给共享 resolver;人类旧参数沿原路径执行。群目标解析为 frozen channel route 后进入现有 typed Tool outcome 与 ChannelDelivery,不直接调用 Provider 裸接口。 -4. Schedule/Trigger CRUD 接受可选 `delivery_target_id`,保存 Session UUID;启用/创建时校验,Run 注册时再次校验并冻结 route。 -5. 前端 Schedule 表单加载飞书群目录并提供“仅保留在 Clawith / 发送到飞书群”选择;Trigger 管理 API 和 Agent Tool schema 支持同一字段。 -6. 运行测试、Ruff、前端测试/build、迁移检查、Architecture Guard;再以窄范围方式部署 3010。 -7. 3010 分别验证部署标记、容器、迁移、目录、浏览器主动群发、自动化群发、Run/Tool/ChannelDelivery/Provider 回执和群内实收。 - -## Post-Design Constitution Check - -全部通过。设计没有新增执行状态机、外部发送 Tool 或 Provider 直连入口;唯一新增持久字段是自动化对稳定 Session 目标的引用,实际投递继续由 Runtime/ChannelDelivery 所有。 - -## Complexity Tracking - -无宪法例外。 diff --git a/specs/003-feishu-group-proactive/quickstart.md b/specs/003-feishu-group-proactive/quickstart.md deleted file mode 100644 index 7226dc9e0..000000000 --- a/specs/003-feishu-group-proactive/quickstart.md +++ /dev/null @@ -1,19 +0,0 @@ -# Quickstart: 飞书群主动消息验证 - -## Local contract verification - -1. 创建两个租户、两个 Agent 及各自飞书群 Session。 -2. 验证 `query_directory(member_type=group)` 只返回当前 Agent/租户群且不泄漏 `chat_id`。 -3. 验证 Direct Chat Tool 使用目录返回 ID,最终 Provider envelope 为 `chat_id`;重复 Tool receipt 不重复写外部消息。 -4. 验证旧 `target_member_id` 飞书私聊和其他 Provider 测试不变。 -5. 给 Schedule/Trigger 绑定群目标,验证 Run 注册时冻结 `delivery_target`,重复 occurrence/execution 只生成一个投递事实。 -6. 运行 scoped pytest、Ruff、前端测试/build、Alembic heads 和 Architecture Guard。 - -## 3010 deployment verification - -1. 重新发现 3010 当前代码、容器、数据库与迁移状态;部署前备份变更文件/数据库。 -2. 部署同一候选代码并执行 migration;记录源码 marker/hash、容器 image/status/restart count 和 Alembic version。 -3. 使用已明确的测试 Agent 主动查询 Bot 所在群并同步可信群 Session,无需群成员先发消息。 -4. 浏览器 Direct Chat 请求 Agent 向该群发送唯一 marker;核对 Run、Tool ledger、ChannelDelivery、Provider receipt 和群内实收。 -5. 创建一次性测试自动化绑定同一群;核对无需群内新消息即可投递唯一 marker,重复调度不重复发送。 -6. 删除测试自动化;保留群消息和数据库证据,分别报告本地、部署和真实 Provider 验证边界。 diff --git a/specs/003-feishu-group-proactive/research.md b/specs/003-feishu-group-proactive/research.md deleted file mode 100644 index 80dc62ca6..000000000 --- a/specs/003-feishu-group-proactive/research.md +++ /dev/null @@ -1,53 +0,0 @@ -# Research: 飞书群主动消息 - -## Decision 1: 复用 ChatSession 作为稳定飞书群目标 - -**Decision**: 调用飞书官方“获取用户或机器人所在的群列表”同步 Bot 当前所在群,并复用可信飞书入站进行增量更新;同步结果落为 Agent 范围内的 `ChatSession`。模型使用 Session UUID,Provider `chat_id` 保持内部。 - -**Rationale**: 官方接口能在群内无人先发消息时列出 Bot 已加入的群;Session 已有 tenant、agent、source channel、group 标志、展示名和唯一外部会话标识。同步到 Session 可避免新的重复群登记表和生命周期。 - -**Alternatives considered**: -- 新建 `feishu_group_targets` 表:重复保存 Session 已拥有的身份与状态,增加同步漂移。 -- 允许模型直接传 `chat_id`:无法证明来源与 Agent/租户授权,且泄漏 Provider 寻址细节。 -- 搜索 Bot 未加入的公开群:扩大权限和误发范围,本期不允许。 - -## Decision 2: 增强现有 send_channel_message - -**Decision**: 增加 `target_recipient_id`,本期只解析飞书群 Session;保留 `target_member_id` 供现有各渠道人类收件人使用。 - -**Rationale**: 用户明确不要独立新 Tool;统一 Tool 让浏览器、Trigger 和 Schedule 共享同一授权、typed outcome 与审计路径,同时避免破坏旧模型调用。 - -**Alternatives considered**: -- `send_feishu_group_message`:Tool 数量和 Provider 特例持续增长。 -- 把群塞入 `target_member_id`:语义错误,容易把群 Session UUID 误解析为 OrgMember UUID。 -- 立即迁移所有人类调用到新字段:本次范围过宽,会影响其他 Provider。 - -## Decision 3: 自动化只保存目标引用,Runtime 注册时冻结 route - -**Decision**: Schedule 与 Trigger 保存可选 `delivery_target_id`;Run 注册时共享 resolver 校验 Session 当前仍有效并生成现有 `delivery_target`/`channel_delivery` route。 - -**Rationale**: 配置保存用户意图,Run 保存该次执行冻结事实;授权撤销后新 Run 不应发送,已登记 Run 的幂等行为仍由 Runtime 管理。 - -**Alternatives considered**: -- 只把群名写进 instruction:模型会猜目标,无法保证幂等或授权。 -- 完成后再查询目标:目标可能漂移,重试时不能证明冻结一致性。 -- 自动化直接调用飞书 API:绕过 Runtime、ChannelDelivery 和 exactly-once 语义。 - -## Decision 4: 浏览器请求使用 Tool side effect,自动化使用 terminal delivery - -**Decision**: Direct Chat 中明确“发到群”由模型调用 `send_channel_message`;绑定群的 Schedule/Trigger 则由 Run 的 terminal delivery 自动投递最终结果,避免依赖模型记得调用 Tool。 - -**Rationale**: 浏览器请求的具体内容和时点由用户指令决定,Tool 是合适的显式副作用;自动化绑定表达固定交付目的地,应由 Runtime 保证投递而不是 Prompt 约定。 - -**Alternatives considered**: -- 所有路径都要求模型调用 Tool:自动化可能漏发或重复调用。 -- 所有路径都自动 terminal delivery:普通 Direct Chat 无法在一个 Run 中决定发送多个或中间结果。 - -## Decision 5: 文件仍按文本链接交付 - -**Decision**: 本期 `send_channel_message` 只发文本;浏览器要求发送文件时复用已有可访问链接生成能力,再把链接作为消息发送。 - -**Rationale**: 用户要求的是群主动消息,附件上传是独立 Provider 合同;不应把本地路径误当外部可访问资源。 - -**Alternatives considered**: -- 同期扩展群附件:显著扩大 Provider、存储、权限和测试范围。 diff --git a/specs/003-feishu-group-proactive/spec.md b/specs/003-feishu-group-proactive/spec.md deleted file mode 100644 index d9cfe0222..000000000 --- a/specs/003-feishu-group-proactive/spec.md +++ /dev/null @@ -1,152 +0,0 @@ -# Feature Specification: 飞书群主动消息 - -**Feature Branch**: `003-feishu-group-proactive` -**Created**: 2026-08-18 -**Status**: Draft -**Input**: 用户要求补齐 Clawith Bot 主动向指定飞书群发消息的完整能力,包括稳定群目标、群目录、发送工具、定时任务与后台事件投递,并部署至 3010 验证。 - -## User Scenarios & Testing *(mandatory)* - -### User Story 1 - Agent 主动向指定飞书群发送消息 (Priority: P1) - -作为 Agent 管理者,我希望 Agent 能从自己被授权联系的飞书群中找到目标群,并通过现有的统一外部渠道消息能力主动发送一条消息,以便 Agent 不必等待群成员先发言就能通知团队。 - -**Why this priority**: 这是所有定时播报和后台事件通知的基础能力,也是当前能力缺口的核心。 - -**Independent Test**: 给一个已配置飞书渠道、已加入目标群且已获授权的 Agent,下达主动群发指令;目标群只收到一条内容正确、身份正确的 Bot 消息,并留下可查询的发送记录。 - -**Acceptance Scenarios**: - -1. **Given** Agent 已配置飞书 Bot 且目标群已登记并授权,**When** Agent 查询群目录并选择该群发送消息,**Then** 消息由该 Agent 的 Bot 投递到目标群且发送结果可审计。 -2. **Given** 同名群存在多个,**When** Agent 查询群目录,**Then** 系统返回稳定群目标标识和足够的区分信息,Agent 不以群名猜测目标。 -3. **Given** 目标群不属于当前租户、未授权给当前 Agent 或 Bot 已不在群中,**When** Agent 尝试发送,**Then** 系统拒绝发送并记录明确失败原因。 - ---- - -### User Story 2 - 浏览器对话要求发送到飞书群 (Priority: P1) - -作为在 Clawith 浏览器聊天中的用户,我希望直接要求 Agent 把文字、工作结果或文件链接发送到指定飞书群,使跨渠道交付不依赖 Trigger 或 Schedule。 - -**Why this priority**: 这是主动群消息最直接的人工入口,也是验证统一 Tool 能否在普通 Direct Chat Run 中工作的关键场景。 - -**Independent Test**: 用户在浏览器 Direct Chat 中要求 Agent 将一段内容发送到已授权飞书群;Agent 查询目录、选中稳定群目标并调用现有外部渠道消息 Tool,目标群只收到一条正确消息。 - -**Acceptance Scenarios**: - -1. **Given** 用户在浏览器与 Agent 对话且目标飞书群已登记授权,**When** 用户要求把指定内容发到该群,**Then** Agent 通过现有 `send_channel_message` 将内容投递到目标群。 -2. **Given** 浏览器用户只提供群名且目录中存在多个同名群,**When** Agent 查询目标,**Then** Agent 不猜测发送,而是依据目录结果要求明确选择。 -3. **Given** 用户要求发送工作区文件,**When** 当前群消息 Tool 仅支持文本,**Then** Agent 发送可访问的文件链接或明确说明限制,不把本地路径当作群成员可访问的附件。 - ---- - -### User Story 3 - 定时任务主动向飞书群播报 (Priority: P1) - -作为 Agent 管理者,我希望给定时任务绑定一个已授权飞书群,使 Agent 在计划时间完成工作后主动把结果发到该群。 - -**Why this priority**: 定时播报是用户明确要求补齐的主要业务场景,且必须复用稳定群目标与可靠投递能力。 - -**Independent Test**: 创建一个绑定目标群的单次或周期任务;到达计划时间后,无需群内新消息,目标群收到且只收到一条本次执行结果。 - -**Acceptance Scenarios**: - -1. **Given** 一个有效的定时任务绑定了已授权飞书群,**When** 到达一次计划执行时间且任务成功,**Then** 执行结果主动投递到绑定群。 -2. **Given** 同一次计划执行被重复调度或工作进程重试,**When** 系统恢复执行,**Then** 目标群不会收到重复消息。 -3. **Given** 到期时目标群已失效或授权已撤销,**When** 任务完成并尝试投递,**Then** 执行事实保留,投递标记为失败或待对账,且不会改投其他群。 - ---- - -### User Story 4 - 后台事件主动向飞书群通知 (Priority: P1) - -作为 Agent 管理者,我希望后台事件触发器能绑定一个已授权飞书群,在条件满足后主动发送处理结果或告警。 - -**Why this priority**: 后台事件是用户明确要求的另一条入口,必须和定时任务遵守相同的目标、权限和幂等契约。 - -**Independent Test**: 配置一个绑定飞书群的后台事件触发器并产生一次匹配事件;即使群内无人发言,目标群也只收到一次与该事件对应的通知。 - -**Acceptance Scenarios**: - -1. **Given** 后台事件触发器绑定了有效飞书群,**When** 事件满足触发条件并完成处理,**Then** 结果主动投递到该群。 -2. **Given** 同一外部事件重复到达,**When** 触发器重复评估,**Then** 系统以同一事件身份去重,不产生重复群消息。 -3. **Given** Provider 接收结果不确定,**When** 执行恢复或人工重试,**Then** 系统先对账,不盲目重复发送。 - ---- - -### User Story 5 - 保持现有飞书对话行为 (Priority: P2) - -作为现有飞书用户,我希望 Bot 私聊、群聊回复和历史记录行为不因主动群消息能力而改变。 - -**Why this priority**: 新能力不能破坏已经工作的私聊和原群回复链路。 - -**Independent Test**: 分别执行飞书私聊回复、群消息回复和主动群发送,验证三类消息进入正确目标且彼此不串会话。 - -**Acceptance Scenarios**: - -1. **Given** 用户在飞书私聊 Bot,**When** Agent 完成回复,**Then** 回复仍进入该用户私聊。 -2. **Given** 用户在飞书群触发 Agent,**When** Agent 完成回复,**Then** 回复仍进入原群。 -3. **Given** Agent 同时处理私聊、群回复和主动群任务,**When** 各 Run 完成,**Then** 每条消息只进入其冻结的目标。 - -### Edge Cases - -- 目标群改名时,稳定目标保持不变,目录展示更新后的名称。 -- Bot 被移出群、应用权限不足或目标群被解散时,发送失败必须可区分且可审计。 -- 同一飞书群被多个租户或多个 Agent 使用时,每个 Agent 只能看到并使用自身租户内明确授权的目标。 -- 定时任务创建后目标授权被撤销时,执行可以发生,但外部发送必须在投递时再次校验。 -- Provider 超时但可能已经接收消息时,结果必须进入未知/待对账状态,禁止自动重发造成重复。 -- 消息为空、群目标格式非法或群目标与当前 Agent 飞书配置不匹配时,在调用外部 Provider 前拒绝。 -- 旧 Trigger/Schedule 没有群目标时,继续采用原有投递行为,不自动选择最近群或默认群。 - -## Requirements *(mandatory)* - -### Functional Requirements - -- **FR-001**: 系统 MUST 将飞书群表示为租户范围内的稳定消息目标,至少包含稳定标识、展示名称、所属飞书渠道、可用状态和授权关系。 -- **FR-002**: 系统 MUST 通过飞书官方接口主动同步 Bot 当前所在群,并可用可信飞书入站事件补充或更新群目标;不允许调用方仅凭任意群 ID 绕过授权登记。 -- **FR-003**: Agent MUST 能通过现有统一目录查询自己获准联系的人、Agent 和飞书群,并获得可直接用于后续发送的统一稳定收件目标标识。 -- **FR-004**: Agent MUST 能通过现有 `send_channel_message`,使用稳定收件目标标识向飞书用户或飞书群主动发送文本消息;发送前必须校验目标类型、租户、Agent、飞书渠道配置和授权。 -- **FR-005**: 群发送接口 MUST 不接受群名称作为唯一寻址依据,也不得在找不到稳定目标时猜测或回退到其他群。 -- **FR-005A**: 普通浏览器 Direct Chat Run MUST 能查询已授权飞书群并调用现有 `send_channel_message`;该能力不得依赖 Trigger、Schedule 或飞书入站消息上下文。 -- **FR-006**: 用户 MUST 能为定时任务选择一个已授权飞书群作为结果投递目标。 -- **FR-007**: 用户 MUST 能为后台事件触发器选择一个已授权飞书群作为结果投递目标。 -- **FR-008**: 每次 Trigger/Schedule 执行 MUST 在 Run 注册时冻结投递目标,后续群改名不得改变目标,授权撤销则必须阻止外部发送。 -- **FR-009**: 系统 MUST 对一次 Run 的群投递使用稳定幂等身份;队列重试、进程重启和重复调度不得重复外部消息。 -- **FR-010**: 系统 MUST 分别记录业务执行状态与外部消息投递状态,不得把投递记录变成第二套 Agent Run 生命周期。 -- **FR-011**: Provider 明确成功、明确拒绝和结果未知 MUST 被区分记录;结果未知时不得自动重放外部发送。 -- **FR-012**: 主动群消息 MUST 进入对应的飞书群会话历史,并保留发送 Agent、Run、触发来源和 Provider 回执的关联信息。 -- **FR-013**: 系统 MUST 保持现有飞书私聊主动消息、私聊回复和群聊回复的寻址与历史行为。 -- **FR-014**: 系统 MUST 为新增群目标、目录、发送和 Trigger/Schedule 投递提供租户隔离、越权拒绝、幂等重试及兼容性验证。 -- **FR-015**: 部署验证 MUST 分别证明部署版本、服务健康、迁移状态、目标目录、真实定时或后台 Run、外部飞书群消息以及对应投递回执。 -- **FR-016**: 系统 MUST 不新增飞书群专用发送工具;`send_platform_message` 保持负责 Clawith 自有平台用户,现有 `send_channel_message` 在本功能中增加飞书用户与飞书群两类收件目标。 -- **FR-017**: 统一目录和 `send_channel_message` MUST 使用 Provider 无关的稳定收件目标标识;飞书 `chat_id` 等 Provider 标识不得作为模型直接寻址参数暴露。 -- **FR-018**: 现有各渠道的 `target_member_id` 调用 MUST 保持兼容;本功能新增的稳定收件目标仅用于飞书群,不得改变钉钉、企微、Slack、Teams、微信等其他 Provider 的发送行为。 - -### Key Entities - -- **稳定收件目标**: 目录中可被 Agent 联系的目标抽象;本功能只新增飞书群形态,对调用者提供稳定目标标识,对飞书适配层保留实际群寻址信息。 -- **飞书群目标**: 稳定收件目标的飞书群形态;包含所属租户、绑定 Agent/渠道、Provider 群标识、展示名称、状态和最后确认时间。 -- **群目标授权**: 描述哪个 Agent 可以发现和使用哪个飞书群目标,以及授权是否仍有效。 -- **自动化投递绑定**: 将一个定时任务或后台事件触发器绑定到稳定群目标,不包含执行生命周期状态。 -- **群消息投递记录**: 一次用户可见消息的幂等投递事实,关联 Run、目标、内容摘要、Provider 回执和投递结果。 - -## Success Criteria *(mandatory)* - -### Measurable Outcomes - -- **SC-001**: 管理者可在不复制 Provider 群 ID 的情况下,从 Agent 可见群目录选择目标并完成主动群发送。 -- **SC-002**: 在正常 Provider 条件下,定时任务或后台事件完成后 60 秒内,绑定群收到对应消息。 -- **SC-003**: 对同一执行进行至少 3 次重复派发或恢复,目标群最终最多出现 1 条对应消息。 -- **SC-004**: 跨租户、未授权 Agent、失效群和错误渠道的群发送测试 100% 在外部调用前被拒绝。 -- **SC-005**: Provider 成功、拒绝、超时未知三类结果在审计记录中 100% 可区分,未知结果不发生自动重复发送。 -- **SC-006**: 现有飞书私聊主动发送、私聊回复和群回复回归场景全部通过,且消息目标不串线。 -- **SC-007**: 3010 上至少完成一次真实主动群发送,以及一次由定时任务或后台事件触发的真实群投递,并从 Provider 回执和群内消息两侧确认。 - -## Assumptions - -- 飞书应用已经具备发送消息和接收群消息事件所需权限,Bot 已被加入目标群。 -- 群目录主动查询 Bot 当前所在群,不要求群成员先发送消息;不搜索 Bot 未加入的任意企业群。 -- 第一阶段只要求文本消息,文件、卡片、富文本和批量广播不属于本功能范围。 -- 本功能只为飞书群扩展 `query_directory` 与 `send_channel_message`,不增加 `send_feishu_group_message` 等专用工具,也不实现其他 Provider 的主动群消息。 -- `send_platform_message` 不扩展到飞书群;它继续表达 Clawith 自有平台投递,避免平台投递与外部 Provider 投递边界混淆。 -- 钉钉、企微、Slack、Teams、微信等其他 Provider 的目录、参数、发送和自动化投递行为均保持不变。 -- 一个自动化绑定一个明确目标群;需要多个群时由用户建立多个明确绑定,避免隐式广播。 -- 旧任务和旧触发器保持兼容,没有配置群目标时不改变原有投递行为。 -- 真实 3010 验证使用测试群和可撤销的测试自动化,不向未明确纳入验证范围的生产群发送消息。 diff --git a/specs/003-feishu-group-proactive/tasks.md b/specs/003-feishu-group-proactive/tasks.md deleted file mode 100644 index 15cfaeee2..000000000 --- a/specs/003-feishu-group-proactive/tasks.md +++ /dev/null @@ -1,60 +0,0 @@ -# Tasks: 飞书群主动消息 - -**Input**: `specs/003-feishu-group-proactive/` 设计工件 - -## Phase 1: Setup - -- [x] T001 确认分支、迁移头、脏工作区边界和现有飞书/Runtime测试基线,记录于 `specs/003-feishu-group-proactive/tasks.md` - -## Phase 2: Foundational - -- [x] T002 [P] 先为飞书群目标范围、route 构造和越权拒绝增加测试 `backend/tests/test_feishu_group_targets.py` -- [x] T003 [P] 先为目录群条目和 Tool 合同增加回归测试 `backend/tests/test_query_directory_tool.py`、`backend/tests/test_human_send_tools.py`、`backend/tests/test_builtin_tool_contracts.py` -- [x] T004 实现共享飞书群目标解析服务 `backend/app/services/feishu_group_targets.py` - -## Phase 3: User Story 1 - Agent 主动群发 - -- [x] T005 [US1] 扩展目录查询返回当前 Agent 可联系飞书群 `backend/app/services/agent_directory.py` -- [x] T006 [US1] 扩展现有 `query_directory`/`send_channel_message` schema 与 handler `backend/app/services/builtin_tool_definitions.py`、`backend/app/services/agent_tools.py` -- [x] T007 [US1] 运行目录、Tool、Provider scoped pytest 与 Ruff - -## Phase 4: User Story 2 - 浏览器 Direct Chat 群发 - -- [x] T008 [US2] 增加普通 Runtime Tool 到飞书群的 typed outcome 回归 `backend/tests/test_human_send_tools.py` -- [x] T009 [US2] 验证浏览器 Direct Chat 无需飞书入站上下文即可使用群目标 `backend/tests/test_human_send_tools.py` - -## Phase 5: User Story 3 - Schedule 群投递 - -- [x] T010 [P] [US3] 增加 Schedule Runtime 兼容与冻结目标回归 `backend/tests/test_schedule_runtime_intake.py`、`backend/tests/test_heartbeat_runtime.py` -- [x] T011 [US3] 增加 Schedule 可选目标模型与迁移 `backend/app/models/schedule.py`、`backend/alembic/versions/*_schedule_trigger_feishu_group_target.py` -- [x] T012 [US3] 接入 Schedule CRUD 与 Runtime intake `backend/app/api/schedules.py`、`backend/app/services/heartbeat_runtime.py`、`backend/app/services/scheduler.py` -- [x] T013 [US3] 扩展前端 Schedule API 类型合同 `frontend/src/services/api.ts` - -## Phase 6: User Story 4 - Trigger 群投递 - -- [x] T014 [P] [US4] 运行 Trigger API/Tool/Runtime scoped 回归 `backend/tests/test_trigger_runtime_intake.py`、相关 Trigger Tool 测试 -- [x] T015 [US4] 增加 Trigger 可选目标模型/API/Tool 合同 `backend/app/models/trigger.py`、`backend/app/api/triggers.py`、`backend/app/services/builtin_tool_definitions.py`、`backend/app/services/agent_tools.py` -- [x] T016 [US4] 在 Trigger Runtime 注册时校验并冻结飞书群 route `backend/app/services/trigger_runtime/intake.py` - -## Phase 7: User Story 5 - 兼容回归 - -- [x] T017 [US5] 验证飞书私聊、飞书群回复和其他 Provider `target_member_id` 行为不变 `backend/tests/test_human_send_tools.py`、`backend/tests/test_agent_runtime_delivery.py` - -## Phase 8: Polish, Merge and Deploy - -- [x] T018 运行 scoped/full backend tests、Ruff、frontend tests/build、Alembic heads、Architecture Guard -- [x] T019 审查最终 diff、提交 Lore commit 并合并到本地 `v1.11.4` -- [x] T020 重新发现并备份 3010 部署目标,部署合并后的 `v1.11.4` 并验证容器/迁移/源码 marker -- [ ] T021 在 3010 验证浏览器主动飞书群发与自动化群投递,核对 Run、Tool、ChannelDelivery、Provider 回执和群内消息 - -## Dependencies - -- T001 → T002/T003 → T004 → T005/T006 → T007 -- T007 → T008/T009 -- T004 → T010 → T011/T012 → T013 -- T004 → T014 → T015/T016 -- T009/T13/T16 → T017 → T018 → T019 → T020 → T021 - -## Implementation Strategy - -先完成并验证现有 Tool 的飞书群目标,再接 Schedule/Trigger;外部部署只使用合并后的 `v1.11.4` 候选,真实 Provider 验证与本地测试分开报告。 diff --git a/specs/004-feishu-passive-listening/checklists/requirements.md b/specs/004-feishu-passive-listening/checklists/requirements.md deleted file mode 100644 index 28937a7c2..000000000 --- a/specs/004-feishu-passive-listening/checklists/requirements.md +++ /dev/null @@ -1,36 +0,0 @@ -# Specification Quality Checklist: 飞书群常驻 Agent V1 - -**Purpose**: Validate specification completeness and quality before proceeding to planning -**Created**: 2026-08-18 -**Feature**: [spec.md](../spec.md) - -## Content Quality - -- [x] No implementation details (languages, frameworks, APIs) -- [x] Focused on user value and business needs -- [x] Written for non-technical stakeholders -- [x] All mandatory sections completed - -## Requirement Completeness - -- [x] No [NEEDS CLARIFICATION] markers remain -- [x] Requirements are testable and unambiguous -- [x] Success criteria are measurable -- [x] Success criteria are technology-agnostic -- [x] All acceptance scenarios are defined -- [x] Edge cases are identified -- [x] Scope is clearly bounded -- [x] Dependencies and assumptions identified - -## Feature Readiness - -- [x] All functional requirements have clear acceptance criteria -- [x] User scenarios cover primary flows -- [x] Feature meets measurable outcomes defined in Success Criteria -- [x] No implementation details leak into specification - -## Notes - -- Validation iteration 1 passed all checks. -- The specification intentionally keeps Provider permission names and concrete code paths out of business requirements; those belong in the design artifact. -- User confirmation is required before proceeding to design and constitution review. diff --git a/specs/004-feishu-passive-listening/contracts/feishu-passive-listening.md b/specs/004-feishu-passive-listening/contracts/feishu-passive-listening.md deleted file mode 100644 index 2f992c23c..000000000 --- a/specs/004-feishu-passive-listening/contracts/feishu-passive-listening.md +++ /dev/null @@ -1,85 +0,0 @@ -# Contract: 飞书群常驻 Agent V1 - -## 1. Inbound Event Contract - -接受条件: - -- event type 为现有飞书消息接收事件; -- message chat type 为 group; -- Provider `message_id` 非空; -- sender 为用户而非当前机器人; -- 消息类型属于现有已支持集合。 - -幂等键: - -```text -channel_message_id(agent_id, "feishu", provider_message_id) -``` - -同一键的重试必须收敛到同一 ChatMessage 和同一 Runtime source execution。 - -## 2. Silent Reply Contract - -常量: - -```text -NO_REPLY -``` - -规范化与判定: - -```text -silent := content.strip().casefold() == "no_reply" -``` - -不得使用 contains、suffix 或正则尾部匹配代替精确判定。 - -## 3. Suppression Scope - -只有以下条件全部成立才抑制: - -```text -delivery kind terminal -lifecycle status completed -channel feishu -receive_id_type chat_id -content exact silent token -``` - -结果: - -```text -Internal ChatMessage allowed -ChannelDelivery absent -Feishu provider call absent -Run terminal status completed -``` - -## 4. Non-silent Examples - -以下内容必须正常发送: - -```text -我来处理 -我来处理\nNO_REPLY -NO_REPLY:因为不相关 -`NO_REPLY` -请输出 NO_REPLY -``` - -## 5. Session Context Contract - -对飞书外部群 Session: - -- 入站用户消息始终可进入 pending/recent/compactable 选择。 -- 正常 Assistant 回复可进入选择。 -- 精确静默 Assistant 消息不进入模型可见 pending/recent/compactable 内容。 -- 原始数据库行不得删除。 -- 压缩水位线必须对应实际纳入 compact request 的最后一条消息位置。 - -## 6. Prohibited Changes - -- 不修改 Runtime `ModelIntent`、checkpoint lifecycle 或 finish schema。 -- 不写入或伪造原生 `group_id`。 -- 不将静默规则应用到飞书私聊或其他渠道。 -- 不在 Provider sender 收到 pending outbox 后才静默。 diff --git a/specs/004-feishu-passive-listening/data-model.md b/specs/004-feishu-passive-listening/data-model.md deleted file mode 100644 index 4dd0744a1..000000000 --- a/specs/004-feishu-passive-listening/data-model.md +++ /dev/null @@ -1,83 +0,0 @@ -# Data Model: 飞书群常驻 Agent V1 - -## 结论 - -本功能不新增表、不新增列、不新增迁移。所有事实继续由既有实体持有。 - -## Existing Entities - -### ChatSession - -飞书外部群形态保持: - -```text -tenant_id 必填,租户范围 -session_type group -group_id NULL;只为 Clawith 原生群保留 -agent_id 飞书 Channel 所属 Agent -source_channel feishu -external_conv_id 既有飞书外部会话映射,V1 不迁移 -is_group true -``` - -验证规则:不得把飞书 `chat_id` 写入 `group_id`;同一 Session 的 Agent 必须属于同一 tenant。 - -### ChatMessage - -- 入站用户消息:Provider `message_id` 经确定性映射得到本地 UUID。 -- 正常 Assistant 回复:保持现有落库行为。 -- 静默 Assistant 回复:允许保存精确 `NO_REPLY` 作为内部审计记录,但后续飞书群 Session Context 不把它当作有意义历史。 - -### AgentRun - -- 每条被接受飞书群消息对应一次现有 chat Run。 -- 不新增 lifecycle 或 completion mode。 -- 精确 `NO_REPLY` 仍是 completed Run。 -- 无外部 outbox 时沿用现有 settled delivery 行为。 - -### ChannelDelivery - -- 正常回答:创建一条 `pending` outbox,Provider sender 成功后转为 delivered。 -- 精确静默:不创建记录。 -- 失败/取消:不受 token 规则影响。 - -### SessionContextState - -- 继续以 `tenant_id + session_id` 持有滚动摘要、版本与 `covered_through_message_id`。 -- 飞书外部群的 `agent_id` 范围保持现有 group Session 规则;压缩模型归属通过 Session 的 `agent_id` 解析,不改变 state schema。 -- 水位线只在成功 CAS 提交有效摘要后前进。 - -## State Transitions - -### Normal reply - -```text -Inbound ChatMessage -→ AgentRun completed(content=normal text) -→ Assistant ChatMessage -→ ChannelDelivery pending -→ Provider delivered/failed -``` - -### Silent reply - -```text -Inbound ChatMessage -→ AgentRun completed(content=NO_REPLY) -→ Internal Assistant ChatMessage -→ no ChannelDelivery -→ no Provider call -``` - -### Session compaction - -```text -Messages after watermark reach threshold -→ lock Session -→ build compact request excluding exact silent Assistant rows -→ model returns candidate -→ CAS SessionContextState -→ watermark advances -``` - -失败时保持旧 state 和全部原始 ChatMessage。 diff --git a/specs/004-feishu-passive-listening/design.md b/specs/004-feishu-passive-listening/design.md deleted file mode 100644 index 71d5eafff..000000000 --- a/specs/004-feishu-passive-listening/design.md +++ /dev/null @@ -1,30 +0,0 @@ -# Design: 飞书群常驻 Agent V1 - -**Status**: Implemented -**Detailed plan**: [plan.md](./plan.md) -**Research**: [research.md](./research.md) -**Data contract**: [data-model.md](./data-model.md) -**Behavior contract**: [contracts/feishu-passive-listening.md](./contracts/feishu-passive-listening.md) - -## Decision Summary - -1. 飞书应用增加全量群用户消息权限,事件入口保持不变。 -2. 入站消息以 Provider `message_id` 幂等,每条有效群消息进入现有 Durable Runtime。 -3. 模型无需发言时输出 token-only `NO_REPLY`;只做精确、大小写不敏感匹配。 -4. 不修改 Runtime 状态机。Run 和内部 Assistant ChatMessage 正常完成并可审计。 -5. 在创建飞书群 `ChannelDelivery` 之前过滤精确静默结果,因此没有 outbox,也不会调用飞书发送接口。 -6. 飞书外部群 Session 复用现有 Session Context summary/watermark/CAS;压缩模型来自 `session.agent_id`。 -7. 精确静默 Assistant 消息不进入后续模型可见 Session Context,但底层记录不删除。 -8. 不迁移外部渠道 ID,不统一其他渠道,不新增表、依赖或 checkpoint 状态。 - -## Critical Boundaries - -- 飞书 `chat_id` 永远不是 Clawith 原生 `group_id`。 -- `正文 + NO_REPLY` 必须发送,只有 token-only 才静默。 -- failed/cancelled/waiting 不属于静默结果。 -- 正常回答继续由现有 ChannelDelivery outbox 与 Provider receipt 保证。 -- 压缩失败不得推进水位线或删除历史。 - -## Constitution Verdict - -设计通过 [docs/constitution.md](../../docs/constitution.md) C1–C6 检查,无例外项。实现阶段必须先写回归测试,再修改代码。 diff --git a/specs/004-feishu-passive-listening/plan.md b/specs/004-feishu-passive-listening/plan.md deleted file mode 100644 index 2b1be0425..000000000 --- a/specs/004-feishu-passive-listening/plan.md +++ /dev/null @@ -1,147 +0,0 @@ -# Implementation Plan: 飞书群常驻 Agent V1 - -**Branch**: `004-feishu-passive-listening` | **Date**: 2026-08-18 | **Spec**: [spec.md](./spec.md) -**Input**: Feature specification from `/specs/004-feishu-passive-listening/spec.md` - -## Summary - -为现有飞书 Agent Channel 增加群内全部用户消息接收能力。每条被接受的群消息继续通过现有 `enqueue_channel_chat_runtime()` 创建 Durable Runtime Run;模型无需发言时输出 token-only `NO_REPLY`。Runtime 仍把它当作正常非空完成文本,产品侧仍可保留内部 Assistant `ChatMessage`,但外部渠道投递在创建 `ChannelDelivery` 之前识别“成功终态 + 飞书群路由 + 精确静默令牌”并抑制 Provider 出站。现有 Session Context 后台压缩扩展到 `group_id IS NULL` 的飞书外部群 Session,并使用该 Session 所属 Agent 的模型预算;不新增状态表、不改变 checkpoint 生命周期、不统一其他渠道。 - -## Technical Context - -**Language/Version**: Python 3.12 deployment baseline(package metadata >=3.11);React 19 / strict TypeScript -**Primary Dependencies**: FastAPI、SQLAlchemy 2.x async ORM、LangGraph 1.2.x、Pydantic、httpx、React/Vite;不新增依赖 -**Storage**: PostgreSQL 15;复用 `chat_sessions`、`chat_messages`、`agent_runs`、`agent_run_events`、`channel_deliveries`、`session_context_states` -**Testing**: Pytest、Ruff;前端使用现有 test/build 命令 -**Target Platform**: Clawith backend/API/Runtime workers + 飞书自建应用机器人 -**Project Type**: Docker Compose monorepo web application -**Performance Goals**: 飞书事件回调只负责持久化消息与 Runtime Command 后返回;模型执行和外部投递保持异步。重复 Provider 消息不产生第二次 Run。长期群历史经滚动压缩后不进行无界全量装载。 -**Constraints**: V1 每条群消息均调用模型;精确 `NO_REPLY` 只抑制飞书群最终出站;不得影响失败/取消语义、飞书私聊、原生群和其他渠道;不得伪造原生 `group_id`。 -**Scale/Scope**: 单 Agent 多飞书群的长期 Session;本版不新增 Activation Gate、不统一企微/钉钉等渠道。 - -## Constitution Check - -*GATE: Passed before Phase 0 and re-checked after Phase 1.* - -| Gate | Result | Design evidence | -|---|---|---| -| Evidence Before Claims | PASS | 当前权限、入站幂等、ChannelDelivery 建立点和压缩器 `group_id` 限制均由源码与测试确认;OpenClaw 静默行为使用官方仓库与文档。 | -| Minimal Scoped Changes | PASS | 仅扩展飞书权限、飞书入站稳定 ID、外部投递静默过滤和飞书群压缩;不做跨渠道抽象或数据库迁移。 | -| Contract and State Ownership | PASS | 模型只拥有最终内容;Runtime checkpoint 不变;产品交付层决定是否建立飞书 outbox;Provider sender 仍拥有真实发送结果。 | -| Tests Prove Behavior | PASS | 计划先加入 token 精确匹配、零 outbox、普通回复、重复入站和外部群压缩水位线回归测试。 | -| Preserve Existing Work | PASS | 规格目录独立;现有 `docs/` 用户改动保持未触碰。 | -| C1 Runtime Boundary Isolation | PASS | 不增加 checkpoint 字段或第二状态机;API 仍只通过 Runtime Command Intake。 | -| C2 Multi-Tenant Scope | PASS | 新增查询分支必须同时按 `tenant_id`、Session、Agent 范围验证。 | -| C3 Idempotent Side Effects | PASS | 入站以飞书 `message_id` 幂等;静默路径不创建 `ChannelDelivery`;普通出站继续复用现有 outbox。 | -| C4 Wrapper Enforcement | PASS | 无新增前端 HTTP 请求;飞书发送继续通过现有 Provider sender。 | -| C5 DB/Performance | PASS | 无新表、无新 FK;压缩扫描保持批量扫描与现有 advisory lock/CAS。 | -| C6 Modularity | PASS | 静默识别作为小型纯函数;复用现有 Session Context policy/compactor/scanner。 | - -## Project Structure - -### Documentation (this feature) - -```text -specs/004-feishu-passive-listening/ -├── spec.md -├── design.md -├── plan.md -├── research.md -├── data-model.md -├── quickstart.md -├── contracts/ -│ └── feishu-passive-listening.md -├── checklists/ -│ └── requirements.md -└── tasks.md # 下一阶段生成 -``` - -### Source Code (repository root) - -```text -backend/app/ -├── api/feishu.py -├── services/ -│ ├── agent_runtime/ -│ │ ├── channel_delivery.py -│ │ ├── delivery.py -│ │ ├── model_step_service.py -│ │ ├── session_context_background.py -│ │ ├── session_context_compactor.py -│ │ └── session_context_service.py -│ └── llm/finish.py - -backend/tests/ -├── test_feishu_channel_runtime.py -├── test_agent_runtime_channel_delivery.py -├── test_agent_runtime_delivery.py -├── test_agent_runtime_session_context_background.py -├── test_agent_runtime_session_context_compactor.py -└── test_session_context_service.py - -frontend/src/ -└── components/ChannelConfig.tsx -``` - -**Structure Decision**: 保持现有 API → Channel Runtime Intake → Durable Runtime → Product Delivery → Provider Worker 分层。静默属于产品侧外部渠道投递过滤,不进入模型协议解析器或 Runtime graph;压缩属于现有 Session Context 子系统。 - -## Phase 0: Research Decisions - -详见 [research.md](./research.md)。已解决所有设计未知项:飞书全量权限、Provider 幂等键、OpenClaw 精确静默令牌、Clawith 投递截断点、飞书外部群压缩模型归属。 - -## Phase 1: Design - -### 1. 飞书入站 - -- 权限模板增加 `im:message.group_msg`;保留现有单聊、群 @ 与发送权限。 -- `im.message.receive_v1` 继续作为唯一事件入口。 -- Provider `message.message_id` 作为 `channel_message_id()` 的外部稳定输入;`event_id` 仅用于观测,不作为消息幂等权威事实。 -- 过滤机器人自身/机器人发送者;V1 处理飞书推送的用户消息,不扩展机器人间群消息。 -- API 在同一事务中持久化 `ChatMessage` 与 Runtime Command 后提交,模型执行不阻塞 Provider 回调。 - -### 2. 模型静默协议 - -- 仅飞书群 Run 的系统指令增加 token-only 规则。 -- 静默令牌常量为 `NO_REPLY`。 -- 识别函数只接受 `text.strip().casefold() == "no_reply"`;正文前后存在任何非空内容均不是静默。 -- 不修改 `finish`、`ModelIntent`、Verifier、checkpoint lifecycle 或 finalizer。 - -### 3. 出站抑制 - -- 在 `deliver_runtime_message()` 已确定实际 Session 和现有 `channel_delivery` route 后、调用 `stage_channel_delivery()` 前判定。 -- 必须同时满足:`kind=terminal`、`lifecycle_status=completed`、route channel 为 `feishu`、target `receive_id_type=chat_id`、内容为精确静默令牌。 -- 命中后仍保留内部 Assistant `ChatMessage` 和常规本地 delivery receipt,使 Run 能正常 settled;不创建 `ChannelDelivery`,因此 Provider worker 无工作项且不会调用飞书 API。 -- `agent_runs.delivery_status` 沿用“产品 Session 已投递”的既有含义:无 outbox 时为 `delivered`。审计通过终态内容为精确令牌且缺少对应 `channel_deliveries` 行证明有意静默;不新增数据库状态。 -- waiting、failed、cancelled 不进入静默判定,沿用既有投递策略。 - -### 4. 模型可见历史过滤 - -- 底层 `ChatMessage(content="NO_REPLY")` 保留用于审计。 -- 仅对 `session_type=group AND source_channel=feishu AND group_id IS NULL` 的 Session,Session Context 读取与 compactable 集合过滤精确静默 Assistant 消息。 -- 用户消息、正常 Assistant 回复、正文包含 `NO_REPLY` 的消息不被过滤。 -- 过滤不删除数据库行、不改变消息时间线水位线的权威位置;压缩水位线仍由最后一个实际纳入压缩的消息 ID 决定。 - -### 5. 飞书外部群 Session 压缩 - -- Session 判定:`session_type=group`、`group_id IS NULL`、`source_channel=feishu`、`agent_id IS NOT NULL`、未删除。 -- Policy resolver 验证 Session Agent 同租户、可用且未删除,使用该 Agent 的 active model 计算阈值;`source_agent_id=session.agent_id`。 -- LLMSessionContextCompactor 对此外部群使用该 Agent active model并记录 usage_agent_id;原生群仍使用 tenant multi-agent compact model。 -- Scanner 使用两个明确分支联合选择原生群候选和飞书外部群候选,保持现有批量游标、advisory lock 和 CAS 提交。 -- 不创建 `Group`/`GroupMember`,不写 `chat_sessions.group_id`,不修改外部会话 ID。 - -### 6. 测试顺序 - -1. 先加入精确静默识别与零 `ChannelDelivery` 回归测试。 -2. 加入飞书 `message_id` 重试幂等测试。 -3. 加入飞书外部群 policy/model selection/scanner 测试。 -4. 加入 Session Context 过滤和水位线测试。 -5. 实现最小代码变更。 -6. 运行 scoped pytest、Ruff、`scripts/arch-guard.sh`;前端权限常量改动后运行前端 test/build。 - -## Post-Design Constitution Re-check - -Phase 1 后仍全部 PASS。特别确认:静默不会修改 Runtime checkpoint state machine;压缩继续使用唯一 `session_context_states` 真相与 CAS;普通飞书发送继续由 `channel_deliveries` 和 Provider receipt 管理。 - -## Complexity Tracking - -无 Constitution 违规,无需例外批准。 diff --git a/specs/004-feishu-passive-listening/quickstart.md b/specs/004-feishu-passive-listening/quickstart.md deleted file mode 100644 index 1c767e05f..000000000 --- a/specs/004-feishu-passive-listening/quickstart.md +++ /dev/null @@ -1,58 +0,0 @@ -# Quickstart: 飞书群常驻 Agent V1 验证 - -## Preconditions - -1. 使用测试租户和测试 Agent。 -2. 飞书自建应用开启机器人能力并订阅消息接收事件。 -3. 飞书应用取得并发布全量群用户消息权限。 -4. 将机器人加入隔离测试群。 - -## Local Contract Checks - -```bash -cd backend -.venv/bin/python -m pytest \ - tests/test_feishu_channel_runtime.py \ - tests/test_agent_runtime_channel_delivery.py \ - tests/test_agent_runtime_delivery.py \ - tests/test_agent_runtime_session_context_background.py \ - tests/test_agent_runtime_session_context_compactor.py \ - tests/test_session_context_service.py - -.venv/bin/ruff check \ - app/api/feishu.py \ - app/services/agent_runtime/channel_delivery.py \ - app/services/agent_runtime/delivery.py \ - app/services/agent_runtime/model_step_service.py \ - app/services/agent_runtime/session_context_background.py \ - app/services/agent_runtime/session_context_compactor.py \ - app/services/agent_runtime/session_context_service.py -``` - -如前端权限模板发生变化: - -```bash -cd frontend -npm test -npm run build -``` - -架构检查: - -```bash -scripts/arch-guard.sh -``` - -## Manual Scenarios - -1. 普通群消息、不 @Agent:确认产生一个入站 ChatMessage 和一个 Run。 -2. 重放同一 Provider message_id 三次:确认仍只有一个消息和一个 Run。 -3. 模型输出精确 `NO_REPLY`:确认内部 Run completed、无 ChannelDelivery、飞书群无消息。 -4. 模型输出 `正文\nNO_REPLY`:确认正常发送完整正文。 -5. 飞书私聊输出相同 token:确认本版未改变私聊路径。 -6. 生成超过压缩阈值的群历史:确认 SessionContextState 水位线前进,原始 ChatMessage 不减少。 - -## Evidence Boundary - -- 单元测试证明本地契约,不证明飞书权限已审批或线上事件实际到达。 -- 真实飞书验证必须分别提供:事件到达、Run 完成、无 outbox/有 outbox、Provider 发送结果和群内可见性的证据。 diff --git a/specs/004-feishu-passive-listening/research.md b/specs/004-feishu-passive-listening/research.md deleted file mode 100644 index 6cc64ed52..000000000 --- a/specs/004-feishu-passive-listening/research.md +++ /dev/null @@ -1,73 +0,0 @@ -# Research: 飞书群常驻 Agent V1 - -## Decision 1: 使用飞书消息事件接收全部群用户消息 - -**Decision**: 继续订阅 `im.message.receive_v1`,新增敏感权限 `im:message.group_msg`。 - -**Rationale**: 飞书官方事件根据应用权限决定推送范围;现有 `im:message.group_at_msg:readonly` 只能收到群内 @ 机器人消息,`im:message.group_msg` 才覆盖机器人所在群的全部用户消息。 - -**Alternatives considered**: - -- 定时调用历史消息 API:增加延迟、分页与重复读取复杂度,不适合实时常驻 Agent。 -- 保持群 @ 权限:无法实现普通消息进入上下文。 - -**Primary source**: [飞书接收消息事件](https://open.feishu.cn/document/server-docs/im-v1/message/events/receive?lang=zh-CN) - -## Decision 2: 入站幂等使用 Provider message_id - -**Decision**: 使用事件体中的飞书 `message.message_id` 生成本地稳定消息 ID;`event_id` 不作为消息幂等权威键。 - -**Rationale**: 飞书官方明确提示特殊情况下可能重复推送,应使用 `message_id` 去重而不是依赖 `event_id`。 - -**Alternatives considered**: - -- 继续优先 `event_id`:同一消息若以不同事件投递会重复执行。 -- 仅用内存集合:进程重启和多 worker 下不可靠。 - -## Decision 3: 采用 OpenClaw token-only 静默模式 - -**Decision**: 模型无需发言时输出精确 `NO_REPLY`;仅 token-only、大小写不敏感、允许首尾空白的结果静默。 - -**Rationale**: OpenClaw 当前将精确静默令牌从出站 payload 中过滤,并专门限制为 token-only,以避免吞掉正文末尾包含 `NO_REPLY` 的有效回答。 - -**Alternatives considered**: - -- 新增 Runtime `no_reply` intent:超过用户要求,扩大 checkpoint/Verifier/终态契约。 -- 空字符串:被现有 finish、node transition、Verifier 和 completed checkpoint 规则拒绝。 -- `endswith("NO_REPLY")`:存在吞掉有效正文的已知风险。 - -**Primary sources**: [OpenClaw agent loop](https://github.com/openclaw/openclaw/blob/main/docs/concepts/agent-loop.md), [OpenClaw tokens.ts](https://github.com/openclaw/openclaw/blob/main/src/auto-reply/tokens.ts) - -## Decision 4: 在外部 ChannelDelivery 建立前抑制 - -**Decision**: 保留正常 Runtime 完成和内部 Assistant ChatMessage,只跳过飞书群 `ChannelDelivery` 建立。 - -**Rationale**: 用户要求仅为“不发到飞书群”。该位置能确保 Provider worker 没有可发送 outbox,同时不修改 Runtime state machine,也不伪造发送失败。 - -**Alternatives considered**: - -- 在模型解析层吞掉:会触发非空 finish/Verifier 修复。 -- 在飞书 Sender 内丢弃:已经创建 pending outbox,容易产生状态与重试语义不一致。 -- 删除内部 ChatMessage:削弱审计且扩大 delivery 事务差异。 - -## Decision 5: 飞书外部群复用现有 Session Context - -**Decision**: 扩展现有 policy resolver、compactor model selection 和 scanner,使 `group_id IS NULL` 的飞书群 Session 使用 `session.agent_id` 对应模型预算。 - -**Rationale**: 当前外部飞书群已经是 `session_type=group`,但没有 Clawith 原生 `group_id`;现有 scanner inner join `groups`,因此不会压缩。复用现有水位线、advisory lock、CAS 和 summary schema 能避免第二套上下文状态。 - -**Alternatives considered**: - -- 将飞书 chat_id 写入原生 group_id:类型和领域都错误。 -- 为飞书新建上下文表:形成重复状态真相。 -- 暂不压缩:全量群消息会造成无界 pending history。 - -## Decision 6: V1 不统一其他渠道 - -**Decision**: 不迁移外部 ID,不建立统一 Conversation Adapter,不修改企微/钉钉/Slack/Teams/Discord。 - -**Rationale**: 用户明确选择先验证飞书临时版本,等更多渠道具备相同行为后再根据真实差异统一。 - -**Alternatives considered**: - -- 本次完成跨渠道统一:范围与迁移风险显著增加,且不是验证核心体验所必需。 diff --git a/specs/004-feishu-passive-listening/spec.md b/specs/004-feishu-passive-listening/spec.md deleted file mode 100644 index 63287dbc6..000000000 --- a/specs/004-feishu-passive-listening/spec.md +++ /dev/null @@ -1,156 +0,0 @@ -# Feature Specification: 飞书群常驻 Agent V1 - -**Feature Branch**: `004-feishu-passive-listening` -**Created**: 2026-08-18 -**Status**: Implemented and live-validated on 3010 -**Input**: 用户希望将 Agent 放入飞书群后接收群内全部用户消息;每条消息均进入 Agent 判断,需要参与时正常回复,不需要参与时输出精确静默令牌 `NO_REPLY`,且不得向飞书群发送该令牌或任何占位回复。长期复用的飞书群 Session 必须具备有界上下文和滚动压缩能力。 - -## User Scenarios & Testing *(mandatory)* - -### User Story 1 - Agent 旁听飞书群并按需发言 (Priority: P1) - -企业管理员将已配置的 Agent 机器人加入飞书群并授予读取群内全部用户消息的权限。此后,群成员无需每次 `@Agent`,群消息也能进入该 Agent 对应的群会话,由 Agent 结合职责和上下文判断是否需要发言。 - -**Why this priority**: 这是“常驻群成员”体验成立的前提;如果普通群消息无法进入 Agent,后续静默判断和长期上下文都没有意义。 - -**Independent Test**: 在机器人已加入且具有全量群消息权限的测试群发送一条不包含 `@Agent` 的普通用户消息,验证消息只被接收一次、进入正确的飞书群 Session,并触发一次 Agent 判断。 - -**Acceptance Scenarios**: - -1. **Given** Agent 机器人已加入飞书群且拥有全量用户消息权限,**When** 群成员发送一条不包含 `@Agent` 的文本消息,**Then** 系统持久化该消息并为对应 Agent 发起一次判断。 -2. **Given** 同一 Agent 同时存在飞书私聊和多个飞书群,**When** 任一群收到消息,**Then** 消息只进入该 Agent 与该飞书群对应的 Session,不进入私聊或其他群 Session。 -3. **Given** 飞书对同一消息重复推送事件,**When** 系统重复收到该消息,**Then** 只保留一条入站消息并只发起一次 Agent 判断。 -4. **Given** 消息由机器人自身或不在本版本支持范围内的发送者产生,**When** 系统收到事件,**Then** 不形成会导致机器人自我回复的循环。 - ---- - -### User Story 2 - 无需参与时保持群内静默 (Priority: P1) - -Agent 判断当前群消息不需要自己参与时,最终输出精确静默令牌 `NO_REPLY`。该次判断正常结束并保留必要的内部审计事实,但群成员看不到 `NO_REPLY`、空白消息、“无需回复”或其他占位内容。 - -**Why this priority**: 全量接收会显著增加 Agent 被调用的次数;如果每次调用都在群内发言,常驻 Agent 会造成严重干扰。 - -**Independent Test**: 让模型分别产生精确静默令牌、正常回答以及“正常回答后附带令牌”三种结果,验证只有精确静默令牌不产生飞书群出站调用。 - -**Acceptance Scenarios**: - -1. **Given** 一次飞书群判断的最终内容去除首尾空白后大小写不敏感地等于 `NO_REPLY`,**When** 判断正常完成,**Then** 系统不向该飞书群调用发送消息能力。 -2. **Given** 最终内容是非空正常回答,**When** 判断正常完成,**Then** 系统沿用现有可靠投递机制向原飞书群发送回答。 -3. **Given** 最终内容包含正常正文并在末尾出现 `NO_REPLY`,**When** 判断正常完成,**Then** 系统将整段内容视为正常回答,不得误判为静默。 -4. **Given** 最终内容为精确 `NO_REPLY`,**When** 查询内部运行记录,**Then** 可以确认该次判断已完成且出站被有意抑制,而不是模型失败或飞书发送失败。 -5. **Given** 飞书私聊、Clawith 原生群或其他外部渠道产生相同文本,**When** 判断完成,**Then** 本版本的飞书群静默规则不改变这些既有路径。 - ---- - -### User Story 3 - Agent 在长期群聊中保留可用上下文 (Priority: P1) - -同一飞书群 Session 可以长期接收大量群消息。Agent 每次判断都能获得近期原文与较早内容的滚动摘要,同时历史增长不会让每次判断读取无限消息或超过模型可用上下文。 - -**Why this priority**: 全量群消息会比 `@Agent` 模式更快累积历史;没有压缩会使成本、延迟和上下文溢出风险持续增长。 - -**Independent Test**: 在一个飞书群 Session 中生成超过压缩阈值的消息,验证压缩水位线前进、近期窗口仍保留原文、后续判断使用摘要加近期消息,且原始消息记录未被删除。 - -**Acceptance Scenarios**: - -1. **Given** 飞书群 Session 的待处理历史达到既有压缩条件,**When** 后台压缩执行,**Then** 系统推进该 Session 的上下文水位线并生成可供后续判断使用的滚动摘要。 -2. **Given** 较早消息已经进入滚动摘要,**When** 新群消息触发 Agent 判断,**Then** Agent 获得摘要、尚未压缩的消息和近期原文,而不是重新装载全部历史。 -3. **Given** Agent 多次输出精确 `NO_REPLY`,**When** 构造后续模型上下文,**Then** 纯静默输出不会作为有意义的群聊内容反复占用上下文预算。 -4. **Given** 飞书群属于外部渠道,**When** 执行上下文压缩,**Then** 系统使用该 Session 所属 Agent 的有效模型预算,不要求或伪造 Clawith 原生群身份。 -5. **Given** 上下文压缩失败,**When** 后续扫描再次执行,**Then** 原始消息仍然完整,水位线不错误前进,且失败可被运维人员识别。 - ---- - -### User Story 4 - 管理员能够正确开通飞书能力 (Priority: P2) - -配置 Agent 飞书渠道的用户能够看到并申请接收群内全部用户消息所需的权限,并了解这是敏感权限且需要在飞书侧发布后才能生效。 - -**Why this priority**: 服务端实现只有在飞书实际推送普通群消息时才能工作;权限遗漏会让功能表面启用但始终只收到 `@Agent` 消息。 - -**Independent Test**: 检查产品提供的飞书权限配置包含全量群消息权限,并验证未开通与已开通两种飞书应用配置下的可观察行为符合说明。 - -**Acceptance Scenarios**: - -1. **Given** 用户查看飞书渠道配置指南,**When** 复制或核对权限列表,**Then** 能看到接收群内全部用户消息所需的敏感权限及发布提示。 -2. **Given** 飞书应用尚未取得该权限,**When** 普通群消息未到达系统,**Then** 产品说明不会错误宣称全量监听已经生效。 - -### Edge Cases - -- 飞书重复推送同一 `message_id`,但 `event_id` 不同。 -- 同一群消息包含文字、富文本、图片或文件;沿用现有已支持类型,不因静默判断重复下载或重复入队。 -- 消息只有对机器人的 `@` 占位符,移除 mention 后没有可判断的正文。 -- 模型返回 `NO_REPLY`、`no_reply` 或带首尾空白的等价形式。 -- 模型返回 `NO_REPLY:因为……`、正文加 `NO_REPLY`、代码块中的 `NO_REPLY`;这些都不是精确静默结果。 -- Agent Run 成功但静默,必须与 Provider 调用失败区分。 -- Agent Run 失败、取消或等待外部输入时不得被静默规则误判为正常 `NO_REPLY`。 -- 高活跃群在压缩工作尚未完成时继续收到新消息;水位线必须保持单调且不跨越未纳入摘要的消息。 -- 飞书群 Session 的外部群标识不是 Clawith 原生 `group_id`,不得创建伪造的原生群记录。 -- 多租户中相同飞书群标识或相同 Agent 名称不得造成跨租户 Session、消息或摘要混用。 - -## Requirements *(mandatory)* - -### Functional Requirements - -- **FR-001**: 系统 MUST 能接收已授权飞书机器人所在群聊中的全部用户消息,而不仅是明确 `@机器人` 的消息。 -- **FR-002**: 系统 MUST 将每条支持的飞书群入站消息归入正确租户、正确 Agent 和正确外部群 Session。 -- **FR-003**: 系统 MUST 使用飞书消息自身的稳定标识保证入站幂等,不得仅依赖一次事件投递的标识。 -- **FR-004**: 系统 MUST 对机器人自身消息和重复消息实施循环与重复执行保护。 -- **FR-005**: 每条被接受的飞书群用户消息 MUST 进入一次 Agent 判断;V1 不引入独立 Activation Gate。 -- **FR-006**: 飞书群判断 Prompt MUST 明确告知 Agent:无需在群内发言时,最终内容只能是精确 `NO_REPLY`。 -- **FR-007**: 系统 MUST 仅在成功完成的飞书群判断最终内容去除首尾空白后大小写不敏感地精确等于 `NO_REPLY` 时抑制出站。 -- **FR-008**: 命中静默规则时,系统 MUST 不调用飞书群发送消息能力,且群内不得出现令牌、空白消息或替代占位内容。 -- **FR-009**: 系统 MUST NOT 使用后缀、子串或模糊匹配判断静默;包含任何其他可见正文的结果 MUST 按正常回答处理。 -- **FR-010**: 系统 MUST 保留足以区分“正常静默完成”“模型失败”“运行取消”和“飞书投递失败”的内部审计事实。 -- **FR-011**: V1 的静默抑制 MUST 仅作用于飞书群最终回复,不改变飞书私聊、Clawith 原生群和其他外部渠道的既有完成与投递行为。 -- **FR-012**: 系统 MUST 继续使用现有飞书群 Session,不得将飞书群 ID 写入或伪装为 Clawith 原生 `group_id`。 -- **FR-013**: 飞书群 Session MUST 使用现有 Session Context 机制维护滚动摘要、近期原文与压缩水位线,不新增另一套上下文状态机。 -- **FR-014**: 飞书外部群 Session 的压缩预算 MUST 来自该 Session 所属 Agent 的有效模型配置,而不是原生群成员列表。 -- **FR-015**: 纯 `NO_REPLY` Assistant 输出 MUST NOT 作为有意义的模型可见历史反复进入后续 Session Context,但底层审计记录可以保留。 -- **FR-016**: 压缩 MUST 保留原始消息记录;压缩失败时不得推进水位线或丢弃消息。 -- **FR-017**: 产品提供的飞书权限配置和指南 MUST 包含接收群内全部用户消息所需的敏感权限及飞书侧发布要求。 -- **FR-018**: 所有新增读取、写入、幂等与压缩操作 MUST 维持严格租户范围。 -- **FR-019**: V1 MUST NOT 统一或迁移飞书、企微、钉钉、Slack、Teams、Discord 的外部会话 ID 数据结构。 -- **FR-020**: V1 MUST NOT 新增依赖、创建第二套 Run 生命周期状态机或改变现有飞书出站成功的权威判定。 - -### Key Entities - -- **飞书群 Session**: 一个 Agent 与一个飞书群的长期逻辑会话;保留现有外部会话映射,不具有 Clawith 原生群身份。 -- **飞书入站消息**: 飞书推送的用户消息,包含稳定消息标识、发送者、群会话标识、消息类型和内容。 -- **Agent 判断 Run**: 针对一条被接受群消息执行的一次 Durable Runtime 判断;可能产生正常回答或精确静默令牌。 -- **静默结果**: 最终可见文本精确为 `NO_REPLY` 的成功完成结果;它只改变飞书群出站行为。 -- **Session Context**: 按 Session 保存的滚动摘要、压缩水位线和近期消息窗口;原始消息仍由消息记录持有。 -- **渠道投递记录**: 仅在需要真正向飞书发送内容时建立的可靠出站事实。 - -## Success Criteria *(mandatory)* - -### Measurable Outcomes - -- **SC-001**: 在具备全量群消息权限的测试群中,100% 的受支持普通用户消息能够进入正确 Session,且无需 `@Agent`。 -- **SC-002**: 对同一飞书消息进行至少 3 次重复事件投递时,系统只产生 1 条入站消息和 1 次 Agent 判断。 -- **SC-003**: 对精确 `NO_REPLY`、大小写变化和首尾空白共至少 6 个静默样例,飞书发送调用次数均为 0。 -- **SC-004**: 对正文包含或结尾附带 `NO_REPLY` 的至少 6 个非静默样例,均不被错误抑制。 -- **SC-005**: 正常回答仍通过现有可靠投递路径到达原飞书群,并能取得 Provider 成功或明确失败证据。 -- **SC-006**: 超过 Session 压缩阈值后,后续 Agent 判断不需要读取完整群历史;摘要水位线前进且近期原文仍可用。 -- **SC-007**: 压缩前后的原始飞书群消息数量和内容保持不变,压缩失败测试中水位线保持不变。 -- **SC-008**: 飞书私聊、原生群以及至少一个其他外部渠道的既有投递回归测试全部通过。 -- **SC-009**: 功能上线后,群内不会出现由精确静默令牌产生的 `NO_REPLY`、空白消息或“无需回复”占位内容。 - -## Assumptions - -- V1 仅覆盖飞书群;其他渠道继续维持当前接收与投递行为。 -- 飞书应用已经开启机器人能力、订阅现有消息接收事件,并由管理员申请及发布全量群消息敏感权限。 -- 每条被接受消息都调用现有 Agent 模型;独立低成本 Activation Gate、批处理与动态关注规则不在 V1 范围。 -- `@Agent` 是强相关信号,但 V1 不新增独立的 mention 调度状态机。 -- `NO_REPLY` 是模型控制令牌而不是面向群成员的内容;仅精确 token-only 结果触发静默。 -- V1 允许内部保留 `NO_REPLY` 审计记录,但不得将它作为有意义的后续群上下文。 -- 模型失败、取消和等待状态沿用现有 Runtime 语义;它们不是 `NO_REPLY`。 -- 现有飞书群 Session 映射继续通过既有渠道字段工作;本功能不迁移外部渠道身份结构。 - -## Out of Scope - -- 企微、钉钉、Slack、Teams、Discord 的全量群消息监听。 -- 跨渠道统一 Conversation Adapter 或新增渠道会话表。 -- Agent 动态修改 Activation Gate。 -- 在模型调用前进行独立相关性分类。 -- 用 `NO_REPLY` 抑制飞书私聊、Web Chat、原生群或其他渠道。 -- 删除内部静默运行记录或原始群消息。 -- 为本功能引入新的第三方依赖。 From 511f8a3a585af1202f805f56d3f29363725dba00 Mon Sep 17 00:00:00 2001 From: Y1fe1Zh0u Date: Tue, 25 Aug 2026 20:03:47 +0800 Subject: [PATCH 3/3] Make repository quality expectations executable Align repository and subsystem guidance with the current ownership model, then add reproducible backend and frontend quality tools so contributors can run the documented checks locally. Constraint: Preserve existing product behavior and introduce no runtime dependencies. Rejected: Format or lint the entire existing frontend in this change | the baseline contains over one thousand lint findings and broad formatting churn would obscure the tooling change Confidence: high Scope-risk: narrow Reversibility: clean Directive: Treat current Pyright, ESLint, and Prettier findings as an explicit adoption baseline; tighten or fix them in reviewed increments Tested: frontend npm test (122 passed); frontend npm run build; Pyright 1.1.411 invocation; uv lock --check; ESLint and Prettier configuration loading; git diff --check Not-tested: Full backend pytest suite; existing Pyright, ESLint, and Prettier findings are not resolved --- AGENTS.md | 33 +- backend/AGENTS.md | 224 ++++-- backend/pyproject.toml | 5 + frontend/.prettierignore | 2 + frontend/AGENTS.md | 6 + frontend/eslint.config.js | 23 + frontend/package-lock.json | 1469 +++++++++++++++++++++++++++++++++--- frontend/package.json | 10 + 8 files changed, 1581 insertions(+), 191 deletions(-) create mode 100644 frontend/.prettierignore create mode 100644 frontend/eslint.config.js diff --git a/AGENTS.md b/AGENTS.md index 80db9757a..d7c7fd46f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,32 @@ Each behavior-driving fact has one authoritative owner. Other layers may submit - **Tests enforce behavior, not product truth.** A passing test proves that the implementation matches its asserted behavior; it does not prove that the asserted behavior matches the current product or architecture contract. Update obsolete tests together with an explicitly approved contract change, and never change an expectation merely to make a failure disappear. - **Non-trivial changes keep code, Agent Notes, and commit history aligned.** Any change to behavior, architecture, a shared contract, Runtime semantics, persistence, security, permissions, compatibility, or engineering process must add or update its owning Agent Note in the same change. The code implements the decision, the Agent Note owns its durable rationale and current contract, and the commit message records the intent, scope, and verification of this change. These three records must not contradict one another. Update an existing owning note instead of creating a duplicate; only mechanical or strictly local changes are exempt. -## 3. Type Checking +## 3. Change Discipline + +- Keep each change scoped to one intent. Do not mix structural refactoring, + behavior changes, compatibility work, and unrelated cleanup. +- Preserve verified behavior unless the task explicitly changes the owning + product or architecture contract. +- Before introducing an abstraction, identify the current owner and consumer. + Delete obsolete code, reuse the existing owner when it already fits, and move + misplaced behavior back to that owner while removing bypass paths. Add a new + layer only when it has an independently changing responsibility and a current + consumer. +- **Delete verified dead code.** Once code, configuration, tests, compatibility + paths, or documentation are confirmed to have no current contract or + production consumer, remove them in the same change. Do not keep + commented-out implementations, speculative fallbacks, or tests that only + preserve deleted behavior. +- Preserve unrelated working-tree changes and user-owned files. +- Use repository-relative paths in code, documentation, and instructions. +- When ownership or a boundary changes, update the nearest path-specific + `AGENTS.md` and the corresponding durable documentation. +- Do not add fallback or compatibility paths without a documented reason, + regression coverage, and a removal condition. +- Keep source facts, test evidence, CI evidence, deployment evidence, and + live-system evidence clearly separated. + +## 4. Type Checking Everything compiles under `strict: true` with `noImplicitAny`; every remaining `any` explains why narrowing is infeasible. @@ -62,14 +87,14 @@ Every new or changed automated rule must include positive and negative coverage: valid cases pass, and representative invalid cases fail for the intended reason. -## 4. Quick Command Reference +## 5. Quick Command Reference Dev and test commands live in sub-project instruction files: - Backend: `backend/AGENTS.md` (Server start, Alembic migrations, Pytest, Ruff) - Frontend: `frontend/AGENTS.md` (Vite dev server, type-check, lint, build) -## 5. Failure Diagnosis and Handling +## 6. Failure Diagnosis and Handling When a command fails: @@ -90,7 +115,7 @@ Do not: - Modify product code to accommodate the current machine before evidence shows that the environment is the failing layer and that a product-level portability change is required. - Dismiss a test failure as an environment problem before collecting environment evidence and ruling out a product-code regression. -## 6. Verification +## 7. Verification After code changes, verification scope is determined by the affected contracts and consumers, not by the number of modified files. Cross-layer changes must follow the real execution path and update and verify every affected layer; local changes require only local evidence. diff --git a/backend/AGENTS.md b/backend/AGENTS.md index 9277d876e..42ff8875f 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -1,75 +1,175 @@ -# Backend AGENTS.md — Clawith Backend Guidelines +# AGENTS.md — Clawith Backend ---- +These backend-specific rules apply to `backend/**` and supplement the +repository-wide [conventions](../AGENTS.md#2-conventions). -## 1. Subsystem Overview +The Backend is a Python 3.11+ FastAPI application built on SQLAlchemy's +asynchronous APIs, PostgreSQL, Redis, and LangGraph with PostgreSQL +checkpoints. It contains the Agent Runtime, product APIs, persistence, +background execution, and external integrations. -**Stack**: Python 3.11+, FastAPI, SQLModel (SQLAlchemy 2.0+), Alembic, LangGraph, Celery / Worker processes, Pytest. -**Root Spec**: Extended from root [`AGENTS.md`](file:///Users/alex/Documents/Code/dataelem/Clawith/AGENTS.md). +Project metadata and dependency declarations are defined in `pyproject.toml`; +`uv.lock` records the resolved dependency graph. ---- +## Commands -## 2. Common Commands - -From `backend/` directory: +Run Backend commands from `backend/`: | Action | Command | |---|---| -| Run Dev Server | `uv run uvicorn app.main:app --reload --port 8000` | -| Run Unit Tests | `uv run pytest` | -| Run Specific Test File | `uv run pytest tests/test_agent_runtime.py` | -| Run Linter / Format Check | `uv run ruff check .` | -| Run Auto-Fix Linter | `uv run ruff check --fix .` | -| Generate DB Migration | `uv run alembic revision --autogenerate -m "description"` | -| Apply DB Migrations | `uv run alembic upgrade head` | - ---- - -## 3. Python Coding Standards - -### 3.1 Import Placement -- **File Header Placement**: All Python imports MUST be placed at the top of the file (file header). -- **No Inline Imports**: Avoid inline/local imports within functions or methods unless strictly necessary (e.g., to break circular import dependencies). - -### 3.2 Multi-Tenant Scope (P0 - C2) -- **Mandatory Tenant Filter**: Every database query (`select(...)`), update, or delete MUST explicitly include `tenant_id` scoping to guarantee data isolation. -- **Worker & Context Var**: Ensure background tasks propagate tenant context correctly. - -### 3.3 Code Formatting & Type Safety -- **Ruff Compliance**: Code must adhere to Ruff rules (max line length: 120, target-version: `py311`). -- **Type Annotations**: All public functions and endpoint handlers must include explicit type hints for parameters and return values. - -### 3.4 Code Splitting Guidelines (C6) -- **Function Length Recommendation**: Recommended ~**100 lines** per function. Treat functions exceeding this size as candidates for refactoring into sub-functions or helper modules (flexible guideline). -- **File Length Recommendation**: Backend Python files recommended ~**1000 lines**. Split oversized files into modular sub-files when reasonable. - -### 3.5 Anti-Reinvention & Helper Layer (C6) -- **Search Before Coding**: Check `app/core/`, `app/utils/`, and `app/helpers/` before writing custom helper/utility functions. -- **Extract Common Logic**: Promote reusable operations (formatting, ID generation, string manipulation) into shared `utils/helpers` modules. - -### 3.6 Database & Query Performance (C5) -- **No Physical Foreign Keys**: Do not define physical `FOREIGN KEY` constraints at the DB layer. Keep relationship checks at the SQLModel / application layer. -- **Minimize DB JOINs & N+1 Prevention**: Avoid multi-table complex JOINs. Use batch query interfaces (`where(Model.id.in_(ids))` / batch APIs) and `selectinload` to prevent N+1 loop queries. - ---- - -## 4. Subsystem Layout & Architectural Invariants +| Install project and development dependencies | `uv sync --extra dev` | +| Run the development server | `uv run uvicorn app.main:app --reload --port 8000` | +| Run a focused test file | `uv run --extra dev pytest tests/.py` | +| Run the complete Backend test suite | `uv run --extra dev pytest` | +| Run lint checks | `uv run --extra dev ruff check .` | +| Run static type checks | `uv run --extra dev pyright app` | +| Apply database migrations | `uv run alembic upgrade head` | + +Use focused Pytest targets during development. Run the complete Backend suite +only when the affected contracts cross multiple Backend areas or when required +by the repository testing policy. + +Read [`alembic/AGENTS.md`](alembic/AGENTS.md) before creating or editing a +database migration. + +## Application layout + +```text +pyproject.toml Project metadata, dependencies, and tool configuration. +uv.lock Locked Python dependency graph. +alembic/ Database schema migrations. +scripts/ Repository-operated Backend maintenance and data-migration scripts. +tests/ Backend unit, contract, integration, and regression tests. +app/main.py FastAPI application composition, lifespan, middleware, and router + registration. +app/config.py Application configuration entry point. +app/database.py + Database engine and Session infrastructure. +app/api/ HTTP and WebSocket transport adapters. +app/schemas/ Request, response, and transport validation models. +app/models/ SQLAlchemy persistence models. +app/dao/ Database access and query ownership. +app/services/ Product services, Runtime capabilities, background execution, + and external integrations. +app/core/ Cross-cutting security, permissions, errors, events, logging, and + middleware. +app/scripts/ Application maintenance, bootstrap, backfill, and migration tools. +``` + +Read the nearest nested `AGENTS.md` before modifying a specialized subtree. +Detailed module structure belongs to that subtree's instruction or owning +architecture document, not this file. + +## Async lifecycle + +Represent one asynchronous operation with one lifecycle controller or +transaction. Readiness, cancellation, disposal, reservation, and sentinel state +remain in that owner unless they describe an independently owned object or +settlement point. Do not split one operation into parallel lifecycle state +machines. + +## Lifecycle verification + +Tests for registration, cancellation, shutdown, and cleanup must observe the +owned resource reaching its terminal or removed state. Asserting only that +`cancel()`, `close()`, `dispose()`, or a cleanup callback was invoked is not +sufficient evidence that work stopped or resources were released. + +## API and service boundaries + +API handlers are transport adapters. They parse and validate request data, +establish the authenticated and authorized caller, pass explicit inputs to the +owning service or command-intake boundary, and map the result to the transport +response. Do not put business orchestration, ORM queries, Runtime node calls, +checkpoint mutation, or private lifecycle control into an API handler. + +Design shared service contracts for all current consumers. Keep transport-, +UI-, channel-, and provider-specific behavior in the owning adapter or consumer. +Do not widen a public service for one internal caller; keep single-consumer +capabilities private until a real shared contract exists. + +## Public choices + +Do not invent public defaults, modes, operation sets, API fields, event fields, +or persisted formats merely to make an interface appear flexible. Every public +choice must be supported by a current consumer, an owning product or +architecture contract, or established behavior already used by the system. + +When that evidence does not exist, require the caller to provide an explicit +value or defer the choice instead of introducing a speculative default or +extension point. + +## Model-facing contracts + +Write prompts, Tool schemas, Tool results, and model-visible diagnostics from +the model's task perspective. Include the information needed to choose and +complete the next action; do not expose UI state, transport details, database +structure, internal service names, or implementation vocabulary unless the +model must act on that concept. + +A failure on a model-visible path must return a bounded, actionable result that +identifies the failed subject, the relevant condition, and any safe next action. +Do not silently drop the failure or dump stack traces, raw provider responses, +internal records, or unbounded diagnostic output into model context. + +Treat stable model-visible wording and schemas as behavior. Changes require an +update to the owning contract and verification through the assembled model +request or Tool execution path. + +## Enforcement + +The operation that reads protected data, mutates authoritative state, or causes +an external side effect must obtain and enforce authorization, tenant scope, +limits, and policy decisions from the owning Backend permission model at that +execution boundary. Upstream layers may perform an equivalent preflight for +faster feedback, but Frontend visibility, prompt instructions, Tool-schema +omission, API wrappers, and ordinary call ordering are user-experience guidance, +not security enforcement. + +Tests for a denial rule must exercise the real executor or mutation boundary, +including relevant alternate callers that could bypass an upstream check. + +## Independent outcomes + +Report independent execution outcomes as separate facts. Acceptance, execution, +persistence, synchronization, delivery, timeout, cancellation, and cleanup may +coexist; do not collapse them into one success flag or infer one outcome from +another. + +## Public result contracts + +A public Backend contract has one documented success, failure, cancellation, +and uncertain-outcome model. Adapters normalize provider-, transport-, worker-, +and implementation-specific result forms at the owning boundary before +returning them to consumers. + +Consumers depend only on the normalized contract and must not guess whether the +same outcome arrives through an exception, status field, terminal event, empty +value, or transport closure. Preserve internal defects as internal failures +instead of misclassifying them as ordinary provider or business outcomes. + +Test every supported source form through the real consumer-facing boundary. + +## State publication -- `app/api/`: FastAPI endpoints & HTTP/WS adapters. - - **Rule**: Must NOT invoke LangGraph node executors directly. Must submit commands through `RuntimeCommandIntake`. Must NOT write raw ORM queries; delegate to `app/dao/`. -- `app/dao/`: Data Access Objects (Detailed guidelines → [`app/dao/AGENTS.md`](file:///Users/alex/Documents/Code/dataelem/Clawith/backend/app/dao/AGENTS.md)). - - **Rule**: Exclusive owner of database queries and persistence. Must enforce `tenant_id` scope. -- `app/services/agent_runtime/`: Core execution boundary. - - `command_worker.py`: Claims durable commands and executes graph turns. - - `graph.py`: LangGraph graph topology definition. -- `app/models/`: SQLModel data models. -- `app/services/`: Product domain logic services. +Publish events, notifications, cache updates, projections, and user-visible +state only after the authoritative operation reaches its documented commit +point. A prepared, accepted, queued, or attempted operation is not a committed +outcome. ---- +Derived state must be rebuilt or updated from the authoritative committed fact, +not from an optimistic side path. When an external side effect has an uncertain +outcome, record and reconcile that uncertainty instead of publishing success or +blindly repeating the operation. -## 5. Testing Conventions +## Complete-operation bounds -- Place unit and integration tests under `tests/`. -- Name test files with `test_` prefix (e.g., `tests/test_runtime_intake.py`). -- Use `@pytest.mark.asyncio` for async test functions. +Apply item, byte, token, time, and concurrency limits at the owner of the +complete returned, persisted, queued, or model-visible result. Include wrappers, +metadata, retries, pagination assembly, and encoded representations when +evaluating the bound; a limit on one intermediate step is not a complete +operation bound. +Test limits below, at, and above the boundary, including one oversized item and +multi-byte text where byte limits apply. Reject or truncate only according to +the owning contract, and report truncation explicitly. diff --git a/backend/pyproject.toml b/backend/pyproject.toml index d4d305883..de1ebb265 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -72,5 +72,10 @@ asyncio_mode = "auto" requires = ["setuptools>=68.0", "wheel"] build-backend = "setuptools.build_meta" +[dependency-groups] +dev = [ + "pyright>=1.1.411", +] + [tool.setuptools.packages.find] include = ["app*"] diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 000000000..009af5438 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,2 @@ +dist +coverage diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index b36020fbc..3a7116ffe 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -43,3 +43,9 @@ From `frontend/` directory: - **Design System**: Use Tailwind CSS and shadcn/ui components for consistent design tokens. - **Responsive Layout**: Ensure layouts adapt gracefully to desktop and mobile viewports. - **Micro-Interactions**: Use smooth CSS transitions and hover states for interactive elements. + +--- + +## 5. Lifecycle Ownership + +Frontend-specific lifecycle ownership and cleanup rules will be defined here. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 000000000..17f0c8f29 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from "@eslint/js"; +import { defineConfig, globalIgnores } from "eslint/config"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +export default defineConfig([ + globalIgnores(["dist"]), + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9d24691dc..b78bb76bc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,10 +24,17 @@ "zustand": "^5.0.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.0.0", + "eslint": "^10.9.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.4", + "globals": "^17.11.0", + "prettier": "^3.9.6", "typescript": "^5.0.0", + "typescript-eslint": "^8.68.0", "vite": "^6.0.0" } }, @@ -62,7 +69,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -765,6 +771,200 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1323,8 +1523,7 @@ } ], "hasInstallScript": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@tsparticles/interaction-external-attract": { "version": "3.9.1", @@ -1845,6 +2044,13 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1852,13 +2058,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1879,161 +2091,466 @@ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz", + "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/type-utils": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": "^14.18.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "@typescript-eslint/parser": "^8.68.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 4" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@typescript-eslint/parser": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz", + "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==", + "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz", + "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.68.0", + "@typescript-eslint/types": "^8.68.0", + "debug": "^4.4.3" }, "engines": { - "node": ">=6.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz", + "integrity": "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" - }, - "bin": { - "browserslist": "cli.js" + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz", + "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001774", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", - "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz", + "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/@typescript-eslint/types": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz", + "integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz", + "integrity": "sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==", + "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@typescript-eslint/project-service": "8.68.0", + "@typescript-eslint/tsconfig-utils": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/visitor-keys": "8.68.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">=7.0.0" - } - }, + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz", + "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.68.0", + "@typescript-eslint/types": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz", + "integrity": "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.68.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", @@ -2060,6 +2577,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -2221,6 +2753,13 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -2292,14 +2831,277 @@ "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.4.tgz", + "integrity": "sha512-7bqTKz7T0r+HKWFarNXByDE9/5+73wI2ru+M3zuqGbR7s/b/5/pQJXZoufWlrngqGqoZto73ZkGumCdLxk+4rw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=6" + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" } }, "node_modules/eventemitter3": { @@ -2308,6 +3110,27 @@ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2326,6 +3149,19 @@ } } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", @@ -2339,6 +3175,27 @@ "node": ">=8" } }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2373,6 +3230,49 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -2401,7 +3301,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.26.10" }, @@ -2423,6 +3322,16 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/immer": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", @@ -2433,6 +3342,16 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -2442,6 +3361,16 @@ "node": ">=12" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2451,6 +3380,26 @@ "node": ">=8" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2471,6 +3420,27 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2484,6 +3454,30 @@ "node": ">=6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", @@ -2506,6 +3500,22 @@ "yallist": "^3.0.2" } }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2532,6 +3542,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -2539,6 +3556,24 @@ "dev": true, "license": "MIT" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", @@ -2584,6 +3619,16 @@ "node": ">=8" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2597,7 +3642,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2643,6 +3687,42 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -2665,7 +3745,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2675,7 +3754,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2721,7 +3799,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -2822,8 +3899,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -2928,6 +4004,29 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2987,13 +4086,38 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3002,6 +4126,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.68.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz", + "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.68.0", + "@typescript-eslint/parser": "8.68.0", + "@typescript-eslint/typescript-estree": "8.68.0", + "@typescript-eslint/utils": "8.68.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -3033,6 +4181,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -3070,7 +4228,6 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -3149,12 +4306,38 @@ "node": ">=0.10.0" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-module": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -3217,6 +4400,42 @@ "node": ">=6" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "5.0.11", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index 032bf74c3..17ddfdbb3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,9 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", "test": "node --test tests/*.test.mjs", "preview": "vite preview" }, @@ -27,10 +30,17 @@ "zustand": "^5.0.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.0.0", + "eslint": "^10.9.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.4", + "globals": "^17.11.0", + "prettier": "^3.9.6", "typescript": "^5.0.0", + "typescript-eslint": "^8.68.0", "vite": "^6.0.0" } }