diff --git a/.env.example b/.env.example index 29bbc80..c038567 100644 --- a/.env.example +++ b/.env.example @@ -3,15 +3,23 @@ ANTHROPIC_API_KEY= OPENAI_API_KEY= +# Default: beam:cloud through a local Ollama daemon (run `ollama signin` +# once for cloud models). Any tagged Ollama name works (e.g. qwen3:8b). # chat-latest follows ChatGPT Instant. gpt-* uses OpenAI; claude-* uses Anthropic. # Model access/availability must be verified with your provider. -# Blank or unsupported model prefixes fail at startup. -# Keys are optional for BYOK-only hosting; visitors enter both keys in the app. -LLM_MODEL=chat-latest +# Blank or unsupported model names fail at startup. +# With Ollama models no keys are needed; visitors may still add their own. +LLM_MODEL=beam:cloud +# OLLAMA_BASE_URL=http://localhost:11434/v1 +# OLLAMA_API_KEY= # Jev selects the renderer and visualization before each new user turn. +# Default: clef-flash in local Ollama (`ollama pull clef-flash`), no key needed. +# For hosted Jev set JEV_URL=https://api.typesafe.ai/v1/systemone, +# JEV_MODEL=jev-latest and TYPESAFE_API_KEY. +JEV_MODEL=clef-flash:latest +# JEV_URL=http://localhost:11434/v1/systemone TYPESAFE_API_KEY= -JEV_MODEL=jev-latest # Frontend settings: place these in apps/app/.env.local. # LANGGRAPH_DEPLOYMENT_URL defaults to http://localhost:8123. diff --git a/CLAUDE.md b/CLAUDE.md index d41dcb4..4bae3dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,7 +29,7 @@ Use the existing validated `Websandbox.connection.remote.sendPrompt({ text })` a ## Configuration and checks -The local default model is `chat-latest` (ChatGPT Instant). `claude-*` requires `ANTHROPIC_API_KEY`; `chat-latest` and `gpt-*` require `OPENAI_API_KEY`. Jev routing requires `TYPESAFE_API_KEY`. Do not invent native model APIs or silently change pinned model IDs. See `render.yaml` for its separate explicit deployment model setting. +The local default answer model is `beam:cloud` through Ollama's OpenAI-compatible API (`OLLAMA_BASE_URL`, default `http://localhost:11434/v1`); any tagged Ollama name works without a key. `claude-*` requires `ANTHROPIC_API_KEY`; `chat-latest` and `gpt-*` require `OPENAI_API_KEY`. Jev routing defaults to `clef-flash:latest` at Ollama's `/v1/systemone` (`JEV_URL`, `JEV_MODEL`); only hosted TypeSafe requires `TYPESAFE_API_KEY`. Visitor BYOK keys always use OpenAI `chat-latest` and hosted Jev, and the chat asks for them only when `GET /credentials/status` reports the server's own models unconfigured. Do not invent native model APIs or silently change pinned model IDs. See `render.yaml` for its separate explicit deployment model setting, which stays on OpenAI and hosted Jev. ```bash pnpm install diff --git a/Makefile b/Makefile index bc30108..3b443f7 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,8 @@ install: ## Install all dependencies (Node + Python) setup: install ## Full setup: install deps and create .env from template @if [ ! -f apps/agent/.env ]; then \ - printf '%s\n' 'OPENAI_API_KEY=' 'TYPESAFE_API_KEY=' 'LLM_MODEL=chat-latest' 'JEV_MODEL=jev-latest' > apps/agent/.env; \ - echo "Created apps/agent/.env — set OPENAI_API_KEY and TYPESAFE_API_KEY"; \ + printf '%s\n' 'LLM_MODEL=beam:cloud' 'JEV_MODEL=clef-flash:latest' 'OPENAI_API_KEY=' 'TYPESAFE_API_KEY=' > apps/agent/.env; \ + echo "Created apps/agent/.env for Ollama — run 'ollama signin' and 'ollama pull clef-flash'"; \ else \ echo "apps/agent/.env already exists, skipping"; \ fi diff --git a/README.md b/README.md index 26671fa..fbc3e35 100644 --- a/README.md +++ b/README.md @@ -140,28 +140,27 @@ These starters run the agent; they are not prerecorded responses. Jev chooses th ## Get started -Prerequisites: Node.js 22+, pnpm 9+, Python 3.12+, and [uv](https://docs.astral.sh/uv/). +Prerequisites: Node.js 22+, pnpm 9+, Python 3.12+, [uv](https://docs.astral.sh/uv/), and [Ollama](https://ollama.com) signed in with `ollama signin` (needed for `:cloud` models). ```bash git clone https://github.com/CopilotKit/OpenIntelligentUI.git cd OpenIntelligentUI make setup -# Optional: set shared OPENAI_API_KEY and TYPESAFE_API_KEY in apps/agent/.env -# Or use API keys in the chat header after starting the app +ollama pull clef-flash:latest # local visualization router make dev ``` Open the [app](http://localhost:3000). The [agent health endpoint](http://localhost:8123/health) confirms the agent service is running. See [Getting started](docs/getting-started.md) for configuration and verification. -The default model is `chat-latest`, OpenAI’s documented alias for the latest ChatGPT Instant model, requiring `OPENAI_API_KEY`. Jev (`jev-latest`, requiring `TYPESAFE_API_KEY`) selects the renderer and visualization for each user turn. Basic tables use A2UI; charts, diagrams, calculators, and maps use Open Generative UI. Provider failures are surfaced instead of silently substituting another router or model. See [Visualization routing](docs/visualization-routing.md). +The default answer model is `beam:cloud`, served through your local Ollama daemon's OpenAI-compatible API. The visualization router defaults to `clef-flash:latest`, which answers Jev's System One API in the same daemon. Neither needs an API key. To use hosted providers instead, see [Getting started](docs/getting-started.md#configure-the-agent). The router selects the renderer and visualization for each user turn. Basic tables use A2UI; charts, diagrams, calculators, and maps use Open Generative UI. Provider failures are surfaced instead of silently substituting another router or model. See [Visualization routing](docs/visualization-routing.md). ## Use your own API keys -The chat asks for both keys before sending your first message or example prompt and keeps your draft while you set them up. Open **API keys** in the chat header, enter an OpenAI key and a Jev key, then choose **Test connection** and **Save keys**. The connection test makes a small request to each provider. Your keys use `chat-latest` for answers and `jev-latest` for visualization routing, with usage billed to your provider accounts. +With the default local models the chat never asks for keys. If the server's own models are not configured, the chat asks for both keys before sending your first message or example prompt and keeps your draft while you set them up. Open **API keys** in the chat header, enter an OpenAI key and a Jev key, then choose **Test connection** and **Save keys**. The connection test makes a small request to each provider. Your keys use `chat-latest` for answers and `jev-latest` for visualization routing, with usage billed to your provider accounts. Keys stay in browser memory until refresh or **Clear keys**; the app does not save them in browser storage. Requests send them through the application server to their respective providers. They are kept out of chat state and checkpoints, and hosted LangSmith tracing is disabled for requests using your keys. Only use this feature on a deployment whose operator you trust. Saving or clearing keys starts a new chat; **New chat** keeps your keys for the current session. -The agent can run without shared provider keys. Visitors must then supply both keys before chatting. Configured server credentials remain available to direct API clients that omit visitor keys; the chat interface requires visitors to add their own keys. +The chat checks `GET /credentials/status` on the agent. It asks visitors for keys only when the server's own answer model or router is unconfigured, for example a hosted `LLM_MODEL` without its key. Visitor keys always use OpenAI `chat-latest` and hosted Jev, never the server's Ollama models. ## How it works diff --git a/apps/agent/main.py b/apps/agent/main.py index 9fec803..eb56e71 100644 --- a/apps/agent/main.py +++ b/apps/agent/main.py @@ -20,6 +20,7 @@ CredentialsMiddleware, CredentialScopedAgent, current_credentials, + server_credentials_available, validate_credentials, ) from src.visualization_router import JevVisualizationMiddleware @@ -61,6 +62,12 @@ async def validate_provider_credentials(): return await validate_credentials(credentials) +@app.get("/credentials/status") +def provider_credentials_status(): + # Lets the chat skip the API-key prompt when the server's models are configured. + return {"required": not server_credentials_available()} + + @app.get("/health") def health(): return {"status": "ok"} diff --git a/apps/agent/src/credentials.py b/apps/agent/src/credentials.py index ea8cac9..b52d4fb 100644 --- a/apps/agent/src/credentials.py +++ b/apps/agent/src/credentials.py @@ -46,9 +46,12 @@ def parse_credentials(headers): def server_credentials_available(): - model = os.environ.get('LLM_MODEL', 'chat-latest').strip() - key = 'ANTHROPIC_API_KEY' if model.startswith('claude-') else 'OPENAI_API_KEY' - return bool(os.environ.get(key, '').strip() and os.environ.get('TYPESAFE_API_KEY', '').strip()) + """Whether the server's own models can answer without visitor keys.""" + from src.model import DEFAULT_MODEL, provider_key_name + from src.visualization_router import router_key_required + model_key = provider_key_name(os.environ.get('LLM_MODEL', DEFAULT_MODEL).strip()) + router_key = 'TYPESAFE_API_KEY' if router_key_required() else None + return all(os.environ.get(key, '').strip() for key in (model_key, router_key) if key) class CredentialsMiddleware: diff --git a/apps/agent/src/model.py b/apps/agent/src/model.py index 33725ab..eb516e2 100644 --- a/apps/agent/src/model.py +++ b/apps/agent/src/model.py @@ -1,5 +1,6 @@ """Chat model factory for the agent.""" +import json import os from contextlib import contextmanager @@ -10,7 +11,11 @@ from langchain_anthropic import ChatAnthropic from langchain_core.language_models.chat_models import BaseChatModel -DEFAULT_MODEL = "chat-latest" +# The server default runs through a local Ollama daemon (cloud models need +# `ollama signin`). Visitor BYOK keys always use OpenAI's chat-latest instead. +DEFAULT_MODEL = "beam:cloud" +BYOK_MODEL = "chat-latest" +DEFAULT_OLLAMA_URL = "http://localhost:11434/v1" # langchain-anthropic defaults max_tokens to 4096, which truncates # generateSandboxedUi tool args mid-stream (jsFunctions/jsExpressions never @@ -19,15 +24,44 @@ MAX_TOKENS = 64000 +def provider_key_name(model_name: str) -> str | None: + """Name the API key a model needs; None means an Ollama model or an invalid name.""" + if is_ollama_model(model_name): + # Checked first: Ollama tags such as gpt-oss:20b-cloud are not OpenAI models. + return None + if model_name == "chat-latest" or model_name.startswith("gpt-"): + return "OPENAI_API_KEY" + if model_name.startswith("claude-"): + return "ANTHROPIC_API_KEY" + return None + + +def is_ollama_model(model_name: str) -> bool: + # Ollama names carry an explicit tag (beam:cloud); this keeps typos + # like "gemini-example" failing at startup instead of at the first request. + name, _, tag = model_name.partition(":") + return bool(name and tag) and not any(c.isspace() for c in model_name) + + def build_model(*, allow_unconfigured: bool = False) -> BaseChatModel: model_name = os.environ.get("LLM_MODEL", DEFAULT_MODEL).strip() - is_openai = model_name == "chat-latest" or model_name.startswith("gpt-") - if not is_openai and not model_name.startswith("claude-"): + key_name = provider_key_name(model_name) + if key_name is None and not is_ollama_model(model_name): raise ValueError( - "LLM_MODEL must name a supported provider: claude-* (Anthropic) " - "or gpt-*/chat-latest (OpenAI). Unset LLM_MODEL to use the default." + "LLM_MODEL must name a supported provider: an Ollama model with a tag " + "(e.g. beam:cloud), claude-* (Anthropic) or gpt-*/chat-latest " + "(OpenAI). Unset LLM_MODEL to use the default." + ) + from langchain_openai import ChatOpenAI + + if key_name is None: + # Ollama serves an OpenAI-compatible API; a local daemon ignores the key. + return ChatOpenAI( + model=model_name, + base_url=os.environ.get("OLLAMA_BASE_URL", "").strip() or DEFAULT_OLLAMA_URL, + api_key=os.environ.get("OLLAMA_API_KEY", "").strip() or "ollama", + max_tokens=MAX_TOKENS, ) - key_name = "OPENAI_API_KEY" if is_openai else "ANTHROPIC_API_KEY" if not os.environ.get(key_name, "").strip(): if allow_unconfigured: return UnconfiguredModel() @@ -35,10 +69,8 @@ def build_model(*, allow_unconfigured: bool = False) -> BaseChatModel: f"{key_name} is required for the selected LLM_MODEL provider. " "Set it in the agent environment and restart the agent." ) - if is_openai: + if key_name == "OPENAI_API_KEY": # chat-latest follows ChatGPT Instant; explicit gpt-* pins also work. - from langchain_openai import ChatOpenAI - return ChatOpenAI(model=model_name) return ChatAnthropic( model=model_name, @@ -91,7 +123,7 @@ def request_model(fallback): # Clear that merge before any request so host Authorization cannot win. client._custom_headers = {} model = ChatOpenAI( - model=DEFAULT_MODEL, + model=BYOK_MODEL, api_key=credentials.openai, base_url=options["base_url"], openai_proxy="", @@ -163,17 +195,84 @@ async def _agenerate(self, messages, stop=None, run_manager=None, **kwargs): def _stream(self, messages, stop=None, run_manager=None, **kwargs): with provider_error_boundary(): model, options = self._target(kwargs) - for chunk in model.stream(messages, stop=stop, **options): - yield ChatGenerationChunk(message=as_chunk(chunk)) + for chunk in model.stream(messages, stop=stop, config=INNER_STREAM, **options): + for part in split_tool_call_chunk(as_chunk(chunk)): + yield ChatGenerationChunk(message=part) async def _astream(self, messages, stop=None, run_manager=None, **kwargs): with provider_error_boundary(): model, options = self._target(kwargs) - async for chunk in model.astream(messages, stop=stop, **options): - yield ChatGenerationChunk(message=as_chunk(chunk)) + async for chunk in model.astream(messages, stop=stop, config=INNER_STREAM, **options): + for part in split_tool_call_chunk(as_chunk(chunk)): + yield ChatGenerationChunk(message=part) + + +# The wrapped model's own stream events would reach AG-UI alongside this +# wrapper's, emitting every tool call twice; only the wrapper's are relayed. +INNER_STREAM = {"metadata": {"emit-messages": False, "emit-tool-calls": False}} def as_chunk(message): if isinstance(message, AIMessageChunk): return message return AIMessageChunk(**message.model_dump(exclude={"type"})) + + +def split_tool_call_chunk(chunk): + """Send a tool call's name before its arguments, as OpenAI streams them. + + Ollama delivers each tool call whole in one chunk, but AG-UI starts the + call on its named chunk and drops any arguments that chunk carries. + """ + calls = chunk.tool_call_chunks + if len(calls) != 1 or not calls[0].get("name") or not calls[0].get("args"): + return [chunk] + call = calls[0] + head = AIMessageChunk( + content=chunk.content, + id=chunk.id, + additional_kwargs=chunk.additional_kwargs, + response_metadata=chunk.response_metadata, + usage_metadata=chunk.usage_metadata, + tool_call_chunks=[{**call, "args": ""}], + ) + args = AIMessageChunk( + content="", + id=chunk.id, + tool_call_chunks=[ + {**call, "name": None, "id": None, "args": widget_args(call["name"], call["args"])} + ], + ) + return [head, args] + + +# CopilotKit's OpenGenerativeUIMiddleware creates the widget when it reads +# initialHeight and drops any css/html patches that arrive before it. +WIDGET_TOOL = "generateSandboxedUi" +DEFAULT_WIDGET_HEIGHT = 200 +WIDGET_ARG_ORDER = ("initialHeight", "placeholderMessages", "css", "html", "jsFunctions", "jsExpressions") + + +def widget_args(name, args): + """Put a whole widget call's arguments in the order the middleware needs. + + Ollama re-serializes arguments with its own key order, and smaller models + sometimes send jsExpressions as one string instead of a list. + """ + if name != WIDGET_TOOL: + return args + try: + parsed = json.loads(args) + except ValueError: + return args + if not isinstance(parsed, dict): + return args + if isinstance(parsed.get("jsExpressions"), str): + parsed["jsExpressions"] = [parsed["jsExpressions"]] + if not isinstance(parsed.get("initialHeight"), (int, float)): + # Without it the middleware never creates the widget; the renderer + # uses the same default and autosizes from there. + parsed["initialHeight"] = DEFAULT_WIDGET_HEIGHT + ordered = {key: parsed[key] for key in WIDGET_ARG_ORDER if key in parsed} + ordered.update((key, value) for key, value in parsed.items() if key not in ordered) + return json.dumps(ordered) diff --git a/apps/agent/src/visualization_router.py b/apps/agent/src/visualization_router.py index 928556b..db8a319 100644 --- a/apps/agent/src/visualization_router.py +++ b/apps/agent/src/visualization_router.py @@ -10,8 +10,16 @@ from langchain.agents.middleware import AgentMiddleware from langchain_core.messages import SystemMessage from pydantic import BaseModel, Field, ValidationError +from urllib.parse import urlparse +# Hosted Jev. Visitor BYOK keys are TypeSafe keys, so they always route here. JEV_URL = "https://api.typesafe.ai/v1/systemone" +# Server default: clef-flash answers the same System One API in local Ollama. +DEFAULT_ROUTER_URL = "http://localhost:11434/v1/systemone" +DEFAULT_ROUTER_MODEL = "clef-flash:latest" +HOSTED_TIMEOUT = 15 +# A local router loads its model on the first request, which takes 10-12s cold. +LOCAL_TIMEOUT = 60 VISUALIZATIONS = { "text": "Direct facts, prose, writing, code, conversation, or a clarification; no visual needed.", "table": "Basic table: exact values, records, simple side-by-side comparison; no custom interaction.", @@ -113,19 +121,47 @@ def routing_input(state): return turn, context +def server_router_url(): + return os.environ.get("JEV_URL", "").strip() or DEFAULT_ROUTER_URL + + +def router_key_required(): + """Only hosted TypeSafe needs a key; a self-hosted System One server does not.""" + host = urlparse(server_router_url()).hostname or "" + return host == "typesafe.ai" or host.endswith(".typesafe.ai") + + +def request_url(): + from src.credentials import current_credentials + + return JEV_URL if current_credentials.get() else server_router_url() + + +def request_timeout(): + from src.credentials import current_credentials + + if current_credentials.get() or router_key_required(): + return HOSTED_TIMEOUT + return LOCAL_TIMEOUT + + def request_options(context): from src.credentials import current_credentials credentials = current_credentials.get() key = credentials.jev if credentials else os.environ.get("TYPESAFE_API_KEY", "").strip() - if not key: + if not key and router_key_required(): raise ValueError( "TYPESAFE_API_KEY is required for Jev visualization routing. Configure it and restart the agent." ) + if credentials: + model = "jev-latest" + else: + model = os.environ.get("JEV_MODEL", "").strip() or DEFAULT_ROUTER_MODEL return { - "headers": {"Authorization": f"Bearer {key}"}, + "headers": {"Authorization": f"Bearer {key}"} if key else {}, "json": { - "model": "jev-latest" if credentials else os.environ.get("JEV_MODEL", "jev-latest"), + "model": model, "state": {"conversation": context}, "questions": { "visualization": { @@ -158,8 +194,8 @@ def provider_trust_env(): def route(context): try: - with httpx.Client(timeout=15, trust_env=provider_trust_env()) as client: - return parse_response(client.post(JEV_URL, **request_options(context))) + with httpx.Client(timeout=request_timeout(), trust_env=provider_trust_env()) as client: + return parse_response(client.post(request_url(), **request_options(context))) except httpx.HTTPError: raise ValueError( "Jev visualization routing is unavailable or timed out. Please retry." @@ -168,9 +204,9 @@ def route(context): async def aroute(context): try: - async with httpx.AsyncClient(timeout=15, trust_env=provider_trust_env()) as client: + async with httpx.AsyncClient(timeout=request_timeout(), trust_env=provider_trust_env()) as client: return parse_response( - await client.post(JEV_URL, **request_options(context)) + await client.post(request_url(), **request_options(context)) ) except httpx.HTTPError: raise ValueError( diff --git a/apps/agent/tests/test_credentials.py b/apps/agent/tests/test_credentials.py index 1217785..efec62b 100644 --- a/apps/agent/tests/test_credentials.py +++ b/apps/agent/tests/test_credentials.py @@ -89,12 +89,28 @@ def test_request_model_and_jev_override_without_environment_mutation(monkeypatch def test_startup_without_credentials_uses_explicit_unconfigured_model(monkeypatch): from src.model import build_model monkeypatch.delenv('OPENAI_API_KEY', raising=False) - monkeypatch.delenv('LLM_MODEL', raising=False) + monkeypatch.setenv('LLM_MODEL', 'chat-latest') model = build_model(allow_unconfigured=True) with pytest.raises(ValueError, match='credentials'): model.invoke('hello') +@pytest.mark.parametrize(('env', 'available'), [ + ({}, True), # Ollama defaults: beam answers, clef-flash routes, no keys + ({'LLM_MODEL': 'chat-latest'}, False), + ({'LLM_MODEL': 'chat-latest', 'OPENAI_API_KEY': 'k'}, True), + ({'JEV_URL': 'https://api.typesafe.ai/v1/systemone'}, False), + ({'JEV_URL': 'https://api.typesafe.ai/v1/systemone', 'TYPESAFE_API_KEY': 'k'}, True), +]) +def test_server_credentials_follow_the_configured_providers(monkeypatch, env, available): + from src.credentials import server_credentials_available + for name in ['LLM_MODEL', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'JEV_URL', 'TYPESAFE_API_KEY']: + monkeypatch.delenv(name, raising=False) + for name, value in env.items(): + monkeypatch.setenv(name, value) + assert server_credentials_available() is available + + def test_thread_namespace_separates_credentials_and_fallback(): from src.credentials import Credentials, current_credentials, private_thread_id ids = [] @@ -164,10 +180,22 @@ def test_main_starts_without_server_credentials(monkeypatch): import sys environment = {k: v for k, v in os.environ.items() if k not in {'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'TYPESAFE_API_KEY', 'LLM_MODEL'}} environment['PYTHON_DOTENV_DISABLED'] = '1' + # An OpenAI model without its key: the Ollama default needs no credentials. + environment['LLM_MODEL'] = 'chat-latest' result = subprocess.run([sys.executable, '-c', "from fastapi.testclient import TestClient; from main import app; c=TestClient(app); assert c.get('/health').status_code == 200; r=c.post('/', json={}); assert r.status_code == 401; assert r.json()['code'] == 'credentials_required'"], env=environment, capture_output=True, text=True) assert result.returncode == 0, result.stderr +@pytest.mark.parametrize(('model', 'required'), [('chat-latest', True), ('beam:cloud', False)]) +def test_credentials_status_tells_the_chat_whether_keys_are_needed(model, required): + import subprocess + import sys + environment = {k: v for k, v in os.environ.items() if k not in {'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'TYPESAFE_API_KEY', 'JEV_URL'}} + environment.update(PYTHON_DOTENV_DISABLED='1', LLM_MODEL=model) + result = subprocess.run([sys.executable, '-c', f"from fastapi.testclient import TestClient; from main import app; assert TestClient(app).get('/credentials/status').json() == {{'required': {required}}}"], env=environment, capture_output=True, text=True) + assert result.returncode == 0, result.stderr + + @pytest.mark.parametrize(('statuses', 'code'), [([200, 200], None), ([200, 403], 'jev_invalid'), ([429, 200], 'provider_unavailable')]) def test_validation_endpoint_results(monkeypatch, statuses, code): from fastapi import FastAPI diff --git a/apps/agent/tests/test_model.py b/apps/agent/tests/test_model.py index b9fc29f..50b6335 100644 --- a/apps/agent/tests/test_model.py +++ b/apps/agent/tests/test_model.py @@ -1,5 +1,6 @@ """Model defaults and provider-specific validation.""" +import json import os import pytest @@ -13,11 +14,30 @@ from src.model import build_model -def test_default_model_is_chatgpt_instant(monkeypatch): +def test_default_model_is_beam_through_local_ollama(monkeypatch): monkeypatch.delenv("LLM_MODEL", raising=False) + monkeypatch.delenv("OLLAMA_BASE_URL", raising=False) model = build_model() assert isinstance(model, ChatOpenAI) - assert model.model_name == "chat-latest" + assert model.model_name == "beam:cloud" + assert model.openai_api_base == "http://localhost:11434/v1" + assert model.max_tokens >= 32000 + + +def test_ollama_models_need_no_provider_key(monkeypatch): + monkeypatch.setenv("LLM_MODEL", "qwen3:8b") + monkeypatch.setenv("OLLAMA_BASE_URL", "http://gpu-box:11434/v1") + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + model = build_model() + assert model.model_name == "qwen3:8b" + assert model.openai_api_base == "http://gpu-box:11434/v1" + + +def test_ollama_gpt_oss_tag_is_not_routed_to_openai(monkeypatch): + from src.model import provider_key_name + + assert provider_key_name("gpt-oss:20b-cloud") is None + assert provider_key_name("gpt-5.4-2026-03-05") == "OPENAI_API_KEY" def test_llm_model_env_override(monkeypatch: pytest.MonkeyPatch): @@ -84,3 +104,93 @@ def test_missing_provider_key_has_actionable_error(monkeypatch, model, key, valu monkeypatch.setenv(key, value) with pytest.raises(ValueError, match=key): build_model() + + +def whole_tool_call_chunk(): + from langchain_core.messages import AIMessageChunk + + return AIMessageChunk( + content="", + id="run-1", + tool_call_chunks=[ + {"name": "generateSandboxedUi", "args": '{"initialHeight": 300, "html": "

Hi

"}', "id": "call_1", "index": 0} + ], + ) + + +def test_whole_ollama_tool_call_streams_name_before_arguments(): + from src.model import split_tool_call_chunk + + head, args = split_tool_call_chunk(whole_tool_call_chunk()) + assert head.tool_call_chunks[0]["name"] == "generateSandboxedUi" + assert head.tool_call_chunks[0]["args"] == "" + assert args.tool_call_chunks[0]["name"] is None + assert json.loads(args.tool_call_chunks[0]["args"]) == {"initialHeight": 300, "html": "

Hi

"} + assert (head + args).tool_calls == whole_tool_call_chunk().tool_calls + + +def test_streamed_tool_calls_reach_ag_ui_once(): + import asyncio + from langchain_core.language_models.chat_models import BaseChatModel + from langchain_core.outputs import ChatGenerationChunk + from src.model import CredentialScopedModel + + class WholeToolCallModel(BaseChatModel): + @property + def _llm_type(self): + return "whole-tool-call" + + def _generate(self, *args, **kwargs): + raise NotImplementedError + + def _stream(self, *args, **kwargs): + yield ChatGenerationChunk(message=whole_tool_call_chunk()) + + async def relayed_chunks(): + model = CredentialScopedModel(fallback=WholeToolCallModel()) + return [ + event["data"]["chunk"] + async for event in model.astream_events("Build it", version="v2") + if event["event"] == "on_chat_model_stream" + # AG-UI drops events carrying these flags. + and event["metadata"].get("emit-tool-calls", True) + and event["data"]["chunk"].tool_call_chunks + ] + + chunks = asyncio.run(relayed_chunks()) + assert [c.tool_call_chunks[0]["name"] for c in chunks] == ["generateSandboxedUi", None] + + +def test_widget_arguments_reach_copilotkit_in_streaming_order(): + from src.model import widget_args + + whole = json.dumps({ + "css": "p{}", + "html": "", + "initialHeight": 520, + "jsExpressions": "draw()", + "jsFunctions": "function draw(){}", + "placeholderMessages": ["Drawing"], + }) + args = json.loads(widget_args("generateSandboxedUi", whole)) + # initialHeight creates the widget; html sent before it would be dropped. + assert list(args) == ["initialHeight", "placeholderMessages", "css", "html", "jsFunctions", "jsExpressions"] + assert args["jsExpressions"] == ["draw()"] + + +def test_widget_without_initial_height_still_gets_created(): + from src.model import widget_args + + args = json.loads(widget_args("generateSandboxedUi", '{"jsFunctions":"f()"}')) + assert list(args) == ["initialHeight", "jsFunctions"] + assert args["initialHeight"] == 200 + + +@pytest.mark.parametrize( + ("name", "args"), + [("query_data", '{"query":"revenue"}'), ("generateSandboxedUi", '{"html":"

cut off')], +) +def test_other_tools_and_unparseable_widget_arguments_pass_through(name, args): + from src.model import widget_args + + assert widget_args(name, args) == args diff --git a/apps/agent/tests/test_visualization_router.py b/apps/agent/tests/test_visualization_router.py index af1a5d0..239a07d 100644 --- a/apps/agent/tests/test_visualization_router.py +++ b/apps/agent/tests/test_visualization_router.py @@ -107,14 +107,38 @@ def test_route_filters_renderers_preserving_data_tools(choice, allowed): assert "coverage limits" in result.system_message.content -def test_missing_key_is_actionable(monkeypatch): +def test_missing_key_is_actionable_for_hosted_jev(monkeypatch): from src.visualization_router import request_options + monkeypatch.setenv("JEV_URL", "https://api.typesafe.ai/v1/systemone") monkeypatch.delenv("TYPESAFE_API_KEY", raising=False) with pytest.raises(ValueError, match="TYPESAFE_API_KEY"): request_options([]) +def test_default_routes_through_clef_flash_in_local_ollama(monkeypatch): + from src.visualization_router import request_options, request_url + + for name in ("JEV_URL", "JEV_MODEL", "TYPESAFE_API_KEY"): + monkeypatch.delenv(name, raising=False) + options = request_options([]) + assert request_url() == "http://localhost:11434/v1/systemone" + assert options["json"]["model"] == "clef-flash:latest" + assert options["headers"] == {} + + +def test_visitor_jev_keys_always_use_hosted_jev(monkeypatch): + from src.credentials import Credentials, current_credentials + from src.visualization_router import JEV_URL, request_url + + monkeypatch.delenv("JEV_URL", raising=False) + token = current_credentials.set(Credentials("visitor-openai", "visitor-jev")) + try: + assert request_url() == JEV_URL + finally: + current_credentials.reset(token) + + def test_provider_errors_do_not_expose_response_body(): import httpx from src.visualization_router import parse_response @@ -139,6 +163,17 @@ def timeout(*args, **kwargs): assert "private-test-value" not in str(error.value) +@pytest.mark.parametrize( + "jev_url, expected", + [("", 60), ("http://localhost:11434/v1/systemone", 60), ("https://api.typesafe.ai/v1/systemone", 15)], +) +def test_local_router_gets_time_to_load_its_model(monkeypatch, jev_url, expected): + import src.visualization_router as router + + monkeypatch.setenv("JEV_URL", jev_url) + assert router.request_timeout() == expected + + def test_context_excludes_system_and_tool_content_and_is_bounded(): from langchain_core.messages import ToolMessage diff --git a/apps/app/src/app/api/provider-keys/route.test.ts b/apps/app/src/app/api/provider-keys/route.test.ts index 0bb9288..4783a4f 100644 --- a/apps/app/src/app/api/provider-keys/route.test.ts +++ b/apps/app/src/app/api/provider-keys/route.test.ts @@ -1,9 +1,27 @@ // @vitest-environment node import { afterEach, describe, expect, it, vi } from "vitest"; -import { POST } from "./route"; +import { GET, POST } from "./route"; const headers = { "x-openai-api-key": "openai-test-secret", "x-jev-api-key": "jev-test-secret" }; afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); }); +describe("provider key requirement", () => { + it("relays only an explicit 'not required' from the agent", async () => { + vi.stubEnv("LANGGRAPH_DEPLOYMENT_URL", "agent:8123"); + const fetchMock = vi.fn().mockResolvedValue(Response.json({ required: false })); + vi.stubGlobal("fetch", fetchMock); + const response = await GET(); + expect(fetchMock).toHaveBeenCalledWith("http://agent:8123/credentials/status", expect.objectContaining({ redirect: "error" })); + expect(await response.json()).toEqual({ required: false }); + }); + it.each([ + ["an agent error", () => Promise.resolve(Response.json({ required: false }, { status: 500 }))], + ["an unreachable agent", () => Promise.reject(new Error("down"))], + ["a malformed reply", () => Promise.resolve(Response.json({ required: "no" }))], + ])("keeps keys required on %s", async (_, reply) => { + vi.stubGlobal("fetch", vi.fn().mockImplementation(reply)); + expect(await (await GET()).json()).toEqual({ required: true }); + }); +}); describe("provider key validation proxy", () => { it("requires both keys before contacting the backend", async () => { const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); diff --git a/apps/app/src/app/api/provider-keys/route.ts b/apps/app/src/app/api/provider-keys/route.ts index 4839b5b..0195e13 100644 --- a/apps/app/src/app/api/provider-keys/route.ts +++ b/apps/app/src/app/api/provider-keys/route.ts @@ -11,6 +11,24 @@ const reply = (body: object, status: number) => Response.json(body, { status, headers: { "Cache-Control": "no-store" }, }); +const agentUrl = (path: string) => + `${normalizeLanggraphUrl(process.env.LANGGRAPH_DEPLOYMENT_URL).replace(/\/$/, "")}${path}`; + +/** Whether visitors must bring keys. Fails closed: any doubt keeps the key prompt. */ +export async function GET() { + try { + const response = await fetch(agentUrl("/credentials/status"), { + cache: "no-store", + redirect: "error", + signal: AbortSignal.timeout(10_000), + }); + const data = await response.json(); + return reply({ required: !(response.ok && data?.required === false) }, 200); + } catch { + return reply({ required: true }, 200); + } +} + export async function POST(request: Request) { let credentials; try { @@ -20,9 +38,7 @@ export async function POST(request: Request) { return reply({ error: PROVIDER_KEY_INPUT_ERROR }, 400); } try { - const base = normalizeLanggraphUrl(process.env.LANGGRAPH_DEPLOYMENT_URL); - const url = `${base.replace(/\/$/, "")}/credentials/validate`; - const response = await fetch(url, { + const response = await fetch(agentUrl("/credentials/validate"), { method: "POST", headers: credentials, cache: "no-store", diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx index 3d82645..65c9218 100644 --- a/apps/app/src/app/page.tsx +++ b/apps/app/src/app/page.tsx @@ -18,7 +18,7 @@ export default function HomePage() { useGenerativeUIExamples(); useExampleSuggestions(); const { agent } = useAgent(); - const { newChat, hasKeys, keysOpen, setKeysOpen } = useProviderKeys(); + const { newChat, hasKeys, canChat, keysOpen, setKeysOpen } = useProviderKeys(); const { copilotkit } = useCopilotKit(); const [error, setError] = useState(null); const [retrying, setRetrying] = useState(false); @@ -142,7 +142,7 @@ export default function HomePage() { welcomeMessageText: "What would you like to explore?", chatInputPlaceholder: "Ask anything…", chatDisclaimerText: - hasKeys ? "AI can make mistakes. Check important details." : "Add your OpenAI and Jev API keys to start. Keys clear when you refresh.", + canChat ? "AI can make mistakes. Check important details." : "Add your OpenAI and Jev API keys to start. Keys clear when you refresh.", }} /> diff --git a/apps/app/src/components/chat/key-gated-chat-view.test.tsx b/apps/app/src/components/chat/key-gated-chat-view.test.tsx index a1556d3..9f425bf 100644 --- a/apps/app/src/components/chat/key-gated-chat-view.test.tsx +++ b/apps/app/src/components/chat/key-gated-chat-view.test.tsx @@ -1,5 +1,5 @@ import { afterEach, expect, it, vi } from "vitest"; -import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom/vitest"; import type { ComponentProps } from "react"; import type { CopilotChatView } from "@copilotkit/react-core/v2"; @@ -57,3 +57,19 @@ it("gates example prompts and clears the preserved draft when starting a new cha fireEvent.click(screen.getByText("Example")); expect(select).toHaveBeenCalledOnce(); }); + +it("sends without visitor keys once the server reports its own models are configured", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ json: async () => ({ required: false }) })); + try { + render(); + await waitFor(() => expect(fetch).toHaveBeenCalledWith("/api/provider-keys", { cache: "no-store" })); + // Let the status response settle before the visitor sends. + await act(() => new Promise((resolve) => setTimeout(resolve, 0))); + fireEvent.change(screen.getByLabelText("Message"), { target: { value: "Show a map" } }); + fireEvent.click(screen.getByText("Send")); + expect(submit).toHaveBeenCalledExactlyOnceWith("Show a map"); + expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); + } finally { + vi.unstubAllGlobals(); + } +}); diff --git a/apps/app/src/components/chat/key-gated-chat-view.tsx b/apps/app/src/components/chat/key-gated-chat-view.tsx index da813d7..09991f3 100644 --- a/apps/app/src/components/chat/key-gated-chat-view.tsx +++ b/apps/app/src/components/chat/key-gated-chat-view.tsx @@ -6,13 +6,13 @@ import { useProviderKeys } from "./provider-keys"; /** Gate submission before CopilotKit adds a message or starts a provider request. */ function KeyGatedChatViewComponent(props: ComponentProps) { - const { hasKeys, draft, setDraft, setKeysOpen } = useProviderKeys(); + const { canChat, draft, setDraft, setKeysOpen } = useProviderKeys(); return { - if (!hasKeys) { + if (!canChat) { setDraft(value); setKeysOpen(true); return; @@ -21,7 +21,7 @@ function KeyGatedChatViewComponent(props: ComponentProps props.onSubmitMessage?.(value); }} onSelectSuggestion={(suggestion, index) => { - if (!hasKeys) { + if (!canChat) { setDraft(suggestion.message); setKeysOpen(true); return; diff --git a/apps/app/src/components/chat/provider-keys-dialog.tsx b/apps/app/src/components/chat/provider-keys-dialog.tsx index 1743e30..a7b26f2 100644 --- a/apps/app/src/components/chat/provider-keys-dialog.tsx +++ b/apps/app/src/components/chat/provider-keys-dialog.tsx @@ -5,7 +5,7 @@ import { useProviderKeys } from "./provider-keys"; import "./provider-keys.css"; export function ProviderKeysDialog({ onClose, disabled = false }: { onClose: () => void; disabled?: boolean }) { - const { hasKeys, save, clear } = useProviderKeys(); + const { hasKeys, keysRequired, save, clear } = useProviderKeys(); const dialog = useRef(null); const request = useRef(null); const [openai, setOpenai] = useState(""); @@ -59,8 +59,10 @@ export function ProviderKeysDialog({ onClose, disabled = false }: { onClose: () return (

{ event.preventDefault(); onClose(); }}> -

{hasKeys ? "API keys" : "Add API keys to start"}

- {!hasKeys &&

Connect OpenAI for answers and Jev for choosing visualizations. Your message will stay ready to send.

} +

{hasKeys || !keysRequired ? "API keys" : "Add API keys to start"}

+ {!hasKeys && (keysRequired + ?

Connect OpenAI for answers and Jev for choosing visualizations. Your message will stay ready to send.

+ :

Optional. This server already answers with its own models. Add OpenAI and Jev keys to use those providers instead.

)}

Keys stay only in this browser’s memory until you refresh or clear them. They are sent through this app’s server to their respective providers. Provider usage is charged to your accounts.

Saving or clearing keys starts a new chat.

{ event.preventDefault(); void testConnection(); }}> @@ -69,7 +71,7 @@ export function ProviderKeysDialog({ onClose, disabled = false }: { onClose: () edit(setJev, event.target.value)} disabled={pending || disabled} /> {error &&

{error}

} -

{pending ? "Checking both providers…" : validated ? "Both keys work. Save to start a new chat." : hasKeys ? "Your own keys are active for this session." : "Both keys are required to start chatting."}

+

{pending ? "Checking both providers…" : validated ? "Both keys work. Save to start a new chat." : hasKeys ? "Your own keys are active for this session." : keysRequired ? "Both keys are required to start chatting." : "Enter both keys to use them."}

{hasKeys && } diff --git a/apps/app/src/components/chat/provider-keys.tsx b/apps/app/src/components/chat/provider-keys.tsx index 8840ee6..aa76533 100644 --- a/apps/app/src/components/chat/provider-keys.tsx +++ b/apps/app/src/components/chat/provider-keys.tsx @@ -1,11 +1,15 @@ "use client"; -import { createContext, useContext, useState, type ReactNode } from "react"; +import { createContext, useContext, useEffect, useState, type ReactNode } from "react"; export type ProviderKeys = { openai: string; jev: string }; type ProviderKeysContextValue = { headers: Record | undefined; hasKeys: boolean; + /** False when the server's own models answer without visitor keys. */ + keysRequired: boolean; + /** Visitor keys are saved, or the server does not need them. */ + canChat: boolean; session: number; save: (keys: ProviderKeys) => void; clear: () => void; @@ -23,11 +27,24 @@ export function ProviderKeysProvider({ children }: { children: ReactNode }) { const [session, setSession] = useState(0); const [draft, setDraft] = useState(""); const [keysOpen, setKeysOpen] = useState(false); + // Assume keys are needed until the server says its own models are configured. + const [keysRequired, setKeysRequired] = useState(true); + useEffect(() => { + let active = true; + Promise.resolve() + .then(() => fetch("/api/provider-keys", { cache: "no-store" })) + .then((response) => response.json()) + .then((data) => { if (active && data?.required === false) setKeysRequired(false); }) + .catch(() => {}); + return () => { active = false; }; + }, []); const newChat = () => { setDraft(""); setSession((value) => value + 1); }; return ( { setKeys(value); setKeysOpen(false); setSession((value) => value + 1); }, clear: () => { setKeys(null); newChat(); }, diff --git a/docs/README.md b/docs/README.md index b5fc402..4e90c52 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ **Answers you can interact with**, by [CopilotKit](https://copilotkit.ai). The agent chooses text, native components, or custom interactive answers to help people understand, compare, and make tools. -Local prerequisites: Node.js 22+, pnpm 9+, Python 3.12+, uv, an OpenAI key for the default `chat-latest` answer model, and a TypeSafe key for Jev visualization routing. `claude-*` overrides use Anthropic instead of OpenAI. +Local prerequisites: Node.js 22+, pnpm 9+, Python 3.12+, uv, and [Ollama](https://ollama.com) signed in with `ollama signin`. The default answer model is `beam:cloud` and the default router is `clef-flash:latest`, both through Ollama with no API keys. `claude-*` and `chat-latest`/`gpt-*` overrides use Anthropic or OpenAI keys. | Guide | Description | | ---------------------------------------------------- | ------------------------------------------------------ | diff --git a/docs/architecture.md b/docs/architecture.md index f233317..d30984c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -27,7 +27,7 @@ User prompt or chat starter suggestion The agent uses `create_deep_agent`, CopilotKit middleware, an Anthropic compatibility middleware, and `BoundedMemorySaver(max_threads=200)`. The checkpoint store is process memory, not durable shared storage. Restarting or scaling the agent can lose or split conversation state. -`src/model.py` chooses ChatAnthropic for `claude-*` and ChatOpenAI for `gpt-*`. It checks the selected provider's key before constructing the client. The local default is unchanged; deployed configuration can override it. +`src/model.py` chooses ChatAnthropic for `claude-*`, ChatOpenAI for `chat-latest`/`gpt-*`, and ChatOpenAI pointed at Ollama for any tagged model name (default `beam:cloud`). It checks the selected provider's key before constructing a hosted client. Ollama returns each tool call whole, so the model wrapper splits it into a name chunk and an arguments chunk, and puts `generateSandboxedUi` arguments in the streaming order below with a default `initialHeight`. Deployed configuration can override the model. `src/prompt.py` and the three agent skills guide response selection. Plain text is appropriate for direct answers. Native components handle supported structured tasks; custom UI serves useful diagrams, comparisons, calculators, and simulations. Planning is optional. See [Product and agent behavior](interactive-answers.md). diff --git a/docs/bring-to-your-app.md b/docs/bring-to-your-app.md index d8867d2..8fd91a4 100644 --- a/docs/bring-to-your-app.md +++ b/docs/bring-to-your-app.md @@ -6,7 +6,7 @@ This guide walks through adopting the CopilotKit + LangGraph patterns from this - A React frontend (Next.js recommended) - A Python backend (or willingness to add one) -- An OpenAI API key (or another LLM provider) +- An LLM: a local [Ollama](https://ollama.com) model, or an OpenAI or Anthropic API key ## Step 1: Install Frontend Packages diff --git a/docs/deployment.md b/docs/deployment.md index 1bbf12d..fda3a9d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -11,11 +11,11 @@ The repository's `render.yaml` defines two services: | `open-generative-ui-agent` | Python 3.12.6, `apps/agent` | `pip install uv && uv sync` | `uv run uvicorn main:app --host 0.0.0.0 --port $PORT`, `/health` | | `open-generative-ui-app` | Node 22, repository root | `corepack enable && pnpm install --no-frozen-lockfile && pnpm exec turbo run build --filter=@repo/app` | `pnpm --filter @repo/app start`, `/api/health` | -For shared credentials, the blueprint and Python factory default to `LLM_MODEL=chat-latest`, using `OPENAI_API_KEY`. Jev visualization routing uses `TYPESAFE_API_KEY`; `JEV_MODEL` defaults to `jev-latest`. Verify access to the selected model before launch; a configured model name does not prove availability. +The Python factory defaults to `beam:cloud` through a local Ollama daemon, which a Render service does not have. The blueprint therefore sets `LLM_MODEL=chat-latest` with `OPENAI_API_KEY`, and hosted Jev with `JEV_URL`, `JEV_MODEL=jev-latest` and `TYPESAFE_API_KEY`. Verify access to the selected model before launch; a configured model name does not prove availability. | Variable | Service | Purpose | | ----------------------------------------- | ------------- | ----------------------------------------------------------------------------------- | -| `LLM_MODEL` | Agent | `claude-*` routes to Anthropic; `gpt-*` routes to OpenAI | +| `LLM_MODEL` | Agent | `claude-*` routes to Anthropic; `gpt-*` routes to OpenAI; tagged names to Ollama | | `ANTHROPIC_API_KEY` | Agent | Required when selecting Anthropic | | `OPENAI_API_KEY` | Agent | Required when selecting OpenAI | | `LANGSMITH_API_KEY` | As configured | Optional tracing configuration | diff --git a/docs/getting-started.md b/docs/getting-started.md index 73fa6fc..4c3b345 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,6 @@ # Getting started -Run Open Intelligent UI by CopilotKit locally with Node.js 22+, pnpm 9+, Python 3.12+, and [uv](https://docs.astral.sh/uv/). +Run Open Intelligent UI by CopilotKit locally with Node.js 22+, pnpm 9+, Python 3.12+, [uv](https://docs.astral.sh/uv/), and [Ollama](https://ollama.com). ## Install @@ -14,20 +14,29 @@ make setup ## Configure the agent -You can start the agent without shared provider keys and use **API keys** in the chat header to test and save your own OpenAI and Jev keys for the current browser session. Refreshing clears the keys. +By default the agent needs no API keys. It sends answers to `beam:cloud` and visualization routing to `clef-flash:latest`, both through the Ollama daemon at `http://localhost:11434`. Before starting the app: -The chat interface asks for visitor keys before sending a message and preserves the draft during setup. To also provide shared credentials for direct API clients, edit `apps/agent/.env`: +```bash +ollama signin # required for :cloud models +ollama pull clef-flash:latest # local router +``` + +`make setup` writes these defaults to `apps/agent/.env`; `.env.example` lists the optional settings: ```dotenv -OPENAI_API_KEY=your-provider-key -LLM_MODEL=chat-latest -TYPESAFE_API_KEY=your-typesafe-key -JEV_MODEL=jev-latest +LLM_MODEL=beam:cloud +# OLLAMA_BASE_URL=http://localhost:11434/v1 +JEV_MODEL=clef-flash:latest +# JEV_URL=http://localhost:11434/v1/systemone ``` -The model factory accepts `claude-*` names through Anthropic and `chat-latest` or `gpt-*` names through OpenAI. To override the default, set a model available to your account in `LLM_MODEL` and supply its provider key. Shared mode requires the selected answer provider’s key and the Jev key; BYOK requests supply their own pair. Unset `LLM_MODEL` uses the local default; an empty value or unsupported prefix fails with a configuration error. +Any Ollama model name with a tag (such as `gemma4:31b-cloud`) works as `LLM_MODEL` without a key; a `:cloud` model must be available on your Ollama plan. The model factory also accepts `claude-*` names through Anthropic (`ANTHROPIC_API_KEY`) and `chat-latest` or `gpt-*` names through OpenAI (`OPENAI_API_KEY`). For hosted Jev, set `JEV_URL=https://api.typesafe.ai/v1/systemone`, `JEV_MODEL=jev-latest` and `TYPESAFE_API_KEY`. Unset `LLM_MODEL` uses the default; an empty value or unsupported name fails with a configuration error. + +The router allows 60 seconds per request against a local server, since Ollama loads the model on the first request, and 15 seconds against hosted Jev. + +When the server's own models are configured, the chat sends without asking for keys. Otherwise visitors use **API keys** in the chat header to test and save their own OpenAI and Jev keys for the current browser session; those always use `chat-latest` and `jev-latest`. Refreshing clears the keys. -Check model availability and access with a real request. This repository does not guarantee that every model name works or implement a separate native GPT-6 API. Never put shared provider keys in public frontend environment variables. See [BYOK behavior](../README.md#use-your-own-api-keys). +Check model availability and access with a real request. This repository does not guarantee that every model name works or implement a separate native GPT-6 API. Ollama cloud models can be retired or require paid credits, and smaller models may answer in text instead of building a requested chart; retrying usually helps. Never put shared provider keys in public frontend environment variables. See [BYOK behavior](../README.md#use-your-own-api-keys). ## Configure the frontend diff --git a/docs/visualization-routing.md b/docs/visualization-routing.md index 77622a0..99da780 100644 --- a/docs/visualization-routing.md +++ b/docs/visualization-routing.md @@ -1,8 +1,8 @@ # Visualization routing -The answer model defaults to `chat-latest`, the documented API alias for the latest ChatGPT Instant model. This is a moving alias, not a pinned `gpt-6-instant` snapshot. Its available model ID was verified using the configured OpenAI account. See [OpenAI's model documentation](https://developers.openai.com/api/docs/models/chat-latest). +The answer model defaults to `beam:cloud` through local Ollama. Visitor BYOK keys use `chat-latest`, OpenAI's moving alias for the latest ChatGPT Instant model (not a pinned `gpt-6-instant` snapshot); see [OpenAI's model documentation](https://developers.openai.com/api/docs/models/chat-latest). -Jev (`jev-latest`) makes one typed presentation decision for each new user message through [TypeSafe's System One API](https://docs.typesafe.ai/introduction/quickstart). It receives up to four recent user/assistant text messages, capped at 6,000 characters each. It does not receive API keys, system instructions, raw tool outputs, or generated tool code. +The router makes one typed presentation decision for each new user message through [TypeSafe's System One API](https://docs.typesafe.ai/introduction/quickstart). By default it is `clef-flash:latest`, which answers that API at Ollama's `/v1/systemone`; hosted Jev (`jev-latest`) is used for visitor keys or when `JEV_URL` points at TypeSafe. It receives up to four recent user/assistant text messages, capped at 6,000 characters each. It does not receive API keys, system instructions, raw tool outputs, or generated tool code. | Selected visualization | Renderer | | --- | --- | @@ -17,7 +17,7 @@ The renderer is derived from Jev's validated choice, preventing contradictory re The A2UI catalog supplies a semantic HTML table with column headers, a caption, keyboard-focusable horizontal scrolling, and a required provenance label. It accepts at most 12 columns and 200 rows and rejects mismatched row widths. React renders values as text. Charts and diagrams continue to use the existing isolated sandbox. -Set `OPENAI_API_KEY`, `TYPESAFE_API_KEY`, `LLM_MODEL=chat-latest`, and optionally `JEV_MODEL=jev-latest` on the agent server. Provider secrets never belong in browser variables. Existing `claude-*` and `gpt-*` overrides remain available. Jev authentication failures, invalid choices, and timeouts fail the turn with a retryable error; there is no silent fallback claiming that Jev made a decision. Successful health checks confirm the server is running, not provider access. +The local defaults need no keys. For hosted providers, set `OPENAI_API_KEY` and `LLM_MODEL=chat-latest`, and `JEV_URL=https://api.typesafe.ai/v1/systemone`, `TYPESAFE_API_KEY` and `JEV_MODEL=jev-latest`, on the agent server. Requests time out after 60 seconds against a local router (its first request loads the model) and 15 seconds against hosted Jev. Provider secrets never belong in browser variables. Existing `claude-*` and `gpt-*` overrides remain available. Jev authentication failures, invalid choices, and timeouts fail the turn with a retryable error; there is no silent fallback claiming that Jev made a decision. Successful health checks confirm the server is running, not provider access. Routing is a model judgment, not a guarantee of the objectively best visualization. Missing source data still requires clarification or explicit illustrative assumptions. Live provider checks and browser checks complement the deterministic routing, error-path, continuation, and A2UI rendering tests. diff --git a/package.json b/package.json index d246f43..234e1c4 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,16 @@ "clean": "turbo run clean" }, "devDependencies": { - "turbo": "^2.5.4", - "concurrently": "^9.1.2" + "concurrently": "^9.1.2", + "turbo": "^2.5.4" }, "packageManager": "pnpm@9.0.0", "pnpm": { "overrides": { "esbuild": "^0.27.3" + }, + "patchedDependencies": { + "@ag-ui/client@0.0.52": "patches/@ag-ui__client@0.0.52.patch" } } } diff --git a/patches/@ag-ui__client@0.0.52.patch b/patches/@ag-ui__client@0.0.52.patch new file mode 100644 index 0000000..34e6c3b --- /dev/null +++ b/patches/@ag-ui__client@0.0.52.patch @@ -0,0 +1,10 @@ +diff --git a/dist/index.mjs b/dist/index.mjs +index da2c8fcdc79b7f93c238332abdf3b8e8b25c2676..33460d0f3b5df32bd2e153ede7b64792172d1ec3 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -1,4 +1,4 @@ +-import{v4 as e}from"uuid";import{AGUIConnectNotImplementedError as t,AGUIError as n,EventSchemas as r,EventType as i}from"@ag-ui/core";import*as a from"fast-json-patch";import{EMPTY as o,Observable as s,ReplaySubject as c,Subject as l,defer as u,finalize as d,from as f,lastValueFrom as p,mergeMap as m,of as h,pipe as g,throwError as _}from"rxjs";import{catchError as v,concatMap as y,defaultIfEmpty as ee,filter as te,finalize as b,map as x,mergeAll as ne,mergeMap as S,switchMap as re,takeUntil as C,tap as ie}from"rxjs/operators";import w from"untruncate-json";import*as T from"@ag-ui/proto";import{z as E}from"zod";import{compareVersions as D}from"compare-versions";export*from"@ag-ui/core";const O=e=>{if(typeof structuredClone==`function`)return structuredClone(e);try{return JSON.parse(JSON.stringify(e))}catch{return Array.isArray(e)?[...e]:{...e}}};function k(){return e()}function A(e){if(Object.freeze(e),typeof e==`object`&&e)for(let t of Object.values(e))typeof t==`object`&&t&&!Object.isFrozen(t)&&A(t);return e}async function j(e,t,n,r){let i=typeof process<`u`&&process.env!==void 0,a=i&&(process.env.NODE_ENV===`test`||!!process.env.VITEST_WORKER_ID),o=i&&(process.env.NODE_ENV===`development`||process.env.NODE_ENV===`test`||!!process.env.VITEST_WORKER_ID),s=O(t),c=O(n),l=s,u=c,d;for(let t of e)try{o&&(A(l),A(u));let e=await r(t,l,u);if(e===void 0)continue;if(e.messages!==void 0&&e.messages!==l&&(l=O(e.messages)),e.state!==void 0&&e.state!==u&&(u=O(e.state)),d=e.stopPropagation,d===!0)break}catch(e){if(o&&e instanceof TypeError){if(a)throw e;console.error(`AG-UI: Subscriber attempted to mutate frozen inputs in-place. Return mutations via AgentStateMutation instead of mutating directly.`,e)}else a||console.error(`Subscriber error:`,e);continue}return{...l===s?{}:{messages:o&&Object.isFrozen(l)?O(l):l},...u===c?{}:{state:o&&Object.isFrozen(u)?O(u):u},...d===void 0?{}:{stopPropagation:d}}}function M(e){if(!e)return{enabled:!1,events:!1,lifecycle:!1,verbose:!1};if(e===!0)return{enabled:!0,events:!0,lifecycle:!0,verbose:!0};let t=e.events??!0,n=e.lifecycle??!0,r=e.verbose??!1;return{enabled:t||n,events:t,lifecycle:n,verbose:r}}function N(e){if(e instanceof P)return e;if(e===!0)return new P(M(!0))}var P=class{constructor(e){this.config=e}event(e,t,n,r){this.config.events&&(this.config.verbose?console.debug(`[${e}] ${t}`,typeof n==`string`?n:JSON.stringify(n)):console.debug(`[${e}] ${t}`,r??n))}lifecycle(e,t,n){this.config.lifecycle&&(n?console.debug(`[${e}] ${t}`,n):console.debug(`[${e}] ${t}`))}get eventsEnabled(){return this.config.events}get lifecycleEnabled(){return this.config.lifecycle}get enabled(){return this.config.enabled}};function F(e){return e.enabled?new P(e):void 0}function ae(e,t,n){if(t){let r=e.find(e=>e.id===t);if(r?.role===`assistant`)return r;r&&console.warn(`TOOL_CALL_START: parentMessageId '${t}' matches a '${r.role}' message, not assistant — falling back to toolCallId`);let i={id:r?n:t,role:`assistant`,toolCalls:[]};return e.push(i),i}let r={id:n,role:`assistant`,toolCalls:[]};return e.push(r),r}const I=(e,t,n,r,s)=>{let c=N(s),l=O(n.messages),u=O(e.state),d={},f=e=>{e.messages!==void 0&&(l=e.messages,d.messages=e.messages),e.state!==void 0&&(u=e.state,d.state=e.state)},p=()=>{let e=O(d);return d={},e.messages!==void 0||e.state!==void 0?h(e):o};return t.pipe(y(async t=>{let o=await j(r,l,u,(r,i,a)=>r.onEvent?.({event:t,agent:n,input:e,messages:i,state:a}));if(f(o),o.stopPropagation===!0?c?.event(`APPLY`,`Event dropped:`,t,{type:t.type,reason:`stopPropagation by subscriber`}):c?.event(`APPLY`,`Event applied:`,t,{type:t.type,subscribers:r.length}),o.stopPropagation===!0)return p();switch(t.type){case i.TEXT_MESSAGE_START:{let i=await j(r,l,u,(r,i,a)=>r.onTextMessageStartEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{messageId:e,role:n=`assistant`,name:r}=t;if(!l.find(t=>t.id===e)){let t={id:e,role:n,content:``,...r!==void 0&&{name:r}};l.push(t),f({messages:l})}}return p()}case i.TEXT_MESSAGE_CONTENT:{let{messageId:i,delta:a}=t,o=l.find(e=>e.id===i);if(!o)return console.warn(`TEXT_MESSAGE_CONTENT: No message found with ID '${i}'`),p();let s=await j(r,l,u,(r,i,a)=>r.onTextMessageContentEvent?.({event:t,messages:i,state:a,agent:n,input:e,textMessageBuffer:typeof o.content==`string`?o.content:``}));return f(s),s.stopPropagation!==!0&&(o.content=`${typeof o.content==`string`?o.content:``}${a}`,f({messages:l})),p()}case i.TEXT_MESSAGE_END:{let{messageId:i}=t,a=l.find(e=>e.id===i);return a?(f(await j(r,l,u,(r,i,o)=>r.onTextMessageEndEvent?.({event:t,messages:i,state:o,agent:n,input:e,textMessageBuffer:typeof a.content==`string`?a.content:``}))),await Promise.all(r.map(t=>{t.onNewMessage?.({message:a,messages:l,state:u,agent:n,input:e})})),p()):(console.warn(`TEXT_MESSAGE_END: No message found with ID '${i}'`),p())}case i.TOOL_CALL_START:{let i=await j(r,l,u,(r,i,a)=>r.onToolCallStartEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{toolCallId:e,toolCallName:n,parentMessageId:r}=t,i=ae(l,r,e);i.toolCalls??=[],i.toolCalls.push({id:e,type:`function`,function:{name:n,arguments:``}}),f({messages:l})}return p()}case i.TOOL_CALL_ARGS:{let{toolCallId:i,delta:a}=t,o=l.find(e=>e.toolCalls?.some(e=>e.id===i));if(!o)return console.warn(`TOOL_CALL_ARGS: No message found containing tool call with ID '${i}'`),p();let s=o.toolCalls?.find(e=>e.id===i);if(!s)return console.warn(`TOOL_CALL_ARGS: No tool call found with ID '${i}'`),p();let c=await j(r,l,u,(r,i,a)=>{let o=s.function.arguments,c=s.function.name,l={};try{l=w(o)}catch{}return r.onToolCallArgsEvent?.({event:t,messages:i,state:a,agent:n,input:e,toolCallBuffer:o,toolCallName:c,partialToolCallArgs:l})});return f(c),c.stopPropagation!==!0&&(s.function.arguments+=a,f({messages:l})),p()}case i.TOOL_CALL_END:{let{toolCallId:i}=t,a=l.find(e=>e.toolCalls?.some(e=>e.id===i));if(!a)return console.warn(`TOOL_CALL_END: No message found containing tool call with ID '${i}'`),p();let o=a.toolCalls?.find(e=>e.id===i);return o?(f(await j(r,l,u,(r,i,a)=>{let s=o.function.arguments,c=o.function.name,l={};try{l=JSON.parse(s)}catch{}return r.onToolCallEndEvent?.({event:t,messages:i,state:a,agent:n,input:e,toolCallName:c,toolCallArgs:l})})),await Promise.all(r.map(t=>{t.onNewToolCall?.({toolCall:o,messages:l,state:u,agent:n,input:e})})),p()):(console.warn(`TOOL_CALL_END: No tool call found with ID '${i}'`),p())}case i.TOOL_CALL_RESULT:{let i=await j(r,l,u,(r,i,a)=>r.onToolCallResultEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{messageId:i,toolCallId:a,content:o,role:s}=t,c={id:i,toolCallId:a,role:s||`tool`,content:o};l.push(c),await Promise.all(r.map(t=>{t.onNewMessage?.({message:c,messages:l,state:u,agent:n,input:e})})),f({messages:l})}return p()}case i.STATE_SNAPSHOT:{let i=await j(r,l,u,(r,i,a)=>r.onStateSnapshotEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{snapshot:e}=t;u=e,f({state:u})}return p()}case i.STATE_DELTA:{let i=await j(r,l,u,(r,i,a)=>r.onStateDeltaEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{delta:e}=t;try{u=a.applyPatch(u,e,!0,!1).newDocument,f({state:u})}catch(t){let n=t instanceof Error?t.message:String(t);console.warn(`Failed to apply state patch:\nCurrent state: ${JSON.stringify(u,null,2)}\nPatch operations: ${JSON.stringify(e,null,2)}\nError: ${n}`)}}return p()}case i.MESSAGES_SNAPSHOT:{let i=await j(r,l,u,(r,i,a)=>r.onMessagesSnapshotEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{messages:e}=t,n=new Map(e.map(e=>[e.id,e])),r=e=>e===`activity`||e===`reasoning`;l=l.filter(e=>r(e.role)||n.has(e.id)).map(e=>r(e.role)?e:n.get(e.id));let i=new Set(l.map(e=>e.id));for(let t of e)i.has(t.id)||l.push(t);f({messages:l})}return p()}case i.ACTIVITY_SNAPSHOT:{let i=t,a=l.findIndex(e=>e.id===i.messageId),o=a>=0?l[a]:void 0,s=o?.role===`activity`?o:void 0,c=i.replace??!0,d=await j(r,l,u,(t,r,a)=>t.onActivitySnapshotEvent?.({event:i,messages:r,state:a,agent:n,input:e,activityMessage:s,existingMessage:o}));if(f(d),d.stopPropagation!==!0){let t={id:i.messageId,role:`activity`,activityType:i.activityType,content:O(i.content)},o;a===-1?(l.push(t),o=t):s?c&&(l[a]={...s,activityType:i.activityType,content:O(i.content)}):c&&(l[a]=t,o=t),f({messages:l}),o&&await Promise.all(r.map(t=>t.onNewMessage?.({message:o,messages:l,state:u,agent:n,input:e})))}return p()}case i.ACTIVITY_DELTA:{let i=t,o=l.findIndex(e=>e.id===i.messageId);if(o===-1)return p();let s=l[o];if(s.role!==`activity`)return console.warn(`ACTIVITY_DELTA: Message '${i.messageId}' is not an activity message`),p();let c=s,d=await j(r,l,u,(t,r,a)=>t.onActivityDeltaEvent?.({event:i,messages:r,state:a,agent:n,input:e,activityMessage:c}));if(f(d),d.stopPropagation!==!0)try{let e=O(c.content??{}),t=a.applyPatch(e,i.patch??[],!0,!1).newDocument;l[o]={...c,content:O(t),activityType:i.activityType},f({messages:l})}catch(e){let t=e instanceof Error?e.message:String(e);console.warn(`Failed to apply activity patch for '${i.messageId}': ${t}`)}return p()}case i.RAW:return f(await j(r,l,u,(r,i,a)=>r.onRawEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.CUSTOM:return f(await j(r,l,u,(r,i,a)=>r.onCustomEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.RUN_STARTED:{let i=await j(r,l,u,(r,i,a)=>r.onRunStartedEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let e=t;if(e.input?.messages){for(let t of e.input.messages)l.find(e=>e.id===t.id)||l.push(t);f({messages:l})}}return p()}case i.RUN_FINISHED:return f(await j(r,l,u,(r,i,a)=>r.onRunFinishedEvent?.({event:t,messages:i,state:a,agent:n,input:e,result:t.result}))),p();case i.RUN_ERROR:return f(await j(r,l,u,(r,i,a)=>r.onRunErrorEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.STEP_STARTED:return f(await j(r,l,u,(r,i,a)=>r.onStepStartedEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.STEP_FINISHED:return f(await j(r,l,u,(r,i,a)=>r.onStepFinishedEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.TEXT_MESSAGE_CHUNK:throw Error(`TEXT_MESSAGE_CHUNK must be tranformed before being applied`);case i.TOOL_CALL_CHUNK:throw Error(`TOOL_CALL_CHUNK must be tranformed before being applied`);case i.THINKING_START:return p();case i.THINKING_END:return p();case i.THINKING_TEXT_MESSAGE_START:return p();case i.THINKING_TEXT_MESSAGE_CONTENT:return p();case i.THINKING_TEXT_MESSAGE_END:return p();case i.REASONING_START:return f(await j(r,l,u,(r,i,a)=>r.onReasoningStartEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.REASONING_MESSAGE_START:{let i=await j(r,l,u,(r,i,a)=>r.onReasoningMessageStartEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{messageId:e}=t;if(!l.find(t=>t.id===e)){let t={id:e,role:`reasoning`,content:``};l.push(t),f({messages:l})}}return p()}case i.REASONING_MESSAGE_CONTENT:{let{messageId:i,delta:a}=t,o=l.find(e=>e.id===i);if(!o)return console.warn(`REASONING_MESSAGE_CONTENT: No message found with ID '${i}'`),p();let s=await j(r,l,u,(r,i,a)=>r.onReasoningMessageContentEvent?.({event:t,messages:i,state:a,agent:n,input:e,reasoningMessageBuffer:typeof o.content==`string`?o.content:``}));return f(s),s.stopPropagation!==!0&&(o.content=`${typeof o.content==`string`?o.content:``}${a}`,f({messages:l})),p()}case i.REASONING_MESSAGE_END:{let{messageId:i}=t,a=l.find(e=>e.id===i);return a?(f(await j(r,l,u,(r,i,o)=>r.onReasoningMessageEndEvent?.({event:t,messages:i,state:o,agent:n,input:e,reasoningMessageBuffer:typeof a.content==`string`?a.content:``}))),await Promise.all(r.map(t=>{t.onNewMessage?.({message:a,messages:l,state:u,agent:n,input:e})})),p()):(console.warn(`REASONING_MESSAGE_END: No message found with ID '${i}'`),p())}case i.REASONING_MESSAGE_CHUNK:throw Error(`REASONING_MESSAGE_CHUNK must be transformed before being applied`);case i.REASONING_END:return f(await j(r,l,u,(r,i,a)=>r.onReasoningEndEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.REASONING_ENCRYPTED_VALUE:{let{subtype:i,entityId:a,encryptedValue:o}=t,s=await j(r,l,u,(r,i,a)=>r.onReasoningEncryptedValueEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(s),s.stopPropagation!==!0){let e=!1;if(i===`tool-call`){for(let t of l)if(t.role===`assistant`&&t.toolCalls){let n=t.toolCalls.find(e=>e.id===a);if(n){n.encryptedValue=o,e=!0;break}}}else{let t=l.find(e=>e.id===a);t?.role!==`activity`&&t&&(t.encryptedValue=o,e=!0)}e&&(d.messages=l)}return p()}}return t.type,p()}),ne(),r.length>0?ee({}):e=>e)},L=e=>t=>{let r=N(e),a=new Map,o=new Map,s=!1,c=!1,l=!1,u=new Map,d=!1,f=!1,p=!1,m=()=>{a.clear(),o.clear(),u.clear(),d=!1,f=!1,s=!1,c=!1,p=!0};return t.pipe(S(e=>{let t=e.type;if(r?.event(`VERIFY`,`Event:`,e,{type:e.type}),c)return _(()=>new n(`Cannot send event type '${t}': The run has already errored with 'RUN_ERROR'. No further events can be sent.`));if(s&&t!==i.RUN_ERROR&&t!==i.RUN_STARTED)return _(()=>new n(`Cannot send event type '${t}': The run has already finished with 'RUN_FINISHED'. Start a new run with 'RUN_STARTED'.`));if(!l){if(l=!0,t!==i.RUN_STARTED&&t!==i.RUN_ERROR)return _(()=>new n(`First event must be 'RUN_STARTED'`))}else if(t===i.RUN_STARTED){if(p&&!s)return _(()=>new n(`Cannot send 'RUN_STARTED' while a run is still active. The previous run must be finished with 'RUN_FINISHED' before starting a new run.`));s&&m()}switch(t){case i.TEXT_MESSAGE_START:{let t=e.messageId;return a.has(t)?_(()=>new n(`Cannot send 'TEXT_MESSAGE_START' event: A text message with ID '${t}' is already in progress. Complete it with 'TEXT_MESSAGE_END' first.`)):(a.set(t,!0),h(e))}case i.TEXT_MESSAGE_CONTENT:{let t=e.messageId;return a.has(t)?h(e):_(()=>new n(`Cannot send 'TEXT_MESSAGE_CONTENT' event: No active text message found with ID '${t}'. Start a text message with 'TEXT_MESSAGE_START' first.`))}case i.TEXT_MESSAGE_END:{let t=e.messageId;return a.has(t)?(a.delete(t),h(e)):_(()=>new n(`Cannot send 'TEXT_MESSAGE_END' event: No active text message found with ID '${t}'. A 'TEXT_MESSAGE_START' event must be sent first.`))}case i.TOOL_CALL_START:{let t=e.toolCallId;return o.has(t)?_(()=>new n(`Cannot send 'TOOL_CALL_START' event: A tool call with ID '${t}' is already in progress. Complete it with 'TOOL_CALL_END' first.`)):(o.set(t,!0),h(e))}case i.TOOL_CALL_ARGS:{let t=e.toolCallId;return o.has(t)?h(e):_(()=>new n(`Cannot send 'TOOL_CALL_ARGS' event: No active tool call found with ID '${t}'. Start a tool call with 'TOOL_CALL_START' first.`))}case i.TOOL_CALL_END:{let t=e.toolCallId;return o.has(t)?(o.delete(t),h(e)):_(()=>new n(`Cannot send 'TOOL_CALL_END' event: No active tool call found with ID '${t}'. A 'TOOL_CALL_START' event must be sent first.`))}case i.STEP_STARTED:{let t=e.stepName;return u.has(t)?_(()=>new n(`Step "${t}" is already active for 'STEP_STARTED'`)):(u.set(t,!0),h(e))}case i.STEP_FINISHED:{let t=e.stepName;return u.has(t)?(u.delete(t),h(e)):_(()=>new n(`Cannot send 'STEP_FINISHED' for step "${t}" that was not started`))}case i.RUN_STARTED:return p=!0,h(e);case i.RUN_FINISHED:if(u.size>0){let e=Array.from(u.keys()).join(`, `);return _(()=>new n(`Cannot send 'RUN_FINISHED' while steps are still active: ${e}`))}if(a.size>0){let e=Array.from(a.keys()).join(`, `);return _(()=>new n(`Cannot send 'RUN_FINISHED' while text messages are still active: ${e}`))}if(o.size>0){let e=Array.from(o.keys()).join(`, `);return _(()=>new n(`Cannot send 'RUN_FINISHED' while tool calls are still active: ${e}`))}return s=!0,h(e);case i.RUN_ERROR:return c=!0,h(e);case i.CUSTOM:return h(e);case i.THINKING_TEXT_MESSAGE_START:return d?f?_(()=>new n(`Cannot send 'THINKING_TEXT_MESSAGE_START' event: A thinking message is already in progress. Complete it with 'THINKING_TEXT_MESSAGE_END' first.`)):(f=!0,h(e)):_(()=>new n(`Cannot send 'THINKING_TEXT_MESSAGE_START' event: A thinking step is not in progress. Create one with 'THINKING_START' first.`));case i.THINKING_TEXT_MESSAGE_CONTENT:return f?h(e):_(()=>new n(`Cannot send 'THINKING_TEXT_MESSAGE_CONTENT' event: No active thinking message found. Start a message with 'THINKING_TEXT_MESSAGE_START' first.`));case i.THINKING_TEXT_MESSAGE_END:return f?(f=!1,h(e)):_(()=>new n(`Cannot send 'THINKING_TEXT_MESSAGE_END' event: No active thinking message found. A 'THINKING_TEXT_MESSAGE_START' event must be sent first.`));case i.THINKING_START:return d?_(()=>new n(`Cannot send 'THINKING_START' event: A thinking step is already in progress. End it with 'THINKING_END' first.`)):(d=!0,h(e));case i.THINKING_END:return d?(d=!1,h(e)):_(()=>new n(`Cannot send 'THINKING_END' event: No active thinking step found. A 'THINKING_START' event must be sent first.`));default:return h(e)}}))};let R=function(e){return e.HEADERS=`headers`,e.DATA=`data`,e}({});const z=(e,t)=>u(()=>f(fetch(e,t))).pipe(re(e=>{if(!e.ok){let t=e.headers.get(`content-type`)||``;return f(e.text()).pipe(S(n=>{let r=n;if(t.includes(`application/json`))try{r=JSON.parse(n)}catch{}let i=Error(`HTTP ${e.status}: ${typeof r==`string`?r:JSON.stringify(r)}`);return i.status=e.status,i.payload=r,_(()=>i)}))}let t={type:R.HEADERS,status:e.status,headers:e.headers},n=e.body?.getReader();return n?new s(e=>(e.next(t),(async()=>{try{for(;;){let{done:t,value:r}=await n.read();if(t)break;let i={type:R.DATA,data:r};e.next(i)}e.complete()}catch(t){e.error(t)}})(),()=>{n.cancel().catch(e=>{if(e?.name!==`AbortError`)throw e})})):_(()=>Error(`Failed to getReader() from response`))})),B=(e,t)=>{let n=N(t),r=new l,i=new TextDecoder(`utf-8`,{fatal:!1}),a=``;e.subscribe({next:e=>{if(e.type!==R.HEADERS&&e.type===R.DATA&&e.data){let t=i.decode(e.data,{stream:!0});a+=t;let n=a.split(/\n\n/);a=n.pop()||``;for(let e of n)o(e)}},error:e=>r.error(e),complete:()=>{a&&(a+=i.decode(),o(a)),r.complete()}});function o(e){let t=e.split(` ++import{v4 as e}from"uuid";import{AGUIConnectNotImplementedError as t,AGUIError as n,EventSchemas as r,EventType as i}from"@ag-ui/core";import a from"fast-json-patch";import{EMPTY as o,Observable as s,ReplaySubject as c,Subject as l,defer as u,finalize as d,from as f,lastValueFrom as p,mergeMap as m,of as h,pipe as g,throwError as _}from"rxjs";import{catchError as v,concatMap as y,defaultIfEmpty as ee,filter as te,finalize as b,map as x,mergeAll as ne,mergeMap as S,switchMap as re,takeUntil as C,tap as ie}from"rxjs/operators";import w from"untruncate-json";import*as T from"@ag-ui/proto";import{z as E}from"zod";import{compareVersions as D}from"compare-versions";export*from"@ag-ui/core";const O=e=>{if(typeof structuredClone==`function`)return structuredClone(e);try{return JSON.parse(JSON.stringify(e))}catch{return Array.isArray(e)?[...e]:{...e}}};function k(){return e()}function A(e){if(Object.freeze(e),typeof e==`object`&&e)for(let t of Object.values(e))typeof t==`object`&&t&&!Object.isFrozen(t)&&A(t);return e}async function j(e,t,n,r){let i=typeof process<`u`&&process.env!==void 0,a=i&&(process.env.NODE_ENV===`test`||!!process.env.VITEST_WORKER_ID),o=i&&(process.env.NODE_ENV===`development`||process.env.NODE_ENV===`test`||!!process.env.VITEST_WORKER_ID),s=O(t),c=O(n),l=s,u=c,d;for(let t of e)try{o&&(A(l),A(u));let e=await r(t,l,u);if(e===void 0)continue;if(e.messages!==void 0&&e.messages!==l&&(l=O(e.messages)),e.state!==void 0&&e.state!==u&&(u=O(e.state)),d=e.stopPropagation,d===!0)break}catch(e){if(o&&e instanceof TypeError){if(a)throw e;console.error(`AG-UI: Subscriber attempted to mutate frozen inputs in-place. Return mutations via AgentStateMutation instead of mutating directly.`,e)}else a||console.error(`Subscriber error:`,e);continue}return{...l===s?{}:{messages:o&&Object.isFrozen(l)?O(l):l},...u===c?{}:{state:o&&Object.isFrozen(u)?O(u):u},...d===void 0?{}:{stopPropagation:d}}}function M(e){if(!e)return{enabled:!1,events:!1,lifecycle:!1,verbose:!1};if(e===!0)return{enabled:!0,events:!0,lifecycle:!0,verbose:!0};let t=e.events??!0,n=e.lifecycle??!0,r=e.verbose??!1;return{enabled:t||n,events:t,lifecycle:n,verbose:r}}function N(e){if(e instanceof P)return e;if(e===!0)return new P(M(!0))}var P=class{constructor(e){this.config=e}event(e,t,n,r){this.config.events&&(this.config.verbose?console.debug(`[${e}] ${t}`,typeof n==`string`?n:JSON.stringify(n)):console.debug(`[${e}] ${t}`,r??n))}lifecycle(e,t,n){this.config.lifecycle&&(n?console.debug(`[${e}] ${t}`,n):console.debug(`[${e}] ${t}`))}get eventsEnabled(){return this.config.events}get lifecycleEnabled(){return this.config.lifecycle}get enabled(){return this.config.enabled}};function F(e){return e.enabled?new P(e):void 0}function ae(e,t,n){if(t){let r=e.find(e=>e.id===t);if(r?.role===`assistant`)return r;r&&console.warn(`TOOL_CALL_START: parentMessageId '${t}' matches a '${r.role}' message, not assistant — falling back to toolCallId`);let i={id:r?n:t,role:`assistant`,toolCalls:[]};return e.push(i),i}let r={id:n,role:`assistant`,toolCalls:[]};return e.push(r),r}const I=(e,t,n,r,s)=>{let c=N(s),l=O(n.messages),u=O(e.state),d={},f=e=>{e.messages!==void 0&&(l=e.messages,d.messages=e.messages),e.state!==void 0&&(u=e.state,d.state=e.state)},p=()=>{let e=O(d);return d={},e.messages!==void 0||e.state!==void 0?h(e):o};return t.pipe(y(async t=>{let o=await j(r,l,u,(r,i,a)=>r.onEvent?.({event:t,agent:n,input:e,messages:i,state:a}));if(f(o),o.stopPropagation===!0?c?.event(`APPLY`,`Event dropped:`,t,{type:t.type,reason:`stopPropagation by subscriber`}):c?.event(`APPLY`,`Event applied:`,t,{type:t.type,subscribers:r.length}),o.stopPropagation===!0)return p();switch(t.type){case i.TEXT_MESSAGE_START:{let i=await j(r,l,u,(r,i,a)=>r.onTextMessageStartEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{messageId:e,role:n=`assistant`,name:r}=t;if(!l.find(t=>t.id===e)){let t={id:e,role:n,content:``,...r!==void 0&&{name:r}};l.push(t),f({messages:l})}}return p()}case i.TEXT_MESSAGE_CONTENT:{let{messageId:i,delta:a}=t,o=l.find(e=>e.id===i);if(!o)return console.warn(`TEXT_MESSAGE_CONTENT: No message found with ID '${i}'`),p();let s=await j(r,l,u,(r,i,a)=>r.onTextMessageContentEvent?.({event:t,messages:i,state:a,agent:n,input:e,textMessageBuffer:typeof o.content==`string`?o.content:``}));return f(s),s.stopPropagation!==!0&&(o.content=`${typeof o.content==`string`?o.content:``}${a}`,f({messages:l})),p()}case i.TEXT_MESSAGE_END:{let{messageId:i}=t,a=l.find(e=>e.id===i);return a?(f(await j(r,l,u,(r,i,o)=>r.onTextMessageEndEvent?.({event:t,messages:i,state:o,agent:n,input:e,textMessageBuffer:typeof a.content==`string`?a.content:``}))),await Promise.all(r.map(t=>{t.onNewMessage?.({message:a,messages:l,state:u,agent:n,input:e})})),p()):(console.warn(`TEXT_MESSAGE_END: No message found with ID '${i}'`),p())}case i.TOOL_CALL_START:{let i=await j(r,l,u,(r,i,a)=>r.onToolCallStartEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{toolCallId:e,toolCallName:n,parentMessageId:r}=t,i=ae(l,r,e);i.toolCalls??=[],i.toolCalls.push({id:e,type:`function`,function:{name:n,arguments:``}}),f({messages:l})}return p()}case i.TOOL_CALL_ARGS:{let{toolCallId:i,delta:a}=t,o=l.find(e=>e.toolCalls?.some(e=>e.id===i));if(!o)return console.warn(`TOOL_CALL_ARGS: No message found containing tool call with ID '${i}'`),p();let s=o.toolCalls?.find(e=>e.id===i);if(!s)return console.warn(`TOOL_CALL_ARGS: No tool call found with ID '${i}'`),p();let c=await j(r,l,u,(r,i,a)=>{let o=s.function.arguments,c=s.function.name,l={};try{l=w(o)}catch{}return r.onToolCallArgsEvent?.({event:t,messages:i,state:a,agent:n,input:e,toolCallBuffer:o,toolCallName:c,partialToolCallArgs:l})});return f(c),c.stopPropagation!==!0&&(s.function.arguments+=a,f({messages:l})),p()}case i.TOOL_CALL_END:{let{toolCallId:i}=t,a=l.find(e=>e.toolCalls?.some(e=>e.id===i));if(!a)return console.warn(`TOOL_CALL_END: No message found containing tool call with ID '${i}'`),p();let o=a.toolCalls?.find(e=>e.id===i);return o?(f(await j(r,l,u,(r,i,a)=>{let s=o.function.arguments,c=o.function.name,l={};try{l=JSON.parse(s)}catch{}return r.onToolCallEndEvent?.({event:t,messages:i,state:a,agent:n,input:e,toolCallName:c,toolCallArgs:l})})),await Promise.all(r.map(t=>{t.onNewToolCall?.({toolCall:o,messages:l,state:u,agent:n,input:e})})),p()):(console.warn(`TOOL_CALL_END: No tool call found with ID '${i}'`),p())}case i.TOOL_CALL_RESULT:{let i=await j(r,l,u,(r,i,a)=>r.onToolCallResultEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{messageId:i,toolCallId:a,content:o,role:s}=t,c={id:i,toolCallId:a,role:s||`tool`,content:o};l.push(c),await Promise.all(r.map(t=>{t.onNewMessage?.({message:c,messages:l,state:u,agent:n,input:e})})),f({messages:l})}return p()}case i.STATE_SNAPSHOT:{let i=await j(r,l,u,(r,i,a)=>r.onStateSnapshotEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{snapshot:e}=t;u=e,f({state:u})}return p()}case i.STATE_DELTA:{let i=await j(r,l,u,(r,i,a)=>r.onStateDeltaEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{delta:e}=t;try{u=a.applyPatch(u,e,!0,!1).newDocument,f({state:u})}catch(t){let n=t instanceof Error?t.message:String(t);console.warn(`Failed to apply state patch:\nCurrent state: ${JSON.stringify(u,null,2)}\nPatch operations: ${JSON.stringify(e,null,2)}\nError: ${n}`)}}return p()}case i.MESSAGES_SNAPSHOT:{let i=await j(r,l,u,(r,i,a)=>r.onMessagesSnapshotEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{messages:e}=t,n=new Map(e.map(e=>[e.id,e])),r=e=>e===`activity`||e===`reasoning`;l=l.filter(e=>r(e.role)||n.has(e.id)).map(e=>r(e.role)?e:n.get(e.id));let i=new Set(l.map(e=>e.id));for(let t of e)i.has(t.id)||l.push(t);f({messages:l})}return p()}case i.ACTIVITY_SNAPSHOT:{let i=t,a=l.findIndex(e=>e.id===i.messageId),o=a>=0?l[a]:void 0,s=o?.role===`activity`?o:void 0,c=i.replace??!0,d=await j(r,l,u,(t,r,a)=>t.onActivitySnapshotEvent?.({event:i,messages:r,state:a,agent:n,input:e,activityMessage:s,existingMessage:o}));if(f(d),d.stopPropagation!==!0){let t={id:i.messageId,role:`activity`,activityType:i.activityType,content:O(i.content)},o;a===-1?(l.push(t),o=t):s?c&&(l[a]={...s,activityType:i.activityType,content:O(i.content)}):c&&(l[a]=t,o=t),f({messages:l}),o&&await Promise.all(r.map(t=>t.onNewMessage?.({message:o,messages:l,state:u,agent:n,input:e})))}return p()}case i.ACTIVITY_DELTA:{let i=t,o=l.findIndex(e=>e.id===i.messageId);if(o===-1)return p();let s=l[o];if(s.role!==`activity`)return console.warn(`ACTIVITY_DELTA: Message '${i.messageId}' is not an activity message`),p();let c=s,d=await j(r,l,u,(t,r,a)=>t.onActivityDeltaEvent?.({event:i,messages:r,state:a,agent:n,input:e,activityMessage:c}));if(f(d),d.stopPropagation!==!0)try{let e=O(c.content??{}),t=a.applyPatch(e,i.patch??[],!0,!1).newDocument;l[o]={...c,content:O(t),activityType:i.activityType},f({messages:l})}catch(e){let t=e instanceof Error?e.message:String(e);console.warn(`Failed to apply activity patch for '${i.messageId}': ${t}`)}return p()}case i.RAW:return f(await j(r,l,u,(r,i,a)=>r.onRawEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.CUSTOM:return f(await j(r,l,u,(r,i,a)=>r.onCustomEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.RUN_STARTED:{let i=await j(r,l,u,(r,i,a)=>r.onRunStartedEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let e=t;if(e.input?.messages){for(let t of e.input.messages)l.find(e=>e.id===t.id)||l.push(t);f({messages:l})}}return p()}case i.RUN_FINISHED:return f(await j(r,l,u,(r,i,a)=>r.onRunFinishedEvent?.({event:t,messages:i,state:a,agent:n,input:e,result:t.result}))),p();case i.RUN_ERROR:return f(await j(r,l,u,(r,i,a)=>r.onRunErrorEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.STEP_STARTED:return f(await j(r,l,u,(r,i,a)=>r.onStepStartedEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.STEP_FINISHED:return f(await j(r,l,u,(r,i,a)=>r.onStepFinishedEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.TEXT_MESSAGE_CHUNK:throw Error(`TEXT_MESSAGE_CHUNK must be tranformed before being applied`);case i.TOOL_CALL_CHUNK:throw Error(`TOOL_CALL_CHUNK must be tranformed before being applied`);case i.THINKING_START:return p();case i.THINKING_END:return p();case i.THINKING_TEXT_MESSAGE_START:return p();case i.THINKING_TEXT_MESSAGE_CONTENT:return p();case i.THINKING_TEXT_MESSAGE_END:return p();case i.REASONING_START:return f(await j(r,l,u,(r,i,a)=>r.onReasoningStartEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.REASONING_MESSAGE_START:{let i=await j(r,l,u,(r,i,a)=>r.onReasoningMessageStartEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(i),i.stopPropagation!==!0){let{messageId:e}=t;if(!l.find(t=>t.id===e)){let t={id:e,role:`reasoning`,content:``};l.push(t),f({messages:l})}}return p()}case i.REASONING_MESSAGE_CONTENT:{let{messageId:i,delta:a}=t,o=l.find(e=>e.id===i);if(!o)return console.warn(`REASONING_MESSAGE_CONTENT: No message found with ID '${i}'`),p();let s=await j(r,l,u,(r,i,a)=>r.onReasoningMessageContentEvent?.({event:t,messages:i,state:a,agent:n,input:e,reasoningMessageBuffer:typeof o.content==`string`?o.content:``}));return f(s),s.stopPropagation!==!0&&(o.content=`${typeof o.content==`string`?o.content:``}${a}`,f({messages:l})),p()}case i.REASONING_MESSAGE_END:{let{messageId:i}=t,a=l.find(e=>e.id===i);return a?(f(await j(r,l,u,(r,i,o)=>r.onReasoningMessageEndEvent?.({event:t,messages:i,state:o,agent:n,input:e,reasoningMessageBuffer:typeof a.content==`string`?a.content:``}))),await Promise.all(r.map(t=>{t.onNewMessage?.({message:a,messages:l,state:u,agent:n,input:e})})),p()):(console.warn(`REASONING_MESSAGE_END: No message found with ID '${i}'`),p())}case i.REASONING_MESSAGE_CHUNK:throw Error(`REASONING_MESSAGE_CHUNK must be transformed before being applied`);case i.REASONING_END:return f(await j(r,l,u,(r,i,a)=>r.onReasoningEndEvent?.({event:t,messages:i,state:a,agent:n,input:e}))),p();case i.REASONING_ENCRYPTED_VALUE:{let{subtype:i,entityId:a,encryptedValue:o}=t,s=await j(r,l,u,(r,i,a)=>r.onReasoningEncryptedValueEvent?.({event:t,messages:i,state:a,agent:n,input:e}));if(f(s),s.stopPropagation!==!0){let e=!1;if(i===`tool-call`){for(let t of l)if(t.role===`assistant`&&t.toolCalls){let n=t.toolCalls.find(e=>e.id===a);if(n){n.encryptedValue=o,e=!0;break}}}else{let t=l.find(e=>e.id===a);t?.role!==`activity`&&t&&(t.encryptedValue=o,e=!0)}e&&(d.messages=l)}return p()}}return t.type,p()}),ne(),r.length>0?ee({}):e=>e)},L=e=>t=>{let r=N(e),a=new Map,o=new Map,s=!1,c=!1,l=!1,u=new Map,d=!1,f=!1,p=!1,m=()=>{a.clear(),o.clear(),u.clear(),d=!1,f=!1,s=!1,c=!1,p=!0};return t.pipe(S(e=>{let t=e.type;if(r?.event(`VERIFY`,`Event:`,e,{type:e.type}),c)return _(()=>new n(`Cannot send event type '${t}': The run has already errored with 'RUN_ERROR'. No further events can be sent.`));if(s&&t!==i.RUN_ERROR&&t!==i.RUN_STARTED)return _(()=>new n(`Cannot send event type '${t}': The run has already finished with 'RUN_FINISHED'. Start a new run with 'RUN_STARTED'.`));if(!l){if(l=!0,t!==i.RUN_STARTED&&t!==i.RUN_ERROR)return _(()=>new n(`First event must be 'RUN_STARTED'`))}else if(t===i.RUN_STARTED){if(p&&!s)return _(()=>new n(`Cannot send 'RUN_STARTED' while a run is still active. The previous run must be finished with 'RUN_FINISHED' before starting a new run.`));s&&m()}switch(t){case i.TEXT_MESSAGE_START:{let t=e.messageId;return a.has(t)?_(()=>new n(`Cannot send 'TEXT_MESSAGE_START' event: A text message with ID '${t}' is already in progress. Complete it with 'TEXT_MESSAGE_END' first.`)):(a.set(t,!0),h(e))}case i.TEXT_MESSAGE_CONTENT:{let t=e.messageId;return a.has(t)?h(e):_(()=>new n(`Cannot send 'TEXT_MESSAGE_CONTENT' event: No active text message found with ID '${t}'. Start a text message with 'TEXT_MESSAGE_START' first.`))}case i.TEXT_MESSAGE_END:{let t=e.messageId;return a.has(t)?(a.delete(t),h(e)):_(()=>new n(`Cannot send 'TEXT_MESSAGE_END' event: No active text message found with ID '${t}'. A 'TEXT_MESSAGE_START' event must be sent first.`))}case i.TOOL_CALL_START:{let t=e.toolCallId;return o.has(t)?_(()=>new n(`Cannot send 'TOOL_CALL_START' event: A tool call with ID '${t}' is already in progress. Complete it with 'TOOL_CALL_END' first.`)):(o.set(t,!0),h(e))}case i.TOOL_CALL_ARGS:{let t=e.toolCallId;return o.has(t)?h(e):_(()=>new n(`Cannot send 'TOOL_CALL_ARGS' event: No active tool call found with ID '${t}'. Start a tool call with 'TOOL_CALL_START' first.`))}case i.TOOL_CALL_END:{let t=e.toolCallId;return o.has(t)?(o.delete(t),h(e)):_(()=>new n(`Cannot send 'TOOL_CALL_END' event: No active tool call found with ID '${t}'. A 'TOOL_CALL_START' event must be sent first.`))}case i.STEP_STARTED:{let t=e.stepName;return u.has(t)?_(()=>new n(`Step "${t}" is already active for 'STEP_STARTED'`)):(u.set(t,!0),h(e))}case i.STEP_FINISHED:{let t=e.stepName;return u.has(t)?(u.delete(t),h(e)):_(()=>new n(`Cannot send 'STEP_FINISHED' for step "${t}" that was not started`))}case i.RUN_STARTED:return p=!0,h(e);case i.RUN_FINISHED:if(u.size>0){let e=Array.from(u.keys()).join(`, `);return _(()=>new n(`Cannot send 'RUN_FINISHED' while steps are still active: ${e}`))}if(a.size>0){let e=Array.from(a.keys()).join(`, `);return _(()=>new n(`Cannot send 'RUN_FINISHED' while text messages are still active: ${e}`))}if(o.size>0){let e=Array.from(o.keys()).join(`, `);return _(()=>new n(`Cannot send 'RUN_FINISHED' while tool calls are still active: ${e}`))}return s=!0,h(e);case i.RUN_ERROR:return c=!0,h(e);case i.CUSTOM:return h(e);case i.THINKING_TEXT_MESSAGE_START:return d?f?_(()=>new n(`Cannot send 'THINKING_TEXT_MESSAGE_START' event: A thinking message is already in progress. Complete it with 'THINKING_TEXT_MESSAGE_END' first.`)):(f=!0,h(e)):_(()=>new n(`Cannot send 'THINKING_TEXT_MESSAGE_START' event: A thinking step is not in progress. Create one with 'THINKING_START' first.`));case i.THINKING_TEXT_MESSAGE_CONTENT:return f?h(e):_(()=>new n(`Cannot send 'THINKING_TEXT_MESSAGE_CONTENT' event: No active thinking message found. Start a message with 'THINKING_TEXT_MESSAGE_START' first.`));case i.THINKING_TEXT_MESSAGE_END:return f?(f=!1,h(e)):_(()=>new n(`Cannot send 'THINKING_TEXT_MESSAGE_END' event: No active thinking message found. A 'THINKING_TEXT_MESSAGE_START' event must be sent first.`));case i.THINKING_START:return d?_(()=>new n(`Cannot send 'THINKING_START' event: A thinking step is already in progress. End it with 'THINKING_END' first.`)):(d=!0,h(e));case i.THINKING_END:return d?(d=!1,h(e)):_(()=>new n(`Cannot send 'THINKING_END' event: No active thinking step found. A 'THINKING_START' event must be sent first.`));default:return h(e)}}))};let R=function(e){return e.HEADERS=`headers`,e.DATA=`data`,e}({});const z=(e,t)=>u(()=>f(fetch(e,t))).pipe(re(e=>{if(!e.ok){let t=e.headers.get(`content-type`)||``;return f(e.text()).pipe(S(n=>{let r=n;if(t.includes(`application/json`))try{r=JSON.parse(n)}catch{}let i=Error(`HTTP ${e.status}: ${typeof r==`string`?r:JSON.stringify(r)}`);return i.status=e.status,i.payload=r,_(()=>i)}))}let t={type:R.HEADERS,status:e.status,headers:e.headers},n=e.body?.getReader();return n?new s(e=>(e.next(t),(async()=>{try{for(;;){let{done:t,value:r}=await n.read();if(t)break;let i={type:R.DATA,data:r};e.next(i)}e.complete()}catch(t){e.error(t)}})(),()=>{n.cancel().catch(e=>{if(e?.name!==`AbortError`)throw e})})):_(()=>Error(`Failed to getReader() from response`))})),B=(e,t)=>{let n=N(t),r=new l,i=new TextDecoder(`utf-8`,{fatal:!1}),a=``;e.subscribe({next:e=>{if(e.type!==R.HEADERS&&e.type===R.DATA&&e.data){let t=i.decode(e.data,{stream:!0});a+=t;let n=a.split(/\n\n/);a=n.pop()||``;for(let e of n)o(e)}},error:e=>r.error(e),complete:()=>{a&&(a+=i.decode(),o(a)),r.complete()}});function o(e){let t=e.split(` + `),i=[];for(let e of t)e.startsWith(`data:`)&&i.push(e.slice(5).replace(/^ /,``));if(i.length>0)try{let e=i.join(` + `),t=JSON.parse(e);n?.event(`SSE`,`Event received:`,t,{type:t.type}),r.next(t)}catch(e){r.error(e)}}return r.asObservable()},V=e=>{let t=new l,n=new Uint8Array;e.subscribe({next:e=>{if(e.type!==R.HEADERS&&e.type===R.DATA&&e.data){let t=new Uint8Array(n.length+e.data.length);t.set(n,0),t.set(e.data,n.length),n=t,r()}},error:e=>t.error(e),complete:()=>{if(n.length>0)try{r()}catch{console.warn(`Incomplete or invalid protocol buffer data at stream end`)}t.complete()}});function r(){for(;n.length>=4;){let e=4+new DataView(n.buffer,n.byteOffset,4).getUint32(0,!1);if(n.length{let n=N(t),a=new l,o=new c,s=!1;return e.subscribe({next:e=>{if(o.next(e),e.type===R.HEADERS&&!s){s=!0;let t=e.headers.get(`content-type`);n?.lifecycle(`HTTP`,`Stream format detected:`,{contentType:t,parser:t===T.AGUI_MEDIA_TYPE?`protobuf`:`sse`}),t===T.AGUI_MEDIA_TYPE?V(o).subscribe({next:e=>a.next(e),error:e=>a.error(e),complete:()=>a.complete()}):B(o,n).subscribe({next:e=>{try{let t=r.parse(e);n?.event(`HTTP`,`Event validated:`,t,{type:t.type,valid:!0}),a.next(t)}catch(t){n?.event(`HTTP`,`Event invalid:`,{json:e,error:String(t)}),a.error(t)}},error:e=>{if(e?.name===`AbortError`){a.next({type:i.RUN_ERROR,message:e.message||`Request aborted`,code:`abort`,rawEvent:e}),a.complete();return}return a.error(e)},complete:()=>a.complete()})}else s||a.error(Error(`No headers event received before data events`))},error:e=>{o.error(e),a.error(e)},complete:()=>{o.complete()}}),a.asObservable()},U=E.enum([`TextMessageStart`,`TextMessageContent`,`TextMessageEnd`,`ActionExecutionStart`,`ActionExecutionArgs`,`ActionExecutionEnd`,`ActionExecutionResult`,`AgentStateMessage`,`MetaEvent`,`RunStarted`,`RunFinished`,`RunError`,`NodeStarted`,`NodeFinished`]),oe=E.enum([`LangGraphInterruptEvent`,`PredictState`,`Exit`]),se=E.object({type:E.literal(U.enum.TextMessageStart),messageId:E.string(),parentMessageId:E.string().optional(),role:E.string().optional()}),ce=E.object({type:E.literal(U.enum.TextMessageContent),messageId:E.string(),content:E.string()}),le=E.object({type:E.literal(U.enum.TextMessageEnd),messageId:E.string()}),ue=E.object({type:E.literal(U.enum.ActionExecutionStart),actionExecutionId:E.string(),actionName:E.string(),parentMessageId:E.string().optional()}),de=E.object({type:E.literal(U.enum.ActionExecutionArgs),actionExecutionId:E.string(),args:E.string()}),fe=E.object({type:E.literal(U.enum.ActionExecutionEnd),actionExecutionId:E.string()}),pe=E.object({type:E.literal(U.enum.ActionExecutionResult),actionName:E.string(),actionExecutionId:E.string(),result:E.string()}),me=E.object({type:E.literal(U.enum.AgentStateMessage),threadId:E.string(),agentName:E.string(),nodeName:E.string(),runId:E.string(),active:E.boolean(),role:E.string(),state:E.string(),running:E.boolean()}),he=E.object({type:E.literal(U.enum.MetaEvent),name:oe,value:E.any()}),ge=E.object({type:E.literal(U.enum.RunError),message:E.string(),code:E.string().optional()});E.discriminatedUnion(`type`,[se,ce,le,ue,de,fe,pe,me,he,ge]),E.object({id:E.string(),role:E.string(),content:E.string(),parentMessageId:E.string().optional()}),E.object({id:E.string(),name:E.string(),arguments:E.any(),parentMessageId:E.string().optional()}),E.object({id:E.string(),result:E.any(),actionExecutionId:E.string(),actionName:E.string()});const _e=e=>{if(typeof e==`string`)return e;if(!Array.isArray(e))return;let t=e.filter(e=>e.type===`text`).map(e=>e.text).filter(e=>e.length>0);if(t.length!==0)return t.join(` + `)},W=(e,t,n)=>r=>{let o={},s=!0,c=!0,l=``,u=null,d=null,f=[],p={},m=e=>{typeof e==`object`&&e&&(`messages`in e&&delete e.messages,o=e)};return r.pipe(S(r=>{switch(r.type){case i.TEXT_MESSAGE_START:{let e=r;return[{type:U.enum.TextMessageStart,messageId:e.messageId,role:e.role}]}case i.TEXT_MESSAGE_CONTENT:{let e=r;return[{type:U.enum.TextMessageContent,messageId:e.messageId,content:e.delta}]}case i.TEXT_MESSAGE_END:{let e=r;return[{type:U.enum.TextMessageEnd,messageId:e.messageId}]}case i.TOOL_CALL_START:{let e=r;return f.push({id:e.toolCallId,type:`function`,function:{name:e.toolCallName,arguments:``}}),c=!0,p[e.toolCallId]=e.toolCallName,[{type:U.enum.ActionExecutionStart,actionExecutionId:e.toolCallId,actionName:e.toolCallName,parentMessageId:e.parentMessageId}]}case i.TOOL_CALL_ARGS:{let i=r,a=f.find(e=>e.id===i.toolCallId);if(!a)return console.warn(`TOOL_CALL_ARGS: No tool call found with ID '${i.toolCallId}'`),[];a.function.arguments+=i.delta;let u=!1;if(d){let e=d.find(e=>e.tool==a.function.name);if(e)try{let t=JSON.parse(w(a.function.arguments));e.tool_argument&&e.tool_argument in t?(m({...o,[e.state_key]:t[e.tool_argument]}),u=!0):e.tool_argument||(m({...o,[e.state_key]:t}),u=!0)}catch{}}return[{type:U.enum.ActionExecutionArgs,actionExecutionId:i.toolCallId,args:i.delta},...u?[{type:U.enum.AgentStateMessage,threadId:e,agentName:n,nodeName:l,runId:t,running:s,role:`assistant`,state:JSON.stringify(o),active:c}]:[]]}case i.TOOL_CALL_END:{let e=r;return[{type:U.enum.ActionExecutionEnd,actionExecutionId:e.toolCallId}]}case i.TOOL_CALL_RESULT:{let e=r;return[{type:U.enum.ActionExecutionResult,actionExecutionId:e.toolCallId,result:e.content,actionName:p[e.toolCallId]||`unknown`}]}case i.RAW:return[];case i.CUSTOM:{let e=r;switch(e.name){case`Exit`:s=!1;break;case`PredictState`:d=e.value;break}return[{type:U.enum.MetaEvent,name:e.name,value:e.value}]}case i.STATE_SNAPSHOT:return m(r.snapshot),[{type:U.enum.AgentStateMessage,threadId:e,agentName:n,nodeName:l,runId:t,running:s,role:`assistant`,state:JSON.stringify(o),active:c}];case i.STATE_DELTA:{let i=r,u=a.applyPatch(o,i.delta,!0,!1);return u?(m(u.newDocument),[{type:U.enum.AgentStateMessage,threadId:e,agentName:n,nodeName:l,runId:t,running:s,role:`assistant`,state:JSON.stringify(o),active:c}]):[]}case i.MESSAGES_SNAPSHOT:return u=r.messages,[{type:U.enum.AgentStateMessage,threadId:e,agentName:n,nodeName:l,runId:t,running:s,role:`assistant`,state:JSON.stringify({...o,...u?{messages:u}:{}}),active:!0}];case i.RUN_STARTED:return[];case i.RUN_FINISHED:return u&&(o.messages=u),Object.keys(o).length===0?[]:[{type:U.enum.AgentStateMessage,threadId:e,agentName:n,nodeName:l,runId:t,running:s,role:`assistant`,state:JSON.stringify({...o,...u?{messages:ve(u)}:{}}),active:!1}];case i.RUN_ERROR:{let e=r;return[{type:U.enum.RunError,message:e.message,code:e.code}]}case i.STEP_STARTED:return l=r.stepName,f=[],d=null,[{type:U.enum.AgentStateMessage,threadId:e,agentName:n,nodeName:l,runId:t,running:s,role:`assistant`,state:JSON.stringify(o),active:!0}];case i.STEP_FINISHED:return f=[],d=null,[{type:U.enum.AgentStateMessage,threadId:e,agentName:n,nodeName:l,runId:t,running:s,role:`assistant`,state:JSON.stringify(o),active:!1}];default:return[]}}))};function ve(e){let t=[];for(let n of e)if(n.role===`assistant`||n.role===`user`||n.role===`system`){let e=_e(n.content);if(e){let r={id:n.id,role:n.role,content:e};t.push(r)}if(n.role===`assistant`&&n.toolCalls&&n.toolCalls.length>0)for(let e of n.toolCalls){let r={id:e.id,name:e.function.name,arguments:JSON.parse(e.function.arguments),parentMessageId:n.id};t.push(r)}}else if(n.role===`tool`){let r=`unknown`;for(let t of e)if(t.role===`assistant`&&t.toolCalls?.length){for(let e of t.toolCalls)if(e.id===n.toolCallId){r=e.function.name;break}}let i={id:n.id,result:n.content,actionExecutionId:n.toolCallId,actionName:r};t.push(i)}return t}const G=e=>t=>{let n=N(e),r,a,o,s,c=()=>{if(!r||s!==`text`)throw Error(`No text message to close`);let e={type:i.TEXT_MESSAGE_END,messageId:r.messageId};return s=void 0,r=void 0,n?.event(`TRANSFORM`,`TEXT_MESSAGE_END`,e,{messageId:e.messageId}),e},l=()=>{if(!a||s!==`tool`)throw Error(`No tool call to close`);let e={type:i.TOOL_CALL_END,toolCallId:a.toolCallId};return s=void 0,a=void 0,n?.event(`TRANSFORM`,`TOOL_CALL_END`,e,{toolCallId:e.toolCallId}),e},u=()=>{if(!o||s!==`reasoning`)throw Error(`No reasoning message to close`);let e={type:i.REASONING_MESSAGE_END,messageId:o.messageId};return s=void 0,o=void 0,n?.event(`TRANSFORM`,`REASONING_MESSAGE_END`,e,{messageId:e.messageId}),e},f=()=>s===`text`?[c()]:s===`tool`?[l()]:s===`reasoning`?[u()]:[];return t.pipe(m(e=>{switch(e.type){case i.TEXT_MESSAGE_START:case i.TEXT_MESSAGE_CONTENT:case i.TEXT_MESSAGE_END:case i.TOOL_CALL_START:case i.TOOL_CALL_ARGS:case i.TOOL_CALL_END:case i.TOOL_CALL_RESULT:case i.STATE_SNAPSHOT:case i.STATE_DELTA:case i.MESSAGES_SNAPSHOT:case i.CUSTOM:case i.RUN_STARTED:case i.RUN_FINISHED:case i.RUN_ERROR:case i.STEP_STARTED:case i.STEP_FINISHED:case i.THINKING_START:case i.THINKING_END:case i.THINKING_TEXT_MESSAGE_START:case i.THINKING_TEXT_MESSAGE_CONTENT:case i.THINKING_TEXT_MESSAGE_END:case i.REASONING_START:case i.REASONING_MESSAGE_START:case i.REASONING_MESSAGE_CONTENT:case i.REASONING_MESSAGE_END:case i.REASONING_END:return[...f(),e];case i.RAW:case i.ACTIVITY_SNAPSHOT:case i.ACTIVITY_DELTA:case i.REASONING_ENCRYPTED_VALUE:return[e];case i.TEXT_MESSAGE_CHUNK:let t=e,c=[];if((s!==`text`||t.messageId!==void 0&&t.messageId!==r?.messageId)&&c.push(...f()),s!==`text`){if(t.messageId===void 0)throw Error(`First TEXT_MESSAGE_CHUNK must have a messageId`);r={messageId:t.messageId,name:t.name},s=`text`;let e={type:i.TEXT_MESSAGE_START,messageId:t.messageId,role:t.role||`assistant`,...t.name!==void 0&&{name:t.name}};c.push(e),n?.event(`TRANSFORM`,`TEXT_MESSAGE_START`,e,{messageId:t.messageId})}if(t.delta!==void 0){let e={type:i.TEXT_MESSAGE_CONTENT,messageId:r.messageId,delta:t.delta};c.push(e),n?.event(`TRANSFORM`,`TEXT_MESSAGE_CONTENT`,e,{messageId:r.messageId})}return c;case i.TOOL_CALL_CHUNK:let l=e,u=[];if((s!==`tool`||l.toolCallId!==void 0&&l.toolCallId!==a?.toolCallId)&&u.push(...f()),s!==`tool`){if(l.toolCallId===void 0)throw Error(`First TOOL_CALL_CHUNK must have a toolCallId`);if(l.toolCallName===void 0)throw Error(`First TOOL_CALL_CHUNK must have a toolCallName`);a={toolCallId:l.toolCallId,toolCallName:l.toolCallName,parentMessageId:l.parentMessageId},s=`tool`;let e={type:i.TOOL_CALL_START,toolCallId:l.toolCallId,toolCallName:l.toolCallName,parentMessageId:l.parentMessageId};u.push(e),n?.event(`TRANSFORM`,`TOOL_CALL_START`,e,{toolCallId:l.toolCallId,toolCallName:l.toolCallName})}if(l.delta!==void 0){let e={type:i.TOOL_CALL_ARGS,toolCallId:a.toolCallId,delta:l.delta};u.push(e),n?.event(`TRANSFORM`,`TOOL_CALL_ARGS`,e,{toolCallId:a.toolCallId})}return u;case i.REASONING_MESSAGE_CHUNK:let d=e,p=[];if((s!==`reasoning`||d.messageId&&d.messageId!==o?.messageId)&&p.push(...f()),s!==`reasoning`){if(d.messageId===void 0)throw Error(`First REASONING_MESSAGE_CHUNK must have a messageId`);o={messageId:d.messageId},s=`reasoning`;let e={type:i.REASONING_MESSAGE_START,messageId:d.messageId};p.push(e),n?.event(`TRANSFORM`,`REASONING_MESSAGE_START`,e,{messageId:d.messageId})}if(d.delta!==void 0){let e={type:i.REASONING_MESSAGE_CONTENT,messageId:o.messageId,delta:d.delta};p.push(e),n?.event(`TRANSFORM`,`REASONING_MESSAGE_CONTENT`,e,{messageId:o.messageId})}return p}return e.type,[]}),d(()=>{f()}))};var K=class{runNext(e,t){return t.run(e).pipe(G(!1))}runNextWithState(e,t){let n=O(e.messages||[]),r=O(e.state||{}),i=new c;return I(e,i,t,[]).subscribe(e=>{e.messages!==void 0&&(n=e.messages),e.state!==void 0&&(r=e.state)}),this.runNext(e,t).pipe(y(async e=>(i.next(e),await new Promise(e=>setTimeout(e,0)),{event:e,messages:O(n),state:O(r)})))}},q=class extends K{constructor(e){super(),this.fn=e}run(e,t){return this.fn(e,t)}},ye=class extends K{constructor(e){if(super(),this.blockedToolCallIds=new Set,e.allowedToolCalls&&e.disallowedToolCalls)throw Error(`Cannot specify both allowedToolCalls and disallowedToolCalls`);if(!e.allowedToolCalls&&!e.disallowedToolCalls)throw Error(`Must specify either allowedToolCalls or disallowedToolCalls`);e.allowedToolCalls?this.allowedTools=new Set(e.allowedToolCalls):e.disallowedToolCalls&&(this.disallowedTools=new Set(e.disallowedToolCalls))}run(e,t){return this.runNext(e,t).pipe(te(e=>{if(e.type===i.TOOL_CALL_START){let t=e;return this.shouldFilterTool(t.toolCallName)?(this.blockedToolCallIds.add(t.toolCallId),!1):!0}if(e.type===i.TOOL_CALL_ARGS){let t=e;return!this.blockedToolCallIds.has(t.toolCallId)}if(e.type===i.TOOL_CALL_END){let t=e;return!this.blockedToolCallIds.has(t.toolCallId)}if(e.type===i.TOOL_CALL_RESULT){let t=e;return this.blockedToolCallIds.has(t.toolCallId)?(this.blockedToolCallIds.delete(t.toolCallId),!1):!0}return!0}))}shouldFilterTool(e){return this.allowedTools?!this.allowedTools.has(e):this.disallowedTools?this.disallowedTools.has(e):!1}};function be(e){let t=e.content;if(Array.isArray(t)){let n=t.filter(e=>typeof e==`object`&&!!e&&`type`in e&&e.type===`text`&&typeof e.text==`string`).map(e=>e.text).join(``);return{...e,content:n}}return typeof t==`string`?e:{...e,content:``}}var J=class extends K{run(e,t){let{parentRunId:n,...r}=e,i={...r,messages:r.messages.map(be)};return this.runNext(i,t)}};const Y=`THINKING_START`,X=`THINKING_END`,Z=`THINKING_TEXT_MESSAGE_START`,Q=`THINKING_TEXT_MESSAGE_CONTENT`,$=`THINKING_TEXT_MESSAGE_END`;var xe=class extends K{constructor(...e){super(...e),this.currentReasoningId=null,this.currentMessageId=null}warnAboutTransformation(e,t){typeof process<`u`&&process.env!==void 0&&process.env.SUPPRESS_TRANSFORMATION_WARNINGS||console.warn(`AG-UI is converting ${e} to ${t}. To remove this warning, upgrade your AG-UI integration package (e.g. @ag-ui/langgraph). To surpress it, set SUPPRESS_TRANSFORMATION_WARNINGS=true in your .env file.`)}run(e,t){return this.currentReasoningId=null,this.currentMessageId=null,this.runNext(e,t).pipe(x(e=>this.transformEvent(e)))}transformEvent(e){switch(e.type){case Y:{this.currentReasoningId=k();let{title:t,...n}=e;return this.warnAboutTransformation(Y,i.REASONING_START),{...n,type:i.REASONING_START,messageId:this.currentReasoningId}}case Z:return this.currentMessageId=k(),this.warnAboutTransformation(Z,i.REASONING_MESSAGE_START),{...e,type:i.REASONING_MESSAGE_START,messageId:this.currentMessageId,role:`assistant`};case Q:{let{delta:t,...n}=e;return this.warnAboutTransformation(Q,i.REASONING_MESSAGE_CONTENT),{...n,type:i.REASONING_MESSAGE_CONTENT,messageId:this.currentMessageId??k(),delta:t}}case $:{let t=this.currentMessageId??k();return this.warnAboutTransformation($,i.REASONING_MESSAGE_END),{...e,type:i.REASONING_MESSAGE_END,messageId:t}}case X:{let t=this.currentReasoningId??k();return this.warnAboutTransformation(X,i.REASONING_END),{...e,type:i.REASONING_END,messageId:t}}default:return e}}},Se=`0.0.52`,Ce=class{get maxVersion(){return Se}get debug(){return this._debug}set debug(e){this._debug=M(e),this._debugLogger=F(this._debug)}get debugLogger(){return this._debugLogger}set debugLogger(e){typeof e==`boolean`?this._debugLogger=e?F(M(!0)):void 0:this._debugLogger=e}constructor({agentId:t,description:n,threadId:r,initialMessages:i,initialState:a,debug:o}={}){this.subscribers=[],this.isRunning=!1,this.middlewares=[],this.agentId=t,this.description=n??``,this.threadId=r??e(),this.messages=O(i??[]),this.state=O(a??{}),this._debug=M(o),this._debugLogger=F(this._debug),D(this.maxVersion,`0.0.39`)<=0&&this.middlewares.unshift(new J),D(this.maxVersion,`0.0.45`)<=0&&this.middlewares.unshift(new xe)}subscribe(e){return this.subscribers.push(e),{unsubscribe:()=>{this.subscribers=this.subscribers.filter(t=>t!==e)}}}use(...e){let t=e.map(e=>typeof e==`function`?new q(e):e);return this.middlewares.push(...t),this}async runAgent(t,n){try{this.isRunning=!0,this.agentId=this.agentId??e();let r=this.prepareRunAgentInput(t);this.debugLogger?.lifecycle(`LIFECYCLE`,`Run started:`,{agentId:this.agentId,threadId:this.threadId});let i,a=new Set(this.messages.map(e=>e.id)),o=[{onRunFinishedEvent:e=>{i=e.result}},...this.subscribers,n??{}];await this.onInitialize(r,o),this.activeRunDetach$=new l;let s;this.activeRunCompletionPromise=new Promise(e=>{s=e}),await p(g(()=>this.middlewares.length===0?this.run(r):this.middlewares.reduceRight((e,t)=>({run:n=>t.run(n,e),get messages(){return e.messages},get state(){return e.state}}),this).run(r),G(this.debugLogger),L(this.debugLogger),e=>e.pipe(C(this.activeRunDetach$)),e=>this.apply(r,e,o),e=>this.processApplyEvents(r,e,o),v(e=>(this.debugLogger?.lifecycle(`LIFECYCLE`,`Run errored:`,{agentId:this.agentId,error:e instanceof Error?e.message:String(e)}),this.isRunning=!1,this.onError(r,e,o))),b(()=>{this.debugLogger?.lifecycle(`LIFECYCLE`,`Run finished:`,{agentId:this.agentId,threadId:this.threadId}),this.isRunning=!1,this.onFinalize(r,o),s?.(),s=void 0,this.activeRunCompletionPromise=void 0,this.activeRunDetach$=void 0}))(h(null)));let c=O(this.messages).filter(e=>!a.has(e.id));return{result:i,newMessages:c}}finally{this.isRunning=!1}}connect(e){throw new t}async connectAgent(n,r){try{this.isRunning=!0,this.agentId=this.agentId??e();let i=this.prepareRunAgentInput(n),a,s=new Set(this.messages.map(e=>e.id)),c=[{onRunFinishedEvent:e=>{a=e.result}},...this.subscribers,r??{}];await this.onInitialize(i,c),this.activeRunDetach$=new l;let d;this.activeRunCompletionPromise=new Promise(e=>{d=e}),await p(g(()=>u(()=>this.connect(i)),G(this.debugLogger),L(this.debugLogger),e=>e.pipe(C(this.activeRunDetach$)),e=>this.apply(i,e,c),e=>this.processApplyEvents(i,e,c),v(e=>(this.isRunning=!1,e instanceof t?o:this.onError(i,e,c))),b(()=>{this.isRunning=!1,this.onFinalize(i,c),d?.(),d=void 0,this.activeRunCompletionPromise=void 0,this.activeRunDetach$=void 0}))(h(null)),{defaultValue:void 0});let f=O(this.messages).filter(e=>!s.has(e.id));return{result:a,newMessages:f}}finally{this.isRunning=!1}}abortRun(){}async detachActiveRun(){if(!this.activeRunDetach$)return;let e=this.activeRunCompletionPromise??Promise.resolve();this.activeRunDetach$.next(),this.activeRunDetach$?.complete(),await e}apply(e,t,n){return I(e,t,this,n,this.debugLogger)}processApplyEvents(e,t,n){return t.pipe(ie(t=>{t.messages&&(this.messages=t.messages,n.forEach(t=>{t.onMessagesChanged?.({messages:this.messages,state:this.state,agent:this,input:e})})),t.state&&(this.state=t.state,n.forEach(t=>{t.onStateChanged?.({state:this.state,messages:this.messages,agent:this,input:e})}))}))}prepareRunAgentInput(t){let n=O(this.messages).filter(e=>e.role!==`activity`);return{threadId:this.threadId,runId:t?.runId||e(),tools:O(t?.tools??[]),context:O(t?.context??[]),forwardedProps:O(t?.forwardedProps??{}),state:O(this.state),messages:n}}async onInitialize(e,t){let n=await j(t,this.messages,this.state,(t,n,r)=>t.onRunInitialized?.({messages:n,state:r,agent:this,input:e}));(n.messages!==void 0||n.state!==void 0)&&(n.messages&&(this.messages=n.messages,e.messages=n.messages,t.forEach(t=>{t.onMessagesChanged?.({messages:this.messages,state:this.state,agent:this,input:e})})),n.state&&(this.state=n.state,e.state=n.state,t.forEach(t=>{t.onStateChanged?.({state:this.state,messages:this.messages,agent:this,input:e})})))}onError(e,t,n){return f(j(n,this.messages,this.state,(n,r,i)=>n.onRunFailed?.({error:t,messages:r,state:i,agent:this,input:e}))).pipe(x(r=>{let i=r;if((i.messages!==void 0||i.state!==void 0)&&(i.messages!==void 0&&(this.messages=i.messages,n.forEach(t=>{t.onMessagesChanged?.({messages:this.messages,state:this.state,agent:this,input:e})})),i.state!==void 0&&(this.state=i.state,n.forEach(t=>{t.onStateChanged?.({state:this.state,messages:this.messages,agent:this,input:e})}))),i.stopPropagation!==!0){let e=String(t);if(!(t.name===`AbortError`||t.message===`Fetch is aborted`||t.message===`signal is aborted without reason`||t.message===`component unmounted`||e===`component unmounted`))throw console.error(`Agent execution failed:`,t),t}return{}}))}async onFinalize(e,t){let n=await j(t,this.messages,this.state,(t,n,r)=>t.onRunFinalized?.({messages:n,state:r,agent:this,input:e}));(n.messages!==void 0||n.state!==void 0)&&(n.messages!==void 0&&(this.messages=n.messages,t.forEach(t=>{t.onMessagesChanged?.({messages:this.messages,state:this.state,agent:this,input:e})})),n.state!==void 0&&(this.state=n.state,t.forEach(t=>{t.onStateChanged?.({state:this.state,messages:this.messages,agent:this,input:e})})))}clone(){let e=Object.create(Object.getPrototypeOf(this));return e.agentId=this.agentId,e.description=this.description,e.threadId=this.threadId,e.messages=O(this.messages),e.state=O(this.state),e._debug=this._debug,e._debugLogger=this._debugLogger,e.isRunning=this.isRunning,e.subscribers=[...this.subscribers],e.middlewares=[...this.middlewares],e}addMessage(e){this.messages.push(e),(async()=>{for(let t of this.subscribers)await t.onNewMessage?.({message:e,messages:this.messages,state:this.state,agent:this});if(e.role===`assistant`&&e.toolCalls)for(let t of e.toolCalls)for(let e of this.subscribers)await e.onNewToolCall?.({toolCall:t,messages:this.messages,state:this.state,agent:this});for(let e of this.subscribers)await e.onMessagesChanged?.({messages:this.messages,state:this.state,agent:this})})()}addMessages(e){this.messages.push(...e),(async()=>{for(let t of e){for(let e of this.subscribers)await e.onNewMessage?.({message:t,messages:this.messages,state:this.state,agent:this});if(t.role===`assistant`&&t.toolCalls)for(let e of t.toolCalls)for(let t of this.subscribers)await t.onNewToolCall?.({toolCall:e,messages:this.messages,state:this.state,agent:this})}for(let e of this.subscribers)await e.onMessagesChanged?.({messages:this.messages,state:this.state,agent:this})})()}setMessages(e){this.messages=O(e),(async()=>{for(let e of this.subscribers)await e.onMessagesChanged?.({messages:this.messages,state:this.state,agent:this})})()}setState(e){this.state=O(e),(async()=>{for(let e of this.subscribers)await e.onStateChanged?.({messages:this.messages,state:this.state,agent:this})})()}legacy_to_be_removed_runAgentBridged(t){this.agentId=this.agentId??e();let n=this.prepareRunAgentInput(t);return(this.middlewares.length===0?this.run(n):this.middlewares.reduceRight((e,t)=>({run:n=>t.run(n,e),get messages(){return e.messages},get state(){return e.state}}),this).run(n)).pipe(G(this.debugLogger),L(this.debugLogger),W(this.threadId,n.runId,this.agentId),e=>e.pipe(x(e=>(this.debugLogger?.event(`LEGACY`,`Event:`,e,{type:e.type}),e))))}},we=class extends Ce{requestInit(e){return{method:`POST`,headers:{...this.headers,"Content-Type":`application/json`,Accept:`text/event-stream`},body:JSON.stringify(e),signal:this.abortController.signal}}runAgent(e,t){return this.abortController=e?.abortController??new AbortController,super.runAgent(e,t)}abortRun(){this.abortController.abort(),super.abortRun()}constructor(e){super(e),this.abortController=new AbortController,this.url=e.url,this.headers=O(e.headers??{})}run(e){return H(z(this.url,this.requestInit(e)),this.debugLogger)}clone(){let e=super.clone();e.url=this.url,e.headers=O(this.headers??{});let t=new AbortController,n=this.abortController.signal;return n.aborted&&t.abort(n.reason),e.abortController=t,e}};function Te(e){let t=[],n=new Map,r=new Map;for(let a of e)if(a.type===i.TEXT_MESSAGE_START){let e=a,t=e.messageId;n.has(t)||n.set(t,{contents:[],otherEvents:[]});let r=n.get(t);r.start=e}else if(a.type===i.TEXT_MESSAGE_CONTENT){let e=a,t=e.messageId;n.has(t)||n.set(t,{contents:[],otherEvents:[]}),n.get(t).contents.push(e)}else if(a.type===i.TEXT_MESSAGE_END){let e=a,r=e.messageId;n.has(r)||n.set(r,{contents:[],otherEvents:[]});let i=n.get(r);i.end=e,Ee(r,i,t),n.delete(r)}else if(a.type===i.TOOL_CALL_START){let e=a,t=e.toolCallId;r.has(t)||r.set(t,{args:[],otherEvents:[]});let n=r.get(t);n.start=e}else if(a.type===i.TOOL_CALL_ARGS){let e=a,t=e.toolCallId;r.has(t)||r.set(t,{args:[],otherEvents:[]}),r.get(t).args.push(e)}else if(a.type===i.TOOL_CALL_END){let e=a,n=e.toolCallId;r.has(n)||r.set(n,{args:[],otherEvents:[]});let i=r.get(n);i.end=e,De(n,i,t),r.delete(n)}else{let e=!1;for(let[t,r]of n)if(r.start&&!r.end){r.otherEvents.push(a),e=!0;break}if(!e){for(let[t,n]of r)if(n.start&&!n.end){n.otherEvents.push(a),e=!0;break}}e||t.push(a)}for(let[e,r]of n)Ee(e,r,t);for(let[e,n]of r)De(e,n,t);return t}function Ee(e,t,n){if(t.start&&n.push(t.start),t.contents.length>0){let r=t.contents.map(e=>e.delta).join(``),a={type:i.TEXT_MESSAGE_CONTENT,messageId:e,delta:r};n.push(a)}t.end&&n.push(t.end);for(let e of t.otherEvents)n.push(e)}function De(e,t,n){if(t.start&&n.push(t.start),t.args.length>0){let r=t.args.map(e=>e.delta).join(``),a={type:i.TOOL_CALL_ARGS,toolCallId:e,delta:r};n.push(a)}t.end&&n.push(t.end);for(let e of t.otherEvents)n.push(e)}export{Ce as AbstractAgent,J as BackwardCompatibility_0_0_39,xe as BackwardCompatibility_0_0_45,P as DebugLogger,ye as FilterToolCallsMiddleware,q as FunctionMiddleware,we as HttpAgent,K as Middleware,Te as compactEvents,W as convertToLegacyEvents,F as createDebugLogger,I as defaultApplyEvents,V as parseProtoStream,B as parseSSEStream,k as randomUUID,M as resolveAgentDebugConfig,N as resolveDebugLogger,z as runHttpRequest,O as structuredClone_,G as transformChunks,H as transformHttpEventStream,L as verifyEvents}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b159b96..27d6729 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,11 @@ settings: overrides: esbuild: ^0.27.3 +patchedDependencies: + '@ag-ui/client@0.0.52': + hash: dn7ifww43neslscynndgqgqway + path: patches/@ag-ui__client@0.0.52.patch + importers: .: @@ -22,10 +27,10 @@ importers: dependencies: '@ag-ui/a2ui-middleware': specifier: ^0.0.2 - version: 0.0.2(@ag-ui/client@0.0.52)(rxjs@7.8.1) + version: 0.0.2(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(rxjs@7.8.1) '@ag-ui/mcp-apps-middleware': specifier: ^0.0.3 - version: 0.0.3(@ag-ui/client@0.0.52)(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76) + version: 0.0.3(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76) '@copilotkit/a2ui-renderer': specifier: 1.55.2-next.1 version: 1.55.2-next.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -5995,14 +6000,14 @@ snapshots: '@adobe/css-tools@4.5.0': {} - '@ag-ui/a2ui-middleware@0.0.2(@ag-ui/client@0.0.52)(rxjs@7.8.1)': + '@ag-ui/a2ui-middleware@0.0.2(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(rxjs@7.8.1)': dependencies: - '@ag-ui/client': 0.0.52 + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) rxjs: 7.8.1 - '@ag-ui/a2ui-middleware@0.0.4(@ag-ui/client@0.0.52)(rxjs@7.8.1)': + '@ag-ui/a2ui-middleware@0.0.4(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(rxjs@7.8.1)': dependencies: - '@ag-ui/client': 0.0.52 + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) clarinet: 0.12.6 rxjs: 7.8.1 @@ -6019,7 +6024,7 @@ snapshots: uuid: 11.1.0 zod: 3.25.76 - '@ag-ui/client@0.0.52': + '@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway)': dependencies: '@ag-ui/core': 0.0.52 '@ag-ui/encoder': 0.0.52 @@ -6051,9 +6056,9 @@ snapshots: '@ag-ui/core': 0.0.52 '@ag-ui/proto': 0.0.52 - '@ag-ui/langgraph@0.0.27(@ag-ui/client@0.0.52)(@ag-ui/core@0.0.52)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0)(zod-to-json-schema@3.25.1(zod@3.25.76))': + '@ag-ui/langgraph@0.0.27(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(@ag-ui/core@0.0.52)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0)(zod-to-json-schema@3.25.1(zod@3.25.76))': dependencies: - '@ag-ui/client': 0.0.52 + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) '@ag-ui/core': 0.0.52 '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76)) '@langchain/langgraph-sdk': 0.1.10(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -6072,9 +6077,9 @@ snapshots: - ws - zod-to-json-schema - '@ag-ui/mcp-apps-middleware@0.0.3(@ag-ui/client@0.0.52)(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76)': + '@ag-ui/mcp-apps-middleware@0.0.3(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76)': dependencies: - '@ag-ui/client': 0.0.52 + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) '@modelcontextprotocol/sdk': 1.25.3(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76) rxjs: 7.8.1 transitivePeerDependencies: @@ -6346,7 +6351,7 @@ snapshots: '@copilotkit/core@1.55.2-next.1(@ag-ui/core@0.0.52)(zod@3.25.76)': dependencies: - '@ag-ui/client': 0.0.52 + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) '@copilotkit/shared': 1.55.2-next.1(@ag-ui/core@0.0.52) phoenix: 1.8.5 rxjs: 7.8.1 @@ -6360,7 +6365,7 @@ snapshots: '@copilotkit/react-core@1.55.2-next.1(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(graphql@16.12.0)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76)': dependencies: - '@ag-ui/client': 0.0.52 + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) '@ag-ui/core': 0.0.52 '@copilotkit/a2ui-renderer': 1.55.2-next.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@copilotkit/core': 1.55.2-next.1(@ag-ui/core@0.0.52)(zod@3.25.76) @@ -6438,12 +6443,12 @@ snapshots: '@copilotkit/runtime@1.55.2-next.1(@cfworker/json-schema@4.1.1)(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@langchain/langgraph-sdk@1.9.20(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@opentelemetry/api@1.9.0)(langchain@1.4.4(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0)(zod-to-json-schema@3.25.1(zod@3.25.76)))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod-to-json-schema@3.25.1(zod@3.25.76))': dependencies: - '@ag-ui/a2ui-middleware': 0.0.4(@ag-ui/client@0.0.52)(rxjs@7.8.1) - '@ag-ui/client': 0.0.52 + '@ag-ui/a2ui-middleware': 0.0.4(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(rxjs@7.8.1) + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) '@ag-ui/core': 0.0.52 '@ag-ui/encoder': 0.0.52 - '@ag-ui/langgraph': 0.0.27(@ag-ui/client@0.0.52)(@ag-ui/core@0.0.52)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0)(zod-to-json-schema@3.25.1(zod@3.25.76)) - '@ag-ui/mcp-apps-middleware': 0.0.3(@ag-ui/client@0.0.52)(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76) + '@ag-ui/langgraph': 0.0.27(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(@ag-ui/core@0.0.52)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(ws@8.19.0)(zod-to-json-schema@3.25.1(zod@3.25.76)) + '@ag-ui/mcp-apps-middleware': 0.0.3(@ag-ui/client@0.0.52(patch_hash=dn7ifww43neslscynndgqgqway))(@cfworker/json-schema@4.1.1)(hono@4.11.7)(zod@3.25.76) '@ai-sdk/anthropic': 3.0.58(zod@3.25.76) '@ai-sdk/google': 3.0.43(zod@3.25.76) '@ai-sdk/google-vertex': 3.0.141(zod@3.25.76) @@ -6499,7 +6504,7 @@ snapshots: '@copilotkit/shared@1.55.2-next.1(@ag-ui/core@0.0.52)': dependencies: - '@ag-ui/client': 0.0.52 + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) '@ag-ui/core': 0.0.52 '@copilotkit/license-verifier': 0.0.1-a1 '@segment/analytics-node': 2.3.0 @@ -6515,7 +6520,7 @@ snapshots: '@copilotkit/web-inspector@1.55.2-next.1(@ag-ui/core@0.0.52)(zod@3.25.76)': dependencies: - '@ag-ui/client': 0.0.52 + '@ag-ui/client': 0.0.52(patch_hash=dn7ifww43neslscynndgqgqway) '@copilotkit/core': 1.55.2-next.1(@ag-ui/core@0.0.52)(zod@3.25.76) lit: 3.3.2 lucide: 0.525.0 diff --git a/render.yaml b/render.yaml index 7d1a87f..b6c2235 100644 --- a/render.yaml +++ b/render.yaml @@ -23,12 +23,15 @@ services: sync: false - key: LANGSMITH_API_KEY sync: false - # chat-latest and gpt-* route via OpenAI, Claude names via Anthropic - # (see apps/agent/src/model.py) + # chat-latest and gpt-* route via OpenAI, Claude names via Anthropic. + # The local default is an Ollama model, which Render cannot reach, so the + # hosted demo pins OpenAI and hosted Jev (see apps/agent/src/model.py). - key: LLM_MODEL value: chat-latest - key: TYPESAFE_API_KEY sync: false + - key: JEV_URL + value: https://api.typesafe.ai/v1/systemone - key: JEV_MODEL value: jev-latest buildFilter: