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 @@ async def api_create_mental_model( 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 @@ ## 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 @@ async def _handle_refresh_mental_model(self, task_dict: dict[str, Any]): 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 @@ async def list_documents( 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 @@ async def list_documents( "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 @@ async def refresh_mental_model( 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 @@ def test_system_prompt_includes_directives(self): 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 @@ async def test_custom_extraction_mode(): # 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 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: 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
+
+
+ )} - {loadingDocument ? ( -
-
-
-
Loading document...
-
+ {/* 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}
- ) : ( -
- {/* Document ID */} +
+ + {/* Created & Memory Units */} + {selectedDocument.created_at && ( +
- Document ID + Created
-
- {selectedDocument.id} +
+ {new Date(selectedDocument.created_at).toLocaleString()}
- - {/* Created & Memory Units */} - {selectedDocument.created_at && ( -
-
-
- Created -
-
- {new Date(selectedDocument.created_at).toLocaleString()} -
-
-
-
- Memory Units -
-
- {selectedDocument.memory_unit_count} -
-
+
+
+ Memory Units
- )} - - {/* Text Length */} - {selectedDocument.original_text && ( -
-
- Text Length -
-
- {selectedDocument.original_text.length.toLocaleString()} characters -
+
+ {selectedDocument.memory_unit_count}
- )} +
+
+ )} - {/* 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)}
-                              
-
- )} -
-
- )} + {/* Text Length */} + {selectedDocument.original_text && ( +
+
+ Text Length +
+
+ {selectedDocument.original_text.length.toLocaleString()} characters +
+
+ )} - {/* Tags */} - {selectedDocument.tags && selectedDocument.tags.length > 0 && ( -
-
- Tags + {/* Retain Parameters */} + {selectedDocument.retain_params && ( +
+
+ Retain Parameters +
+
+ {selectedDocument.retain_params.context && ( +
+ Context:{" "} + {selectedDocument.retain_params.context}
-
- {selectedDocument.tags.map((tag: string, i: number) => ( - - {tag} - - ))} + )} + {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)}
+                          
+
+ )} +
+
+ )} - {/* Delete Button */} -
- + {/* Tags */} + {selectedDocument.tags && selectedDocument.tags.length > 0 && ( +
+
+ Tags +
+
+ {selectedDocument.tags.map((tag: string, i: number) => ( + + {tag} + + ))}
+
+ )} - {/* Original Text */} - {selectedDocument.original_text && ( -
-
- Original Text -
-
-
-                            {selectedDocument.original_text}
-                          
-
-
+ {/* 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