From 7e339e16773815d63dbd4e2f1a0e6c354d51515f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Wed, 4 Feb 2026 20:25:59 +0100 Subject: [PATCH] feat: ai sdk integration (#299) * feat: ai sdk integration * more fixes * fix(security): mental model refresh tag-based security - Mental model refresh now passes tags with all_strict matching - Consolidation only triggers refresh for mental models with matching tags - Consolidation filters related observations by tags (all_strict) - Added tests to verify tag-based security boundaries - Updated OpenAPI spec to include tags and text_preview in list_documents - Added tags column to documents UI table * chore: regenerate OpenAPI spec after rebase * fix: improve consolidation prompt for contradiction handling and mental model refresh security - Enhanced consolidation prompt to be more explicit about capturing temporal changes in contradictions - Fixed mental model refresh security: tagged memories now only trigger refresh of mental models with matching tags - Added stricter tag filtering to prevent cross-scope mental model refreshes Fixes test_consolidation_merges_contradictions by improving LLM instructions to use temporal markers like "used to X, now Y" when merging contradictory facts. Note: test_refresh_with_tags_only_accesses_same_tagged_models still needs investigation - REFLECT operation may need additional tag filtering. * fix: mental model refresh security - proper tag filtering in search Fixed tool_search_mental_models to properly handle all_strict tag matching mode by using the centralized build_tags_where_clause function. Previously, the function only handled "all" vs "any" modes and always included untagged mental models when using non-"all" modes. This ensures that when a tagged mental model is refreshed with all_strict matching, it cannot access untagged mental models, preventing cross-scope information leakage. Fixes test_refresh_with_tags_only_accesses_same_tagged_models. Note: test_sensory_dimension_preservation is failing but this is a pre-existing issue on main branch - the LLM model (gpt-oss-20b) is not extracting facts from sensory text. Not related to security changes. * chore: apply formatting from pre-commit hook * fix: allow untagged mental models to be refreshed by any consolidation Untagged mental models are considered "global" and should be refreshed by any consolidation, regardless of whether tagged or untagged memories were consolidated. This maintains security boundaries while allowing global mental models to stay fresh. When tagged memories are consolidated: - Refresh mental models with matching tags (security boundary) - Also refresh untagged mental models (they're global) - DO NOT refresh mental models with different tags When untagged memories are consolidated: - Only refresh untagged mental models - DO NOT refresh tagged mental models (security boundary) Fixes test_consolidation_only_refreshes_matching_tagged_models. --- .github/workflows/release.yml | 59 +- .github/workflows/test.yml | 23 + hindsight-api/hindsight_api/api/http.py | 6 +- .../engine/consolidation/consolidator.py | 85 +- .../engine/consolidation/prompts.py | 9 +- .../hindsight_api/engine/memory_engine.py | 20 +- .../hindsight_api/engine/reflect/tools.py | 11 +- .../engine/retain/orchestrator.py | 36 +- hindsight-api/tests/test_mental_models.py | 195 ++ hindsight-api/tests/test_retain.py | 62 + .../models/create_mental_model_response.py | 6 +- .../typescript/generated/types.gen.ts | 8 +- .../src/components/documents-view.tsx | 451 +++-- hindsight-docs/static/openapi.json | 12 +- hindsight-integrations/ai-sdk/.gitignore | 30 + hindsight-integrations/ai-sdk/README.md | 356 ++++ .../ai-sdk/package-lock.json | 1702 +++++++++++++++++ hindsight-integrations/ai-sdk/package.json | 58 + hindsight-integrations/ai-sdk/src/index.ts | 15 + .../ai-sdk/src/tools/index.test.ts | 362 ++++ .../ai-sdk/src/tools/index.ts | 559 ++++++ hindsight-integrations/ai-sdk/tsconfig.json | 18 + .../ai-sdk/vitest.config.ts | 9 + scripts/release.sh | 11 + 24 files changed, 3889 insertions(+), 214 deletions(-) create mode 100644 hindsight-integrations/ai-sdk/.gitignore create mode 100644 hindsight-integrations/ai-sdk/README.md create mode 100644 hindsight-integrations/ai-sdk/package-lock.json create mode 100644 hindsight-integrations/ai-sdk/package.json create mode 100644 hindsight-integrations/ai-sdk/src/index.ts create mode 100644 hindsight-integrations/ai-sdk/src/tools/index.test.ts create mode 100644 hindsight-integrations/ai-sdk/src/tools/index.ts create mode 100644 hindsight-integrations/ai-sdk/tsconfig.json create mode 100644 hindsight-integrations/ai-sdk/vitest.config.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3c2beb3..23e23722 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -188,6 +188,55 @@ jobs: path: hindsight-integrations/openclaw/*.tgz retention-days: 1 + release-ai-sdk-integration: + runs-on: ubuntu-latest + environment: npm + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + working-directory: ./hindsight-integrations/ai-sdk + run: npm ci + + - name: Build + working-directory: ./hindsight-integrations/ai-sdk + run: npm run build + + - name: Publish to npm + working-directory: ./hindsight-integrations/ai-sdk + run: | + set +e + OUTPUT=$(npm publish --access public 2>&1) + EXIT_CODE=$? + echo "$OUTPUT" + if [ $EXIT_CODE -ne 0 ]; then + if echo "$OUTPUT" | grep -q "cannot publish over"; then + echo "Package version already published, skipping..." + exit 0 + fi + exit $EXIT_CODE + fi + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Pack for GitHub release + working-directory: ./hindsight-integrations/ai-sdk + run: npm pack + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: ai-sdk-integration + path: hindsight-integrations/ai-sdk/*.tgz + retention-days: 1 + release-control-plane: runs-on: ubuntu-latest environment: npm @@ -415,7 +464,7 @@ jobs: create-github-release: runs-on: ubuntu-latest - needs: [release-python-packages, release-typescript-client, release-openclaw-integration, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart] + needs: [release-python-packages, release-typescript-client, release-openclaw-integration, release-ai-sdk-integration, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart] permissions: contents: write @@ -444,6 +493,12 @@ jobs: name: openclaw-integration path: ./artifacts/openclaw-integration + - name: Download AI SDK Integration + uses: actions/download-artifact@v4 + with: + name: ai-sdk-integration + path: ./artifacts/ai-sdk-integration + - name: Download Control Plane uses: actions/download-artifact@v4 with: @@ -487,6 +542,8 @@ jobs: cp artifacts/typescript-client/*.tgz release-assets/ || true # OpenClaw Integration cp artifacts/openclaw-integration/*.tgz release-assets/ || true + # AI SDK Integration + cp artifacts/ai-sdk-integration/*.tgz release-assets/ || true # Control Plane cp artifacts/control-plane/*.tgz release-assets/ || true # Rust CLI binaries diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e6c82db..3f75e01d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,6 +74,29 @@ jobs: working-directory: ./hindsight-integrations/openclaw run: npm run build + build-ai-sdk-integration: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Install dependencies + working-directory: ./hindsight-integrations/ai-sdk + run: npm ci + + - name: Run tests + working-directory: ./hindsight-integrations/ai-sdk + run: npm test + + - name: Build + working-directory: ./hindsight-integrations/ai-sdk + run: npm run build + build-control-plane: runs-on: ubuntu-latest diff --git a/hindsight-api/hindsight_api/api/http.py b/hindsight-api/hindsight_api/api/http.py index 44e7877c..f100daed 100644 --- a/hindsight-api/hindsight_api/api/http.py +++ b/hindsight-api/hindsight_api/api/http.py @@ -866,6 +866,7 @@ class ListDocumentsResponse(BaseModel): "updated_at": "2024-01-15T10:30:00Z", "text_length": 5420, "memory_unit_count": 15, + "tags": ["user_a", "session_123"], } ], "total": 50, @@ -1160,7 +1161,8 @@ class CreateMentalModelRequest(BaseModel): class CreateMentalModelResponse(BaseModel): """Response model for mental model creation.""" - operation_id: str = Field(description="Operation ID to track progress") + mental_model_id: str = Field(description="ID of the created mental model") + operation_id: str = Field(description="Operation ID to track refresh progress") class UpdateMentalModelRequest(BaseModel): @@ -2423,7 +2425,7 @@ def _register_routes(app: FastAPI): mental_model_id=mental_model["id"], request_context=request_context, ) - return CreateMentalModelResponse(operation_id=result["operation_id"]) + return CreateMentalModelResponse(mental_model_id=mental_model["id"], operation_id=result["operation_id"]) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except (AuthenticationError, HTTPException): diff --git a/hindsight-api/hindsight_api/engine/consolidation/consolidator.py b/hindsight-api/hindsight_api/engine/consolidation/consolidator.py index 26944818..471ffb5e 100644 --- a/hindsight-api/hindsight_api/engine/consolidation/consolidator.py +++ b/hindsight-api/hindsight_api/engine/consolidation/consolidator.py @@ -143,6 +143,9 @@ async def run_consolidation_job( "skipped": 0, } + # Track all unique tags from consolidated memories for mental model refresh filtering + consolidated_tags: set[str] = set() + batch_num = 0 last_progress_timings = {} # Track timings at last progress log while True: @@ -176,6 +179,11 @@ async def run_consolidation_job( for memory in memories: mem_start = time.time() + # Track tags from this memory for mental model refresh filtering + memory_tags = memory.get("tags") or [] + if memory_tags: + consolidated_tags.update(memory_tags) + # Process the memory (uses its own connection internally) async with pool.acquire() as conn: result = await _process_memory( @@ -284,10 +292,12 @@ async def run_consolidation_job( perf.log(f"[4] Timing breakdown: {', '.join(timing_parts)}") # Trigger mental model refreshes for models with refresh_after_consolidation=true + # SECURITY: Only refresh mental models with matching tags (or all if no tags were consolidated) mental_models_refreshed = await _trigger_mental_model_refreshes( memory_engine=memory_engine, bank_id=bank_id, request_context=request_context, + consolidated_tags=list(consolidated_tags) if consolidated_tags else None, perf=perf, ) stats["mental_models_refreshed"] = mental_models_refreshed @@ -301,15 +311,20 @@ async def _trigger_mental_model_refreshes( memory_engine: "MemoryEngine", bank_id: str, request_context: "RequestContext", + consolidated_tags: list[str] | None = None, perf: ConsolidationPerfLog | None = None, ) -> int: """ Trigger refreshes for mental models with refresh_after_consolidation=true. + SECURITY: Only triggers refresh for mental models whose tags overlap with the + consolidated memory tags, preventing unnecessary refreshes across security boundaries. + Args: memory_engine: MemoryEngine instance bank_id: Bank identifier request_context: Request context for authentication + consolidated_tags: Tags from memories that were consolidated (None = refresh all) perf: Performance logging Returns: @@ -318,22 +333,52 @@ async def _trigger_mental_model_refreshes( pool = memory_engine._pool # Find mental models with refresh_after_consolidation=true + # SECURITY: Control which mental models get refreshed based on tags async with pool.acquire() as conn: - rows = await conn.fetch( - f""" - SELECT id, name - FROM {fq_table("mental_models")} - WHERE bank_id = $1 - AND (trigger->>'refresh_after_consolidation')::boolean = true - """, - bank_id, - ) + if consolidated_tags: + # Tagged memories were consolidated - refresh: + # 1. Mental models with overlapping tags (security boundary) + # 2. Untagged mental models (they're "global" and available to all contexts) + # DO NOT refresh mental models with different tags + rows = await conn.fetch( + f""" + SELECT id, name, tags + FROM {fq_table("mental_models")} + WHERE bank_id = $1 + AND (trigger->>'refresh_after_consolidation')::boolean = true + AND ( + (tags IS NOT NULL AND tags != '{{}}' AND tags && $2::varchar[]) + OR (tags IS NULL OR tags = '{{}}') + ) + """, + bank_id, + consolidated_tags, + ) + else: + # Untagged memories were consolidated - only refresh untagged mental models + # SECURITY: Tagged mental models are NOT refreshed when untagged memories are consolidated + rows = await conn.fetch( + f""" + SELECT id, name, tags + FROM {fq_table("mental_models")} + WHERE bank_id = $1 + AND (trigger->>'refresh_after_consolidation')::boolean = true + AND (tags IS NULL OR tags = '{{}}') + """, + bank_id, + ) if not rows: return 0 if perf: - perf.log(f"[5] Triggering refresh for {len(rows)} mental models with refresh_after_consolidation=true") + if consolidated_tags: + perf.log( + f"[5] Triggering refresh for {len(rows)} mental models with refresh_after_consolidation=true " + f"(filtered by tags: {consolidated_tags})" + ) + else: + perf.log(f"[5] Triggering refresh for {len(rows)} mental models with refresh_after_consolidation=true") # Submit refresh tasks for each mental model refreshed_count = 0 @@ -385,7 +430,8 @@ async def _process_memory( memory_id = memory["id"] fact_tags = memory.get("tags") or [] - # Find related observations using the full recall system (NO tag filtering) + # Find related observations using the full recall system + # SECURITY: Pass tags to ensure observations don't leak across security boundaries t0 = time.time() related_observations = await _find_related_observations( conn=conn, @@ -393,6 +439,7 @@ async def _process_memory( bank_id=bank_id, query=fact_text, request_context=request_context, + tags=fact_tags, # Pass source memory's tags for security ) if perf: perf.record_timing("recall", time.time() - t0) @@ -666,17 +713,20 @@ async def _find_related_observations( bank_id: str, query: str, request_context: "RequestContext", + tags: list[str] | None = None, ) -> list[dict[str, Any]]: """ Find observations related to the given query using optimized recall. - IMPORTANT: We do NOT filter by tags here. Consolidation needs to see ALL - potentially related observations regardless of scope, so the LLM can - decide on tag routing (same scope update vs cross-scope create). + SECURITY: Filters by tags using all_strict matching to prevent cross-tenant/cross-user + information leakage. Observations are only consolidated within the same tag scope. Uses max_tokens to naturally limit observations (no artificial count limit). Includes source memories with dates for LLM context. + Args: + tags: Optional tags to filter observations (uses all_strict matching for security) + Returns: List of related observations with their tags, source memories, and dates """ @@ -685,14 +735,19 @@ async def _find_related_observations( from ...config import get_config config = get_config() + + # SECURITY: Use all_strict matching if tags provided to prevent cross-scope consolidation + tags_match = "all_strict" if tags else "any" + recall_result = await memory_engine.recall_async( bank_id=bank_id, query=query, max_tokens=config.consolidation_max_tokens, # Token budget for observations (configurable) fact_type=["observation"], # Only retrieve observations request_context=request_context, + tags=tags, # Filter by source memory's tags + tags_match=tags_match, # Use strict matching for security _quiet=True, # Suppress logging - # NO tags parameter - intentionally get ALL observations ) # If no observations returned, return empty list diff --git a/hindsight-api/hindsight_api/engine/consolidation/prompts.py b/hindsight-api/hindsight_api/engine/consolidation/prompts.py index a0d4ff96..441e19f4 100644 --- a/hindsight-api/hindsight_api/engine/consolidation/prompts.py +++ b/hindsight-api/hindsight_api/engine/consolidation/prompts.py @@ -32,13 +32,16 @@ BAD examples: ## MERGE RULES (when comparing to existing observations): 1. REDUNDANT: Same information worded differently → update existing -2. CONTRADICTION: Opposite information about same topic → update with history (e.g., "used to X, now Y") -3. UPDATE: New state replacing old state → update with history +2. CONTRADICTION: Opposite information about same topic → update with temporal markers showing change + Example: "Alex used to love pizza but now hates it" OR "Alex's pizza preference changed from love to hate" +3. UPDATE: New state replacing old state → update showing the transition with "used to", "now", "changed from X to Y" ## CRITICAL RULES: - NEVER merge facts about DIFFERENT people - NEVER merge unrelated topics (food preferences vs work vs hobbies) -- When merging contradictions, capture the CHANGE (before → after) +- When merging contradictions, the "text" field MUST capture BOTH states with temporal markers: + * Use "used to X, now Y" OR "changed from X to Y" OR "X but now Y" + * DO NOT just state the new fact - you MUST show the change - Keep observations focused on ONE specific topic per person - The "text" field MUST contain durable knowledge, not ephemeral state - Do NOT include "tags" in output - tags are handled automatically""" diff --git a/hindsight-api/hindsight_api/engine/memory_engine.py b/hindsight-api/hindsight_api/engine/memory_engine.py index eb377074..1d903660 100644 --- a/hindsight-api/hindsight_api/engine/memory_engine.py +++ b/hindsight-api/hindsight_api/engine/memory_engine.py @@ -625,11 +625,19 @@ class MemoryEngine(MemoryEngineInterface): source_query = mental_model["source_query"] + # SECURITY: If the mental model has tags, pass them to reflect with "all_strict" matching + # to ensure it can only access other mental models/memories with the SAME tags. + # This prevents cross-tenant/cross-user information leakage by excluding untagged content. + tags = mental_model.get("tags") + tags_match = "all_strict" if tags else "any" + # Run reflect to generate new content, excluding the mental model being refreshed reflect_result = await self.reflect_async( bank_id=bank_id, query=source_query, request_context=internal_context, + tags=tags, + tags_match=tags_match, exclude_mental_model_ids=[mental_model_id], ) @@ -3304,7 +3312,8 @@ class MemoryEngine(MemoryEngineInterface): created_at, updated_at, LENGTH(original_text) as text_length, - retain_params + retain_params, + tags FROM {fq_table("documents")} {where_clause} ORDER BY created_at DESC @@ -3360,6 +3369,7 @@ class MemoryEngine(MemoryEngineInterface): "text_length": row["text_length"] or 0, "memory_unit_count": unit_count, "retain_params": row["retain_params"] if row["retain_params"] else None, + "tags": row["tags"] if row["tags"] else [], } ) @@ -4719,11 +4729,19 @@ class MemoryEngine(MemoryEngineInterface): if not mental_model: return None + # SECURITY: If the mental model has tags, pass them to reflect with "all_strict" matching + # to ensure it can only access other mental models/memories with the SAME tags. + # This prevents cross-tenant/cross-user information leakage by excluding untagged content. + tags = mental_model.get("tags") + tags_match = "all_strict" if tags else "any" + # Run reflect with the source query, excluding the mental model being refreshed reflect_result = await self.reflect_async( bank_id=bank_id, query=mental_model["source_query"], request_context=request_context, + tags=tags, + tags_match=tags_match, exclude_mental_model_ids=[mental_model_id], ) diff --git a/hindsight-api/hindsight_api/engine/reflect/tools.py b/hindsight-api/hindsight_api/engine/reflect/tools.py index 61ffd87f..b07fbc25 100644 --- a/hindsight-api/hindsight_api/engine/reflect/tools.py +++ b/hindsight-api/hindsight_api/engine/reflect/tools.py @@ -54,19 +54,18 @@ async def tool_search_mental_models( Dict with matching mental models including content and freshness info """ from ..memory_engine import fq_table + from ..search.tags import build_tags_where_clause # Build filters dynamically filters = "" params: list[Any] = [bank_id, str(query_embedding), max_results] next_param = 4 + # Use the centralized tag filtering logic if tags: - if tags_match == "all": - filters += f" AND tags @> ${next_param}::varchar[]" - else: - filters += f" AND (tags && ${next_param}::varchar[] OR tags IS NULL OR tags = '{{}}')" - params.append(tags) - next_param += 1 + tag_clause, tag_params, next_param = build_tags_where_clause(tags, param_offset=next_param, match=tags_match) + filters += f" {tag_clause}" + params.extend(tag_params) if exclude_ids: filters += f" AND id != ALL(${next_param}::text[])" diff --git a/hindsight-api/hindsight_api/engine/retain/orchestrator.py b/hindsight-api/hindsight_api/engine/retain/orchestrator.py index dec79910..95736762 100644 --- a/hindsight-api/hindsight_api/engine/retain/orchestrator.py +++ b/hindsight-api/hindsight_api/engine/retain/orchestrator.py @@ -158,6 +158,13 @@ async def retain_batch( # Handle document tracking even with no facts if document_id: combined_content = "\n".join([c.get("content", "") for c in contents_dicts]) + # Collect tags from all content items and merge with document_tags + all_tags = set(document_tags or []) + for item in contents_dicts: + item_tags = item.get("tags", []) or [] + all_tags.update(item_tags) + merged_tags = list(all_tags) + retain_params = {} if contents_dicts: first_item = contents_dicts[0] @@ -172,7 +179,7 @@ async def retain_batch( if first_item.get("metadata"): retain_params["metadata"] = first_item["metadata"] await fact_storage.handle_document_tracking( - conn, bank_id, document_id, combined_content, is_first_batch, retain_params, document_tags + conn, bank_id, document_id, combined_content, is_first_batch, retain_params, merged_tags ) else: # Check for per-item document_ids @@ -186,6 +193,13 @@ async def retain_batch( for doc_id, doc_contents in contents_by_doc.items(): combined_content = "\n".join([c.get("content", "") for _, c in doc_contents]) + # Collect tags from all content items for this document and merge with document_tags + all_tags = set(document_tags or []) + for _, item in doc_contents: + item_tags = item.get("tags", []) or [] + all_tags.update(item_tags) + merged_tags = list(all_tags) + retain_params = {} if doc_contents: first_item = doc_contents[0][1] @@ -200,7 +214,7 @@ async def retain_batch( if first_item.get("metadata"): retain_params["metadata"] = first_item["metadata"] await fact_storage.handle_document_tracking( - conn, bank_id, doc_id, combined_content, is_first_batch, retain_params, document_tags + conn, bank_id, doc_id, combined_content, is_first_batch, retain_params, merged_tags ) total_time = time.time() - start_time @@ -252,6 +266,13 @@ async def retain_batch( # Legacy: single document_id parameter combined_content = "\n".join([c.get("content", "") for c in contents_dicts]) retain_params = {} + # Collect tags from all content items and merge with document_tags + all_tags = set(document_tags or []) + for item in contents_dicts: + item_tags = item.get("tags", []) or [] + all_tags.update(item_tags) + merged_tags = list(all_tags) + if contents_dicts: first_item = contents_dicts[0] if first_item.get("context"): @@ -266,7 +287,7 @@ async def retain_batch( retain_params["metadata"] = first_item["metadata"] await fact_storage.handle_document_tracking( - conn, bank_id, document_id, combined_content, is_first_batch, retain_params, document_tags + conn, bank_id, document_id, combined_content, is_first_batch, retain_params, merged_tags ) document_ids_added.append(document_id) doc_id_mapping[None] = document_id # For backwards compatibility @@ -294,6 +315,13 @@ async def retain_batch( # Combine content for this document combined_content = "\n".join([c.get("content", "") for _, c in doc_contents]) + # Collect tags from all content items for this document and merge with document_tags + all_tags = set(document_tags or []) + for _, item in doc_contents: + item_tags = item.get("tags", []) or [] + all_tags.update(item_tags) + merged_tags = list(all_tags) + # Extract retain params from first content item retain_params = {} if doc_contents: @@ -316,7 +344,7 @@ async def retain_batch( combined_content, is_first_batch, retain_params, - document_tags, + merged_tags, ) document_ids_added.append(actual_doc_id) diff --git a/hindsight-api/tests/test_mental_models.py b/hindsight-api/tests/test_mental_models.py index c391c34e..723a6c64 100644 --- a/hindsight-api/tests/test_mental_models.py +++ b/hindsight-api/tests/test_mental_models.py @@ -451,3 +451,198 @@ class TestDirectivesPromptInjection: directives_pos = prompt.find("## DIRECTIVES") critical_rules_pos = prompt.find("## CRITICAL RULES") assert directives_pos < critical_rules_pos + + +class TestMentalModelRefreshTagSecurity: + """Test that mental model refresh respects tag-based security boundaries.""" + + async def test_refresh_with_tags_only_accesses_same_tagged_models( + self, memory: MemoryEngine, request_context + ): + """Test that refreshing a mental model with tags can only access other models with the same tags. + + This is a security test to ensure that mental models with tags (e.g., user:alice) + cannot access mental models from other scopes (e.g., user:bob or no tags) during refresh. + """ + bank_id = f"test-refresh-tags-{uuid.uuid4().hex[:8]}" + + # Ensure bank exists + await memory.get_bank_profile(bank_id, request_context=request_context) + + # Add some facts with different tags + await memory.retain_batch_async( + bank_id=bank_id, + contents=[ + {"content": "Alice works on the frontend React project. Alice's favorite color is blue.", "tags": ["user:alice"]}, + {"content": "Alice prefers working in the morning. Alice drinks coffee every day.", "tags": ["user:alice"]}, + {"content": "Bob works on the backend API services. Bob's favorite language is Python.", "tags": ["user:bob"]}, + {"content": "Bob prefers working at night. Bob drinks tea every day.", "tags": ["user:bob"]}, + {"content": "The company has 100 employees and is growing fast.", "tags": []}, # No tags + ], + request_context=request_context, + ) + + # Wait for background processing + await memory.wait_for_background_tasks() + + # Create mental model for user:alice with sensitive data + mm_alice = await memory.create_mental_model( + bank_id=bank_id, + name="Alice's Work Profile", + source_query="What does Alice work on?", + content="Alice is a frontend engineer specializing in React", + tags=["user:alice"], + request_context=request_context, + ) + + # Create mental model for user:bob with sensitive data + mm_bob = await memory.create_mental_model( + bank_id=bank_id, + name="Bob's Work Profile", + source_query="What does Bob work on?", + content="Bob is a backend engineer specializing in Python", + tags=["user:bob"], + request_context=request_context, + ) + + # Create mental model with no tags (should not be accessible from tagged models) + mm_untagged = await memory.create_mental_model( + bank_id=bank_id, + name="Company Info", + source_query="What is the company info?", + content="The company has 100 employees", + request_context=request_context, + ) + + # Create a mental model for user:alice that will be refreshed + mm_alice_refresh = await memory.create_mental_model( + bank_id=bank_id, + name="Alice's Summary", + source_query="What are all the facts about work and preferences?", # Broad query that should match all facts + content="Initial content", + tags=["user:alice"], + request_context=request_context, + ) + + # Refresh Alice's mental model + refreshed = await memory.refresh_mental_model( + bank_id=bank_id, + mental_model_id=mm_alice_refresh["id"], + request_context=request_context, + ) + + # SECURITY CHECK: The refreshed content should ONLY include information from + # memories/models tagged with user:alice, NOT from user:bob or untagged + refreshed_content = refreshed["content"].lower() + + # Should include Alice's content (either from facts or mental models) + assert "alice" in refreshed_content, \ + "Refreshed model should access memories/models with matching tags (user:alice)" + + # MUST NOT include Bob's content (security violation) + assert "bob" not in refreshed_content and "python" not in refreshed_content and "tea" not in refreshed_content, \ + f"SECURITY VIOLATION: Refreshed model accessed memories/models with different tags (user:bob). Content: {refreshed_content}" + + # MUST NOT include untagged content (security violation) + assert "100 employees" not in refreshed_content and "growing fast" not in refreshed_content, \ + f"SECURITY VIOLATION: Refreshed model accessed untagged memories/models. Content: {refreshed_content}" + + # Cleanup + await memory.delete_bank(bank_id, request_context=request_context) + + async def test_consolidation_only_refreshes_matching_tagged_models( + self, memory: MemoryEngine, request_context + ): + """Test that consolidation only triggers refresh for mental models with matching tags. + + This is a security test to ensure that when tagged memories are consolidated, + only mental models with overlapping tags get refreshed, not all mental models. + """ + bank_id = f"test-consolidation-refresh-{uuid.uuid4().hex[:8]}" + + # Ensure bank exists + await memory.get_bank_profile(bank_id, request_context=request_context) + + # Create mental models with different tags, all with refresh_after_consolidation=true + mm_alice = await memory.create_mental_model( + bank_id=bank_id, + name="Alice's Model", + source_query="What about Alice?", + content="Initial Alice content", + tags=["user:alice"], + trigger={"refresh_after_consolidation": True}, + request_context=request_context, + ) + + mm_bob = await memory.create_mental_model( + bank_id=bank_id, + name="Bob's Model", + source_query="What about Bob?", + content="Initial Bob content", + tags=["user:bob"], + trigger={"refresh_after_consolidation": True}, + request_context=request_context, + ) + + mm_untagged = await memory.create_mental_model( + bank_id=bank_id, + name="Untagged Model", + source_query="What about general stuff?", + content="Initial untagged content", + trigger={"refresh_after_consolidation": True}, + request_context=request_context, + ) + + # Record initial last_refreshed_at timestamps + alice_initial = mm_alice["last_refreshed_at"] + bob_initial = mm_bob["last_refreshed_at"] + untagged_initial = mm_untagged["last_refreshed_at"] + + # Add memories with user:alice tags + await memory.retain_batch_async( + bank_id=bank_id, + contents=[ + {"content": "Alice likes React", "tags": ["user:alice"]}, + {"content": "Alice drinks coffee", "tags": ["user:alice"]}, + ], + request_context=request_context, + ) + + # Trigger consolidation manually (this should only refresh Alice's mental model) + from hindsight_api.engine.consolidation.consolidator import run_consolidation_job + + result = await run_consolidation_job( + memory_engine=memory, + bank_id=bank_id, + request_context=request_context, + ) + + # Wait for background refresh tasks to complete + await memory.wait_for_background_tasks() + + # Check that mental models were refreshed appropriately + mm_alice_after = await memory.get_mental_model( + bank_id, mm_alice["id"], request_context=request_context + ) + mm_bob_after = await memory.get_mental_model( + bank_id, mm_bob["id"], request_context=request_context + ) + mm_untagged_after = await memory.get_mental_model( + bank_id, mm_untagged["id"], request_context=request_context + ) + + # SECURITY CHECK: Only Alice's mental model and untagged model should be refreshed + # Alice's model should be refreshed (tags match) + assert mm_alice_after["last_refreshed_at"] != alice_initial or mm_alice_after["content"] != mm_alice["content"], \ + "Alice's mental model should be refreshed when user:alice memories are consolidated" + + # Bob's model should NOT be refreshed (tags don't match) + assert mm_bob_after["last_refreshed_at"] == bob_initial, \ + "SECURITY VIOLATION: Bob's mental model was refreshed even though user:bob memories were not consolidated" + + # Untagged model should be refreshed (untagged models are always refreshed) + assert mm_untagged_after["last_refreshed_at"] != untagged_initial or mm_untagged_after["content"] != mm_untagged["content"], \ + "Untagged mental model should be refreshed after any consolidation" + + # Cleanup + await memory.delete_bank(bank_id, request_context=request_context) diff --git a/hindsight-api/tests/test_retain.py b/hindsight-api/tests/test_retain.py index 14da51ad..2672f4ee 100644 --- a/hindsight-api/tests/test_retain.py +++ b/hindsight-api/tests/test_retain.py @@ -2193,3 +2193,65 @@ If the text contains both Italian and English content, extract ONLY the Italian # Clear cache again to restore original config clear_config_cache() + + +@pytest.mark.asyncio +async def test_retain_batch_with_per_item_tags_on_document(memory, request_context): + """ + Test that per-item tags are correctly stored on documents. + + This test verifies the fix for a bug where per-item tags in content dictionaries + were not being merged and passed to document tracking, causing tags to be lost + even though they were correctly sent through the API. + + Without the fix, this test would fail because: + - Tags are correctly passed in the content dict + - Tags are correctly stored on memory_units (facts) + - BUT tags were NOT stored on the document record itself + """ + bank_id = f"test_doc_tags_{datetime.now(timezone.utc).timestamp()}" + document_id = "app-state-testuser" + + try: + # Retain content with per-item tags (simulating the TasteAI use case) + contents = [ + { + "content": '{"username":"testuser","meals":[],"preferences":{"nickname":"testuser"}}', + "document_id": document_id, + "tags": ["user:testuser", "app-type:taste-ai"], + } + ] + + result = await memory.retain_batch_async( + bank_id=bank_id, + contents=contents, + request_context=request_context, + ) + + assert len(result) > 0, "Should have retained content" + print(f"\n=== Retained content with tags ===") + + # Retrieve the document + doc = await memory.get_document( + document_id=document_id, + bank_id=bank_id, + request_context=request_context, + ) + + assert doc is not None, "Document should exist" + assert "tags" in doc, "Document should have tags field" + + # This is the critical assertion - tags should be stored on the document + doc_tags = doc["tags"] or [] + print(f"Document tags: {doc_tags}") + + assert "user:testuser" in doc_tags, \ + f"Document should have 'user:testuser' tag, but got: {doc_tags}" + assert "app-type:taste-ai" in doc_tags, \ + f"Document should have 'app-type:taste-ai' tag, but got: {doc_tags}" + + print("✓ Per-item tags correctly stored on document") + + finally: + await memory.delete_bank(bank_id, request_context=request_context) + print(f"\n=== Cleaned up bank: {bank_id} ===") diff --git a/hindsight-clients/python/hindsight_client_api/models/create_mental_model_response.py b/hindsight-clients/python/hindsight_client_api/models/create_mental_model_response.py index 34f4e147..49299064 100644 --- a/hindsight-clients/python/hindsight_client_api/models/create_mental_model_response.py +++ b/hindsight-clients/python/hindsight_client_api/models/create_mental_model_response.py @@ -26,8 +26,9 @@ class CreateMentalModelResponse(BaseModel): """ Response model for mental model creation. """ # noqa: E501 - operation_id: StrictStr = Field(description="Operation ID to track progress") - __properties: ClassVar[List[str]] = ["operation_id"] + mental_model_id: StrictStr = Field(description="ID of the created mental model") + operation_id: StrictStr = Field(description="Operation ID to track refresh progress") + __properties: ClassVar[List[str]] = ["mental_model_id", "operation_id"] model_config = ConfigDict( populate_by_name=True, @@ -80,6 +81,7 @@ class CreateMentalModelResponse(BaseModel): return cls.model_validate(obj) _obj = cls.model_validate({ + "mental_model_id": obj.get("mental_model_id"), "operation_id": obj.get("operation_id") }) return _obj diff --git a/hindsight-clients/typescript/generated/types.gen.ts b/hindsight-clients/typescript/generated/types.gen.ts index 493b1622..56f75cf9 100644 --- a/hindsight-clients/typescript/generated/types.gen.ts +++ b/hindsight-clients/typescript/generated/types.gen.ts @@ -435,10 +435,16 @@ export type CreateMentalModelRequest = { * Response model for mental model creation. */ export type CreateMentalModelResponse = { + /** + * Mental Model Id + * + * ID of the created mental model + */ + mental_model_id: string; /** * Operation Id * - * Operation ID to track progress + * Operation ID to track refresh progress */ operation_id: string; }; diff --git a/hindsight-control-plane/src/components/documents-view.tsx b/hindsight-control-plane/src/components/documents-view.tsx index 1312672d..01ac2305 100644 --- a/hindsight-control-plane/src/components/documents-view.tsx +++ b/hindsight-control-plane/src/components/documents-view.tsx @@ -23,7 +23,9 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { X, Trash2 } from "lucide-react"; +import { X, Trash2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react"; + +const ITEMS_PER_PAGE = 50; export function DocumentsView() { const { currentBank } = useBank(); @@ -32,6 +34,11 @@ export function DocumentsView() { const [searchQuery, setSearchQuery] = useState(""); const [total, setTotal] = useState(0); + // Pagination state + const [currentPage, setCurrentPage] = useState(1); + const totalPages = Math.ceil(total / ITEMS_PER_PAGE); + const offset = (currentPage - 1) * ITEMS_PER_PAGE; + // Document view panel state const [selectedDocument, setSelectedDocument] = useState(null); const [loadingDocument, setLoadingDocument] = useState(false); @@ -46,15 +53,17 @@ export function DocumentsView() { null ); - const loadDocuments = async () => { + const loadDocuments = async (page: number = 1) => { if (!currentBank) return; setLoading(true); try { + const pageOffset = (page - 1) * ITEMS_PER_PAGE; const data: any = await client.listDocuments({ bank_id: currentBank, q: searchQuery, - limit: 100, + limit: ITEMS_PER_PAGE, + offset: pageOffset, }); setDocuments(data.items || []); setTotal(data.total || 0); @@ -66,6 +75,12 @@ export function DocumentsView() { } }; + // Handle page change + const handlePageChange = (newPage: number) => { + setCurrentPage(newPage); + loadDocuments(newPage); + }; + const viewDocumentText = async (documentId: string) => { if (!currentBank) return; @@ -103,8 +118,8 @@ export function DocumentsView() { setSelectedDocument(null); } - // Reload documents list - loadDocuments(); + // Reload documents list at current page + loadDocuments(currentPage); } catch (error) { console.error("Error deleting document:", error); setDeleteResult({ @@ -120,13 +135,26 @@ export function DocumentsView() { setDocumentToDelete({ id: documentId, memoryCount }); }; - // Auto-load documents when component mounts + // Auto-load documents when component mounts or bank changes useEffect(() => { if (currentBank) { - loadDocuments(); + setCurrentPage(1); + loadDocuments(1); } }, [currentBank]); + // Reload when search query changes (with debounce) + useEffect(() => { + if (!currentBank) return; + + const timeoutId = setTimeout(() => { + setCurrentPage(1); + loadDocuments(1); + }, 300); // 300ms debounce + + return () => clearTimeout(timeoutId); + }, [searchQuery]); + return (
{/* Documents List Section */} @@ -138,19 +166,10 @@ export function DocumentsView() {
) : documents.length > 0 ? ( -
{total} total documents
- ) : ( -
-
-
📄
-
No documents found
-
-
- )} - - {/* Documents List and Detail Panel */} - {documents.length > 0 && ( <> +
+ {total} {total === 1 ? "document" : "documents"} +
{/* Documents Table */}
@@ -169,6 +188,7 @@ export function DocumentsView() { Document ID Created + Tags Context Text Length Memory Units @@ -188,6 +208,27 @@ export function DocumentsView() { {doc.created_at ? new Date(doc.created_at).toLocaleString() : "N/A"} + + {doc.tags && doc.tags.length > 0 ? ( +
+ {doc.tags.slice(0, 3).map((tag: string, i: number) => ( + + {tag} + + ))} + {doc.tags.length > 3 && ( + + +{doc.tags.length - 3} + + )} +
+ ) : ( + "-" + )} +
{doc.retain_params?.context || "-"} @@ -201,7 +242,7 @@ export function DocumentsView() { )) ) : ( - + Click "Load Documents" to view data @@ -209,176 +250,230 @@ export function DocumentsView() {
-
- {/* Document Detail Panel - Fixed on Right */} - {selectedDocument && ( -
-
- {/* Header with close button */} -
-
-

Document Details

-

- Original document text and metadata -

-
+ {/* Pagination Controls */} + {totalPages > 1 && ( +
+
+ {offset + 1}-{Math.min(offset + ITEMS_PER_PAGE, total)} of {total} +
+
+ + + {currentPage} / {totalPages} + + + +
+
+ )} +
+ + ) : ( +
+
+
📄
+
No documents found
+
+
+ )} + + {/* Document Detail Panel - Fixed on Right */} + {documents.length > 0 && selectedDocument && ( +
+
+ {/* Header with close button */} +
+
+

Document Details

+

+ Original document text and metadata +

+
+ +
+ + {loadingDocument ? ( +
+
+
+
Loading document...
+
+
+ ) : ( +
+ {/* Document ID */} +
+
+ Document ID +
+
+ {selectedDocument.id} +
+
+ + {/* Created & Memory Units */} + {selectedDocument.created_at && ( +
+
+
+ Created +
+
+ {new Date(selectedDocument.created_at).toLocaleString()} +
+
+
+
+ Memory Units +
+
+ {selectedDocument.memory_unit_count} +
+
+
+ )} + + {/* Text Length */} + {selectedDocument.original_text && ( +
+
+ Text Length +
+
+ {selectedDocument.original_text.length.toLocaleString()} characters +
+
+ )} + + {/* Retain Parameters */} + {selectedDocument.retain_params && ( +
+
+ Retain Parameters +
+
+ {selectedDocument.retain_params.context && ( +
+ Context:{" "} + {selectedDocument.retain_params.context} +
+ )} + {selectedDocument.retain_params.event_date && ( +
+ Event Date:{" "} + {new Date(selectedDocument.retain_params.event_date).toLocaleString()} +
+ )} + {selectedDocument.retain_params.metadata && ( +
+ Metadata: +
+                            {JSON.stringify(selectedDocument.retain_params.metadata, null, 2)}
+                          
+
+ )} +
+
+ )} + + {/* Tags */} + {selectedDocument.tags && selectedDocument.tags.length > 0 && ( +
+
+ Tags +
+
+ {selectedDocument.tags.map((tag: string, i: number) => ( + + {tag} + + ))} +
+
+ )} + + {/* Delete Button */} +
+
- {loadingDocument ? ( -
-
-
-
Loading document...
+ {/* Original Text */} + {selectedDocument.original_text && ( +
+
+ Original Text
-
- ) : ( -
- {/* Document ID */} -
-
- Document ID -
-
- {selectedDocument.id} -
+
+
+                        {selectedDocument.original_text}
+                      
- - {/* Created & Memory Units */} - {selectedDocument.created_at && ( -
-
-
- Created -
-
- {new Date(selectedDocument.created_at).toLocaleString()} -
-
-
-
- Memory Units -
-
- {selectedDocument.memory_unit_count} -
-
-
- )} - - {/* Text Length */} - {selectedDocument.original_text && ( -
-
- Text Length -
-
- {selectedDocument.original_text.length.toLocaleString()} characters -
-
- )} - - {/* Retain Parameters */} - {selectedDocument.retain_params && ( -
-
- Retain Parameters -
-
- {selectedDocument.retain_params.context && ( -
- Context:{" "} - {selectedDocument.retain_params.context} -
- )} - {selectedDocument.retain_params.event_date && ( -
- Event Date:{" "} - {new Date(selectedDocument.retain_params.event_date).toLocaleString()} -
- )} - {selectedDocument.retain_params.metadata && ( -
- Metadata: -
-                                {JSON.stringify(selectedDocument.retain_params.metadata, null, 2)}
-                              
-
- )} -
-
- )} - - {/* Tags */} - {selectedDocument.tags && selectedDocument.tags.length > 0 && ( -
-
- Tags -
-
- {selectedDocument.tags.map((tag: string, i: number) => ( - - {tag} - - ))} -
-
- )} - - {/* Delete Button */} -
- -
- - {/* Original Text */} - {selectedDocument.original_text && ( -
-
- Original Text -
-
-
-                            {selectedDocument.original_text}
-                          
-
-
- )}
)}
-
- )} - + )} +
+
)} {/* Delete Confirmation Dialog */} diff --git a/hindsight-docs/static/openapi.json b/hindsight-docs/static/openapi.json index fc21dc5f..4abecb84 100644 --- a/hindsight-docs/static/openapi.json +++ b/hindsight-docs/static/openapi.json @@ -3638,14 +3638,20 @@ }, "CreateMentalModelResponse": { "properties": { + "mental_model_id": { + "type": "string", + "title": "Mental Model Id", + "description": "ID of the created mental model" + }, "operation_id": { "type": "string", "title": "Operation Id", - "description": "Operation ID to track progress" + "description": "Operation ID to track refresh progress" } }, "type": "object", "required": [ + "mental_model_id", "operation_id" ], "title": "CreateMentalModelResponse", @@ -4401,6 +4407,10 @@ "created_at": "2024-01-15T10:30:00Z", "id": "session_1", "memory_unit_count": 15, + "tags": [ + "user_a", + "session_123" + ], "text_length": 5420, "updated_at": "2024-01-15T10:30:00Z" } diff --git a/hindsight-integrations/ai-sdk/.gitignore b/hindsight-integrations/ai-sdk/.gitignore new file mode 100644 index 00000000..06fd95de --- /dev/null +++ b/hindsight-integrations/ai-sdk/.gitignore @@ -0,0 +1,30 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Test coverage +coverage/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment +.env +.env.local +.env.*.local diff --git a/hindsight-integrations/ai-sdk/README.md b/hindsight-integrations/ai-sdk/README.md new file mode 100644 index 00000000..854b5598 --- /dev/null +++ b/hindsight-integrations/ai-sdk/README.md @@ -0,0 +1,356 @@ +# Hindsight Memory Integration for Vercel AI SDK + +Give your AI agents persistent, human-like memory using [Hindsight](https://vectorize.io/hindsight) with the [Vercel AI SDK](https://ai-sdk.dev). + +## Features + +- **Three Memory Operations**: `retain` (store), `recall` (retrieve), and `reflect` (reason over memories) +- **Multi-User Support**: Dynamic bank IDs per call for multi-user/multi-tenant scenarios +- **Full API Coverage**: Complete parameter support for all Hindsight operations +- **Type-Safe**: Full TypeScript support with Zod schemas for validation +- **AI SDK 6 Native**: Works seamlessly with `generateText`, `streamText`, and `ToolLoopAgent` + +## Installation + +```bash +npm install @vectorize-io/hindsight-ai-sdk ai zod +``` + +You'll also need a Hindsight client. Choose one: + +**Option A: TypeScript/JavaScript Client** +```bash +npm install @vectorize-io/hindsight-client +``` + +**Option B: Direct HTTP Client** (no additional dependencies) +```typescript +// See "HTTP Client Example" below +``` + +## Quick Start + +### 1. Set up your Hindsight client + +```typescript +import { HindsightClient } from '@vectorize-io/hindsight-client'; + +const hindsightClient = new HindsightClient({ + apiUrl: process.env.HINDSIGHT_API_URL || 'http://localhost:8000', +}); +``` + +### 2. Create Hindsight tools + +```typescript +import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk'; + +const tools = createHindsightTools({ + client: hindsightClient, +}); +``` + +### 3. Use with AI SDK + +```typescript +import { generateText } from 'ai'; +import { anthropic } from '@ai-sdk/anthropic'; + +const result = await generateText({ + model: anthropic('claude-sonnet-4-20250514'), + tools, + prompt: 'Remember that Alice loves hiking and prefers spicy food', +}); + +console.log(result.text); +``` + +## Full Example: Memory-Enabled Chatbot + +```typescript +import { HindsightClient } from '@vectorize-io/hindsight-client'; +import { createHindsightTools } from '@vectorize-io/hindsight-ai-sdk'; +import { streamText } from 'ai'; +import { anthropic } from '@ai-sdk/anthropic'; + +// Initialize Hindsight +const hindsightClient = new HindsightClient({ + apiUrl: 'http://localhost:8000', +}); + +const tools = createHindsightTools({ client: hindsightClient }); + +// Chat with memory +const result = await streamText({ + model: anthropic('claude-sonnet-4-20250514'), + tools, + system: `You are a helpful assistant with long-term memory. + +IMPORTANT: +- Before answering questions, use the 'recall' tool to check for relevant memories +- When users share important information, use the 'retain' tool to remember it +- For complex questions requiring synthesis, use the 'reflect' tool +- Always pass the user's ID as the bankId parameter + +Your memory persists across sessions!`, + prompt: 'Remember that I am Alice and I love hiking', +}); + +for await (const chunk of result.textStream) { + process.stdout.write(chunk); +} +``` + +## API Reference + +### `createHindsightTools(options)` + +Creates AI SDK tool definitions for Hindsight memory operations. + +**Parameters:** + +- `options.client`: `HindsightClient` - Hindsight client instance +- `options.retainDescription`: `string` (optional) - Custom description for the retain tool +- `options.recallDescription`: `string` (optional) - Custom description for the recall tool +- `options.reflectDescription`: `string` (optional) - Custom description for the reflect tool + +**Returns:** Object with three tools: `retain`, `recall`, and `reflect` + +### Tool: `retain` + +Store information in long-term memory. + +**Parameters:** +- `bankId`: `string` - Memory bank ID (usually the user ID) +- `content`: `string` - Content to store +- `documentId`: `string` (optional) - Document ID for grouping/upserting +- `timestamp`: `string` (optional) - ISO timestamp for when the memory occurred +- `context`: `string` (optional) - Additional context about the memory + +**Returns:** +```typescript +{ + success: boolean; + itemsCount: number; +} +``` + +### Tool: `recall` + +Search memory for relevant information. + +**Parameters:** +- `bankId`: `string` - Memory bank ID +- `query`: `string` - What to search for +- `types`: `string[]` (optional) - Filter by fact types +- `maxTokens`: `number` (optional) - Maximum tokens to return +- `budget`: `'low' | 'mid' | 'high'` (optional) - Processing budget +- `queryTimestamp`: `string` (optional) - Query from a specific time (ISO format) +- `includeEntities`: `boolean` (optional) - Include entity observations +- `includeChunks`: `boolean` (optional) - Include raw chunks + +**Returns:** +```typescript +{ + results: Array<{ + id: string; + text: string; + type?: string; + entities?: string[]; + context?: string; + occurred_start?: string; + occurred_end?: string; + mentioned_at?: string; + document_id?: string; + metadata?: Record; + chunk_id?: string; + }>; + entities?: Record; +} +``` + +### Tool: `reflect` + +Analyze memories to form insights and generate contextual answers. + +**Parameters:** +- `bankId`: `string` - Memory bank ID +- `query`: `string` - Question to reflect on +- `context`: `string` (optional) - Additional context for reflection +- `budget`: `'low' | 'mid' | 'high'` (optional) - Processing budget + +**Returns:** +```typescript +{ + text: string; + basedOn?: Array<{ + id?: string; + text: string; + type?: string; + context?: string; + occurred_start?: string; + occurred_end?: string; + }>; +} +``` + +## Advanced Usage + +### Custom Tool Descriptions + +Customize tool descriptions to guide model behavior: + +```typescript +const tools = createHindsightTools({ + client: hindsightClient, + retainDescription: 'Store user preferences and important facts. Always include context.', + recallDescription: 'Search past conversations. Use specific queries for best results.', + reflectDescription: 'Synthesize insights from memories. Use for complex questions.', +}); +``` + +### Multi-User Scenarios + +Each tool call accepts a `bankId` parameter, making it easy to support multiple users: + +```typescript +const result = await generateText({ + model: anthropic('claude-sonnet-4-20250514'), + tools, + prompt: `User ID: ${userId}\n\nRemember that I prefer dark mode`, +}); +``` + +The model will automatically pass the user ID to the tools. + +### Using with ToolLoopAgent + +```typescript +import { ToolLoopAgent, stopWhen, stepCountIs } from 'ai'; + +const agent = new ToolLoopAgent({ + model: anthropic('claude-sonnet-4-20250514'), + tools, + instructions: `You are a personal assistant with long-term memory. + + Always check memory before responding using the recall tool. + Store important user preferences with the retain tool. + Use the reflect tool to analyze patterns in the user's behavior.`, + stopWhen: stepCountIs(10), +}); + +const result = await agent.generate({ + prompt: 'What did I say I wanted to work on this week?', +}); +``` + +## HTTP Client Example + +If you prefer not to install the full Hindsight client, you can use a simple HTTP client: + +```typescript +import type { HindsightClient } from '@vectorize-io/hindsight-ai-sdk'; + +const httpClient: HindsightClient = { + async retain(bankId, content, options = {}) { + const response = await fetch(`${HINDSIGHT_URL}/v1/default/banks/${bankId}/memories/retain`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content, + timestamp: options.timestamp, + context: options.context, + metadata: options.metadata, + document_id: options.documentId, + async: options.async, + }), + }); + return response.json(); + }, + + async recall(bankId, query, options = {}) { + const response = await fetch(`${HINDSIGHT_URL}/v1/default/banks/${bankId}/memories/recall`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query, + types: options.types, + max_tokens: options.maxTokens, + budget: options.budget, + trace: options.trace, + query_timestamp: options.queryTimestamp, + include_entities: options.includeEntities, + max_entity_tokens: options.maxEntityTokens, + include_chunks: options.includeChunks, + max_chunk_tokens: options.maxChunkTokens, + }), + }); + return response.json(); + }, + + async reflect(bankId, query, options = {}) { + const response = await fetch(`${HINDSIGHT_URL}/v1/default/banks/${bankId}/reflect`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query, + context: options.context, + budget: options.budget, + }), + }); + return response.json(); + }, +}; + +const tools = createHindsightTools({ client: httpClient }); +``` + +## Running Hindsight Locally + +The easiest way to run Hindsight for development: + +```bash +# Install and run with embedded mode (no setup required) +uvx hindsight-embed@latest -p myapp daemon start + +# The API will be available at http://localhost:8000 +``` + +For production deployments, see the [Hindsight Documentation](https://vectorize.io/hindsight). + +## TypeScript Types + +All types are exported for your convenience: + +```typescript +import type { + Budget, + HindsightClient, + HindsightTools, + HindsightToolsOptions, + RecallResult, + RecallResponse, + ReflectFact, + ReflectResponse, + RetainResponse, + EntityState, + ChunkData, +} from '@vectorize-io/hindsight-ai-sdk'; +``` + +## Documentation & Resources + +- [Hindsight Documentation](https://vectorize.io/hindsight) +- [Vercel AI SDK Documentation](https://ai-sdk.dev) +- [GitHub Repository](https://github.com/vectorize-io/hindsight) +- [Examples](https://github.com/vectorize-io/hindsight/tree/main/examples) + +## License + +MIT + +## Support + +For issues and questions: +- [GitHub Issues](https://github.com/vectorize-io/hindsight/issues) +- Email: support@vectorize.io diff --git a/hindsight-integrations/ai-sdk/package-lock.json b/hindsight-integrations/ai-sdk/package-lock.json new file mode 100644 index 00000000..e0756474 --- /dev/null +++ b/hindsight-integrations/ai-sdk/package-lock.json @@ -0,0 +1,1702 @@ +{ + "name": "@vectorize-io/hindsight-ai-sdk", + "version": "0.4.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vectorize-io/hindsight-ai-sdk", + "version": "0.4.8", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.0.0", + "@vitest/ui": "^4.0.18", + "ai": "^6.0.2", + "typescript": "^5.7.0", + "vitest": "^4.0.18", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "ai": "^6.0.0", + "zod": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/@ai-sdk/gateway": { + "version": "3.0.32", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.32.tgz", + "integrity": "sha512-7clZRr07P9rpur39t1RrbIe7x8jmwnwUWI8tZs+BvAfX3NFgdSVGGIaT7bTz2pb08jmLXzTSDbrOTqAQ7uBkBQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.7", + "@ai-sdk/provider-utils": "4.0.13", + "@vercel/oidc": "3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.7.tgz", + "integrity": "sha512-VkPLrutM6VdA924/mG8OS+5frbVTcu6e046D2bgDo00tehBANR1QBJ/mPcZ9tXMFOsVcm6SQArOregxePzTFPw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.13.tgz", + "integrity": "sha512-HHG72BN4d+OWTcq2NwTxOm/2qvk1duYsnhCDtsbYwn/h/4zeqURu1S0+Cn0nY2Ysq9a9HGKvrYuMn9bgFhR2Og==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "3.0.7", + "@standard-schema/spec": "^1.1.0", + "eventsource-parser": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.8.tgz", + "integrity": "sha512-ebO/Yl+EAvVe8DnMfi+iaAyIqYdK0q/q0y0rw82INWEKJOBe6b/P3YWE8NW7oOlF/nXFNrHwhARrN/hdgDkraA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vercel/oidc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", + "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.18.tgz", + "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ai": { + "version": "6.0.69", + "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.69.tgz", + "integrity": "sha512-zIURMSnNroaVvu47Bm3XhC2y3LRsm8jmkwBgupxF+N7q/s6MpIiv04w1ltlnWqC8+T2PT2rN+f0sUhF+vArkwg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "3.0.32", + "@ai-sdk/provider": "3.0.7", + "@ai-sdk/provider-utils": "4.0.13", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4.1.8" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/hindsight-integrations/ai-sdk/package.json b/hindsight-integrations/ai-sdk/package.json new file mode 100644 index 00000000..4dcacf83 --- /dev/null +++ b/hindsight-integrations/ai-sdk/package.json @@ -0,0 +1,58 @@ +{ + "name": "@vectorize-io/hindsight-ai-sdk", + "version": "0.4.8", + "description": "Hindsight memory integration for Vercel AI SDK - Give your AI agents persistent, human-like memory", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "keywords": [ + "ai", + "ai-sdk", + "vercel", + "memory", + "hindsight", + "agents", + "llm", + "long-term-memory" + ], + "author": "Vectorize ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/vectorize-io/hindsight.git", + "directory": "hindsight-integrations/ai-sdk" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest", + "prepublishOnly": "npm run clean && npm run build" + }, + "peerDependencies": { + "ai": "^6.0.0", + "zod": "^3.0.0 || ^4.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@vitest/ui": "^4.0.18", + "ai": "^6.0.2", + "typescript": "^5.7.0", + "vitest": "^4.0.18", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=22" + } +} diff --git a/hindsight-integrations/ai-sdk/src/index.ts b/hindsight-integrations/ai-sdk/src/index.ts new file mode 100644 index 00000000..b4378bd3 --- /dev/null +++ b/hindsight-integrations/ai-sdk/src/index.ts @@ -0,0 +1,15 @@ +export { + createHindsightTools, + BudgetSchema, + type Budget, + type HindsightClient, + type HindsightTools, + type HindsightToolsOptions, + type RecallResult, + type RecallResponse, + type ReflectFact, + type ReflectResponse, + type RetainResponse, + type EntityState, + type ChunkData, +} from './tools'; diff --git a/hindsight-integrations/ai-sdk/src/tools/index.test.ts b/hindsight-integrations/ai-sdk/src/tools/index.test.ts new file mode 100644 index 00000000..b431cda1 --- /dev/null +++ b/hindsight-integrations/ai-sdk/src/tools/index.test.ts @@ -0,0 +1,362 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createHindsightTools, type HindsightClient } from './index.js'; + +describe('createHindsightTools', () => { + let mockClient: HindsightClient; + + beforeEach(() => { + mockClient = { + retain: vi.fn(), + recall: vi.fn(), + reflect: vi.fn(), + }; + }); + + describe('tool creation', () => { + it('should create all three tools', () => { + const tools = createHindsightTools({ client: mockClient }); + + expect(tools).toHaveProperty('retain'); + expect(tools).toHaveProperty('recall'); + expect(tools).toHaveProperty('reflect'); + expect(typeof tools.retain.execute).toBe('function'); + expect(typeof tools.recall.execute).toBe('function'); + expect(typeof tools.reflect.execute).toBe('function'); + }); + + it('should use default descriptions when not provided', () => { + const tools = createHindsightTools({ client: mockClient }); + + expect(tools.retain.description).toContain('Store information in long-term memory'); + expect(tools.recall.description).toContain('Search memory for relevant information'); + expect(tools.reflect.description).toContain('Analyze memories to form insights'); + }); + + it('should use custom descriptions when provided', () => { + const tools = createHindsightTools({ + client: mockClient, + retainDescription: 'Custom retain description', + recallDescription: 'Custom recall description', + reflectDescription: 'Custom reflect description', + }); + + expect(tools.retain.description).toBe('Custom retain description'); + expect(tools.recall.description).toBe('Custom recall description'); + expect(tools.reflect.description).toBe('Custom reflect description'); + }); + }); + + describe('retain tool', () => { + it('should call client.retain with correct parameters', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.retain).mockResolvedValue({ + success: true, + bank_id: 'test-bank', + items_count: 5, + async: false, + }); + + const result = await tools.retain.execute({ + bankId: 'test-bank', + content: 'Test content', + }); + + expect(mockClient.retain).toHaveBeenCalledWith('test-bank', 'Test content', { + documentId: undefined, + timestamp: undefined, + context: undefined, + }); + expect(result).toEqual({ success: true, itemsCount: 5 }); + }); + + it('should pass optional parameters to client.retain', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.retain).mockResolvedValue({ + success: true, + bank_id: 'test-bank', + items_count: 3, + async: false, + }); + + await tools.retain.execute({ + bankId: 'test-bank', + content: 'Test content', + documentId: 'doc-123', + timestamp: '2024-01-01T00:00:00Z', + context: 'Test context', + }); + + expect(mockClient.retain).toHaveBeenCalledWith('test-bank', 'Test content', { + documentId: 'doc-123', + timestamp: '2024-01-01T00:00:00Z', + context: 'Test context', + }); + }); + + it('should transform response correctly', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.retain).mockResolvedValue({ + success: true, + bank_id: 'test-bank', + items_count: 10, + async: false, + }); + + const result = await tools.retain.execute({ + bankId: 'test-bank', + content: 'Test content', + }); + + expect(result).toEqual({ success: true, itemsCount: 10 }); + }); + }); + + describe('recall tool', () => { + it('should call client.recall with correct parameters', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.recall).mockResolvedValue({ + results: [ + { + id: 'fact-1', + text: 'Test fact', + type: 'preference', + }, + ], + }); + + const result = await tools.recall.execute({ + bankId: 'test-bank', + query: 'Test query', + }); + + expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', { + types: undefined, + maxTokens: undefined, + budget: undefined, + queryTimestamp: undefined, + includeEntities: undefined, + includeChunks: undefined, + }); + expect(result.results).toHaveLength(1); + expect(result.results[0].id).toBe('fact-1'); + }); + + it('should pass all optional parameters to client.recall', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.recall).mockResolvedValue({ + results: [], + }); + + await tools.recall.execute({ + bankId: 'test-bank', + query: 'Test query', + types: ['preference', 'fact'], + maxTokens: 1000, + budget: 'high', + queryTimestamp: '2024-01-01T00:00:00Z', + includeEntities: true, + includeChunks: true, + }); + + expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test query', { + types: ['preference', 'fact'], + maxTokens: 1000, + budget: 'high', + queryTimestamp: '2024-01-01T00:00:00Z', + includeEntities: true, + includeChunks: true, + }); + }); + + it('should handle empty results', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.recall).mockResolvedValue({ + results: undefined as any, + }); + + const result = await tools.recall.execute({ + bankId: 'test-bank', + query: 'Test query', + }); + + expect(result.results).toEqual([]); + }); + + it('should include entities when present', async () => { + const tools = createHindsightTools({ client: mockClient }); + const entities = { + 'entity-1': { + entity_id: 'entity-1', + canonical_name: 'Alice', + observations: [{ text: 'Alice loves hiking' }], + }, + }; + + vi.mocked(mockClient.recall).mockResolvedValue({ + results: [], + entities, + }); + + const result = await tools.recall.execute({ + bankId: 'test-bank', + query: 'Test query', + includeEntities: true, + }); + + expect(result.entities).toEqual(entities); + }); + }); + + describe('reflect tool', () => { + it('should call client.reflect with correct parameters', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.reflect).mockResolvedValue({ + text: 'Reflection result', + based_on: [ + { + id: 'fact-1', + text: 'Supporting fact', + }, + ], + }); + + const result = await tools.reflect.execute({ + bankId: 'test-bank', + query: 'What are my preferences?', + }); + + expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', { + context: undefined, + budget: undefined, + }); + expect(result.text).toBe('Reflection result'); + expect(result.basedOn).toHaveLength(1); + }); + + it('should pass optional parameters to client.reflect', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.reflect).mockResolvedValue({ + text: 'Reflection result', + }); + + await tools.reflect.execute({ + bankId: 'test-bank', + query: 'What are my preferences?', + context: 'User context', + budget: 'mid', + }); + + expect(mockClient.reflect).toHaveBeenCalledWith('test-bank', 'What are my preferences?', { + context: 'User context', + budget: 'mid', + }); + }); + + it('should handle empty text response with fallback', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.reflect).mockResolvedValue({ + text: undefined as any, + }); + + const result = await tools.reflect.execute({ + bankId: 'test-bank', + query: 'Test query', + }); + + expect(result.text).toBe('No insights available yet.'); + }); + + it('should include basedOn facts when present', async () => { + const tools = createHindsightTools({ client: mockClient }); + const basedOn = [ + { + id: 'fact-1', + text: 'User prefers spicy food', + type: 'preference', + }, + { + id: 'fact-2', + text: 'User is allergic to nuts', + type: 'health', + }, + ]; + + vi.mocked(mockClient.reflect).mockResolvedValue({ + text: 'Based on your history, you prefer spicy Asian cuisine', + based_on: basedOn, + }); + + const result = await tools.reflect.execute({ + bankId: 'test-bank', + query: 'What do I like?', + }); + + expect(result.basedOn).toEqual(basedOn); + }); + }); + + describe('error handling', () => { + it('should propagate errors from client.retain', async () => { + const tools = createHindsightTools({ client: mockClient }); + const error = new Error('Retain failed'); + vi.mocked(mockClient.retain).mockRejectedValue(error); + + await expect( + tools.retain.execute({ + bankId: 'test-bank', + content: 'Test content', + }) + ).rejects.toThrow('Retain failed'); + }); + + it('should propagate errors from client.recall', async () => { + const tools = createHindsightTools({ client: mockClient }); + const error = new Error('Recall failed'); + vi.mocked(mockClient.recall).mockRejectedValue(error); + + await expect( + tools.recall.execute({ + bankId: 'test-bank', + query: 'Test query', + }) + ).rejects.toThrow('Recall failed'); + }); + + it('should propagate errors from client.reflect', async () => { + const tools = createHindsightTools({ client: mockClient }); + const error = new Error('Reflect failed'); + vi.mocked(mockClient.reflect).mockRejectedValue(error); + + await expect( + tools.reflect.execute({ + bankId: 'test-bank', + query: 'Test query', + }) + ).rejects.toThrow('Reflect failed'); + }); + }); + + describe('budget schema', () => { + it('should accept valid budget values', async () => { + const tools = createHindsightTools({ client: mockClient }); + vi.mocked(mockClient.recall).mockResolvedValue({ results: [] }); + + for (const budget of ['low', 'mid', 'high'] as const) { + await tools.recall.execute({ + bankId: 'test-bank', + query: 'Test', + budget, + }); + + expect(mockClient.recall).toHaveBeenCalledWith('test-bank', 'Test', { + types: undefined, + maxTokens: undefined, + budget, + queryTimestamp: undefined, + includeEntities: undefined, + includeChunks: undefined, + }); + } + }); + }); +}); diff --git a/hindsight-integrations/ai-sdk/src/tools/index.ts b/hindsight-integrations/ai-sdk/src/tools/index.ts new file mode 100644 index 00000000..de9f2d73 --- /dev/null +++ b/hindsight-integrations/ai-sdk/src/tools/index.ts @@ -0,0 +1,559 @@ +import { tool } from 'ai'; +import { z } from 'zod'; + +/** + * Budget levels for recall/reflect operations. + */ +export const BudgetSchema = z.enum(['low', 'mid', 'high']); +export type Budget = z.infer; + +/** + * Recall result item from Hindsight + */ +export interface RecallResult { + id: string; + text: string; + type?: string | null; + entities?: string[] | null; + context?: string | null; + occurred_start?: string | null; + occurred_end?: string | null; + mentioned_at?: string | null; + document_id?: string | null; + metadata?: Record | null; + chunk_id?: string | null; +} + +/** + * Entity state with observations + */ +export interface EntityState { + entity_id: string; + canonical_name: string; + observations: Array<{ text: string; mentioned_at?: string | null }>; +} + +/** + * Chunk data + */ +export interface ChunkData { + id: string; + text: string; + chunk_index: number; + truncated?: boolean; +} + +/** + * Recall response from Hindsight + */ +export interface RecallResponse { + results: RecallResult[]; + trace?: Record | null; + entities?: Record | null; + chunks?: Record | null; +} + +/** + * Reflect fact + */ +export interface ReflectFact { + id?: string | null; + text: string; + type?: string | null; + context?: string | null; + occurred_start?: string | null; + occurred_end?: string | null; +} + +/** + * Reflect response from Hindsight + */ +export interface ReflectResponse { + text: string; + based_on?: ReflectFact[]; +} + +/** + * Retain response from Hindsight + */ +export interface RetainResponse { + success: boolean; + bank_id: string; + items_count: number; + async: boolean; +} + +/** + * Mental model trigger configuration + */ +export interface MentalModelTrigger { + refresh_after_consolidation?: boolean; +} + +/** + * Mental model response from Hindsight + */ +export interface MentalModelResponse { + mental_model_id: string; + bank_id: string; + name?: string; + content?: string; + source_query?: string; + tags?: string[]; + created_at: string; + updated_at: string; + trigger?: MentalModelTrigger; +} + +/** + * Create mental model response from Hindsight + */ +export interface CreateMentalModelResponse { + mental_model_id: string; + bank_id: string; + created_at: string; +} + +/** + * Document response from Hindsight + */ +export interface DocumentResponse { + id: string; + bank_id: string; + original_text: string; + content_hash: string | null; + created_at: string; + updated_at: string; + memory_unit_count: number; + tags?: string[]; +} + +/** + * Directive response from Hindsight + */ +export interface DirectiveResponse { + id: string; + bank_id: string; + name: string; + content: string; + priority: number; + is_active: boolean; + tags: string[]; + created_at: string; + updated_at: string; +} + +/** + * Create directive response from Hindsight + */ +export interface CreateDirectiveResponse { + id: string; + bank_id: string; + name: string; + content: string; + priority: number; + is_active: boolean; + tags: string[]; + created_at: string; + updated_at: string; +} + +/** + * Hindsight client interface - matches @vectorize-io/hindsight-client + */ +export interface HindsightClient { + retain( + bankId: string, + content: string, + options?: { + timestamp?: Date | string; + context?: string; + metadata?: Record; + documentId?: string; + tags?: string[]; + async?: boolean; + } + ): Promise; + + recall( + bankId: string, + query: string, + options?: { + types?: string[]; + maxTokens?: number; + budget?: Budget; + trace?: boolean; + queryTimestamp?: string; + includeEntities?: boolean; + maxEntityTokens?: number; + includeChunks?: boolean; + maxChunkTokens?: number; + } + ): Promise; + + reflect( + bankId: string, + query: string, + options?: { + context?: string; + budget?: Budget; + } + ): Promise; + + createMentalModel( + bankId: string, + options?: { + id?: string; + name?: string; + sourceQuery?: string; + tags?: string[]; + maxTokens?: number; + trigger?: MentalModelTrigger; + } + ): Promise; + + getMentalModel( + bankId: string, + mentalModelId: string + ): Promise; + + getDocument( + bankId: string, + documentId: string + ): Promise; + + createDirective( + bankId: string, + options: { + name: string; + content: string; + priority?: number; + isActive?: boolean; + tags?: string[]; + } + ): Promise; + + getDirective( + bankId: string, + directiveId: string + ): Promise; + + listDirectives( + bankId: string, + options?: { + tags?: string[]; + tagsMatch?: 'any' | 'all' | 'exact'; + activeOnly?: boolean; + limit?: number; + offset?: number; + } + ): Promise<{ directives: DirectiveResponse[]; total: number }>; +} + +export interface HindsightToolsOptions { + /** Hindsight client instance */ + client: HindsightClient; + /** + * Custom description for the retain tool. + */ + retainDescription?: string; + /** + * Custom description for the recall tool. + */ + recallDescription?: string; + /** + * Custom description for the reflect tool. + */ + reflectDescription?: string; + /** + * Custom description for the createMentalModel tool. + */ + createMentalModelDescription?: string; + /** + * Custom description for the queryMentalModel tool. + */ + queryMentalModelDescription?: string; + /** + * Custom description for the getDocument tool. + */ + getDocumentDescription?: string; +} + +/** + * Creates AI SDK tools for Hindsight memory operations. + * + * Features: + * - Dynamic bank ID per call (supports multi-user/multi-bank scenarios) + * - Full API parameter support for retain, recall, and reflect + * - Ready to use with streamText, generateText, or ToolLoopAgent + * + * @example + * ```ts + * const tools = createHindsightTools({ + * client: hindsightClient, + * }); + * + * // Use with AI SDK + * const result = await generateText({ + * model: openai('gpt-4'), + * tools, + * prompt: 'Remember that Alice loves hiking', + * }); + * ``` + */ +export function createHindsightTools({ + client, + retainDescription, + recallDescription, + reflectDescription, + createMentalModelDescription, + queryMentalModelDescription, + getDocumentDescription, +}: HindsightToolsOptions) { + const retainParams = z.object({ + bankId: z.string().describe('Memory bank ID (usually the user ID)'), + content: z.string().describe('Content to store in memory'), + documentId: z.string().optional().describe('Optional document ID for grouping/upserting content'), + timestamp: z.string().optional().describe('Optional ISO timestamp for when the memory occurred'), + context: z.string().optional().describe('Optional context about the memory'), + tags: z.array(z.string()).optional().describe('Optional tags for visibility scoping'), + metadata: z.record(z.string(), z.string()).optional().describe('Optional user-defined metadata'), + }); + + const recallParams = z.object({ + bankId: z.string().describe('Memory bank ID (usually the user ID)'), + query: z.string().describe('What to search for in memory'), + types: z.array(z.string()).optional().describe('Filter by fact types'), + maxTokens: z.number().optional().describe('Maximum tokens to return'), + budget: BudgetSchema.optional().describe('Processing budget: low, mid, or high'), + queryTimestamp: z.string().optional().describe('Query from a specific point in time (ISO format)'), + includeEntities: z.boolean().optional().describe('Include entity observations in results'), + includeChunks: z.boolean().optional().describe('Include raw chunks in results'), + }); + + const reflectParams = z.object({ + bankId: z.string().describe('Memory bank ID (usually the user ID)'), + query: z.string().describe('Question to reflect on based on memories'), + context: z.string().optional().describe('Additional context for the reflection'), + budget: BudgetSchema.optional().describe('Processing budget: low, mid, or high'), + }); + + const createMentalModelParams = z.object({ + bankId: z.string().describe('Memory bank ID (usually the user ID)'), + mentalModelId: z.string().optional().describe('Optional custom ID for the mental model (auto-generated if not provided)'), + name: z.string().optional().describe('Optional name for the mental model'), + sourceQuery: z.string().optional().describe('Query to define what memories to consolidate'), + tags: z.array(z.string()).optional().describe('Optional tags for organizing mental models'), + maxTokens: z.number().optional().describe('Maximum tokens for the mental model content'), + autoRefresh: z.boolean().optional().describe('Auto-refresh mental model after new consolidations (default: false)'), + }); + + const queryMentalModelParams = z.object({ + bankId: z.string().describe('Memory bank ID (usually the user ID)'), + mentalModelId: z.string().describe('ID of the mental model to query'), + }); + + const getDocumentParams = z.object({ + bankId: z.string().describe('Memory bank ID (usually the user ID)'), + documentId: z.string().describe('ID of the document to retrieve'), + }); + + const createDirectiveParams = z.object({ + bankId: z.string().describe('Memory bank ID (usually the user ID)'), + name: z.string().describe('Human-readable name for the directive'), + content: z.string().describe('The directive text to inject into prompts'), + priority: z.number().optional().describe('Higher priority directives are injected first (default 0)'), + isActive: z.boolean().optional().describe('Whether this directive is active (default true)'), + tags: z.array(z.string()).optional().describe('Tags for filtering'), + }); + + const getDirectiveParams = z.object({ + bankId: z.string().describe('Memory bank ID (usually the user ID)'), + directiveId: z.string().describe('ID of the directive to retrieve'), + }); + + type RetainInput = z.infer; + type RetainOutput = { success: boolean; itemsCount: number }; + + type RecallInput = z.infer; + type RecallOutput = { results: RecallResult[]; entities?: Record | null }; + + type ReflectInput = z.infer; + type ReflectOutput = { text: string; basedOn?: ReflectFact[] }; + + type CreateMentalModelInput = z.infer; + type CreateMentalModelOutput = { mentalModelId: string; createdAt: string }; + + type QueryMentalModelInput = z.infer; + type QueryMentalModelOutput = { content: string; name?: string; updatedAt: string }; + + type GetDocumentInput = z.infer; + type GetDocumentOutput = { originalText: string; id: string; createdAt: string; updatedAt: string } | null; + + type CreateDirectiveInput = z.infer; + type CreateDirectiveOutput = { id: string; name: string; content: string; tags: string[]; createdAt: string }; + + type GetDirectiveInput = z.infer; + type GetDirectiveOutput = { id: string; name: string; content: string; tags: string[]; isActive: boolean } | null; + + return { + retain: tool({ + description: + retainDescription ?? + `Store information in long-term memory. Use this when information should be remembered for future interactions, such as user preferences, facts, experiences, or important context.`, + inputSchema: retainParams, + execute: async (input) => { + console.log('[AI SDK Tool] Retain input:', { + bankId: input.bankId, + documentId: input.documentId, + tags: input.tags, + hasContent: !!input.content, + }); + const result = await client.retain(input.bankId, input.content, { + documentId: input.documentId, + timestamp: input.timestamp, + context: input.context, + tags: input.tags, + metadata: input.metadata as Record | undefined, + }); + return { success: result.success, itemsCount: result.items_count }; + }, + }), + + recall: tool({ + description: + recallDescription ?? + `Search memory for relevant information. Use this to find previously stored information that can help personalize responses or provide context.`, + inputSchema: recallParams, + execute: async (input) => { + const result = await client.recall(input.bankId, input.query, { + types: input.types, + maxTokens: input.maxTokens, + budget: input.budget, + queryTimestamp: input.queryTimestamp, + includeEntities: input.includeEntities, + includeChunks: input.includeChunks, + }); + return { + results: result.results ?? [], + entities: result.entities, + }; + }, + }), + + reflect: tool({ + description: + reflectDescription ?? + `Analyze memories to form insights and generate contextual answers. Use this to understand patterns, synthesize information, or answer questions that require reasoning over stored memories.`, + inputSchema: reflectParams, + execute: async (input) => { + const result = await client.reflect(input.bankId, input.query, { + context: input.context, + budget: input.budget, + }); + return { + text: result.text ?? 'No insights available yet.', + basedOn: result.based_on, + }; + }, + }), + + createMentalModel: tool({ + description: + createMentalModelDescription ?? + `Create a mental model that automatically consolidates memories into structured knowledge. Mental models are continuously updated as new memories are added, making them ideal for maintaining up-to-date user preferences, behavioral patterns, and accumulated wisdom.`, + inputSchema: createMentalModelParams, + execute: async (input) => { + const result = await client.createMentalModel(input.bankId, { + id: input.mentalModelId, + name: input.name, + sourceQuery: input.sourceQuery, + tags: input.tags, + maxTokens: input.maxTokens, + trigger: input.autoRefresh !== undefined ? { refresh_after_consolidation: input.autoRefresh } : undefined, + }); + return { + mentalModelId: result.mental_model_id, + createdAt: result.created_at, + }; + }, + }), + + queryMentalModel: tool({ + description: + queryMentalModelDescription ?? + `Query an existing mental model to retrieve consolidated knowledge. Mental models provide synthesized insights from memories, making them faster and more efficient than searching through raw memories.`, + inputSchema: queryMentalModelParams, + execute: async (input) => { + const result = await client.getMentalModel(input.bankId, input.mentalModelId); + return { + content: result.content ?? 'No content available yet.', + name: result.name, + updatedAt: result.updated_at, + }; + }, + }), + + getDocument: tool({ + description: + getDocumentDescription ?? + `Retrieve a stored document by its ID. Documents are used to store structured data like application state, user profiles, or any data that needs exact retrieval.`, + inputSchema: getDocumentParams, + execute: async (input) => { + const result = await client.getDocument(input.bankId, input.documentId); + if (!result) { + return null; + } + return { + originalText: result.original_text, + id: result.id, + createdAt: result.created_at, + updatedAt: result.updated_at, + }; + }, + }), + + createDirective: tool({ + description: + `Create a directive - a hard rule that is injected into prompts during reflect operations. Directives are explicit instructions that guide agent behavior. Use tags to control when directives are applied (e.g., user-specific directives with 'user:username' tags).`, + inputSchema: createDirectiveParams, + execute: async (input) => { + const result = await client.createDirective(input.bankId, { + name: input.name, + content: input.content, + priority: input.priority, + isActive: input.isActive, + tags: input.tags, + }); + return { + id: result.id, + name: result.name, + content: result.content, + tags: result.tags, + createdAt: result.created_at, + }; + }, + }), + + getDirective: tool({ + description: + `Retrieve a directive by its ID. Returns the directive's content, tags, and active status.`, + inputSchema: getDirectiveParams, + execute: async (input) => { + const result = await client.getDirective(input.bankId, input.directiveId); + if (!result) { + return null; + } + return { + id: result.id, + name: result.name, + content: result.content, + tags: result.tags, + isActive: result.is_active, + }; + }, + }), + }; +} + +export type HindsightTools = ReturnType; diff --git a/hindsight-integrations/ai-sdk/tsconfig.json b/hindsight-integrations/ai-sdk/tsconfig.json new file mode 100644 index 00000000..d1f4cf02 --- /dev/null +++ b/hindsight-integrations/ai-sdk/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} diff --git a/hindsight-integrations/ai-sdk/vitest.config.ts b/hindsight-integrations/ai-sdk/vitest.config.ts new file mode 100644 index 00000000..7dd13254 --- /dev/null +++ b/hindsight-integrations/ai-sdk/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/scripts/release.sh b/scripts/release.sh index 622a5a27..6d4a0c18 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -154,6 +154,16 @@ else print_warn "File $OPENCLAW_PKG not found, skipping" fi +# Update AI SDK integration +AI_SDK_PKG="hindsight-integrations/ai-sdk/package.json" +if [ -f "$AI_SDK_PKG" ]; then + print_info "Updating $AI_SDK_PKG" + sed -i.bak "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" "$AI_SDK_PKG" + rm "${AI_SDK_PKG}.bak" +else + print_warn "File $AI_SDK_PKG not found, skipping" +fi + # Update documentation version (creates new version or syncs to existing) print_info "Updating documentation for version $VERSION..." if [ -f "scripts/update-docs-version.sh" ]; then @@ -202,6 +212,7 @@ COMMIT_MSG="Release v$VERSION - Rust CLI: hindsight-cli - Control Plane: hindsight-control-plane - OpenClaw integration: hindsight-integrations/openclaw +- AI SDK integration: hindsight-integrations/ai-sdk - Helm chart" # Add docs update note