brain/knowledge/flows-execution/chat.md
A platform-level AI chat assistant that manages Activepieces projects via natural language. Streams LLM responses over WebSocket and exposes project resources (flows, tables, connections, runs) as callable tools through the project's MCP server. Conversations persist per-user with cross-session memory (personal instructions + remembered facts injected into every turn), compaction, attachments, multi-project context, and two-phase tool gating. EE/Cloud only (not registered in CE).
The chat LLM loop runs in the worker, not the API. Send path: agent-conversation-controller.ts (POST /conversations/:id/messages) enqueues a WorkerJobType.EXECUTE_AGENT_RUN job → worker execute-agent-run.ts calls getAgentConfig RPC, assembles tools, runs run-agent-turn.ts (shared streamText() DI loop) → chunks stream back via sendAgentEvent RPC → websocket CHAT_MESSAGE_CHUNK (filtered by runId) → frontend reducer. agent-conversation-service.ts only does conversation CRUD + persistence.
chat_personalization) — first-run onboarding: role + company, background research, researched empty-state cards. See chat personalization.agent_conversation) — per-user, per-platform, optionally per-project; status STREAMING/IDLE/ERROR, activeRunId, messages (ModelMessage[] JSONB), uiMessages, summary/summarizedUpToIndex for compaction.chat_rollout_user) — cloud rollout cohort; chattedAt drives the cap.user_memory) — one row per (platformId, userId): instructions (nullable text) + memories (jsonb string[]); capped at 50 facts × 280 chars and 4000 chars of instructions (agentHelpers.capMemories).ee/agent/; shared tool phase/classification in core/shared/.../ee/agent/.ap_execute_action, ap_select_project, ap_load_guide, ap_fetch_url, ap_set_phase…), display cards (ap_show_connection_picker, ap_show_questions, ap_show_quick_replies…), and project-scoped MCP tools. Each tool is wired across up to four files — see gotcha: a chat tool lives in four files.discovery vs build; a denylist hides build-only tools during discovery to shrink the surface. ap_set_phase flips it; auto-widens if a build tool fires.web_search_20250305, Google grounding, OpenRouter web plugin); ap_fetch_url works everywhere.buildMemoryNote in agent-rpc-handlers.ts). Writes go through agentMemoryAi.applyInstruction — an LLM reconcile on the fast-tier model (add/forget, dedupe, supersede contradictions; non-AI fallbacks so it never hard-fails) — used by both the ap_remember tool and the /v1/chat/memory[/import|/instruct] endpoints; concurrent saves are merged under a pessimistic_write lock. UI lives in the settings hub (packages/web/src/app/components/settings-hub/).POST /conversations/:id/messages gates pre-enqueue — assertCreditsAndAppSumoNotExceeded blocks ALL chat (any provider) on the platform's credit/AppSumo balance (QUOTA_EXCEEDED), next to a per-user rate limit (40 messages / 10 min, HTTP 429). After each turn chatUsageTracker.track meters Autumn credits with creditValue = creditWeight + billableToolCalls (tier's weight for the managed ACTIVEPIECES provider, default 2; 1 for BYO), idempotency key {conversationId}:chat:{turnIndex} (CreditUsageSource.CHAT), plus the AppSumo meter on AppSumo plans; it then emits the PostHog chat_message billing event (skipped when the platform has no license key — the Autumn tracking always runs). chat-tool-billing.ts decides which tool calls bill: every mcp__ tool plus a fixed set (ap_web_search, ap_scrape_url, ap_generate_image, ap_execute_action, ap_explore_data, ap_run_code).A turn is kept alive / reclaimed by three separate mechanisms in execute-agent-run.ts; confusing them causes "chat randomly stops" bugs:
HEARTBEAT_INTERVAL_MS 15s): a setInterval that bumps conversation.updated (via heartbeatAgentConversation RPC) + sends an empty keepalive chunk, so a live-but-slow turn is never reclaimed as stale.STREAMING_STALENESS_TIMEOUT_MS 90s, agent-helpers.ts): on-read (getConversationOrThrow) + a per-minute sweep flip any STREAMING conversation whose updated is >90s old back to IDLE. The heartbeat is what holds this off.STREAM_IDLE_TIMEOUT_MS 90s, in streamChunksToClient): aborts the turn if the drain-stream reader is silent 90s. It must be SUSPENDED while legitimate silent work is in flight — pending tool calls AND in-flight reasoning (reasoning-start→reasoning-end). Reasoning-awareness was missing and caused the bug where long "thinking" on the Expert tier randomly aborted a healthy turn (a >90s gap between reasoning deltas looked like a wedge). Backstop for a genuine mid-reasoning wedge is MAX_TURN_WALL_CLOCK_MS (20 min).Server-managed connections: the LLM never sees connection externalIds; ap_execute_action auto-fills them from a Redis store.
Prompt-injection taint: a per-turn taintState flips to tainted after consuming untrusted content (ap_fetch_url/ap_scrape_url/ap_web_search/ap_explore_data), which then forces the action-preview gate on any non-read-only action, ignoring the model's needsConfirmation.
Write-check gate: before a live ap_test_flow, __flow_write_check RPC flags write/destructive PIECE steps; read-only flows run ungated; gate fails open on RPC error.
Cloud rollout cap: opens to non-embed users without chatEnabled until 200 distinct users have sent a message (CLOUD_CHAT_ROLLOUT_CAP); grandfathered after close. Embedded sessions never see chat.
Flow correctness is 100% prompt/guide-driven — nothing in code enforces it. The "#1 silent bug" ("Class A"): the agent frames a recurring automation as a one-time task and omits any anti-reprocessing step, so run N+1 redoes run N's work (re-pays, re-sends). It's a design-time reasoning gap, not a testing gap — ap_test_flow runs ONCE, so a single test looks perfect; the bug only shows on the 2nd run. Fix lives in the prompt (chat-system-prompt.md <decision_framework> + build_flow.md "Recurring flows must not reprocess") + capability eval fixtures with a recurring_avoids_reprocessing judge dimension. The platform already has every primitive (Tables New-Record webhook, polling DedupeStrategy, _dedupe_key, Store, update/delete-record); the agent just wasn't reaching for them. Watch the build_flow.md "don't over-build" bias — it once actively discouraged the fix.
The context budget ignores tool schemas and reserved max_tokens. Anthropic/OpenRouter count both against the 200k window; agent-compaction.ts budgets neither. It trims history to COMPACTION_THRESHOLD (0.7) × 200_000 = 140_000 and its fit check looks only at message chars, while run-agent-turn.ts sets maxOutputTokens: tier.thinkingBudget + 32_000 → 52k reserved on premium, plus ~12k of tool schemas (62 tools, 41 via MCP). 140k + 12k + 52k = 204k, so a conversation that compacts to just under the threshold still 400s with "maximum context length is 200000 tokens" — and it gets retried ~6× (streamText maxRetries: 3 × MAX_STREAM_RETRIES), burning ~20s per turn. maxOutputTokens is set at the streamText call level, so the full thinking budget stays reserved even on step one where prepareStep disables thinking and swaps in haiku-4.5 (real case: 148_628 text + 11_872 tool + 52_000 output = 212_500; dropping the unused 20k reservation alone would have fit). ESTIMATED_TOKENS_PER_MESSAGE = 200 also sizes the recent window by message count, so a 12-message history holding ~235k tokens of uploaded documents summarized only 1 message. When budgeting, subtract the reserved output window and tool-schema size from getMaxContextTokens, and don't reserve thinkingBudget on a thinking-disabled step.
A write tool in BUILD_ONLY_TOOL_NAMES is only reachable if something flips the phase for it. The denylist is the consistent home for anything that writes (ap_create_flow, ap_create_table, ap_lock_and_publish are all in it), but the only route out of discovery is ap_set_phase, whose description tells the model to switch when it starts building an automation. A tool for a subject with no build guide and no sibling build-only call, the agent-building tools being the case that found this, becomes invisible in any conversation that never builds a flow: activeToolsForPhase filters it out and the prompt names no tool, so the model cannot discover that it exists. Classifying by "does it write" is not enough; check what would actually flip the phase in a conversation about that subject. The four agent write tools (ap_create_agent, ap_update_agent, ap_add_agent_tool, ap_remove_agent_tool) were added to the set and then reverted for exactly this, with a test pinning the choice; ap_list_agents is a read and was never in it, so the group is five tools and only four were ever candidates.
Capability notes are built where discoveryOnly is not known, so a prompt can promise tools the worker has stripped. getAgentConfig composes the system prompt in the api, while discoveryOnly rides on the job data; before Aug 2026 it never crossed that boundary. Meanwhile the worker strips image tools, email tools and the agent tools on such a run, so the notes claimed all three. Two of the three had been wrong since long before anyone noticed, because each note computed its own availability term. The flag now travels with the config request and the three notes read one shared actingRun = !dryRun && !discoveryOnly. Whenever a tool group is gated on a run mode in the worker, the note that advertises it has to be gated on the same term, in one place.
Local dev needs AP_DB_TYPE=POSTGRES + Redis; refuses PGLite. Prefer AP_EDITION=cloud over ee for chat work. Cloud boots locally against plain Postgres and Redis with no Autumn, Stripe or license-key config (verified Aug 2026: API healthy, migrations applied, zero billing or license errors), and on Cloud chatVisibility returns planChatEnabled || cloudRolloutOpen || userHasChatted, so chat is simply on while the rollout cap is unfilled. On ee it is gated behind plan.chatEnabled and you have to get a plan onto the platform first. Note SMTP is usually unset locally, which makes the auth card open on the password form rather than the email-code step. Debug a run with npm run chat:logs -- <conversationId> [runId] (needs LOG_FILE=true/AP_LOG_FILE=true set when the turn ran — otherwise .evlog/logs is empty).
Chat was renamed to agent in code and DB, but only the storage half. As of release 0.87.1 (1823000000000-AddRenamedChatTableCompatViews) chat_conversation → agent_conversation and user_chat_memory → user_memory, the server module moved ee/chat/ → ee/agent/ (entry point agentModule), the worker dir moved jobs/ee/chat/ → jobs/ee/agent/, shared types moved core/shared/.../ee/chat/ → .../ee/agent/, and server/utils/src/chat-ai-utils.ts → agent-ai-utils.ts. The rename is not uniform, and the split is the thing to learn: files describing chat as a user-facing surface deliberately kept their chat- names inside ee/agent/ — chat-visibility.ts, chat-rollout-service.ts, chat-rollout-user-entity.ts, chat-analytics-sync.ts, chat-tool-billing.ts, chat-usage-tracker.ts, chat-plan-grant.ts. So a new chat-surface concern keeps the chat- prefix; a new stored entity takes agent_. The migration also leaves CREATE OR REPLACE VIEW compat views at both old table names, so raw SQL against chat_conversation still reads fine and will NOT tell you the rename happened — grep the entity, not the database.
An AI SDK major bump can typecheck clean while a callback payload silently changed shape. v7 keeps most v6 option names as working deprecated aliases (system, onStepFinish, experimental_repairToolCall, stepCountIs, result.toUIMessageStream), so the option compiles but the data underneath can differ: experimental_onToolCallFinish survived as an alias for onToolExecutionEnd while its event lost durationMs/success/error (now toolExecutionMs plus a toolOutput.type === 'tool-result' discriminator). A type-probe that only names the option passes; you have to exercise each callback's property access. onStepEnd's content is also cast to a structural ContentPartLike with an args ?? input fallback (agent-ai-utils.ts), which means a shape change there fails at runtime, not compile time — always smoke a real turn after a provider/SDK major.
ai and evlog are version-coupled. evlog ≤2.18.1 imports TelemetryIntegration from ai, which v7 renamed to Telemetry, so bumping ai to 7 without bumping evlog (≥2.22.4, which peers ai >=6.0.168 <8.0.0 and supports both v6 and v7 hooks) will not compile. That evlog bump in turn changes DefinedAuditAction from <TargetType> to <Action, Options> and breaks helper/audit-events.ts — drop the explicit annotation and let defineAuditAction's inference supply it.
AI SDK v7 is ESM-only, and that is NOT a reason to convert the server to ESM. ai@7 ships type: module with no require condition, but the CJS server consumes it fine through Node's require(esm) (Node 22.12+/24, verified), and TS 5.5.4 resolves its types under module: CommonJS + moduleResolution: node because a root main and an adjacent index.d.ts still exist and skipLibCheck is on. No ESM migration, no TypeScript upgrade. Mixed ai majors across workspaces are also safe and intentional — bunfig.toml sets linker = "isolated", so pieces/framework/engine can stay on v6 while the agent path runs v7.
ap_show_connection_required is an alias of ap_show_connection_picker, not a smaller capability. Both names resolve to the same ConnectionPickerCard, which lists every account the caller has for that piece and offers "Use a different account"; the only schema difference is an optional status: 'missing' | 'error' hint. So an allow-list that grants one name and asserts the other is absent proves nothing: verified live on the agent surface, granting only ap_show_connection_required renders "Which <app> account should I use?". The card also fetches the account list itself from the frontend, keyed by conversationId, so the tool payload cannot constrain what it offers. A repair-only variant therefore lives in the endpoint feeding the card, not in the tool set.
On a saved-agent run, choosing a different account in the connection card does nothing. onConnectionSelected writes into selectedConnectionByPiece (execute-agent-run.ts), which is read only through getSelectedAuth, passed only to the MCP tool set — and AgentRunSource.AGENT is not granted groups.mcp at all. Configured piece tools carry the agent's stored pieceMetadata auth instead. So the card reports the account as connected while the tool keeps calling on the pinned one. Only the in-place Reconnect actually repairs an agent run, because it re-authorizes the same connection row the agent is pinned to. So /v1/agents/conversations/:id/connections answers { connections, reconnectOnly }, and for an AGENT-source conversation returns only the accounts that agent's tools pin. Three things that branch has to get right, each of which was a live bug first: match on (projectId, externalId), because externalId is caller-supplied and its index is not unique, so a same-id row in another project can pose as the pinned one; read the pin through published ?? draft, the same as the run; and unwrap the pre-0.87 {{connections['id']}} template form, or an older agent reads as having no pinned account and the card tells the user their live account is gone. The card must also carry the row's own projectId into the reconnect dialog, which otherwise falls back to the session project and repairs the wrong one.
Entry point: agentModule, the Fastify plugin registered in packages/server/api/src/app/app.ts.
packages/server/api/src/app/ee/agent/ — the API module: controllers, service, helpers, approval gate, compaction, rollout, console sync, billing (chat-usage-tracker.ts, chat-tool-billing.ts), memory (agent-memory-ai.ts, user-memory-entity.ts), entities, plus tools/, mcp/, prompt/, history/ subdirspackages/server/worker/src/lib/execute/jobs/ee/agent/ — where the LLM loop actually runs: execute-agent-run.ts job handler (+ the three liveness timers + streamChunksToClient idle watchdog), run-agent-turn.ts DI streaming loop, agent-worker-tools.ts tool defspackages/server/utils/src/agent-ai-utils.ts — the AI-utils bag: createChatModel per provider, supportsWebSearch/buildWebSearchTools, collapseStaleToolOutputs history hygienepackages/core/shared/src/lib/ee/agent/ — shared zod schemas and types, tool-phases.ts gating, tool-classification.ts, chat-visibility.tspackages/server/api/src/assets/prompts/ — system prompt + project-context markdown and the on-demand guides/; agent-eval fixtures live in packages/server/worker/test/lib/agent-eval/packages/web/src/app/routes/chat-with-ai/ — the chat page, chat box, conversation list, and components/ cardspackages/web/src/features/chat/ — API client, Zustand store, use-chat.ts, chunk-reducer.ts, streaming and voice hooksPaths verified 2026-08-19 against main. An earlier version pointed at ee/chat/chat-model-factory.ts and ee/chat/chat-history-hygiene.ts; both were folded into packages/server/utils/src/agent-ai-utils.ts. Every ee/chat/ path on this page before that date is dead — see the chat-to-agent rename gotcha above.
setConversationId is a reload, not a setter. It calls stopStream(), resets the interaction stores and refetches history, so handing it the id of a conversation the hook is already in destroys the turn in flight. AIChatBox seeds it from the conversationId prop in an effect, which makes the obvious wiring — feed onConversationCreated back into that prop — kill the very turn that created the conversation: the pane goes blank while the reply completes fine on the server. It now early-returns when the id is unchanged, so re-seeding is a no-op, but the shape is worth knowing before adding another caller.