93 Commits

Author SHA1 Message Date
ryan 61acd85954 Merge pull request 'Only presign and accept images in the caller's own conversations' (#81) from fix/image-ownership into main
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
2026-10-13 07:53:05 -04:00
Ryan Chen c329d15f7f Prune LangGraph checkpoints per thread and delete them with the conversation
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
Every graph step stores a full snapshot of the conversation, nothing pruned
old ones, and deleting a conversation left its thread behind (about 1.6 MB per
checkpoint at 200 turns). After each turn, only the newest
CHECKPOINT_KEEP_PER_THREAD (default 20) checkpoints of that thread are kept,
plus their pending writes; state reads only use the newest, and pruning a
200-checkpoint thread takes about 2 ms. Deleting a conversation also deletes
its thread, without failing the request if the checkpoint store errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:41:47 -04:00
Ryan Chen b8ccc88b58 Give the model full retrieved chunks instead of 400-character snippets
CI / backend (pull_request) Successful in 41s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 6s
Compaction replaces a tool's text with its artifact, and the search artifact
only carries UI snippets, so the full chunk the retriever found never reached
the model. Search tools now attach the full text as model_context, which
compaction prefers and shortens under budget pressure while keeping source
ids; the persisted artifact and UI are unchanged. The per-chunk header drops
the raw metadata dict (about a fifth of search output tokens) for title, date
and source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:40:52 -04:00
Ryan Chen ef3a438048 Answer concisely by default and drop prompt text the tool schemas repeat
CI / backend (pull_request) Successful in 41s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
The local model is decode-bound, so output length drives latency. The system
prompt asked for "thorough, detailed answers" and to "think things through",
which lengthened both answers and reasoning. It now leads with the answer and
expands only on request. Per-integration tool lists that duplicated the bound
tool docstrings collapse to one paragraph of routing and duplicate-avoidance
rules. The prompt drops from about 1,446 to 897 tokens and stays static, so
the llama-server prompt cache still hits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:40:17 -04:00
Ryan Chen 6f093e3f6f Only presign and accept images in the caller's own conversations
GET /api/conversation/image/<key> presigned any S3 key for any logged-in
user, and /upload-image wrote into whatever conversation_id the form named.
Both now require the conversation in the key (or form) to belong to the
caller and return 404 otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:18:41 -04:00
Ryan Chen 5809ad3adb Add durable chat runs, shared agent runtime, reliable jobs, history tools and citations
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s
Shared runtime
- One agent entry point (runtime.invoke_agent_for_user / prepare_agent_turn) for
  web, email, iMessage, WhatsApp, scheduled and cron, so identity, permissions,
  memories, summaries and metrics match across channels.
- Serialize turns per checkpoint thread; /query validates input and returns 409
  while a durable run owns the conversation.

Reconnectable chat (durable runs)
- POST /runs creates an idempotent, owner-scoped run; GET .../events replays an
  ordered SSE log by cursor, so generation survives refresh and device switches.
  Stop cancels; disconnect only detaches. /stream-query is a compatibility adapter.
- Coalesce streamed deltas (150 ms) and wake same-process subscribers instead of
  polling; keep buffered text when a run is stopped.
- Drain runs on shutdown; serve via Hypercorn with a shutdown timeout covering
  the drain (and no longer in debug mode). CHAT_RUN_* settings are forwarded.

Scheduled messages and cron
- Atomic delivery claims, attempt history and explicit retries; ambiguous sends
  become "uncertain" and are never resent automatically.
- Cron triggers need a run_id/Idempotency-Key; legacy jobs need an explicit owner.
- Cancelling never rewrites a started delivery; series stop via tombstones.
  Legacy sent rows are not forked into new series. Malformed bodies return 400.

History, integrations and retrieval
- Message-content search, pinned conversations, JSON/Markdown export and a
  memory editor.
- Integration status panel and YNAB data-freshness provenance.
- Date-filtered document/note search with stable citations and a source viewer.

Migration 20 is generated by aerich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:11:04 -04:00
ryan 24f6764c64 Merge pull request 'Run image decoding and resizing off the event loop' (#75) from perf/offload-image-processing into main 2026-10-12 23:06:40 -04:00
ryan b2a53821f8 Merge pull request 'Index conversation, message, memory and artifact foreign keys' (#76) from perf/fk-indexes into main 2026-10-12 23:06:09 -04:00
Ryan Chen 266c6a8d79 Drop dropped-turn excerpts when the summary ledger is included
The ledger supersedes the "Earlier conversation excerpts" message, as on
main; sending both duplicated dropped-turn content. Removal is consistent
whenever the ledger is included, so the cached prefix stays stable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:05:21 -04:00
Ryan Chen 1e638e1e3b Place conversation evidence before the current turn for prompt caching
The summary ledger is regenerated every turn. At the head of history it
invalidated the cached retained turns on every request once trimming
started. Insert it immediately before the current turn's HumanMessage so
system, tools and retained history stay byte-identical; excerpts of dropped
turns stay at the head since they only change when the cut moves.

Reserve a fixed ledger ceiling (SUMMARY_RESERVE_TOKENS) rather than its
current size, so a shrinking ledger cannot pull the cut back and change the
retained prefix; fall back to the actual size for tight budgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:04:24 -04:00
Ryan Chen 891f44c1a9 Keep the chat prompt prefix stable for llama-server prompt caching
llama-server only reuses a byte-identical prompt prefix. Several things
changed the prefix every turn, forcing most of the prompt to be re-prefilled:

- Memories: rank still chooses which memories fit the budget, but the chosen
  ones are emitted in stored order, so the block is identical across queries.
- History trimming: the conversation-evidence summary is only included when
  turns were actually dropped (it duplicated retained turns otherwise). On
  overflow, old turns are dropped at stable cut points down to ~65% of the
  budget, so the retained prefix holds for several turns instead of shifting
  by one turn every request.
- Tools: TOOL_ROUTING_MODE=static (default) binds the full role-appropriate
  tool set in registration order with no discovery tool; admin gating is
  unchanged. TOOL_ROUTING_MODE=dynamic keeps the keyword router.
- Observability: per-call model_calls in the stream_request log now include
  cached_prompt_tokens when the provider reports cache reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:01:46 -04:00
Ryan Chen 746aaa4844 Index conversation, message, memory and artifact foreign keys
Postgres had only primary keys on these tables, so loading a
conversation, listing conversations, checking for a first message on
every send, and fetching memories/artifacts were sequential scans.
Migration generated with aerich; index-only apart from the no-op JSONB
re-casts aerich emits on every run (as in migrations 16 and 18), which
were checked not to rewrite tables. Upgrade and downgrade verified on a
throwaway pgvector database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:57:35 -04:00
Ryan Chen 68a3085ac1 Run image decoding and resizing off the event loop
process_image takes ~560ms on a 12MP photo and image_content_block
decodes HEIC/JPEG with PIL; both ran synchronously inside async
handlers, freezing every concurrent chat stream. Run them via
asyncio.to_thread in the upload handlers, the chat stream, and iMessage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:56:13 -04:00
Ryan Chen 4645920c64 Route tools and retain structured conversation context with durable evidence 2026-10-12 20:19:12 -04:00
Ryan Chen 1c9405cfdf Budget conversation context while preserving current turns and tool exchanges 2026-10-12 19:45:51 -04:00
Ryan Chen 2cb131bbd2 Prevent Quart from cancelling active chat streams after 60 seconds 2026-10-12 19:38:04 -04:00
Ryan Chen 4ce254193e Treat thinking as progress in stream timeout; trim replayed history
The 30s time-to-first-token guard measured from the turn's first model call
to the first answer token, so thinking, tool execution and the second call's
prefill all counted against it. Long reasoning on a long conversation was
cancelled mid-generation (llama-server: stop: cancel task at n_gen=1287).

- stream_query: the timeout now fires only when an in-flight model call goes
  silent for TTFT_TIMEOUT_SECONDS. Any chunk (thinking or empty) resets the
  clock, each model call starts a fresh one, and time spent in tools between
  calls doesn't count.
- Trim replayed history (blueprints/conversation/context.py): tool results
  and user messages from earlier turns are capped at 4000 chars (attachments
  included) and their images replaced by a placeholder. The current turn is
  untouched. The window no longer starts with orphaned ToolMessages.
- Log per-call prefill time, duration and prompt/output tokens
  (stream_usage=True) plus max_prompt_tokens in the stream_request line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:14:02 -04:00
Ryan Chen cc29ae1ef8 Extract attachment text off the event loop; reject text-less files
- extract_text (PyMuPDF for PDFs) now runs via asyncio.to_thread in both
  /upload and stream-query, so a large PDF doesn't stall other streams.
- /upload returns 422 when extraction yields no text: a scanned PDF
  without a text layer, or an empty text file.
- Mock server: *scan*.pdf uploads return that 422.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:43:10 -04:00
Ryan Chen b2f0b9f9f1 Add conversation file upload and file_key in stream-query
POST /api/conversation/upload takes an image (via process_image) or a
PDF/txt/md/csv file, checks the conversation belongs to the caller, and
stores files at conversations/<id>/files/<uuid>.<ext> with the original
name in S3 metadata. Anything else gets a 415.

stream-query accepts file_key (must sit under this conversation's
files/ prefix), extracts its text capped at 12k chars, appends it to the
agent's user turn as <attached file> and records 📎 name on the saved
message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:41:56 -04:00
Ryan Chen 20ec7ca8b6 Return artifacts from YNAB budget, transaction and spending tools
Convert ynab_budget_summary, ynab_search_transactions, ynab_spending_report
and ynab_category_budget to content_and_artifact (ynab.summary,
ynab.transactions, ynab.spending_report, ynab.category_budget). The text
the LLM sees is unchanged. Not-synced, bad-input, empty and error paths
return no artifact; Decimals become floats and dates ISO strings.

ynab_search_transactions now fetches up to 500 rows in one query (with an
id tie-break so ordering is stable) and lists the first `limit` in the
text; the artifact carries all of them. ynab_account_balances,
ynab_recent_changes and ynab_sql stay text-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:12:23 -04:00
Ryan Chen 50d123e4d2 Return artifacts from Mealie, Obsidian and search tools
Convert mealie_get_recipe, mealie_meal_plans, mealie_todays_meals,
mealie_shopping_lists, obsidian_read_note, journal_get_today,
obsidian_search_notes and simba_search to content_and_artifact. The text
the LLM sees is unchanged; the artifact is a JSON-safe ToolArtifact dict
per the Cowork UI contract (mealie.recipe, mealie.meal_plan,
mealie.shopping_lists, obsidian.note, search.documents). Error, empty and
not-configured paths return no artifact.

simba_search's artifact changes from a list of Documents (unused) to a
search.documents dict.

New optional env vars: MEALIE_GROUP_SLUG (default "home") for Mealie v2
recipe links, OBSIDIAN_VAULT_NAME for obsidian://open links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:10:07 -04:00
Ryan Chen 0fa30edb82 Merge origin/main into feat/ynab-mirror
Resolve config/db.py (artifacts + ynab models) and regenerate the YNAB
migration with aerich as 18 on top of main's 17_add_artifacts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:07:26 -04:00
Ryan Chen 10f3f1bcab Merge origin/main into feat/ynab-mirror
Resolve the Makefile .PHONY conflict (desktop + ynab targets) and
regenerate the YNAB migration with aerich as 17 on top of main's
16_add_oidc_states, replacing the colliding 16_add_ynab_mirror.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:06:47 -04:00
Ryan Chen 1c1ae9f158 Validate artifact ids before querying; match artifacts by str id
asyncpg raises OperationalError (not ValueError) on malformed UUIDs, so
bad artifact ids now 404 and a bad conversation_id 400s before any query.
get_conversation keys artifacts by str(message_id) so matching doesn't
depend on the driver's UUID types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:56:26 -04:00
Ryan Chen c678bf55bc Stream v2 tool events, save artifacts, enrich conversation payload
stream_query emits the new tool_start/tool_end payloads, maps
on_tool_error to tool_end status=error, and saves an artifact linked to
the tool message. GET /api/conversation/<id> adds tool, label and
artifact to tool messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:53:22 -04:00
Ryan Chen d2f21a6d23 Add tool label registry and SSE v2 tool event shaping
Pure functions build tool_start/tool_end payloads with run_id, friendly
label, truncated input (config dropped), status, duration and artifact ref.
Labels cover every agent tool, both old and #55 YNAB names, and the
homebox_/bookstack_ MCP prefixes, falling back to "Using <tool>…".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:53:22 -04:00
Ryan Chen bc3e3d2797 Accept access tokens on protected routes; persist OIDC state
- Add transitional @jwt_access_required (accepts access tokens, and for now
  also refresh tokens) and use it on every protected route and inside
  admin_required. /api/user/refresh stays refresh-only.
- Move pending OIDC state/PKCE verifiers from an in-process dict to an
  oidc_states table (10 min TTL, single-use) so logins survive a blue/green
  flip. Drop the unused redirect_after_login.
- Unit tests for the decorator and the state store.

Deploy this commit on its own, before the access-token frontend: the old
backend answers access tokens with 422, so the new frontend only works once
this backend is live on both colors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:44:34 -04:00
Ryan Chen 8f02318542 Mirror YNAB budget into Postgres with delta sync and change log
YNAB has no event feed, so keep a local copy in ynab_* tables synced via
server_knowledge delta requests: full download on first run, deltas every
YNAB_SYNC_INTERVAL seconds and on demand before each tool call, full
resync daily. Each sync diffs against the mirror and appends real edits
(transactions, assignments, renames) to ynab_changes, the edit history
YNAB doesn't keep.

Agent YNAB tools now answer from the mirror: budget summary (spending
computed from transactions, since YNAB's month activity nets in credit
card payment categories), account balances, transaction search with
splits expanded, spending reports by category/payee/account/month,
category budget, and recent changes. Adds an admin-only read-only
ynab_sql tool guarded by a sqlglot allowlist (scope-resolved tables, no
dangerous functions), a READ ONLY transaction, a timeout, and a row cap.

Replaces utils/ynab_service.py and drops the unused ynab SDK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:32:38 -04:00
Ryan Chen b1e210de5d Expose Homebox set_item edit tool to admin agent
Allowlist homebox-mcp's general item editor so the agent can rename items
and edit description, notes, quantity, purchase info, flags and custom
fields. Only passed args change; the rest of the item is preserved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:02:17 -04:00
Ryan Chen 0b8fe7b2df Add admin-only BookStack MCP connector
Load an allowlisted subset of bookstack-mcp tools (search/read plus
create/update books, chapters, pages and comments; no deletes) over stdio
and add them to the admin agent alongside Homebox. Extract the shared
allowlist + is_admin wrapping into utils/mcp_tools.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:54:01 -04:00
Ryan Chen 9858e06b73 Add admin-only Homebox MCP connector
Load an allowlisted subset of homebox-mcp tools (read + safe writes, no
deletes) over stdio and compile them into a separate admin agent, so
non-admins never see them. Each tool also re-checks is_admin per call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 11:01:19 -04:00
Ryan Chen 4d29bb3ca4 Pin llama-server reasoning_effort to low to fix Jinja template error
LangChain was sending reasoning_effort=thinking which Qwen3's chat
template rejects (only xhigh/medium/low are valid). low disables
thinking mode, which is appropriate for agentic tool use and saves tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-18 07:09:11 -04:00
Ryan Chen 64b20754a1 Trim all tool docstrings to reduce token count per request
Removed verbose "Use this tool when the user asks about" bullet lists
from all 31 tools. Each tool now has a 1-2 line description plus compact
arg docs. Saves ~3-5k tokens per request on a 32k context budget.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-18 07:04:26 -04:00
Ryan Chen 6fb9bade08 Trim conversation history to last 12 messages to prevent context overflow
The LangGraph checkpointer replays full history on every request. Without
trimming, long conversations compound past the 32k llama-server slot limit.
12 messages covers ~4-6 turns including tool call/result pairs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-18 06:56:12 -04:00
Ryan Chen 70475865cc Sample up to 3 top picks per friend for friend restaurant queries
Previously ordered by friend_beli_user_id first, returning 15 results
from whoever sorts first. Now fetches top-ranked rows globally and
caps at 3 per friend, giving a diverse cross-friend sample.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 22:49:34 -04:00
Ryan Chen 98b4b46b11 Add friends_only filter to beli_search_restaurants tool
Replaces include_friends with friends_only so the LLM can return
exclusively friend-sourced rows when user asks what their friends like,
rather than mixing in own rankings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 22:44:28 -04:00
Ryan Chen 834c2c5bfe Drop beli result cap to 15 to stay under 32k context
30 still overflowed when combined with system prompt and conversation history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 22:39:31 -04:00
Ryan Chen b7ac24f2ff Cap beli_search_restaurants results at 30 to prevent context overflow
50k+ token requests were blowing past the 32k llama-server context window
when returning the full ranked list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 22:36:25 -04:00
Ryan Chen 69bf5edefe Add manual friend refresh endpoint and fix category filter guidance
POST /api/beli/profiles/<id>/refresh-friends triggers friend ranking
sampling on demand without waiting for the nightly cron. Also tightens
the beli_search_restaurants docstring so the LLM passes category=restaurant
when the user asks for restaurants, preventing bars/bakeries from appearing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 22:29:52 -04:00
Ryan Chen c682d2b320 Add beli_category (RES/DES/BAR/BAK) to restaurant records
Store Beli's category code on each ranking so the agent can filter by type
(restaurant/bar/cafe/bakery). Category label shown in tool output.

Requires migration for beli_category column + updated unique constraint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 22:09:09 -04:00
Ryan Chen b79229a5b0 Add random friend ranking refresh to Beli integration
Each nightly profile refresh now also picks FRIEND_SAMPLE_SIZE (5) random
followed users and fetches their rankings, building up a picture of friends'
lists over time. Rows are tagged with friend_beli_user_id/friend_name so the
agent tool can surface attribution ("via Alex").

Requires migration to add friend_beli_user_id/friend_name columns and update
unique_together on beli_restaurants.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 20:52:14 -04:00
Ryan Chen d23273590f Add Beli restaurant recommendation integration
Multi-tenant system to fetch and store restaurant lists from Beli accounts.
Each user can connect up to 3 Beli profiles; a nightly Cronicle job (1–5 AM,
randomised) refreshes the 3 stalest profiles using the reverse-engineered Beli
API (mirrored at git.torrtle.co/ryan/beli-api). Rankings (been) and bookmarks
(want-to-try) are stored in Postgres and queryable via a new LangChain agent
tool. Beli passwords and refresh tokens are stored Fernet-encrypted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 19:36:16 -04:00
Ryan Chen 2542fa2342 Fix: capture main_agent before SSE generator to avoid lost app context
Quart exits the request/app context before iterating the SSE response
body, so current_app inside event_generator() raised "Not within an app
context". Capture the reference before the generator is defined. Also
add return after the exception error yield to prevent falling through to
a second "No response generated" event on the same stream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-17 07:43:52 -04:00
Ryan Chen e36bf35b2a Add ynab_category_allocations tool and fix category spending cap
Add get_category_allocations() to YNABService that fetches all categories
grouped by category group (using the categories API for group structure),
showing assigned and available amounts for every non-hidden category.

Add the corresponding ynab_category_allocations agent tool so the LLM
can answer questions about per-category budgets even when a category has
no spending activity this month.

Also remove the [:10] cap in ynab_category_spending so all spending
categories are returned — previously categories ranked below 10th by
activity (e.g. Coffee slush fund) were silently dropped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 23:19:17 -04:00
Ryan Chen 19aa2b17d7 Fix _build_prompt receiving state dict instead of messages list
LangGraph passes the full agent state dict {"messages": [...]} to the
prompt callable, not just the messages list. Iterating a dict yields
its keys, so "messages" (the string) was getting appended to the LLM
input — causing the agent to respond as if the literal word "messages"
was the user's query.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 23:10:22 -04:00
Ryan Chen 43e0de24d6 Restore dynamic system prompt via RunnableLambda prompt parameter
Uses prompt=_build_prompt (a RunnableLambda) so LangGraph passes the
runtime config through, allowing per-request system_prompt injection.
Restores user memory loading for web and iMessage plain-text enforcement.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 23:04:43 -04:00
Ryan Chen 810d32dcc3 Fix: store agent on app object, access via current_app
Global module variable was unreliable across import boundaries. Storing
the compiled agent on app.main_agent and reading it via current_app is
the idiomatic Quart pattern and guaranteed to reflect the initialized value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 22:55:39 -04:00
Ryan Chen 6621a99149 Fix: access main_agent via module reference to avoid None at import time
from-import captures the None value set at module load; importing the module
and accessing .main_agent at call time gets the value set by build_agent().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 22:51:18 -04:00
Ryan Chen 801a90746b Fix: replace deprecated state_modifier with prompt in create_react_agent
state_modifier was removed in this version of LangGraph; use the prompt
parameter with a static system prompt string instead. Dynamic memory
injection via configurable is deferred — memories are still saved by the
save_user_memory tool, just not pre-loaded into the system prompt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 22:43:39 -04:00
Ryan Chen 202baa17e2 Fix: use langgraph.prebuilt.create_react_agent for state_modifier support
langchain.agents.create_agent does not accept state_modifier or checkpointer;
those are LangGraph-only params available on create_react_agent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-16 22:37:11 -04:00