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).
- Format currency with the appropriate symbol: ₡ for CRC (no decimals), $ for
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.
- 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
@@ -47,17 +49,16 @@ Generative UI — render tools:
- When showing spending totals, cycle summaries, or category breakdowns →
call render_spending_summary. Source data: get_cycle_summary (by_source,
grand_total_crc) + get_analytics_by_category (by_category).
- When showing transaction lists or other structured data →
call render_a2ui in a SEPARATE tool-call step, only after all data-fetching
calls have returned. NEVER call render_a2ui in the same batch as any other
tool.
- Do NOT use markdown tables for data a render tool can display.
- CRITICAL RULE: When you call a render tool (render_spending_summary or
render_a2ui), that tool call MUST be the ONLY content in your message.
Do NOT include any text content alongside the tool call — no introduction,
no list, no explanation, nothing. The rendered card IS the complete
response. Any text you write in the same message as a render call will
appear as a duplicate below the card, which is wrong.
- Do NOT use markdown tables for data render_spending_summary can display.
- CRITICAL RULE: When you call render_spending_summary, that tool call MUST
be the ONLY content in your message. Do NOT include any text content
alongside the tool call — no introduction, no list, no explanation,
nothing. The rendered card IS the complete response. Any text you write in
the same message as a render call will appear as a duplicate below the
card, which is wrong.
- When a render_spending_summary tool result ("ok") is already present for
the current question, the card is already on screen. Do NOT call the tool
again and do NOT add text — end your response with no further output.
"""

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,
budget,
categories,
chat,
exchange_rate,
import_transactions,
installments,
@@ -38,3 +39,4 @@ api_router.include_router(pensions.router)
api_router.include_router(municipal_receipts.router)
api_router.include_router(savings_accrual.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 app.agent.agent import build_agent
from app.agent.snapshot_store import PostgresSnapshotStore
from app.agent.tools import reset_session, set_session
from app.api.v1.router import api_router
from app.auth import (
@@ -148,8 +149,17 @@ async def agent_auth_and_session(request: Request, call_next):
# Register app routes
app.include_router(api_router)
# Mount the AG-UI agent endpoint.
add_agent_framework_fastapi_endpoint(app, build_agent(), AGENT_PATH)
# Mount the AG-UI agent endpoint. The snapshot store persists the chat
# 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("/")

View File

@@ -537,3 +537,32 @@ class WaterMeterReading(WaterMeterReadingBase, table=True):
class WaterMeterReadingRead(WaterMeterReadingBase):
id: int
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
ag-ui-protocol==0.1.19
agent-framework==1.2.1
agent-framework==1.10.0
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-azure-ai-search==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-claude==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-devui==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-mem0==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-purview==1.0.0b260428
agent-framework-redis==1.0.0b260428

View File

@@ -11,6 +11,6 @@ httpx
pywebpush
py-vapid
python-dateutil
agent-framework==1.2.1
agent-framework-ag-ui==1.0.0b260428
agent-framework-openai==1.2.1
agent-framework==1.10.0
agent-framework-ag-ui==1.0.0rc7
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}
OPENAI_API_KEY: ${OPENAI_API_KEY}
AGENT_MODEL: ${AGENT_MODEL:-gpt-5.4-mini}
# MAF 1.6+ enables OTel instrumentation by default; keep it off explicitly.
ENABLE_INSTRUMENTATION: "false"
expose:
- "8000"
networks:

View File

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

View File

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

297
frontend/pnpm-lock.yaml generated
View File

@@ -9,20 +9,20 @@ importers:
.:
dependencies:
'@ag-ui/client':
specifier: 0.0.52
version: 0.0.52
specifier: 0.0.57
version: 0.0.57
'@base-ui/react':
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)
'@copilotkit/react-core':
specifier: 1.56.4
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)
specifier: 1.62.2
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':
specifier: 1.56.4
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)
specifier: 1.62.2
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':
specifier: 1.56.4
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))
specifier: 1.62.2
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':
specifier: ^5.2.8
version: 5.2.8
@@ -131,23 +131,35 @@ packages:
'@a2ui/web_core@0.9.0':
resolution: {integrity: sha512-TsMWuEeuVDsScGIGPy/fWIZu+EOBRfhx6KwjKh3VwY1AwysRenQM8zDr8VrSk14Wck/aBgVxk2zWVrMCK2/s6A==}
'@ag-ui/a2ui-middleware@0.0.5':
resolution: {integrity: sha512-nhF8LBzq36BmLR1O3nStxqSpIu1+4//ttqOyQgiXUsKZK1znCyN4q4MBTARrkFa7CuFr3fcNTiZuS4V1HRUHTw==}
'@ag-ui/a2ui-middleware@0.0.10':
resolution: {integrity: sha512-2BQFUQ9vJzUAQSR0dNW/ijhyH8KpiRWISSLTP6mIe6ENyQ2cM1/XLG38/Dcb69olcs36gtZADZzAQWcno5H6fA==}
peerDependencies:
'@ag-ui/client': '>=0.0.40'
rxjs: 7.8.1
'@ag-ui/client@0.0.52':
resolution: {integrity: sha512-U407VvDDwR5qs8TiyN1qY38x87qMWc2n0epw8iA5aa1qwzCKBBDgg3Fkm4JogQf0X4jwNsz8HUbIZrBB56mrpg==}
'@ag-ui/a2ui-toolkit@0.0.4':
resolution: {integrity: sha512-9VPmgpCAsFVICk7z23kh+Kp/BwYMxw0D0KGjOiKXhm1MAH+Wid2iC3yKsXso+q7UZZiyhWgeDUSgaAkltyLmGg==}
'@ag-ui/core@0.0.52':
resolution: {integrity: sha512-Xo0bUaNV56EqylzcrAuhUkQX7et7+SZIrqZZtEByGwEq/I1EHny6ZMkWHLkKR7UNi0FJZwJyhKYmKJS3B2SEgA==}
'@ag-ui/client@0.0.54':
resolution: {integrity: sha512-N5UVXEBV5gPHqTuMoR/21brconRn42URf+MB4L8OniCJKqLcl/qUJb5kMamK0nnfBhDfPs/uq7LxDn6bsDJzJg==}
'@ag-ui/encoder@0.0.52':
resolution: {integrity: sha512-6GVDTb1dv2rjap7VVnmXYypDutZi6nrsTcdfxoP6ryDG5ynlXtmmS+FSDAt62JbIMD5CtEE963xNCb6d1iXw9g==}
'@ag-ui/client@0.0.57':
resolution: {integrity: sha512-Xap2alG9Z0/j5kb3x4D7oTpe2sw1dfrC9rgJJr2NZu5vKcm8dzIPNd31mF2B4zS3BKqYIu245yxKPhEtT30MHw==}
'@ag-ui/langgraph@0.0.29':
resolution: {integrity: sha512-7Q4YMtuI53N3uyZbEhYsIpfwYK1l5mWX88nm7BE242lIpPsiLIj3RL8xpT8lojqmR9MyZ72jlHIhXm1hbsrZjg==}
'@ag-ui/core@0.0.54':
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:
'@ag-ui/client': '>=0.0.42'
'@ag-ui/core': '>=0.0.42'
@@ -157,8 +169,16 @@ packages:
peerDependencies:
'@ag-ui/client': '>=0.0.40'
'@ag-ui/proto@0.0.52':
resolution: {integrity: sha512-+iCGzNUNL50YIoThVmsolWPjG4MJidl+R9k8QAGVwErEfHRtQ64KFyrdpeOXNVuWtM3SViJqPSgFyv7eGVS63A==}
'@ag-ui/mcp-middleware@0.0.1':
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':
resolution: {integrity: sha512-8n7ApEzFOxqVvT3HyqLrEQlgUx/2nUmPFLTGY3fNKwUA8KVNU3Ovd2C66Qh1Y93Iq5NkHsOWuLiTyAZpRKQhgw==}
@@ -300,38 +320,43 @@ packages:
'@chevrotain/utils@12.0.0':
resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==}
'@copilotkit/a2ui-renderer@1.56.4':
resolution: {integrity: sha512-eYikRML0l8p9zrNYm6SW5YO3PygmQPM8QcBcvq7kbRrvlJR7LP1qT+sbkzK8dUEU7k5WrE63BEOgLRNVscn60w==}
'@copilotkit/a2ui-renderer@1.62.2':
resolution: {integrity: sha512-9V7s3aSFGBWS65EXdP04Fg597x9g1WpwoBT6mHW2Ev2uvIzo84KhObw2jWpNsaecAheesP70xS7752FB4GExxw==}
peerDependencies:
react: ^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':
resolution: {integrity: sha512-fCOhaxmNTgeCJRs1IDOmKoMXcAOJmTlWzYPum/vi6xuXM7M6+3OAtlDAed/47FVAxqny72SWCynBudH8hKwBQg==}
'@copilotkit/core@1.62.2':
resolution: {integrity: sha512-I3fCb6nZVtRi8+HuJULD0RzgTv6ZxkNZtrz+Ewd1lwhLUmw0gBsmsv1HitsM6opayuAZj+4mVrhwhgKJyZwSQQ==}
engines: {node: '>=18'}
'@copilotkit/license-verifier@0.2.0':
resolution: {integrity: sha512-hliCifqy5a65YTozgRckuQmvBEQlt4L2PhbpSDY6fb/TKqPHyNDJgItRSnOpxOVDqvEfHEbUUqw3NaD88ZtdJA==}
'@copilotkit/license-verifier@0.5.0':
resolution: {integrity: sha512-vrwKtIpYwF0FT9ZoYASH8owa2cGV0dhDvJGaCRaRMStwDxpc6DRdydKkhx8cWZXyBRxEYcq/Vygv4JvevhQQdQ==}
'@copilotkit/react-core@1.56.4':
resolution: {integrity: sha512-HObN7YSX3KaCN+Dg0WzXHBylayI3WWQKCFB/DMsGajWefd2+Y3I33gs+hCKQFDcD/l8IWpR1/XvmGfzo6kvxdQ==}
'@copilotkit/react-core@1.62.2':
resolution: {integrity: sha512-tw3/PMzP4NEEdkBrK3PVJbkbh1ztnun9WdBHObLyU/Ou2d/37Rm3w5cnt17WVIxTa1FLtMoSdp3kC26656RAbg==}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc
zod: '>=3.0.0'
'@copilotkit/react-ui@1.56.4':
resolution: {integrity: sha512-pl2LZSYFOo6DjEiJDX/IstGqgvPn/xfLVzKkKyWuiJqxs69jynQIziCdUblbpV1d47hEqJOGgiAc7z7ltATLMA==}
'@copilotkit/react-ui@1.62.2':
resolution: {integrity: sha512-lFbf/7GiSpu3WFk0+Fbd6v4eqNx3n2B3vYZle2hF+7YM1c/jtlll6wM4JlB0CNtxFvAbos5sNifyuMiliF6bbg==}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
'@copilotkit/runtime-client-gql@1.56.4':
resolution: {integrity: sha512-XHE45+Gz5vlMNuTt8noF/0Pc22GqAO68oU4O/J2ktKjI25WUBTvakbS2cgXELhxJszum6JeFjMyMvQpurXQukg==}
'@copilotkit/runtime-client-gql@1.62.2':
resolution: {integrity: sha512-ZDPsMAFk6BFd0tfDJd7kbmxmMErdulcWLWaoVhWLhoBunNcKaq/V487vel3sOy5IkQPM2xia5Vh9GJeirMTo+w==}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
'@copilotkit/runtime@1.56.4':
resolution: {integrity: sha512-WLupYn1K3hK7xkBY4MdM2lIhoyrGf+3K68fTkRUVc7fa5gADI0WjbRtBJnNwSUPFOBpVTTPAokqFl2+GdC1oEw==}
'@copilotkit/runtime@1.62.2':
resolution: {integrity: sha512-sZg38j0MGYlIAMjdUBvkSPoOGfucV3pSHG3wne783l9LyQMrOjLpDoT1DKixdybJg+6sSv84iEJlsQhR7UXiEA==}
peerDependencies:
'@anthropic-ai/sdk': ^0.57.0
'@langchain/aws': '>=0.1.9'
@@ -363,13 +388,19 @@ packages:
openai:
optional: true
'@copilotkit/shared@1.56.4':
resolution: {integrity: sha512-sHmjRyZkmbHqhbK0OGT66gtCHAMA2AIWtNEFb2nwStzVu64a5dNhaayzpu7VbLpfkzeNzeWCPmZecdrN/EK1wQ==}
'@copilotkit/shared@1.62.2':
resolution: {integrity: sha512-MLSvQIq7q8Z3FbQaqcSYR2i5sDvKIconidCXLbmZYIwdfbjnjPFjHzJM8PU9WTxJsYwQ3iM6/EysEm+5+bV4yQ==}
peerDependencies:
'@ag-ui/core': '>=0.0.48'
'@copilotkit/web-inspector@1.56.4':
resolution: {integrity: sha512-0PS1AnrTHbmvctveYuRrMr+CQ1FqiRQFhUBcDhSmz2eINr00dOybqIr8xXW78LD6E/6kyuLpTfaGHQLtBftMNw==}
'@copilotkit/web-components@1.62.2':
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'}
'@date-fns/tz@1.5.0':
@@ -1103,15 +1134,6 @@ packages:
'@types/react':
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':
resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==}
peerDependencies:
@@ -1729,6 +1751,7 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@upsetjs/venn.js@2.0.0':
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
@@ -4152,6 +4175,7 @@ packages:
uuid@10.0.0:
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
uuid@11.1.0:
@@ -4303,17 +4327,20 @@ snapshots:
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:
'@ag-ui/client': 0.0.52
'@ag-ui/a2ui-toolkit': 0.0.4
'@ag-ui/client': 0.0.57
clarinet: 0.12.6
rxjs: 7.8.1
'@ag-ui/client@0.0.52':
'@ag-ui/a2ui-toolkit@0.0.4': {}
'@ag-ui/client@0.0.54':
dependencies:
'@ag-ui/core': 0.0.52
'@ag-ui/encoder': 0.0.52
'@ag-ui/proto': 0.0.52
'@ag-ui/core': 0.0.54
'@ag-ui/encoder': 0.0.54
'@ag-ui/proto': 0.0.54
'@types/uuid': 10.0.0
compare-versions: 6.1.1
fast-json-patch: 3.1.1
@@ -4322,19 +4349,42 @@ snapshots:
uuid: 11.1.0
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:
zod: 3.25.76
'@ag-ui/encoder@0.0.52':
'@ag-ui/core@0.0.57':
dependencies:
'@ag-ui/core': 0.0.52
'@ag-ui/proto': 0.0.52
zod: 3.25.76
'@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:
'@ag-ui/client': 0.0.52
'@ag-ui/core': 0.0.52
'@ag-ui/core': 0.0.54
'@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/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))
@@ -4352,9 +4402,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)(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:
'@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)
rxjs: 7.8.1
transitivePeerDependencies:
@@ -4362,9 +4412,25 @@ snapshots:
- supports-color
- 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:
'@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
'@protobuf-ts/protoc': 2.11.1
@@ -4513,19 +4579,21 @@ snapshots:
'@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:
'@a2ui/web_core': 0.9.0
clsx: 2.1.1
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
lit: 3.3.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:
'@ag-ui/client': 0.0.52
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52)
'@ag-ui/client': 0.0.57
'@copilotkit/shared': 1.62.2(@ag-ui/core@0.0.57)
'@tanstack/pacer': 0.20.1
phoenix: 1.8.5
rxjs: 7.8.1
@@ -4535,27 +4603,29 @@ snapshots:
- encoding
- 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:
'@ag-ui/client': 0.0.52
'@ag-ui/core': 0.0.52
'@copilotkit/a2ui-renderer': 1.56.4(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/runtime-client-gql': 1.56.4(@ag-ui/core@0.0.52)(graphql@16.13.2)(react@19.2.5)
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52)
'@copilotkit/web-inspector': 1.56.4(@ag-ui/core@0.0.52)(zod@4.3.6)
'@ag-ui/client': 0.0.57
'@ag-ui/core': 0.0.57
'@copilotkit/a2ui-renderer': 1.62.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@copilotkit/core': 1.62.2(@ag-ui/core@0.0.57)(zod@4.3.6)
'@copilotkit/runtime-client-gql': 1.62.2(@ag-ui/core@0.0.57)(graphql@16.13.2)(react@19.2.5)
'@copilotkit/shared': 1.62.2(@ag-ui/core@0.0.57)
'@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
'@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-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)
'@scarf/scarf': 1.4.0
'@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
clsx: 2.1.1
katex: 0.16.45
lit: 3.3.2
lucide-react: 0.525.0(react@19.2.5)
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
@@ -4578,16 +4648,17 @@ snapshots:
- micromark-util-types
- 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:
'@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/runtime-client-gql': 1.56.4(@ag-ui/core@0.0.52)(graphql@16.13.2)(react@19.2.5)
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52)
'@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.62.2(@ag-ui/core@0.0.57)(graphql@16.13.2)(react@19.2.5)
'@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)
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)
rehype-raw: 7.0.0
rehype-sanitize: 6.0.0
remark-gfm: 4.0.1
remark-math: 6.0.0
transitivePeerDependencies:
@@ -4603,9 +4674,9 @@ snapshots:
- supports-color
- 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:
'@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)
react: 19.2.5
untruncate-json: 0.0.1
@@ -4615,21 +4686,22 @@ snapshots:
- encoding
- 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:
'@ag-ui/a2ui-middleware': 0.0.5(@ag-ui/client@0.0.52)(rxjs@7.8.1)
'@ag-ui/client': 0.0.52
'@ag-ui/core': 0.0.52
'@ag-ui/encoder': 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/mcp-apps-middleware': 0.0.3(@ag-ui/client@0.0.52)(@cfworker/json-schema@4.1.1)(zod@3.25.76)
'@ag-ui/a2ui-middleware': 0.0.10(@ag-ui/client@0.0.57)(rxjs@7.8.1)
'@ag-ui/client': 0.0.57
'@ag-ui/core': 0.0.57
'@ag-ui/encoder': 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))
'@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/google': 3.0.64(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/openai': 3.0.53(zod@3.25.76)
'@copilotkit/license-verifier': 0.2.0
'@copilotkit/shared': 1.56.4(@ag-ui/core@0.0.52)
'@copilotkit/license-verifier': 0.5.0
'@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)
'@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)
@@ -4676,11 +4748,11 @@ snapshots:
- vue
- 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:
'@ag-ui/client': 0.0.52
'@ag-ui/core': 0.0.52
'@copilotkit/license-verifier': 0.2.0
'@ag-ui/client': 0.0.57
'@ag-ui/core': 0.0.57
'@copilotkit/license-verifier': 0.5.0
'@segment/analytics-node': 2.3.0
'@standard-schema/spec': 1.1.0
chalk: 4.1.2
@@ -4692,10 +4764,14 @@ snapshots:
transitivePeerDependencies:
- 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:
'@ag-ui/client': 0.0.52
'@copilotkit/core': 1.56.4(@ag-ui/core@0.0.52)(zod@4.3.6)
lit: 3.3.2
'@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
lucide: 0.525.0
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)
mustache: 4.2.0
p-queue: 6.6.2
zod: 3.25.76
zod: 4.3.6
transitivePeerDependencies:
- '@opentelemetry/api'
- '@opentelemetry/exporter-trace-otlp-proto'
@@ -5003,14 +5079,14 @@ snapshots:
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:
'@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)
'@standard-schema/spec': 1.1.0
uuid: 10.0.0
zod: 3.25.76
zod: 4.3.6
optionalDependencies:
zod-to-json-schema: 3.25.2(zod@4.3.6)
transitivePeerDependencies:
@@ -5379,13 +5455,6 @@ snapshots:
optionalDependencies:
'@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)':
dependencies:
'@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)):
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/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))
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:
- '@opentelemetry/api'
- '@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 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
return new Observable<any>((observer) => {
// 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 (!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);
} 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 (type === EventType.TOOL_CALL_END && id) {
open.delete(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
@@ -192,23 +251,30 @@ class StripModelArtifactsMiddleware extends (Middleware as any) {
}
// ── ReconcileSnapshotMiddleware ──────────────────────────────────────────────
// MAF's `_build_messages_snapshot` (agent_framework_ag_ui/_agent_run.py:686)
// mints a fresh UUID for the post-tool-call assistant text instead of reusing
// the streamed TEXT_MESSAGE_START id. The resulting MESSAGES_SNAPSHOT then
// contains TWO assistant entries: the streamed id (holding the toolCalls) and
// a brand-new id (holding the duplicated text). ag-ui's snapshot merge replaces
// by id then APPENDS unknown ids, so the browser ends up with two assistant
// bubbles for the same answer. Dropping the snapshot entirely fixes the dupe
// but breaks render_a2ui card persistence (cards rely on the snapshot to keep
// the assistant-with-toolCalls message in state past the run). The right fix
// 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`.
// MAF 1.10 reuses the streamed id for PLAIN-text snapshots, but still mints a
// fresh UUID for post-tool-call assistant text — deliberately, per their
// issue #3619 ("separate message"). ag-ui's snapshot merge appends unknown
// ids, so tool+text turns still produce a duplicate assistant bubble without
// this reconciliation (re-verified live on MAF 1.10 / CK 1.62, 2026-07-04).
// Drops only the orphan text-only assistant message with no streamed
// counterpart. Caveat: interfered with a2ui retry runs (duplicated the user
// message) — a2ui is disabled while that pipeline matures; revisit both
// together.
//
// 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
class ReconcileSnapshotMiddleware extends (Middleware as any) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
run(input: any, next: any): Observable<any> {
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
return new Observable<any>((observer) => {
@@ -223,7 +289,7 @@ class ReconcileSnapshotMiddleware extends (Middleware as any) {
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
const msgs: any[] = Array.isArray(event?.messages) ? event.messages : [];
const filtered = msgs.filter((m) => {
@@ -233,7 +299,12 @@ class ReconcileSnapshotMiddleware extends (Middleware as any) {
const hasToolCalls =
(Array.isArray(m?.toolCalls) && m.toolCalls.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 true;
@@ -261,7 +332,7 @@ class ReconcileSnapshotMiddleware extends (Middleware as any) {
// below the card. This middleware buffers TEXT_MESSAGE_* events and discards
// 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
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 });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
agent.use(new SuppressRenderToolTextMiddleware() as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
agent.use(new StripModelArtifactsMiddleware() as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
agent.use(new ReconcileSnapshotMiddleware() as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
agent.use(new DeduplicateToolCallMiddleware() as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
agent.use(new DebugLogMiddleware() as any);
// CK_MIDDLEWARES=off runs the stack bare — used to audit which of these
// workarounds are still needed after CopilotKit/MAF upgrades.
/* eslint-disable @typescript-eslint/no-explicit-any */
if (process.env.CK_MIDDLEWARES !== "off") {
agent.use(new SuppressRenderToolTextMiddleware() as any);
agent.use(new StripModelArtifactsMiddleware() as any);
agent.use(new ReconcileSnapshotMiddleware() as any);
agent.use(new DeduplicateToolCallMiddleware() 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({
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 }) => {
if (outbound.method !== "POST") return;
const ct = outbound.headers.get("content-type") ?? "";
if (!ct.includes("application/json")) return;
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;
const paired = pairOrphanToolCalls(body.messages);
if (paired.length === body.messages.length) return;
// An empty messages array is MAF's hydration trigger (known threadId
// + 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, {
method: outbound.method,
headers: outbound.headers,
body: JSON.stringify({ ...body, messages: paired }),
body: JSON.stringify({ ...body, messages }),
});
} catch (err) {
// Skipping the repair is survivable; doing it silently is not —
// 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;
}
},

View File

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

View File

@@ -27,11 +27,14 @@ export default function Layout() {
const { privacyMode, togglePrivacy } = usePrivacy();
const { pathname } = useLocation();
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 (
<SidebarProvider>
<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">
<SidebarTrigger className="-ml-1" />
<Separator

View File

@@ -345,6 +345,23 @@ export interface paths {
patch: operations["update_category_api_v1_categories__category_id__patch"];
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/": {
parameters: {
query?: never;
@@ -1200,6 +1217,11 @@ export interface components {
/** Name */
name?: string | null;
};
/** ClearThreadResponse */
ClearThreadResponse: {
/** Cleared */
cleared: boolean;
};
/**
* Currency
* @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: {
parameters: {
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 {
CopilotChat,
@@ -7,10 +7,17 @@ import {
useCopilotKit,
} from "@copilotkit/react-core/v2";
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 { 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 = {
available: "before-first-message" as const,
suggestions: [
@@ -24,28 +31,66 @@ const STATIC_SUGGESTIONS = {
export default function Asistente() {
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 navigate = useNavigate();
const { agent } = useAgent({ agentId: "wealthysmart" });
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(() => {
if (!agent || bootRef.current) return;
bootRef.current = true;
const ask = (location.state as { ask?: string } | null)?.ask;
if (!ask || sentRef.current || !agent) return;
sentRef.current = true;
// Clear the state so back/refresh doesn't re-send the question.
navigate(location.pathname, { replace: true, state: null });
agent.addMessage({
id: crypto.randomUUID(),
role: "user",
content: ask,
});
void copilotkit.runAgent({ agent });
if (ask) {
// Clear the state so back/refresh doesn't re-send the question.
navigate(location.pathname, { replace: true, state: null });
}
void (async () => {
agent.threadId = CHAT_THREAD_ID;
if (agent.messages.length === 0) {
// Empty-messages run = MAF's hydration trigger: replays the stored
// snapshot as MESSAGES_SNAPSHOT without invoking the LLM.
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
}, [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({
name: "render_spending_summary",
description:
@@ -92,12 +137,35 @@ export default function Asistente() {
icon={Sparkles}
title="Asistente"
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>
{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">
<CopilotChat
className="h-full"
threadId={CHAT_THREAD_ID}
labels={{
modalHeaderTitle: "WealthySmart",
welcomeMessageText: "¿Qué quieres saber sobre tus finanzas?",