System overview
- Frontend — React SPA with WebSocket streaming, tool approval UI, and file attachments
- Worker — Cloudflare Worker handling HTTP routing, auth, rate limiting, and Durable Object orchestration
- Durable Object — One per agent. Holds conversation state, manages the Claude agentic loop, executes tools, and gates write operations behind approval
- Connector — Java 21 Spring Boot service that talks to SAP via JCo. Each workspace can configure multiple named SAP connections via
workspace_connections
Entry point
src/index.ts exports:
Agent— The Durable Object class (re-exported for Cloudflare binding)- Default
{ fetch, scheduled }— Worker handlers
| Route | Handler | Purpose |
|---|---|---|
/api/orgs | orgs.ts | Organization CRUD |
/api/workspaces | workspaces.ts | Workspace CRUD + connection CRUD (workspace_connections) |
/api/workspaces/:id/index | indexing.ts | SAP metadata indexing |
/api/workspaces/:id/sync-runs | indexing.ts | Sync run history |
/api/workspaces/:id/dashboard | dashboard.ts | Connection analytics, diff comparison, object inventory (details) |
/api/workspaces/:id/clean-core | clean-core.ts | Clean Core / readiness analysis |
/api/workspaces/:id/docs | docs.ts | Generated documentation |
/api/workspaces/:id/source | source.ts | ABAP source retrieval by connectionId |
/api/workspaces/:id/connector-health | connection-test.ts | Connector reachability check for setup wizard |
/api/workspaces/:id/test-connection-preview | connection-test.ts | Test unsaved connection settings during setup wizard |
/api/agents/:id/* | agents.ts | Chat, messages, approval, WebSocket |
/api/user/sap-credentials* | credentials.ts | Per-user SAP credential CRUD (encrypted at rest) |
/api/workspaces/:id/connections/:connId/secrets* | credentials.ts | Admin connection secret management |
/api/files | files.ts | R2 file upload/download |
/api/sandbox/:agentId/tool-call | sandbox-callback.ts | Sandbox tool callbacks (token auth, no JWT) |
/api/workspaces/:id/test-connection | connection-test.ts | SAP connection + permission testing |
/api/health | index.ts | Worker health check |
/api/* routes. Rate limiting (60 req/min per user) applies to mutation endpoints.
Durable Object: Agent
Each agent is an Agent that extends DurableObject<Env>. It persists its own state in Durable Object storage and handles WebSocket connections for real-time streaming.
Persistent state
Stored in DO storage, survives hibernation:| Key | Type | Description |
|---|---|---|
status | 'idle' | 'active' | 'waiting' | Agent lifecycle |
messages | Message[] | Full conversation (user, assistant, tool) |
modelMessages | ModelMessage[] | Lossless provider-agnostic history (text, tool-call, tool-result, reasoning, approvals) |
pendingApprovalGroup | ApprovalGroup | Grouped write-tool approvals awaiting user response |
agentId | string | Supabase record ID |
connections | ConnectionInfo[] | Workspace connections (id, name, systemType, connectorUrl) |
connectionSecrets | Record<string, Record<string, string>> | Per-connection secrets (including connector_key) |
workspaceId | string | Parent workspace |
llmProvider | 'anthropic' | 'openai' | Active model provider |
llmModel | string? | Provider/model override in provider:model format |
Write-through sync and restoration
DO storage is the primary data store during active use, but it can be evicted after a 7-day idle alarm. To survive eviction, the agent syncs state to Supabase as a non-blocking write-through:- Messages — Upserted to the
messagestable (keyed by message ID) after everysaveState()call model_messages+llm_model(+ derivedllm_provider) — PATCHed to theagentsrow for lossless restoration and provider/model continuity
agentId breadcrumb, restoreFromSupabase() rebuilds state:
- Fetches messages from the
messagestable (ordered byseq) - Fetches
model_messages,llm_provider, andllm_modelfrom theagentsrow —llm_modelis canonicalized toprovider:modeland provider is derived from it. Ifmodel_messagesexists, uses it directly (lossless). If null, falls back torebuildModelMessages()(lossy text-only reconstruction) - Re-saves to DO storage for future fast reads
No blob storage for messages. Unlike LangChain/LangGraph’s postgres checkpointer which stores conversation state as blobs, we sync messages as individual rows to Supabase’s
messages table. This gives us queryability and avoids large blob reads, but means we’re making more individual writes. If message volume grows significantly, consider a blob-based approach or more aggressive batching.In-memory state
Not persisted — regenerated on wake:sessions— Map of connected WebSocket clientsstreamingAccumulator/thinkingAccumulator— Partial content for reconnecting clientscurrentAbortController— Active generation controller (for stop support)cachedSystemPrompt— System prompt with 5-minute TTLuserCredentials— Per-request decrypted SAP credentials (never persisted to DO storage)sandboxToken/sandboxContextId— Sandbox session state (see Sandbox Code Execution)
Request lifecycle
Agentic loop: generateResponse()
This is the core loop. It runs via AI SDK v6 streamText() and supports Anthropic and OpenAI models.
Build tools
Calls
buildSapTools({ connections: ConnectionInfo[] }) to get the SAP tool set. Base SAP tools are exposed as sap_* and route using connection_id. Cross-connection ETL tools appear only when 2+ connections are configured. If the SANDBOX binding is available, execute_code is added with a dynamically generated description containing return schemas from all active tools.By default, SAP/testing tools are execute-code-callable but not direct-callable (direct_call_allowed=false, execute_code_allowed=true). Web tools remain direct provider tools.Separately, web_search / web_fetch are conditionally added when the workspace has them enabled and at least one allowed domain is configured (Anthropic provider only). See Web Search & Fetch.Stream model response
Calls
streamText() with selected provider/model, system prompt, message history, tools, and stepCountIs(MAX_LOOP_STEPS) loop control. Thinking/text deltas are buffered and broadcast to WebSocket clients every 100ms. Tool lifecycle events (tool-input-start, tool-input-delta, tool-call, tool-result, tool-error, tool-approval-request) update tool cards and approval state in real time.Model selection precedence for the main loop is: llmModel (agent override) → PRIMARY_MODEL (env). Both use provider:model format.Process stream events
The agent builds UI messages from streamed thinking/text/source events, tracks citations, creates early tool cards on
tool-input-start, streams draft tool input on tool-input-delta, then finalizes tool args on tool-call. Tool cards are updated on tool-result / tool-error / tool-output-denied.Gate write tools
Approval-required tools create a
pendingApprovalGroup. The agent broadcasts approval_needed and waits for approve/reject actions.Approval flow
Top-level tool approvals are typically driven byexecute_code: the backend evaluates nested helper usage in submitted Python and only requests approval when at least one referenced nested tool is write-capable. (Web tools are direct provider tools and do not use this approval path.)
- Agent broadcasts
approval_neededwith tool name, arguments, and a uniquetoolCallId - Frontend shows an
ApprovalCardfor each pending tool - User clicks approve or reject (individually or bulk)
POST /api/agents/:id/approveresolves the pending entry- If approved → tool executes, result stored
- If rejected → error result stored (“User rejected this tool call”)
- Once all entries in the group are resolved,
checkGroupComplete()merges results and continues the loop
WebSocket protocol
Clients connect viaGET /api/agents/:id/ws (WebSocket upgrade). Messages are JSON with a type field:
| Type | Direction | Payload |
|---|---|---|
init | Server → Client | Full state snapshot (messages, status, pending approvals, partial stream) |
status | Server → Client | Status change (active, idle, waiting) |
message | Server → Client | New complete message |
thinking_start / thinking_end | Server → Client | Extended thinking boundaries |
stream | Server → Client | Text delta (partial content) |
stream_end | Server → Client | Stream finished |
approval_needed | Server → Client | Write tool awaiting approval |
tool_update | Server → Client | Patch to tool message state (tool input drafts/final input and/or tool output) |
tool_error | Server → Client | Tool execution error |
tool_rejected | Server → Client | Tool output denied/rejected |
title_update | Server → Client | Auto-generated conversation title |
stopped | Server → Client | User interrupted generation |
ctx.acceptWebSocket, ctx.getWebSockets()) so connections survive DO hibernation.
Tool system
Tool definitions
Base tools are defined insrc/tools/tool-definitions.ts. Each tool has:
Tool routing
buildSapTools() in src/tools/sap-tools.ts exposes sap_* tools that include a connection_id parameter:
- Examples:
sap_read_table,sap_get_table_schema,sap_execute_rfc,sap_search_index - Single-connection context:
connection_iddefaults automatically - Multi-connection context:
connection_idis required
ToolExecutionContext.connections and routes the connector request to that connection’s URL.
By default these SAP/testing tools are callable from execute_code helpers and not directly callable by the model unless a tool explicitly sets direct_call_allowed=true.
Server tools (web search & fetch)
Web search and web fetch are added inbuildAiTools() as Anthropic provider tools (anthropic.tools.webSearch_20250305 / webFetch_20250910) when llmProvider === 'anthropic', workspace toggles are enabled, and web_allowed_domains is configured. Domain restrictions are enforced by allowedDomains. See Web Search & Fetch for full details.
Cross-system tools
When 2+ connections are configured, additional tools are added that operate across connections:generate_field_mapping— Compares schemas between source and targetpreview_data_transform— Shows before/after data transformation- ETL tools (
suggest_transforms,validate_migration,extract_transform_load,reconcile_migration)
source_connection_id and target_connection_id parameters to route calls to the correct connectors.
Tool execution context
Every tool receives a context object at execution time:Connector client
src/tools/sap-connector-client.ts handles HTTP communication with the Java connector.
callConnector(connectorUrl, auth, endpoint, body, environment):
- POSTs JSON to the connector endpoint
- Adds
X-Connector-Keyheader whenauth.connectorKeyis present - Adds
X-SAP-User/X-SAP-Passwordheaders when user credentials are present - Returns
response.json().dataon success - Blocks redirects (security measure)
validateConnectorUrl):
- Blocks private IPs (10.x, 192.168.x, 172.16-31.x, 169.254.x) and cloud metadata IPs
- Allows localhost/127.x only in non-production environments
- Enforces HTTPS in production
connectorUrl is configured, tools fall back to mock data generators in src/tools/mock-data.ts.
System prompt
Built dynamically insrc/agents/system-prompt.ts. The prompt includes:
- Base instructions (SAP domain knowledge, tool usage patterns, safety rules)
- SAP index fingerprint (if the workspace has indexed SAP metadata — includes table counts, BAPI counts, custom object counts)
- ETL execution guidance (mandatory validation → dry run → execute → reconcile)
- Transform type reference (12 valid types)
- BAPI verification rules (verify on target system, not source)
Frontend
Stack: React 18, React Router 7, Vite, Tailwind CSS, Radix UI, SWR Key components:| Component | File | Purpose |
|---|---|---|
ChatInterface | components/chat/ChatInterface.tsx | WebSocket connection, message rendering, streaming, approval UI |
MessageBubble | components/chat/MessageBubble.tsx | Markdown rendering, code blocks, thinking blocks, citation links |
ApprovalCard | components/chat/ApprovalCard.tsx | Approve/reject UI for write tools |
ToolCard | components/chat/ToolCard.tsx | Collapsible tool execution results |
Sidebar | components/layout/Sidebar.tsx | Org switcher, workspace list, agent list |
AuthContext— Supabase auth sessionDashboardContext— Shared workspace/agent state- SWR for API data fetching with automatic revalidation
- Frontend accumulates
streamWebSocket events into the current message stream_endfinalizes the message- Thinking blocks render in a collapsible UI
- Reconnecting clients receive full state (including partial stream content) via the
initmessage
Data flow
Message storage
Conversation messages live primarily in Durable Object storage during active use, with a write-through copy in Supabase for durability:messagestable — UI-facing conversation (user, assistant, tool messages withseqordering). Written via non-blocking upsert after eachsaveState().agents.model_messages— Full provider-agnostic model conversation as JSONB. PATCHed alongside message sync.agents.llm_model— Canonical model selection (provider:model) for the agent.agents.llm_provideris derived from this value for compatibility.- DO
ctx.storage— Primary store during active sessions. Provides strong consistency and low latency.
File attachments
Uploaded viaPOST /api/files → stored in Cloudflare R2 (FILE_STORAGE bucket). The agent fetches files from R2 when processing messages with attachments. Supports PDF and image files, which are sent as model file/image content parts.
SAP metadata indexing & enrichment
Indexing and enrichment run as a two-phase pipeline:- Indexing (fast, metadata-only) —
runFullIndex()calls the connector phases:index-custom-objects,index-relationships,index-custom-code,index-catalog-context,index-enhancements, andindex-s4-extensions(S/4HANA only). - Enrichment (slow, doc + analysis) —
SAPSyncWorkflowgenerates docs, infers business processes, runs Clean Core or conversion readiness analysis, and generates embeddings.
| Table | Purpose |
|---|---|
workspace_sap_index | Phase-by-phase indexing status + raw data |
workspace_sap_graph | Connection-level status, fingerprints, progress |
sap_search_entries | Normalized searchable objects (replaces workspace_sap_graph.search_index) |
sap_generated_docs | Module, technical, ER, and business-process docs |
clean_core_analysis | Clean Core / conversion readiness results |
sap_sync_runs | Full/delta/enrich-only sync history |
connection_secrets | Per-connection secret key/value storage (connector_key, host/sysnr/client/router) |
user_connection_credentials | Per-user encrypted SAP credentials keyed by connection |
sap_search_entries (RPC-backed) and use embeddings when available.
Sync workflow (Cloudflare Workflows)
TheSAPSyncWorkflow (binding: SAP_SYNC_WORKFLOW) orchestrates indexing and enrichment in the background per connection:
- Probe (
/api/sap/probe-changes) to skip no-op delta runs - Index metadata with
runFullIndex(workspaceId, connectionId, ...)and updateworkspace_sap_graph - Enrich by generating docs, inferring processes, running Clean Core or conversion readiness, and embedding docs
src/routes/scheduled.ts) triggers weekly full runs and daily delta runs based on last_full_sync_at / last_delta_sync_at. Progress is surfaced via workspace_sap_graph.sync_* and workspace_sap_graph.enrichment_* fields.