Compare commits

..

9 Commits

Author SHA1 Message Date
Carlos Escalante
eb400ab1e9 Assistant: suppress repeated render-tool calls
All checks were successful
Deploy to VPS / test (push) Successful in 1m31s
Deploy to VPS / deploy (push) Successful in 2m1s
After the client returns a render tool's result, the model sometimes
re-issues the identical call with a fresh id on the follow-up run,
painting a second copy of the spending-summary card (observed live on
MAF 1.10 / CK 1.62). DeduplicateToolCallMiddleware now tracks which
render tools already ran in the current user turn and drops repeats —
their streamed TOOL_CALL_* events and their entries in the run's
MESSAGES_SNAPSHOT. The system prompt also tells the model the card is
already displayed once its "ok" result is present.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:10:51 -06:00
Carlos Escalante
bbcfaa7808 Assistant: hydrate the persisted thread in the UI
- Asistente.tsx pins threadId "main" on CopilotChat (an explicit stable
  id stops the per-mount message wipe), runs a hydration pass on page
  load — an empty-messages run makes MAF replay the stored snapshot as
  MESSAGES_SNAPSHOT with no LLM call — and sends the Inicio ask-box
  question only after hydration so every normal run carries the full
  client-known history. "Nueva conversación" clears the stored thread
  behind a ConfirmDialog.
- server.ts: the BFF no longer injects context into empty-messages
  requests (that would defeat MAF's hydration trigger), and the snapshot
  reconciliation passes hydration replays through untouched and treats
  ids the client already sent as history rather than duplicates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:10:35 -06:00
Carlos Escalante
6bce5539f7 Assistant: persist the chat thread in Postgres
Back MAF's opt-in AG-UI snapshot persistence with a DB store so the
conversation survives page reloads and restarts:

- ChatThreadSnapshot: one upserted row per (scope, thread_id), JSONB
  messages/state/interrupt (migration 794baf50635b)
- PostgresSnapshotStore implements the AGUIThreadSnapshotStore protocol.
  It opens its own sessions (MAF saves during SSE streaming, after the
  request middleware has closed the ContextVar session) and sanitizes on
  save: camelCase toolCalls/toolCallId (the @ag-ui/client Zod schema
  strips snake_case, which would lose cards on replay), drop BFF ctx-*
  context messages, dedupe MAF's re-recorded multi-run turns, and drop
  orphaned tool results
- Endpoint mount gains snapshot_store + a constant scope resolver
  (single-user app; auth already enforced by agent_auth_and_session)
- DELETE /api/v1/chat/thread resets the conversation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:09:55 -06:00
Carlos Escalante
0e03284c95 Assistant: restore ReconcileSnapshotMiddleware
Retiring it in the middleware audit was premature: MAF 1.10 reuses the
streamed id for plain-text snapshots but deliberately mints a fresh UUID
for post-tool-call assistant text (their #3619), so tool+text turns
still rendered a duplicate assistant bubble (re-verified live). The
user-message duplication that motivated the retirement only fired on
a2ui retry runs, which no longer exist now that a2ui is disabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:10:12 -06:00
Carlos Escalante
4687fa9669 Assistant: drop a2ui surfaces, fall back to markdown tables
CK 1.60+ validates generated a2ui ops against a component catalog before
painting, and our agent's ops fail validation — surfaces hang at
"Building interface" forever (MAF also drops RunAgentInput.context, so
the catalog/guidelines never even reach the model). Disable a2ui end to
end until the pipeline matures:

- server.ts: a2ui { enabled: false }; RENDER_TOOLS no longer includes
  render_a2ui
- App.tsx: remove the a2ui provider prop
- agent.py: prompt now asks for clean markdown tables for lists and
  structured data; render_spending_summary card guidance unchanged

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 16:09:56 -06:00
Carlos Escalante
bbae121fec Middleware audit for CK 1.62 / MAF 1.10: retire ReconcileSnapshot, inject dropped AG-UI context
- ReconcileSnapshotMiddleware removed: MAF now reuses the streamed
  message id in MESSAGES_SNAPSHOT (verified in _agent_run.py at
  python-1.10.0 and live event logs); keeping the surgery duplicated
  user messages on a2ui follow-up runs.
- beforeRequestMiddleware now injects RunAgentInput.context as a system
  message: MAF declares the field but never feeds it to the model,
  which starves the model of CopilotKit's A2UI catalog/guidelines.
  Remove when MAF consumes context upstream.
- Workaround chain env-gated: CK_MIDDLEWARES=off runs bare for audits,
  CK_DEBUG_EVENTS=1 enables the event logger.
- Kept: SuppressRenderToolText, StripModelArtifacts,
  DeduplicateToolCall, pairOrphanToolCalls (behavior-verified).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:55:29 -06:00
Carlos Escalante
033a920746 Upgrade CopilotKit 1.56.4 -> 1.62.2, @ag-ui/client 0.0.52 -> 0.0.57
No API drift: v2 hooks, v1 useCopilotAction interop, runtime endpoint
and a2ui config all verified against the v1.62.2 source. Brings the
1.60.2 chat scroll stability fixes and 1.57.2 thread-state fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:30:54 -06:00
Carlos Escalante
0d24c7f9be Upgrade MAF 1.2.1 -> 1.10.0 (ag-ui rc7), pin instrumentation off
Agent/OpenAIChatCompletionClient/add_agent_framework_fastapi_endpoint
APIs verified unchanged against the python-1.10.0 source; 115 tests
green, pip check clean. MAF 1.6.0 turns OTel on by default —
ENABLE_INSTRUMENTATION=false in both compose files keeps prod logs
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:24:54 -06:00
Carlos Escalante
00f059ee91 Fix chat scroll: cap the shell at the viewport on /asistente
The shell revamp left SidebarInset unbounded (wrapper is min-h-svh), so
Asistente's flex chain resolved to content height — the chat's internal
scroller never engaged and long answers hid behind the composer.
Asistente now gets h-svh + overflow-hidden on the inset; every other
page keeps body scroll. Verified: inset == viewport on /asistente,
body scroll intact elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 15:16:05 -06:00
19 changed files with 935 additions and 186 deletions

View File

@@ -0,0 +1,45 @@
"""chat thread snapshot
Revision ID: 794baf50635b
Revises: 961802a2f50d
Create Date: 2026-07-04 17:49:14.914103
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '794baf50635b'
down_revision: Union[str, Sequence[str], None] = '961802a2f50d'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('chatthreadsnapshot',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('scope', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('thread_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column('messages', sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), 'postgresql'), server_default='[]', nullable=False),
sa.Column('state', sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), 'postgresql'), nullable=True),
sa.Column('interrupt', sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), 'postgresql'), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('scope', 'thread_id')
)
op.create_index(op.f('ix_chatthreadsnapshot_scope'), 'chatthreadsnapshot', ['scope'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_chatthreadsnapshot_scope'), table_name='chatthreadsnapshot')
op.drop_table('chatthreadsnapshot')
# ### end Alembic commands ###

View File

@@ -34,7 +34,9 @@ How to answer:
(e.g. net worth + recent transactions). (e.g. net worth + recent transactions).
- Format currency with the appropriate symbol: ₡ for CRC (no decimals), $ for - Format currency with the appropriate symbol: ₡ for CRC (no decimals), $ for
USD (two decimals), € for EUR (two decimals). USD (two decimals), € for EUR (two decimals).
- When showing lists, prefer markdown tables over prose. - When showing lists or structured data (transactions, breakdowns, funds),
format them as clean markdown tables: right-align amounts, include a total
row when summing, keep merchant names as-is.
- If a tool returns no data, say so explicitly — do not fill in zeros. - If a tool returns no data, say so explicitly — do not fill in zeros.
- You are read-only in this version. If asked to create, edit or delete - You are read-only in this version. If asked to create, edit or delete
anything, explain that write actions aren't available yet and offer to anything, explain that write actions aren't available yet and offer to
@@ -47,17 +49,16 @@ Generative UI — render tools:
- When showing spending totals, cycle summaries, or category breakdowns → - When showing spending totals, cycle summaries, or category breakdowns →
call render_spending_summary. Source data: get_cycle_summary (by_source, call render_spending_summary. Source data: get_cycle_summary (by_source,
grand_total_crc) + get_analytics_by_category (by_category). grand_total_crc) + get_analytics_by_category (by_category).
- When showing transaction lists or other structured data → - Do NOT use markdown tables for data render_spending_summary can display.
call render_a2ui in a SEPARATE tool-call step, only after all data-fetching - CRITICAL RULE: When you call render_spending_summary, that tool call MUST
calls have returned. NEVER call render_a2ui in the same batch as any other be the ONLY content in your message. Do NOT include any text content
tool. alongside the tool call — no introduction, no list, no explanation,
- Do NOT use markdown tables for data a render tool can display. nothing. The rendered card IS the complete response. Any text you write in
- CRITICAL RULE: When you call a render tool (render_spending_summary or the same message as a render call will appear as a duplicate below the
render_a2ui), that tool call MUST be the ONLY content in your message. card, which is wrong.
Do NOT include any text content alongside the tool call — no introduction, - When a render_spending_summary tool result ("ok") is already present for
no list, no explanation, nothing. The rendered card IS the complete the current question, the card is already on screen. Do NOT call the tool
response. Any text you write in the same message as a render call will again and do NOT add text — end your response with no further output.
appear as a duplicate below the card, which is wrong.
""" """

View File

@@ -0,0 +1,185 @@
"""Postgres-backed AG-UI thread snapshot store.
Implements the AGUIThreadSnapshotStore protocol from agent-framework-ag-ui so
the assistant conversation survives page reloads: MAF calls `save` with the
full cumulative message list at each run end, and `get` on the next request
(server-side context rebuild, or a client hydration run with empty messages).
Two deliberate choices:
- Own sessions per call: MAF saves during SSE streaming, after the
agent_auth_and_session middleware has already closed the request-bound
ContextVar session. Sync SQLModel work runs in a thread so the event loop
keeps streaming.
- `save` normalizes assistant `tool_calls` -> `toolCalls`: @ag-ui/client's
Zod schema silently strips the snake_case field, so a replayed snapshot
would lose tool calls (and the spending-summary card) on the way to the
browser. MAF reads both casings back, so storing camelCase is safe.
"""
from __future__ import annotations
import asyncio
from typing import Any
from agent_framework_ag_ui import AGUIThreadSnapshot
from sqlmodel import Session, select
from app.db import engine
from app.models.models import ChatThreadSnapshot
from app.timeutil import utcnow
def _normalize_message(msg: dict[str, Any]) -> dict[str, Any]:
out = dict(msg)
tool_calls = out.pop("tool_calls", None)
if tool_calls is None:
tool_calls = out.pop("toolCalls", None)
else:
out.pop("toolCalls", None)
# MAF writes tool_calls: null on plain-text assistant messages; only a
# non-empty list is meaningful downstream.
if isinstance(tool_calls, list) and tool_calls:
out["toolCalls"] = tool_calls
if "tool_call_id" in out:
out.setdefault("toolCallId", out["tool_call_id"])
del out["tool_call_id"]
return out
def _clean_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Sanitize the cumulative snapshot before storing it.
MAF's history merge re-records the resent suffix of multi-run turns
(frontend-tool roundtrips) without deduplicating, and the model
occasionally re-issues an identical render tool call on the follow-up
run. Left as-is, the snapshot grows duplicates that replay as repeated
bubbles/cards after a reload.
"""
# Pass 1: drop BFF-injected ctx-* system messages, duplicate message
# ids, and repeated identical tool calls (same function + arguments)
# within one user turn.
seen_ids: set[str] = set()
turn_calls: set[tuple[Any, Any]] = set()
kept_call_ids: set[str] = set()
first_pass: list[dict[str, Any]] = []
for raw in messages:
m = _normalize_message(raw)
mid = str(m.get("id", ""))
if mid.startswith("ctx-"):
continue
if mid and mid in seen_ids:
continue
role = m.get("role")
if role == "user":
turn_calls = set()
if role == "assistant" and isinstance(m.get("toolCalls"), list):
kept: list[dict[str, Any]] = []
for tc in m["toolCalls"]:
fn = tc.get("function") or {}
key = (fn.get("name"), fn.get("arguments"))
if key in turn_calls:
continue
turn_calls.add(key)
kept.append(tc)
if tc.get("id"):
kept_call_ids.add(str(tc["id"]))
if kept:
m["toolCalls"] = kept
else:
m.pop("toolCalls", None)
if not m.get("content"):
continue # nothing left worth replaying
if mid:
seen_ids.add(mid)
first_pass.append(m)
# Pass 2: one tool result per surviving call id. Results whose call was
# deduplicated away must go too — an orphan tool message in the stored
# history would 400 the next OpenAI request when MAF rebuilds context.
answered: set[str] = set()
out: list[dict[str, Any]] = []
for m in first_pass:
if m.get("role") == "tool":
tcid = str(m.get("toolCallId") or "")
if tcid and (tcid not in kept_call_ids or tcid in answered):
continue
if tcid:
answered.add(tcid)
out.append(m)
return out
class PostgresSnapshotStore:
"""One upserted row per (scope, thread_id); latest snapshot only."""
async def save(
self, *, scope: str, thread_id: str, snapshot: AGUIThreadSnapshot
) -> None:
def _save() -> None:
with Session(engine) as session:
row = session.exec(
select(ChatThreadSnapshot).where(
ChatThreadSnapshot.scope == scope,
ChatThreadSnapshot.thread_id == thread_id,
)
).first()
if row is None:
row = ChatThreadSnapshot(scope=scope, thread_id=thread_id)
row.messages = _clean_messages(snapshot.messages)
row.state = snapshot.state
row.interrupt = snapshot.interrupt
row.updated_at = utcnow()
session.add(row)
session.commit()
await asyncio.to_thread(_save)
async def get(
self, *, scope: str, thread_id: str
) -> AGUIThreadSnapshot | None:
def _get() -> AGUIThreadSnapshot | None:
with Session(engine) as session:
row = session.exec(
select(ChatThreadSnapshot).where(
ChatThreadSnapshot.scope == scope,
ChatThreadSnapshot.thread_id == thread_id,
)
).first()
if row is None:
return None
return AGUIThreadSnapshot(
messages=row.messages or [],
state=row.state,
interrupt=row.interrupt,
)
return await asyncio.to_thread(_get)
async def delete(self, *, scope: str, thread_id: str) -> bool:
def _delete() -> bool:
with Session(engine) as session:
row = session.exec(
select(ChatThreadSnapshot).where(
ChatThreadSnapshot.scope == scope,
ChatThreadSnapshot.thread_id == thread_id,
)
).first()
if row is None:
return False
session.delete(row)
session.commit()
return True
return await asyncio.to_thread(_delete)
async def clear(self, *, scope: str | None = None) -> None:
def _clear() -> None:
with Session(engine) as session:
stmt = select(ChatThreadSnapshot)
if scope is not None:
stmt = stmt.where(ChatThreadSnapshot.scope == scope)
for row in session.exec(stmt).all():
session.delete(row)
session.commit()
await asyncio.to_thread(_clear)

View File

@@ -0,0 +1,41 @@
"""Assistant chat thread management.
The conversation itself is persisted by the AG-UI snapshot store
(app/agent/snapshot_store.py) under a single ("default", "main") thread;
this router only exposes the reset used by "Nueva conversación".
"""
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlmodel import Session, select
from app.auth import get_current_user
from app.db import get_session
from app.models.models import ChatThreadSnapshot
router = APIRouter(prefix="/chat", tags=["chat"])
CHAT_SCOPE = "default"
CHAT_THREAD_ID = "main"
class ClearThreadResponse(BaseModel):
cleared: bool
@router.delete("/thread", response_model=ClearThreadResponse)
def clear_thread(
session: Session = Depends(get_session),
_user: str = Depends(get_current_user),
):
row = session.exec(
select(ChatThreadSnapshot).where(
ChatThreadSnapshot.scope == CHAT_SCOPE,
ChatThreadSnapshot.thread_id == CHAT_THREAD_ID,
)
).first()
if row is None:
return {"cleared": False}
session.delete(row)
session.commit()
return {"cleared": True}

View File

@@ -6,6 +6,7 @@ from app.api.v1.endpoints import (
auth, auth,
budget, budget,
categories, categories,
chat,
exchange_rate, exchange_rate,
import_transactions, import_transactions,
installments, installments,
@@ -38,3 +39,4 @@ api_router.include_router(pensions.router)
api_router.include_router(municipal_receipts.router) api_router.include_router(municipal_receipts.router)
api_router.include_router(savings_accrual.router) api_router.include_router(savings_accrual.router)
api_router.include_router(sync_status.router) api_router.include_router(sync_status.router)
api_router.include_router(chat.router)

View File

@@ -11,6 +11,7 @@ from jose import JWTError, jwt
from pydantic import BaseModel from pydantic import BaseModel
from app.agent.agent import build_agent from app.agent.agent import build_agent
from app.agent.snapshot_store import PostgresSnapshotStore
from app.agent.tools import reset_session, set_session from app.agent.tools import reset_session, set_session
from app.api.v1.router import api_router from app.api.v1.router import api_router
from app.auth import ( from app.auth import (
@@ -148,8 +149,17 @@ async def agent_auth_and_session(request: Request, call_next):
# Register app routes # Register app routes
app.include_router(api_router) app.include_router(api_router)
# Mount the AG-UI agent endpoint. # Mount the AG-UI agent endpoint. The snapshot store persists the chat
add_agent_framework_fastapi_endpoint(app, build_agent(), AGENT_PATH) # thread across page reloads; scope is a constant because this is a
# single-user app and auth is already enforced by agent_auth_and_session
# (the scope resolver only sees the AGUIRequest, which carries no identity).
add_agent_framework_fastapi_endpoint(
app,
build_agent(),
AGENT_PATH,
snapshot_store=PostgresSnapshotStore(),
snapshot_scope_resolver=lambda _request: "default",
)
@app.get("/") @app.get("/")

View File

@@ -537,3 +537,32 @@ class WaterMeterReading(WaterMeterReadingBase, table=True):
class WaterMeterReadingRead(WaterMeterReadingBase): class WaterMeterReadingRead(WaterMeterReadingBase):
id: int id: int
created_at: datetime created_at: datetime
# --- Assistant Chat Thread ---
class ChatThreadSnapshot(SQLModel, table=True):
"""Latest AG-UI thread snapshot per (scope, thread_id).
Written by app/agent/snapshot_store.py: MAF overwrites the full
cumulative message list at each run end — one row per thread, not an
append log.
"""
__table_args__ = (UniqueConstraint("scope", "thread_id"),)
id: Optional[int] = Field(default=None, primary_key=True)
scope: str = Field(index=True)
thread_id: str
messages: list = Field(
default_factory=list,
sa_column=Column(JSON_OR_JSONB, nullable=False, server_default="[]"),
)
state: Optional[dict] = Field(
default=None, sa_column=Column(JSON_OR_JSONB, nullable=True)
)
interrupt: Optional[list] = Field(
default=None, sa_column=Column(JSON_OR_JSONB, nullable=True)
)
updated_at: datetime = Field(default_factory=utcnow)

View File

@@ -1,8 +1,8 @@
a2a-sdk==0.3.23 a2a-sdk==0.3.23
ag-ui-protocol==0.1.19 ag-ui-protocol==0.1.19
agent-framework==1.2.1 agent-framework==1.10.0
agent-framework-a2a==1.0.0b260428 agent-framework-a2a==1.0.0b260428
agent-framework-ag-ui==1.0.0b260428 agent-framework-ag-ui==1.0.0rc7
agent-framework-anthropic==1.0.0b260428 agent-framework-anthropic==1.0.0b260428
agent-framework-azure-ai-search==1.0.0b260428 agent-framework-azure-ai-search==1.0.0b260428
agent-framework-azure-cosmos==1.0.0b260428 agent-framework-azure-cosmos==1.0.0b260428
@@ -11,7 +11,7 @@ agent-framework-bedrock==1.0.0b260428
agent-framework-chatkit==1.0.0b260428 agent-framework-chatkit==1.0.0b260428
agent-framework-claude==1.0.0b260428 agent-framework-claude==1.0.0b260428
agent-framework-copilotstudio==1.0.0b260428 agent-framework-copilotstudio==1.0.0b260428
agent-framework-core==1.2.1 agent-framework-core==1.10.0
agent-framework-declarative==1.0.0b260428 agent-framework-declarative==1.0.0b260428
agent-framework-devui==1.0.0b260428 agent-framework-devui==1.0.0b260428
agent-framework-durabletask==1.0.0b260428 agent-framework-durabletask==1.0.0b260428
@@ -21,7 +21,7 @@ agent-framework-github-copilot==1.0.0b260402
agent-framework-lab==1.0.0b251024 agent-framework-lab==1.0.0b251024
agent-framework-mem0==1.0.0b260428 agent-framework-mem0==1.0.0b260428
agent-framework-ollama==1.0.0b260428 agent-framework-ollama==1.0.0b260428
agent-framework-openai==1.2.1 agent-framework-openai==1.10.0
agent-framework-orchestrations==1.0.0b260428 agent-framework-orchestrations==1.0.0b260428
agent-framework-purview==1.0.0b260428 agent-framework-purview==1.0.0b260428
agent-framework-redis==1.0.0b260428 agent-framework-redis==1.0.0b260428

View File

@@ -11,6 +11,6 @@ httpx
pywebpush pywebpush
py-vapid py-vapid
python-dateutil python-dateutil
agent-framework==1.2.1 agent-framework==1.10.0
agent-framework-ag-ui==1.0.0b260428 agent-framework-ag-ui==1.0.0rc7
agent-framework-openai==1.2.1 agent-framework-openai==1.10.0

View File

@@ -0,0 +1,131 @@
"""Chat thread persistence: snapshot store round-trip + clear endpoint."""
import asyncio
from agent_framework_ag_ui import AGUIThreadSnapshot
from sqlmodel import select
import app.agent.snapshot_store as snapshot_store_module
from app.agent.snapshot_store import PostgresSnapshotStore
from app.api.v1.endpoints.chat import clear_thread
from app.models.models import ChatThreadSnapshot
def _store(monkeypatch, engine):
# The store opens its own sessions from app.db.engine; point it at the
# test engine instead.
monkeypatch.setattr(snapshot_store_module, "engine", engine)
return PostgresSnapshotStore()
def test_save_get_roundtrip_normalizes_and_filters(monkeypatch, engine):
store = _store(monkeypatch, engine)
snapshot = AGUIThreadSnapshot(
messages=[
{"id": "u1", "role": "user", "content": "hola"},
{
"id": "a1",
"role": "assistant",
"content": "",
# MAF emits snake_case; the store must return camelCase so
# @ag-ui/client's Zod schema doesn't strip it.
"tool_calls": [{"id": "t1", "type": "function"}],
},
{"id": "t1r", "role": "tool", "content": "ok", "tool_call_id": "t1"},
# MAF writes tool_calls: null on plain-text assistant messages —
# must not survive as toolCalls: null in the replay.
{"id": "a2", "role": "assistant", "content": "done", "tool_calls": None},
{"id": "ctx-123", "role": "system", "content": "run context"},
],
state={"k": "v"},
)
asyncio.run(store.save(scope="default", thread_id="main", snapshot=snapshot))
loaded = asyncio.run(store.get(scope="default", thread_id="main"))
assert loaded is not None
assert [m["id"] for m in loaded.messages] == ["u1", "a1", "t1r", "a2"]
assistant = loaded.messages[1]
assert "tool_calls" not in assistant
assert assistant["toolCalls"] == [{"id": "t1", "type": "function"}]
tool_msg = loaded.messages[2]
assert "tool_call_id" not in tool_msg
assert tool_msg["toolCallId"] == "t1"
text_msg = loaded.messages[3]
assert "toolCalls" not in text_msg and "tool_calls" not in text_msg
assert loaded.state == {"k": "v"}
def test_save_upserts_single_row(monkeypatch, engine, session):
store = _store(monkeypatch, engine)
for n in range(3):
snap = AGUIThreadSnapshot(
messages=[{"id": f"u{i}", "role": "user", "content": str(i)} for i in range(n + 1)]
)
asyncio.run(store.save(scope="default", thread_id="main", snapshot=snap))
rows = session.exec(select(ChatThreadSnapshot)).all()
assert len(rows) == 1
assert len(rows[0].messages) == 3
def test_get_unknown_thread_returns_none(monkeypatch, engine):
store = _store(monkeypatch, engine)
assert asyncio.run(store.get(scope="default", thread_id="nope")) is None
def test_delete_and_scope_isolation(monkeypatch, engine):
store = _store(monkeypatch, engine)
snap = AGUIThreadSnapshot(messages=[{"id": "u1", "role": "user", "content": "x"}])
asyncio.run(store.save(scope="default", thread_id="main", snapshot=snap))
assert asyncio.run(store.get(scope="other", thread_id="main")) is None
assert asyncio.run(store.delete(scope="default", thread_id="main")) is True
assert asyncio.run(store.delete(scope="default", thread_id="main")) is False
assert asyncio.run(store.get(scope="default", thread_id="main")) is None
def test_save_dedupes_multi_run_turn(monkeypatch, engine):
"""Mirror of MAF's duplicate merge on frontend-tool roundtrips: repeated
message ids, a re-issued identical render call, and extra tool results."""
store = _store(monkeypatch, engine)
def call(cid, name, args='{"a":1}'):
return {"id": cid, "type": "function", "function": {"name": name, "arguments": args}}
snapshot = AGUIThreadSnapshot(
messages=[
{"id": "u1", "role": "user", "content": "resumen"},
{"id": "a1", "role": "assistant", "content": "",
"tool_calls": [call("c1", "get_cycle_summary"), call("c2", "render_spending_summary")]},
{"id": "t1", "role": "tool", "content": "data", "tool_call_id": "c1"},
# MAF re-records the resent suffix verbatim…
{"id": "u1", "role": "user", "content": "resumen"},
{"id": "t1", "role": "tool", "content": "data", "tool_call_id": "c1"},
{"id": "t2", "role": "tool", "content": "ok", "tool_call_id": "c2"},
# …plus a synthetic second result for an answered call…
{"id": "t3", "role": "tool", "content": "", "tool_call_id": "c1"},
# …and the model re-issues the same render call with a new id.
{"id": "a2", "role": "assistant", "content": "",
"tool_calls": [call("c3", "render_spending_summary")]},
{"id": "t4", "role": "tool", "content": "ok", "tool_call_id": "c3"},
]
)
asyncio.run(store.save(scope="default", thread_id="main", snapshot=snapshot))
loaded = asyncio.run(store.get(scope="default", thread_id="main"))
assert [m["id"] for m in loaded.messages] == ["u1", "a1", "t1", "t2"]
# The duplicate render call (c3) and its result are gone; each surviving
# call has exactly one result.
assert [tc["id"] for tc in loaded.messages[1]["toolCalls"]] == ["c1", "c2"]
assert [(m["toolCallId"]) for m in loaded.messages if m["role"] == "tool"] == ["c1", "c2"]
def test_clear_thread_endpoint(monkeypatch, engine, session):
store = _store(monkeypatch, engine)
assert clear_thread(session=session, _user="testadmin") == {"cleared": False}
snap = AGUIThreadSnapshot(messages=[{"id": "u1", "role": "user", "content": "x"}])
asyncio.run(store.save(scope="default", thread_id="main", snapshot=snap))
assert clear_thread(session=session, _user="testadmin") == {"cleared": True}
assert asyncio.run(store.get(scope="default", thread_id="main")) is None

View File

@@ -32,6 +32,8 @@ services:
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY} VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY}
OPENAI_API_KEY: ${OPENAI_API_KEY} OPENAI_API_KEY: ${OPENAI_API_KEY}
AGENT_MODEL: ${AGENT_MODEL:-gpt-5.4-mini} AGENT_MODEL: ${AGENT_MODEL:-gpt-5.4-mini}
# MAF 1.6+ enables OTel instrumentation by default; keep it off explicitly.
ENABLE_INSTRUMENTATION: "false"
expose: expose:
- "8000" - "8000"
networks: networks:

View File

@@ -31,6 +31,8 @@ services:
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-} VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-} OPENAI_API_KEY: ${OPENAI_API_KEY:-}
AGENT_MODEL: ${AGENT_MODEL:-gpt-5.4-mini} AGENT_MODEL: ${AGENT_MODEL:-gpt-5.4-mini}
# MAF 1.6+ enables OTel instrumentation by default; keep it off explicitly.
ENABLE_INSTRUMENTATION: "false"
ports: ports:
- "8001:8000" - "8001:8000"
volumes: volumes:

View File

@@ -12,11 +12,11 @@
"gen:api": "openapi-typescript openapi.json -o src/lib/api-types.gen.ts" "gen:api": "openapi-typescript openapi.json -o src/lib/api-types.gen.ts"
}, },
"dependencies": { "dependencies": {
"@ag-ui/client": "0.0.52", "@ag-ui/client": "0.0.57",
"@base-ui/react": "^1.4.1", "@base-ui/react": "^1.4.1",
"@copilotkit/react-core": "1.56.4", "@copilotkit/react-core": "1.62.2",
"@copilotkit/react-ui": "1.56.4", "@copilotkit/react-ui": "1.62.2",
"@copilotkit/runtime": "1.56.4", "@copilotkit/runtime": "1.62.2",
"@fontsource-variable/ibm-plex-sans": "^5.2.8", "@fontsource-variable/ibm-plex-sans": "^5.2.8",
"@fontsource-variable/noto-sans": "^5.2.10", "@fontsource-variable/noto-sans": "^5.2.10",
"@hono/node-server": "^1.14.4", "@hono/node-server": "^1.14.4",

297
frontend/pnpm-lock.yaml generated
View File

@@ -9,20 +9,20 @@ importers:
.: .:
dependencies: dependencies:
'@ag-ui/client': '@ag-ui/client':
specifier: 0.0.52 specifier: 0.0.57
version: 0.0.52 version: 0.0.57
'@base-ui/react': '@base-ui/react':
specifier: ^1.4.1 specifier: ^1.4.1
version: 1.4.1(@date-fns/tz@1.5.0)(@types/react@19.2.14)(date-fns@4.4.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) version: 1.4.1(@date-fns/tz@1.5.0)(@types/react@19.2.14)(date-fns@4.4.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@copilotkit/react-core': '@copilotkit/react-core':
specifier: 1.56.4 specifier: 1.62.2
version: 1.56.4(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6) version: 1.62.2(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6)
'@copilotkit/react-ui': '@copilotkit/react-ui':
specifier: 1.56.4 specifier: 1.62.2
version: 1.56.4(@ag-ui/core@0.0.52)(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6) version: 1.62.2(@ag-ui/core@0.0.57)(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6)
'@copilotkit/runtime': '@copilotkit/runtime':
specifier: 1.56.4 specifier: 1.62.2
version: 1.56.4(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@langchain/langgraph-sdk@1.8.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@opentelemetry/api@1.9.0)(langchain@1.3.5(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6)))(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6)) version: 1.62.2(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@langchain/langgraph-sdk@1.8.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@opentelemetry/api@1.9.0)(langchain@1.3.5(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6)))(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))
'@fontsource-variable/ibm-plex-sans': '@fontsource-variable/ibm-plex-sans':
specifier: ^5.2.8 specifier: ^5.2.8
version: 5.2.8 version: 5.2.8
@@ -131,23 +131,35 @@ packages:
'@a2ui/web_core@0.9.0': '@a2ui/web_core@0.9.0':
resolution: {integrity: sha512-TsMWuEeuVDsScGIGPy/fWIZu+EOBRfhx6KwjKh3VwY1AwysRenQM8zDr8VrSk14Wck/aBgVxk2zWVrMCK2/s6A==} resolution: {integrity: sha512-TsMWuEeuVDsScGIGPy/fWIZu+EOBRfhx6KwjKh3VwY1AwysRenQM8zDr8VrSk14Wck/aBgVxk2zWVrMCK2/s6A==}
'@ag-ui/a2ui-middleware@0.0.5': '@ag-ui/a2ui-middleware@0.0.10':
resolution: {integrity: sha512-nhF8LBzq36BmLR1O3nStxqSpIu1+4//ttqOyQgiXUsKZK1znCyN4q4MBTARrkFa7CuFr3fcNTiZuS4V1HRUHTw==} resolution: {integrity: sha512-2BQFUQ9vJzUAQSR0dNW/ijhyH8KpiRWISSLTP6mIe6ENyQ2cM1/XLG38/Dcb69olcs36gtZADZzAQWcno5H6fA==}
peerDependencies: peerDependencies:
'@ag-ui/client': '>=0.0.40' '@ag-ui/client': '>=0.0.40'
rxjs: 7.8.1 rxjs: 7.8.1
'@ag-ui/client@0.0.52': '@ag-ui/a2ui-toolkit@0.0.4':
resolution: {integrity: sha512-U407VvDDwR5qs8TiyN1qY38x87qMWc2n0epw8iA5aa1qwzCKBBDgg3Fkm4JogQf0X4jwNsz8HUbIZrBB56mrpg==} resolution: {integrity: sha512-9VPmgpCAsFVICk7z23kh+Kp/BwYMxw0D0KGjOiKXhm1MAH+Wid2iC3yKsXso+q7UZZiyhWgeDUSgaAkltyLmGg==}
'@ag-ui/core@0.0.52': '@ag-ui/client@0.0.54':
resolution: {integrity: sha512-Xo0bUaNV56EqylzcrAuhUkQX7et7+SZIrqZZtEByGwEq/I1EHny6ZMkWHLkKR7UNi0FJZwJyhKYmKJS3B2SEgA==} resolution: {integrity: sha512-N5UVXEBV5gPHqTuMoR/21brconRn42URf+MB4L8OniCJKqLcl/qUJb5kMamK0nnfBhDfPs/uq7LxDn6bsDJzJg==}
'@ag-ui/encoder@0.0.52': '@ag-ui/client@0.0.57':
resolution: {integrity: sha512-6GVDTb1dv2rjap7VVnmXYypDutZi6nrsTcdfxoP6ryDG5ynlXtmmS+FSDAt62JbIMD5CtEE963xNCb6d1iXw9g==} resolution: {integrity: sha512-Xap2alG9Z0/j5kb3x4D7oTpe2sw1dfrC9rgJJr2NZu5vKcm8dzIPNd31mF2B4zS3BKqYIu245yxKPhEtT30MHw==}
'@ag-ui/langgraph@0.0.29': '@ag-ui/core@0.0.54':
resolution: {integrity: sha512-7Q4YMtuI53N3uyZbEhYsIpfwYK1l5mWX88nm7BE242lIpPsiLIj3RL8xpT8lojqmR9MyZ72jlHIhXm1hbsrZjg==} resolution: {integrity: sha512-Ilx31OvRQaZfU7jSArGqz06JZKOsAt8zWiCPJljyp9zR6Tzl18oyfx8o6FsuGfAktGRe50GI9SCCxNXXysZwtA==}
'@ag-ui/core@0.0.57':
resolution: {integrity: sha512-gho1OWjNE6E3Rl7ZEZ1wr2CEpUHjLFU0FqzCZZk439TicLu+BfLCMkMokB07bMGlRmbJ60hM6LW60iOVauCx+Q==}
'@ag-ui/encoder@0.0.54':
resolution: {integrity: sha512-0dPuE/eAeBRBDj/OOj5AW8SoP1r0dufmoOdrtKgmf+dlbVXKSNkDDHGrrvIWFPxwvPTWhHeN6wnsVUayWpUsGg==}
'@ag-ui/encoder@0.0.57':
resolution: {integrity: sha512-ifD9NctR4xyPDR58xF9GK1bj/S8oECFkTeDfuYD8tXdbcOstIJ2TOqU2zhiCKnw7Vw+zR9Qv3TbsM9E7Gi9X3Q==}
'@ag-ui/langgraph@0.0.42':
resolution: {integrity: sha512-dXasEbGQFJcasdoy5khYyDHZHYoD1/i7hioaP8cejfw+Dss4tLvN8ndzD6c5j0hmANaKscekl+zkF7UQq3sI9w==}
peerDependencies: peerDependencies:
'@ag-ui/client': '>=0.0.42' '@ag-ui/client': '>=0.0.42'
'@ag-ui/core': '>=0.0.42' '@ag-ui/core': '>=0.0.42'
@@ -157,8 +169,16 @@ packages:
peerDependencies: peerDependencies:
'@ag-ui/client': '>=0.0.40' '@ag-ui/client': '>=0.0.40'
'@ag-ui/proto@0.0.52': '@ag-ui/mcp-middleware@0.0.1':
resolution: {integrity: sha512-+iCGzNUNL50YIoThVmsolWPjG4MJidl+R9k8QAGVwErEfHRtQ64KFyrdpeOXNVuWtM3SViJqPSgFyv7eGVS63A==} resolution: {integrity: sha512-TayUu7kB+jXUTPRUJesNvJYrP+0weTL9F2VJJ8QQ4sWxY/Ihjo+GgFYgJZYNcLwbo1DKgmVJtdm2XUouPCbxeg==}
peerDependencies:
rxjs: 7.8.1
'@ag-ui/proto@0.0.54':
resolution: {integrity: sha512-IPF+xeFaBAKKP2FO74MaVTkKUP8VaGGkbPzORCvC5TLDdGs+oQgQFqz+XoBeksQGE14+jgLWiAr9EPXdhqr1NA==}
'@ag-ui/proto@0.0.57':
resolution: {integrity: sha512-pPENOZt0P6ibH8sCTgq05wLYXi5t3P9B5r/1bWYehXjUxtyOdnukSlWM++SsCIwUXsQdm/b3aBgGjEeTF7RenA==}
'@ai-sdk/anthropic@2.0.77': '@ai-sdk/anthropic@2.0.77':
resolution: {integrity: sha512-8n7ApEzFOxqVvT3HyqLrEQlgUx/2nUmPFLTGY3fNKwUA8KVNU3Ovd2C66Qh1Y93Iq5NkHsOWuLiTyAZpRKQhgw==} resolution: {integrity: sha512-8n7ApEzFOxqVvT3HyqLrEQlgUx/2nUmPFLTGY3fNKwUA8KVNU3Ovd2C66Qh1Y93Iq5NkHsOWuLiTyAZpRKQhgw==}
@@ -300,38 +320,43 @@ packages:
'@chevrotain/utils@12.0.0': '@chevrotain/utils@12.0.0':
resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==}
'@copilotkit/a2ui-renderer@1.56.4': '@copilotkit/a2ui-renderer@1.62.2':
resolution: {integrity: sha512-eYikRML0l8p9zrNYm6SW5YO3PygmQPM8QcBcvq7kbRrvlJR7LP1qT+sbkzK8dUEU7k5WrE63BEOgLRNVscn60w==} resolution: {integrity: sha512-9V7s3aSFGBWS65EXdP04Fg597x9g1WpwoBT6mHW2Ev2uvIzo84KhObw2jWpNsaecAheesP70xS7752FB4GExxw==}
peerDependencies: peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
'@copilotkit/core@1.56.4': '@copilotkit/core@1.62.2':
resolution: {integrity: sha512-fCOhaxmNTgeCJRs1IDOmKoMXcAOJmTlWzYPum/vi6xuXM7M6+3OAtlDAed/47FVAxqny72SWCynBudH8hKwBQg==} resolution: {integrity: sha512-I3fCb6nZVtRi8+HuJULD0RzgTv6ZxkNZtrz+Ewd1lwhLUmw0gBsmsv1HitsM6opayuAZj+4mVrhwhgKJyZwSQQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@copilotkit/license-verifier@0.2.0': '@copilotkit/license-verifier@0.5.0':
resolution: {integrity: sha512-hliCifqy5a65YTozgRckuQmvBEQlt4L2PhbpSDY6fb/TKqPHyNDJgItRSnOpxOVDqvEfHEbUUqw3NaD88ZtdJA==} resolution: {integrity: sha512-vrwKtIpYwF0FT9ZoYASH8owa2cGV0dhDvJGaCRaRMStwDxpc6DRdydKkhx8cWZXyBRxEYcq/Vygv4JvevhQQdQ==}
'@copilotkit/react-core@1.56.4': '@copilotkit/react-core@1.62.2':
resolution: {integrity: sha512-HObN7YSX3KaCN+Dg0WzXHBylayI3WWQKCFB/DMsGajWefd2+Y3I33gs+hCKQFDcD/l8IWpR1/XvmGfzo6kvxdQ==} resolution: {integrity: sha512-tw3/PMzP4NEEdkBrK3PVJbkbh1ztnun9WdBHObLyU/Ou2d/37Rm3w5cnt17WVIxTa1FLtMoSdp3kC26656RAbg==}
peerDependencies: peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc
zod: '>=3.0.0' zod: '>=3.0.0'
'@copilotkit/react-ui@1.56.4': '@copilotkit/react-ui@1.62.2':
resolution: {integrity: sha512-pl2LZSYFOo6DjEiJDX/IstGqgvPn/xfLVzKkKyWuiJqxs69jynQIziCdUblbpV1d47hEqJOGgiAc7z7ltATLMA==} resolution: {integrity: sha512-lFbf/7GiSpu3WFk0+Fbd6v4eqNx3n2B3vYZle2hF+7YM1c/jtlll6wM4JlB0CNtxFvAbos5sNifyuMiliF6bbg==}
peerDependencies: peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
'@copilotkit/runtime-client-gql@1.56.4': '@copilotkit/runtime-client-gql@1.62.2':
resolution: {integrity: sha512-XHE45+Gz5vlMNuTt8noF/0Pc22GqAO68oU4O/J2ktKjI25WUBTvakbS2cgXELhxJszum6JeFjMyMvQpurXQukg==} resolution: {integrity: sha512-ZDPsMAFk6BFd0tfDJd7kbmxmMErdulcWLWaoVhWLhoBunNcKaq/V487vel3sOy5IkQPM2xia5Vh9GJeirMTo+w==}
peerDependencies: peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
'@copilotkit/runtime@1.56.4': '@copilotkit/runtime@1.62.2':
resolution: {integrity: sha512-WLupYn1K3hK7xkBY4MdM2lIhoyrGf+3K68fTkRUVc7fa5gADI0WjbRtBJnNwSUPFOBpVTTPAokqFl2+GdC1oEw==} resolution: {integrity: sha512-sZg38j0MGYlIAMjdUBvkSPoOGfucV3pSHG3wne783l9LyQMrOjLpDoT1DKixdybJg+6sSv84iEJlsQhR7UXiEA==}
peerDependencies: peerDependencies:
'@anthropic-ai/sdk': ^0.57.0 '@anthropic-ai/sdk': ^0.57.0
'@langchain/aws': '>=0.1.9' '@langchain/aws': '>=0.1.9'
@@ -363,13 +388,19 @@ packages:
openai: openai:
optional: true optional: true
'@copilotkit/shared@1.56.4': '@copilotkit/shared@1.62.2':
resolution: {integrity: sha512-sHmjRyZkmbHqhbK0OGT66gtCHAMA2AIWtNEFb2nwStzVu64a5dNhaayzpu7VbLpfkzeNzeWCPmZecdrN/EK1wQ==} resolution: {integrity: sha512-MLSvQIq7q8Z3FbQaqcSYR2i5sDvKIconidCXLbmZYIwdfbjnjPFjHzJM8PU9WTxJsYwQ3iM6/EysEm+5+bV4yQ==}
peerDependencies: peerDependencies:
'@ag-ui/core': '>=0.0.48' '@ag-ui/core': '>=0.0.48'
'@copilotkit/web-inspector@1.56.4': '@copilotkit/web-components@1.62.2':
resolution: {integrity: sha512-0PS1AnrTHbmvctveYuRrMr+CQ1FqiRQFhUBcDhSmz2eINr00dOybqIr8xXW78LD6E/6kyuLpTfaGHQLtBftMNw==} resolution: {integrity: sha512-JHqIakneTlGXR9X0TdgmfbttsdT6ztifJvDM642YrTDR5+nv5DLJoAX0V36XxH3CsbpIMU/GrTS9YyPs1bJgrg==}
engines: {node: '>=18'}
peerDependencies:
lit: ^3.3.2
'@copilotkit/web-inspector@1.62.2':
resolution: {integrity: sha512-OUYmtN1VrsgykYGMFnGgvBZy2HuaOi/gCjWn6yZiSCkhOOHfSRk0kvRe0605RDObSamspVIPWfVNGimPe56WcQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@date-fns/tz@1.5.0': '@date-fns/tz@1.5.0':
@@ -1103,15 +1134,6 @@ packages:
'@types/react': '@types/react':
optional: true optional: true
'@radix-ui/react-slot@1.2.4':
resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-slot@1.3.0': '@radix-ui/react-slot@1.3.0':
resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==}
peerDependencies: peerDependencies:
@@ -1729,6 +1751,7 @@ packages:
'@ungap/structured-clone@1.3.0': '@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@upsetjs/venn.js@2.0.0': '@upsetjs/venn.js@2.0.0':
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
@@ -4152,6 +4175,7 @@ packages:
uuid@10.0.0: uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true hasBin: true
uuid@11.1.0: uuid@11.1.0:
@@ -4303,17 +4327,20 @@ snapshots:
zod: 3.25.76 zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76) zod-to-json-schema: 3.25.2(zod@3.25.76)
'@ag-ui/a2ui-middleware@0.0.5(@ag-ui/client@0.0.52)(rxjs@7.8.1)': '@ag-ui/a2ui-middleware@0.0.10(@ag-ui/client@0.0.57)(rxjs@7.8.1)':
dependencies: dependencies:
'@ag-ui/client': 0.0.52 '@ag-ui/a2ui-toolkit': 0.0.4
'@ag-ui/client': 0.0.57
clarinet: 0.12.6 clarinet: 0.12.6
rxjs: 7.8.1 rxjs: 7.8.1
'@ag-ui/client@0.0.52': '@ag-ui/a2ui-toolkit@0.0.4': {}
'@ag-ui/client@0.0.54':
dependencies: dependencies:
'@ag-ui/core': 0.0.52 '@ag-ui/core': 0.0.54
'@ag-ui/encoder': 0.0.52 '@ag-ui/encoder': 0.0.54
'@ag-ui/proto': 0.0.52 '@ag-ui/proto': 0.0.54
'@types/uuid': 10.0.0 '@types/uuid': 10.0.0
compare-versions: 6.1.1 compare-versions: 6.1.1
fast-json-patch: 3.1.1 fast-json-patch: 3.1.1
@@ -4322,19 +4349,42 @@ snapshots:
uuid: 11.1.0 uuid: 11.1.0
zod: 3.25.76 zod: 3.25.76
'@ag-ui/core@0.0.52': '@ag-ui/client@0.0.57':
dependencies:
'@ag-ui/core': 0.0.57
'@ag-ui/encoder': 0.0.57
'@ag-ui/proto': 0.0.57
'@types/uuid': 10.0.0
compare-versions: 6.1.1
fast-json-patch: 3.1.1
rxjs: 7.8.1
untruncate-json: 0.0.1
uuid: 11.1.0
zod: 3.25.76
'@ag-ui/core@0.0.54':
dependencies: dependencies:
zod: 3.25.76 zod: 3.25.76
'@ag-ui/encoder@0.0.52': '@ag-ui/core@0.0.57':
dependencies: dependencies:
'@ag-ui/core': 0.0.52 zod: 3.25.76
'@ag-ui/proto': 0.0.52
'@ag-ui/langgraph@0.0.29(@ag-ui/client@0.0.52)(@ag-ui/core@0.0.52)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6))': '@ag-ui/encoder@0.0.54':
dependencies: dependencies:
'@ag-ui/client': 0.0.52 '@ag-ui/core': 0.0.54
'@ag-ui/core': 0.0.52 '@ag-ui/proto': 0.0.54
'@ag-ui/encoder@0.0.57':
dependencies:
'@ag-ui/core': 0.0.57
'@ag-ui/proto': 0.0.57
'@ag-ui/langgraph@0.0.42(@ag-ui/client@0.0.57)(@ag-ui/core@0.0.57)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6))':
dependencies:
'@ag-ui/a2ui-toolkit': 0.0.4
'@ag-ui/client': 0.0.57
'@ag-ui/core': 0.0.57
'@langchain/core': 1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) '@langchain/core': 1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)
'@langchain/langgraph-sdk': 1.8.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@langchain/langgraph-sdk': 1.8.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
langchain: 1.3.5(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6)) langchain: 1.3.5(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6))
@@ -4352,9 +4402,9 @@ snapshots:
- ws - ws
- zod-to-json-schema - zod-to-json-schema
'@ag-ui/mcp-apps-middleware@0.0.3(@ag-ui/client@0.0.52)(@cfworker/json-schema@4.1.1)(zod@3.25.76)': '@ag-ui/mcp-apps-middleware@0.0.3(@ag-ui/client@0.0.57)(@cfworker/json-schema@4.1.1)(zod@3.25.76)':
dependencies: dependencies:
'@ag-ui/client': 0.0.52 '@ag-ui/client': 0.0.57
'@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)
rxjs: 7.8.1 rxjs: 7.8.1
transitivePeerDependencies: transitivePeerDependencies:
@@ -4362,9 +4412,25 @@ snapshots:
- supports-color - supports-color
- zod - zod
'@ag-ui/proto@0.0.52': '@ag-ui/mcp-middleware@0.0.1(@cfworker/json-schema@4.1.1)(rxjs@7.8.1)(zod@3.25.76)':
dependencies: dependencies:
'@ag-ui/core': 0.0.52 '@ag-ui/client': 0.0.54
'@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)
rxjs: 7.8.1
transitivePeerDependencies:
- '@cfworker/json-schema'
- supports-color
- zod
'@ag-ui/proto@0.0.54':
dependencies:
'@ag-ui/core': 0.0.54
'@bufbuild/protobuf': 2.11.0
'@protobuf-ts/protoc': 2.11.1
'@ag-ui/proto@0.0.57':
dependencies:
'@ag-ui/core': 0.0.57
'@bufbuild/protobuf': 2.11.0 '@bufbuild/protobuf': 2.11.0
'@protobuf-ts/protoc': 2.11.1 '@protobuf-ts/protoc': 2.11.1
@@ -4513,19 +4579,21 @@ snapshots:
'@chevrotain/utils@12.0.0': {} '@chevrotain/utils@12.0.0': {}
'@copilotkit/a2ui-renderer@1.56.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': '@copilotkit/a2ui-renderer@1.62.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies: dependencies:
'@a2ui/web_core': 0.9.0 '@a2ui/web_core': 0.9.0
clsx: 2.1.1 clsx: 2.1.1
react: 19.2.5 lit: 3.3.2
react-dom: 19.2.5(react@19.2.5)
zod: 3.25.76 zod: 3.25.76
zod-to-json-schema: 3.25.2(zod@3.25.76) zod-to-json-schema: 3.25.2(zod@3.25.76)
optionalDependencies:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
'@copilotkit/core@1.56.4(@ag-ui/core@0.0.52)(zod@4.3.6)': '@copilotkit/core@1.62.2(@ag-ui/core@0.0.57)(zod@4.3.6)':
dependencies: dependencies:
'@ag-ui/client': 0.0.52 '@ag-ui/client': 0.0.57
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52) '@copilotkit/shared': 1.62.2(@ag-ui/core@0.0.57)
'@tanstack/pacer': 0.20.1 '@tanstack/pacer': 0.20.1
phoenix: 1.8.5 phoenix: 1.8.5
rxjs: 7.8.1 rxjs: 7.8.1
@@ -4535,27 +4603,29 @@ snapshots:
- encoding - encoding
- zod - zod
'@copilotkit/license-verifier@0.2.0': {} '@copilotkit/license-verifier@0.5.0': {}
'@copilotkit/react-core@1.56.4(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6)': '@copilotkit/react-core@1.62.2(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6)':
dependencies: dependencies:
'@ag-ui/client': 0.0.52 '@ag-ui/client': 0.0.57
'@ag-ui/core': 0.0.52 '@ag-ui/core': 0.0.57
'@copilotkit/a2ui-renderer': 1.56.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@copilotkit/a2ui-renderer': 1.62.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@copilotkit/core': 1.56.4(@ag-ui/core@0.0.52)(zod@4.3.6) '@copilotkit/core': 1.62.2(@ag-ui/core@0.0.57)(zod@4.3.6)
'@copilotkit/runtime-client-gql': 1.56.4(@ag-ui/core@0.0.52)(graphql@16.13.2)(react@19.2.5) '@copilotkit/runtime-client-gql': 1.62.2(@ag-ui/core@0.0.57)(graphql@16.13.2)(react@19.2.5)
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52) '@copilotkit/shared': 1.62.2(@ag-ui/core@0.0.57)
'@copilotkit/web-inspector': 1.56.4(@ag-ui/core@0.0.52)(zod@4.3.6) '@copilotkit/web-components': 1.62.2(lit@3.3.2)
'@copilotkit/web-inspector': 1.62.2(@ag-ui/core@0.0.57)(zod@4.3.6)
'@jetbrains/websandbox': 1.1.3 '@jetbrains/websandbox': 1.1.3
'@lit-labs/react': 2.1.3(@types/react@19.2.14) '@lit-labs/react': 2.1.3(@types/react@19.2.14)
'@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5) '@radix-ui/react-slot': 1.3.0(@types/react@19.2.14)(react@19.2.5)
'@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@scarf/scarf': 1.4.0 '@scarf/scarf': 1.4.0
'@tanstack/react-virtual': 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-virtual': 3.13.24(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
class-variance-authority: 0.7.1 class-variance-authority: 0.7.1
clsx: 2.1.1 clsx: 2.1.1
katex: 0.16.45 katex: 0.16.45
lit: 3.3.2
lucide-react: 0.525.0(react@19.2.5) lucide-react: 0.525.0(react@19.2.5)
react: 19.2.5 react: 19.2.5
react-dom: 19.2.5(react@19.2.5) react-dom: 19.2.5(react@19.2.5)
@@ -4578,16 +4648,17 @@ snapshots:
- micromark-util-types - micromark-util-types
- supports-color - supports-color
'@copilotkit/react-ui@1.56.4(@ag-ui/core@0.0.52)(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6)': '@copilotkit/react-ui@1.62.2(@ag-ui/core@0.0.57)(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6)':
dependencies: dependencies:
'@copilotkit/react-core': 1.56.4(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6) '@copilotkit/react-core': 1.62.2(@types/mdast@4.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(graphql@16.13.2)(micromark-util-types@2.0.2)(micromark@4.0.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.3.6)
'@copilotkit/runtime-client-gql': 1.56.4(@ag-ui/core@0.0.52)(graphql@16.13.2)(react@19.2.5) '@copilotkit/runtime-client-gql': 1.62.2(@ag-ui/core@0.0.57)(graphql@16.13.2)(react@19.2.5)
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52) '@copilotkit/shared': 1.62.2(@ag-ui/core@0.0.57)
'@headlessui/react': 2.2.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@headlessui/react': 2.2.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
react: 19.2.5 react: 19.2.5
react-markdown: 10.1.0(@types/react@19.2.14)(react@19.2.5) react-markdown: 10.1.0(@types/react@19.2.14)(react@19.2.5)
react-syntax-highlighter: 15.6.6(react@19.2.5) react-syntax-highlighter: 15.6.6(react@19.2.5)
rehype-raw: 7.0.0 rehype-raw: 7.0.0
rehype-sanitize: 6.0.0
remark-gfm: 4.0.1 remark-gfm: 4.0.1
remark-math: 6.0.0 remark-math: 6.0.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -4603,9 +4674,9 @@ snapshots:
- supports-color - supports-color
- zod - zod
'@copilotkit/runtime-client-gql@1.56.4(@ag-ui/core@0.0.52)(graphql@16.13.2)(react@19.2.5)': '@copilotkit/runtime-client-gql@1.62.2(@ag-ui/core@0.0.57)(graphql@16.13.2)(react@19.2.5)':
dependencies: dependencies:
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52) '@copilotkit/shared': 1.62.2(@ag-ui/core@0.0.57)
'@urql/core': 5.2.0(graphql@16.13.2) '@urql/core': 5.2.0(graphql@16.13.2)
react: 19.2.5 react: 19.2.5
untruncate-json: 0.0.1 untruncate-json: 0.0.1
@@ -4615,21 +4686,22 @@ snapshots:
- encoding - encoding
- graphql - graphql
'@copilotkit/runtime@1.56.4(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@langchain/langgraph-sdk@1.8.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@opentelemetry/api@1.9.0)(langchain@1.3.5(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6)))(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))': '@copilotkit/runtime@1.62.2(@cfworker/json-schema@4.1.1)(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@langchain/langgraph-sdk@1.8.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@opentelemetry/api@1.9.0)(langchain@1.3.5(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6)))(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))':
dependencies: dependencies:
'@ag-ui/a2ui-middleware': 0.0.5(@ag-ui/client@0.0.52)(rxjs@7.8.1) '@ag-ui/a2ui-middleware': 0.0.10(@ag-ui/client@0.0.57)(rxjs@7.8.1)
'@ag-ui/client': 0.0.52 '@ag-ui/client': 0.0.57
'@ag-ui/core': 0.0.52 '@ag-ui/core': 0.0.57
'@ag-ui/encoder': 0.0.52 '@ag-ui/encoder': 0.0.57
'@ag-ui/langgraph': 0.0.29(@ag-ui/client@0.0.52)(@ag-ui/core@0.0.52)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6)) '@ag-ui/langgraph': 0.0.42(@ag-ui/client@0.0.57)(@ag-ui/core@0.0.57)(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6))
'@ag-ui/mcp-apps-middleware': 0.0.3(@ag-ui/client@0.0.52)(@cfworker/json-schema@4.1.1)(zod@3.25.76) '@ag-ui/mcp-apps-middleware': 0.0.3(@ag-ui/client@0.0.57)(@cfworker/json-schema@4.1.1)(zod@3.25.76)
'@ag-ui/mcp-middleware': 0.0.1(@cfworker/json-schema@4.1.1)(rxjs@7.8.1)(zod@3.25.76)
'@ai-sdk/anthropic': 3.0.71(zod@3.25.76) '@ai-sdk/anthropic': 3.0.71(zod@3.25.76)
'@ai-sdk/google': 3.0.64(zod@3.25.76) '@ai-sdk/google': 3.0.64(zod@3.25.76)
'@ai-sdk/google-vertex': 3.0.132(zod@3.25.76) '@ai-sdk/google-vertex': 3.0.132(zod@3.25.76)
'@ai-sdk/mcp': 1.0.36(zod@3.25.76) '@ai-sdk/mcp': 1.0.36(zod@3.25.76)
'@ai-sdk/openai': 3.0.53(zod@3.25.76) '@ai-sdk/openai': 3.0.53(zod@3.25.76)
'@copilotkit/license-verifier': 0.2.0 '@copilotkit/license-verifier': 0.5.0
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52) '@copilotkit/shared': 1.62.2(@ag-ui/core@0.0.57)
'@graphql-yoga/plugin-defer-stream': 3.21.0(graphql-yoga@5.21.0(graphql@16.13.2))(graphql@16.13.2) '@graphql-yoga/plugin-defer-stream': 3.21.0(graphql-yoga@5.21.0(graphql@16.13.2))(graphql@16.13.2)
'@hono/node-server': 1.19.14(hono@4.12.15) '@hono/node-server': 1.19.14(hono@4.12.15)
'@langchain/core': 1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) '@langchain/core': 1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)
@@ -4676,11 +4748,11 @@ snapshots:
- vue - vue
- zod-to-json-schema - zod-to-json-schema
'@copilotkit/shared@1.56.4(@ag-ui/core@0.0.52)': '@copilotkit/shared@1.62.2(@ag-ui/core@0.0.57)':
dependencies: dependencies:
'@ag-ui/client': 0.0.52 '@ag-ui/client': 0.0.57
'@ag-ui/core': 0.0.52 '@ag-ui/core': 0.0.57
'@copilotkit/license-verifier': 0.2.0 '@copilotkit/license-verifier': 0.5.0
'@segment/analytics-node': 2.3.0 '@segment/analytics-node': 2.3.0
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
chalk: 4.1.2 chalk: 4.1.2
@@ -4692,10 +4764,14 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
'@copilotkit/web-inspector@1.56.4(@ag-ui/core@0.0.52)(zod@4.3.6)': '@copilotkit/web-components@1.62.2(lit@3.3.2)':
dependencies: dependencies:
'@ag-ui/client': 0.0.52 lit: 3.3.2
'@copilotkit/core': 1.56.4(@ag-ui/core@0.0.52)(zod@4.3.6)
'@copilotkit/web-inspector@1.62.2(@ag-ui/core@0.0.57)(zod@4.3.6)':
dependencies:
'@ag-ui/client': 0.0.57
'@copilotkit/core': 1.62.2(@ag-ui/core@0.0.57)(zod@4.3.6)
lit: 3.3.2 lit: 3.3.2
lucide: 0.525.0 lucide: 0.525.0
marked: 12.0.2 marked: 12.0.2
@@ -4979,7 +5055,7 @@ snapshots:
langsmith: 0.5.25(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) langsmith: 0.5.25(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)
mustache: 4.2.0 mustache: 4.2.0
p-queue: 6.6.2 p-queue: 6.6.2
zod: 3.25.76 zod: 4.3.6
transitivePeerDependencies: transitivePeerDependencies:
- '@opentelemetry/api' - '@opentelemetry/api'
- '@opentelemetry/exporter-trace-otlp-proto' - '@opentelemetry/exporter-trace-otlp-proto'
@@ -5003,14 +5079,14 @@ snapshots:
react: 19.2.5 react: 19.2.5
react-dom: 19.2.5(react@19.2.5) react-dom: 19.2.5(react@19.2.5)
'@langchain/langgraph@1.2.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@3.25.76)': '@langchain/langgraph@1.2.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)':
dependencies: dependencies:
'@langchain/core': 1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) '@langchain/core': 1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)
'@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)) '@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))
'@langchain/langgraph-sdk': 1.8.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@langchain/langgraph-sdk': 1.8.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
uuid: 10.0.0 uuid: 10.0.0
zod: 3.25.76 zod: 4.3.6
optionalDependencies: optionalDependencies:
zod-to-json-schema: 3.25.2(zod@4.3.6) zod-to-json-schema: 3.25.2(zod@4.3.6)
transitivePeerDependencies: transitivePeerDependencies:
@@ -5379,13 +5455,6 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/react': 19.2.14 '@types/react': 19.2.14
'@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.5)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5)
react: 19.2.5
optionalDependencies:
'@types/react': 19.2.14
'@radix-ui/react-slot@1.3.0(@types/react@19.2.14)(react@19.2.5)': '@radix-ui/react-slot@1.3.0(@types/react@19.2.14)(react@19.2.5)':
dependencies: dependencies:
'@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.5) '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.14)(react@19.2.5)
@@ -7186,10 +7255,10 @@ snapshots:
langchain@1.3.5(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6)): langchain@1.3.5(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(ws@8.20.0)(zod-to-json-schema@3.25.2(zod@4.3.6)):
dependencies: dependencies:
'@langchain/core': 1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) '@langchain/core': 1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)
'@langchain/langgraph': 1.2.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@3.25.76) '@langchain/langgraph': 1.2.9(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)
'@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)) '@langchain/langgraph-checkpoint': 1.0.1(@langchain/core@1.1.42(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0))
langsmith: 0.5.25(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0) langsmith: 0.5.25(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.20.0)(zod@4.3.6))(ws@8.20.0)
zod: 3.25.76 zod: 4.3.6
transitivePeerDependencies: transitivePeerDependencies:
- '@opentelemetry/api' - '@opentelemetry/api'
- '@opentelemetry/exporter-trace-otlp-proto' - '@opentelemetry/exporter-trace-otlp-proto'

View File

@@ -120,6 +120,27 @@ class DeduplicateToolCallMiddleware extends (Middleware as any) {
const open = new Set<string>(); // tool call IDs currently in progress const open = new Set<string>(); // tool call IDs currently in progress
const closed = new Set<string>(); // tool call IDs that already received END const closed = new Set<string>(); // tool call IDs that already received END
// Render tools are display-once: after the client returns their result,
// the model sometimes re-issues the same call (fresh id) on the
// follow-up run, painting a second identical card. Track which render
// tools already ran in the current user turn (from the request's own
// message history) and suppress repeats — both their streamed events
// and their entries in this run's MESSAGES_SNAPSHOT.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const inputMessages: any[] = Array.isArray(input?.messages) ? input.messages : [];
let lastUserIdx = -1;
inputMessages.forEach((m, i) => { if (m?.role === "user") lastUserIdx = i; });
const renderedThisTurn = new Set<string>();
for (const m of inputMessages.slice(lastUserIdx + 1)) {
const calls = m?.toolCalls ?? m?.tool_calls;
if (m?.role !== "assistant" || !Array.isArray(calls)) continue;
for (const tc of calls) {
const name = tc?.function?.name;
if (name && RENDER_TOOLS.has(name)) renderedThisTurn.add(name);
}
}
const suppressed = new Set<string>(); // toolCallIds of dropped repeats
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Observable<any>((observer) => { return new Observable<any>((observer) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -132,13 +153,51 @@ class DeduplicateToolCallMiddleware extends (Middleware as any) {
if (type === EventType.TOOL_CALL_START) { if (type === EventType.TOOL_CALL_START) {
if (!id || closed.has(id) || open.has(id)) return; // duplicate if (!id || closed.has(id) || open.has(id)) return; // duplicate
const name: string | undefined = event?.toolCallName;
if (name && RENDER_TOOLS.has(name)) {
if (renderedThisTurn.has(name)) {
suppressed.add(id);
return; // repeat render call — drop the whole call
}
renderedThisTurn.add(name);
}
open.add(id); open.add(id);
} else if (type === EventType.TOOL_CALL_ARGS || type === EventType.TOOL_CALL_END) { } else if (type === EventType.TOOL_CALL_ARGS || type === EventType.TOOL_CALL_END) {
if (id && suppressed.has(id)) return;
if (id && closed.has(id)) return; // already completed, drop if (id && closed.has(id)) return; // already completed, drop
if (type === EventType.TOOL_CALL_END && id) { if (type === EventType.TOOL_CALL_END && id) {
open.delete(id); open.delete(id);
closed.add(id); closed.add(id);
} }
} else if (type === EventType.MESSAGES_SNAPSHOT && suppressed.size > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const msgs: any[] = Array.isArray(event?.messages) ? event.messages : [];
const cleaned = msgs
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.map((m: any) => {
const calls = m?.toolCalls ?? m?.tool_calls;
if (m?.role !== "assistant" || !Array.isArray(calls)) return m;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const kept = calls.filter((tc: any) => !suppressed.has(String(tc?.id ?? "")));
if (kept.length === calls.length) return m;
const { toolCalls: _a, tool_calls: _b, ...rest } = m;
return kept.length > 0 ? { ...rest, toolCalls: kept } : rest;
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.filter((m: any) => {
if (m?.role === "tool" && suppressed.has(String(m?.toolCallId ?? m?.tool_call_id ?? ""))) {
return false;
}
if (m?.role === "assistant") {
const calls = m?.toolCalls ?? m?.tool_calls;
const hasCalls = Array.isArray(calls) && calls.length > 0;
const hasText = typeof m?.content === "string" && m.content.length > 0;
if (!hasCalls && !hasText) return false;
}
return true;
});
observer.next({ ...event, messages: cleaned });
return;
} }
observer.next(event); // emit raw BaseEvent observer.next(event); // emit raw BaseEvent
@@ -192,23 +251,30 @@ class StripModelArtifactsMiddleware extends (Middleware as any) {
} }
// ── ReconcileSnapshotMiddleware ────────────────────────────────────────────── // ── ReconcileSnapshotMiddleware ──────────────────────────────────────────────
// MAF's `_build_messages_snapshot` (agent_framework_ag_ui/_agent_run.py:686) // MAF 1.10 reuses the streamed id for PLAIN-text snapshots, but still mints a
// mints a fresh UUID for the post-tool-call assistant text instead of reusing // fresh UUID for post-tool-call assistant text — deliberately, per their
// the streamed TEXT_MESSAGE_START id. The resulting MESSAGES_SNAPSHOT then // issue #3619 ("separate message"). ag-ui's snapshot merge appends unknown
// contains TWO assistant entries: the streamed id (holding the toolCalls) and // ids, so tool+text turns still produce a duplicate assistant bubble without
// a brand-new id (holding the duplicated text). ag-ui's snapshot merge replaces // this reconciliation (re-verified live on MAF 1.10 / CK 1.62, 2026-07-04).
// by id then APPENDS unknown ids, so the browser ends up with two assistant // Drops only the orphan text-only assistant message with no streamed
// bubbles for the same answer. Dropping the snapshot entirely fixes the dupe // counterpart. Caveat: interfered with a2ui retry runs (duplicated the user
// but breaks render_a2ui card persistence (cards rely on the snapshot to keep // message) — a2ui is disabled while that pipeline matures; revisit both
// the assistant-with-toolCalls message in state past the run). The right fix // together.
// is to drop just the orphan text-only assistant message that has no streamed //
// counterpart. Remove once `_agent_run.py:686` reuses `flow.message_id`. // Thread persistence (2026-07-04) adds two exemptions: a hydration replay
// (empty input.messages) is passed through untouched — nothing in it was
// streamed this run — and messages whose id the client already sent in
// input.messages are history, not duplicates.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
class ReconcileSnapshotMiddleware extends (Middleware as any) { class ReconcileSnapshotMiddleware extends (Middleware as any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
run(input: any, next: any): Observable<any> { run(input: any, next: any): Observable<any> {
const streamedTextIds = new Set<string>(); const streamedTextIds = new Set<string>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const inputMessages: any[] = Array.isArray(input?.messages) ? input.messages : [];
const isHydration = inputMessages.length === 0;
const clientKnownIds = new Set<string>(inputMessages.map((m) => String(m?.id ?? "")));
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return new Observable<any>((observer) => { return new Observable<any>((observer) => {
@@ -223,7 +289,7 @@ class ReconcileSnapshotMiddleware extends (Middleware as any) {
streamedTextIds.add(String(event.messageId)); streamedTextIds.add(String(event.messageId));
} }
if (type === EventType.MESSAGES_SNAPSHOT) { if (type === EventType.MESSAGES_SNAPSHOT && !isHydration) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const msgs: any[] = Array.isArray(event?.messages) ? event.messages : []; const msgs: any[] = Array.isArray(event?.messages) ? event.messages : [];
const filtered = msgs.filter((m) => { const filtered = msgs.filter((m) => {
@@ -233,7 +299,12 @@ class ReconcileSnapshotMiddleware extends (Middleware as any) {
const hasToolCalls = const hasToolCalls =
(Array.isArray(m?.toolCalls) && m.toolCalls.length > 0) || (Array.isArray(m?.toolCalls) && m.toolCalls.length > 0) ||
(Array.isArray(m?.tool_calls) && m.tool_calls.length > 0); (Array.isArray(m?.tool_calls) && m.tool_calls.length > 0);
if (hasText && !hasToolCalls && !streamedTextIds.has(id)) { if (
hasText &&
!hasToolCalls &&
!streamedTextIds.has(id) &&
!clientKnownIds.has(id)
) {
return false; // drop orphan text-only assistant duplicate return false; // drop orphan text-only assistant duplicate
} }
return true; return true;
@@ -261,7 +332,7 @@ class ReconcileSnapshotMiddleware extends (Middleware as any) {
// below the card. This middleware buffers TEXT_MESSAGE_* events and discards // below the card. This middleware buffers TEXT_MESSAGE_* events and discards
// them if any render tool call is detected in the same turn. // them if any render tool call is detected in the same turn.
const RENDER_TOOLS = new Set(["render_a2ui", "render_spending_summary"]); const RENDER_TOOLS = new Set(["render_spending_summary"]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
class SuppressRenderToolTextMiddleware extends (Middleware as any) { class SuppressRenderToolTextMiddleware extends (Middleware as any) {
@@ -356,38 +427,73 @@ app.all("/api/copilotkit/*", async (c) => {
: {}; : {};
const agent = new HttpAgent({ url: AGENT_URL, headers: agentHeaders }); const agent = new HttpAgent({ url: AGENT_URL, headers: agentHeaders });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // CK_MIDDLEWARES=off runs the stack bare — used to audit which of these
agent.use(new SuppressRenderToolTextMiddleware() as any); // workarounds are still needed after CopilotKit/MAF upgrades.
// eslint-disable-next-line @typescript-eslint/no-explicit-any /* eslint-disable @typescript-eslint/no-explicit-any */
agent.use(new StripModelArtifactsMiddleware() as any); if (process.env.CK_MIDDLEWARES !== "off") {
// eslint-disable-next-line @typescript-eslint/no-explicit-any agent.use(new SuppressRenderToolTextMiddleware() as any);
agent.use(new ReconcileSnapshotMiddleware() as any); agent.use(new StripModelArtifactsMiddleware() as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any agent.use(new ReconcileSnapshotMiddleware() as any);
agent.use(new DeduplicateToolCallMiddleware() as any); agent.use(new DeduplicateToolCallMiddleware() as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any }
agent.use(new DebugLogMiddleware() as any); if (process.env.CK_DEBUG_EVENTS === "1" || process.env.CK_MIDDLEWARES === "off") {
agent.use(new DebugLogMiddleware() as any);
}
/* eslint-enable @typescript-eslint/no-explicit-any */
const runtime = new CopilotRuntime({ const runtime = new CopilotRuntime({
agents: { wealthysmart: agent }, agents: { wealthysmart: agent },
a2ui: { injectA2UITool: true }, // a2ui disabled 2026-07-04: CK 1.60+ validates generated ops against a
// catalog and our agent's ops fail validation (surface never paints).
// Markdown tables cover the use case until the pipeline matures.
a2ui: { enabled: false },
beforeRequestMiddleware: async ({ request: outbound }) => { beforeRequestMiddleware: async ({ request: outbound }) => {
if (outbound.method !== "POST") return; if (outbound.method !== "POST") return;
const ct = outbound.headers.get("content-type") ?? ""; const ct = outbound.headers.get("content-type") ?? "";
if (!ct.includes("application/json")) return; if (!ct.includes("application/json")) return;
try { try {
const body = (await outbound.clone().json()) as { messages?: AGUIMessage[] }; const body = (await outbound.clone().json()) as {
messages?: AGUIMessage[];
context?: { description?: string; value?: string }[];
};
if (!Array.isArray(body.messages)) return; if (!Array.isArray(body.messages)) return;
const paired = pairOrphanToolCalls(body.messages); // An empty messages array is MAF's hydration trigger (known threadId
if (paired.length === body.messages.length) return; // + no messages → replay the stored snapshot without invoking the
// LLM). Injecting anything would turn it into a normal run.
if (body.messages.length === 0) return;
let messages = pairOrphanToolCalls(body.messages);
// MAF's AG-UI endpoint declares `context` on its request model but
// never feeds it to the model (agent_framework_ag_ui, checked at
// python-1.10.0). CopilotKit 1.60+ ships the A2UI catalog schema and
// guidelines in that context — without them the model emits ops the
// A2UI validator rejects and the surface never paints. Inject the
// context as a system message ourselves. Remove once MAF consumes
// RunAgentInput.context.
if (Array.isArray(body.context) && body.context.length > 0) {
const contextText = body.context
.map((c) => `## ${c.description ?? "Context"}\n${c.value ?? ""}`)
.join("\n\n");
messages = [
{
id: `ctx-${Date.now()}`,
role: "system",
content: `Additional run context:\n\n${contextText}`,
} as AGUIMessage,
...messages,
];
}
if (messages === body.messages) return;
return new Request(outbound.url, { return new Request(outbound.url, {
method: outbound.method, method: outbound.method,
headers: outbound.headers, headers: outbound.headers,
body: JSON.stringify({ ...body, messages: paired }), body: JSON.stringify({ ...body, messages }),
}); });
} catch (err) { } catch (err) {
// Skipping the repair is survivable; doing it silently is not — // Skipping the repair is survivable; doing it silently is not —
// orphan tool calls then fail downstream with no trace of why. // orphan tool calls then fail downstream with no trace of why.
console.error("[copilotkit] orphan-tool-call repair skipped:", err); console.error("[copilotkit] outbound request repair skipped:", err);
return; return;
} }
}, },

View File

@@ -77,7 +77,7 @@ export default function App() {
<PrivacyProvider> <PrivacyProvider>
<AuthProvider> <AuthProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<CopilotKit runtimeUrl="/api/copilotkit" agent="wealthysmart" a2ui={{}}> <CopilotKit runtimeUrl="/api/copilotkit" agent="wealthysmart">
<AppRoutes /> <AppRoutes />
<Toaster richColors position="top-right" closeButton /> <Toaster richColors position="top-right" closeButton />
</CopilotKit> </CopilotKit>

View File

@@ -27,11 +27,14 @@ export default function Layout() {
const { privacyMode, togglePrivacy } = usePrivacy(); const { privacyMode, togglePrivacy } = usePrivacy();
const { pathname } = useLocation(); const { pathname } = useLocation();
const title = titleFor(pathname); const title = titleFor(pathname);
// Asistente hosts its own scroll container (the chat), so its route caps
// the shell at the viewport; every other page keeps normal body scroll.
const fullHeight = pathname === "/asistente";
return ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />
<SidebarInset> <SidebarInset className={fullHeight ? "h-svh overflow-hidden" : undefined}>
<header className="sticky top-0 z-40 flex h-14 shrink-0 items-center gap-2 border-b border-border bg-background/90 px-4 backdrop-blur-sm"> <header className="sticky top-0 z-40 flex h-14 shrink-0 items-center gap-2 border-b border-border bg-background/90 px-4 backdrop-blur-sm">
<SidebarTrigger className="-ml-1" /> <SidebarTrigger className="-ml-1" />
<Separator <Separator

View File

@@ -345,6 +345,23 @@ export interface paths {
patch: operations["update_category_api_v1_categories__category_id__patch"]; patch: operations["update_category_api_v1_categories__category_id__patch"];
trace?: never; trace?: never;
}; };
"/api/v1/chat/thread": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** Clear Thread */
delete: operations["clear_thread_api_v1_chat_thread_delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/exchange-rate/": { "/api/v1/exchange-rate/": {
parameters: { parameters: {
query?: never; query?: never;
@@ -1200,6 +1217,11 @@ export interface components {
/** Name */ /** Name */
name?: string | null; name?: string | null;
}; };
/** ClearThreadResponse */
ClearThreadResponse: {
/** Cleared */
cleared: boolean;
};
/** /**
* Currency * Currency
* @enum {string} * @enum {string}
@@ -3023,6 +3045,39 @@ export interface operations {
}; };
}; };
}; };
clear_thread_api_v1_chat_thread_delete: {
parameters: {
query?: never;
header?: {
authorization?: string | null;
};
path?: never;
cookie?: {
ws_token?: string | null;
};
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ClearThreadResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
current_rate_api_v1_exchange_rate__get: { current_rate_api_v1_exchange_rate__get: {
parameters: { parameters: {
query?: never; query?: never;

View File

@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import { import {
CopilotChat, CopilotChat,
@@ -7,10 +7,17 @@ import {
useCopilotKit, useCopilotKit,
} from "@copilotkit/react-core/v2"; } from "@copilotkit/react-core/v2";
import { useCopilotAction } from "@copilotkit/react-core"; import { useCopilotAction } from "@copilotkit/react-core";
import { Sparkles } from "lucide-react"; import { MessageSquarePlus, Sparkles } from "lucide-react";
import { Button } from "@/components/ui/button";
import ConfirmDialog from "@/components/ConfirmDialog";
import { PageHeader } from '@/components/page-header'; import { PageHeader } from '@/components/page-header';
import { SpendingSummaryCard, type SpendingSummaryArgs } from "@/components/chat/ChatCards"; import { SpendingSummaryCard, type SpendingSummaryArgs } from "@/components/chat/ChatCards";
// Single persistent conversation: the backend snapshot store keys on this
// same id (backend/app/api/v1/endpoints/chat.py). An explicit, stable
// threadId also stops CopilotChat from wiping messages on remount.
const CHAT_THREAD_ID = "main";
const STATIC_SUGGESTIONS = { const STATIC_SUGGESTIONS = {
available: "before-first-message" as const, available: "before-first-message" as const,
suggestions: [ suggestions: [
@@ -24,28 +31,66 @@ const STATIC_SUGGESTIONS = {
export default function Asistente() { export default function Asistente() {
useConfigureSuggestions(STATIC_SUGGESTIONS); useConfigureSuggestions(STATIC_SUGGESTIONS);
// A question typed on the Inicio dashboard arrives via router state:
// append it as a user message and run the agent once.
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { agent } = useAgent({ agentId: "wealthysmart" }); const { agent } = useAgent({ agentId: "wealthysmart" });
const { copilotkit } = useCopilotKit(); const { copilotkit } = useCopilotKit();
const sentRef = useRef(false); const [confirmClear, setConfirmClear] = useState(false);
const [clearing, setClearing] = useState(false);
// Boot sequence, once per page load: hydrate the persisted thread, then
// send any question that arrived from the Inicio ask-box via router state.
// Order matters — hydration first keeps the invariant that every normal
// run's input carries the full client-known history (the BFF's snapshot
// reconciliation relies on it).
const bootRef = useRef(false);
useEffect(() => { useEffect(() => {
if (!agent || bootRef.current) return;
bootRef.current = true;
const ask = (location.state as { ask?: string } | null)?.ask; const ask = (location.state as { ask?: string } | null)?.ask;
if (!ask || sentRef.current || !agent) return; if (ask) {
sentRef.current = true; // Clear the state so back/refresh doesn't re-send the question.
// Clear the state so back/refresh doesn't re-send the question. navigate(location.pathname, { replace: true, state: null });
navigate(location.pathname, { replace: true, state: null }); }
agent.addMessage({ void (async () => {
id: crypto.randomUUID(), agent.threadId = CHAT_THREAD_ID;
role: "user", if (agent.messages.length === 0) {
content: ask, // Empty-messages run = MAF's hydration trigger: replays the stored
}); // snapshot as MESSAGES_SNAPSHOT without invoking the LLM.
void copilotkit.runAgent({ agent }); try {
await copilotkit.runAgent({ agent });
} catch (err) {
console.error("[asistente] thread hydration failed:", err);
}
}
if (ask) {
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: ask,
});
void copilotkit.runAgent({ agent });
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [agent]); }, [agent]);
const clearThread = async () => {
setClearing(true);
try {
await fetch("/api/v1/chat/thread", {
method: "DELETE",
credentials: "include",
});
agent?.setMessages([]);
setConfirmClear(false);
} catch (err) {
console.error("[asistente] clear thread failed:", err);
} finally {
setClearing(false);
}
};
useCopilotAction({ useCopilotAction({
name: "render_spending_summary", name: "render_spending_summary",
description: description:
@@ -92,12 +137,35 @@ export default function Asistente() {
icon={Sparkles} icon={Sparkles}
title="Asistente" title="Asistente"
subtitle="Pregúntale a WealthySmart sobre tus finanzas." subtitle="Pregúntale a WealthySmart sobre tus finanzas."
actions={
<Button
variant="outline"
size="sm"
onClick={() => setConfirmClear(true)}
disabled={!agent || agent.messages.length === 0}
>
<MessageSquarePlus />
Nueva conversación
</Button>
}
/> />
</div> </div>
{confirmClear && (
<ConfirmDialog
title="¿Nueva conversación?"
message="Se borrará el historial del chat guardado. Esta acción no se puede deshacer."
confirmLabel="Borrar historial"
onConfirm={clearThread}
onCancel={() => setConfirmClear(false)}
loading={clearing}
/>
)}
<div className="flex-1 min-h-0 rounded-xl border border-border overflow-hidden bg-card"> <div className="flex-1 min-h-0 rounded-xl border border-border overflow-hidden bg-card">
<CopilotChat <CopilotChat
className="h-full" className="h-full"
threadId={CHAT_THREAD_ID}
labels={{ labels={{
modalHeaderTitle: "WealthySmart", modalHeaderTitle: "WealthySmart",
welcomeMessageText: "¿Qué quieres saber sobre tus finanzas?", welcomeMessageText: "¿Qué quieres saber sobre tus finanzas?",