diff --git a/.github/workflows/agents-auto-pilot.yml b/.github/workflows/agents-auto-pilot.yml index 3de7c28b..c6ca1e31 100644 --- a/.github/workflows/agents-auto-pilot.yml +++ b/.github/workflows/agents-auto-pilot.yml @@ -150,7 +150,7 @@ jobs: - name: Set up Node if: steps.check_enabled.outputs.enabled == 'true' - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: node-version: '20' diff --git a/.github/workflows/agents-keepalive-loop.yml b/.github/workflows/agents-keepalive-loop.yml index 1defd0cf..15ec9593 100644 --- a/.github/workflows/agents-keepalive-loop.yml +++ b/.github/workflows/agents-keepalive-loop.yml @@ -74,7 +74,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: node-version: 20 @@ -492,7 +492,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: node-version: 20 @@ -614,7 +614,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Node.js - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6 with: node-version: 20 diff --git a/.github/workflows/maint-76-claude-code-review.yml b/.github/workflows/maint-76-claude-code-review.yml index a1abc5e8..4b63894f 100644 --- a/.github/workflows/maint-76-claude-code-review.yml +++ b/.github/workflows/maint-76-claude-code-review.yml @@ -188,7 +188,7 @@ jobs: - name: Run Claude Code Review id: claude continue-on-error: true - uses: anthropics/claude-code-action@26ec041249acb0a944c0a47b6c0c13f05dbc5b44 + uses: anthropics/claude-code-action@220272d38887a1caed373da96a9ffdb0919c26cc with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} allowed_bots: '*' diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..bfd3f27e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,87 @@ +# AGENTS.md - Consumer Repository Context + +> Read this before changing workflows, prompts, or synced automation files. + +## This Is A Consumer Repo + +Most workflow logic for this repository lives in `stranske/Workflows`. The consumer repo should only carry repo-specific configuration unless it has an explicitly documented exception. + +## Source Of Truth + +For infrastructure work, follow this order: + +1. `stranske/Workflows` root docs: `README.md`, `docs/WORKFLOW_GUIDE.md`, `docs/ci/WORKFLOWS.md` +2. `stranske/Workflows/docs/INTEGRATION_GUIDE.md` and `docs/ops/CONSUMER_REPO_MAINTENANCE.md` +3. The consumer sync source in `stranske/Workflows/templates/consumer-repo/` +4. This repo's local repo-specific files + +If a file is synced from Workflows, fix it in Workflows first. + +## Current Consumer Defaults + +- First-party consumers currently reference reusable workflows with `@main`. Match that unless you are intentionally pinning to an exact commit SHA for a controlled reason. +- `ci.yml` and `autofix-versions.env` are repo-specific. +- `pr-00-gate.yml` is a create-only standard file. Keep it aligned with the standard gate unless this repo has a documented reason to diverge. +- Synced workflows, prompts, scripts, and consumer docs are managed through `.github/sync-manifest.yml` in Workflows. + +## Commonly Managed Files + +Usually edit locally only when the file is repo-specific: + +| File | Default owner | Notes | +|------|---------------|-------| +| `ci.yml` | Consumer repo | Repo-specific CI wiring | +| `autofix-versions.env` | Consumer repo | Repo-specific dependency pins | +| `pr-00-gate.yml` | Consumer repo, but should match Workflows standard by default | Create-only standard file | +| `agents-*.yml` | Workflows | Fix in Workflows, not here | +| `autofix.yml` | Workflows | Fix in Workflows, not here | +| `.github/codex/` prompts | Workflows | Fix in Workflows, not here | +| synced scripts/docs | Workflows | Fix in Workflows, not here | + +## Current Workflow Surfaces + +The current consumer default automation surface is centered on: + +- `agents-issue-intake.yml` +- `agents-80-pr-event-hub.yml` +- `agents-81-gate-followups.yml` +- `agents-verifier.yml` +- `autofix.yml` +- `ci.yml` +- `pr-00-gate.yml` + +Legacy compatibility workflows may still exist during migrations. Do not assume an older filename is canonical without checking the Workflows docs first. + +## Cross-Repo Policy + +Before editing local workflow infrastructure, ask: + +**Does this work belong in `stranske/Workflows` instead?** + +The answer is usually yes if the change affects any of these: + +- reusable workflows +- agent prompts or routing +- keepalive/autofix/verifier behavior +- synced workflow files +- synced scripts or docs + +If yes: + +1. Make the source-of-truth change in `stranske/Workflows` +2. Update the sync manifest if a consumer-facing file changed +3. Sync or manually align this repo afterward + +## Useful References + +- `stranske/Workflows/README.md` +- `stranske/Workflows/docs/WORKFLOW_GUIDE.md` +- `stranske/Workflows/docs/ci/WORKFLOWS.md` +- `stranske/Workflows/docs/INTEGRATION_GUIDE.md` +- `stranske/Workflows/docs/ops/CONSUMER_REPO_MAINTENANCE.md` +- `stranske/Workflows/docs/keepalive/Agents.md` +- `stranske/Travel-Plan-Permission` as a reference consumer + +## Agent-Specific Note + +This file is the agent-generic contract. Keep it materially aligned with `CLAUDE.md`; differences between the two should only be agent-specific execution notes, not different repository rules. diff --git a/CLAUDE.md b/CLAUDE.md index 685aac40..40def435 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,171 +1,87 @@ # CLAUDE.md - Consumer Repository Context -> **READ THIS FIRST** before making workflow changes. +> Read this before changing workflows, prompts, or synced automation files. -## This is a Consumer Repo +## This Is A Consumer Repo -This repository uses the **stranske/Workflows** workflow library. Most workflow logic lives there, not here. +Most workflow logic for this repository lives in `stranske/Workflows`. The consumer repo should only carry repo-specific configuration unless it has an explicitly documented exception. -**DO NOT** modify agent workflow files directly - they are synced from Workflows and will be overwritten. +## Source Of Truth -## Architecture +For infrastructure work, follow this order: -``` -stranske/Workflows (central library) - │ - │ reusable workflows called via: - │ uses: stranske/Workflows/.github/workflows/reusable-*.yml@v1 - │ - ▼ -This Repo (consumer) - .github/workflows/ - ├── agents-*.yml → SYNCED from Workflows (don't edit) - ├── autofix.yml → SYNCED from Workflows (don't edit) - ├── pr-00-gate.yml → SYNCED but customizable - ├── ci.yml → REPO-SPECIFIC (can edit) - └── autofix-versions.env → REPO-SPECIFIC (can edit) -``` +1. `stranske/Workflows` root docs: `README.md`, `docs/WORKFLOW_GUIDE.md`, `docs/ci/WORKFLOWS.md` +2. `stranske/Workflows/docs/INTEGRATION_GUIDE.md` and `docs/ops/CONSUMER_REPO_MAINTENANCE.md` +3. The consumer sync source in `stranske/Workflows/templates/consumer-repo/` +4. This repo's local repo-specific files -## Which Files Can Be Edited +If a file is synced from Workflows, fix it in Workflows first. -| File | Editable? | Notes | -|------|-----------|-------| -| `ci.yml` | ✅ Yes | Repo-specific CI configuration | -| `autofix-versions.env` | ✅ Yes | Repo-specific dependency versions | -| `pr-00-gate.yml` | ⚠️ Careful | Synced, but can customize if needed | -| `agents-*.yml` | ❌ No | Synced from Workflows | -| `autofix.yml` | ❌ No | Synced from Workflows | +## Current Consumer Defaults -## Keepalive System +- First-party consumers currently reference reusable workflows with `@main`. Match that unless you are intentionally pinning to an exact commit SHA for a controlled reason. +- `ci.yml` and `autofix-versions.env` are repo-specific. +- `pr-00-gate.yml` is a create-only standard file. Keep it aligned with the standard gate unless this repo has a documented reason to diverge. +- Synced workflows, prompts, scripts, and consumer docs are managed through `.github/sync-manifest.yml` in Workflows. -When an issue is labeled `agent:codex`: -1. `agents-63-issue-intake.yml` creates a PR with bootstrap file -2. `agents-keepalive-loop.yml` runs Codex in iterations -3. Codex works through tasks in PR body until all complete +## Commonly Managed Files -**Key prompts** (in `.github/codex/prompts/`): -- `keepalive_next_task.md` - Normal work instructions -- `fix_ci_failures.md` - CI fix instructions +Usually edit locally only when the file is repo-specific: -## Common Issues +| File | Default owner | Notes | +|------|---------------|-------| +| `ci.yml` | Consumer repo | Repo-specific CI wiring | +| `autofix-versions.env` | Consumer repo | Repo-specific dependency pins | +| `pr-00-gate.yml` | Consumer repo, but should match Workflows standard by default | Create-only standard file | +| `agents-*.yml` | Workflows | Fix in Workflows, not here | +| `autofix.yml` | Workflows | Fix in Workflows, not here | +| `.github/codex/` prompts | Workflows | Fix in Workflows, not here | +| synced scripts/docs | Workflows | Fix in Workflows, not here | -### Workflow fails with "workflow file issue" -- A reusable workflow is being called that doesn't exist -- Check Workflows repo has the required `reusable-*.yml` file -- Consumer workflows call INTO Workflows repo, not local files +## Current Workflow Surfaces -### Workflow startup_failure (zero jobs) +The current consumer default automation surface is centered on: -**MANDATORY**: Before theorizing, find what changed: +- `agents-issue-intake.yml` +- `agents-80-pr-event-hub.yml` +- `agents-81-gate-followups.yml` +- `agents-verifier.yml` +- `autofix.yml` +- `ci.yml` +- `pr-00-gate.yml` -```bash -# 1. Find boundary between success and failure -gh run list --repo owner/repo --workflow="workflow.yml" --limit 100 --json databaseId,conclusion,createdAt \ - --jq '[.[] | select(.conclusion == "success" or .conclusion == "startup_failure")] | group_by(.conclusion) | .[] | {conclusion: .[0].conclusion, first: .[-1].createdAt, last: .[0].createdAt}' +Legacy compatibility workflows may still exist during migrations. Do not assume an older filename is canonical without checking the Workflows docs first. -# 2. Find commits between last success and first failure -git log --oneline --since="LAST_SUCCESS_DATE" --until="FIRST_FAILURE_DATE" -- path/to/workflow.yml +## Cross-Repo Policy -# 3. Fix ONLY what that diff changed -git show COMMIT_SHA -- path/to/workflow.yml -``` +Before editing local workflow infrastructure, ask: -Common causes: -- **Top-level `permissions:` on hybrid workflows** (workflow_call + workflow_dispatch) -- Invalid YAML syntax -- Invalid permission scopes +**Does this work belong in `stranske/Workflows` instead?** -See `docs/INTEGRATION_GUIDE.md` in Workflows repo. +The answer is usually yes if the change affects any of these: -### Agent not picking up changes -- Check PR has `agent:codex` label -- Check Gate workflow passed (green checkmark) -- Check PR body has unchecked tasks +- reusable workflows +- agent prompts or routing +- keepalive/autofix/verifier behavior +- synced workflow files +- synced scripts or docs -### Need to update agent workflows -- DON'T edit locally - changes will be overwritten -- Fix in Workflows repo → sync will propagate here -- Or request manual sync: `gh workflow run maint-68-sync-consumer-repos.yml --repo stranske/Workflows` +If yes: -## Reference Implementation +1. Make the source-of-truth change in `stranske/Workflows` +2. Update the sync manifest if a consumer-facing file changed +3. Sync or manually align this repo afterward -**Travel-Plan-Permission** is the reference consumer repo. When debugging: -1. Check if it works there first -2. Compare this repo's `.github/` with Travel-Plan-Permission -3. Look for missing files or differences +## Useful References -## Workflows Documentation +- `stranske/Workflows/README.md` +- `stranske/Workflows/docs/WORKFLOW_GUIDE.md` +- `stranske/Workflows/docs/ci/WORKFLOWS.md` +- `stranske/Workflows/docs/INTEGRATION_GUIDE.md` +- `stranske/Workflows/docs/ops/CONSUMER_REPO_MAINTENANCE.md` +- `stranske/Workflows/docs/keepalive/Agents.md` +- `stranske/Travel-Plan-Permission` as a reference consumer -For detailed docs, see **stranske/Workflows**: -- `docs/INTEGRATION_GUIDE.md` - How consumer repos work -- `docs/keepalive/GoalsAndPlumbing.md` - Keepalive system design -- `docs/keepalive/SETUP_CHECKLIST.md` - Required files and secrets +## Claude-Specific Note -## Quick Debug Commands - -```bash -# Compare workflows with reference repo -diff .github/workflows/autofix.yml \ - <(gh api repos/stranske/Travel-Plan-Permission/contents/.github/workflows/autofix.yml --jq '.content' | base64 -d) - -# Check for missing files -gh api repos/stranske/Travel-Plan-Permission/contents/.github/workflows --jq '.[].name' | sort > /tmp/tpp.txt -ls .github/workflows/ | sort > /tmp/here.txt -diff /tmp/tpp.txt /tmp/here.txt - -# Trigger workflow sync from Workflows repo -gh workflow run maint-68-sync-consumer-repos.yml --repo stranske/Workflows -``` - ---- - -## �� POLICY: Cross-Repo Work - -> **CRITICAL**: Read this before ANY work that might affect the Workflows repo. - -### Policy Checkpoint Trigger - -When creating a todo list, ask: - -**"Does this work need changes in stranske/Workflows?"** - -Signs that you need Workflows changes: -- Adding a new agent capability -- Modifying how keepalive/autofix/verifier works -- Needing a new Codex prompt -- Bug in a reusable workflow - -### If YES → Work in Workflows First - -1. Clone/checkout stranske/Workflows -2. Make changes there (following Workflows CLAUDE.md policy) -3. Ensure sync manifest is updated -4. Trigger sync to propagate to this repo -5. Then verify in this repo - -**DO NOT** try to fix Workflows issues by editing local files - they will be overwritten on next sync. - -### Add Policy Verification Todo - -When your todo list involves cross-repo coordination, add as **FINAL** item: - -``` -✅ Verify cross-repo policy compliance: - - [ ] Changes made in correct repo (Workflows vs Consumer) - - [ ] Sync triggered if needed - - [ ] Both repos have passing CI -``` - -### Quick Commands - -```bash -# Check if a file is synced (compare to template) -diff .github/workflows/agents-keepalive-loop.yml \ - <(gh api repos/stranske/Workflows/contents/templates/consumer-repo/.github/workflows/agents-keepalive-loop.yml --jq '.content' | base64 -d) - -# Trigger sync from Workflows -gh workflow run maint-68-sync-consumer-repos.yml --repo stranske/Workflows -f repos="${{ github.repository }}" - -# Check sync manifest for what SHOULD be here -gh api repos/stranske/Workflows/contents/.github/sync-manifest.yml --jq '.content' | base64 -d -``` +Keep this file materially aligned with `AGENTS.md`. Differences between the two should only be agent-specific execution notes, not different repository rules. diff --git a/WORKFLOW_USER_GUIDE.md b/WORKFLOW_USER_GUIDE.md index 64982f7a..2ebf1931 100644 --- a/WORKFLOW_USER_GUIDE.md +++ b/WORKFLOW_USER_GUIDE.md @@ -754,17 +754,17 @@ The Workflows repository includes maintenance workflows that handle sync, update --- -### `maint-61-create-floating-v1-tag.yml` - Update Floating Tag -**Purpose:** Updates `v1` tag to latest `v1.x.x` release +### `maint-73-refresh-reusable-tags.yml` - Legacy Tag Refresh Notice +**Purpose:** Historical maintenance workflow for floating-tag management **Trigger:** After new release created **What It Does:** -- Finds latest v1 series release -- Updates `v1` tag to point to it -- Enables consumers to use `@v1` for latest +- Records that first-party consumers now standardize on `@main` +- Leaves any historical floating-tag maintenance to explicit migration work +- Does not change the current first-party consumer default -**Use When:** After any v1.x.x release +**Use When:** Only when you are auditing historical versioning behavior --- diff --git a/scripts/langchain/integration_layer.py b/scripts/langchain/integration_layer.py index a97b58c6..839ed8f3 100755 --- a/scripts/langchain/integration_layer.py +++ b/scripts/langchain/integration_layer.py @@ -8,13 +8,12 @@ import re from collections.abc import Iterable, Mapping, Sequence from dataclasses import dataclass, field -from importlib import import_module from typing import Any try: - label_matcher = import_module("scripts.langchain.label_matcher") + from scripts.langchain import label_matcher except ModuleNotFoundError: - label_matcher = import_module("label_matcher") + import label_matcher @dataclass @@ -67,7 +66,7 @@ def _build_issue_text(issue: IssueData) -> str: return "\n\n".join(parts) -def _build_label_store(labels: Iterable[Any]) -> Any: +def _build_label_store(labels: Iterable[Any]) -> label_matcher.LabelVectorStore | None: label_records = _collect_label_records(labels) if not label_records: return None @@ -85,7 +84,7 @@ def _build_label_store(labels: Iterable[Any]) -> Any: ) -def _collect_label_records(labels: Iterable[Any]) -> list[Any]: +def _collect_label_records(labels: Iterable[Any]) -> list[label_matcher.LabelRecord]: if labels is None: raise ValueError("labels must be an iterable of label records, not None.") if isinstance(labels, (str, bytes)): @@ -93,7 +92,7 @@ def _collect_label_records(labels: Iterable[Any]) -> list[Any]: if not isinstance(labels, Iterable): raise ValueError("labels must be an iterable of label records.") - records: list[Any] = [] + records: list[label_matcher.LabelRecord] = [] for index, item in enumerate(labels): record = _coerce_label_record(item) if record is not None: @@ -107,7 +106,7 @@ def _collect_label_records(labels: Iterable[Any]) -> list[Any]: return records -def _coerce_label_record(item: Any) -> Any: +def _coerce_label_record(item: Any) -> label_matcher.LabelRecord | None: if isinstance(item, label_matcher.LabelRecord): return item if isinstance(item, (str, bytes)): @@ -135,7 +134,11 @@ def _coerce_label_record(item: Any) -> Any: ) -def _select_label_names(matches: Sequence[Any], *, max_labels: int | None = None) -> list[str]: +def _select_label_names( + matches: Sequence[label_matcher.LabelMatch], + *, + max_labels: int | None = None, +) -> list[str]: if not matches: return [] names: list[str] = [] diff --git a/scripts/langchain/issue_dedup.py b/scripts/langchain/issue_dedup.py index 33bbec88..44bf8639 100755 --- a/scripts/langchain/issue_dedup.py +++ b/scripts/langchain/issue_dedup.py @@ -9,13 +9,12 @@ import os from collections.abc import Iterable, Mapping from dataclasses import dataclass -from importlib import import_module from typing import Any try: - semantic_matcher = import_module("scripts.langchain.semantic_matcher") + from scripts.langchain import semantic_matcher except ModuleNotFoundError: - semantic_matcher = import_module("semantic_matcher") + import semantic_matcher @dataclass(frozen=True) @@ -91,7 +90,7 @@ def _issue_text(issue: IssueRecord) -> str: def build_issue_vector_store( issues: Iterable[Any], *, - client_info: Any = None, + client_info: semantic_matcher.EmbeddingClientInfo | None = None, model: str | None = None, ) -> IssueVectorStore | None: issue_records: list[IssueRecord] = [] diff --git a/scripts/langchain/issue_formatter.py b/scripts/langchain/issue_formatter.py index 2f4e3c6f..71fe6163 100755 --- a/scripts/langchain/issue_formatter.py +++ b/scripts/langchain/issue_formatter.py @@ -13,16 +13,13 @@ import json import re import sys -from importlib import import_module from pathlib import Path -from typing import Any, cast +from typing import Any try: - check_prompt_injection = import_module( - "scripts.langchain.injection_guard" - ).check_prompt_injection + from scripts.langchain.injection_guard import check_prompt_injection except ImportError: # pragma: no cover - fallback for direct invocation - check_prompt_injection = import_module("injection_guard").check_prompt_injection + from injection_guard import check_prompt_injection # Maximum issue body size to prevent OpenAI rate limit errors (30k TPM limit) # ~4 chars per token, so 50k chars ≈ 12.5k tokens, leaving headroom for prompt + output @@ -393,15 +390,15 @@ def _validate_and_refine_tasks(formatted: str, *, use_llm: bool) -> tuple[str, s return formatted, None try: - task_validator_module = import_module("scripts.langchain.task_validator") + from scripts.langchain import task_validator except ImportError: try: - task_validator_module = import_module("task_validator") + import task_validator except ImportError: return formatted, None # Run validation - result = task_validator_module.validate_tasks(tasks, context=formatted, use_llm=use_llm) + result = task_validator.validate_tasks(tasks, context=formatted, use_llm=use_llm) # If no changes, return original if set(result.tasks) == set(tasks) and len(result.tasks) == len(tasks): @@ -483,7 +480,7 @@ def format_issue_body(issue_body: str, *, use_llm: bool = True) -> dict[str, Any prompt = _load_prompt() template = ChatPromptTemplate.from_template(prompt) - chain: Any = template | cast(Any, client) + chain = template | client try: response = chain.invoke({"issue_body": issue_body}) except Exception as e: @@ -492,7 +489,7 @@ def format_issue_body(issue_body: str, *, use_llm: bool = True) -> dict[str, Any fallback_info = _get_llm_client(force_openai=True) if fallback_info: client, provider = fallback_info - chain = template | cast(Any, client) + chain = template | client response = chain.invoke({"issue_body": issue_body}) else: raise diff --git a/scripts/langchain/label_matcher.py b/scripts/langchain/label_matcher.py index 0344aaec..2d02b744 100755 --- a/scripts/langchain/label_matcher.py +++ b/scripts/langchain/label_matcher.py @@ -10,13 +10,12 @@ import re from collections.abc import Iterable, Mapping from dataclasses import dataclass -from importlib import import_module from typing import Any try: - semantic_matcher = import_module("scripts.langchain.semantic_matcher") + from scripts.langchain import semantic_matcher except ModuleNotFoundError: - semantic_matcher = import_module("semantic_matcher") + import semantic_matcher @dataclass(frozen=True) @@ -254,7 +253,7 @@ def _label_text(label: LabelRecord) -> str: def build_label_vector_store( labels: Iterable[Any], *, - client_info: Any = None, + client_info: semantic_matcher.EmbeddingClientInfo | None = None, model: str | None = None, ) -> LabelVectorStore | None: label_records: list[LabelRecord] = [] @@ -438,9 +437,9 @@ def find_similar_labels( search_fn = store.similarity_search_with_score score_type = "distance" else: - keyword_only_matches = _keyword_matches(label_store.labels, query, threshold=threshold) - keyword_only_matches.sort(key=lambda match: match.score, reverse=True) - return keyword_only_matches + matches = _keyword_matches(label_store.labels, query, threshold=threshold) + matches.sort(key=lambda match: match.score, reverse=True) + return matches limit = k or DEFAULT_LABEL_SIMILARITY_K try: diff --git a/scripts/langchain/semantic_matcher.py b/scripts/langchain/semantic_matcher.py index 2c0a9b8e..71585062 100755 --- a/scripts/langchain/semantic_matcher.py +++ b/scripts/langchain/semantic_matcher.py @@ -12,7 +12,6 @@ import os from collections.abc import Iterable from dataclasses import dataclass -from typing import Protocol from tools.embedding_provider import ( EmbeddingProvider, @@ -25,13 +24,9 @@ DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small" -class EmbeddingClient(Protocol): - def embed_documents(self, texts: list[str]) -> list[list[float]]: ... - - @dataclass class EmbeddingClientInfo: - client: EmbeddingClient + client: object provider: str model: str is_fallback: bool