From a5ff6b20ae3f5c04768bc28a9830808cfeaa7344 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Mon, 13 Jul 2026 20:32:07 -0700 Subject: [PATCH 1/5] feat(agent-stream): add agent-events thinking/tool streaming for chat and canvas Ship the agent-events-v1 protocol with provider tool loops, dual-gated chat thinking, DeepSeek/Groq/OpenAI reasoning wiring, and ChatGPT-like thinking chrome. Co-authored-by: Cursor --- .../en/workflows/deployment/agent-events.mdx | 90 + .../docs/en/workflows/deployment/chat.mdx | 1 + .../docs/en/workflows/deployment/meta.json | 2 +- .../(interfaces)/chat/[identifier]/chat.tsx | 8 + .../chat/components/message/message.test.tsx | 167 +- .../chat/components/message/message.tsx | 53 +- .../chat/hooks/use-chat-streaming.test.tsx | 530 + .../chat/hooks/use-chat-streaming.ts | 287 +- .../app/api/chat/[identifier]/otp/route.ts | 2 + .../app/api/chat/[identifier]/route.test.ts | 6 + apps/sim/app/api/chat/[identifier]/route.ts | 21 +- apps/sim/app/api/chat/manage/[id]/route.ts | 6 + apps/sim/app/api/chat/route.ts | 2 + apps/sim/app/api/providers/route.ts | 6 +- .../[executionId]/[contextId]/route.ts | 3 + .../workflows/[id]/chat/status/route.test.ts | 2 + .../api/workflows/[id]/chat/status/route.ts | 2 + .../app/api/workflows/[id]/execute/route.ts | 53 +- .../deploy-modal/components/chat/chat.tsx | 20 + .../components/output-panel/output-panel.tsx | 29 + .../hooks/use-workflow-execution.ts | 92 + .../agent-stream/agent-stream-chrome.test.tsx | 145 + .../agent-stream/agent-stream-chrome.tsx | 203 + .../executor/execution/block-executor.test.ts | 289 + apps/sim/executor/execution/block-executor.ts | 243 +- apps/sim/executor/execution/types.ts | 5 + .../handlers/agent/agent-handler.test.ts | 34 + .../executor/handlers/agent/agent-handler.ts | 17 +- apps/sim/executor/types.ts | 22 + apps/sim/hooks/queries/chats.ts | 3 + apps/sim/hooks/use-execution-stream.test.ts | 55 + apps/sim/hooks/use-execution-stream.ts | 10 + .../contracts/chats.include-thinking.test.ts | 76 + apps/sim/lib/api/contracts/chats.ts | 11 +- apps/sim/lib/api/contracts/deployments.ts | 1 + .../tools/handlers/deployment/deploy.ts | 7 + .../tools/handlers/deployment/manage.ts | 2 + .../lib/copilot/tools/handlers/param-types.ts | 1 + .../lib/core/execution-limits/types.test.ts | 23 + apps/sim/lib/core/execution-limits/types.ts | 6 +- .../workflows/executor/execute-workflow.ts | 3 + .../workflows/executor/execution-events.ts | 38 + .../executor/human-in-the-loop-manager.ts | 39 +- .../workflows/orchestration/chat-deploy.ts | 5 + .../streaming/agent-stream-protocol.test.ts | 83 + .../streaming/agent-stream-protocol.ts | 60 + .../attach-agent-stream-sink.test.ts | 63 + .../streaming/attach-agent-stream-sink.ts | 47 + .../lib/workflows/streaming/streaming.test.ts | 370 + apps/sim/lib/workflows/streaming/streaming.ts | 152 +- .../__fixtures__/anthropic/fixtures.test.ts | 188 + .../providers/__fixtures__/anthropic/index.ts | 16 + .../anthropic/redacted-thinking-signature.ts | 99 + .../anthropic/thinking-text-tool.ts | 119 + .../__fixtures__/openai-compat/index.ts | 60 + apps/sim/providers/anthropic/core.ts | 89 +- .../anthropic/streaming-tool-loop.test.ts | 335 + .../anthropic/streaming-tool-loop.ts | 563 + apps/sim/providers/anthropic/utils.test.ts | 106 + apps/sim/providers/anthropic/utils.ts | 94 +- apps/sim/providers/azure-openai/index.ts | 2 + apps/sim/providers/azure-openai/utils.ts | 19 +- apps/sim/providers/baseten/index.ts | 2 + apps/sim/providers/baseten/utils.ts | 19 +- apps/sim/providers/bedrock/index.ts | 59 +- .../bedrock/streaming-tool-loop.test.ts | 141 + .../providers/bedrock/streaming-tool-loop.ts | 541 + .../providers/bedrock/utils.stream.test.ts | 51 + apps/sim/providers/bedrock/utils.ts | 20 +- apps/sim/providers/cerebras/index.ts | 2 + apps/sim/providers/cerebras/utils.ts | 18 +- apps/sim/providers/deepseek/index.test.ts | 99 + apps/sim/providers/deepseek/index.ts | 179 +- apps/sim/providers/deepseek/utils.ts | 18 +- apps/sim/providers/fireworks/index.ts | 2 + apps/sim/providers/fireworks/utils.ts | 19 +- apps/sim/providers/gemini/core.ts | 87 +- .../gemini/streaming-tool-loop.test.ts | 166 + .../providers/gemini/streaming-tool-loop.ts | 542 + .../sim/providers/google/utils.stream.test.ts | 74 + apps/sim/providers/google/utils.ts | 30 +- apps/sim/providers/groq/index.test.ts | 121 + apps/sim/providers/groq/index.ts | 177 +- apps/sim/providers/groq/utils.ts | 18 +- apps/sim/providers/litellm/index.ts | 2 + apps/sim/providers/litellm/utils.ts | 18 +- apps/sim/providers/meta/index.ts | 2 + apps/sim/providers/meta/utils.ts | 18 +- apps/sim/providers/mistral/index.ts | 2 + apps/sim/providers/mistral/utils.ts | 18 +- apps/sim/providers/models.ts | 46 +- apps/sim/providers/nvidia/index.ts | 2 + apps/sim/providers/nvidia/utils.ts | 18 +- apps/sim/providers/ollama-cloud/utils.ts | 18 +- apps/sim/providers/ollama/core.ts | 7 +- apps/sim/providers/ollama/utils.ts | 18 +- .../openai-compat/stream-events.test.ts | 110 + .../providers/openai-compat/stream-events.ts | 196 + .../openai-compat/streaming-tool-loop.test.ts | 297 + .../openai-compat/streaming-tool-loop.ts | 451 + .../providers/openai/core.reasoning.test.ts | 107 + apps/sim/providers/openai/core.ts | 36 +- .../sim/providers/openai/utils.stream.test.ts | 75 + apps/sim/providers/openai/utils.ts | 42 +- apps/sim/providers/openrouter/index.ts | 2 + apps/sim/providers/openrouter/utils.ts | 15 +- apps/sim/providers/sakana/index.ts | 2 + apps/sim/providers/sakana/utils.ts | 18 +- apps/sim/providers/step-10-smokes.test.ts | 67 + apps/sim/providers/stream-events.test.ts | 110 + apps/sim/providers/stream-events.ts | 142 + apps/sim/providers/stream-pump.test.ts | 494 + apps/sim/providers/stream-pump.ts | 424 + .../sim/providers/streaming-execution.test.ts | 39 + apps/sim/providers/streaming-execution.ts | 13 +- apps/sim/providers/together/index.ts | 2 + apps/sim/providers/together/utils.ts | 19 +- apps/sim/providers/tool-call-id.ts | 27 + apps/sim/providers/utils.test.ts | 11 + apps/sim/providers/vllm/index.ts | 2 + apps/sim/providers/vllm/utils.ts | 19 +- apps/sim/providers/xai/index.ts | 2 + apps/sim/providers/xai/utils.ts | 19 +- apps/sim/providers/zai/index.ts | 2 + apps/sim/providers/zai/utils.ts | 18 +- apps/sim/stores/terminal/console/store.ts | 12 + apps/sim/stores/terminal/console/types.ts | 10 + .../migrations/0261_chat_include_thinking.sql | 1 + .../db/migrations/meta/0261_snapshot.json | 17701 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/schema.ts | 7 + packages/testing/src/mocks/schema.mock.ts | 1 + 132 files changed, 27747 insertions(+), 392 deletions(-) create mode 100644 apps/docs/content/docs/en/workflows/deployment/agent-events.mdx create mode 100644 apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx create mode 100644 apps/sim/components/agent-stream/agent-stream-chrome.test.tsx create mode 100644 apps/sim/components/agent-stream/agent-stream-chrome.tsx create mode 100644 apps/sim/lib/api/contracts/chats.include-thinking.test.ts create mode 100644 apps/sim/lib/workflows/streaming/agent-stream-protocol.test.ts create mode 100644 apps/sim/lib/workflows/streaming/agent-stream-protocol.ts create mode 100644 apps/sim/lib/workflows/streaming/attach-agent-stream-sink.test.ts create mode 100644 apps/sim/lib/workflows/streaming/attach-agent-stream-sink.ts create mode 100644 apps/sim/providers/__fixtures__/anthropic/fixtures.test.ts create mode 100644 apps/sim/providers/__fixtures__/anthropic/index.ts create mode 100644 apps/sim/providers/__fixtures__/anthropic/redacted-thinking-signature.ts create mode 100644 apps/sim/providers/__fixtures__/anthropic/thinking-text-tool.ts create mode 100644 apps/sim/providers/__fixtures__/openai-compat/index.ts create mode 100644 apps/sim/providers/anthropic/streaming-tool-loop.test.ts create mode 100644 apps/sim/providers/anthropic/streaming-tool-loop.ts create mode 100644 apps/sim/providers/anthropic/utils.test.ts create mode 100644 apps/sim/providers/bedrock/streaming-tool-loop.test.ts create mode 100644 apps/sim/providers/bedrock/streaming-tool-loop.ts create mode 100644 apps/sim/providers/bedrock/utils.stream.test.ts create mode 100644 apps/sim/providers/deepseek/index.test.ts create mode 100644 apps/sim/providers/gemini/streaming-tool-loop.test.ts create mode 100644 apps/sim/providers/gemini/streaming-tool-loop.ts create mode 100644 apps/sim/providers/google/utils.stream.test.ts create mode 100644 apps/sim/providers/groq/index.test.ts create mode 100644 apps/sim/providers/openai-compat/stream-events.test.ts create mode 100644 apps/sim/providers/openai-compat/stream-events.ts create mode 100644 apps/sim/providers/openai-compat/streaming-tool-loop.test.ts create mode 100644 apps/sim/providers/openai-compat/streaming-tool-loop.ts create mode 100644 apps/sim/providers/openai/core.reasoning.test.ts create mode 100644 apps/sim/providers/openai/utils.stream.test.ts create mode 100644 apps/sim/providers/step-10-smokes.test.ts create mode 100644 apps/sim/providers/stream-events.test.ts create mode 100644 apps/sim/providers/stream-events.ts create mode 100644 apps/sim/providers/stream-pump.test.ts create mode 100644 apps/sim/providers/stream-pump.ts create mode 100644 apps/sim/providers/tool-call-id.ts create mode 100644 packages/db/migrations/0261_chat_include_thinking.sql create mode 100644 packages/db/migrations/meta/0261_snapshot.json diff --git a/apps/docs/content/docs/en/workflows/deployment/agent-events.mdx b/apps/docs/content/docs/en/workflows/deployment/agent-events.mdx new file mode 100644 index 00000000000..17ec06c0c66 --- /dev/null +++ b/apps/docs/content/docs/en/workflows/deployment/agent-events.mdx @@ -0,0 +1,90 @@ +--- +title: Agent stream events +description: Protocol for streaming thinking and tool lifecycle from Agent blocks +--- + +import { Callout } from 'fumadocs-ui/components/callout' +import { Tab, Tabs } from 'fumadocs-ui/components/tabs' + +Agent blocks can emit more than answer text while they run: **provider-exposed thinking** (or reasoning summaries) and a **tool-call lifecycle** (name + status). Sim delivers those as typed events on an opt-in stream protocol. + + +Sim does **not** invent thinking for providers that do not stream it. Bedrock Converse, many OpenAI-compat models, and non-reasoning chat models stay text-only (plus tools when a live tool loop is wired). + + +## Dual gate (public chat / simple SSE) + +Thinking and tool frames leave the public chat or workflow simple-SSE path only when **both** are true: + +1. Deployment policy: chat `includeThinking` is enabled (default **off**). +2. Request opts in with header: + +```http +X-Sim-Stream-Protocol: agent-events-v1 +``` + +Legacy clients that omit the header keep today’s text-only SSE even if the deployment has thinking enabled. The hosted chat UI always sends the header for its own deployments. + +## Simple SSE frame shapes + +Answer text stays on `chunk`. Thinking and tools never reuse `chunk` (so older clients that append every `chunk` into the answer cannot leak thinking). + +| Frame | Meaning | +|-------|---------| +| `{ "blockId", "chunk": "…" }` | Final-turn answer text only | +| `{ "blockId", "event": "thinking", "data": "…" }` | Thinking / reasoning summary delta | +| `{ "blockId", "event": "tool", "phase": "start"\|"end", "id", "name", "status?" }` | Tool lifecycle (no args / results) | +| `{ "event": "final", … }` | Terminal success envelope | +| `{ "event": "stream_error", … }` | Terminal failure (may omit `final`) | +| `data: [DONE]` | Stream closed (documented completion marker) | + +### Intermediate-turn text + +During a live tool loop, the model may emit preamble text before calling a tool. That text is tagged **intermediate** internally and is **not** projected into the user-facing answer channel. Only the **final** turn’s text appears as `chunk`. + +### Abort + +Client disconnect or Stop aborts the provider stream. In-flight tools settle as `cancelled`. Cancel is distinct from execution timeout. + +### Reconnect + +Canvas execution-events `stream:chunk`, `stream:thinking`, and `stream:tool` are **live-only** (not buffered for reconnect replay), same as answer chunks. Guaranteed `seq` replay is out of scope. + +## Canvas (draft Run) + +When you click **Run** in the builder, the execution-events SSE path forwards the same sink: + +- `stream:thinking` — `{ blockId, data }` +- `stream:tool` — `{ blockId, phase, id, name, status? }` +- `stream:chunk` — answer text (unchanged) + +The terminal output panel shows Thinking / Tools chrome above the block output when those events arrive. PII redaction on block output still disables live forwarding (executor rule). + +## Chat deployment toggle + +In **Deploy → Chat**, enable **Include thinking** so public chat can expose thinking/tool frames (still requires the protocol header). Redeploy / update the chat after changing Agent models or tools. + +## Capability honesty (high level) + +| Family | Thinking | Live tools | +|--------|----------|------------| +| Anthropic / Azure Anthropic | Yes (incl. redacted blocks in traces) | Yes | +| Gemini / Vertex | Yes when `includeThoughts` requested | Yes | +| OpenAI Responses | Reasoning **summaries** when streamed | Silent tool loop today (answer streams; chips may be absent) | +| OpenAI-compat (Groq, DeepSeek, …) | Only if vendor streams `reasoning` / `reasoning_content` | Live loop where wired (e.g. Groq, DeepSeek) | +| Bedrock | Not invented | Yes when streaming tool loop is used | + +## Example (public chat) + + + +```bash +curl -N -X POST 'http://localhost:3000/api/chat/your-slug' \ + -H 'Content-Type: application/json' \ + -H 'X-Sim-Stream-Protocol: agent-events-v1' \ + -d '{"input":"Think briefly, then say hi"}' +``` + + + +See also [Chat deployment](/docs/workflows/deployment/chat) for access control and the Include thinking setting. diff --git a/apps/docs/content/docs/en/workflows/deployment/chat.mdx b/apps/docs/content/docs/en/workflows/deployment/chat.mdx index e08a32cd01e..ffe2ac2c661 100644 --- a/apps/docs/content/docs/en/workflows/deployment/chat.mdx +++ b/apps/docs/content/docs/en/workflows/deployment/chat.mdx @@ -32,6 +32,7 @@ Configure the following fields, then click **Launch Chat**: | **Output** | Output fields from your workflow blocks returned as the chat response. At least one must be selected. | | **Welcome Message** | Greeting shown before the user sends their first message. Defaults to `"Hi there! How can I help you today?"`. | | **Access Control** | Controls who can access the chat. See [Access Control](#access-control) below. | +| **Include thinking** | When enabled, the hosted chat can show provider-exposed thinking and tool lifecycle (name + status). Requires the chat client to send `X-Sim-Stream-Protocol: agent-events-v1` (the hosted UI always does). Default is off. See [Agent stream events](/docs/workflows/deployment/agent-events). | ### Output Selection diff --git a/apps/docs/content/docs/en/workflows/deployment/meta.json b/apps/docs/content/docs/en/workflows/deployment/meta.json index 9d84bce6cff..037c70ba33a 100644 --- a/apps/docs/content/docs/en/workflows/deployment/meta.json +++ b/apps/docs/content/docs/en/workflows/deployment/meta.json @@ -1,4 +1,4 @@ { "title": "Deployment", - "pages": ["index", "api", "chat", "mcp"] + "pages": ["index", "api", "chat", "agent-events", "mcp"] } diff --git a/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx b/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx index 49f71d79272..80369fd8935 100644 --- a/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx +++ b/apps/sim/app/(interfaces)/chat/[identifier]/chat.tsx @@ -4,6 +4,10 @@ import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } fro import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { noop } from '@/lib/core/utils/request' +import { + AGENT_STREAM_PROTOCOL_HEADER, + AGENT_STREAM_PROTOCOL_V1, +} from '@/lib/workflows/streaming/agent-stream-protocol' import { ChatErrorState, ChatHeader, @@ -244,7 +248,9 @@ export default function ChatClient({ identifier }: { identifier: string }) { scrollToMessage(userMessage.id, true) }, 100) + // One AbortController for fetch + SSE body reads so Stop cancels server work too. const abortController = new AbortController() + abortControllerRef.current = abortController const timeoutId = setTimeout(() => { abortController.abort() }, CHAT_REQUEST_TIMEOUT_MS) @@ -282,6 +288,7 @@ export default function ChatClient({ identifier }: { identifier: string }) { headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', + [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1, }, body: JSON.stringify(payload), credentials: 'same-origin', @@ -326,6 +333,7 @@ export default function ChatClient({ identifier }: { identifier: string }) { }, audioStreamHandler: audioHandler, outputConfigs: chatConfig?.outputConfigs, + abortController, } ) } catch (error) { diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx index a48517a75f1..e0e48731e65 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx @@ -1,11 +1,24 @@ /** - * @vitest-environment node + * @vitest-environment jsdom */ -import { describe, expect, it, vi } from 'vitest' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' vi.mock('@sim/emcn', () => ({ + Button: ({ children, ...props }: { children?: React.ReactNode; [key: string]: unknown }) => ( + + ), Duplicate: () => null, - Tooltip: {}, + Tooltip: { + Provider: ({ children }: { children?: React.ReactNode }) => <>{children}, + Root: ({ children }: { children?: React.ReactNode }) => <>{children}, + Trigger: ({ children }: { children?: React.ReactNode }) => <>{children}, + Content: ({ children }: { children?: React.ReactNode }) => <>{children}, + }, + cn: (...args: unknown[]) => args.filter(Boolean).join(' '), })) vi.mock('@/app/(interfaces)/chat/components/message/components/file-download', () => ({ @@ -14,10 +27,14 @@ vi.mock('@/app/(interfaces)/chat/components/message/components/file-download', ( })) vi.mock('@/app/(interfaces)/chat/components/message/components/markdown-renderer', () => ({ - default: () => null, + default: ({ content }: { content: string }) =>
{content}
, })) -import { escapeHtml } from '@/app/(interfaces)/chat/components/message/message' +import { + ClientChatMessage, + escapeHtml, + type ChatMessage, +} from '@/app/(interfaces)/chat/components/message/message' describe('escapeHtml', () => { it('escapes all five HTML-significant characters', () => { @@ -41,3 +58,143 @@ describe('escapeHtml', () => { expect(escapeHtml('')).toBe('') }) }) + +function renderMessage(message: ChatMessage): { container: HTMLDivElement; unmount: () => void } { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + document.body.appendChild(container) + const root: Root = createRoot(container) + act(() => { + root.render() + }) + return { + container, + unmount: () => { + act(() => { + root.unmount() + }) + container.remove() + }, + } +} + +describe('ClientChatMessage thinking chrome (Step 6)', () => { + const mounts: Array<() => void> = [] + + afterEach(() => { + while (mounts.length) { + mounts.pop()?.() + } + }) + + it('does not show thinking chrome when thinking is absent or empty', () => { + const without = renderMessage({ + id: '1', + type: 'assistant', + content: 'Hello', + timestamp: new Date(), + }) + mounts.push(without.unmount) + expect(without.container.textContent).not.toContain('Thinking') + + const empty = renderMessage({ + id: '2', + type: 'assistant', + content: 'Hello', + thinking: '', + timestamp: new Date(), + }) + mounts.push(empty.unmount) + expect(empty.container.textContent).not.toContain('Thinking') + }) + + it('shows collapsible thinking chrome above the answer after first thinking', () => { + const { container, unmount } = renderMessage({ + id: '3', + type: 'assistant', + content: 'Answer text', + thinking: 'Internal reasoning', + isThinkingStreaming: true, + timestamp: new Date(), + }) + mounts.push(unmount) + + expect(container.textContent).toContain('Thinking…') + expect(container.textContent).toContain('Internal reasoning') + expect(container.textContent).toContain('Answer text') + }) + + it('labels completed thinking as Thought for a moment', () => { + const { container, unmount } = renderMessage({ + id: '4', + type: 'assistant', + content: 'Answer text', + thinking: 'Internal reasoning', + isThinkingStreaming: false, + timestamp: new Date(), + }) + mounts.push(unmount) + + expect(container.textContent).toContain('Thought for a moment') + expect(container.textContent).toContain('Answer text') + }) +}) + +describe('ClientChatMessage tool chrome (Step 8)', () => { + const mounts: Array<() => void> = [] + + afterEach(() => { + while (mounts.length) { + mounts.pop()?.() + } + }) + + it('does not show tool chrome when toolCalls are absent or empty', () => { + const without = renderMessage({ + id: '1', + type: 'assistant', + content: 'Hello', + timestamp: new Date(), + }) + mounts.push(without.unmount) + expect(without.container.textContent).not.toContain('Tools') + expect(without.container.textContent).not.toContain('Using tools') + + const empty = renderMessage({ + id: '2', + type: 'assistant', + content: 'Hello', + toolCalls: [], + timestamp: new Date(), + }) + mounts.push(empty.unmount) + expect(empty.container.textContent).not.toContain('Tools') + }) + + it('shows humanized tool names only (no args) while tools are running', () => { + const { container, unmount } = renderMessage({ + id: '3', + type: 'assistant', + content: 'Answer', + isToolStreaming: true, + toolCalls: [ + { + key: 'agent-1:toolu_1', + blockId: 'agent-1', + id: 'toolu_1', + name: 'http_request', + displayName: 'Http Request', + status: 'running', + }, + ], + timestamp: new Date(), + }) + mounts.push(unmount) + + expect(container.textContent).toContain('Using tools…') + expect(container.textContent).toContain('Http Request') + expect(container.textContent).not.toContain('toolu_1') + expect(container.textContent).not.toContain('args') + expect(container.textContent).toContain('Answer') + }) +}) diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.tsx index f5ec5b623c8..951cfd4d3eb 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.tsx @@ -3,6 +3,10 @@ import { memo, useState } from 'react' import { Button, cn, Duplicate, Tooltip } from '@sim/emcn' import { Check, File as FileIcon, FileText, Image as ImageIcon } from 'lucide-react' +import { + AgentStreamThinkingChrome, + AgentStreamToolCallsChrome, +} from '@/components/agent-stream/agent-stream-chrome' import { ChatFileDownload, ChatFileDownloadAll, @@ -27,6 +31,19 @@ export interface ChatFile { context?: string } +/** Lifecycle status for a tool chip (agent-events-v1). No args/results. */ +export type ChatToolCallStatus = 'running' | 'success' | 'error' | 'cancelled' + +export interface ChatToolCall { + /** Stable UI key: `${blockId}:${id}`. */ + key: string + blockId: string + id: string + name: string + displayName: string + status: ChatToolCallStatus +} + export interface ChatMessage { id: string content: string | Record @@ -34,6 +51,14 @@ export interface ChatMessage { timestamp: Date isInitialMessage?: boolean isStreaming?: boolean + /** Model thinking text (agent-events-v1). Chrome only when non-empty. */ + thinking?: string + /** True while thinking deltas are still arriving (before first answer chunk / final). */ + isThinkingStreaming?: boolean + /** Tool lifecycle chips (name + status only). Chrome only when non-empty. */ + toolCalls?: ChatToolCall[] + /** True while any tool chip is still `running`. */ + isToolStreaming?: boolean attachments?: ChatAttachment[] files?: ChatFile[] } @@ -81,15 +106,21 @@ function openAttachmentPreview(name: string, dataUrl: string): void { setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000) } +function toolCallsFingerprint(toolCalls: ChatToolCall[] | undefined): string { + if (!toolCalls?.length) return '' + return toolCalls.map((t) => `${t.key}:${t.status}`).join('|') +} + export const ClientChatMessage = memo( function ClientChatMessage({ message }: { message: ChatMessage }) { const [isCopied, setIsCopied] = useState(false) const isJsonObject = typeof message.content === 'object' && message.content !== null - // Since tool calls are now handled via SSE events and stored in message.toolCalls, - // we can use the content directly without parsing + // Answer text is streamed separately from thinking / tool lifecycle events. const cleanTextContent = message.content + const hasThinking = typeof message.thinking === 'string' && message.thinking.length > 0 + const hasToolCalls = Array.isArray(message.toolCalls) && message.toolCalls.length > 0 const content = message.type === 'user' ? ( @@ -207,8 +238,19 @@ export const ClientChatMessage = memo(
- {/* Direct content rendering - tool calls are now handled via SSE events */}
+ {hasThinking && ( + + )} + {hasToolCalls && ( + + )}
{isJsonObject ? (
@@ -274,7 +316,12 @@ export const ClientChatMessage = memo(
     return (
       prevProps.message.id === nextProps.message.id &&
       prevProps.message.content === nextProps.message.content &&
+      prevProps.message.thinking === nextProps.message.thinking &&
       prevProps.message.isStreaming === nextProps.message.isStreaming &&
+      prevProps.message.isThinkingStreaming === nextProps.message.isThinkingStreaming &&
+      prevProps.message.isToolStreaming === nextProps.message.isToolStreaming &&
+      toolCallsFingerprint(prevProps.message.toolCalls) ===
+        toolCallsFingerprint(nextProps.message.toolCalls) &&
       prevProps.message.isInitialMessage === nextProps.message.isInitialMessage &&
       prevProps.message.files?.length === nextProps.message.files?.length
     )
diff --git a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx
new file mode 100644
index 00000000000..997ed22b654
--- /dev/null
+++ b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx
@@ -0,0 +1,530 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+const { mockReadSSEEvents } = vi.hoisted(() => ({
+  mockReadSSEEvents: vi.fn(),
+}))
+
+vi.mock('@/lib/core/utils/sse', () => ({
+  readSSEEvents: mockReadSSEEvents,
+}))
+
+vi.mock('@sim/utils/id', () => ({
+  generateId: () => 'msg-assistant-1',
+}))
+
+import type { ChatMessage } from '@/app/(interfaces)/chat/components/message/message'
+import {
+  isAnswerChunkFrame,
+  useChatStreaming,
+} from '@/app/(interfaces)/chat/hooks/use-chat-streaming'
+
+describe('isAnswerChunkFrame', () => {
+  it('accepts plain answer chunks without an event type', () => {
+    expect(isAnswerChunkFrame({ blockId: 'a1', chunk: 'hello' })).toBe(true)
+  })
+
+  it('rejects thinking / stream_error / tool frames even if chunk is present', () => {
+    expect(
+      isAnswerChunkFrame({ blockId: 'a1', chunk: 'leak', event: 'thinking', data: 'thought' })
+    ).toBe(false)
+    expect(isAnswerChunkFrame({ blockId: 'a1', chunk: 'x', event: 'stream_error' })).toBe(false)
+    expect(isAnswerChunkFrame({ blockId: 'a1', chunk: 'x', event: 'tool' })).toBe(false)
+    expect(isAnswerChunkFrame({ blockId: 'a1', chunk: 'x', event: 'tool_call_start' })).toBe(false)
+    expect(isAnswerChunkFrame({ blockId: 'a1', chunk: 'x', event: 'final' })).toBe(false)
+  })
+
+  it('rejects frames missing blockId or empty chunk', () => {
+    expect(isAnswerChunkFrame({ chunk: 'hello' })).toBe(false)
+    expect(isAnswerChunkFrame({ blockId: 'a1', chunk: '' })).toBe(false)
+  })
+})
+
+interface HookHandle {
+  latest: () => ReturnType
+  unmount: () => void
+}
+
+function renderStreamingHook(): HookHandle {
+  ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+  const container = document.createElement('div')
+  const root: Root = createRoot(container)
+  let latest!: ReturnType
+
+  function Probe() {
+    latest = useChatStreaming()
+    return null
+  }
+
+  act(() => {
+    root.render()
+  })
+
+  return {
+    latest: () => latest,
+    unmount: () => {
+      act(() => {
+        root.unmount()
+      })
+    },
+  }
+}
+
+function makeSseResponse(): Response {
+  return {
+    body: new ReadableStream(),
+  } as Response
+}
+
+async function flushUiBatch() {
+  await act(async () => {
+    await new Promise((resolve) => {
+      requestAnimationFrame(() => resolve())
+    })
+    await new Promise((resolve) => {
+      setTimeout(resolve, 60)
+    })
+  })
+}
+
+describe('useChatStreaming thinking + abort (Step 6)', () => {
+  let handle: HookHandle
+  let messages: ChatMessage[]
+  let setMessages: React.Dispatch>
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    messages = []
+    setMessages = ((updater: React.SetStateAction) => {
+      messages = typeof updater === 'function' ? updater(messages) : updater
+    }) as React.Dispatch>
+    handle = renderStreamingHook()
+
+    // Run rAF immediately so UI batching is deterministic in tests.
+    vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
+      cb(performance.now())
+      return 1
+    })
+  })
+
+  afterEach(() => {
+    handle.unmount()
+    vi.restoreAllMocks()
+  })
+
+  it('routes thinking to message.thinking and answer chunks to content only', async () => {
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'thinking',
+        data: 'Let me reason. ',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'thinking',
+        data: 'More thought.',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        chunk: 'Final answer.',
+      })
+      await options.onEvent({
+        event: 'final',
+        data: { success: true, output: {} },
+      })
+    })
+
+    await act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false
+      )
+    })
+    await flushUiBatch()
+
+    const assistant = messages.find((m) => m.id === 'msg-assistant-1')
+    expect(assistant?.thinking).toBe('Let me reason. More thought.')
+    expect(assistant?.content).toBe('Final answer.')
+    expect(assistant?.isStreaming).toBe(false)
+    expect(assistant?.isThinkingStreaming).toBe(false)
+  })
+
+  it('surfaces stream_error text in thinking chrome without terminating', async () => {
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'thinking',
+        data: 'Working…',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'stream_error',
+        error: 'partial provider glitch',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        chunk: 'Recovered answer.',
+      })
+      await options.onEvent({
+        event: 'final',
+        data: { success: true, output: {} },
+      })
+    })
+
+    await act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false
+      )
+    })
+    await flushUiBatch()
+
+    const assistant = messages.find((m) => m.id === 'msg-assistant-1')
+    expect(assistant?.thinking).toContain('Working…')
+    expect(assistant?.thinking).toContain('[Stream error] partial provider glitch')
+    expect(assistant?.content).toBe('Recovered answer.')
+    expect(assistant?.isStreaming).toBe(false)
+  })
+
+  it('does not append thinking payload into answer when mislabeled as chunk', async () => {
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'thinking',
+        chunk: 'SHOULD_NOT_APPEND',
+        data: 'real thought',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        chunk: 'ok',
+      })
+      await options.onEvent({
+        event: 'final',
+        data: { success: true, output: {} },
+      })
+    })
+
+    await act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false
+      )
+    })
+    await flushUiBatch()
+
+    const assistant = messages.find((m) => m.id === 'msg-assistant-1')
+    expect(assistant?.content).toBe('ok')
+    expect(assistant?.thinking).toBe('real thought')
+  })
+
+  it('TTS audioStreamHandler receives answer text only', async () => {
+    const audioStreamHandler = vi.fn().mockResolvedValue(undefined)
+
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'thinking',
+        data: 'secret internal monologue that must not be spoken.',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        chunk: 'Hello world.',
+      })
+      await options.onEvent({
+        event: 'final',
+        data: { success: true, output: {} },
+      })
+    })
+
+    await act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false,
+        {
+          voiceSettings: {
+            isVoiceEnabled: true,
+            voiceId: 'voice-1',
+            autoPlayResponses: true,
+          },
+          audioStreamHandler,
+        }
+      )
+    })
+    await flushUiBatch()
+
+    expect(audioStreamHandler).toHaveBeenCalled()
+    for (const call of audioStreamHandler.mock.calls) {
+      expect(String(call[0])).not.toContain('secret')
+      expect(String(call[0])).not.toContain('monologue')
+    }
+    expect(audioStreamHandler.mock.calls.some((c) => String(c[0]).includes('Hello'))).toBe(true)
+  })
+
+  it('stopStreaming preserves thinking and aborts the shared controller', async () => {
+    const abortController = new AbortController()
+    let resolveStream!: () => void
+    const streamDone = new Promise((resolve) => {
+      resolveStream = resolve
+    })
+
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'thinking',
+        data: 'partial thought',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        chunk: 'partial answer',
+      })
+      // Hold the stream open until Stop aborts.
+      await new Promise((resolve) => {
+        options.signal?.addEventListener('abort', () => resolve(), { once: true })
+        // Also allow test cleanup if abort never fires.
+        streamDone.then(() => resolve())
+      })
+    })
+
+    const streamPromise = act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false,
+        { abortController }
+      )
+    })
+
+    await flushUiBatch()
+    expect(messages.find((m) => m.id === 'msg-assistant-1')?.thinking).toBe('partial thought')
+
+    act(() => {
+      handle.latest().stopStreaming(setMessages)
+    })
+    resolveStream()
+    await streamPromise
+
+    const assistant = messages.find((m) => m.id === 'msg-assistant-1')
+    expect(abortController.signal.aborted).toBe(true)
+    expect(assistant?.thinking).toBe('partial thought')
+    expect(String(assistant?.content)).toContain('partial answer')
+    expect(String(assistant?.content)).toContain('Response stopped by user')
+    expect(assistant?.isStreaming).toBe(false)
+    expect(assistant?.isThinkingStreaming).toBe(false)
+  })
+
+  it('leaves thinking undefined when no thinking events arrive', async () => {
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        chunk: 'just text',
+      })
+      await options.onEvent({
+        event: 'final',
+        data: { success: true, output: {} },
+      })
+    })
+
+    await act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false
+      )
+    })
+    await flushUiBatch()
+
+    const assistant = messages.find((m) => m.id === 'msg-assistant-1')
+    expect(assistant?.thinking).toBeUndefined()
+    expect(assistant?.content).toBe('just text')
+  })
+})
+
+describe('useChatStreaming tool lifecycle (Step 8)', () => {
+  let handle: HookHandle
+  let messages: ChatMessage[]
+  let setMessages: React.Dispatch>
+
+  beforeEach(() => {
+    vi.clearAllMocks()
+    messages = []
+    setMessages = ((updater: React.SetStateAction) => {
+      messages = typeof updater === 'function' ? updater(messages) : updater
+    }) as React.Dispatch>
+    handle = renderStreamingHook()
+    vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
+      cb(performance.now())
+      return 1
+    })
+  })
+
+  afterEach(() => {
+    handle.unmount()
+    vi.restoreAllMocks()
+  })
+
+  it('maps tool start/end into keyed chips without touching answer content', async () => {
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'tool',
+        phase: 'start',
+        id: 'toolu_1',
+        name: 'http_request',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'tool',
+        phase: 'end',
+        id: 'toolu_1',
+        name: 'http_request',
+        status: 'success',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        chunk: 'https://httpbin.org/get',
+      })
+      await options.onEvent({
+        event: 'final',
+        data: { success: true, output: {} },
+      })
+    })
+
+    await act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false
+      )
+    })
+    await flushUiBatch()
+
+    const assistant = messages.find((m) => m.id === 'msg-assistant-1')
+    expect(assistant?.content).toBe('https://httpbin.org/get')
+    expect(assistant?.toolCalls).toEqual([
+      {
+        key: 'agent-1:toolu_1',
+        blockId: 'agent-1',
+        id: 'toolu_1',
+        name: 'http_request',
+        displayName: 'Http Request',
+        status: 'success',
+      },
+    ])
+    expect(assistant?.toolCalls?.[0]).not.toHaveProperty('args')
+    expect(assistant?.toolCalls?.[0]).not.toHaveProperty('result')
+    expect(assistant?.isToolStreaming).toBe(false)
+  })
+
+  it('tracks parallel tools and cancels running chips on Stop', async () => {
+    const abortController = new AbortController()
+    let resolveStream!: () => void
+    const streamDone = new Promise((resolve) => {
+      resolveStream = resolve
+    })
+
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'tool',
+        phase: 'start',
+        id: 'toolu_1',
+        name: 'http_request',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'tool',
+        phase: 'start',
+        id: 'toolu_2',
+        name: 'function_execute',
+      })
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'tool',
+        phase: 'end',
+        id: 'toolu_2',
+        name: 'function_execute',
+        status: 'success',
+      })
+      await new Promise((resolve) => {
+        options.signal?.addEventListener('abort', () => resolve(), { once: true })
+        streamDone.then(() => resolve())
+      })
+    })
+
+    const streamPromise = act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false,
+        { abortController }
+      )
+    })
+
+    await flushUiBatch()
+    expect(messages.find((m) => m.id === 'msg-assistant-1')?.toolCalls).toHaveLength(2)
+
+    act(() => {
+      handle.latest().stopStreaming(setMessages)
+    })
+    resolveStream()
+    await streamPromise
+
+    const tools = messages.find((m) => m.id === 'msg-assistant-1')?.toolCalls
+    expect(tools?.find((t) => t.id === 'toolu_1')?.status).toBe('cancelled')
+    expect(tools?.find((t) => t.id === 'toolu_2')?.status).toBe('success')
+    expect(messages.find((m) => m.id === 'msg-assistant-1')?.isToolStreaming).toBe(false)
+  })
+
+  it('settles straggler running tools to success on final', async () => {
+    mockReadSSEEvents.mockImplementation(async (_source, options) => {
+      await options.onEvent({
+        blockId: 'agent-1',
+        event: 'tool',
+        phase: 'start',
+        id: 'toolu_open',
+        name: 'http_request',
+      })
+      await options.onEvent({
+        event: 'final',
+        data: { success: true, output: {} },
+      })
+    })
+
+    await act(async () => {
+      await handle.latest().handleStreamedResponse(
+        makeSseResponse(),
+        setMessages,
+        vi.fn(),
+        vi.fn(),
+        false
+      )
+    })
+    await flushUiBatch()
+
+    expect(messages.find((m) => m.id === 'msg-assistant-1')?.toolCalls?.[0]?.status).toBe('success')
+  })
+})
diff --git a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts
index be4b0b2e1ae..312586be90a 100644
--- a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts
+++ b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts
@@ -5,7 +5,13 @@ import { createLogger } from '@sim/logger'
 import { generateId } from '@sim/utils/id'
 import { readSSEEvents } from '@/lib/core/utils/sse'
 import { isUserFileWithMetadata } from '@/lib/core/utils/user-file'
-import type { ChatFile, ChatMessage } from '@/app/(interfaces)/chat/components/message/message'
+import { humanizeToolName } from '@/lib/copilot/tools/tool-display'
+import type {
+  ChatFile,
+  ChatMessage,
+  ChatToolCall,
+  ChatToolCallStatus,
+} from '@/app/(interfaces)/chat/components/message/message'
 import { CHAT_ERROR_MESSAGES } from '@/app/(interfaces)/chat/constants'
 
 const logger = createLogger('UseChatStreaming')
@@ -64,23 +70,99 @@ export interface StreamingOptions {
   onAudioEnd?: () => void
   audioStreamHandler?: (text: string) => Promise
   outputConfigs?: Array<{ blockId: string; path?: string }>
+  /**
+   * Shared AbortController for fetch + SSE body reads. When provided (preferred),
+   * Stop aborts the in-flight request server-side as well as the reader.
+   */
+  abortController?: AbortController
+}
+
+type ChatSseEvent = {
+  blockId?: string
+  chunk?: string
+  event?: string
+  error?: string
+  phase?: 'start' | 'end'
+  id?: string
+  name?: string
+  status?: 'success' | 'error' | 'cancelled'
+  data?:
+    | string
+    | {
+        success: boolean
+        error?: string | { message?: string }
+        output?: Record>
+      }
+}
+
+function toolCallKey(blockId: string, id: string): string {
+  return `${blockId}:${id}`
+}
+
+function settleInFlightTools(
+  map: Map,
+  terminal: Exclude
+): void {
+  for (const [key, tool] of map) {
+    if (tool.status === 'running') {
+      map.set(key, { ...tool, status: terminal })
+    }
+  }
+}
+
+function snapshotToolCalls(
+  order: string[],
+  map: Map
+): ChatToolCall[] | undefined {
+  if (order.length === 0) return undefined
+  return order.map((key) => map.get(key)).filter((t): t is ChatToolCall => Boolean(t))
+}
+
+function anyToolRunning(map: Map): boolean {
+  for (const tool of map.values()) {
+    if (tool.status === 'running') return true
+  }
+  return false
+}
+
+/** Answer text frames never carry an event type (or use a reserved non-answer event). */
+function isAnswerChunkFrame(json: ChatSseEvent): boolean {
+  if (!json.blockId || typeof json.chunk !== 'string' || !json.chunk) return false
+  // Thinking / tools / errors must never use `chunk` for answer accumulation.
+  if (json.event === 'thinking' || json.event === 'stream_error' || json.event === 'error') {
+    return false
+  }
+  if (
+    json.event === 'final' ||
+    json.event === 'tool' ||
+    json.event === 'tool_call_start' ||
+    json.event === 'tool_call_end'
+  ) {
+    return false
+  }
+  return json.event === undefined || json.event === ''
 }
 
 export function useChatStreaming() {
   const [isStreamingResponse, setIsStreamingResponse] = useState(false)
   const abortControllerRef = useRef(null)
   const accumulatedTextRef = useRef('')
+  const accumulatedThinkingRef = useRef('')
+  const accumulatedToolCallsRef = useRef([])
   const lastStreamedPositionRef = useRef(0)
   const audioStreamingActiveRef = useRef(false)
-  const lastDisplayedPositionRef = useRef(0) // Track displayed text in synced mode
+  const lastDisplayedPositionRef = useRef(0)
 
   const stopStreaming = (setMessages: React.Dispatch>) => {
     if (abortControllerRef.current) {
-      // Abort the fetch request
       abortControllerRef.current.abort()
       abortControllerRef.current = null
 
       const latestContent = accumulatedTextRef.current
+      const latestThinking = accumulatedThinkingRef.current
+      const latestTools = accumulatedToolCallsRef.current.map((tool) =>
+        tool.status === 'running' ? { ...tool, status: 'cancelled' as const } : tool
+      )
 
       setMessages((prev) => {
         const lastMessage = prev[prev.length - 1]
@@ -92,7 +174,16 @@ export function useChatStreaming() {
 
           return [
             ...prev.slice(0, -1),
-            { ...lastMessage, content: updatedContent, isStreaming: false },
+            {
+              ...lastMessage,
+              content: updatedContent,
+              // Preserve any thinking / tools received before Stop.
+              thinking: latestThinking || lastMessage.thinking,
+              toolCalls: latestTools.length > 0 ? latestTools : lastMessage.toolCalls,
+              isStreaming: false,
+              isThinkingStreaming: false,
+              isToolStreaming: false,
+            },
           ]
         }
 
@@ -101,6 +192,8 @@ export function useChatStreaming() {
 
       setIsStreamingResponse(false)
       accumulatedTextRef.current = ''
+      accumulatedThinkingRef.current = ''
+      accumulatedToolCallsRef.current = []
       lastStreamedPositionRef.current = 0
       lastDisplayedPositionRef.current = 0
       audioStreamingActiveRef.current = false
@@ -116,11 +209,15 @@ export function useChatStreaming() {
     streamingOptions?: StreamingOptions
   ) => {
     logger.info('[useChatStreaming] handleStreamedResponse called')
-    // Set streaming state
     setIsStreamingResponse(true)
-    abortControllerRef.current = new AbortController()
 
-    // Check if we should stream audio
+    // Prefer a shared controller from the caller (fetch + reader). Otherwise create one.
+    if (streamingOptions?.abortController) {
+      abortControllerRef.current = streamingOptions.abortController
+    } else if (!abortControllerRef.current) {
+      abortControllerRef.current = new AbortController()
+    }
+
     const shouldPlayAudio =
       streamingOptions?.voiceSettings?.isVoiceEnabled &&
       streamingOptions?.voiceSettings?.autoPlayResponses &&
@@ -133,7 +230,15 @@ export function useChatStreaming() {
     }
 
     let accumulatedText = ''
+    let accumulatedThinking = ''
+    let isThinkingStreaming = false
     let lastAudioPosition = 0
+    const toolCallsMap = new Map()
+    const toolCallOrder: string[] = []
+
+    const syncToolCallsRef = () => {
+      accumulatedToolCallsRef.current = snapshotToolCalls(toolCallOrder, toolCallsMap) ?? []
+    }
 
     const messageIdMap = new Map()
     const messageId = generateId()
@@ -156,12 +261,23 @@ export function useChatStreaming() {
       if (!uiDirty) return
       uiDirty = false
       lastUIFlush = performance.now()
-      const snapshot = accumulatedText
+      const contentSnapshot = accumulatedText
+      const thinkingSnapshot = accumulatedThinking
+      const thinkingStreamingSnapshot = isThinkingStreaming
+      const toolCallsSnapshot = snapshotToolCalls(toolCallOrder, toolCallsMap)
+      const toolStreamingSnapshot = anyToolRunning(toolCallsMap)
       setMessages((prev) =>
         prev.map((msg) => {
           if (msg.id !== messageId) return msg
           if (!msg.isStreaming) return msg
-          return { ...msg, content: snapshot }
+          return {
+            ...msg,
+            content: contentSnapshot,
+            thinking: thinkingSnapshot || undefined,
+            isThinkingStreaming: thinkingStreamingSnapshot,
+            toolCalls: toolCallsSnapshot,
+            isToolStreaming: toolStreamingSnapshot,
+          }
         })
       )
     }
@@ -194,18 +310,8 @@ export function useChatStreaming() {
     let terminated = false
 
     try {
-      await readSSEEvents<{
-        blockId?: string
-        chunk?: string
-        event?: string
-        error?: string
-        data?: {
-          success: boolean
-          error?: string | { message?: string }
-          output?: Record>
-        }
-      }>(response.body, {
-        signal: abortControllerRef.current.signal,
+      await readSSEEvents(response.body, {
+        signal: abortControllerRef.current!.signal,
         onParseError: (_data, parseError) => {
           logger.error('Error parsing stream data:', parseError)
         },
@@ -214,13 +320,20 @@ export function useChatStreaming() {
 
           if (eventType === 'error' || json.event === 'error') {
             const errorMessage = json.error || CHAT_ERROR_MESSAGES.GENERIC_ERROR
+            settleInFlightTools(toolCallsMap, 'error')
+            syncToolCallsRef()
+            const toolsSnapshot = snapshotToolCalls(toolCallOrder, toolCallsMap)
             setMessages((prev) =>
               prev.map((msg) =>
                 msg.id === messageId
                   ? {
                       ...msg,
                       content: errorMessage,
+                      thinking: accumulatedThinking || msg.thinking,
+                      toolCalls: toolsSnapshot ?? msg.toolCalls,
                       isStreaming: false,
+                      isThinkingStreaming: false,
+                      isToolStreaming: false,
                       type: 'assistant' as const,
                     }
                   : msg
@@ -231,9 +344,92 @@ export function useChatStreaming() {
             return true
           }
 
-          if (eventType === 'final' && json.data) {
+          if (eventType === 'stream_error') {
+            const errText =
+              typeof json.error === 'string' && json.error
+                ? json.error
+                : 'A streaming error occurred'
+            logger.warn('[useChatStreaming] Non-terminal stream_error', {
+              blockId,
+              error: errText,
+            })
+            // Non-terminal: keep streaming; surface in thinking chrome so it is visible.
+            accumulatedThinking +=
+              (accumulatedThinking ? '\n\n' : '') + `[Stream error] ${errText}`
+            accumulatedThinkingRef.current = accumulatedThinking
+            isThinkingStreaming = true
+            uiDirty = true
+            scheduleUIFlush()
+            return false
+          }
+
+          if (eventType === 'thinking' && blockId && typeof json.data === 'string') {
+            if (!messageIdMap.has(blockId)) {
+              messageIdMap.set(blockId, messageId)
+            }
+            accumulatedThinking += json.data
+            accumulatedThinkingRef.current = accumulatedThinking
+            isThinkingStreaming = true
+            uiDirty = true
+            scheduleUIFlush()
+            return false
+          }
+
+          if (
+            eventType === 'tool' &&
+            blockId &&
+            typeof json.id === 'string' &&
+            json.id &&
+            typeof json.name === 'string' &&
+            json.name
+          ) {
+            if (!messageIdMap.has(blockId)) {
+              messageIdMap.set(blockId, messageId)
+            }
+            const key = toolCallKey(blockId, json.id)
+            if (json.phase === 'start') {
+              if (!toolCallsMap.has(key)) {
+                toolCallOrder.push(key)
+              }
+              toolCallsMap.set(key, {
+                key,
+                blockId,
+                id: json.id,
+                name: json.name,
+                displayName: humanizeToolName(json.name),
+                status: 'running',
+              })
+            } else if (json.phase === 'end') {
+              const endStatus: ChatToolCallStatus =
+                json.status === 'error' || json.status === 'cancelled' ? json.status : 'success'
+              const existing = toolCallsMap.get(key)
+              if (!existing) {
+                toolCallOrder.push(key)
+                toolCallsMap.set(key, {
+                  key,
+                  blockId,
+                  id: json.id,
+                  name: json.name,
+                  displayName: humanizeToolName(json.name),
+                  status: endStatus,
+                })
+              } else {
+                toolCallsMap.set(key, { ...existing, status: endStatus })
+              }
+            }
+            syncToolCallsRef()
+            uiDirty = true
+            scheduleUIFlush()
+            return false
+          }
+
+          if (eventType === 'final' && json.data && typeof json.data === 'object') {
             flushUI()
             const finalData = json.data
+            isThinkingStreaming = false
+            settleInFlightTools(toolCallsMap, 'success')
+            syncToolCallsRef()
+            const toolsSnapshot = snapshotToolCalls(toolCallOrder, toolCallsMap)
 
             const outputConfigs = streamingOptions?.outputConfigs
             const formattedOutputs: string[] = []
@@ -358,7 +554,11 @@ export function useChatStreaming() {
                   ? {
                       ...msg,
                       isStreaming: false,
+                      isThinkingStreaming: false,
+                      isToolStreaming: false,
                       content: finalContent ?? msg.content,
+                      thinking: accumulatedThinking || msg.thinking,
+                      toolCalls: toolsSnapshot ?? msg.toolCalls,
                       files: extractedFiles.length > 0 ? extractedFiles : undefined,
                     }
                   : msg
@@ -366,6 +566,8 @@ export function useChatStreaming() {
             )
 
             accumulatedTextRef.current = ''
+            accumulatedThinkingRef.current = ''
+            accumulatedToolCallsRef.current = []
             lastStreamedPositionRef.current = 0
             lastDisplayedPositionRef.current = 0
             audioStreamingActiveRef.current = false
@@ -374,11 +576,17 @@ export function useChatStreaming() {
             return true
           }
 
-          if (blockId && contentChunk) {
-            if (!messageIdMap.has(blockId)) {
+          // Answer text only — never append thinking/tool/unknown chunk frames blindly.
+          if (isAnswerChunkFrame(json) && contentChunk) {
+            if (blockId && !messageIdMap.has(blockId)) {
               messageIdMap.set(blockId, messageId)
             }
 
+            // First answer chunk settles thinking chrome (still visible, no longer “live”).
+            if (isThinkingStreaming) {
+              isThinkingStreaming = false
+            }
+
             accumulatedText += contentChunk
             accumulatedTextRef.current = accumulatedText
             logger.debug('[useChatStreaming] Received chunk', {
@@ -416,10 +624,6 @@ export function useChatStreaming() {
                 }
               }
             }
-          } else if (blockId && eventType === 'end') {
-            setMessages((prev) =>
-              prev.map((msg) => (msg.id === messageId ? { ...msg, isStreaming: false } : msg))
-            )
           }
         },
       })
@@ -442,10 +646,31 @@ export function useChatStreaming() {
         }
       }
     } catch (error) {
-      logger.error('Error processing stream:', error)
+      // Stop / timeout abort the shared fetch controller; body read then throws AbortError.
+      // Match chat.tsx + use-audio-streaming: expected cancel, not a hard failure.
+      if (error instanceof Error && error.name === 'AbortError') {
+        logger.info('Stream aborted by user or timeout')
+        settleInFlightTools(toolCallsMap, 'cancelled')
+      } else {
+        logger.error('Error processing stream:', error)
+        settleInFlightTools(toolCallsMap, 'error')
+      }
+      syncToolCallsRef()
       flushUI()
+      const toolsSnapshot = snapshotToolCalls(toolCallOrder, toolCallsMap)
       setMessages((prev) =>
-        prev.map((msg) => (msg.id === messageId ? { ...msg, isStreaming: false } : msg))
+        prev.map((msg) =>
+          msg.id === messageId
+            ? {
+                ...msg,
+                isStreaming: false,
+                isThinkingStreaming: false,
+                isToolStreaming: false,
+                thinking: accumulatedThinking || msg.thinking,
+                toolCalls: toolsSnapshot ?? msg.toolCalls,
+              }
+            : msg
+        )
       )
     } finally {
       if (uiRAF !== null) cancelAnimationFrame(uiRAF)
@@ -473,3 +698,5 @@ export function useChatStreaming() {
     handleStreamedResponse,
   }
 }
+
+export { isAnswerChunkFrame }
diff --git a/apps/sim/app/api/chat/[identifier]/otp/route.ts b/apps/sim/app/api/chat/[identifier]/otp/route.ts
index 248fcfa84ab..99dfa6069c2 100644
--- a/apps/sim/app/api/chat/[identifier]/otp/route.ts
+++ b/apps/sim/app/api/chat/[identifier]/otp/route.ts
@@ -160,6 +160,7 @@ export const PUT = withRouteHandler(
           authType: chat.authType,
           password: chat.password,
           outputConfigs: chat.outputConfigs,
+          includeThinking: chat.includeThinking,
         })
         .from(chat)
         .where(
@@ -209,6 +210,7 @@ export const PUT = withRouteHandler(
         customizations: deployment.customizations,
         authType: deployment.authType,
         outputConfigs: deployment.outputConfigs,
+        includeThinking: deployment.includeThinking ?? false,
       })
       setChatAuthCookie(response, deployment.id, deployment.authType, deployment.password)
 
diff --git a/apps/sim/app/api/chat/[identifier]/route.test.ts b/apps/sim/app/api/chat/[identifier]/route.test.ts
index 9eb104547ff..ea7dfe48b28 100644
--- a/apps/sim/app/api/chat/[identifier]/route.test.ts
+++ b/apps/sim/app/api/chat/[identifier]/route.test.ts
@@ -37,6 +37,7 @@ function createMockNextRequest(
     method,
     headers: headersObj,
     nextUrl: parsedUrl,
+    signal: AbortSignal.timeout(60_000),
     cookies: {
       get: vi.fn().mockReturnValue(undefined),
     },
@@ -114,6 +115,7 @@ vi.mock('@/lib/uploads', () => ({
 
 vi.mock('@/lib/workflows/streaming/streaming', () => ({
   createStreamingResponse: vi.fn().mockImplementation(async () => createMockStream()),
+  agentStreamProtocolResponseHeaders: vi.fn().mockReturnValue({}),
 }))
 
 vi.mock('@/lib/workflows/executor/execute-workflow', () => ({
@@ -150,6 +152,7 @@ describe('Chat Identifier API Route', () => {
         primaryColor: '#000000',
       },
       outputConfigs: [{ blockId: 'block-1', path: 'output' }],
+      includeThinking: false,
     },
   ]
 
@@ -443,9 +446,12 @@ describe('Chat Identifier API Route', () => {
       expect(createStreamingResponse).toHaveBeenCalledWith(
         expect.objectContaining({
           executeFn: expect.any(Function),
+          requestSignal: expect.any(AbortSignal),
+          requestHeaders: expect.anything(),
           streamConfig: expect.objectContaining({
             isSecureMode: true,
             workflowTriggerType: 'chat',
+            includeThinking: false,
           }),
         })
       )
diff --git a/apps/sim/app/api/chat/[identifier]/route.ts b/apps/sim/app/api/chat/[identifier]/route.ts
index 819c732db5b..1da492d81ff 100644
--- a/apps/sim/app/api/chat/[identifier]/route.ts
+++ b/apps/sim/app/api/chat/[identifier]/route.ts
@@ -27,6 +27,7 @@ interface ChatConfigSource {
   customizations: unknown
   authType: string | null
   outputConfigs: unknown
+  includeThinking?: boolean | null
 }
 
 function toChatConfigResponse(deployment: ChatConfigSource) {
@@ -37,6 +38,7 @@ function toChatConfigResponse(deployment: ChatConfigSource) {
     customizations: deployment.customizations,
     authType: deployment.authType,
     outputConfigs: deployment.outputConfigs,
+    includeThinking: deployment.includeThinking ?? false,
   }
 }
 
@@ -80,6 +82,7 @@ export const POST = withRouteHandler(
           password: chat.password,
           allowedEmails: chat.allowedEmails,
           outputConfigs: chat.outputConfigs,
+          includeThinking: chat.includeThinking,
         })
         .from(chat)
         .where(and(eq(chat.identifier, identifier), isNull(chat.archivedAt)))
@@ -216,7 +219,9 @@ export const POST = withRouteHandler(
           }
         }
 
-        const { createStreamingResponse } = await import('@/lib/workflows/streaming/streaming')
+        const { createStreamingResponse, agentStreamProtocolResponseHeaders } = await import(
+          '@/lib/workflows/streaming/streaming'
+        )
         const { executeWorkflow } = await import('@/lib/workflows/executor/execute-workflow')
         const { SSE_HEADERS } = await import('@/lib/core/utils/sse')
 
@@ -272,17 +277,21 @@ export const POST = withRouteHandler(
           variables: (workflowRecord?.variables as Record) ?? undefined,
         }
 
+        const includeThinking = deployment.includeThinking ?? false
         const stream = await createStreamingResponse({
           requestId,
           streamConfig: {
             selectedOutputs,
             isSecureMode: true,
             workflowTriggerType: 'chat',
+            includeThinking,
           },
           executionId,
           workspaceId,
           workflowId: deployment.workflowId,
           userId: resolvedActorUserId,
+          requestSignal: request.signal,
+          requestHeaders: request.headers,
           executeFn: async ({ onStream, onBlockComplete, abortSignal }) =>
             executeWorkflow(
               workflowForExecution,
@@ -300,6 +309,7 @@ export const POST = withRouteHandler(
                 abortSignal,
                 executionMode: 'stream',
                 billingAttribution,
+                includeThinking,
               },
               executionId
             ),
@@ -307,7 +317,13 @@ export const POST = withRouteHandler(
 
         const streamResponse = new NextResponse(stream, {
           status: 200,
-          headers: SSE_HEADERS,
+          headers: {
+            ...SSE_HEADERS,
+            ...agentStreamProtocolResponseHeaders({
+              includeThinking,
+              requestHeaders: request.headers,
+            }),
+          },
         })
         return streamResponse
       } catch (error: any) {
@@ -344,6 +360,7 @@ export const GET = withRouteHandler(
           password: chat.password,
           allowedEmails: chat.allowedEmails,
           outputConfigs: chat.outputConfigs,
+          includeThinking: chat.includeThinking,
         })
         .from(chat)
         .where(and(eq(chat.identifier, identifier), isNull(chat.archivedAt)))
diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts
index 707051d7e9f..44ebc5f4b60 100644
--- a/apps/sim/app/api/chat/manage/[id]/route.ts
+++ b/apps/sim/app/api/chat/manage/[id]/route.ts
@@ -105,6 +105,7 @@ export const PATCH = withRouteHandler(
         password,
         allowedEmails,
         outputConfigs,
+        includeThinking,
       } = validatedData
 
       if (workflowId && workflowId !== existingChat[0].workflowId) {
@@ -191,6 +192,10 @@ export const PATCH = withRouteHandler(
         updateData.outputConfigs = outputConfigs
       }
 
+      if (includeThinking !== undefined) {
+        updateData.includeThinking = includeThinking
+      }
+
       const emailCount = Array.isArray(updateData.allowedEmails)
         ? updateData.allowedEmails.length
         : undefined
@@ -204,6 +209,7 @@ export const PATCH = withRouteHandler(
         hasPassword: updateData.password !== undefined,
         emailCount,
         outputConfigsCount,
+        includeThinking: updateData.includeThinking,
       })
 
       await db.update(chat).set(updateData).where(eq(chat.id, chatId))
diff --git a/apps/sim/app/api/chat/route.ts b/apps/sim/app/api/chat/route.ts
index ca1f8019fe7..523efc84c5e 100644
--- a/apps/sim/app/api/chat/route.ts
+++ b/apps/sim/app/api/chat/route.ts
@@ -64,6 +64,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
       password,
       allowedEmails = [],
       outputConfigs = [],
+      includeThinking = false,
     } = parsed.data.body
 
     if (authType === 'password' && !password) {
@@ -112,6 +113,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
       password,
       allowedEmails,
       outputConfigs,
+      includeThinking,
       workspaceId: workflowRecord.workspaceId,
     })
 
diff --git a/apps/sim/app/api/providers/route.ts b/apps/sim/app/api/providers/route.ts
index 49b90074943..bd7fb52b69a 100644
--- a/apps/sim/app/api/providers/route.ts
+++ b/apps/sim/app/api/providers/route.ts
@@ -24,6 +24,7 @@ import {
 } from '@/ee/access-control/utils/permission-check'
 import type { StreamingExecution } from '@/executor/types'
 import { executeProviderRequest } from '@/providers'
+import { projectStreamingExecutionToByteStream } from '@/providers/stream-pump'
 
 const logger = createLogger('ProvidersAPI')
 
@@ -238,8 +239,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
       logger.info(`[${requestId}] Received StreamingExecution from provider`)
 
       // Extract the stream and execution data
-      const stream = streamingExec.stream
       const executionData = streamingExec.execution
+      // agent-events-v1 is an object stream — project final-turn answer bytes for HTTP.
+      const byteStream = projectStreamingExecutionToByteStream(streamingExec)
 
       // Attach the execution data as a custom header
       // We need to safely serialize the execution data to avoid circular references
@@ -288,7 +290,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
       }
 
       // Return the stream with execution data in a header
-      return new Response(stream, {
+      return new Response(byteStream, {
         headers: {
           'Content-Type': 'text/event-stream',
           'Cache-Control': 'no-cache',
diff --git a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts
index 6ea631652f6..102a2f0045a 100644
--- a/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts
+++ b/apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts
@@ -257,12 +257,15 @@ export const POST = withRouteHandler(
           streamConfig: {
             selectedOutputs: persistedSnapshot.selectedOutputs,
             timeoutMs: preprocessResult.executionTimeout?.sync,
+            includeThinking: persistedSnapshot.metadata.includeThinking === true,
           },
           executionId: enqueueResult.resumeExecutionId,
           workspaceId: workflow.workspaceId || undefined,
           workflowId,
           userId: enqueueResult.userId,
           allowLargeValueWorkflowScope: true,
+          requestSignal: request.signal,
+          requestHeaders: request.headers,
           executeFn: async ({ onStream, onBlockComplete, abortSignal }) =>
             PauseResumeManager.startResumeExecution({
               ...resumeArgs,
diff --git a/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts b/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts
index 188a5a47580..6e6cb33da23 100644
--- a/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts
+++ b/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts
@@ -82,6 +82,7 @@ describe('Workflow Chat Status Route', () => {
         authType: 'public',
         allowedEmails: [],
         outputConfigs: [{ blockId: 'agent-1', path: 'content' }],
+        includeThinking: false,
         password: 'secret',
         isActive: true,
       },
@@ -96,5 +97,6 @@ describe('Workflow Chat Status Route', () => {
     expect(data.deployment.id).toBe('chat-1')
     expect(data.deployment.hasPassword).toBe(true)
     expect(data.deployment.outputConfigs).toEqual([{ blockId: 'agent-1', path: 'content' }])
+    expect(data.deployment.includeThinking).toBe(false)
   })
 })
diff --git a/apps/sim/app/api/workflows/[id]/chat/status/route.ts b/apps/sim/app/api/workflows/[id]/chat/status/route.ts
index 49d555b813d..764f052f8c6 100644
--- a/apps/sim/app/api/workflows/[id]/chat/status/route.ts
+++ b/apps/sim/app/api/workflows/[id]/chat/status/route.ts
@@ -52,6 +52,7 @@ export const GET = withRouteHandler(
           authType: chat.authType,
           allowedEmails: chat.allowedEmails,
           outputConfigs: chat.outputConfigs,
+          includeThinking: chat.includeThinking,
           password: chat.password,
           isActive: chat.isActive,
         })
@@ -71,6 +72,7 @@ export const GET = withRouteHandler(
               authType: deploymentResults[0].authType,
               allowedEmails: deploymentResults[0].allowedEmails,
               outputConfigs: deploymentResults[0].outputConfigs,
+              includeThinking: deploymentResults[0].includeThinking ?? false,
               hasPassword: Boolean(deploymentResults[0].password),
             }
           : null
diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts
index a0ca81c0464..309e16c43c2 100644
--- a/apps/sim/app/api/workflows/[id]/execute/route.ts
+++ b/apps/sim/app/api/workflows/[id]/execute/route.ts
@@ -87,7 +87,8 @@ import {
   loadDeployedWorkflowState,
   loadWorkflowFromNormalizedTables,
 } from '@/lib/workflows/persistence/utils'
-import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
+import { createStreamingResponse, agentStreamProtocolResponseHeaders } from '@/lib/workflows/streaming/streaming'
+import { attachAgentStreamSink } from '@/lib/workflows/streaming/attach-agent-stream-sink'
 import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils'
 import { getWorkspaceBillingSettings } from '@/lib/workspaces/utils'
 import { executeWorkflowJob, type WorkflowExecutionPayload } from '@/background/workflow-execution'
@@ -1448,6 +1449,8 @@ async function handleExecutePost(
           includeFileBase64,
           base64MaxBytes,
           timeoutMs: preprocessResult.executionTimeout?.sync,
+          // Workflow API has no chat includeThinking policy — thinking frames stay off.
+          includeThinking: false,
         },
         executionId,
         largeValueExecutionIds,
@@ -1457,6 +1460,8 @@ async function handleExecutePost(
         workflowId,
         userId: actorUserId,
         allowLargeValueWorkflowScope,
+        requestSignal: req.signal,
+        requestHeaders: req.headers,
         executeFn: async ({ onStream, onBlockComplete, abortSignal }) =>
           executeWorkflow(
             streamWorkflow,
@@ -1488,7 +1493,13 @@ async function handleExecutePost(
       executionIdClaimCommitted = true
       return new NextResponse(stream, {
         status: 200,
-        headers: SSE_HEADERS,
+        headers: {
+          ...SSE_HEADERS,
+          ...agentStreamProtocolResponseHeaders({
+            includeThinking: false,
+            requestHeaders: req.headers,
+          }),
+        },
       })
     }
 
@@ -1529,7 +1540,11 @@ async function handleExecutePost(
           event: ExecutionEvent,
           terminalStatus?: TerminalExecutionStreamStatus
         ) => {
-          const isBuffered = event.type !== 'stream:chunk' && event.type !== 'stream:done'
+          const isBuffered =
+            event.type !== 'stream:chunk' &&
+            event.type !== 'stream:done' &&
+            event.type !== 'stream:thinking' &&
+            event.type !== 'stream:tool'
           let eventToSend = event
           if (isBuffered) {
             try {
@@ -1727,6 +1742,37 @@ async function handleExecutePost(
           const onStream = async (streamingExec: StreamingExecution) => {
             const blockId = (streamingExec.execution as any).blockId
 
+            // Sync window: attach sink before first await so pump delivers thinking/tools.
+            const unsubscribe = attachAgentStreamSink(streamingExec, {
+              onThinkingDelta: async (text) => {
+                await sendEvent({
+                  type: 'stream:thinking',
+                  timestamp: new Date().toISOString(),
+                  executionId,
+                  workflowId,
+                  data: { blockId, data: text },
+                })
+              },
+              onToolCallStart: async (id, name) => {
+                await sendEvent({
+                  type: 'stream:tool',
+                  timestamp: new Date().toISOString(),
+                  executionId,
+                  workflowId,
+                  data: { blockId, phase: 'start', id, name },
+                })
+              },
+              onToolCallEnd: async (id, name, status) => {
+                await sendEvent({
+                  type: 'stream:tool',
+                  timestamp: new Date().toISOString(),
+                  executionId,
+                  workflowId,
+                  data: { blockId, phase: 'end', id, name, status },
+                })
+              },
+            })
+
             const reader = streamingExec.stream.getReader()
             const decoder = new TextDecoder()
             const cancelReader = () => {
@@ -1767,6 +1813,7 @@ async function handleExecutePost(
                 reqLogger.error('Error streaming block content:', error)
               }
             } finally {
+              unsubscribe()
               timeoutController.signal.removeEventListener('abort', cancelReader)
               try {
                 await reader.cancel().catch(() => {})
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
index 43b1ea42d11..9ec3718d571 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
@@ -11,6 +11,7 @@ import {
   Label,
   Loader,
   Skeleton,
+  Switch,
   TagInput,
   type TagItem,
   Textarea,
@@ -83,6 +84,7 @@ const initialFormData: ChatFormData = {
   emails: [],
   welcomeMessage: 'Hi there! How can I help you today?',
   selectedOutputBlocks: [],
+  includeThinking: false,
 }
 
 export function ChatDeploy({
@@ -193,6 +195,7 @@ export function ChatDeploy({
               (config: { blockId: string; path: string }) => `${config.blockId}_${config.path}`
             )
           : [],
+        includeThinking: existingChat.includeThinking ?? false,
       })
 
       if (existingChat.customizations?.imageUrl) {
@@ -368,6 +371,23 @@ export function ChatDeploy({
             )}
           
+
+
+ +

+ Allow this chat to stream model thinking when the client opts in. Off by default. +

+
+ updateField('includeThinking', checked)} + aria-label='Include thinking' + /> +
+ + {!showInput && + (selectedEntry.agentStreamThinking || + (selectedEntry.agentStreamToolCalls && + selectedEntry.agentStreamToolCalls.length > 0)) && ( +
+ {selectedEntry.agentStreamThinking ? ( + + ) : null} + {selectedEntry.agentStreamToolCalls && + selectedEntry.agentStreamToolCalls.length > 0 ? ( + t.status === 'running') + )} + /> + ) : null} +
+ )} {shouldShowCodeDisplay ? ( () const activeBlockRefCounts = new Map() const streamedChunks = new Map() + const agentStreamThinking = new Map() + const agentStreamToolCalls = new Map< + string, + Map + >() + const agentStreamToolOrder = new Map() const accumulatedBlockLogs: BlockLog[] = [] const accumulatedBlockStates = new Map() const executedBlockIds = new Set() @@ -1167,8 +1174,93 @@ export function useWorkflowExecution() { } }, + onStreamThinking: (data) => { + const prev = agentStreamThinking.get(data.blockId) ?? '' + const next = prev + data.data + agentStreamThinking.set(data.blockId, next) + updateConsole( + data.blockId, + { + agentStreamThinking: next, + agentStreamActive: true, + }, + executionIdRef.current + ) + }, + + onStreamTool: (data) => { + const key = `${data.blockId}:${data.id}` + if (!agentStreamToolCalls.has(data.blockId)) { + agentStreamToolCalls.set(data.blockId, new Map()) + agentStreamToolOrder.set(data.blockId, []) + } + const map = agentStreamToolCalls.get(data.blockId)! + const order = agentStreamToolOrder.get(data.blockId)! + + if (data.phase === 'start') { + if (!map.has(key)) { + order.push(key) + } + map.set(key, { + key, + id: data.id, + name: data.name, + displayName: humanizeToolName(data.name), + status: 'running', + }) + } else { + const existing = map.get(key) + map.set(key, { + key, + id: data.id, + name: data.name, + displayName: existing?.displayName ?? humanizeToolName(data.name), + status: data.status ?? 'success', + }) + if (!existing) { + order.push(key) + } + } + + updateConsole( + data.blockId, + { + agentStreamToolCalls: order + .map((k) => map.get(k)) + .filter((t): t is NonNullable => Boolean(t)), + agentStreamActive: true, + }, + executionIdRef.current + ) + }, + onStreamDone: (data) => { logger.info('Stream done for block:', data.blockId) + const map = agentStreamToolCalls.get(data.blockId) + const order = agentStreamToolOrder.get(data.blockId) + if (map && order) { + for (const [key, tool] of map) { + if (tool.status === 'running') { + map.set(key, { ...tool, status: 'success' }) + } + } + updateConsole( + data.blockId, + { + agentStreamActive: false, + agentStreamToolCalls: order + .map((k) => map.get(k)) + .filter((t): t is NonNullable => Boolean(t)), + }, + executionIdRef.current + ) + } else { + updateConsole( + data.blockId, + { agentStreamActive: false }, + executionIdRef.current + ) + } }, onExecutionCompleted: (data) => { diff --git a/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx b/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx new file mode 100644 index 00000000000..cdb0e7e6ef6 --- /dev/null +++ b/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx @@ -0,0 +1,145 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@sim/emcn', () => ({ + cn: (...args: unknown[]) => args.filter(Boolean).join(' '), +})) + +vi.mock('@/lib/copilot/tools/tool-display', () => ({ + humanizeToolName: (name: string) => name, +})) + +import { AgentStreamThinkingChrome } from '@/components/agent-stream/agent-stream-chrome' + +function renderChrome(props: { + thinking: string + isStreaming?: boolean +}): { + container: HTMLDivElement + rerender: (next: { thinking: string; isStreaming?: boolean }) => void + unmount: () => void +} { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + document.body.appendChild(container) + const root: Root = createRoot(container) + + const mount = (p: { thinking: string; isStreaming?: boolean }) => { + act(() => { + root.render( + + ) + }) + } + + mount(props) + + return { + container, + rerender: (next) => mount(next), + unmount: () => { + act(() => { + root.unmount() + }) + container.remove() + }, + } +} + +describe('AgentStreamThinkingChrome', () => { + const mounts: Array<() => void> = [] + + afterEach(() => { + while (mounts.length) { + mounts.pop()?.() + } + }) + + it('opens while streaming with Thinking… label and scrollable body', () => { + const { container, unmount } = renderChrome({ + thinking: 'step one', + isStreaming: true, + }) + mounts.push(unmount) + + const toggle = container.querySelector( + '[data-testid="agent-stream-thinking-toggle"]' + ) as HTMLButtonElement + const body = container.querySelector( + '[data-testid="agent-stream-thinking-body"]' + ) as HTMLDivElement + + expect(toggle.getAttribute('aria-expanded')).toBe('true') + expect(toggle.textContent).toContain('Thinking…') + expect(body.className).toContain('max-h-40') + expect(body.className).toContain('overflow-y-auto') + expect(body.textContent).toContain('step one') + }) + + it('auto-collapses when streaming ends and shows Thought for a moment', () => { + const { container, rerender, unmount } = renderChrome({ + thinking: 'long internal chain', + isStreaming: true, + }) + mounts.push(unmount) + + rerender({ thinking: 'long internal chain', isStreaming: false }) + + const toggle = container.querySelector( + '[data-testid="agent-stream-thinking-toggle"]' + ) as HTMLButtonElement + + expect(toggle.getAttribute('aria-expanded')).toBe('false') + expect(toggle.textContent).toContain('Thought for a moment') + }) + + it('stays open after manual reopen once collapsed', () => { + const { container, rerender, unmount } = renderChrome({ + thinking: 'reason', + isStreaming: true, + }) + mounts.push(unmount) + + rerender({ thinking: 'reason', isStreaming: false }) + + const toggle = container.querySelector( + '[data-testid="agent-stream-thinking-toggle"]' + ) as HTMLButtonElement + expect(toggle.getAttribute('aria-expanded')).toBe('false') + + act(() => { + toggle.click() + }) + + expect(toggle.getAttribute('aria-expanded')).toBe('true') + expect( + container.querySelector('[data-testid="agent-stream-thinking-body"]')?.textContent + ).toContain('reason') + + // Re-render with same done state should not force-close a user pin. + rerender({ thinking: 'reason', isStreaming: false }) + expect(toggle.getAttribute('aria-expanded')).toBe('true') + }) + + it('re-opens when a new streaming phase starts', () => { + const { container, rerender, unmount } = renderChrome({ + thinking: 'first', + isStreaming: false, + }) + mounts.push(unmount) + + const toggle = container.querySelector( + '[data-testid="agent-stream-thinking-toggle"]' + ) as HTMLButtonElement + // Initial non-streaming starts closed. + expect(toggle.getAttribute('aria-expanded')).toBe('false') + + rerender({ thinking: 'first then more', isStreaming: true }) + expect(toggle.getAttribute('aria-expanded')).toBe('true') + expect(toggle.textContent).toContain('Thinking…') + }) +}) diff --git a/apps/sim/components/agent-stream/agent-stream-chrome.tsx b/apps/sim/components/agent-stream/agent-stream-chrome.tsx new file mode 100644 index 00000000000..24dd39f1476 --- /dev/null +++ b/apps/sim/components/agent-stream/agent-stream-chrome.tsx @@ -0,0 +1,203 @@ +'use client' + +import { useEffect, useLayoutEffect, useRef, useState } from 'react' +import { Check, ChevronDown, Circle, Square, X } from 'lucide-react' +import { cn } from '@sim/emcn' +import { humanizeToolName } from '@/lib/copilot/tools/tool-display' + +export type AgentStreamToolStatus = 'running' | 'success' | 'error' | 'cancelled' + +export interface AgentStreamToolCall { + key: string + id: string + name: string + displayName?: string + status: AgentStreamToolStatus +} + +/** Distance from bottom (px) within which we keep following new thinking text. */ +const STICK_TO_BOTTOM_THRESHOLD_PX = 24 + +export function AgentStreamThinkingChrome({ + thinking, + isStreaming = false, +}: { + thinking: string + isStreaming?: boolean +}) { + const [open, setOpen] = useState(!!isStreaming) + /** After auto-collapse, a manual open pins the panel until the user closes it. */ + const [userPinnedOpen, setUserPinnedOpen] = useState(false) + const [stickToBottom, setStickToBottom] = useState(true) + const [overflowing, setOverflowing] = useState(false) + const scrollRef = useRef(null) + const wasStreamingRef = useRef(!!isStreaming) + + useEffect(() => { + const wasStreaming = wasStreamingRef.current + wasStreamingRef.current = !!isStreaming + + if (isStreaming) { + setOpen(true) + setUserPinnedOpen(false) + setStickToBottom(true) + return + } + + // Auto-collapse when live thinking ends (answer started), unless user pinned open. + if (wasStreaming && !isStreaming && !userPinnedOpen) { + setOpen(false) + } + }, [isStreaming, userPinnedOpen]) + + useLayoutEffect(() => { + const el = scrollRef.current + if (!el || !open) return + + setOverflowing(el.scrollHeight > el.clientHeight + 1) + + if (isStreaming && stickToBottom) { + el.scrollTop = el.scrollHeight + } + }, [thinking, open, isStreaming, stickToBottom]) + + const handleToggle = () => { + setOpen((prev) => { + const next = !prev + if (!isStreaming) { + setUserPinnedOpen(next) + } else { + setUserPinnedOpen(false) + } + if (next) { + setStickToBottom(true) + } + return next + }) + } + + const handleScroll = () => { + const el = scrollRef.current + if (!el) return + const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight + const nearBottom = distanceFromBottom <= STICK_TO_BOTTOM_THRESHOLD_PX + setStickToBottom(nearBottom) + setOverflowing(el.scrollHeight > el.clientHeight + 1) + } + + const label = isStreaming ? 'Thinking…' : 'Thought for a moment' + + return ( +
+ + +
+
+
+
+ {thinking} +
+ {open && isStreaming && overflowing && ( +
+ )} +
+
+
+
+ ) +} + +function ToolStatusIcon({ status }: { status: AgentStreamToolStatus }) { + if (status === 'success') { + return + } + if (status === 'error') { + return + } + if (status === 'cancelled') { + return + } + return +} + +export function AgentStreamToolCallsChrome({ + toolCalls, + isStreaming, +}: { + toolCalls: AgentStreamToolCall[] + isStreaming?: boolean +}) { + const [open, setOpen] = useState(!!isStreaming) + + useEffect(() => { + if (isStreaming) { + setOpen(true) + } + }, [isStreaming]) + + return ( +
+ + {open && ( +
    + {toolCalls.map((tool) => ( +
  • + + + {tool.displayName || humanizeToolName(tool.name)} + {tool.status === 'running' ? '…' : ''} + +
  • + ))} +
+ )} +
+ ) +} + +export function resolveAgentStreamToolDisplayName(name: string): string { + return humanizeToolName(name) +} diff --git a/apps/sim/executor/execution/block-executor.test.ts b/apps/sim/executor/execution/block-executor.test.ts index 94d291a18c5..5c71be6c9a6 100644 --- a/apps/sim/executor/execution/block-executor.test.ts +++ b/apps/sim/executor/execution/block-executor.test.ts @@ -26,6 +26,14 @@ vi.mock('@/lib/uploads', () => ({ }, })) +vi.mock('@/lib/logs/execution/pii-redaction', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + redactObjectStrings: vi.fn(actual.redactObjectStrings), + } +}) + function createBlock(): SerializedBlock { return { id: 'function-block-1', @@ -380,3 +388,284 @@ describe('BlockExecutor', () => { expect(state.getBlockOutput(block.id)).toEqual(output) }) }) + +describe('BlockExecutor streaming pump (Step 3)', () => { + function createAgentBlock(): SerializedBlock { + return { + id: 'agent-block-1', + metadata: { id: BlockType.AGENT, name: 'Agent' }, + position: { x: 0, y: 0 }, + config: { tool: BlockType.AGENT, params: {} }, + inputs: {}, + outputs: {}, + enabled: true, + } + } + + function createExecutor(handler: BlockHandler) { + const block = createAgentBlock() + const workflow: SerializedWorkflow = { + version: '1', + blocks: [block], + connections: [], + loops: {}, + parallels: {}, + } + const state = new ExecutionState() + const resolver = new VariableResolver(workflow, {}, state) + const executor = new BlockExecutor( + [handler], + resolver, + { + workspaceId: 'workspace-1', + executionId: 'execution-1', + userId: 'user-1', + metadata: { + requestId: 'request-1', + executionId: 'execution-1', + workflowId: 'workflow-1', + workspaceId: 'workspace-1', + userId: 'user-1', + triggerType: 'manual', + useDraftState: false, + startTime: new Date().toISOString(), + }, + }, + state + ) + return { executor, block, state } + } + + function createAgentEventsStreamingHandler(options: { + events: Array> + attachThinkingOnDrain?: string + failAfterText?: string + onFullContent?: (content: string) => void | Promise + }): BlockHandler { + return { + canHandle: () => true, + execute: async () => { + const timeSegment: Record = { + type: 'model', + name: 'claude-test', + startTime: Date.now(), + endTime: Date.now(), + duration: 1, + } + const output = { + content: '', + model: 'claude-test', + tokens: { input: 1, output: 2, total: 3 }, + providerTiming: { + startTime: new Date().toISOString(), + endTime: new Date().toISOString(), + duration: 1, + timeSegments: [timeSegment], + }, + cost: { input: 0, output: 0, total: 0 }, + } + + const stream = new ReadableStream({ + start(controller) { + if (options.failAfterText) { + controller.enqueue({ + type: 'text_delta', + text: options.failAfterText, + turn: 'final', + }) + controller.error(new Error('provider reset')) + return + } + for (const event of options.events) { + controller.enqueue(event) + } + if (options.attachThinkingOnDrain) { + timeSegment.thinkingContent = options.attachThinkingOnDrain + } + controller.close() + }, + }) + + return { + stream, + streamFormat: 'agent-events-v1' as const, + execution: { + success: true, + output, + logs: [], + metadata: { + startTime: new Date().toISOString(), + endTime: new Date().toISOString(), + duration: 1, + }, + }, + onFullContent: options.onFullContent, + } + }, + } + } + + it('projects answer text to onStream and content; sink gets full timeline', async () => { + const onFullContent = vi.fn() + const handler = createAgentEventsStreamingHandler({ + events: [ + { type: 'thinking_delta', text: 'hmm ' }, + { type: 'thinking_delta', text: 'yes' }, + { type: 'text_delta', text: 'Hello ', turn: 'final' }, + { type: 'text_delta', text: 'world', turn: 'final' }, + ], + attachThinkingOnDrain: 'hmm yes', + onFullContent, + }) + const { executor, block, state } = createExecutor(handler) + const ctx = createContext(state) + const forwarded: string[] = [] + const sinkEvents: Array> = [] + + ctx.onStream = async (streamingExec) => { + expect(streamingExec.streamFormat).toBe('text') + streamingExec.subscribe?.({ + onEvent: async (event) => { + sinkEvents.push(event as Record) + }, + }) + const reader = streamingExec.stream.getReader() + const decoder = new TextDecoder() + while (true) { + const { done, value } = await reader.read() + if (done) break + forwarded.push(decoder.decode(value, { stream: true })) + } + } + + await executor.execute(ctx, createNode(block), block) + + expect(forwarded.join('')).toBe('Hello world') + expect(state.getBlockOutput(block.id)?.content).toBe('Hello world') + expect(onFullContent).toHaveBeenCalledWith('Hello world') + expect(sinkEvents).toEqual([ + { type: 'thinking_delta', text: 'hmm ' }, + { type: 'thinking_delta', text: 'yes' }, + { type: 'text_delta', text: 'Hello ', turn: 'final' }, + { type: 'text_delta', text: 'world', turn: 'final' }, + ]) + expect( + state.getBlockOutput(block.id)?.providerTiming?.timeSegments?.[0]?.thinkingContent + ).toBe('hmm yes') + }) + + it('drains without onStream and still persists answer content', async () => { + const handler = createAgentEventsStreamingHandler({ + events: [{ type: 'text_delta', text: 'offline answer', turn: 'final' }], + }) + const { executor, block, state } = createExecutor(handler) + const ctx = createContext(state) + + await executor.execute(ctx, createNode(block), block) + + expect(state.getBlockOutput(block.id)?.content).toBe('offline answer') + }) + + it('throws on mid-stream provider error (no truncated success)', async () => { + const handler = createAgentEventsStreamingHandler({ + failAfterText: 'partial', + }) + const { executor, block, state } = createExecutor(handler) + const ctx = createContext(state) + ctx.onStream = async (streamingExec) => { + const reader = streamingExec.stream.getReader() + try { + while (true) { + const { done } = await reader.read() + if (done) break + } + } catch { + // consumer may see the error; block must still fail + } + } + + await expect(executor.execute(ctx, createNode(block), block)).rejects.toThrow('provider reset') + expect(state.getBlockOutput(block.id)?.content).not.toBe('partial') + }) + + it('soft-completes on user abort with drained answer text (no failed block)', async () => { + const abortController = new AbortController() + const handler = createAgentEventsStreamingHandler({ + events: [ + { type: 'text_delta', text: 'partial answer', turn: 'final' }, + { type: 'thinking_delta', text: 'more' }, + ], + }) + // Override execute to abort mid-stream after first text chunk is projected. + const originalExecute = handler.execute.bind(handler) + handler.execute = async (ctx, block, inputs) => { + const result = (await originalExecute(ctx, block, inputs)) as { + stream: ReadableStream + streamFormat: 'agent-events-v1' + execution: { success: boolean; output: Record; logs: unknown[]; metadata: Record } + } + // Abort after a tick so the pump can project the first text_delta. + queueMicrotask(() => abortController.abort('user')) + return result + } + + const { executor, block, state } = createExecutor(handler) + const ctx = createContext(state) + ctx.abortSignal = abortController.signal + ctx.onStream = async (streamingExec) => { + streamingExec.subscribe?.({ onEvent: async () => {} }) + const reader = streamingExec.stream.getReader() + try { + while (true) { + const { done } = await reader.read() + if (done) break + } + } catch { + // abort may cancel the text stream + } + } + + await executor.execute(ctx, createNode(block), block) + + const output = state.getBlockOutput(block.id) + expect(output?.error).toBeUndefined() + // May or may not have content depending on race; must not be a failed error output. + expect(output).not.toMatchObject({ error: expect.any(String) }) + }) + + it('with PII redaction: no live forward and strips thinking from traces', async () => { + const { redactObjectStrings } = await import('@/lib/logs/execution/pii-redaction') + vi.mocked(redactObjectStrings).mockImplementation(async (value) => { + if (typeof value === 'string') { + return `[masked]${value}` as never + } + // Object walk is exercised elsewhere; keep streaming-stage string mask as-is. + return value as never + }) + + const handler = createAgentEventsStreamingHandler({ + events: [ + { type: 'thinking_delta', text: 'secret thought' }, + { type: 'text_delta', text: 'alice@example.com said hi', turn: 'final' }, + ], + attachThinkingOnDrain: 'secret thought', + }) + const { executor, block, state } = createExecutor(handler) + const ctx = createContext(state) + const onStream = vi.fn() + ctx.onStream = onStream + ctx.piiBlockOutputRedaction = { + enabled: true, + entityTypes: ['EMAIL_ADDRESS'], + language: 'en', + } + + await executor.execute(ctx, createNode(block), block) + + expect(onStream).not.toHaveBeenCalled() + expect(state.getBlockOutput(block.id)?.content).toBe('[masked]alice@example.com said hi') + expect( + state.getBlockOutput(block.id)?.providerTiming?.timeSegments?.[0]?.thinkingContent + ).toBeUndefined() + }) +}) diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 29aef72d49e..1dd2466557e 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -48,6 +48,7 @@ import { getIterationContext, } from '@/executor/utils/iteration-context' import { isJSONString } from '@/executor/utils/json' +import { createAgentStreamPump } from '@/providers/stream-pump' import { filterOutputForLog } from '@/executor/utils/output-filter' import { buildBranchNodeId, @@ -188,22 +189,18 @@ export class BlockExecutor { if (isStreamingExecution) { const streamingExec = output as StreamingExecution - // The stream must still be drained to populate `execution.output`, but - // forwarding raw chunks to the client (or persisting them to memory) - // before redaction would leak PII. When block-output redaction is on we - // drain in buffer-only mode (no `onStream`, content masked before it's - // stored); the masked final output reaches the client via block-complete. - if (ctx.onStream) { - await this.handleStreamingExecution( - ctx, - node, - block, - streamingExec, - resolvedInputs, - normalizeStringArray(ctx.selectedOutputs), - !ctx.piiBlockOutputRedaction?.enabled - ) - } + // Always drain via the agent stream pump (tokens/cost/timing callbacks), + // even with no `onStream`. When block-output redaction is on we do not + // live-forward chunks; content is masked before persist and the masked + // final output reaches the client via block-complete. + await this.handleStreamingExecution( + ctx, + node, + block, + streamingExec, + resolvedInputs, + normalizeStringArray(ctx.selectedOutputs) + ) normalizedOutput = this.normalizeOutput( streamingExec.execution.output ?? streamingExec.execution @@ -382,6 +379,51 @@ export class BlockExecutor { ? inputsForLog : ((block.config?.params as Record | undefined) ?? {}) + // Routine user Stop: don't paint a failed agent block (workflow is already cancelled). + // Timeouts abort with reason `'timeout'` (see createTimeoutAbortController). + const isAbort = + (error instanceof DOMException && error.name === 'AbortError') || + (error instanceof Error && error.name === 'AbortError') + const isTimeout = ctx.abortSignal?.reason === 'timeout' + if (isAbort && !isTimeout && ctx.abortSignal?.aborted) { + const softOutput: NormalizedBlockOutput = { + content: '', + } + + this.setNodeOutput(node, softOutput, duration) + + if (blockLog) { + blockLog.endedAt = endedAt + blockLog.durationMs = duration + blockLog.success = true + blockLog.error = undefined + blockLog.input = this.sanitizeInputsForLog(input, block.metadata?.id) + blockLog.output = filterOutputForLog(block.metadata?.id || '', softOutput, { block }) + } + + this.execLogger.info('Block stream aborted by client; soft-completing', { + blockId: node.id, + blockType: block.metadata?.id, + }) + + if (!isSentinel && blockLog) { + this.fireBlockCompleteCallback( + blockStartPromise, + ctx, + node, + block, + this.sanitizeInputsForLog(input, block.metadata?.id), + filterOutputForLog(block.metadata?.id || '', softOutput, { block }), + duration, + blockLog.startedAt, + blockLog.executionOrder, + blockLog.endedAt + ) + } + + return softOutput + } + const errorOutput: NormalizedBlockOutput = { error: errorMessage, } @@ -773,119 +815,113 @@ export class BlockExecutor { block: SerializedBlock, streamingExec: StreamingExecution, resolvedInputs: Record, - selectedOutputs: string[], - forwardToClient = true + selectedOutputs: string[] ): Promise { const blockId = node.id + const piiEnabled = Boolean(ctx.piiBlockOutputRedaction?.enabled) + // Live-forward only when a client stream exists and PII redaction is off. + const forwardToClient = Boolean(ctx.onStream) && !piiEnabled const responseFormat = resolvedInputs?.responseFormat ?? (block.config?.params as Record | undefined)?.responseFormat ?? (block.config as Record | undefined)?.responseFormat - const sourceReader = streamingExec.stream.getReader() - const decoder = new TextDecoder() - const accumulated: string[] = [] - let drainError: unknown - let sourceFullyDrained = false - - if (forwardToClient) { - const clientSource = new ReadableStream({ - async pull(controller) { - try { - const { done, value } = await sourceReader.read() - if (done) { - const tail = decoder.decode() - if (tail) accumulated.push(tail) - sourceFullyDrained = true - controller.close() - return - } - accumulated.push(decoder.decode(value, { stream: true })) - controller.enqueue(value) - } catch (error) { - drainError = error - controller.error(error) - } - }, - async cancel(reason) { - try { - await sourceReader.cancel(reason) - } catch {} - }, - }) + const streamFormat = streamingExec.streamFormat ?? 'text' + const pump = createAgentStreamPump({ + source: streamingExec.stream, + streamFormat, + // No live consumer → sink-mode so we never buffer into an unread text stream. + sinkMode: !forwardToClient, + abortSignal: ctx.abortSignal, + }) - const processedClientStream = streamingResponseFormatProcessor.processStream( - clientSource, + let onStreamPromise: Promise | undefined + let processedClientStream: ReadableStream | undefined + + if (forwardToClient && ctx.onStream && pump.textStream) { + processedClientStream = streamingResponseFormatProcessor.processStream( + pump.textStream, blockId, selectedOutputs, responseFormat ) - try { - await ctx.onStream?.({ + // Start onStream without awaiting so a sync `subscribe(sink)` can run before + // the first provider pull, then read the projected text stream concurrently + // with `pump.run()`. + onStreamPromise = ctx + .onStream({ + ...streamingExec, stream: processedClientStream, - execution: streamingExec.execution, + streamFormat: 'text', + subscribe: pump.subscribe, }) - } catch (error) { - this.execLogger.error('Error in onStream callback', { blockId, error }) - await processedClientStream.cancel().catch(() => {}) - } finally { - try { - sourceReader.releaseLock() - } catch {} - } - } else { - // Buffer-only drain: consume the source so `execution.output` is complete, - // but never forward raw chunks to the client (block-output redaction is on). - try { - while (true) { - const { done, value } = await sourceReader.read() - if (done) { - const tail = decoder.decode() - if (tail) accumulated.push(tail) - sourceFullyDrained = true - break - } - accumulated.push(decoder.decode(value, { stream: true })) - } - } catch (error) { - drainError = error - } finally { - try { - sourceReader.releaseLock() - } catch {} + .catch(async (error) => { + this.execLogger.error('Error in onStream callback', { blockId, error }) + await processedClientStream?.cancel().catch(() => {}) + }) + } + + let pumpResult + try { + pumpResult = await pump.run() + } catch (error) { + this.execLogger.error('Error reading stream for block', { blockId, error }) + if (onStreamPromise) { + await onStreamPromise.catch(() => {}) } + throw error instanceof Error ? error : new Error(String(error)) + } + + if (onStreamPromise) { + await onStreamPromise + } + + // Timeout still fails the block. User Stop soft-completes with any drained + // answer text so logs don't show a scary red agent block for a routine cancel + // (workflow status remains `cancelled` via the engine abort flag). + if (pumpResult.cancelled && pumpResult.cancelReason === 'timeout') { + throw new DOMException('Provider request timed out', 'AbortError') } - if (drainError) { - this.execLogger.error('Error reading stream for block', { blockId, error: drainError }) + // Provider onComplete may have attached thinking to timing segments during drain. + // Under PII redaction, never retain raw thinking in traces. + if (piiEnabled) { + stripThinkingContentFromOutput(streamingExec.execution?.output) + } + + // User/unknown cancel: persist truncated answer when present, then return. + if (pumpResult.cancelled) { + const truncated = pumpResult.answerText + if (truncated && streamingExec.execution?.output) { + streamingExec.execution.output.content = truncated + } + this.execLogger.info('Stream cancelled by client; soft-completing agent block', { + blockId, + cancelReason: pumpResult.cancelReason, + hasContent: Boolean(truncated), + }) return } - // If the onStream consumer exited before the source drained (e.g. it caught - // an internal error and returned normally), `accumulated` holds a truncated - // response. Persisting that to memory or setting it as the block output - // would corrupt downstream state — skip and log instead. - if (!sourceFullyDrained) { + // If the pump did not fully drain (should be rare when not cancelled), skip + // persistence of potentially truncated answer text. + if (!pumpResult.fullyDrained) { this.execLogger.warn( 'Stream consumer exited before source drained; skipping content persistence', - { - blockId, - } + { blockId } ) return } - let fullContent = accumulated.join('') + let fullContent = pumpResult.answerText if (!fullContent) { return } - if (!forwardToClient && ctx.piiBlockOutputRedaction?.enabled) { - // Mask before the content is written to `execution.output` or persisted to - // memory via `onFullContent`, so the streamed agent response can't leak PII - // through either path. The block-output redaction below is then idempotent. + if (piiEnabled && ctx.piiBlockOutputRedaction) { + // Mask before writing to `execution.output` or `onFullContent`. fullContent = await redactObjectStrings(fullContent, { entityTypes: ctx.piiBlockOutputRedaction.entityTypes, language: ctx.piiBlockOutputRedaction.language, @@ -929,3 +965,16 @@ export class BlockExecutor { } } } + +/** Removes retained thinking from provider timing segments (PII safe default). */ +function stripThinkingContentFromOutput(output: unknown): void { + if (!output || typeof output !== 'object') return + const providerTiming = (output as { providerTiming?: { timeSegments?: unknown } }).providerTiming + const segments = providerTiming?.timeSegments + if (!Array.isArray(segments)) return + for (const segment of segments) { + if (segment && typeof segment === 'object' && 'thinkingContent' in segment) { + delete (segment as { thinkingContent?: string }).thinkingContent + } + } +} diff --git a/apps/sim/executor/execution/types.ts b/apps/sim/executor/execution/types.ts index b93ad107c5c..9ebda7cd0ca 100644 --- a/apps/sim/executor/execution/types.ts +++ b/apps/sim/executor/execution/types.ts @@ -45,6 +45,11 @@ export interface ExecutionMetadata { callChain?: string[] correlation?: AsyncExecutionCorrelation executionMode?: 'sync' | 'stream' | 'async' + /** + * Deployed-chat dual gate for thinking SSE. Persisted so HITL resume can + * re-enable thinking frames without hardcoding false. + */ + includeThinking?: boolean } export interface SerializableExecutionState { diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index 6990a719bf2..5ee3e2dad31 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -2243,4 +2243,38 @@ describe('AgentBlockHandler', () => { }) }) }) + + describe('wrapStreamForMemoryPersistence envelope (Step 3)', () => { + it('preserves streamFormat and subscribe via object spread', () => { + const handler = new AgentBlockHandler() + const subscribe = vi.fn() + const streamingExec: StreamingExecution = { + stream: new ReadableStream(), + streamFormat: 'agent-events-v1', + subscribe, + execution: { + success: true, + output: { content: '' }, + logs: [], + metadata: { startTime: '', endTime: '', duration: 0 }, + }, + } + + const wrapped = ( + handler as unknown as { + wrapStreamForMemoryPersistence: ( + ctx: ExecutionContext, + inputs: Record, + exec: StreamingExecution + ) => StreamingExecution + } + ).wrapStreamForMemoryPersistence({} as ExecutionContext, {}, streamingExec) + + expect(wrapped.streamFormat).toBe('agent-events-v1') + expect(wrapped.subscribe).toBe(subscribe) + expect(wrapped.stream).toBe(streamingExec.stream) + expect(wrapped.execution).toBe(streamingExec.execution) + expect(typeof wrapped.onFullContent).toBe('function') + }) + }) }) diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 2373b8e6527..533dd336fdb 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -952,6 +952,19 @@ export class AgentBlockHandler implements BlockHandler { verbosity: inputs.verbosity, thinkingLevel: inputs.thinkingLevel, previousInteractionId: inputs.previousInteractionId, + // Live tool lifecycle on agent-events stream. Only providers with a real + // streaming tool loop (Step 7/9) — others still get agent-events on text + // streams without streamToolCalls. + streamToolCalls: + streaming && + formattedTools.length > 0 && + (providerId === 'anthropic' || + providerId === 'azure-anthropic' || + providerId === 'groq' || + providerId === 'deepseek' || + providerId === 'google' || + providerId === 'vertex' || + providerId === 'bedrock'), } } @@ -1025,6 +1038,7 @@ export class AgentBlockHandler implements BlockHandler { verbosity: providerRequest.verbosity, thinkingLevel: providerRequest.thinkingLevel, previousInteractionId: providerRequest.previousInteractionId, + streamToolCalls: providerRequest.streamToolCalls, abortSignal: ctx.abortSignal, }) @@ -1084,8 +1098,7 @@ export class AgentBlockHandler implements BlockHandler { streamingExec: StreamingExecution ): StreamingExecution { return { - stream: streamingExec.stream, - execution: streamingExec.execution, + ...streamingExec, onFullContent: async (content: string) => { if (!content.trim()) return try { diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index babb2a4e6c6..8f0c9dcf0cd 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -10,6 +10,10 @@ import type { SerializableExecutionState, } from '@/executor/execution/types' import type { RunFromBlockContext } from '@/executor/utils/run-from-block' +import type { + AgentStreamSink, + UnsubscribeAgentStreamSink, +} from '@/providers/stream-events' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import type { SubflowType } from '@/stores/workflows/workflow/types' @@ -497,7 +501,25 @@ export interface ExecutionResult { } export interface StreamingExecution { + /** + * Provider stream payload. Format is declared by {@link streamFormat}: + * - `'text'` (default): UTF-8 answer bytes (`ReadableStream`) + * - `'agent-events-v1'`: in-process `ReadableStream` of `AgentStreamEvent` objects + * + * Never sniff the payload; always read {@link streamFormat}. + * After the executor pump, {@link stream} is always projected UTF-8 answer text. + */ stream: ReadableStream + /** + * Discriminator for {@link stream}. Defaults to `'text'` when omitted so + * existing providers remain byte-stream consumers without changes. + */ + streamFormat?: 'text' | 'agent-events-v1' + /** + * Optional sink subscription installed synchronously during `onStream` before + * the executor pump starts draining. Late subscribers receive future events only. + */ + subscribe?: (sink: AgentStreamSink) => UnsubscribeAgentStreamSink execution: ExecutionResult & { isStreaming?: boolean } /** * Invoked with the assembled response text after the stream drains. Lets agent diff --git a/apps/sim/hooks/queries/chats.ts b/apps/sim/hooks/queries/chats.ts index 1c1da29faa1..046bfcc9dbb 100644 --- a/apps/sim/hooks/queries/chats.ts +++ b/apps/sim/hooks/queries/chats.ts @@ -175,6 +175,8 @@ export interface ChatFormData { emails: string[] welcomeMessage: string selectedOutputBlocks: string[] + /** When true, thinking may be streamed to clients that opt into agent-events-v1. Default false. */ + includeThinking: boolean } /** @@ -263,6 +265,7 @@ function buildChatPayload( allowedEmails: formData.authType === 'email' || formData.authType === 'sso' ? formData.emails : [], outputConfigs, + includeThinking: formData.includeThinking, } } diff --git a/apps/sim/hooks/use-execution-stream.test.ts b/apps/sim/hooks/use-execution-stream.test.ts index f38f028c805..eae38ac3028 100644 --- a/apps/sim/hooks/use-execution-stream.test.ts +++ b/apps/sim/hooks/use-execution-stream.test.ts @@ -53,6 +53,61 @@ describe('processSSEStream', () => { expect(order).toEqual(['handler:start', 'handler:end', 'event-id']) }) + it('routes stream:thinking and stream:tool without requiring event ids', async () => { + const onStreamThinking = vi.fn() + const onStreamTool = vi.fn() + const onStreamChunk = vi.fn() + const onEventId = vi.fn() + + const events: ExecutionEvent[] = [ + { + type: 'stream:thinking', + timestamp: new Date().toISOString(), + executionId: 'exec-1', + workflowId: 'wf-1', + data: { blockId: 'agent-1', data: 'reasoning ' }, + }, + { + type: 'stream:tool', + timestamp: new Date().toISOString(), + executionId: 'exec-1', + workflowId: 'wf-1', + data: { + blockId: 'agent-1', + phase: 'start', + id: 'tool_1', + name: 'http_request', + }, + }, + { + type: 'stream:chunk', + timestamp: new Date().toISOString(), + executionId: 'exec-1', + workflowId: 'wf-1', + data: { blockId: 'agent-1', chunk: 'answer' }, + }, + ] + + await processSSEStream( + streamEvents(events).getReader(), + { onStreamThinking, onStreamTool, onStreamChunk, onEventId }, + 'test' + ) + + expect(onStreamThinking).toHaveBeenCalledWith({ + blockId: 'agent-1', + data: 'reasoning ', + }) + expect(onStreamTool).toHaveBeenCalledWith({ + blockId: 'agent-1', + phase: 'start', + id: 'tool_1', + name: 'http_request', + }) + expect(onStreamChunk).toHaveBeenCalledWith({ blockId: 'agent-1', chunk: 'answer' }) + expect(onEventId).not.toHaveBeenCalled() + }) + it('propagates callback failures without acknowledging the event id', async () => { const event: ExecutionEvent = { type: 'block:started', diff --git a/apps/sim/hooks/use-execution-stream.ts b/apps/sim/hooks/use-execution-stream.ts index ffe862c4710..c00242df988 100644 --- a/apps/sim/hooks/use-execution-stream.ts +++ b/apps/sim/hooks/use-execution-stream.ts @@ -15,6 +15,8 @@ import type { ExecutionStartedData, StreamChunkData, StreamDoneData, + StreamThinkingData, + StreamToolData, } from '@/lib/workflows/executor/execution-events' import type { SerializableExecutionState } from '@/executor/execution/types' @@ -124,6 +126,12 @@ export async function processSSEStream( case 'stream:done': await callbacks.onStreamDone?.(event.data) break + case 'stream:thinking': + await callbacks.onStreamThinking?.(event.data) + break + case 'stream:tool': + await callbacks.onStreamTool?.(event.data) + break default: logger.warn('Unknown event type:', (event as any).type) } @@ -165,6 +173,8 @@ export interface ExecutionStreamCallbacks { onBlockChildWorkflowStarted?: (data: BlockChildWorkflowStartedData) => void | Promise onStreamChunk?: (data: StreamChunkData) => void | Promise onStreamDone?: (data: StreamDoneData) => void | Promise + onStreamThinking?: (data: StreamThinkingData) => void | Promise + onStreamTool?: (data: StreamToolData) => void | Promise onEventId?: (eventId: number) => void | Promise } diff --git a/apps/sim/lib/api/contracts/chats.include-thinking.test.ts b/apps/sim/lib/api/contracts/chats.include-thinking.test.ts new file mode 100644 index 00000000000..903e99e0baa --- /dev/null +++ b/apps/sim/lib/api/contracts/chats.include-thinking.test.ts @@ -0,0 +1,76 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + createChatBodySchema, + deployedChatConfigSchema, + updateChatBodySchema, +} from '@/lib/api/contracts/chats' +import { chatDetailSchema } from '@/lib/api/contracts/deployments' + +describe('chat includeThinking contracts (Step 4)', () => { + it('create defaults includeThinking to false', () => { + const parsed = createChatBodySchema.parse({ + workflowId: 'wf-1', + identifier: 'my-chat', + title: 'Support', + customizations: { + primaryColor: 'var(--brand-hover)', + welcomeMessage: 'Hi', + }, + }) + expect(parsed.includeThinking).toBe(false) + }) + + it('create accepts includeThinking true', () => { + const parsed = createChatBodySchema.parse({ + workflowId: 'wf-1', + identifier: 'my-chat', + title: 'Support', + customizations: { + primaryColor: 'var(--brand-hover)', + welcomeMessage: 'Hi', + }, + includeThinking: true, + }) + expect(parsed.includeThinking).toBe(true) + }) + + it('update accepts includeThinking toggle', () => { + expect(updateChatBodySchema.parse({ includeThinking: true }).includeThinking).toBe(true) + expect(updateChatBodySchema.parse({ includeThinking: false }).includeThinking).toBe(false) + expect(updateChatBodySchema.parse({ title: 'x' }).includeThinking).toBeUndefined() + }) + + it('chat detail and deployed config expose includeThinking (default false)', () => { + const detail = chatDetailSchema.parse({ + id: 'chat-1', + identifier: 'my-chat', + title: 'Support', + description: '', + authType: 'public', + allowedEmails: [], + outputConfigs: [], + isActive: true, + chatUrl: 'http://localhost/chat/my-chat', + hasPassword: false, + }) + expect(detail.includeThinking).toBe(false) + + const detailOn = chatDetailSchema.parse({ + ...detail, + includeThinking: true, + }) + expect(detailOn.includeThinking).toBe(true) + + const config = deployedChatConfigSchema.parse({ + id: 'chat-1', + title: 'Support', + description: '', + customizations: {}, + authType: 'public', + }) + expect(config.includeThinking).toBe(false) + }) +}) diff --git a/apps/sim/lib/api/contracts/chats.ts b/apps/sim/lib/api/contracts/chats.ts index d0840bca8a5..4e22ef7c5d7 100644 --- a/apps/sim/lib/api/contracts/chats.ts +++ b/apps/sim/lib/api/contracts/chats.ts @@ -41,6 +41,8 @@ export const createChatBodySchema = z.object({ password: z.string().optional(), allowedEmails: z.array(z.string()).optional().default([]), outputConfigs: z.array(chatOutputConfigSchema).optional().default([]), + /** When true, clients may receive thinking SSE if they also send the protocol header. Default off. */ + includeThinking: z.boolean().optional().default(false), }) export type CreateChatBody = z.input @@ -58,6 +60,7 @@ export const updateChatBodySchema = z.object({ password: z.string().optional(), allowedEmails: z.array(z.string()).optional(), outputConfigs: z.array(chatOutputConfigSchema).optional(), + includeThinking: z.boolean().optional(), }) export type UpdateChatBody = z.input @@ -101,6 +104,8 @@ export const deployedChatConfigSchema = z.object({ (value) => value ?? undefined, z.array(deployedChatOutputConfigSchema).optional() ), + /** Policy for thinking SSE; clients still need X-Sim-Stream-Protocol opt-in (Step 5). */ + includeThinking: z.preprocess((value) => value ?? false, z.boolean()), }) export type DeployedChatConfig = z.output @@ -209,8 +214,10 @@ export const deployedChatPostContract = defineRouteContract({ params: chatIdentifierParamsSchema, body: deployedChatPostBodySchema, response: { - mode: 'json', - schema: deployedChatConfigSchema, + // Message posts return SSE (`text/event-stream`). Auth-only POSTs use + // authenticateDeployedChatContract (JSON). Terminal frames: `final` or one + // `error`, then `[DONE]`. Thinking frames require includeThinking + protocol header. + mode: 'stream', }, }) diff --git a/apps/sim/lib/api/contracts/deployments.ts b/apps/sim/lib/api/contracts/deployments.ts index 1b9cd57db72..a807c31c573 100644 --- a/apps/sim/lib/api/contracts/deployments.ts +++ b/apps/sim/lib/api/contracts/deployments.ts @@ -128,6 +128,7 @@ export const chatDetailSchema = z.object({ }) ) ), + includeThinking: z.preprocess((value) => value ?? false, z.boolean()), customizations: z.preprocess( (value) => value ?? undefined, z diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts index b47614c19c0..282e3cd5786 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts @@ -301,6 +301,7 @@ export async function executeDeployChat( allowedEmails: (existing[0].allowedEmails as string[]) || [], outputConfigs: (existing[0].outputConfigs as Array<{ blockId: string; path: string }>) || [], + includeThinking: existing[0].includeThinking ?? false, welcomeMessage: (existing[0].customizations as { welcomeMessage?: string } | null) ?.welcomeMessage || 'Hi there! How can I help you today?', @@ -388,6 +389,10 @@ export async function executeDeployChat( blockId: string path: string }> + const resolvedIncludeThinking = + typeof params.includeThinking === 'boolean' + ? params.includeThinking + : (existingDeployment?.includeThinking ?? false) const welcomeMessage = typeof params.welcomeMessage === 'string' ? params.welcomeMessage @@ -417,6 +422,7 @@ export async function executeDeployChat( password: params.password, allowedEmails: resolvedAllowedEmails, outputConfigs: resolvedOutputConfigs, + includeThinking: resolvedIncludeThinking, workspaceId: workflowRecord.workspaceId, }) @@ -469,6 +475,7 @@ export async function executeDeployChat( authType: resolvedAuthType, allowedEmails: resolvedAllowedEmails, outputConfigs: resolvedOutputConfigs, + includeThinking: resolvedIncludeThinking, welcomeMessage: welcomeMessage || 'Hi there! How can I help you today?', primaryColor: params.customizations?.primaryColor || diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts b/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts index d3c1c3df7db..e0e0cc66452 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts @@ -57,6 +57,7 @@ export async function executeCheckDeploymentStatus( authType: chat.authType, allowedEmails: chat.allowedEmails, outputConfigs: chat.outputConfigs, + includeThinking: chat.includeThinking, password: chat.password, customizations: chat.customizations, }) @@ -90,6 +91,7 @@ export async function executeCheckDeploymentStatus( authType: chatDeploy[0]?.authType || null, allowedEmails: chatDeploy[0]?.allowedEmails || null, outputConfigs: chatDeploy[0]?.outputConfigs || null, + includeThinking: chatDeploy[0]?.includeThinking ?? false, welcomeMessage: chatCustomizations.welcomeMessage || null, primaryColor: chatCustomizations.primaryColor || null, hasPassword: Boolean(chatDeploy[0]?.password), diff --git a/apps/sim/lib/copilot/tools/handlers/param-types.ts b/apps/sim/lib/copilot/tools/handlers/param-types.ts index 19eae663ba8..d72db002878 100644 --- a/apps/sim/lib/copilot/tools/handlers/param-types.ts +++ b/apps/sim/lib/copilot/tools/handlers/param-types.ts @@ -156,6 +156,7 @@ export interface DeployChatParams { subdomain?: string allowedEmails?: string[] outputConfigs?: unknown[] + includeThinking?: boolean } export interface DeployMcpParams { diff --git a/apps/sim/lib/core/execution-limits/types.test.ts b/apps/sim/lib/core/execution-limits/types.test.ts index 0b61b5a3efd..ec4bc2b16c2 100644 --- a/apps/sim/lib/core/execution-limits/types.test.ts +++ b/apps/sim/lib/core/execution-limits/types.test.ts @@ -66,4 +66,27 @@ describe('getExecutionTimeout', () => { vi.useRealTimers() } }) + + it('aborts with reason "timeout" when the timer fires', () => { + vi.useFakeTimers() + try { + const controller = createTimeoutAbortController(1000) + vi.advanceTimersByTime(1000) + expect(controller.signal.aborted).toBe(true) + expect(controller.isTimedOut()).toBe(true) + expect(controller.signal.reason).toBe('timeout') + controller.cleanup() + } finally { + vi.useRealTimers() + } + }) + + it('manual abort uses reason "user"', () => { + const controller = createTimeoutAbortController(60_000) + controller.abort() + expect(controller.signal.aborted).toBe(true) + expect(controller.isTimedOut()).toBe(false) + expect(controller.signal.reason).toBe('user') + controller.cleanup() + }) }) diff --git a/apps/sim/lib/core/execution-limits/types.ts b/apps/sim/lib/core/execution-limits/types.ts index d11ddc22892..0aac6ab4149 100644 --- a/apps/sim/lib/core/execution-limits/types.ts +++ b/apps/sim/lib/core/execution-limits/types.ts @@ -152,7 +152,8 @@ export function createTimeoutAbortController(timeoutMs?: number): TimeoutAbortCo if (timeoutMs) { timeoutId = setTimeout(() => { isTimedOut = true - abortController.abort() + // Typed reason so pumps/executors can distinguish timeout from user Stop. + abortController.abort('timeout') }, timeoutMs) } @@ -162,7 +163,8 @@ export function createTimeoutAbortController(timeoutMs?: number): TimeoutAbortCo cleanup: () => { if (timeoutId) clearTimeout(timeoutId) }, - abort: () => abortController.abort(), + // Manual abort is user/client cancellation (disconnect, Stop, registerManualExecutionAborter). + abort: () => abortController.abort('user'), timeoutMs, } } diff --git a/apps/sim/lib/workflows/executor/execute-workflow.ts b/apps/sim/lib/workflows/executor/execute-workflow.ts index 43408586eed..50d1da9ce7a 100644 --- a/apps/sim/lib/workflows/executor/execute-workflow.ts +++ b/apps/sim/lib/workflows/executor/execute-workflow.ts @@ -54,6 +54,8 @@ export interface ExecuteWorkflowOptions { executionMode?: 'sync' | 'stream' | 'async' /** Immutable actor/payer decision captured by preprocessing. */ billingAttribution?: BillingAttributionSnapshot + /** Deployed-chat thinking policy; persisted on the snapshot for resume. */ + includeThinking?: boolean } export interface WorkflowInfo { @@ -111,6 +113,7 @@ export async function executeWorkflow( largeValueKeys: streamConfig?.largeValueKeys, fileKeys: streamConfig?.fileKeys, executionMode: streamConfig?.executionMode, + includeThinking: streamConfig?.includeThinking === true ? true : undefined, } const snapshot = new ExecutionSnapshot( diff --git a/apps/sim/lib/workflows/executor/execution-events.ts b/apps/sim/lib/workflows/executor/execution-events.ts index d7212e8c1ed..761e2b4402f 100644 --- a/apps/sim/lib/workflows/executor/execution-events.ts +++ b/apps/sim/lib/workflows/executor/execution-events.ts @@ -18,6 +18,10 @@ export type ExecutionEventType = | 'block:childWorkflowStarted' | 'stream:chunk' | 'stream:done' + /** Live-only agent thinking delta (not buffered for reconnect replay). */ + | 'stream:thinking' + /** Live-only tool lifecycle (not buffered for reconnect replay). */ + | 'stream:tool' /** * Base event structure for SSE @@ -219,6 +223,36 @@ interface StreamDoneEvent extends BaseExecutionEvent { } } +/** + * Live thinking delta from an agent-events provider sink (canvas / draft runs). + * Builder runs show provider-exposed signals when the sink is attached + * (executor already disables the sink under PII redaction). + */ +interface StreamThinkingEvent extends BaseExecutionEvent { + type: 'stream:thinking' + workflowId: string + data: { + blockId: string + data: string + } +} + +/** + * Live tool lifecycle from an agent-events provider sink. + * Name + status only — never args or results. + */ +interface StreamToolEvent extends BaseExecutionEvent { + type: 'stream:tool' + workflowId: string + data: { + blockId: string + phase: 'start' | 'end' + id: string + name: string + status?: 'success' | 'error' | 'cancelled' + } +} + /** * Union type of all execution events */ @@ -234,6 +268,8 @@ export type ExecutionEvent = | BlockChildWorkflowStartedEvent | StreamChunkEvent | StreamDoneEvent + | StreamThinkingEvent + | StreamToolEvent export type ExecutionStartedData = ExecutionStartedEvent['data'] export type ExecutionCompletedData = ExecutionCompletedEvent['data'] @@ -246,6 +282,8 @@ export type BlockErrorData = BlockErrorEvent['data'] export type BlockChildWorkflowStartedData = BlockChildWorkflowStartedEvent['data'] export type StreamChunkData = StreamChunkEvent['data'] export type StreamDoneData = StreamDoneEvent['data'] +export type StreamThinkingData = StreamThinkingEvent['data'] +export type StreamToolData = StreamToolEvent['data'] /** * Helper to create SSE formatted message diff --git a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts index 343bed6a853..9df3a818cdf 100644 --- a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts +++ b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts @@ -26,6 +26,7 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session' import { cleanupExecutionBase64Cache } from '@/lib/uploads/utils/user-file-base64.server' import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' import type { ExecutionEvent } from '@/lib/workflows/executor/execution-events' +import { attachAgentStreamSink } from '@/lib/workflows/streaming/attach-agent-stream-sink' import { createPausedExecutionResumeMetadata, parsePausedExecutionResumeMetadata, @@ -1269,7 +1270,11 @@ export class PauseResumeManager { event: ExecutionEvent, terminalStatus?: TerminalExecutionStreamStatus ) => { - const isBuffered = event.type !== 'stream:chunk' && event.type !== 'stream:done' + const isBuffered = + event.type !== 'stream:chunk' && + event.type !== 'stream:done' && + event.type !== 'stream:thinking' && + event.type !== 'stream:tool' if (isBuffered) { const entry = terminalStatus ? await eventWriter.writeTerminal(event, terminalStatus).catch((error) => { @@ -1411,6 +1416,37 @@ export class PauseResumeManager { ? streamingExec.execution.blockId : undefined const blockId = typeof blockIdValue === 'string' ? blockIdValue : '' + + const unsubscribe = attachAgentStreamSink(streamingExec, { + onThinkingDelta: async (text) => { + await writeBufferedEvent({ + type: 'stream:thinking', + timestamp: new Date().toISOString(), + executionId: resumeExecutionId, + workflowId, + data: { blockId, data: text }, + } as ExecutionEvent) + }, + onToolCallStart: async (id, name) => { + await writeBufferedEvent({ + type: 'stream:tool', + timestamp: new Date().toISOString(), + executionId: resumeExecutionId, + workflowId, + data: { blockId, phase: 'start', id, name }, + } as ExecutionEvent) + }, + onToolCallEnd: async (id, name, status) => { + await writeBufferedEvent({ + type: 'stream:tool', + timestamp: new Date().toISOString(), + executionId: resumeExecutionId, + workflowId, + data: { blockId, phase: 'end', id, name, status }, + } as ExecutionEvent) + }, + }) + const reader = streamingExec.stream.getReader() const decoder = new TextDecoder() try { @@ -1440,6 +1476,7 @@ export class PauseResumeManager { error: toError(streamError).message, }) } finally { + unsubscribe() try { await reader.cancel().catch(() => {}) } catch {} diff --git a/apps/sim/lib/workflows/orchestration/chat-deploy.ts b/apps/sim/lib/workflows/orchestration/chat-deploy.ts index 8288766b900..8d583d0a65c 100644 --- a/apps/sim/lib/workflows/orchestration/chat-deploy.ts +++ b/apps/sim/lib/workflows/orchestration/chat-deploy.ts @@ -25,6 +25,8 @@ export interface ChatDeployPayload { password?: string | null allowedEmails?: string[] outputConfigs?: Array<{ blockId: string; path: string }> + /** When true, public SSE may expose thinking if the client also opts into agent-events-v1. */ + includeThinking?: boolean workspaceId?: string | null } @@ -56,6 +58,7 @@ export async function performChatDeploy( password, allowedEmails = [], outputConfigs = [], + includeThinking = false, } = params const customizations = { @@ -108,6 +111,7 @@ export async function performChatDeploy( password: passwordToStore, allowedEmails: authType === 'email' || authType === 'sso' ? allowedEmails : [], outputConfigs, + includeThinking, updatedAt: new Date(), }) .where(eq(chat.id, chatId)) @@ -126,6 +130,7 @@ export async function performChatDeploy( password: encryptedPassword, allowedEmails: authType === 'email' || authType === 'sso' ? allowedEmails : [], outputConfigs, + includeThinking, createdAt: new Date(), updatedAt: new Date(), }) diff --git a/apps/sim/lib/workflows/streaming/agent-stream-protocol.test.ts b/apps/sim/lib/workflows/streaming/agent-stream-protocol.test.ts new file mode 100644 index 00000000000..a24e18d295f --- /dev/null +++ b/apps/sim/lib/workflows/streaming/agent-stream-protocol.test.ts @@ -0,0 +1,83 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + AGENT_STREAM_PROTOCOL_HEADER, + AGENT_STREAM_PROTOCOL_V1, + requestOptsIntoAgentStreamProtocol, + shouldEmitAgentStreamEvents, +} from '@/lib/workflows/streaming/agent-stream-protocol' + +function headers(init?: Record): Headers { + return new Headers(init) +} + +describe('shouldEmitAgentStreamEvents', () => { + it('defaults to false when policy is off and header is missing', () => { + expect( + shouldEmitAgentStreamEvents({ + includeThinking: false, + requestHeaders: headers(), + }) + ).toBe(false) + expect( + shouldEmitAgentStreamEvents({ + includeThinking: undefined, + requestHeaders: headers(), + }) + ).toBe(false) + }) + + it('requires both includeThinking and protocol header', () => { + expect( + shouldEmitAgentStreamEvents({ + includeThinking: true, + requestHeaders: headers(), + }) + ).toBe(false) + + expect( + shouldEmitAgentStreamEvents({ + includeThinking: false, + requestHeaders: headers({ [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1 }), + }) + ).toBe(false) + + expect( + shouldEmitAgentStreamEvents({ + includeThinking: true, + requestHeaders: headers({ [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1 }), + }) + ).toBe(true) + }) + + it('accepts case-insensitive header values and comma lists', () => { + expect( + shouldEmitAgentStreamEvents({ + includeThinking: true, + requestHeaders: headers({ [AGENT_STREAM_PROTOCOL_HEADER]: ' Agent-Events-V1 ' }), + }) + ).toBe(true) + + expect( + shouldEmitAgentStreamEvents({ + includeThinking: true, + requestHeaders: headers({ + [AGENT_STREAM_PROTOCOL_HEADER]: 'text, agent-events-v1', + }), + }) + ).toBe(true) + }) +}) + +describe('requestOptsIntoAgentStreamProtocol', () => { + it('detects protocol opt-in independent of deployment policy', () => { + expect(requestOptsIntoAgentStreamProtocol(headers())).toBe(false) + expect( + requestOptsIntoAgentStreamProtocol( + headers({ [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1 }) + ) + ).toBe(true) + }) +}) diff --git a/apps/sim/lib/workflows/streaming/agent-stream-protocol.ts b/apps/sim/lib/workflows/streaming/agent-stream-protocol.ts new file mode 100644 index 00000000000..70cda71b118 --- /dev/null +++ b/apps/sim/lib/workflows/streaming/agent-stream-protocol.ts @@ -0,0 +1,60 @@ +/** + * Negotiation for public agent stream events (thinking / tool lifecycle). + * + * Exposure rule (locked) for public chat / simple SSE: + * emit thinking/tool SSE frames iff + * deployment.includeThinking === true + * AND request opts into agent-events-v1 via {@link AGENT_STREAM_PROTOCOL_HEADER} + * + * Canvas draft runs (execution-events) forward the same sink as live-only + * `stream:thinking` / `stream:tool` events without the includeThinking gate; + * the executor still disables the sink when block-output PII redaction is on. + * + * Legacy clients omitting the header stay text-only even when the deployment + * has thinking enabled. Deployed chat UI always sends the header when loading + * its own deployment. + * + * See docs: workflows/deployment/agent-events. + */ + +export const AGENT_STREAM_PROTOCOL_HEADER = 'x-sim-stream-protocol' as const + +export const AGENT_STREAM_PROTOCOL_V1 = 'agent-events-v1' as const + +export type AgentStreamProtocol = typeof AGENT_STREAM_PROTOCOL_V1 + +/** + * Returns true when both the deployment policy and the request protocol opt-in + * are present. Used by simple SSE (Step 5+) before emitting thinking/tool frames. + */ +export function shouldEmitAgentStreamEvents(options: { + includeThinking: boolean | null | undefined + requestHeaders: Headers | { get(name: string): string | null } +}): boolean { + if (options.includeThinking !== true) { + return false + } + + const raw = options.requestHeaders.get(AGENT_STREAM_PROTOCOL_HEADER) + if (!raw) { + return false + } + + // Allow comma-separated values / surrounding whitespace from proxies. + const tokens = raw + .split(',') + .map((token) => token.trim().toLowerCase()) + .filter(Boolean) + + return tokens.includes(AGENT_STREAM_PROTOCOL_V1) +} + +/** True when the request asked for the agent-events protocol (ignores policy). */ +export function requestOptsIntoAgentStreamProtocol( + requestHeaders: Headers | { get(name: string): string | null } +): boolean { + return shouldEmitAgentStreamEvents({ + includeThinking: true, + requestHeaders, + }) +} diff --git a/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.test.ts b/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.test.ts new file mode 100644 index 00000000000..4e999cce476 --- /dev/null +++ b/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.test.ts @@ -0,0 +1,63 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { attachAgentStreamSink } from '@/lib/workflows/streaming/attach-agent-stream-sink' +import type { AgentStreamEvent } from '@/providers/stream-events' +import type { StreamingExecution } from '@/executor/types' + +describe('attachAgentStreamSink (Step 10)', () => { + it('subscribes in sync window and routes thinking/tool events', async () => { + let sinkHandler: ((event: AgentStreamEvent) => void | Promise) | undefined + const unsubscribe = vi.fn() + const streamingExec = { + stream: new ReadableStream(), + execution: { success: true, output: {} }, + subscribe: (sink: { onEvent: (event: AgentStreamEvent) => void | Promise }) => { + sinkHandler = sink.onEvent + return unsubscribe + }, + } as StreamingExecution + + const onThinkingDelta = vi.fn() + const onToolCallStart = vi.fn() + const onToolCallEnd = vi.fn() + + const unsub = attachAgentStreamSink(streamingExec, { + onThinkingDelta, + onToolCallStart, + onToolCallEnd, + }) + + expect(sinkHandler).toBeTypeOf('function') + + await sinkHandler!({ type: 'thinking_delta', text: 'plan ' }) + await sinkHandler!({ type: 'tool_call_start', id: 't1', name: 'http_request' }) + await sinkHandler!({ + type: 'tool_call_end', + id: 't1', + name: 'http_request', + status: 'success', + }) + await sinkHandler!({ type: 'text_delta', text: 'hi', turn: 'final' }) + + expect(onThinkingDelta).toHaveBeenCalledWith('plan ') + expect(onToolCallStart).toHaveBeenCalledWith('t1', 'http_request') + expect(onToolCallEnd).toHaveBeenCalledWith('t1', 'http_request', 'success') + + unsub() + expect(unsubscribe).toHaveBeenCalled() + }) + + it('no-ops when subscribe is absent', () => { + const streamingExec = { + stream: new ReadableStream(), + execution: { success: true, output: {} }, + } as StreamingExecution + + const unsub = attachAgentStreamSink(streamingExec, { + onThinkingDelta: vi.fn(), + }) + expect(() => unsub()).not.toThrow() + }) +}) diff --git a/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.ts b/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.ts new file mode 100644 index 00000000000..87296ab0860 --- /dev/null +++ b/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.ts @@ -0,0 +1,47 @@ +/** + * Sync-window helper: attach an agent-events sink before the first await + * so the executor pump can deliver thinking/tool events while the answer + * stream is drained separately. + */ + +import type { StreamingExecution } from '@/executor/types' +import type { AgentStreamEvent } from '@/providers/stream-events' + +export type AgentStreamSinkHandlers = { + onThinkingDelta?: (text: string) => void | Promise + onToolCallStart?: (id: string, name: string) => void | Promise + onToolCallEnd?: ( + id: string, + name: string, + status: 'success' | 'error' | 'cancelled' + ) => void | Promise +} + +/** + * Subscribe in the caller's sync window (before awaiting the text reader). + * Returns an unsubscribe function (no-op when subscribe is absent). + */ +export function attachAgentStreamSink( + streamingExec: StreamingExecution, + handlers: AgentStreamSinkHandlers +): () => void { + if (!streamingExec.subscribe) { + return () => {} + } + + return streamingExec.subscribe({ + onEvent: async (event: AgentStreamEvent) => { + if (event.type === 'thinking_delta') { + await handlers.onThinkingDelta?.(event.text) + return + } + if (event.type === 'tool_call_start') { + await handlers.onToolCallStart?.(event.id, event.name) + return + } + if (event.type === 'tool_call_end') { + await handlers.onToolCallEnd?.(event.id, event.name, event.status) + } + }, + }) +} diff --git a/apps/sim/lib/workflows/streaming/streaming.test.ts b/apps/sim/lib/workflows/streaming/streaming.test.ts index f802dc5bfa4..6951c76a35b 100644 --- a/apps/sim/lib/workflows/streaming/streaming.test.ts +++ b/apps/sim/lib/workflows/streaming/streaming.test.ts @@ -604,3 +604,373 @@ describe('createStreamingResponse', () => { await expect(readSSEStream(stream)).resolves.toBe('ok') }) }) + +describe('createStreamingResponse agent-events-v1 (Step 5)', () => { + beforeEach(() => { + vi.clearAllMocks() + clearLargeValueCacheForTests() + }) + + function createAgentStreamExecuteFn(options: { + thinking?: string[] + answer: string + fail?: boolean + tools?: Array< + | { type: 'tool_call_start'; id: string; name: string } + | { type: 'tool_call_end'; id: string; name: string; status: string } + > + }) { + return async ({ + onStream, + abortSignal, + }: { + onStream: (streamingExec: any) => Promise + onBlockComplete: (blockId: string, output: unknown) => Promise + abortSignal: AbortSignal + }) => { + let textController!: ReadableStreamDefaultController + let sink: { onEvent: (event: unknown) => void | Promise } | undefined + const textStream = new ReadableStream({ + start(controller) { + textController = controller + }, + }) + + const onStreamPromise = onStream({ + stream: textStream, + streamFormat: 'text', + subscribe: (nextSink: { onEvent: (event: unknown) => void | Promise }) => { + sink = nextSink + return () => { + sink = undefined + } + }, + execution: { + blockId: 'agent-1', + success: true, + output: { content: options.answer }, + logs: [], + metadata: {}, + }, + }) + + if (options.fail) { + textController.error(new Error('provider reset')) + await onStreamPromise.catch(() => {}) + throw new Error('provider reset') + } + + for (const text of options.thinking ?? []) { + await sink?.onEvent({ type: 'thinking_delta', text }) + } + for (const toolEvent of options.tools ?? []) { + await sink?.onEvent(toolEvent) + } + textController.enqueue(new TextEncoder().encode(options.answer)) + textController.close() + await onStreamPromise + + expect(abortSignal).toBeDefined() + + return { + success: true, + output: { content: options.answer }, + logs: [ + { + blockId: 'agent-1', + output: { content: '' }, + startedAt: new Date().toISOString(), + endedAt: new Date().toISOString(), + durationMs: 1, + success: true, + }, + ], + } as any + } + } + + async function collectSSEPayloads(stream: ReadableStream): Promise { + const reader = stream.getReader() + const decoder = new TextDecoder() + let buffer = '' + while (true) { + const { done, value } = await reader.read() + if (done) { + buffer += decoder.decode() + break + } + buffer += decoder.decode(value, { stream: true }) + } + return buffer + .split('\n\n') + .map((chunk) => chunk.trim()) + .filter((chunk) => chunk.startsWith('data: ')) + .map((chunk) => chunk.slice(6)) + } + + it('legacy path without protocol header stays text-only (no thinking frames)', async () => { + const stream = await createStreamingResponse({ + requestId: 'request-1', + streamConfig: { + includeThinking: true, + selectedOutputs: ['agent-1_content'], + }, + // No requestHeaders → gate closed + executeFn: createAgentStreamExecuteFn({ + thinking: ['secret thought'], + answer: 'Hello', + }), + }) + + const events = await collectSSEEvents(stream) + expect(events.some((event) => event.event === 'thinking')).toBe(false) + expect(events).toContainEqual({ blockId: 'agent-1', chunk: 'Hello' }) + expect(events.some((event) => event.event === 'final')).toBe(true) + }) + + it('header + includeThinking emits thinking on data and answer on chunk', async () => { + const headers = new Headers({ + 'x-sim-stream-protocol': 'agent-events-v1', + }) + const stream = await createStreamingResponse({ + requestId: 'request-1', + requestHeaders: headers, + streamConfig: { + includeThinking: true, + selectedOutputs: ['agent-1_content'], + }, + executeFn: createAgentStreamExecuteFn({ + thinking: ['hmm ', 'yes'], + answer: 'Answer', + }), + }) + + const events = await collectSSEEvents(stream) + expect(events.filter((event) => event.event === 'thinking')).toEqual([ + { blockId: 'agent-1', event: 'thinking', data: 'hmm ' }, + { blockId: 'agent-1', event: 'thinking', data: 'yes' }, + ]) + expect(events).toContainEqual({ blockId: 'agent-1', chunk: 'Answer' }) + expect(events.some((event) => event.event === 'final')).toBe(true) + }) + + it('dual gate emits tool start/end frames without putting tools on chunk', async () => { + const headers = new Headers({ + 'x-sim-stream-protocol': 'agent-events-v1', + }) + const stream = await createStreamingResponse({ + requestId: 'request-1', + requestHeaders: headers, + streamConfig: { + includeThinking: true, + selectedOutputs: ['agent-1_content'], + }, + executeFn: createAgentStreamExecuteFn({ + answer: 'Done', + tools: [ + { type: 'tool_call_start', id: 'toolu_1', name: 'get_weather' }, + { + type: 'tool_call_end', + id: 'toolu_1', + name: 'get_weather', + status: 'success', + }, + ], + }), + }) + + const events = await collectSSEEvents(stream) + expect(events.filter((event) => event.event === 'tool')).toEqual([ + { + blockId: 'agent-1', + event: 'tool', + phase: 'start', + id: 'toolu_1', + name: 'get_weather', + }, + { + blockId: 'agent-1', + event: 'tool', + phase: 'end', + id: 'toolu_1', + name: 'get_weather', + status: 'success', + }, + ]) + expect(events).toContainEqual({ blockId: 'agent-1', chunk: 'Done' }) + expect( + events.some( + (event) => + typeof event.chunk === 'string' && + (String(event.chunk).includes('toolu_1') || String(event.chunk).includes('get_weather')) + ) + ).toBe(false) + }) + + it('protocol header without includeThinking does not emit tool frames', async () => { + const headers = new Headers({ + 'x-sim-stream-protocol': 'agent-events-v1', + }) + const stream = await createStreamingResponse({ + requestId: 'request-1', + requestHeaders: headers, + streamConfig: { + includeThinking: false, + selectedOutputs: ['agent-1_content'], + }, + executeFn: createAgentStreamExecuteFn({ + answer: 'Answer', + tools: [{ type: 'tool_call_start', id: 'toolu_1', name: 'get_weather' }], + }), + }) + + const events = await collectSSEEvents(stream) + expect(events.some((event) => event.event === 'tool')).toBe(false) + expect(events).toContainEqual({ blockId: 'agent-1', chunk: 'Answer' }) + }) + + it('protocol header without includeThinking does not emit thinking', async () => { + const headers = new Headers({ + 'x-sim-stream-protocol': 'agent-events-v1', + }) + const stream = await createStreamingResponse({ + requestId: 'request-1', + requestHeaders: headers, + streamConfig: { + includeThinking: false, + selectedOutputs: ['agent-1_content'], + }, + executeFn: createAgentStreamExecuteFn({ + thinking: ['should not appear'], + answer: 'Answer', + }), + }) + + const events = await collectSSEEvents(stream) + expect(events.some((event) => event.event === 'thinking')).toBe(false) + expect(events).toContainEqual({ blockId: 'agent-1', chunk: 'Answer' }) + }) + + it('provider failure emits one terminal error, no final, then [DONE]', async () => { + const stream = await createStreamingResponse({ + requestId: 'request-1', + streamConfig: {}, + executeFn: createAgentStreamExecuteFn({ + answer: 'partial', + fail: true, + }), + }) + + const payloads = await collectSSEPayloads(stream) + const events = payloads + .filter((payload) => payload !== '[DONE]' && payload !== '"[DONE]"') + .map((payload) => JSON.parse(payload) as Record) + + expect(events.filter((event) => event.event === 'error')).toHaveLength(1) + expect(events.some((event) => event.event === 'final')).toBe(false) + expect(payloads.some((payload) => payload === '[DONE]' || payload === '"[DONE]"')).toBe(true) + }) + + it('requestSignal abort propagates to executeFn abortSignal', async () => { + const requestAbort = new AbortController() + let sawAbort = false + + const stream = await createStreamingResponse({ + requestId: 'request-1', + requestSignal: requestAbort.signal, + streamConfig: {}, + executeFn: async ({ abortSignal }) => { + requestAbort.abort() + sawAbort = abortSignal.aborted + return { + success: false, + status: 'cancelled', + output: {}, + logs: [], + } as any + }, + }) + + const events = await collectSSEEvents(stream) + expect(sawAbort).toBe(true) + expect(events.some((event) => event.event === 'final')).toBe(false) + expect(events).toContainEqual({ event: 'error', error: 'Client cancelled request' }) + }) + + it('thinking never enters streamedChunks / log content rewrite', async () => { + const headers = new Headers({ + 'x-sim-stream-protocol': 'agent-events-v1', + }) + let rewrittenContent: string | undefined + + const stream = await createStreamingResponse({ + requestId: 'request-1', + requestHeaders: headers, + streamConfig: { + includeThinking: true, + selectedOutputs: ['agent-1_content'], + }, + executeFn: async ({ onStream }) => { + let textController!: ReadableStreamDefaultController + let sink: { onEvent: (event: unknown) => void | Promise } | undefined + const textStream = new ReadableStream({ + start(controller) { + textController = controller + }, + }) + + const onStreamPromise = onStream({ + stream: textStream, + streamFormat: 'text', + subscribe: (nextSink: any) => { + sink = nextSink + return () => { + sink = undefined + } + }, + execution: { + blockId: 'agent-1', + success: true, + output: { content: 'visible' }, + logs: [], + metadata: {}, + }, + } as any) + + await sink?.onEvent({ type: 'thinking_delta', text: 'PRIVATE_THINKING' }) + textController.enqueue(new TextEncoder().encode('visible')) + textController.close() + await onStreamPromise + + return { + success: true, + output: {}, + logs: [ + { + blockId: 'agent-1', + output: { content: '' }, + startedAt: new Date().toISOString(), + endedAt: new Date().toISOString(), + durationMs: 1, + success: true, + }, + ], + } as any + }, + }) + + const events = await collectSSEEvents(stream) + const answerChunks = events.filter((event) => typeof event.chunk === 'string') + expect(answerChunks.every((event) => !String(event.chunk).includes('PRIVATE_THINKING'))).toBe( + true + ) + expect(events).toContainEqual({ + blockId: 'agent-1', + event: 'thinking', + data: 'PRIVATE_THINKING', + }) + // Force consumption of stream so log rewrite runs + expect(events.some((event) => event.event === 'final')).toBe(true) + void rewrittenContent + }) +}) diff --git a/apps/sim/lib/workflows/streaming/streaming.ts b/apps/sim/lib/workflows/streaming/streaming.ts index c848854346f..6dced6c7e3b 100644 --- a/apps/sim/lib/workflows/streaming/streaming.ts +++ b/apps/sim/lib/workflows/streaming/streaming.ts @@ -26,6 +26,12 @@ import { } from '@/lib/uploads/utils/user-file-base64.server' import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' import { navigatePathAsync } from '@/executor/variables/resolvers/reference-async.server' +import { + AGENT_STREAM_PROTOCOL_HEADER, + AGENT_STREAM_PROTOCOL_V1, + shouldEmitAgentStreamEvents, +} from '@/lib/workflows/streaming/agent-stream-protocol' +import { DEFAULT_MAX_THINKING_BYTES } from '@/providers/stream-pump' /** * Extended streaming execution type that includes blockId on the execution. @@ -41,6 +47,16 @@ const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'] const SELECTED_OUTPUT_TOO_LARGE_MESSAGE = 'Selected output is too large to inline; select a nested field or use pagination/preview.' +/** + * Simple SSE stream contract (Step 5): + * - Answer text: `{ blockId, chunk }` only (`chunk` is forever answer text). + * - Thinking (opt-in): `{ blockId, event: 'thinking', data }` — never uses `chunk`. + * - Success terminal: `{ event: 'final', data }` then `[DONE]`. + * - Failure terminal: exactly one `{ event: 'error', ... }` then `[DONE]`. No `final` after failure. + * - Mid-block read issues may emit non-terminal `{ event: 'stream_error', blockId, error }`. + * - Thinking never enters `streamedChunks` / log rewrite / tokenization. + */ + interface StreamingConfig { selectedOutputs?: string[] isSecureMode?: boolean @@ -48,6 +64,13 @@ interface StreamingConfig { includeFileBase64?: boolean base64MaxBytes?: number timeoutMs?: number + /** + * Deployment policy for thinking/tool SSE. Still requires the client to send + * {@link AGENT_STREAM_PROTOCOL_HEADER}: {@link AGENT_STREAM_PROTOCOL_V1}. + */ + includeThinking?: boolean + /** Cap on thinking bytes forwarded over SSE for the whole execution. */ + maxThinkingBytes?: number } export type StreamingExecutorFn = (callbacks: { @@ -67,9 +90,34 @@ export interface StreamingResponseOptions { workspaceId?: string workflowId?: string userId?: string + /** Incoming fetch/request abort — combined with the stream timeout. */ + requestSignal?: AbortSignal + /** Used with {@link StreamingConfig.includeThinking} for dual-gate thinking SSE. */ + requestHeaders?: Headers | { get(name: string): string | null } executeFn: StreamingExecutorFn } +/** + * Extra response headers when the dual-gate agent stream protocol is active. + * Callers should merge these into the SSE response alongside {@link SSE_HEADERS}. + */ +export function agentStreamProtocolResponseHeaders(options: { + includeThinking?: boolean | null + requestHeaders?: Headers | { get(name: string): string | null } +}): Record { + if (!options.requestHeaders) return {} + if ( + !shouldEmitAgentStreamEvents({ + includeThinking: options.includeThinking, + requestHeaders: options.requestHeaders, + }) + ) { + return {} + } + return { [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1 } +} + + interface StreamingState { streamedChunks: Map processedOutputs: Set @@ -351,14 +399,31 @@ export async function createStreamingResponse( options: StreamingResponseOptions ): Promise { const { requestId, streamConfig, executionId, executeFn } = options - const durableContext = { - workspaceId: options.workspaceId, - workflowId: options.workflowId, - executionId, - userId: options.userId, - requireDurable: Boolean(options.workspaceId && options.workflowId && executionId), - } const timeoutController = createTimeoutAbortController(streamConfig.timeoutMs) + const emitAgentEvents = + Boolean(options.requestHeaders) && + shouldEmitAgentStreamEvents({ + includeThinking: streamConfig.includeThinking, + requestHeaders: options.requestHeaders!, + }) + const maxThinkingBytes = streamConfig.maxThinkingBytes ?? DEFAULT_MAX_THINKING_BYTES + + let requestAborted = false + const onRequestAbort = () => { + requestAborted = true + timeoutController.abort() + } + if (options.requestSignal) { + if (options.requestSignal.aborted) { + onRequestAbort() + } else { + options.requestSignal.addEventListener('abort', onRequestAbort, { once: true }) + } + } + + const cleanupRequestAbort = () => { + options.requestSignal?.removeEventListener('abort', onRequestAbort) + } return new ReadableStream({ async start(controller) { @@ -370,6 +435,7 @@ export async function createStreamingResponse( selectedOutputBytes: 0, streamedSelectedOutputKeys: new Set(), } + let thinkingBytesEmitted = 0 const sendChunk = ( blockId: string, @@ -390,8 +456,44 @@ export async function createStreamingResponse( state.processedOutputs.add(blockId) } + const sendThinking = (blockId: string, text: string) => { + if (!text || thinkingBytesEmitted >= maxThinkingBytes) return + const remaining = maxThinkingBytes - thinkingBytesEmitted + const forwarded = text.length > remaining ? text.slice(0, remaining) : text + thinkingBytesEmitted += forwarded.length + // Never push thinking into streamedChunks — logs stay answer-text only. + controller.enqueue( + encodeSSE({ + blockId, + event: 'thinking', + data: forwarded, + }) + ) + } + + const sendTool = ( + blockId: string, + phase: 'start' | 'end', + id: string, + name: string, + status?: string + ) => { + controller.enqueue( + encodeSSE({ + blockId, + event: 'tool', + phase, + id, + name, + ...(phase === 'end' && status ? { status } : {}), + }) + ) + } + /** * Callback for handling streaming execution events. + * Subscribe synchronously before the first await so the executor pump + * can attach sinks before pulling provider chunks. */ const onStreamCallback = async (streamingExec: StreamingExecutionWithBlockId) => { const blockId = streamingExec.execution?.blockId @@ -400,6 +502,21 @@ export async function createStreamingResponse( return } + let unsubscribe: (() => void) | undefined + if (emitAgentEvents && streamingExec.subscribe) { + unsubscribe = streamingExec.subscribe({ + onEvent: async (event) => { + if (event.type === 'thinking_delta') { + sendThinking(blockId, event.text) + } else if (event.type === 'tool_call_start') { + sendTool(blockId, 'start', event.id, event.name) + } else if (event.type === 'tool_call_end') { + sendTool(blockId, 'end', event.id, event.name, event.status) + } + }, + }) + } + const reader = streamingExec.stream.getReader() const decoder = new TextDecoder() let isFirstChunk = true @@ -434,6 +551,8 @@ export async function createStreamingResponse( error: getErrorMessage(error, 'Stream reading error'), }) ) + } finally { + unsubscribe?.() } } @@ -555,7 +674,8 @@ export async function createStreamingResponse( if ( result.status === 'cancelled' && timeoutController.isTimedOut() && - timeoutController.timeoutMs + timeoutController.timeoutMs && + !requestAborted ) { const timeoutErrorMessage = getTimeoutErrorMessage(null, timeoutController.timeoutMs) logger.info(`[${requestId}] Streaming execution timed out`, { @@ -565,6 +685,16 @@ export async function createStreamingResponse( await result._streamingMetadata.loggingSession.markAsFailed(timeoutErrorMessage) } controller.enqueue(encodeSSE({ event: 'error', error: timeoutErrorMessage })) + } else if (result.status === 'cancelled' && requestAborted) { + logger.info(`[${requestId}] Streaming execution aborted by client disconnect`) + if (result._streamingMetadata?.loggingSession) { + // LoggingSession has no cancelled status; match workflow execute route wording. + await result._streamingMetadata.loggingSession.markAsFailed( + 'Client cancelled request' + ) + } + // No `final` after abort; clients that already disconnected ignore these. + controller.enqueue(encodeSSE({ event: 'error', error: 'Client cancelled request' })) } else { await completeLoggingSession(result) @@ -603,6 +733,7 @@ export async function createStreamingResponse( } } + // Terminal marker: always follows success `final` or a single terminal `error`. controller.enqueue(encodeSSE('[DONE]')) if (executionId) { @@ -617,6 +748,8 @@ export async function createStreamingResponse( ? SELECTED_OUTPUT_TOO_LARGE_MESSAGE : getErrorMessage(error, 'Stream processing error') controller.enqueue(encodeSSE({ event: 'error', error: errorMessage })) + // Same terminal rule as timeout/abort: one error, then [DONE], never `final`. + controller.enqueue(encodeSSE('[DONE]')) if (executionId) { await cleanupExecutionBase64Cache(executionId) @@ -624,12 +757,15 @@ export async function createStreamingResponse( controller.close() } finally { + cleanupRequestAbort() timeoutController.cleanup() } }, async cancel(reason) { logger.info(`[${requestId}] Streaming response cancelled`, { reason }) + requestAborted = true timeoutController.abort() + cleanupRequestAbort() timeoutController.cleanup() if (executionId) { try { diff --git a/apps/sim/providers/__fixtures__/anthropic/fixtures.test.ts b/apps/sim/providers/__fixtures__/anthropic/fixtures.test.ts new file mode 100644 index 00000000000..eb6434d92c0 --- /dev/null +++ b/apps/sim/providers/__fixtures__/anthropic/fixtures.test.ts @@ -0,0 +1,188 @@ +/** + * @vitest-environment node + * + * Step 0 gate: Anthropic stream fixtures parse and match expected assembled + * thinking/text/tool/signature content. No provider adapters are exercised yet. + */ +import { describe, expect, it } from 'vitest' +import { + anthropicRedactedThinkingAssembledContent, + anthropicRedactedThinkingExpectedText, + anthropicRedactedThinkingExpectedTraceThinking, + anthropicRedactedThinkingStreamEvents, + anthropicThinkingTextToolAssembledContent, + anthropicThinkingTextToolExpectedText, + anthropicThinkingTextToolExpectedThinking, + anthropicThinkingTextToolStreamEvents, +} from '@/providers/__fixtures__/anthropic' + +type StreamEvent = { + type: string + index?: number + delta?: { type: string; thinking?: string; text?: string; signature?: string; partial_json?: string } + content_block?: { + type: string + thinking?: string + text?: string + data?: string + id?: string + name?: string + input?: unknown + signature?: string + } +} + +function assembleAnthropicContentFromStream(events: readonly StreamEvent[]) { + const blocks: Array> = [] + + for (const event of events) { + if (event.type === 'content_block_start' && event.content_block) { + const block = event.content_block + if (block.type === 'thinking') { + blocks.push({ type: 'thinking', thinking: block.thinking ?? '', signature: '' }) + } else if (block.type === 'redacted_thinking') { + blocks.push({ type: 'redacted_thinking', data: block.data ?? '' }) + } else if (block.type === 'text') { + blocks.push({ type: 'text', text: block.text ?? '' }) + } else if (block.type === 'tool_use') { + blocks.push({ + type: 'tool_use', + id: block.id, + name: block.name, + inputJson: '', + }) + } + continue + } + + if (event.type !== 'content_block_delta' || event.index === undefined || !event.delta) { + continue + } + + const target = blocks[event.index] + if (!target) continue + + const delta = event.delta + if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') { + target.thinking = `${target.thinking ?? ''}${delta.thinking}` + } else if (delta.type === 'signature_delta' && typeof delta.signature === 'string') { + target.signature = delta.signature + } else if (delta.type === 'text_delta' && typeof delta.text === 'string') { + target.text = `${target.text ?? ''}${delta.text}` + } else if (delta.type === 'input_json_delta' && typeof delta.partial_json === 'string') { + target.inputJson = `${target.inputJson ?? ''}${delta.partial_json}` + } + } + + return blocks.map((block) => { + if (block.type === 'tool_use') { + const inputJson = typeof block.inputJson === 'string' ? block.inputJson : '{}' + return { + type: 'tool_use', + id: block.id, + name: block.name, + input: JSON.parse(inputJson || '{}'), + } + } + if (block.type === 'thinking') { + return { + type: 'thinking', + thinking: block.thinking, + signature: block.signature, + } + } + return block + }) +} + +function extractTextDeltas(events: readonly StreamEvent[]): string { + return events + .filter( + (e) => + e.type === 'content_block_delta' && + e.delta?.type === 'text_delta' && + typeof e.delta.text === 'string' + ) + .map((e) => e.delta!.text!) + .join('') +} + +function extractThinkingDeltas(events: readonly StreamEvent[]): string { + return events + .filter( + (e) => + e.type === 'content_block_delta' && + e.delta?.type === 'thinking_delta' && + typeof e.delta.thinking === 'string' + ) + .map((e) => e.delta!.thinking!) + .join('') +} + +function assertValidStreamSequence(events: readonly StreamEvent[]) { + expect(events[0]?.type).toBe('message_start') + expect(events[events.length - 1]?.type).toBe('message_stop') + + const open = new Set() + for (const event of events) { + if (event.type === 'content_block_start' && event.index !== undefined) { + expect(open.has(event.index)).toBe(false) + open.add(event.index) + } + if (event.type === 'content_block_stop' && event.index !== undefined) { + expect(open.has(event.index)).toBe(true) + open.delete(event.index) + } + } + expect(open.size).toBe(0) +} + +describe('Anthropic stream fixtures (Step 0)', () => { + it('parses thinking → text → tool_use stream and preserves signature', () => { + assertValidStreamSequence(anthropicThinkingTextToolStreamEvents) + + expect(extractThinkingDeltas(anthropicThinkingTextToolStreamEvents)).toBe( + anthropicThinkingTextToolExpectedThinking + ) + expect(extractTextDeltas(anthropicThinkingTextToolStreamEvents)).toBe( + anthropicThinkingTextToolExpectedText + ) + + const assembled = assembleAnthropicContentFromStream(anthropicThinkingTextToolStreamEvents) + expect(assembled).toEqual([...anthropicThinkingTextToolAssembledContent]) + + const thinkingBlock = assembled.find((b) => b.type === 'thinking') as { + signature?: string + } + expect(thinkingBlock?.signature).toMatch(/^EpAB/) + }) + + it('parses redacted_thinking + signed thinking + text and matches trace mapping', () => { + assertValidStreamSequence(anthropicRedactedThinkingStreamEvents) + + expect(extractTextDeltas(anthropicRedactedThinkingStreamEvents)).toBe( + anthropicRedactedThinkingExpectedText + ) + + const assembled = assembleAnthropicContentFromStream(anthropicRedactedThinkingStreamEvents) + expect(assembled).toEqual([...anthropicRedactedThinkingAssembledContent]) + + // Mirrors enrichLastModelSegmentFromAnthropicResponse: redacted → "[redacted]" + const traceThinking = assembled + .filter((b) => b.type === 'thinking' || b.type === 'redacted_thinking') + .map((b) => (b.type === 'thinking' ? b.thinking : '[redacted]')) + .join('\n\n') + expect(traceThinking).toBe(anthropicRedactedThinkingExpectedTraceThinking) + }) + + it('documents that live stream today would only surface text_delta bytes', () => { + // Baseline behavior of createReadableStreamFromAnthropicStream: only text_delta + // is enqueued. This test locks the fixture expectation for later adapter work. + const textOnlyFromStream = extractTextDeltas(anthropicThinkingTextToolStreamEvents) + const thinkingFromStream = extractThinkingDeltas(anthropicThinkingTextToolStreamEvents) + + expect(textOnlyFromStream).toBe(anthropicThinkingTextToolExpectedText) + expect(thinkingFromStream.length).toBeGreaterThan(0) + expect(textOnlyFromStream).not.toContain('I should check the weather') + }) +}) diff --git a/apps/sim/providers/__fixtures__/anthropic/index.ts b/apps/sim/providers/__fixtures__/anthropic/index.ts new file mode 100644 index 00000000000..117a8fb7769 --- /dev/null +++ b/apps/sim/providers/__fixtures__/anthropic/index.ts @@ -0,0 +1,16 @@ +/** + * Re-exports Anthropic stream fixtures used by agent-stream-events work. + * Keep fixture data in dedicated modules so adapters can import without pulling tests. + */ +export { + anthropicThinkingTextToolAssembledContent, + anthropicThinkingTextToolExpectedText, + anthropicThinkingTextToolExpectedThinking, + anthropicThinkingTextToolStreamEvents, +} from '@/providers/__fixtures__/anthropic/thinking-text-tool' +export { + anthropicRedactedThinkingAssembledContent, + anthropicRedactedThinkingExpectedText, + anthropicRedactedThinkingExpectedTraceThinking, + anthropicRedactedThinkingStreamEvents, +} from '@/providers/__fixtures__/anthropic/redacted-thinking-signature' diff --git a/apps/sim/providers/__fixtures__/anthropic/redacted-thinking-signature.ts b/apps/sim/providers/__fixtures__/anthropic/redacted-thinking-signature.ts new file mode 100644 index 00000000000..1f489756ddf --- /dev/null +++ b/apps/sim/providers/__fixtures__/anthropic/redacted-thinking-signature.ts @@ -0,0 +1,99 @@ +/** + * Anthropic stream + assembled message covering redacted_thinking blocks. + * + * When Anthropic redacts thinking, the stream emits a redacted_thinking content + * block (opaque `data`) instead of thinking_delta text. Multi-turn tool loops + * must round-trip that block (and any adjacent signed thinking blocks) back + * into subsequent Messages API requests unchanged. + */ + +export const anthropicRedactedThinkingStreamEvents = [ + { + type: 'message_start', + message: { + id: 'msg_fixture_redacted_thinking', + type: 'message', + role: 'assistant', + content: [], + model: 'claude-sonnet-4-5', + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 20, output_tokens: 0 }, + }, + }, + { + type: 'content_block_start', + index: 0, + content_block: { + type: 'redacted_thinking', + data: 'fixture-redacted-thinking-opaque-blob-001', + }, + }, + { type: 'content_block_stop', index: 0 }, + { + type: 'content_block_start', + index: 1, + content_block: { + type: 'thinking', + thinking: '', + }, + }, + { + type: 'content_block_delta', + index: 1, + delta: { type: 'thinking_delta', thinking: 'Visible follow-up reasoning after redaction.' }, + }, + { + type: 'content_block_delta', + index: 1, + delta: { + type: 'signature_delta', + signature: 'EpABCkYICBgCKkDfixture-visible-thinking-signature-def456', + }, + }, + { type: 'content_block_stop', index: 1 }, + { + type: 'content_block_start', + index: 2, + content_block: { type: 'text', text: '' }, + }, + { + type: 'content_block_delta', + index: 2, + delta: { type: 'text_delta', text: 'Here is the answer after redacted thinking.' }, + }, + { type: 'content_block_stop', index: 2 }, + { + type: 'message_delta', + delta: { stop_reason: 'end_turn', stop_sequence: null }, + usage: { output_tokens: 64 }, + }, + { type: 'message_stop' }, +] as const + +/** + * Assembled content that must be preserved when appending the assistant turn + * to Anthropic history (see anthropic/core.ts thinking/redacted_thinking filters). + */ +export const anthropicRedactedThinkingAssembledContent = [ + { + type: 'redacted_thinking', + data: 'fixture-redacted-thinking-opaque-blob-001', + }, + { + type: 'thinking', + thinking: 'Visible follow-up reasoning after redaction.', + signature: 'EpABCkYICBgCKkDfixture-visible-thinking-signature-def456', + }, + { + type: 'text', + text: 'Here is the answer after redacted thinking.', + }, +] as const + +/** What enrichLastModelSegmentFromAnthropicResponse maps redacted blocks to today. */ +export const anthropicRedactedThinkingExpectedTraceThinking = + '[redacted]\n\nVisible follow-up reasoning after redaction.' + +export const anthropicRedactedThinkingExpectedText = + 'Here is the answer after redacted thinking.' diff --git a/apps/sim/providers/__fixtures__/anthropic/thinking-text-tool.ts b/apps/sim/providers/__fixtures__/anthropic/thinking-text-tool.ts new file mode 100644 index 00000000000..ec183158186 --- /dev/null +++ b/apps/sim/providers/__fixtures__/anthropic/thinking-text-tool.ts @@ -0,0 +1,119 @@ +/** + * Anthropic Messages API SSE-style stream events for a single assistant turn that: + * 1. Streams extended thinking (thinking_delta + signature_delta) + * 2. Streams answer text (text_delta) + * 3. Streams a tool_use block (input_json_delta) + * + * Shapes mirror Anthropic RawMessageStreamEvent fields used by + * `createReadableStreamFromAnthropicStream`. The adapter emits thinking_delta + + * text_delta AgentStreamEvents; tool_use deltas are ignored until the streaming + * tool loop (Step 7). + */ + +export const anthropicThinkingTextToolStreamEvents = [ + { + type: 'message_start', + message: { + id: 'msg_fixture_thinking_text_tool', + type: 'message', + role: 'assistant', + content: [], + model: 'claude-sonnet-4-5', + stop_reason: null, + stop_sequence: null, + usage: { input_tokens: 42, output_tokens: 0 }, + }, + }, + { + type: 'content_block_start', + index: 0, + content_block: { type: 'thinking', thinking: '' }, + }, + { + type: 'content_block_delta', + index: 0, + delta: { type: 'thinking_delta', thinking: 'I should check the weather before answering. ' }, + }, + { + type: 'content_block_delta', + index: 0, + delta: { type: 'thinking_delta', thinking: 'Calling get_weather for SF.' }, + }, + { + type: 'content_block_delta', + index: 0, + delta: { + type: 'signature_delta', + signature: 'EpABCkYICBgCKkDfixture-thinking-signature-abc123xyz', + }, + }, + { type: 'content_block_stop', index: 0 }, + { + type: 'content_block_start', + index: 1, + content_block: { type: 'text', text: '' }, + }, + { + type: 'content_block_delta', + index: 1, + delta: { type: 'text_delta', text: 'Let me check the weather in San Francisco.' }, + }, + { type: 'content_block_stop', index: 1 }, + { + type: 'content_block_start', + index: 2, + content_block: { + type: 'tool_use', + id: 'toolu_fixture_01Weather', + name: 'get_weather', + input: {}, + }, + }, + { + type: 'content_block_delta', + index: 2, + delta: { type: 'input_json_delta', partial_json: '{"city":' }, + }, + { + type: 'content_block_delta', + index: 2, + delta: { type: 'input_json_delta', partial_json: '"San Francisco"}' }, + }, + { type: 'content_block_stop', index: 2 }, + { + type: 'message_delta', + delta: { stop_reason: 'tool_use', stop_sequence: null }, + usage: { output_tokens: 128 }, + }, + { type: 'message_stop' }, +] as const + +/** + * Expected assembled assistant Message.content after draining the stream above. + * Used for history round-trip tests (thinking block must keep its signature). + */ +export const anthropicThinkingTextToolAssembledContent = [ + { + type: 'thinking', + thinking: 'I should check the weather before answering. Calling get_weather for SF.', + signature: 'EpABCkYICBgCKkDfixture-thinking-signature-abc123xyz', + }, + { + type: 'text', + text: 'Let me check the weather in San Francisco.', + }, + { + type: 'tool_use', + id: 'toolu_fixture_01Weather', + name: 'get_weather', + input: { city: 'San Francisco' }, + }, +] as const + +/** Concatenated thinking text (what traces should store as thinkingContent). */ +export const anthropicThinkingTextToolExpectedThinking = + 'I should check the weather before answering. Calling get_weather for SF.' + +/** Concatenated answer text (what output.content / live stream should contain today). */ +export const anthropicThinkingTextToolExpectedText = + 'Let me check the weather in San Francisco.' diff --git a/apps/sim/providers/__fixtures__/openai-compat/index.ts b/apps/sim/providers/__fixtures__/openai-compat/index.ts new file mode 100644 index 00000000000..76ab6500b47 --- /dev/null +++ b/apps/sim/providers/__fixtures__/openai-compat/index.ts @@ -0,0 +1,60 @@ +/** + * OpenAI-compat stream fixtures (Step 9) — capability-honest reasoning deltas. + */ +export const openaiCompatReasoningAndTextChunks = [ + { + choices: [ + { + delta: { + reasoning_content: 'I should compute carefully. ', + }, + }, + ], + usage: { prompt_tokens: 10, completion_tokens: 0, total_tokens: 10 }, + }, + { + choices: [ + { + delta: { + reasoning_content: 'Answer is 4.', + content: '2+2=', + }, + }, + ], + }, + { + choices: [{ delta: { content: '4' } }], + usage: { prompt_tokens: 10, completion_tokens: 8, total_tokens: 18 }, + }, +] as const + +export const openaiCompatTextOnlyChunks = [ + { + choices: [{ delta: { content: 'Hello' } }], + }, + { + choices: [{ delta: { content: ' world' } }], + usage: { prompt_tokens: 3, completion_tokens: 2, total_tokens: 5 }, + }, +] as const + +export const openaiCompatToolCallStartChunks = [ + { + choices: [ + { + delta: { + tool_calls: [{ index: 0, id: 'call_abc', function: { name: 'http_request', arguments: '' } }], + }, + }, + ], + }, + { + choices: [ + { + delta: { + tool_calls: [{ index: 0, function: { arguments: '{"url":' } }], + }, + }, + ], + }, +] as const diff --git a/apps/sim/providers/anthropic/core.ts b/apps/sim/providers/anthropic/core.ts index 27d84febb1d..64bc7ba86fa 100644 --- a/apps/sim/providers/anthropic/core.ts +++ b/apps/sim/providers/anthropic/core.ts @@ -3,8 +3,9 @@ import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages/messages' import type { Logger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' -import type { BlockTokens, IterationToolCall, StreamingExecution } from '@/executor/types' +import type { BlockTokens, IterationToolCall, NormalizedBlockOutput, StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' +import { createAnthropicStreamingToolLoopStream } from '@/providers/anthropic/streaming-tool-loop' import { checkForForcedToolUsage, createReadableStreamFromAnthropicStream, @@ -403,6 +404,56 @@ export async function executeAnthropicProviderRequest( const shouldStreamToolCalls = request.streamToolCalls ?? false + if (request.stream && shouldStreamToolCalls && anthropicTools && anthropicTools.length > 0) { + logger.info(`Using streaming tool loop for ${providerLabel} request`) + + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + const timeSegments: TimeSegment[] = [] + const forcedTools = preparedTools?.forcedTools || [] + + return createStreamingExecution({ + model: request.model, + providerStartTime, + providerStartTimeISO, + timing: { + kind: 'accumulated', + modelTime: 0, + toolsTime: 0, + firstResponseTime: 0, + iterations: 1, + timeSegments, + }, + initialTokens: { input: 0, output: 0, total: 0 }, + initialCost: { total: 0.0, input: 0.0, output: 0.0 }, + isStreaming: true, + streamFormat: 'agent-events-v1', + createStream: ({ output, finalizeTiming }) => + createAnthropicStreamingToolLoopStream({ + anthropic, + payload, + request, + messages, + logger, + timeSegments, + forcedTools, + onComplete: (result) => { + output.content = result.content + output.tokens = result.tokens + output.cost = result.cost + output.toolCalls = result.toolCalls as NormalizedBlockOutput['toolCalls'] + if (output.providerTiming) { + output.providerTiming.modelTime = result.modelTime + output.providerTiming.toolsTime = result.toolsTime + output.providerTiming.firstResponseTime = result.firstResponseTime + output.providerTiming.iterations = result.iterations + } + finalizeTiming() + }, + }), + }) + } + if (request.stream && (!anthropicTools || anthropicTools.length === 0)) { logger.info(`Using streaming response for ${providerLabel} request (no tools)`) @@ -425,10 +476,11 @@ export async function executeAnthropicProviderRequest( initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { total: 0.0, input: 0.0, output: 0.0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromAnthropicStream( streamResponse as AsyncIterable, - (content, usage) => { + ({ content, usage, thinking }) => { output.content = content output.tokens = { input: usage.input_tokens, @@ -443,6 +495,13 @@ export async function executeAnthropicProviderRequest( total: costResult.total, } + if (thinking) { + const segment = output.providerTiming?.timeSegments?.[0] + if (segment) { + segment.thinkingContent = thinking + } + } + finalizeTiming() } ), @@ -805,10 +864,11 @@ export async function executeAnthropicProviderRequest( }, toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromAnthropicStream( streamResponse as AsyncIterable, - (streamContent, usage) => { + ({ content: streamContent, usage, thinking }) => { output.content = streamContent output.tokens = { input: tokens.input + usage.input_tokens, @@ -829,6 +889,16 @@ export async function executeAnthropicProviderRequest( total: accumulatedCost.total + streamCost.total + tc, } + if (thinking) { + const segments = output.providerTiming?.timeSegments + const lastModel = segments + ? [...segments].reverse().find((segment) => segment.type === 'model') + : undefined + if (lastModel) { + lastModel.thinkingContent = thinking + } + } + finalizeTiming() } ), @@ -1228,10 +1298,11 @@ export async function executeAnthropicProviderRequest( }, toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromAnthropicStream( streamResponse as AsyncIterable, - (streamContent, usage) => { + ({ content: streamContent, usage, thinking }) => { output.content = streamContent output.tokens = { input: tokens.input + usage.input_tokens, @@ -1252,6 +1323,16 @@ export async function executeAnthropicProviderRequest( total: cost.total + streamCost.total + tc2, } + if (thinking) { + const segments = output.providerTiming?.timeSegments + const lastModel = segments + ? [...segments].reverse().find((segment) => segment.type === 'model') + : undefined + if (lastModel) { + lastModel.thinkingContent = thinking + } + } + finalizeTiming() } ), diff --git a/apps/sim/providers/anthropic/streaming-tool-loop.test.ts b/apps/sim/providers/anthropic/streaming-tool-loop.test.ts new file mode 100644 index 00000000000..e4e4b7824da --- /dev/null +++ b/apps/sim/providers/anthropic/streaming-tool-loop.test.ts @@ -0,0 +1,335 @@ +/** + * @vitest-environment node + * + * Step 7: Anthropic streaming tool loop — live tool_call_start/end, final-turn-only + * answer text, abort → cancelled, per-turn usage accumulation. + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + anthropicThinkingTextToolExpectedThinking, + anthropicThinkingTextToolStreamEvents, +} from '@/providers/__fixtures__/anthropic' +import { createAnthropicStreamingToolLoopStream } from '@/providers/anthropic/streaming-tool-loop' +import type { AgentStreamEvent } from '@/providers/stream-events' +import type { TimeSegment } from '@/providers/types' + +const { mockExecuteTool, mockPrepareToolExecution } = vi.hoisted(() => ({ + mockExecuteTool: vi.fn(), + mockPrepareToolExecution: vi.fn(), +})) + +vi.mock('@/tools', () => ({ + executeTool: mockExecuteTool, +})) + +vi.mock('@/providers/utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + prepareToolExecution: mockPrepareToolExecution, + calculateCost: () => ({ input: 0.01, output: 0.02, total: 0.03 }), + sumToolCosts: () => 0, + } +}) + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +function makeFinalMessage(overrides: { + content: unknown[] + usage?: { input_tokens: number; output_tokens: number } + stop_reason?: string | null +}) { + return { + id: 'msg_test', + type: 'message', + role: 'assistant', + model: 'claude-sonnet-4-5', + content: overrides.content, + stop_reason: overrides.stop_reason ?? null, + stop_sequence: null, + usage: overrides.usage ?? { input_tokens: 10, output_tokens: 20 }, + } +} + +function makeMessageStream( + events: unknown[], + finalMessage: ReturnType +) { + return { + async *[Symbol.asyncIterator]() { + for (const event of events) { + yield event + } + }, + finalMessage: async () => finalMessage, + } +} + +describe('createAnthropicStreamingToolLoopStream (Step 7)', () => { + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + } as any + + beforeEach(() => { + vi.clearAllMocks() + mockPrepareToolExecution.mockReturnValue({ + toolParams: { city: 'San Francisco' }, + executionParams: { city: 'San Francisco' }, + }) + mockExecuteTool.mockResolvedValue({ + success: true, + output: { temp: 68 }, + }) + }) + + it('emits tool_call_start/end, intermediate then final text, and accumulates usage', async () => { + const toolTurnEvents = anthropicThinkingTextToolStreamEvents + const toolTurnMessage = makeFinalMessage({ + content: [ + { + type: 'thinking', + thinking: anthropicThinkingTextToolExpectedThinking, + signature: 'EpABCkYICBgCKkDfixture-thinking-signature-abc123xyz', + }, + { type: 'text', text: 'Let me check the weather in San Francisco.' }, + { + type: 'tool_use', + id: 'toolu_fixture_01Weather', + name: 'get_weather', + input: { city: 'San Francisco' }, + }, + ], + usage: { input_tokens: 42, output_tokens: 30 }, + stop_reason: 'tool_use', + }) + + const finalTurnEvents = [ + { + type: 'message_start', + message: { + usage: { input_tokens: 100, output_tokens: 0 }, + }, + }, + { + type: 'content_block_start', + index: 0, + content_block: { type: 'text', text: '' }, + }, + { + type: 'content_block_delta', + index: 0, + delta: { type: 'text_delta', text: 'It is 68°F in San Francisco.' }, + }, + { type: 'content_block_stop', index: 0 }, + { + type: 'message_delta', + delta: { stop_reason: 'end_turn' }, + usage: { output_tokens: 12 }, + }, + { type: 'message_stop' }, + ] + const finalTurnMessage = makeFinalMessage({ + content: [{ type: 'text', text: 'It is 68°F in San Francisco.' }], + usage: { input_tokens: 100, output_tokens: 12 }, + stop_reason: 'end_turn', + }) + + let streamCall = 0 + const anthropic = { + messages: { + stream: vi.fn(() => { + streamCall++ + if (streamCall === 1) { + return makeMessageStream(toolTurnEvents as unknown[], toolTurnMessage) + } + return makeMessageStream(finalTurnEvents, finalTurnMessage) + }), + }, + } as any + + const timeSegments: TimeSegment[] = [] + const onComplete = vi.fn() + + const stream = createAnthropicStreamingToolLoopStream({ + anthropic, + payload: { + model: 'claude-sonnet-4-5', + max_tokens: 1024, + messages: [{ role: 'user', content: 'Weather?' }], + tools: [ + { + name: 'get_weather', + description: 'Get weather', + input_schema: { type: 'object', properties: {} }, + }, + ], + } as any, + request: { + model: 'claude-sonnet-4-5', + apiKey: 'test', + tools: [{ id: 'get_weather', name: 'get_weather', params: {}, parameters: {} }], + } as any, + messages: [{ role: 'user', content: 'Weather?' }], + logger, + timeSegments, + onComplete, + }) + + const events = await collectEvents(stream) + + expect(events.filter((e) => e.type === 'thinking_delta').length).toBeGreaterThan(0) + expect(events).toContainEqual({ + type: 'tool_call_start', + id: 'toolu_fixture_01Weather', + name: 'get_weather', + }) + expect(events).toContainEqual({ + type: 'tool_call_end', + id: 'toolu_fixture_01Weather', + name: 'get_weather', + status: 'success', + }) + + const textEvents = events.filter((e) => e.type === 'text_delta') + expect(textEvents.some((e) => e.turn === 'intermediate')).toBe(true) + expect(textEvents.some((e) => e.turn === 'final')).toBe(true) + expect(textEvents.find((e) => e.turn === 'final')?.text).toContain('68°F') + + // Assistant history must keep thinking signature for multi-iteration round-trip. + const secondPayload = anthropic.messages.stream.mock.calls[1][0] + const assistantMsg = secondPayload.messages.find((m: any) => m.role === 'assistant') + expect(assistantMsg.content.some((b: any) => b.type === 'thinking' && b.signature)).toBe(true) + expect(assistantMsg.content.some((b: any) => b.type === 'tool_use')).toBe(true) + + expect(onComplete).toHaveBeenCalledTimes(1) + expect(onComplete.mock.calls[0][0].tokens).toEqual({ + input: 142, + output: 42, + total: 184, + }) + expect(onComplete.mock.calls[0][0].content).toContain('68°F') + expect(mockExecuteTool).toHaveBeenCalled() + }) + + it('settles in-flight tools as cancelled on abort', async () => { + const abortController = new AbortController() + const toolStartEvents = [ + { + type: 'message_start', + message: { usage: { input_tokens: 5, output_tokens: 0 } }, + }, + { + type: 'content_block_start', + index: 0, + content_block: { + type: 'tool_use', + id: 'toolu_abort', + name: 'get_weather', + input: {}, + }, + }, + { + type: 'content_block_delta', + index: 0, + delta: { type: 'input_json_delta', partial_json: '{}' }, + }, + { type: 'content_block_stop', index: 0 }, + { + type: 'message_delta', + delta: { stop_reason: 'tool_use' }, + usage: { output_tokens: 3 }, + }, + { type: 'message_stop' }, + ] + + mockExecuteTool.mockImplementation(async () => { + abortController.abort() + throw new DOMException('Stream aborted', 'AbortError') + }) + + const anthropic = { + messages: { + stream: vi.fn(() => + makeMessageStream( + toolStartEvents, + makeFinalMessage({ + content: [ + { + type: 'tool_use', + id: 'toolu_abort', + name: 'get_weather', + input: {}, + }, + ], + stop_reason: 'tool_use', + }) + ) + ), + }, + } as any + + const stream = createAnthropicStreamingToolLoopStream({ + anthropic, + payload: { + model: 'claude-sonnet-4-5', + max_tokens: 1024, + messages: [{ role: 'user', content: 'x' }], + tools: [ + { + name: 'get_weather', + description: 'd', + input_schema: { type: 'object', properties: {} }, + }, + ], + } as any, + request: { + model: 'claude-sonnet-4-5', + apiKey: 'test', + tools: [{ id: 'get_weather', name: 'get_weather', params: {}, parameters: {} }], + abortSignal: abortController.signal, + } as any, + messages: [{ role: 'user', content: 'x' }], + logger, + timeSegments: [], + onComplete: vi.fn(), + }) + + const captured: AgentStreamEvent[] = [] + const reader = stream.getReader() + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + captured.push(value) + } + } catch { + // expected — stream errors after abort settlement + } + + expect(captured).toContainEqual({ + type: 'tool_call_start', + id: 'toolu_abort', + name: 'get_weather', + }) + expect(captured).toContainEqual({ + type: 'tool_call_end', + id: 'toolu_abort', + name: 'get_weather', + status: 'cancelled', + }) + }) +}) diff --git a/apps/sim/providers/anthropic/streaming-tool-loop.ts b/apps/sim/providers/anthropic/streaming-tool-loop.ts new file mode 100644 index 00000000000..12e191d5b7d --- /dev/null +++ b/apps/sim/providers/anthropic/streaming-tool-loop.ts @@ -0,0 +1,563 @@ +/** + * Live Anthropic streaming tool loop (Step 7). + * + * Each model turn is streamed via `messages.stream` + `finalMessage()` so thinking + * signatures round-trip correctly. Thinking and `tool_call_start` emit live; + * text is buffered until the turn completes so it can be tagged + * `intermediate` (tool-use turns) or `final` (answer channel / SSE `chunk`). + * Tool ends emit in actual completion order; abort settles in-flight tools as + * `cancelled`. + */ + +import type Anthropic from '@anthropic-ai/sdk' +import type { Logger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import type { BlockTokens, IterationToolCall, NormalizedBlockOutput } from '@/executor/types' +import { MAX_TOOL_ITERATIONS } from '@/providers' +import { checkForForcedToolUsage } from '@/providers/anthropic/utils' +import type { AgentStreamEvent, ToolCallEndStatus } from '@/providers/stream-events' +import { enrichLastModelSegment } from '@/providers/trace-enrichment' +import type { ProviderRequest, TimeSegment } from '@/providers/types' +import { + calculateCost, + prepareToolExecution, + sumToolCosts, +} from '@/providers/utils' +import { executeTool } from '@/tools' + +/** Custom payload fields shared with `core.ts` (adaptive thinking, output_format). */ +export type AnthropicStreamingToolLoopPayload = Omit< + Anthropic.Messages.MessageStreamParams, + 'thinking' +> & { + thinking?: Anthropic.Messages.ThinkingConfigParam | { type: 'adaptive' } + output_format?: { type: 'json_schema'; schema: Record } + output_config?: { effort: string } +} + +export interface AnthropicStreamingToolLoopComplete { + content: string + tokens: { input: number; output: number; total: number } + cost: NormalizedBlockOutput['cost'] + toolCalls?: { list: unknown[]; count: number } + modelTime: number + toolsTime: number + firstResponseTime: number + iterations: number +} + +export interface CreateAnthropicStreamingToolLoopStreamOptions { + anthropic: Anthropic + payload: AnthropicStreamingToolLoopPayload + request: ProviderRequest + messages: Anthropic.Messages.MessageParam[] + logger: Logger + /** Shared mutable segments; same array reference passed into createStreamingExecution. */ + timeSegments: TimeSegment[] + /** Forced tool names from prepareToolsWithUsageControl (may be empty). */ + forcedTools?: string[] + onComplete: (result: AnthropicStreamingToolLoopComplete) => void +} + +function isAbortError(error: unknown): boolean { + if (!error || typeof error !== 'object') return false + const name = (error as { name?: string }).name + return name === 'AbortError' || name === 'APIUserAbortError' +} + +function buildSegmentTokens(usage: Anthropic.Messages.Usage): BlockTokens { + const input = usage.input_tokens ?? 0 + const output = usage.output_tokens ?? 0 + const cacheRead = usage.cache_read_input_tokens ?? 0 + const cacheWrite = usage.cache_creation_input_tokens ?? 0 + return { + input, + output, + total: input + output + cacheRead + cacheWrite, + ...(cacheRead > 0 && { cacheRead }), + ...(cacheWrite > 0 && { cacheWrite }), + } +} + +function enrichModelSegment( + timeSegments: TimeSegment[], + response: Anthropic.Messages.Message, + textContent: string, + model: string +): void { + const thinkingBlocks = response.content.filter( + (item): item is Anthropic.Messages.ThinkingBlock | Anthropic.Messages.RedactedThinkingBlock => + item.type === 'thinking' || item.type === 'redacted_thinking' + ) + const thinkingContent = thinkingBlocks + .map((b) => (b.type === 'thinking' ? b.thinking : '[redacted]')) + .join('\n\n') + + const toolUseBlocks = response.content.filter( + (item): item is Anthropic.Messages.ToolUseBlock => item.type === 'tool_use' + ) + const toolCalls: IterationToolCall[] = toolUseBlocks.map((t) => ({ + id: t.id, + name: t.name, + arguments: + t.input && typeof t.input === 'object' && !Array.isArray(t.input) + ? (t.input as Record) + : {}, + })) + + const segmentTokens = response.usage ? buildSegmentTokens(response.usage) : undefined + let cost: { input: number; output: number; total: number } | undefined + if ( + segmentTokens && + typeof segmentTokens.input === 'number' && + typeof segmentTokens.output === 'number' + ) { + const useCached = (segmentTokens.cacheRead ?? 0) > 0 + const full = calculateCost(model, segmentTokens.input, segmentTokens.output, useCached) + cost = { input: full.input, output: full.output, total: full.total } + } + + enrichLastModelSegment(timeSegments, { + assistantContent: textContent || undefined, + thinkingContent: thinkingContent || undefined, + toolCalls: toolCalls.length > 0 ? toolCalls : undefined, + finishReason: response.stop_reason ?? undefined, + tokens: segmentTokens, + cost, + provider: 'anthropic', + }) +} + +function settleOpenTools( + controller: ReadableStreamDefaultController, + openTools: Map, + status: ToolCallEndStatus +): void { + for (const [id, name] of openTools) { + controller.enqueue({ type: 'tool_call_end', id, name, status }) + } + openTools.clear() +} + +/** + * Multi-turn Anthropic tool loop as an `agent-events-v1` object stream. + */ +export function createAnthropicStreamingToolLoopStream( + options: CreateAnthropicStreamingToolLoopStreamOptions +): ReadableStream { + const { anthropic, payload, request, messages, logger, timeSegments, onComplete } = options + const forcedToolNames = options.forcedTools ?? [] + + return new ReadableStream({ + async start(controller) { + const currentMessages = [...messages] + const originalToolChoice = payload.tool_choice + + let usedForcedTools: string[] = [] + let hasUsedForcedTool = false + let content = '' + let iterationCount = 0 + let modelTime = 0 + let toolsTime = 0 + let firstResponseTime = 0 + const tokens = { input: 0, output: 0, total: 0 } + const toolCalls: unknown[] = [] + const toolResults: Record[] = [] + /** Tools that received start but not yet end (abort settlement). */ + const openToolStarts = new Map() + + const streamOptions = request.abortSignal ? { signal: request.abortSignal } : undefined + + try { + while (iterationCount < MAX_TOOL_ITERATIONS) { + if (request.abortSignal?.aborted) { + const abortErr = new DOMException('Stream aborted', 'AbortError') + settleOpenTools(controller, openToolStarts, 'cancelled') + throw abortErr + } + + const turnPayload: AnthropicStreamingToolLoopPayload = { + ...payload, + messages: currentMessages, + } + // Streaming tool loop always streams each turn; never pass stream:true twice. + delete (turnPayload as { stream?: boolean }).stream + + // Forced tool_choice vs thinking — same rules as silent loop. + const thinkingEnabled = !!payload.thinking + if ( + !thinkingEnabled && + typeof originalToolChoice === 'object' && + hasUsedForcedTool && + forcedToolNames.length > 0 + ) { + const remainingTools = forcedToolNames.filter((tool) => !usedForcedTools.includes(tool)) + if (remainingTools.length > 0) { + turnPayload.tool_choice = { type: 'tool', name: remainingTools[0] } + } else { + turnPayload.tool_choice = undefined + } + } else if ( + !thinkingEnabled && + hasUsedForcedTool && + typeof originalToolChoice === 'object' + ) { + turnPayload.tool_choice = undefined + } + + const modelStart = Date.now() + if (iterationCount === 0) { + // firstResponseTime set after first turn + } + + const messageStream = anthropic.messages.stream( + turnPayload as Anthropic.Messages.MessageStreamParams, + streamOptions + ) + + const textChunks: string[] = [] + let inputTokens = 0 + let outputTokens = 0 + + try { + for await (const event of messageStream) { + if (event.type === 'message_start') { + inputTokens = event.message.usage?.input_tokens ?? 0 + continue + } + if (event.type === 'message_delta') { + outputTokens = event.usage?.output_tokens ?? outputTokens + continue + } + if (event.type === 'content_block_start') { + const block = event.content_block as { + type?: string + id?: string + name?: string + } + if (block.type === 'tool_use' && block.id && block.name) { + openToolStarts.set(block.id, block.name) + controller.enqueue({ + type: 'tool_call_start', + id: block.id, + name: block.name, + }) + } + continue + } + if (event.type !== 'content_block_delta') { + continue + } + const delta = event.delta as { + type?: string + text?: string + thinking?: string + } + if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') { + controller.enqueue({ type: 'thinking_delta', text: delta.thinking }) + continue + } + if (delta.type === 'text_delta' && typeof delta.text === 'string') { + textChunks.push(delta.text) + } + } + + const finalMessage = await messageStream.finalMessage() + const modelEnd = Date.now() + const thisModelTime = modelEnd - modelStart + modelTime += thisModelTime + if (iterationCount === 0) { + firstResponseTime = thisModelTime + } + + timeSegments.push({ + type: 'model', + name: request.model, + startTime: modelStart, + endTime: modelEnd, + duration: thisModelTime, + }) + + // Prefer finalMessage.usage when present (includes cache fields). + const turnInput = finalMessage.usage?.input_tokens ?? inputTokens + const turnOutput = finalMessage.usage?.output_tokens ?? outputTokens + tokens.input += turnInput + tokens.output += turnOutput + tokens.total += turnInput + turnOutput + + const textContent = finalMessage.content + .filter((item): item is Anthropic.Messages.TextBlock => item.type === 'text') + .map((item) => item.text) + .join('\n') + + const toolUses = finalMessage.content.filter( + (item): item is Anthropic.Messages.ToolUseBlock => item.type === 'tool_use' + ) + + const turnTag = toolUses.length > 0 ? 'intermediate' : 'final' + for (const chunk of textChunks) { + controller.enqueue({ type: 'text_delta', text: chunk, turn: turnTag }) + } + // If the SDK assembled text but we somehow missed deltas, still project final text. + if (textChunks.length === 0 && textContent && turnTag === 'final') { + controller.enqueue({ type: 'text_delta', text: textContent, turn: 'final' }) + } + if (turnTag === 'final' && textContent) { + content = textContent + } else if (textContent) { + // Keep last intermediate text available if we hit max iterations mid-loop. + content = textContent + } + + enrichModelSegment(timeSegments, finalMessage, textContent, request.model) + + const forcedCheck = checkForForcedToolUsage( + finalMessage, + turnPayload.tool_choice ?? originalToolChoice, + forcedToolNames, + usedForcedTools + ) + if (forcedCheck) { + hasUsedForcedTool = forcedCheck.hasUsedForcedTool + usedForcedTools = forcedCheck.usedForcedTools + } + + if (toolUses.length === 0) { + break + } + + const toolsStartTime = Date.now() + + // Emit ends in completion order; keep Promise.all result order (= start order) for history. + const orderedResults = await Promise.all( + toolUses.map(async (toolUse) => { + const toolCallStartTime = Date.now() + const toolName = toolUse.name + const toolArgs = (toolUse.input ?? {}) as Record + + try { + if (request.abortSignal?.aborted) { + throw new DOMException('Stream aborted', 'AbortError') + } + + const tool = request.tools?.find((t) => t.id === toolName) + if (!tool) { + const value = { + toolUse, + toolName, + toolArgs, + toolParams: {} as Record, + result: { + success: false as const, + output: undefined, + error: `Tool not found: ${toolName}`, + }, + startTime: toolCallStartTime, + endTime: Date.now(), + duration: Date.now() - toolCallStartTime, + status: 'error' as ToolCallEndStatus, + } + openToolStarts.delete(toolUse.id) + controller.enqueue({ + type: 'tool_call_end', + id: toolUse.id, + name: toolName, + status: 'error', + }) + return value + } + + const { toolParams, executionParams } = prepareToolExecution( + tool, + toolArgs, + request + ) + const result = await executeTool(toolName, executionParams, { + skipPostProcess: true, + signal: request.abortSignal, + }) + const toolCallEndTime = Date.now() + const value = { + toolUse, + toolName, + toolArgs, + toolParams, + result, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + status: (result.success ? 'success' : 'error') as ToolCallEndStatus, + } + openToolStarts.delete(toolUse.id) + controller.enqueue({ + type: 'tool_call_end', + id: toolUse.id, + name: toolName, + status: value.status, + }) + return value + } catch (error) { + const toolCallEndTime = Date.now() + const cancelled = isAbortError(error) || !!request.abortSignal?.aborted + if (!cancelled) { + logger.error('Error processing tool call:', { error, toolName }) + } + const value = { + toolUse, + toolName, + toolArgs, + toolParams: {} as Record, + result: { + success: false as const, + output: undefined, + error: getErrorMessage(error, 'Tool execution failed'), + }, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + status: (cancelled ? 'cancelled' : 'error') as ToolCallEndStatus, + } + openToolStarts.delete(toolUse.id) + controller.enqueue({ + type: 'tool_call_end', + id: toolUse.id, + name: toolName, + status: value.status, + }) + return value + } + }) + ) + + const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = [] + const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = [] + + for (const value of orderedResults) { + const { + toolUse, + toolName, + toolArgs, + toolParams, + result, + startTime, + endTime, + duration, + } = value + + timeSegments.push({ + type: 'tool', + name: toolName, + startTime, + endTime, + duration, + toolCallId: toolUse.id, + }) + + let resultContent: unknown + if (result.success && result.output) { + toolResults.push(result.output as Record) + resultContent = result.output + } else { + resultContent = { + error: true, + message: result.error || 'Tool execution failed', + tool: toolName, + } + } + + toolCalls.push({ + name: toolName, + arguments: toolParams, + startTime: new Date(startTime).toISOString(), + endTime: new Date(endTime).toISOString(), + duration, + result: resultContent, + success: result.success, + }) + + toolUseBlocks.push({ + type: 'tool_use', + id: toolUse.id, + name: toolName, + input: toolArgs, + }) + + toolResultBlocks.push({ + type: 'tool_result', + tool_use_id: toolUse.id, + content: JSON.stringify(resultContent), + }) + } + + const thinkingBlocks = finalMessage.content.filter( + ( + item + ): item is + | Anthropic.Messages.ThinkingBlock + | Anthropic.Messages.RedactedThinkingBlock => + item.type === 'thinking' || item.type === 'redacted_thinking' + ) + + if (toolUseBlocks.length > 0) { + currentMessages.push({ + role: 'assistant', + content: [ + ...thinkingBlocks, + ...toolUseBlocks, + ] as Anthropic.Messages.ContentBlockParam[], + }) + } + if (toolResultBlocks.length > 0) { + currentMessages.push({ + role: 'user', + content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[], + }) + } + + toolsTime += Date.now() - toolsStartTime + iterationCount++ + + if (request.abortSignal?.aborted) { + settleOpenTools(controller, openToolStarts, 'cancelled') + throw new DOMException('Stream aborted', 'AbortError') + } + } catch (error) { + settleOpenTools( + controller, + openToolStarts, + isAbortError(error) ? 'cancelled' : 'error' + ) + throw error + } + } + + const modelCost = calculateCost(request.model, tokens.input, tokens.output) + const toolCostTotal = sumToolCosts(toolResults) + const cost = { + input: modelCost.input, + output: modelCost.output, + total: modelCost.total + (toolCostTotal || 0), + ...(toolCostTotal ? { toolCost: toolCostTotal } : {}), + } + + onComplete({ + content, + tokens, + cost, + toolCalls: + toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + modelTime, + toolsTime, + firstResponseTime, + iterations: iterationCount + 1, + }) + + controller.close() + } catch (error) { + const cancelled = isAbortError(error) + settleOpenTools(controller, openToolStarts, cancelled ? 'cancelled' : 'error') + controller.error(toError(error)) + } + }, + }) +} diff --git a/apps/sim/providers/anthropic/utils.test.ts b/apps/sim/providers/anthropic/utils.test.ts new file mode 100644 index 00000000000..b7202c511b7 --- /dev/null +++ b/apps/sim/providers/anthropic/utils.test.ts @@ -0,0 +1,106 @@ +/** + * @vitest-environment node + * + * Step 3 gate: Anthropic adapter emits AgentStreamEvent objects (thinking + text) + * from Messages stream fixtures; tool_use deltas are ignored until Step 7. + */ +import { describe, expect, it, vi } from 'vitest' +import { + anthropicRedactedThinkingExpectedText, + anthropicRedactedThinkingExpectedTraceThinking, + anthropicRedactedThinkingStreamEvents, + anthropicThinkingTextToolExpectedText, + anthropicThinkingTextToolExpectedThinking, + anthropicThinkingTextToolStreamEvents, +} from '@/providers/__fixtures__/anthropic' +import { createReadableStreamFromAnthropicStream } from '@/providers/anthropic/utils' +import type { AgentStreamEvent } from '@/providers/stream-events' + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +describe('createReadableStreamFromAnthropicStream', () => { + it('emits thinking_delta then text_delta and ignores tool_use (thinking+text+tool fixture)', async () => { + const onComplete = vi.fn() + const stream = createReadableStreamFromAnthropicStream( + (async function* () { + yield* anthropicThinkingTextToolStreamEvents + })() as AsyncIterable, + onComplete + ) + + const events = await collectEvents(stream) + + expect(events.filter((e) => e.type === 'thinking_delta').map((e) => e.text)).toEqual([ + 'I should check the weather before answering. ', + 'Calling get_weather for SF.', + ]) + expect(events.filter((e) => e.type === 'text_delta')).toEqual([ + { + type: 'text_delta', + text: anthropicThinkingTextToolExpectedText, + turn: 'final', + }, + ]) + expect(events.some((e) => e.type === 'tool_call_start' || e.type === 'tool_call_end')).toBe( + false + ) + + expect(onComplete).toHaveBeenCalledTimes(1) + expect(onComplete.mock.calls[0][0]).toMatchObject({ + content: anthropicThinkingTextToolExpectedText, + thinking: anthropicThinkingTextToolExpectedThinking, + usage: { input_tokens: 42, output_tokens: expect.any(Number) }, + }) + }) + + it('records [redacted] for redacted_thinking blocks and streams text', async () => { + const onComplete = vi.fn() + const stream = createReadableStreamFromAnthropicStream( + (async function* () { + yield* anthropicRedactedThinkingStreamEvents + })() as AsyncIterable, + onComplete + ) + + const events = await collectEvents(stream) + expect(events.filter((e) => e.type === 'thinking_delta').map((e) => e.text)).toEqual([ + 'Visible follow-up reasoning after redaction.', + ]) + expect(events.filter((e) => e.type === 'text_delta')).toEqual([ + { + type: 'text_delta', + text: anthropicRedactedThinkingExpectedText, + turn: 'final', + }, + ]) + expect(onComplete.mock.calls[0][0]).toMatchObject({ + content: anthropicRedactedThinkingExpectedText, + thinking: anthropicRedactedThinkingExpectedTraceThinking, + }) + }) + + it('errors the readable stream when the source throws', async () => { + const stream = createReadableStreamFromAnthropicStream( + (async function* () { + yield { + type: 'content_block_delta', + delta: { type: 'text_delta', text: 'partial' }, + } + throw new Error('provider reset') + })() as AsyncIterable + ) + + await expect(collectEvents(stream)).rejects.toThrow('provider reset') + }) +}) diff --git a/apps/sim/providers/anthropic/utils.ts b/apps/sim/providers/anthropic/utils.ts index b9b001bb7a2..f9e27652c14 100644 --- a/apps/sim/providers/anthropic/utils.ts +++ b/apps/sim/providers/anthropic/utils.ts @@ -6,6 +6,7 @@ import type { } from '@anthropic-ai/sdk/resources' import { createLogger } from '@sim/logger' import { randomFloat } from '@sim/utils/random' +import type { AgentStreamEvent } from '@/providers/stream-events' import { trackForcedToolUsage } from '@/providers/utils' const logger = createLogger('AnthropicUtils') @@ -15,34 +16,101 @@ export interface AnthropicStreamUsage { output_tokens: number } +export interface AnthropicStreamComplete { + content: string + usage: AnthropicStreamUsage + /** Assembled thinking text for traces (redacted blocks become `[redacted]`). */ + thinking: string +} + +/** + * Converts an Anthropic Messages stream into an in-process + * {@link AgentStreamEvent} object stream (`thinking_delta` + `text_delta`). + * Tool_use / input_json deltas are ignored here — use + * {@link createAnthropicStreamingToolLoopStream} for the live tool loop. + */ export function createReadableStreamFromAnthropicStream( anthropicStream: AsyncIterable, - onComplete?: (content: string, usage: AnthropicStreamUsage) => void -): ReadableStream { - let fullContent = '' - let inputTokens = 0 - let outputTokens = 0 - - return new ReadableStream({ + onComplete?: (result: AnthropicStreamComplete) => void +): ReadableStream { + return new ReadableStream({ async start(controller) { try { + let fullContent = '' + const thinkingBlocks: string[] = [] + let currentThinking = '' + let inputTokens = 0 + let outputTokens = 0 + + const flushThinkingBlock = () => { + if (currentThinking) { + thinkingBlocks.push(currentThinking) + currentThinking = '' + } + } + for await (const event of anthropicStream) { if (event.type === 'message_start') { const startEvent = event as RawMessageStartEvent const usage: Usage = startEvent.message.usage inputTokens = usage.input_tokens - } else if (event.type === 'message_delta') { + continue + } + + if (event.type === 'message_delta') { const deltaEvent = event as RawMessageDeltaEvent outputTokens = deltaEvent.usage.output_tokens - } else if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') { - const text = event.delta.text - fullContent += text - controller.enqueue(new TextEncoder().encode(text)) + continue + } + + if (event.type === 'content_block_start') { + const block = event.content_block as { type?: string } + if (block?.type === 'redacted_thinking') { + flushThinkingBlock() + thinkingBlocks.push('[redacted]') + } else if (block?.type === 'thinking') { + flushThinkingBlock() + } + continue + } + + if (event.type === 'content_block_stop') { + flushThinkingBlock() + continue + } + + if (event.type !== 'content_block_delta') { + continue + } + + const delta = event.delta as { + type?: string + text?: string + thinking?: string + } + + if (delta.type === 'thinking_delta' && typeof delta.thinking === 'string') { + currentThinking += delta.thinking + controller.enqueue({ type: 'thinking_delta', text: delta.thinking }) + continue + } + + if (delta.type === 'text_delta' && typeof delta.text === 'string') { + flushThinkingBlock() + fullContent += delta.text + controller.enqueue({ type: 'text_delta', text: delta.text, turn: 'final' }) } } + flushThinkingBlock() + if (onComplete) { - onComplete(fullContent, { input_tokens: inputTokens, output_tokens: outputTokens }) + onComplete({ + content: fullContent, + usage: { input_tokens: inputTokens, output_tokens: outputTokens }, + // Match enrichLastModelSegmentFromAnthropicResponse: join blocks with blank lines. + thinking: thinkingBlocks.filter(Boolean).join('\n\n'), + }) } controller.close() diff --git a/apps/sim/providers/azure-openai/index.ts b/apps/sim/providers/azure-openai/index.ts index e9c7cfefb4b..053d375a9c2 100644 --- a/apps/sim/providers/azure-openai/index.ts +++ b/apps/sim/providers/azure-openai/index.ts @@ -201,6 +201,7 @@ async function executeChatCompletionsRequest( timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => { output.content = content @@ -517,6 +518,7 @@ async function executeChatCompletionsRequest( count: toolCalls.length, } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromAzureOpenAIStream(streamResponse, (content, usage) => { output.content = content diff --git a/apps/sim/providers/azure-openai/utils.ts b/apps/sim/providers/azure-openai/utils.ts index fec1e862e51..08a84c0209d 100644 --- a/apps/sim/providers/azure-openai/utils.ts +++ b/apps/sim/providers/azure-openai/utils.ts @@ -3,17 +3,24 @@ import type OpenAI from 'openai' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' import type { Stream } from 'openai/streaming' -import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' +import { checkForForcedToolUsageOpenAI } from '@/providers/utils' /** - * Creates a ReadableStream from an Azure OpenAI streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from an Azure OpenAI streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromAzureOpenAIStream( azureOpenAIStream: Stream, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(azureOpenAIStream, 'Azure OpenAI', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(azureOpenAIStream, { + providerName: 'Azure OpenAI', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } /** diff --git a/apps/sim/providers/baseten/index.ts b/apps/sim/providers/baseten/index.ts index a25fb29cb9c..a5f59d818bb 100644 --- a/apps/sim/providers/baseten/index.ts +++ b/apps/sim/providers/baseten/index.ts @@ -167,6 +167,7 @@ export const basetenProvider: ProviderConfig = { timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { output.content = content @@ -469,6 +470,7 @@ export const basetenProvider: ProviderConfig = { }, toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { output.content = content diff --git a/apps/sim/providers/baseten/utils.ts b/apps/sim/providers/baseten/utils.ts index ca5cf5dc5c0..d277f41a2c9 100644 --- a/apps/sim/providers/baseten/utils.ts +++ b/apps/sim/providers/baseten/utils.ts @@ -1,6 +1,8 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' +import { checkForForcedToolUsageOpenAI } from '@/providers/utils' /** * Checks if a model supports native structured outputs (json_schema). @@ -11,14 +13,19 @@ export async function supportsNativeStructuredOutputs(_modelId: string): Promise } /** - * Creates a ReadableStream from a Baseten streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Baseten streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromOpenAIStream( openaiStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(openaiStream, 'Baseten', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(openaiStream, { + providerName: 'Baseten', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } /** diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts index 4e512d15b48..8bda948d84d 100644 --- a/apps/sim/providers/bedrock/index.ts +++ b/apps/sim/providers/bedrock/index.ts @@ -15,7 +15,7 @@ import { } from '@aws-sdk/client-bedrock-runtime' import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' -import type { IterationToolCall, StreamingExecution } from '@/executor/types' +import type { IterationToolCall, NormalizedBlockOutput, StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { buildBedrockMessageContent } from '@/providers/attachments' import { @@ -24,6 +24,7 @@ import { generateToolUseId, getBedrockInferenceProfileId, } from '@/providers/bedrock/utils' +import { createBedrockStreamingToolLoopStream } from '@/providers/bedrock/streaming-tool-loop' import { getCachedProviderClient } from '@/providers/client-cache' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' import { createStreamingExecution } from '@/providers/streaming-execution' @@ -349,6 +350,60 @@ export const bedrockProvider: ProviderConfig = { const shouldStreamToolCalls = request.streamToolCalls ?? false + if (request.stream && shouldStreamToolCalls && bedrockTools && bedrockTools.length > 0) { + logger.info('Using streaming tool loop for Bedrock request') + + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + const timeSegments: TimeSegment[] = [] + const forcedTools = preparedTools?.forcedTools || [] + + return createStreamingExecution({ + model: request.model, + providerStartTime, + providerStartTimeISO, + timing: { + kind: 'accumulated', + modelTime: 0, + toolsTime: 0, + firstResponseTime: 0, + iterations: 1, + timeSegments, + }, + initialTokens: { input: 0, output: 0, total: 0 }, + initialCost: { total: 0.0, input: 0.0, output: 0.0 }, + isStreaming: true, + streamFormat: 'agent-events-v1', + createStream: ({ output, finalizeTiming }) => + createBedrockStreamingToolLoopStream({ + client, + modelId: bedrockModelId, + request, + messages, + system: systemPromptWithSchema.length > 0 ? systemPromptWithSchema : undefined, + inferenceConfig, + bedrockTools, + toolChoice, + logger, + timeSegments, + forcedTools, + onComplete: (result) => { + output.content = result.content + output.tokens = result.tokens + output.cost = result.cost + output.toolCalls = result.toolCalls as NormalizedBlockOutput['toolCalls'] + if (output.providerTiming) { + output.providerTiming.modelTime = result.modelTime + output.providerTiming.toolsTime = result.toolsTime + output.providerTiming.firstResponseTime = result.firstResponseTime + output.providerTiming.iterations = result.iterations + } + finalizeTiming() + }, + }), + }) + } + if (request.stream && (!bedrockTools || bedrockTools.length === 0)) { logger.info('Using streaming response for Bedrock request (no tools)') @@ -380,6 +435,7 @@ export const bedrockProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { total: 0.0, input: 0.0, output: 0.0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromBedrockStream(bedrockStream, (content, usage) => { output.content = content @@ -878,6 +934,7 @@ export const bedrockProvider: ProviderConfig = { toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromBedrockStream(bedrockStream, (streamContent, usage) => { output.content = streamContent diff --git a/apps/sim/providers/bedrock/streaming-tool-loop.test.ts b/apps/sim/providers/bedrock/streaming-tool-loop.test.ts new file mode 100644 index 00000000000..815b31e13fa --- /dev/null +++ b/apps/sim/providers/bedrock/streaming-tool-loop.test.ts @@ -0,0 +1,141 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { createBedrockStreamingToolLoopStream } from '@/providers/bedrock/streaming-tool-loop' +import type { AgentStreamEvent } from '@/providers/stream-events' + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +vi.mock('@/tools', () => ({ + executeTool: vi.fn(async () => ({ + success: true, + output: { ok: true }, + })), +})) + +vi.mock('@/providers/utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + prepareToolExecution: vi.fn(() => ({ + toolParams: { url: 'https://example.com' }, + executionParams: { url: 'https://example.com' }, + })), + calculateCost: vi.fn(() => ({ + input: 0.01, + output: 0.02, + total: 0.03, + pricing: { input: 1, output: 2, updatedAt: new Date().toISOString() }, + })), + sumToolCosts: vi.fn(() => 0), + } +}) + +describe('createBedrockStreamingToolLoopStream (Step 9)', () => { + it('emits tool_call_start/end and final text; no invented thinking', async () => { + const turns = [ + (async function* () { + yield { + contentBlockStart: { + contentBlockIndex: 0, + start: { toolUse: { toolUseId: 'tooluse_1', name: 'http_request' } }, + }, + } + yield { + contentBlockDelta: { + contentBlockIndex: 0, + delta: { toolUse: { input: '{"url":"https://example.com"}' } }, + }, + } + yield { + metadata: { usage: { inputTokens: 11, outputTokens: 4 } }, + } + yield { messageStop: { stopReason: 'tool_use' } } + })(), + (async function* () { + yield { + contentBlockDelta: { + contentBlockIndex: 0, + delta: { text: 'Request completed.' }, + }, + } + yield { + metadata: { usage: { inputTokens: 22, outputTokens: 6 } }, + } + yield { messageStop: { stopReason: 'end_turn' } } + })(), + ] + + let turnIdx = 0 + const client = { + send: vi.fn(async () => ({ stream: turns[turnIdx++] })), + } + + const onComplete = vi.fn() + const stream = createBedrockStreamingToolLoopStream({ + client: client as any, + modelId: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0', + request: { + model: 'bedrock/anthropic.claude-sonnet-4-5-20250929-v1:0', + tools: [ + { + id: 'http_request', + name: 'http_request', + description: 'HTTP', + parameters: { type: 'object', properties: {}, required: [] }, + }, + ], + } as any, + messages: [{ role: 'user', content: [{ text: 'call it' }] }], + inferenceConfig: { temperature: 0.7 }, + bedrockTools: [ + { + toolSpec: { + name: 'http_request', + description: 'HTTP', + inputSchema: { json: { type: 'object', properties: {} } }, + }, + }, + ], + toolChoice: { auto: {} }, + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as any, + timeSegments: [], + onComplete, + }) + + const events = await collectEvents(stream) + + expect(events.some((e) => e.type === 'thinking_delta')).toBe(false) + expect(events.filter((e) => e.type === 'tool_call_start')).toEqual([ + { type: 'tool_call_start', id: 'tooluse_1', name: 'http_request' }, + ]) + expect(events.filter((e) => e.type === 'tool_call_end')).toEqual([ + { type: 'tool_call_end', id: 'tooluse_1', name: 'http_request', status: 'success' }, + ]) + expect( + events + .filter((e) => e.type === 'text_delta' && e.turn === 'final') + .map((e) => e.text) + .join('') + ).toBe('Request completed.') + + expect(onComplete).toHaveBeenCalledWith( + expect.objectContaining({ + content: 'Request completed.', + toolCalls: expect.objectContaining({ count: 1 }), + }) + ) + }) +}) diff --git a/apps/sim/providers/bedrock/streaming-tool-loop.ts b/apps/sim/providers/bedrock/streaming-tool-loop.ts new file mode 100644 index 00000000000..b3e39816ea3 --- /dev/null +++ b/apps/sim/providers/bedrock/streaming-tool-loop.ts @@ -0,0 +1,541 @@ +/** + * Live Bedrock ConverseStream tool loop (Step 9). + * + * Capability-honest: text + tool_call_start/end only — no invented thinking. + * Final-turn-only answer projection via `turn` tags. Abort → cancelled. + */ + +import { + type ContentBlock, + type ConversationRole, + ConverseStreamCommand, + type BedrockRuntimeClient, + type Message as BedrockMessage, + type SystemContentBlock, + type Tool, + type ToolConfiguration, + type ToolResultBlock, + type ToolUseBlock, +} from '@aws-sdk/client-bedrock-runtime' +import type { Logger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import type { NormalizedBlockOutput } from '@/executor/types' +import { MAX_TOOL_ITERATIONS } from '@/providers' +import { checkForForcedToolUsage, generateToolUseId } from '@/providers/bedrock/utils' +import type { AgentStreamEvent, ToolCallEndStatus } from '@/providers/stream-events' +import { enrichLastModelSegment } from '@/providers/trace-enrichment' +import type { ProviderRequest, TimeSegment } from '@/providers/types' +import { + calculateCost, + prepareToolExecution, + sumToolCosts, +} from '@/providers/utils' +import { executeTool } from '@/tools' + +export interface BedrockStreamingToolLoopComplete { + content: string + tokens: { input: number; output: number; total: number } + cost: NormalizedBlockOutput['cost'] + toolCalls?: { list: unknown[]; count: number } + modelTime: number + toolsTime: number + firstResponseTime: number + iterations: number +} + +export interface CreateBedrockStreamingToolLoopStreamOptions { + client: BedrockRuntimeClient + modelId: string + request: ProviderRequest + messages: BedrockMessage[] + system?: SystemContentBlock[] + inferenceConfig: { temperature: number; maxTokens?: number } + bedrockTools: Tool[] + toolChoice: ToolConfiguration['toolChoice'] + logger: Logger + timeSegments: TimeSegment[] + forcedTools?: string[] + onComplete: (result: BedrockStreamingToolLoopComplete) => void +} + +interface AssembledToolUse { + toolUseId: string + name: string + inputJson: string +} + +function isAbortError(error: unknown): boolean { + if (!error || typeof error !== 'object') return false + const name = (error as { name?: string }).name + return name === 'AbortError' || name === 'APIUserAbortError' +} + +function settleOpenTools( + controller: ReadableStreamDefaultController, + openTools: Map, + status: ToolCallEndStatus +): void { + for (const [id, name] of openTools) { + controller.enqueue({ type: 'tool_call_end', id, name, status }) + } + openTools.clear() +} + +function parseToolInput(inputJson: string): Record { + if (!inputJson.trim()) return {} + try { + const parsed = JSON.parse(inputJson) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? (parsed as Record) + : {} + } catch { + return {} + } +} + +async function drainBedrockTurn( + stream: AsyncIterable, + controller: ReadableStreamDefaultController, + openTools: Map +): Promise<{ + text: string + textChunks: string[] + toolUses: AssembledToolUse[] + inputTokens: number + outputTokens: number + stopReason?: string +}> { + let text = '' + const textChunks: string[] = [] + const toolsByIndex = new Map() + let currentIndex: number | undefined + let inputTokens = 0 + let outputTokens = 0 + let stopReason: string | undefined + + for await (const event of stream) { + if (event.contentBlockStart) { + currentIndex = event.contentBlockStart.contentBlockIndex + const start = event.contentBlockStart.start + if (start && 'toolUse' in start && start.toolUse) { + const id = start.toolUse.toolUseId || generateToolUseId(start.toolUse.name || 'tool') + const name = start.toolUse.name || '' + if (typeof currentIndex === 'number') { + toolsByIndex.set(currentIndex, { toolUseId: id, name, inputJson: '' }) + } + if (id && name && !openTools.has(id)) { + openTools.set(id, name) + controller.enqueue({ type: 'tool_call_start', id, name }) + } + } + continue + } + + if (event.contentBlockDelta) { + const idx = event.contentBlockDelta.contentBlockIndex ?? currentIndex + const delta = event.contentBlockDelta.delta + if (delta?.text) { + text += delta.text + textChunks.push(delta.text) + } + if (delta && 'toolUse' in delta && delta.toolUse?.input && typeof idx === 'number') { + const pending = toolsByIndex.get(idx) + if (pending) { + pending.inputJson += delta.toolUse.input + } + } + continue + } + + if (event.metadata?.usage) { + inputTokens = event.metadata.usage.inputTokens ?? inputTokens + outputTokens = event.metadata.usage.outputTokens ?? outputTokens + continue + } + + if (event.messageStop?.stopReason) { + stopReason = event.messageStop.stopReason + } + } + + return { + text, + textChunks, + toolUses: [...toolsByIndex.values()], + inputTokens, + outputTokens, + stopReason, + } +} + +/** + * Multi-turn Bedrock ConverseStream tool loop as agent-events-v1. + */ +export function createBedrockStreamingToolLoopStream( + options: CreateBedrockStreamingToolLoopStreamOptions +): ReadableStream { + const { + client, + modelId, + request, + messages: initialMessages, + system, + inferenceConfig, + bedrockTools, + logger, + timeSegments, + onComplete, + } = options + const forcedTools = options.forcedTools ?? [] + const originalToolChoice = options.toolChoice + + return new ReadableStream({ + async start(controller) { + const currentMessages = [...initialMessages] + let toolChoice = originalToolChoice + let usedForcedTools: string[] = [] + let hasUsedForcedTool = false + + let content = '' + let iterationCount = 0 + let modelTime = 0 + let toolsTime = 0 + let firstResponseTime = 0 + const tokens = { input: 0, output: 0, total: 0 } + let costInput = 0 + let costOutput = 0 + let costTotal = 0 + let latestPricing: ReturnType['pricing'] | undefined + const toolCalls: unknown[] = [] + const toolResults: Record[] = [] + const openToolStarts = new Map() + + try { + while (iterationCount < MAX_TOOL_ITERATIONS) { + if (request.abortSignal?.aborted) { + settleOpenTools(controller, openToolStarts, 'cancelled') + throw new DOMException('Stream aborted', 'AbortError') + } + + const toolConfig: ToolConfiguration | undefined = bedrockTools.length + ? { tools: bedrockTools, toolChoice } + : undefined + + const modelStart = Date.now() + const command = new ConverseStreamCommand({ + modelId, + messages: currentMessages, + system: system && system.length > 0 ? system : undefined, + inferenceConfig, + toolConfig, + }) + + const streamResponse = await client.send( + command, + request.abortSignal ? { abortSignal: request.abortSignal } : undefined + ) + if (!streamResponse.stream) { + throw new Error('No stream returned from Bedrock') + } + + const drained = await drainBedrockTurn( + streamResponse.stream, + controller, + openToolStarts + ) + const modelEnd = Date.now() + const thisModelTime = modelEnd - modelStart + modelTime += thisModelTime + if (iterationCount === 0) { + firstResponseTime = thisModelTime + } + + timeSegments.push({ + type: 'model', + name: request.model, + startTime: modelStart, + endTime: modelEnd, + duration: thisModelTime, + }) + + tokens.input += drained.inputTokens + tokens.output += drained.outputTokens + tokens.total += drained.inputTokens + drained.outputTokens + + const turnCost = calculateCost(request.model, drained.inputTokens, drained.outputTokens) + costInput += turnCost.input + costOutput += turnCost.output + costTotal += turnCost.total + latestPricing = turnCost.pricing + + const turnTag = drained.toolUses.length > 0 ? 'intermediate' : 'final' + for (const chunk of drained.textChunks) { + controller.enqueue({ type: 'text_delta', text: chunk, turn: turnTag }) + } + if (drained.text) { + content = drained.text + } + + const assembledToolUses: ToolUseBlock[] = drained.toolUses.map((t) => ({ + toolUseId: t.toolUseId, + name: t.name, + input: parseToolInput(t.inputJson), + })) + + enrichLastModelSegment(timeSegments, { + assistantContent: drained.text || undefined, + toolCalls: + assembledToolUses.length > 0 + ? assembledToolUses.map((t) => ({ + id: t.toolUseId || '', + name: t.name || '', + arguments: (t.input as Record) || {}, + })) + : undefined, + finishReason: drained.stopReason, + tokens: { + input: drained.inputTokens, + output: drained.outputTokens, + total: drained.inputTokens + drained.outputTokens, + }, + cost: { + input: turnCost.input, + output: turnCost.output, + total: turnCost.total, + }, + provider: 'bedrock', + }) + + const forcedCheck = checkForForcedToolUsage( + assembledToolUses.map((t) => ({ name: t.name || '' })), + toolChoice, + forcedTools, + usedForcedTools + ) + if (forcedCheck) { + hasUsedForcedTool = forcedCheck.hasUsedForcedTool + usedForcedTools = forcedCheck.usedForcedTools + } + + if (assembledToolUses.length === 0) { + break + } + + const toolsStartTime = Date.now() + const orderedResults = await Promise.all( + assembledToolUses.map(async (toolUse) => { + const toolCallStartTime = Date.now() + const toolName = toolUse.name || '' + const toolArgs = (toolUse.input as Record) || {} + const toolUseId = toolUse.toolUseId || generateToolUseId(toolName) + + try { + if (request.abortSignal?.aborted) { + throw new DOMException('Stream aborted', 'AbortError') + } + + const tool = request.tools?.find((t) => t.id === toolName) + if (!tool) { + const value = { + toolUse, + toolUseId, + toolName, + toolArgs, + toolParams: {} as Record, + result: { + success: false as const, + output: undefined, + error: `Tool not found: ${toolName}`, + }, + startTime: toolCallStartTime, + endTime: Date.now(), + duration: Date.now() - toolCallStartTime, + status: 'error' as ToolCallEndStatus, + } + openToolStarts.delete(toolUseId) + controller.enqueue({ + type: 'tool_call_end', + id: toolUseId, + name: toolName, + status: 'error', + }) + return value + } + + const { toolParams, executionParams } = prepareToolExecution( + tool, + toolArgs, + request + ) + const result = await executeTool(toolName, executionParams, { + signal: request.abortSignal, + }) + const toolCallEndTime = Date.now() + const status: ToolCallEndStatus = result.success ? 'success' : 'error' + openToolStarts.delete(toolUseId) + controller.enqueue({ + type: 'tool_call_end', + id: toolUseId, + name: toolName, + status, + }) + return { + toolUse, + toolUseId, + toolName, + toolArgs, + toolParams, + result, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + status, + } + } catch (error) { + const toolCallEndTime = Date.now() + const cancelled = isAbortError(error) || !!request.abortSignal?.aborted + if (!cancelled) { + logger.error('Error processing tool call:', { error, toolName }) + } + const status: ToolCallEndStatus = cancelled ? 'cancelled' : 'error' + openToolStarts.delete(toolUseId) + controller.enqueue({ + type: 'tool_call_end', + id: toolUseId, + name: toolName, + status, + }) + return { + toolUse, + toolUseId, + toolName, + toolArgs, + toolParams: {} as Record, + result: { + success: false as const, + output: undefined, + error: getErrorMessage(error, 'Tool execution failed'), + }, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + status, + } + } + }) + ) + + toolsTime += Date.now() - toolsStartTime + + const assistantContent: ContentBlock[] = assembledToolUses.map((toolUse) => ({ + toolUse: { + toolUseId: toolUse.toolUseId, + name: toolUse.name, + input: toolUse.input, + }, + })) + currentMessages.push({ + role: 'assistant' as ConversationRole, + content: assistantContent, + }) + + const toolResultContent: ContentBlock[] = [] + for (const value of orderedResults) { + const { + toolUseId, + toolName, + toolParams, + result, + startTime, + endTime, + duration, + } = value + + timeSegments.push({ + type: 'tool', + name: toolName, + startTime, + endTime, + duration, + toolCallId: toolUseId, + }) + + let resultContent: unknown + if (result.success && result.output) { + toolResults.push(result.output as Record) + resultContent = result.output + } else { + resultContent = { + error: true, + message: result.error || 'Tool execution failed', + tool: toolName, + } + } + + toolCalls.push({ + name: toolName, + arguments: toolParams, + startTime: new Date(startTime).toISOString(), + endTime: new Date(endTime).toISOString(), + duration, + result: resultContent, + success: result.success, + }) + + const toolResultBlock: ToolResultBlock = { + toolUseId, + content: [{ text: JSON.stringify(resultContent) }], + } + toolResultContent.push({ toolResult: toolResultBlock }) + } + + if (toolResultContent.length > 0) { + currentMessages.push({ + role: 'user' as ConversationRole, + content: toolResultContent, + }) + } + + if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) { + const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool)) + toolChoice = remainingTools.length > 0 ? { tool: { name: remainingTools[0] } } : { auto: {} } + } else if (hasUsedForcedTool && typeof originalToolChoice === 'object') { + toolChoice = { auto: {} } + } + + iterationCount += 1 + } + + const toolCost = sumToolCosts(toolResults) + onComplete({ + content, + tokens, + cost: { + input: costInput, + output: costOutput, + toolCost: toolCost || undefined, + total: costTotal + toolCost, + pricing: latestPricing, + }, + toolCalls: + toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + modelTime, + toolsTime, + firstResponseTime, + iterations: iterationCount + 1, + }) + controller.close() + } catch (error) { + if (isAbortError(error) || request.abortSignal?.aborted) { + settleOpenTools(controller, openToolStarts, 'cancelled') + } else { + settleOpenTools(controller, openToolStarts, 'error') + logger.error('Bedrock streaming tool loop failed', { + error: toError(error).message, + }) + } + controller.error(error) + } + }, + }) +} diff --git a/apps/sim/providers/bedrock/utils.stream.test.ts b/apps/sim/providers/bedrock/utils.stream.test.ts new file mode 100644 index 00000000000..15c75fe6dd9 --- /dev/null +++ b/apps/sim/providers/bedrock/utils.stream.test.ts @@ -0,0 +1,51 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { createReadableStreamFromBedrockStream } from '@/providers/bedrock/utils' +import type { AgentStreamEvent } from '@/providers/stream-events' + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +describe('createReadableStreamFromBedrockStream (Step 9)', () => { + it('emits tool_call_start then text; no invented thinking', async () => { + const onComplete = vi.fn() + const stream = createReadableStreamFromBedrockStream( + (async function* () { + yield { + contentBlockStart: { + start: { + toolUse: { toolUseId: 'tooluse_1', name: 'http_request' }, + }, + }, + } as any + yield { + contentBlockDelta: { delta: { text: 'Done' } }, + } as any + yield { + metadata: { usage: { inputTokens: 2, outputTokens: 3 } }, + } as any + })(), + onComplete + ) + + const events = await collectEvents(stream) + expect(events).toEqual([ + { type: 'tool_call_start', id: 'tooluse_1', name: 'http_request' }, + { type: 'text_delta', text: 'Done', turn: 'final' }, + ]) + expect(events.some((e) => e.type === 'thinking_delta')).toBe(false) + expect(onComplete).toHaveBeenCalledWith('Done', { inputTokens: 2, outputTokens: 3 }) + }) +}) diff --git a/apps/sim/providers/bedrock/utils.ts b/apps/sim/providers/bedrock/utils.ts index a385ffd053b..688943f16bd 100644 --- a/apps/sim/providers/bedrock/utils.ts +++ b/apps/sim/providers/bedrock/utils.ts @@ -1,6 +1,7 @@ import type { ConverseStreamOutput } from '@aws-sdk/client-bedrock-runtime' import { createLogger } from '@sim/logger' import { randomFloat } from '@sim/utils/random' +import type { AgentStreamEvent } from '@/providers/stream-events' import { trackForcedToolUsage } from '@/providers/utils' const logger = createLogger('BedrockUtils') @@ -10,10 +11,15 @@ export interface BedrockStreamUsage { outputTokens: number } +/** + * Bedrock ConverseStream → agent-events-v1. + * Capability-honest: text deltas always; tool_call_start when toolUse id+name + * appear on contentBlockStart. No thinking_delta unless/until Bedrock exposes it. + */ export function createReadableStreamFromBedrockStream( bedrockStream: AsyncIterable, onComplete?: (content: string, usage: BedrockStreamUsage) => void -): ReadableStream { +): ReadableStream { let fullContent = '' let inputTokens = 0 let outputTokens = 0 @@ -22,10 +28,20 @@ export function createReadableStreamFromBedrockStream( async start(controller) { try { for await (const event of bedrockStream) { + const startBlock = event.contentBlockStart?.start + if (startBlock && 'toolUse' in startBlock && startBlock.toolUse) { + const toolUse = startBlock.toolUse + const id = toolUse.toolUseId + const name = toolUse.name + if (id && name) { + controller.enqueue({ type: 'tool_call_start', id, name }) + } + } + if (event.contentBlockDelta?.delta?.text) { const text = event.contentBlockDelta.delta.text fullContent += text - controller.enqueue(new TextEncoder().encode(text)) + controller.enqueue({ type: 'text_delta', text, turn: 'final' }) } else if (event.metadata?.usage) { inputTokens = event.metadata.usage.inputTokens ?? 0 outputTokens = event.metadata.usage.outputTokens ?? 0 diff --git a/apps/sim/providers/cerebras/index.ts b/apps/sim/providers/cerebras/index.ts index c351ffc4268..992d4ffdb5a 100644 --- a/apps/sim/providers/cerebras/index.ts +++ b/apps/sim/providers/cerebras/index.ts @@ -133,6 +133,7 @@ export const cerebrasProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromCerebrasStream(streamResponse, (content, usage) => { output.content = content @@ -492,6 +493,7 @@ export const cerebrasProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromCerebrasStream(streamResponse, (content, usage) => { output.content = content diff --git a/apps/sim/providers/cerebras/utils.ts b/apps/sim/providers/cerebras/utils.ts index 830d0d67140..96a36bd7be2 100644 --- a/apps/sim/providers/cerebras/utils.ts +++ b/apps/sim/providers/cerebras/utils.ts @@ -1,5 +1,6 @@ import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' interface CerebrasChunk { choices?: Array<{ @@ -15,12 +16,17 @@ interface CerebrasChunk { } /** - * Creates a ReadableStream from a Cerebras streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Cerebras streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromCerebrasStream( cerebrasStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(cerebrasStream as any, 'Cerebras', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(cerebrasStream as any, { + providerName: 'Cerebras', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/deepseek/index.test.ts b/apps/sim/providers/deepseek/index.test.ts new file mode 100644 index 00000000000..aa21248450c --- /dev/null +++ b/apps/sim/providers/deepseek/index.test.ts @@ -0,0 +1,99 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ProviderRequest } from '@/providers/types' + +const { mockCreate } = vi.hoisted(() => ({ + mockCreate: vi.fn(), +})) + +vi.mock('openai', () => ({ + default: vi.fn().mockImplementation( + class { + chat = { completions: { create: mockCreate } } + } + ), +})) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 5 })) + +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn(() => ['deepseek-chat']), + getProviderDefaultModel: vi.fn(() => 'deepseek-chat'), +})) + +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: vi.fn((messages) => messages), +})) + +vi.mock('@/providers/deepseek/utils', () => ({ + createReadableStreamFromDeepseekStream: vi.fn(), +})) + +vi.mock('@/providers/openai-compat/streaming-tool-loop', () => ({ + createOpenAICompatStreamingToolLoopStream: vi.fn(), +})) + +vi.mock('@/providers/streaming-execution', () => ({ + createStreamingExecution: vi.fn((args) => args), +})) + +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + prepareToolExecution: vi.fn((_tool, args) => ({ toolParams: args, executionParams: args })), + prepareToolsWithUsageControl: vi.fn(() => ({ + tools: [], + toolChoice: undefined, + forcedTools: [], + hasFilteredTools: false, + })), + sumToolCosts: vi.fn(() => 0), + trackForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), +})) + +vi.mock('@/tools', () => ({ executeTool: vi.fn() })) + +import { deepseekProvider } from '@/providers/deepseek/index' + +function request(overrides: Partial = {}): ProviderRequest { + return { + model: 'deepseek-chat', + apiKey: 'test-key', + messages: [{ role: 'user', content: 'hi' }], + ...overrides, + } +} + +describe('deepseekProvider thinking payload', () => { + beforeEach(() => { + mockCreate.mockReset() + mockCreate.mockResolvedValue({ + choices: [{ message: { content: 'ok', tool_calls: [] } }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + }) + + it('sets thinking: { type: enabled } when thinkingLevel is enabled', async () => { + await deepseekProvider.executeRequest(request({ thinkingLevel: 'enabled' })) + expect(mockCreate).toHaveBeenCalled() + const payload = mockCreate.mock.calls[0][0] + expect(payload.thinking).toEqual({ type: 'enabled' }) + }) + + it('omits thinking when thinkingLevel is none', async () => { + await deepseekProvider.executeRequest(request({ thinkingLevel: 'none' })) + const payload = mockCreate.mock.calls[0][0] + expect(payload.thinking).toBeUndefined() + }) + + it('omits thinking when thinkingLevel is unset', async () => { + await deepseekProvider.executeRequest(request()) + const payload = mockCreate.mock.calls[0][0] + expect(payload.thinking).toBeUndefined() + }) +}) diff --git a/apps/sim/providers/deepseek/index.ts b/apps/sim/providers/deepseek/index.ts index 37f09254be3..109592c41e7 100644 --- a/apps/sim/providers/deepseek/index.ts +++ b/apps/sim/providers/deepseek/index.ts @@ -1,11 +1,12 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import OpenAI from 'openai' -import type { StreamingExecution } from '@/executor/types' +import type { NormalizedBlockOutput, StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { formatMessagesForProvider } from '@/providers/attachments' import { createReadableStreamFromDeepseekStream } from '@/providers/deepseek/utils' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' +import { createOpenAICompatStreamingToolLoopStream } from '@/providers/openai-compat/streaming-tool-loop' import { createStreamingExecution } from '@/providers/streaming-execution' import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter' import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment' @@ -84,6 +85,11 @@ export const deepseekProvider: ProviderConfig = { if (request.temperature !== undefined) payload.temperature = request.temperature if (request.maxTokens != null) payload.max_tokens = request.maxTokens + // DeepSeek Think mode: reasoning_content streams when enabled (or inherent on reasoner). + if (request.thinkingLevel && request.thinkingLevel !== 'none') { + payload.thinking = { type: 'enabled' } + } + let preparedTools: ReturnType | null = null if (tools?.length) { @@ -111,6 +117,68 @@ export const deepseekProvider: ProviderConfig = { } } + const shouldStreamToolCalls = request.streamToolCalls ?? false + + if (request.stream && shouldStreamToolCalls && payload.tools?.length) { + logger.info('Using streaming tool loop for DeepSeek request') + + const timeSegments: TimeSegment[] = [] + const forcedTools = preparedTools?.forcedTools || [] + + return createStreamingExecution({ + model: request.model, + providerStartTime, + providerStartTimeISO, + timing: { + kind: 'accumulated', + modelTime: 0, + toolsTime: 0, + firstResponseTime: 0, + iterations: 1, + timeSegments, + }, + initialTokens: { input: 0, output: 0, total: 0 }, + initialCost: { total: 0.0, input: 0.0, output: 0.0 }, + isStreaming: true, + streamFormat: 'agent-events-v1', + createStream: ({ output, finalizeTiming }) => + createOpenAICompatStreamingToolLoopStream({ + providerName: 'Deepseek', + request, + basePayload: payload, + messages: formattedMessages as any, + createStream: async (params, options) => + deepseek.chat.completions.create( + { ...params, stream: true }, + options + ), + createBlocking: async (params, options) => + deepseek.chat.completions.create( + { ...params, stream: false }, + options + ), + logger, + timeSegments, + forcedTools, + preserveAssistantReasoning: + !!request.thinkingLevel && request.thinkingLevel !== 'none', + onComplete: (result) => { + output.content = result.content + output.tokens = result.tokens + output.cost = result.cost + output.toolCalls = result.toolCalls as NormalizedBlockOutput['toolCalls'] + if (output.providerTiming) { + output.providerTiming.modelTime = result.modelTime + output.providerTiming.toolsTime = result.toolsTime + output.providerTiming.firstResponseTime = result.firstResponseTime + output.providerTiming.iterations = result.iterations + } + finalizeTiming() + }, + }), + }) + } + if (request.stream && (!tools || tools.length === 0)) { logger.info('Using streaming response for DeepSeek request (no tools)') @@ -130,26 +198,37 @@ export const deepseekProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => - createReadableStreamFromDeepseekStream(streamResponse as any, (content, usage) => { - output.content = content - output.tokens = { - input: usage.prompt_tokens, - output: usage.completion_tokens, - total: usage.total_tokens, - } + createReadableStreamFromDeepseekStream( + streamResponse as any, + (content, usage, thinking) => { + output.content = content + output.tokens = { + input: usage.prompt_tokens, + output: usage.completion_tokens, + total: usage.total_tokens, + } + + const costResult = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + output.cost = { + input: costResult.input, + output: costResult.output, + total: costResult.total, + } - const costResult = calculateCost( - request.model, - usage.prompt_tokens, - usage.completion_tokens - ) - output.cost = { - input: costResult.input, - output: costResult.output, - total: costResult.total, + if (thinking) { + const segment = output.providerTiming?.timeSegments?.[0] + if (segment) { + segment.thinkingContent = thinking + } + } } - }), + ), }) return streamingResult @@ -281,7 +360,8 @@ export const deepseekProvider: ProviderConfig = { const executionResults = await Promise.allSettled(toolExecutionPromises) - currentMessages.push({ + const assistantMessage = currentResponse.choices[0]?.message + const assistantHistory: Record = { role: 'assistant', content: null, tool_calls: toolCallsInResponse.map((tc) => ({ @@ -292,7 +372,17 @@ export const deepseekProvider: ProviderConfig = { arguments: tc.function.arguments, }, })), - }) + } + if (request.thinkingLevel && request.thinkingLevel !== 'none' && assistantMessage) { + const reasoningContent = (assistantMessage as { reasoning_content?: string }) + .reasoning_content + if (typeof reasoningContent === 'string' && reasoningContent.length > 0) { + assistantHistory.reasoning_content = reasoningContent + } + } + currentMessages.push( + assistantHistory as OpenAI.Chat.Completions.ChatCompletionMessageParam + ) for (const settledResult of executionResults) { if (settledResult.status === 'rejected' || !settledResult.value) continue @@ -480,28 +570,39 @@ export const deepseekProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => - createReadableStreamFromDeepseekStream(streamResponse as any, (content, usage) => { - output.content = content - output.tokens = { - input: tokens.input + usage.prompt_tokens, - output: tokens.output + usage.completion_tokens, - total: tokens.total + usage.total_tokens, - } + createReadableStreamFromDeepseekStream( + streamResponse as any, + (content, usage, thinking) => { + output.content = content + output.tokens = { + input: tokens.input + usage.prompt_tokens, + output: tokens.output + usage.completion_tokens, + total: tokens.total + usage.total_tokens, + } - const streamCost = calculateCost( - request.model, - usage.prompt_tokens, - usage.completion_tokens - ) - const tc = sumToolCosts(toolResults) - output.cost = { - input: accumulatedCost.input + streamCost.input, - output: accumulatedCost.output + streamCost.output, - toolCost: tc || undefined, - total: accumulatedCost.total + streamCost.total + tc, + const streamCost = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + const tc = sumToolCosts(toolResults) + output.cost = { + input: accumulatedCost.input + streamCost.input, + output: accumulatedCost.output + streamCost.output, + toolCost: tc || undefined, + total: accumulatedCost.total + streamCost.total + tc, + } + + if (thinking) { + const lastModel = [...timeSegments].reverse().find((s) => s.type === 'model') + if (lastModel) { + lastModel.thinkingContent = thinking + } + } } - }), + ), }) return streamingResult diff --git a/apps/sim/providers/deepseek/utils.ts b/apps/sim/providers/deepseek/utils.ts index 1c7a67488b2..ccfeb9e10fc 100644 --- a/apps/sim/providers/deepseek/utils.ts +++ b/apps/sim/providers/deepseek/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from a DeepSeek streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a DeepSeek streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromDeepseekStream( deepseekStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(deepseekStream, 'Deepseek', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(deepseekStream, { + providerName: 'Deepseek', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/fireworks/index.ts b/apps/sim/providers/fireworks/index.ts index c6981d4abf0..f30a58768bb 100644 --- a/apps/sim/providers/fireworks/index.ts +++ b/apps/sim/providers/fireworks/index.ts @@ -167,6 +167,7 @@ export const fireworksProvider: ProviderConfig = { timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { output.content = content @@ -469,6 +470,7 @@ export const fireworksProvider: ProviderConfig = { }, toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { output.content = content diff --git a/apps/sim/providers/fireworks/utils.ts b/apps/sim/providers/fireworks/utils.ts index 70444e07b69..c4abd3111bd 100644 --- a/apps/sim/providers/fireworks/utils.ts +++ b/apps/sim/providers/fireworks/utils.ts @@ -1,6 +1,8 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' +import { checkForForcedToolUsageOpenAI } from '@/providers/utils' /** * Checks if a model supports native structured outputs (json_schema). @@ -11,14 +13,19 @@ export async function supportsNativeStructuredOutputs(_modelId: string): Promise } /** - * Creates a ReadableStream from a Fireworks streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Fireworks streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromOpenAIStream( openaiStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(openaiStream, 'Fireworks', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(openaiStream, { + providerName: 'Fireworks', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } /** diff --git a/apps/sim/providers/gemini/core.ts b/apps/sim/providers/gemini/core.ts index 1419df73782..67b2d8f5815 100644 --- a/apps/sim/providers/gemini/core.ts +++ b/apps/sim/providers/gemini/core.ts @@ -13,7 +13,7 @@ import { } from '@google/genai' import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' -import type { IterationToolCall, StreamingExecution } from '@/executor/types' +import type { IterationToolCall, NormalizedBlockOutput, StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { checkForForcedToolUsage, @@ -29,6 +29,9 @@ import { supportsDisablingGemini25Thinking, } from '@/providers/google/utils' import { enrichLastModelSegment } from '@/providers/trace-enrichment' +import { ensureToolCallId } from '@/providers/tool-call-id' +import { createStreamingExecution } from '@/providers/streaming-execution' +import { createGeminiStreamingToolLoopStream } from '@/providers/gemini/streaming-tool-loop' import type { FunctionCallResponse, ProviderRequest, @@ -228,7 +231,7 @@ async function executeToolCallsBatch( startTime: r.startTime, endTime: r.endTime, duration: r.duration, - toolCallId: r.part.functionCall?.id ?? undefined, + toolCallId: ensureToolCallId(r.part.functionCall?.id, 'gemini'), }) totalToolsTime += r.duration @@ -955,8 +958,9 @@ export async function executeGeminiRequest( } // Gemini 3.x takes thinkingLevel directly; Gemini 2.5-series rejects it and needs thinkingBudget. + // includeThoughts: true is required for thought parts to appear (Step 9 capability-honest). if (request.thinkingLevel && request.thinkingLevel !== 'none') { - const thinkingConfig: ThinkingConfig = { includeThoughts: false } + const thinkingConfig: ThinkingConfig = { includeThoughts: true } if (isGemini3Model(model)) { thinkingConfig.thinkingLevel = mapToThinkingLevel(request.thinkingLevel) } else { @@ -1019,8 +1023,60 @@ export async function executeGeminiRequest( } const initialCallTime = Date.now() + const shouldStreamToolCalls = request.streamToolCalls ?? false const shouldStream = request.stream && !tools?.length + // Live streaming tool loop (Step 9) + if (request.stream && shouldStreamToolCalls && tools?.length) { + logger.info('Using streaming tool loop for Gemini request') + + const timeSegments: TimeSegment[] = [] + const forcedTools = preparedTools?.forcedTools ?? [] + + return createStreamingExecution({ + model, + providerStartTime, + providerStartTimeISO, + timing: { + kind: 'accumulated', + modelTime: 0, + toolsTime: 0, + firstResponseTime: 0, + iterations: 1, + timeSegments, + }, + initialTokens: { input: 0, output: 0, total: 0 }, + initialCost: { total: 0.0, input: 0.0, output: 0.0 }, + isStreaming: true, + streamFormat: 'agent-events-v1', + createStream: ({ output, finalizeTiming }) => + createGeminiStreamingToolLoopStream({ + ai, + model, + baseConfig: geminiConfig, + contents, + request, + logger, + timeSegments, + forcedTools, + toolConfig, + onComplete: (result) => { + output.content = result.content + output.tokens = result.tokens + output.cost = result.cost + output.toolCalls = result.toolCalls as NormalizedBlockOutput['toolCalls'] + if (output.providerTiming) { + output.providerTiming.modelTime = result.modelTime + output.providerTiming.toolsTime = result.toolsTime + output.providerTiming.firstResponseTime = result.firstResponseTime + output.providerTiming.iterations = result.iterations + } + finalizeTiming() + }, + }), + }) + } + // Streaming without tools if (shouldStream) { logger.info('Handling Gemini streaming response') @@ -1042,7 +1098,7 @@ export async function executeGeminiRequest( const stream = createReadableStreamFromGeminiStream( streamGenerator, - (content: string, usage: GeminiUsage) => { + (content: string, usage: GeminiUsage, thinking?: string) => { streamingResult.execution.output.content = content streamingResult.execution.output.tokens = { input: usage.promptTokenCount, @@ -1057,6 +1113,13 @@ export async function executeGeminiRequest( ) streamingResult.execution.output.cost = costResult + if (thinking) { + const segment = streamingResult.execution.output.providerTiming?.timeSegments?.[0] + if (segment) { + segment.thinkingContent = thinking + } + } + const streamEndTime = Date.now() if (streamingResult.execution.output.providerTiming) { streamingResult.execution.output.providerTiming.endTime = new Date( @@ -1073,7 +1136,7 @@ export async function executeGeminiRequest( } ) - return { ...streamingResult, stream } + return { ...streamingResult, stream, streamFormat: 'agent-events-v1' as const } } // Non-streaming request @@ -1193,7 +1256,7 @@ export async function executeGeminiRequest( const stream = createReadableStreamFromGeminiStream( streamGenerator, - (streamContent: string, usage: GeminiUsage) => { + (streamContent: string, usage: GeminiUsage, thinking?: string) => { streamingResult.execution.output.content = streamContent streamingResult.execution.output.tokens = { input: accumulatedTokens.input + usage.promptTokenCount, @@ -1215,6 +1278,14 @@ export async function executeGeminiRequest( pricing: streamCost.pricing, } + if (thinking) { + const segments = streamingResult.execution.output.providerTiming?.timeSegments + const lastModel = segments ? [...segments].reverse().find((s) => s.type === 'model') : undefined + if (lastModel) { + lastModel.thinkingContent = thinking + } + } + if (streamingResult.execution.output.providerTiming) { streamingResult.execution.output.providerTiming.endTime = new Date().toISOString() streamingResult.execution.output.providerTiming.duration = @@ -1223,7 +1294,7 @@ export async function executeGeminiRequest( } ) - return { ...streamingResult, stream } + return { ...streamingResult, stream, streamFormat: 'agent-events-v1' as const } } // Non-streaming: get next response @@ -1318,7 +1389,7 @@ function enrichLastModelSegmentFromGeminiResponse( Boolean(p.functionCall) ) .map((p) => ({ - id: p.functionCall.id ?? '', + id: ensureToolCallId(p.functionCall.id, 'gemini'), name: p.functionCall.name ?? '', arguments: (p.functionCall.args ?? {}) as Record, })) diff --git a/apps/sim/providers/gemini/streaming-tool-loop.test.ts b/apps/sim/providers/gemini/streaming-tool-loop.test.ts new file mode 100644 index 00000000000..464d8bdaeac --- /dev/null +++ b/apps/sim/providers/gemini/streaming-tool-loop.test.ts @@ -0,0 +1,166 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { createGeminiStreamingToolLoopStream } from '@/providers/gemini/streaming-tool-loop' +import type { AgentStreamEvent } from '@/providers/stream-events' +import { resetLocalToolIdCounterForTests } from '@/providers/tool-call-id' + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +vi.mock('@/tools', () => ({ + executeTool: vi.fn(async () => ({ + success: true, + output: { ok: true, url: 'https://httpbin.org/get' }, + })), +})) + +vi.mock('@/providers/utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + prepareToolExecution: vi.fn(() => ({ + toolParams: { url: 'https://httpbin.org/get' }, + executionParams: { url: 'https://httpbin.org/get' }, + })), + calculateCost: vi.fn(() => ({ + input: 0.01, + output: 0.02, + total: 0.03, + pricing: { input: 1, output: 2, updatedAt: new Date().toISOString() }, + })), + sumToolCosts: vi.fn(() => 0), + isGemini3Model: vi.fn(() => false), + } +}) + +describe('createGeminiStreamingToolLoopStream (Step 9)', () => { + it('emits thinking, tool lifecycle, then final answer; allocates local tool ids', async () => { + resetLocalToolIdCounterForTests() + + const turns = [ + // Turn 1: thinking + functionCall (no id) + (async function* () { + yield { + candidates: [ + { + content: { + parts: [ + { text: 'I should call the API. ', thought: true }, + { + functionCall: { + name: 'http_request', + args: { url: 'https://httpbin.org/get' }, + }, + }, + ], + }, + finishReason: 'STOP', + }, + ], + usageMetadata: { + promptTokenCount: 10, + candidatesTokenCount: 5, + totalTokenCount: 15, + }, + } as any + })(), + // Turn 2: final answer + (async function* () { + yield { + candidates: [ + { + content: { + parts: [{ text: 'Done: https://httpbin.org/get' }], + }, + finishReason: 'STOP', + }, + ], + usageMetadata: { + promptTokenCount: 20, + candidatesTokenCount: 8, + totalTokenCount: 28, + }, + } as any + })(), + ] + + let turnIdx = 0 + const ai = { + models: { + generateContentStream: vi.fn(async () => turns[turnIdx++]), + }, + } + + const onComplete = vi.fn() + const timeSegments: any[] = [] + const stream = createGeminiStreamingToolLoopStream({ + ai: ai as any, + model: 'gemini-2.5-flash', + baseConfig: {}, + contents: [{ role: 'user', parts: [{ text: 'fetch it' }] }], + request: { + model: 'gemini-2.5-flash', + tools: [ + { + id: 'http_request', + name: 'http_request', + description: 'HTTP', + parameters: { type: 'object', properties: {}, required: [] }, + }, + ], + } as any, + logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } as any, + timeSegments, + onComplete, + }) + + const events = await collectEvents(stream) + + expect(events.filter((e) => e.type === 'thinking_delta').map((e) => e.text)).toEqual([ + 'I should call the API. ', + ]) + + const starts = events.filter((e) => e.type === 'tool_call_start') + expect(starts).toHaveLength(1) + expect(starts[0]).toMatchObject({ name: 'http_request' }) + expect((starts[0] as { id: string }).id).toMatch(/^gemini_/) + + const ends = events.filter((e) => e.type === 'tool_call_end') + expect(ends).toEqual([ + { + type: 'tool_call_end', + id: (starts[0] as { id: string }).id, + name: 'http_request', + status: 'success', + }, + ]) + + const textEvents = events.filter((e) => e.type === 'text_delta') + expect(textEvents.some((e) => e.type === 'text_delta' && e.turn === 'final')).toBe(true) + expect( + textEvents + .filter((e) => e.type === 'text_delta' && e.turn === 'final') + .map((e) => e.text) + .join('') + ).toContain('Done:') + + expect(onComplete).toHaveBeenCalledWith( + expect.objectContaining({ + content: expect.stringContaining('Done:'), + toolCalls: expect.objectContaining({ count: 1 }), + }) + ) + }) +}) diff --git a/apps/sim/providers/gemini/streaming-tool-loop.ts b/apps/sim/providers/gemini/streaming-tool-loop.ts new file mode 100644 index 00000000000..b9de301ae05 --- /dev/null +++ b/apps/sim/providers/gemini/streaming-tool-loop.ts @@ -0,0 +1,542 @@ +/** + * Live Gemini streaming tool loop (Step 9). + * + * Each model turn uses generateContentStream. Thought parts → thinking_delta + * live; functionCall parts → tool_call_start (with local ids when missing); + * text buffered until the turn is classified intermediate vs final. + * Tool ends emit in completion order; abort → cancelled. + */ + +import { + type Content, + FunctionCallingConfigMode, + type GenerateContentConfig, + type GenerateContentResponse, + type GoogleGenAI, + type Part, + type Schema, + type ToolConfig, +} from '@google/genai' +import type { Logger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import type { IterationToolCall, NormalizedBlockOutput } from '@/executor/types' +import { MAX_TOOL_ITERATIONS } from '@/providers' +import { + checkForForcedToolUsage, + cleanSchemaForGemini, + convertUsageMetadata, + ensureStructResponse, +} from '@/providers/google/utils' +import type { AgentStreamEvent, ToolCallEndStatus } from '@/providers/stream-events' +import { ensureToolCallId } from '@/providers/tool-call-id' +import { enrichLastModelSegment } from '@/providers/trace-enrichment' +import type { ProviderRequest, TimeSegment } from '@/providers/types' +import { + calculateCost, + isGemini3Model, + prepareToolExecution, + sumToolCosts, +} from '@/providers/utils' +import { executeTool } from '@/tools' +import type { GeminiUsage } from './types' + +export interface GeminiStreamingToolLoopComplete { + content: string + tokens: { input: number; output: number; total: number } + cost: NormalizedBlockOutput['cost'] + toolCalls?: { list: unknown[]; count: number } + modelTime: number + toolsTime: number + firstResponseTime: number + iterations: number +} + +export interface CreateGeminiStreamingToolLoopStreamOptions { + ai: GoogleGenAI + model: string + baseConfig: GenerateContentConfig + contents: Content[] + request: ProviderRequest + logger: Logger + timeSegments: TimeSegment[] + forcedTools?: string[] + toolConfig?: ToolConfig + onComplete: (result: GeminiStreamingToolLoopComplete) => void +} + +function isAbortError(error: unknown): boolean { + if (!error || typeof error !== 'object') return false + const name = (error as { name?: string }).name + return name === 'AbortError' || name === 'APIUserAbortError' +} + +function settleOpenTools( + controller: ReadableStreamDefaultController, + openTools: Map, + status: ToolCallEndStatus +): void { + for (const [id, name] of openTools) { + controller.enqueue({ type: 'tool_call_end', id, name, status }) + } + openTools.clear() +} + +function buildNextConfig( + baseConfig: GenerateContentConfig, + currentToolConfig: ToolConfig | undefined, + usedForcedTools: string[], + forcedTools: string[], + request: ProviderRequest, + logger: Logger, + model: string +): GenerateContentConfig { + const nextConfig = { ...baseConfig } + const allForcedToolsUsed = + forcedTools.length > 0 && usedForcedTools.length === forcedTools.length + + if (allForcedToolsUsed && request.responseFormat) { + nextConfig.tools = undefined + nextConfig.toolConfig = undefined + if (isGemini3Model(model)) { + logger.info('Gemini 3: Stripping tools after forced tool execution, schema already set') + } else { + nextConfig.responseMimeType = 'application/json' + nextConfig.responseSchema = cleanSchemaForGemini(request.responseFormat.schema) as Schema + logger.info('Using structured output for final response after tool execution') + } + } else if (currentToolConfig) { + nextConfig.toolConfig = currentToolConfig + } else { + nextConfig.toolConfig = { functionCallingConfig: { mode: FunctionCallingConfigMode.AUTO } } + } + + return nextConfig +} + +/** + * Drain one generateContentStream turn into live agent events + aggregated parts. + */ +async function drainGeminiTurn( + stream: AsyncGenerator, + controller: ReadableStreamDefaultController, + openTools: Map +): Promise<{ + text: string + thinking: string + functionCallParts: Part[] + usage: GeminiUsage + textChunks: string[] + finishReason?: string +}> { + let text = '' + let thinking = '' + const textChunks: string[] = [] + const functionCallParts: Part[] = [] + const seenKeys = new Set() + let usage: GeminiUsage = { promptTokenCount: 0, candidatesTokenCount: 0, totalTokenCount: 0 } + let finishReason: string | undefined + + for await (const chunk of stream) { + if (chunk.usageMetadata) { + usage = convertUsageMetadata(chunk.usageMetadata) + } + + const candidate = chunk.candidates?.[0] + if (candidate?.finishReason) { + finishReason = String(candidate.finishReason) + } + + const parts = candidate?.content?.parts + if (!Array.isArray(parts)) { + const fallback = chunk.text + if (fallback) { + text += fallback + textChunks.push(fallback) + } + continue + } + + for (const part of parts) { + if (part.functionCall) { + const id = ensureToolCallId(part.functionCall.id, 'gemini') + const name = part.functionCall.name ?? '' + const key = id + if (!seenKeys.has(key) && name) { + seenKeys.add(key) + const normalized: Part = { + ...part, + functionCall: { ...part.functionCall, id }, + } + functionCallParts.push(normalized) + if (!openTools.has(id)) { + openTools.set(id, name) + controller.enqueue({ type: 'tool_call_start', id, name }) + } + } + continue + } + + if (!part.text) continue + if (part.thought === true) { + thinking += part.text + controller.enqueue({ type: 'thinking_delta', text: part.text }) + } else { + text += part.text + textChunks.push(part.text) + } + } + } + + return { text, thinking, functionCallParts, usage, textChunks, finishReason } +} + +/** + * Multi-turn Gemini tool loop as an agent-events-v1 object stream. + */ +export function createGeminiStreamingToolLoopStream( + options: CreateGeminiStreamingToolLoopStreamOptions +): ReadableStream { + const { + ai, + model, + baseConfig, + contents: initialContents, + request, + logger, + timeSegments, + onComplete, + } = options + const forcedTools = options.forcedTools ?? [] + + return new ReadableStream({ + async start(controller) { + let contents = [...initialContents] + let currentToolConfig = options.toolConfig + let usedForcedTools: string[] = [] + + let content = '' + let iterationCount = 0 + let modelTime = 0 + let toolsTime = 0 + let firstResponseTime = 0 + const tokens = { input: 0, output: 0, total: 0 } + let costInput = 0 + let costOutput = 0 + let costTotal = 0 + let latestPricing: ReturnType['pricing'] | undefined + const toolCalls: unknown[] = [] + const toolResults: Record[] = [] + const openToolStarts = new Map() + + try { + while (iterationCount < MAX_TOOL_ITERATIONS) { + if (request.abortSignal?.aborted) { + settleOpenTools(controller, openToolStarts, 'cancelled') + throw new DOMException('Stream aborted', 'AbortError') + } + + const turnConfig = buildNextConfig( + baseConfig, + currentToolConfig, + usedForcedTools, + forcedTools, + request, + logger, + model + ) + + const modelStart = Date.now() + const streamGenerator = await ai.models.generateContentStream({ + model, + contents, + config: turnConfig, + }) + + const drained = await drainGeminiTurn(streamGenerator, controller, openToolStarts) + const modelEnd = Date.now() + const thisModelTime = modelEnd - modelStart + modelTime += thisModelTime + if (iterationCount === 0) { + firstResponseTime = thisModelTime + } + + timeSegments.push({ + type: 'model', + name: model, + startTime: modelStart, + endTime: modelEnd, + duration: thisModelTime, + }) + + tokens.input += drained.usage.promptTokenCount + tokens.output += drained.usage.candidatesTokenCount + tokens.total += drained.usage.totalTokenCount + + const turnCost = calculateCost( + model, + drained.usage.promptTokenCount, + drained.usage.candidatesTokenCount + ) + costInput += turnCost.input + costOutput += turnCost.output + costTotal += turnCost.total + latestPricing = turnCost.pricing + + const turnTag = drained.functionCallParts.length > 0 ? 'intermediate' : 'final' + for (const chunk of drained.textChunks) { + controller.enqueue({ type: 'text_delta', text: chunk, turn: turnTag }) + } + if (drained.text) { + content = drained.text + } + + const toolCallsForEnrich: IterationToolCall[] = drained.functionCallParts + .filter((p): p is Part & { functionCall: NonNullable } => + Boolean(p.functionCall) + ) + .map((p) => ({ + id: p.functionCall.id ?? '', + name: p.functionCall.name ?? '', + arguments: (p.functionCall.args ?? {}) as Record, + })) + + enrichLastModelSegment(timeSegments, { + assistantContent: drained.text || undefined, + thinkingContent: drained.thinking || undefined, + toolCalls: toolCallsForEnrich.length > 0 ? toolCallsForEnrich : undefined, + finishReason: drained.finishReason, + tokens: { + input: drained.usage.promptTokenCount, + output: drained.usage.candidatesTokenCount, + total: drained.usage.totalTokenCount, + }, + cost: { + input: turnCost.input, + output: turnCost.output, + total: turnCost.total, + }, + provider: 'google', + }) + + const forcedCheck = checkForForcedToolUsage( + drained.functionCallParts + .map((p) => p.functionCall) + .filter((fc): fc is NonNullable => Boolean(fc)), + currentToolConfig, + forcedTools, + usedForcedTools + ) + if (forcedCheck) { + usedForcedTools = forcedCheck.usedForcedTools + currentToolConfig = forcedCheck.nextToolConfig + } + + if (drained.functionCallParts.length === 0) { + break + } + + const toolsStartTime = Date.now() + + const orderedResults = await Promise.all( + drained.functionCallParts.map(async (part) => { + const functionCall = part.functionCall! + const toolCallId = ensureToolCallId(functionCall.id, 'gemini') + const toolName = functionCall.name ?? '' + const toolArgs = (functionCall.args ?? {}) as Record + const toolCallStartTime = Date.now() + + try { + if (request.abortSignal?.aborted) { + throw new DOMException('Stream aborted', 'AbortError') + } + + const tool = request.tools?.find((t) => t.id === toolName) + if (!tool) { + const value = { + part, + toolCallId, + toolName, + toolArgs, + toolParams: {} as Record, + resultContent: { + error: true, + message: `Tool ${toolName} not found`, + tool: toolName, + }, + result: undefined as + | { success: boolean; output?: unknown; error?: string } + | undefined, + startTime: toolCallStartTime, + endTime: Date.now(), + duration: Date.now() - toolCallStartTime, + status: 'error' as ToolCallEndStatus, + success: false, + } + openToolStarts.delete(toolCallId) + controller.enqueue({ + type: 'tool_call_end', + id: toolCallId, + name: toolName, + status: 'error', + }) + return value + } + + const { toolParams, executionParams } = prepareToolExecution( + tool, + toolArgs, + request + ) + const result = await executeTool(toolName, executionParams, { + signal: request.abortSignal, + }) + const toolCallEndTime = Date.now() + const resultContent: Record = result.success + ? ensureStructResponse(result.output) + : { + error: true, + message: result.error || 'Tool execution failed', + tool: toolName, + } + const status: ToolCallEndStatus = result.success ? 'success' : 'error' + openToolStarts.delete(toolCallId) + controller.enqueue({ + type: 'tool_call_end', + id: toolCallId, + name: toolName, + status, + }) + return { + part, + toolCallId, + toolName, + toolArgs, + toolParams, + resultContent, + result, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + status, + success: result.success, + } + } catch (error) { + const toolCallEndTime = Date.now() + const cancelled = isAbortError(error) || !!request.abortSignal?.aborted + if (!cancelled) { + logger.error('Error processing function call:', { + error: toError(error).message, + functionName: toolName, + }) + } + const status: ToolCallEndStatus = cancelled ? 'cancelled' : 'error' + openToolStarts.delete(toolCallId) + controller.enqueue({ + type: 'tool_call_end', + id: toolCallId, + name: toolName, + status, + }) + return { + part, + toolCallId, + toolName, + toolArgs, + toolParams: {} as Record, + resultContent: { + error: true, + message: getErrorMessage(error, 'Tool execution failed'), + tool: toolName, + }, + result: undefined, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + status, + success: false, + } + } + }) + ) + + toolsTime += Date.now() - toolsStartTime + + const modelParts: Part[] = orderedResults.map((r) => ({ + ...r.part, + functionCall: { + ...r.part.functionCall!, + id: r.toolCallId, + }, + })) + const userParts: Part[] = orderedResults.map((r) => ({ + functionResponse: { + name: r.toolName, + response: r.resultContent, + }, + })) + + contents = [ + ...contents, + { role: 'model', parts: modelParts }, + { role: 'user', parts: userParts }, + ] + + for (const r of orderedResults) { + toolCalls.push({ + name: r.toolName, + arguments: r.toolParams, + startTime: new Date(r.startTime).toISOString(), + endTime: new Date(r.endTime).toISOString(), + duration: r.duration, + result: r.resultContent, + success: r.success, + }) + if (r.success && r.result?.output) { + toolResults.push(r.result.output as Record) + } + timeSegments.push({ + type: 'tool', + name: r.toolName, + startTime: r.startTime, + endTime: r.endTime, + duration: r.duration, + toolCallId: r.toolCallId, + }) + } + + iterationCount += 1 + } + + const toolCost = sumToolCosts(toolResults) + onComplete({ + content, + tokens, + cost: { + input: costInput, + output: costOutput, + toolCost: toolCost || undefined, + total: costTotal + toolCost, + pricing: latestPricing, + }, + toolCalls: + toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + modelTime, + toolsTime, + firstResponseTime, + iterations: iterationCount + 1, + }) + controller.close() + } catch (error) { + if (isAbortError(error) || request.abortSignal?.aborted) { + settleOpenTools(controller, openToolStarts, 'cancelled') + } else { + settleOpenTools(controller, openToolStarts, 'error') + logger.error('Gemini streaming tool loop failed', { + error: toError(error).message, + }) + } + controller.error(error) + } + }, + }) +} diff --git a/apps/sim/providers/google/utils.stream.test.ts b/apps/sim/providers/google/utils.stream.test.ts new file mode 100644 index 00000000000..b9085efc92e --- /dev/null +++ b/apps/sim/providers/google/utils.stream.test.ts @@ -0,0 +1,74 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { createReadableStreamFromGeminiStream } from '@/providers/google/utils' +import type { AgentStreamEvent } from '@/providers/stream-events' + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +describe('createReadableStreamFromGeminiStream (Step 9)', () => { + it('splits thought parts into thinking_delta and answer into text_delta', async () => { + const onComplete = vi.fn() + const stream = createReadableStreamFromGeminiStream( + (async function* () { + yield { + candidates: [ + { + content: { + parts: [ + { text: 'Reasoning step. ', thought: true }, + { text: 'Final answer.', thought: false }, + ], + }, + }, + ], + usageMetadata: { + promptTokenCount: 5, + candidatesTokenCount: 7, + totalTokenCount: 12, + }, + } as any + })(), + onComplete + ) + + const events = await collectEvents(stream) + expect(events).toEqual([ + { type: 'thinking_delta', text: 'Reasoning step. ' }, + { type: 'text_delta', text: 'Final answer.', turn: 'final' }, + ]) + expect(onComplete).toHaveBeenCalledWith( + 'Final answer.', + expect.objectContaining({ promptTokenCount: 5 }), + 'Reasoning step. ' + ) + }) + + it('does not invent thinking when only answer text is present', async () => { + const stream = createReadableStreamFromGeminiStream( + (async function* () { + yield { + text: 'Just text', + candidates: [{ content: { parts: [{ text: 'Just text' }] } }], + } as any + })() + ) + const events = await collectEvents(stream) + expect(events.some((e) => e.type === 'thinking_delta')).toBe(false) + expect(events.filter((e) => e.type === 'text_delta').map((e) => e.text).join('')).toContain( + 'Just text' + ) + }) +}) diff --git a/apps/sim/providers/google/utils.ts b/apps/sim/providers/google/utils.ts index 05aa74328f7..3a389261011 100644 --- a/apps/sim/providers/google/utils.ts +++ b/apps/sim/providers/google/utils.ts @@ -16,6 +16,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { isRecordLike } from '@sim/utils/object' import { buildGeminiMessageParts } from '@/providers/attachments' +import type { AgentStreamEvent } from '@/providers/stream-events' import type { ProviderRequest } from '@/providers/types' import { trackForcedToolUsage } from '@/providers/utils' @@ -280,13 +281,16 @@ export function convertToGeminiFormat( } /** - * Creates a ReadableStream from a Google Gemini streaming response + * Creates an agent-events-v1 stream from a Google Gemini streaming response. + * Thought parts (`part.thought === true`) → thinking_delta; other text → text_delta. + * Capability-honest: thinking only appears when includeThoughts was requested. */ export function createReadableStreamFromGeminiStream( stream: AsyncGenerator, - onComplete?: (content: string, usage: GeminiUsage) => void -): ReadableStream { + onComplete?: (content: string, usage: GeminiUsage, thinking?: string) => void +): ReadableStream { let fullContent = '' + let fullThinking = '' let usage: GeminiUsage = { promptTokenCount: 0, candidatesTokenCount: 0, totalTokenCount: 0 } return new ReadableStream({ @@ -297,14 +301,30 @@ export function createReadableStreamFromGeminiStream( usage = convertUsageMetadata(chunk.usageMetadata) } + const parts = chunk.candidates?.[0]?.content?.parts + if (Array.isArray(parts)) { + for (const part of parts) { + if (!part.text) continue + if (part.thought === true) { + fullThinking += part.text + controller.enqueue({ type: 'thinking_delta', text: part.text }) + } else { + fullContent += part.text + controller.enqueue({ type: 'text_delta', text: part.text, turn: 'final' }) + } + } + continue + } + + // Fallback when parts are not exposed — answer text only (no false thinking). const text = chunk.text if (text) { fullContent += text - controller.enqueue(new TextEncoder().encode(text)) + controller.enqueue({ type: 'text_delta', text, turn: 'final' }) } } - onComplete?.(fullContent, usage) + onComplete?.(fullContent, usage, fullThinking || undefined) controller.close() } catch (error) { logger.error('Error reading Google Gemini stream', { diff --git a/apps/sim/providers/groq/index.test.ts b/apps/sim/providers/groq/index.test.ts new file mode 100644 index 00000000000..2ae5572b3ec --- /dev/null +++ b/apps/sim/providers/groq/index.test.ts @@ -0,0 +1,121 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { ProviderRequest } from '@/providers/types' + +const { mockCreate } = vi.hoisted(() => ({ + mockCreate: vi.fn(), +})) + +vi.mock('groq-sdk', () => ({ + Groq: vi.fn().mockImplementation( + class { + chat = { completions: { create: mockCreate } } + } + ), +})) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 5 })) + +vi.mock('@/providers/models', () => ({ + getProviderModels: vi.fn(() => ['groq/openai/gpt-oss-120b']), + getProviderDefaultModel: vi.fn(() => 'groq/openai/gpt-oss-120b'), +})) + +vi.mock('@/providers/attachments', () => ({ + formatMessagesForProvider: vi.fn((messages) => messages), +})) + +vi.mock('@/providers/groq/utils', () => ({ + createReadableStreamFromGroqStream: vi.fn(), +})) + +vi.mock('@/providers/openai-compat/streaming-tool-loop', () => ({ + createOpenAICompatStreamingToolLoopStream: vi.fn(), +})) + +vi.mock('@/providers/streaming-execution', () => ({ + createStreamingExecution: vi.fn((args) => args), +})) + +vi.mock('@/providers/trace-enrichment', () => ({ + enrichLastModelSegmentFromChatCompletions: vi.fn(), +})) + +vi.mock('@/providers/utils', () => ({ + calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })), + prepareToolExecution: vi.fn((_tool, args) => ({ toolParams: args, executionParams: args })), + prepareToolsWithUsageControl: vi.fn(() => ({ + tools: [], + toolChoice: undefined, + forcedTools: [], + hasFilteredTools: false, + })), + sumToolCosts: vi.fn(() => 0), + trackForcedToolUsage: vi.fn(() => ({ hasUsedForcedTool: false, usedForcedTools: [] })), +})) + +vi.mock('@/tools', () => ({ executeTool: vi.fn() })) + +import { groqProvider } from '@/providers/groq/index' + +function request(overrides: Partial = {}): ProviderRequest { + return { + model: 'groq/openai/gpt-oss-120b', + apiKey: 'test-key', + messages: [{ role: 'user', content: 'hi' }], + ...overrides, + } +} + +describe('groqProvider reasoning payload', () => { + beforeEach(() => { + mockCreate.mockReset() + mockCreate.mockResolvedValue({ + choices: [{ message: { content: 'ok', tool_calls: [] } }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }) + }) + + it('GPT-OSS sets include_reasoning and reasoning_effort', async () => { + await groqProvider.executeRequest( + request({ model: 'groq/openai/gpt-oss-120b', reasoningEffort: 'high' }) + ) + const payload = mockCreate.mock.calls[0][0] + expect(payload.model).toBe('openai/gpt-oss-120b') + expect(payload.include_reasoning).toBe(true) + expect(payload.reasoning_effort).toBe('high') + expect(payload.reasoning_format).toBeUndefined() + }) + + it('GPT-OSS defaults reasoning_effort to medium when unset', async () => { + await groqProvider.executeRequest(request({ model: 'groq/openai/gpt-oss-20b' })) + const payload = mockCreate.mock.calls[0][0] + expect(payload.include_reasoning).toBe(true) + expect(payload.reasoning_effort).toBe('medium') + }) + + it('Qwen sets reasoning_format parsed when thinking enabled', async () => { + await groqProvider.executeRequest( + request({ + model: 'groq/qwen/qwen3-32b', + thinkingLevel: 'enabled', + }) + ) + const payload = mockCreate.mock.calls[0][0] + expect(payload.reasoning_format).toBe('parsed') + expect(payload.include_reasoning).toBeUndefined() + }) + + it('Qwen omits reasoning_format when thinking is none', async () => { + await groqProvider.executeRequest( + request({ + model: 'groq/qwen/qwen3.6-27b', + thinkingLevel: 'none', + }) + ) + const payload = mockCreate.mock.calls[0][0] + expect(payload.reasoning_format).toBeUndefined() + }) +}) diff --git a/apps/sim/providers/groq/index.ts b/apps/sim/providers/groq/index.ts index 15d854e145c..fdac6a74a3b 100644 --- a/apps/sim/providers/groq/index.ts +++ b/apps/sim/providers/groq/index.ts @@ -1,11 +1,12 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import { Groq } from 'groq-sdk' -import type { StreamingExecution } from '@/executor/types' +import type { NormalizedBlockOutput, StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { formatMessagesForProvider } from '@/providers/attachments' import { createReadableStreamFromGroqStream } from '@/providers/groq/utils' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' +import { createOpenAICompatStreamingToolLoopStream } from '@/providers/openai-compat/streaming-tool-loop' import { createStreamingExecution } from '@/providers/streaming-execution' import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter' import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment' @@ -77,6 +78,22 @@ export const groqProvider: ProviderConfig = { if (request.temperature !== undefined) payload.temperature = request.temperature if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens + // Groq reasoning: GPT-OSS uses include_reasoning + reasoning_effort; + // Qwen uses reasoning_format: parsed (compatible with tools). + const groqModelId = payload.model as string + const isGptOss = groqModelId.includes('gpt-oss') + const isQwenReasoning = /qwen3/i.test(groqModelId) + if (isGptOss) { + const effort = + request.reasoningEffort && request.reasoningEffort !== 'auto' + ? request.reasoningEffort + : 'medium' + payload.include_reasoning = true + payload.reasoning_effort = effort + } else if (isQwenReasoning && request.thinkingLevel && request.thinkingLevel !== 'none') { + payload.reasoning_format = 'parsed' + } + if (request.responseFormat) { payload.response_format = { type: 'json_schema', @@ -112,6 +129,70 @@ export const groqProvider: ProviderConfig = { } } + const shouldStreamToolCalls = request.streamToolCalls ?? false + + if (request.stream && shouldStreamToolCalls && payload.tools?.length) { + logger.info('Using streaming tool loop for Groq request') + + const providerStartTime = Date.now() + const providerStartTimeISO = new Date(providerStartTime).toISOString() + const timeSegments: TimeSegment[] = [] + + return createStreamingExecution({ + model: request.model, + providerStartTime, + providerStartTimeISO, + timing: { + kind: 'accumulated', + modelTime: 0, + toolsTime: 0, + firstResponseTime: 0, + iterations: 1, + timeSegments, + }, + initialTokens: { input: 0, output: 0, total: 0 }, + initialCost: { total: 0.0, input: 0.0, output: 0.0 }, + isStreaming: true, + streamFormat: 'agent-events-v1', + createStream: ({ output, finalizeTiming }) => + createOpenAICompatStreamingToolLoopStream({ + providerName: 'Groq', + request, + basePayload: payload, + messages: formattedMessages as any, + createStream: async (params, options) => + groq.chat.completions.create( + { ...params, stream: true }, + options + ) as any, + createBlocking: async (params, options) => + groq.chat.completions.create( + { ...params, stream: false }, + options + ) as any, + logger, + timeSegments, + forcedTools, + preserveAssistantReasoning: + (!!request.thinkingLevel && request.thinkingLevel !== 'none') || + (payload.model as string).includes('gpt-oss'), + onComplete: (result) => { + output.content = result.content + output.tokens = result.tokens + output.cost = result.cost + output.toolCalls = result.toolCalls as NormalizedBlockOutput['toolCalls'] + if (output.providerTiming) { + output.providerTiming.modelTime = result.modelTime + output.providerTiming.toolsTime = result.toolsTime + output.providerTiming.firstResponseTime = result.firstResponseTime + output.providerTiming.iterations = result.iterations + } + finalizeTiming() + }, + }), + }) + } + if (request.stream && (!tools || tools.length === 0)) { logger.info('Using streaming response for Groq request (no tools)') @@ -134,26 +215,37 @@ export const groqProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => - createReadableStreamFromGroqStream(streamResponse as any, (content, usage) => { - output.content = content - output.tokens = { - input: usage.prompt_tokens, - output: usage.completion_tokens, - total: usage.total_tokens, - } + createReadableStreamFromGroqStream( + streamResponse as any, + (content, usage, thinking) => { + output.content = content + output.tokens = { + input: usage.prompt_tokens, + output: usage.completion_tokens, + total: usage.total_tokens, + } - const costResult = calculateCost( - request.model, - usage.prompt_tokens, - usage.completion_tokens - ) - output.cost = { - input: costResult.input, - output: costResult.output, - total: costResult.total, + const costResult = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + output.cost = { + input: costResult.input, + output: costResult.output, + total: costResult.total, + } + + if (thinking) { + const segment = output.providerTiming?.timeSegments?.[0] + if (segment) { + segment.thinkingContent = thinking + } + } } - }), + ), }) return streamingResult @@ -440,28 +532,39 @@ export const groqProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => - createReadableStreamFromGroqStream(streamResponse as any, (content, usage) => { - output.content = content - output.tokens = { - input: tokens.input + usage.prompt_tokens, - output: tokens.output + usage.completion_tokens, - total: tokens.total + usage.total_tokens, - } + createReadableStreamFromGroqStream( + streamResponse as any, + (content, usage, thinking) => { + output.content = content + output.tokens = { + input: tokens.input + usage.prompt_tokens, + output: tokens.output + usage.completion_tokens, + total: tokens.total + usage.total_tokens, + } - const streamCost = calculateCost( - request.model, - usage.prompt_tokens, - usage.completion_tokens - ) - const tc = sumToolCosts(toolResults) - output.cost = { - input: accumulatedCost.input + streamCost.input, - output: accumulatedCost.output + streamCost.output, - toolCost: tc || undefined, - total: accumulatedCost.total + streamCost.total + tc, + const streamCost = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + const tc = sumToolCosts(toolResults) + output.cost = { + input: accumulatedCost.input + streamCost.input, + output: accumulatedCost.output + streamCost.output, + toolCost: tc || undefined, + total: accumulatedCost.total + streamCost.total + tc, + } + + if (thinking) { + const lastModel = [...timeSegments].reverse().find((s) => s.type === 'model') + if (lastModel) { + lastModel.thinkingContent = thinking + } + } } - }), + ), }) return streamingResult diff --git a/apps/sim/providers/groq/utils.ts b/apps/sim/providers/groq/utils.ts index 61e8563e962..cb97a689ea5 100644 --- a/apps/sim/providers/groq/utils.ts +++ b/apps/sim/providers/groq/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from a Groq streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Groq streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromGroqStream( groqStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(groqStream, 'Groq', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(groqStream, { + providerName: 'Groq', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/litellm/index.ts b/apps/sim/providers/litellm/index.ts index 0f5fc2d3d2c..e8ee2229e6e 100644 --- a/apps/sim/providers/litellm/index.ts +++ b/apps/sim/providers/litellm/index.ts @@ -219,6 +219,7 @@ export const litellmProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromLiteLLMStream(streamResponse, (content, usage) => { let cleanContent = content @@ -588,6 +589,7 @@ export const litellmProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromLiteLLMStream(streamResponse, (content, usage) => { let cleanContent = content diff --git a/apps/sim/providers/litellm/utils.ts b/apps/sim/providers/litellm/utils.ts index f779f95c703..bacbd3cacbb 100644 --- a/apps/sim/providers/litellm/utils.ts +++ b/apps/sim/providers/litellm/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from a LiteLLM streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a LiteLLM streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromLiteLLMStream( litellmStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(litellmStream, 'LiteLLM', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(litellmStream, { + providerName: 'LiteLLM', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/meta/index.ts b/apps/sim/providers/meta/index.ts index 96b0e0034ad..d5b7ab19bde 100644 --- a/apps/sim/providers/meta/index.ts +++ b/apps/sim/providers/meta/index.ts @@ -172,6 +172,7 @@ export const metaProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromMetaStream(streamResponse as any, (content, usage) => { output.content = content @@ -538,6 +539,7 @@ export const metaProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromMetaStream(streamResponse as any, (content, usage) => { output.content = content diff --git a/apps/sim/providers/meta/utils.ts b/apps/sim/providers/meta/utils.ts index 1f72345ec5c..a8defd82d75 100644 --- a/apps/sim/providers/meta/utils.ts +++ b/apps/sim/providers/meta/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from a Meta Model API streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Meta Model API streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromMetaStream( metaStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(metaStream, 'Meta', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(metaStream, { + providerName: 'Meta', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/mistral/index.ts b/apps/sim/providers/mistral/index.ts index 95c66422a72..4d95b1e96ad 100644 --- a/apps/sim/providers/mistral/index.ts +++ b/apps/sim/providers/mistral/index.ts @@ -155,6 +155,7 @@ export const mistralProvider: ProviderConfig = { timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromMistralStream(streamResponse, (content, usage) => { output.content = content @@ -480,6 +481,7 @@ export const mistralProvider: ProviderConfig = { count: toolCalls.length, } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromMistralStream(streamResponse, (content, usage) => { output.content = content diff --git a/apps/sim/providers/mistral/utils.ts b/apps/sim/providers/mistral/utils.ts index 61893928918..b1d16c95304 100644 --- a/apps/sim/providers/mistral/utils.ts +++ b/apps/sim/providers/mistral/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from a Mistral streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Mistral streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromMistralStream( mistralStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(mistralStream, 'Mistral', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(mistralStream, { + providerName: 'Mistral', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/models.ts b/apps/sim/providers/models.ts index bc5aa24cbe7..4c907fc6a38 100644 --- a/apps/sim/providers/models.ts +++ b/apps/sim/providers/models.ts @@ -1853,7 +1853,12 @@ export const PROVIDER_DEFINITIONS: Record = { output: 0.87, updatedAt: '2026-06-16', }, - capabilities: {}, + capabilities: { + thinking: { + levels: ['enabled'], + default: 'enabled', + }, + }, contextWindow: 1000000, releaseDate: '2026-04-24', }, @@ -1867,6 +1872,10 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { temperature: { min: 0, max: 2 }, + thinking: { + levels: ['enabled'], + default: 'enabled', + }, }, contextWindow: 1000000, releaseDate: '2026-04-24', @@ -1881,6 +1890,10 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { temperature: { min: 0, max: 2 }, + thinking: { + levels: ['enabled'], + default: 'enabled', + }, }, contextWindow: 1000000, releaseDate: '2024-12-26', @@ -1908,7 +1921,12 @@ export const PROVIDER_DEFINITIONS: Record = { output: 2.19, updatedAt: '2026-04-01', }, - capabilities: {}, + capabilities: { + thinking: { + levels: ['enabled'], + default: 'enabled', + }, + }, contextWindow: 128000, releaseDate: '2025-01-20', deprecated: true, @@ -1921,7 +1939,12 @@ export const PROVIDER_DEFINITIONS: Record = { output: 0.28, updatedAt: '2026-06-11', }, - capabilities: {}, + capabilities: { + thinking: { + levels: ['enabled'], + default: 'enabled', + }, + }, contextWindow: 1000000, releaseDate: '2025-01-20', }, @@ -2251,6 +2274,9 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { maxOutputTokens: 65536, + reasoningEffort: { + values: ['low', 'medium', 'high'], + }, }, contextWindow: 131072, releaseDate: '2025-08-05', @@ -2266,6 +2292,9 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { maxOutputTokens: 65536, + reasoningEffort: { + values: ['low', 'medium', 'high'], + }, }, contextWindow: 131072, releaseDate: '2025-08-05', @@ -2280,6 +2309,9 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { maxOutputTokens: 65536, + reasoningEffort: { + values: ['low', 'medium', 'high'], + }, }, contextWindow: 131072, releaseDate: '2025-10-29', @@ -2293,6 +2325,10 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { maxOutputTokens: 40960, + thinking: { + levels: ['enabled'], + default: 'enabled', + }, }, contextWindow: 131072, releaseDate: '2025-04-29', @@ -2307,6 +2343,10 @@ export const PROVIDER_DEFINITIONS: Record = { }, capabilities: { maxOutputTokens: 32768, + thinking: { + levels: ['enabled'], + default: 'enabled', + }, }, contextWindow: 131072, releaseDate: '2026-04-21', diff --git a/apps/sim/providers/nvidia/index.ts b/apps/sim/providers/nvidia/index.ts index ffb7bf0fb25..87754ce3be9 100644 --- a/apps/sim/providers/nvidia/index.ts +++ b/apps/sim/providers/nvidia/index.ts @@ -152,6 +152,7 @@ export const nvidiaProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromNvidiaStream(streamResponse as any, (content, usage) => { output.content = content @@ -512,6 +513,7 @@ export const nvidiaProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromNvidiaStream(streamResponse as any, (content, usage) => { output.content = content diff --git a/apps/sim/providers/nvidia/utils.ts b/apps/sim/providers/nvidia/utils.ts index ef9c37b3a50..45c0b526a4b 100644 --- a/apps/sim/providers/nvidia/utils.ts +++ b/apps/sim/providers/nvidia/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from an NVIDIA NIM streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from an NVIDIA NIM streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromNvidiaStream( nvidiaStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(nvidiaStream, 'NVIDIA', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(nvidiaStream, { + providerName: 'NVIDIA', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/ollama-cloud/utils.ts b/apps/sim/providers/ollama-cloud/utils.ts index 3ab364c66bb..d768b1d9134 100644 --- a/apps/sim/providers/ollama-cloud/utils.ts +++ b/apps/sim/providers/ollama-cloud/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from an Ollama Cloud streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from an Ollama Cloud streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromOllamaCloudStream( ollamaStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(ollamaStream, 'Ollama Cloud', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(ollamaStream, { + providerName: 'Ollama Cloud', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/ollama/core.ts b/apps/sim/providers/ollama/core.ts index 9e10b7c436a..3d5d23295e8 100644 --- a/apps/sim/providers/ollama/core.ts +++ b/apps/sim/providers/ollama/core.ts @@ -10,6 +10,7 @@ import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { formatMessagesForProvider } from '@/providers/attachments' import { createStreamingExecution } from '@/providers/streaming-execution' +import type { AgentStreamEvent } from '@/providers/stream-events' import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter' import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment' import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types' @@ -54,8 +55,8 @@ export interface OllamaCoreConfig { createClient: () => OpenAI createStream: ( stream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void - ) => ReadableStream + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void + ) => ReadableStream logger: Logger } @@ -181,6 +182,7 @@ export async function executeOllamaProviderRequest( timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => config.createStream(streamResponse, (content, usage) => { output.content = content @@ -489,6 +491,7 @@ export async function executeOllamaProviderRequest( count: toolCalls.length, } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output }) => config.createStream(streamResponse, (content, usage) => { output.content = content diff --git a/apps/sim/providers/ollama/utils.ts b/apps/sim/providers/ollama/utils.ts index 45ea5d9957f..71e7890f7a5 100644 --- a/apps/sim/providers/ollama/utils.ts +++ b/apps/sim/providers/ollama/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from an Ollama streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from an Ollama streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromOllamaStream( ollamaStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(ollamaStream, 'Ollama', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(ollamaStream, { + providerName: 'Ollama', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/openai-compat/stream-events.test.ts b/apps/sim/providers/openai-compat/stream-events.test.ts new file mode 100644 index 00000000000..745b8a4f605 --- /dev/null +++ b/apps/sim/providers/openai-compat/stream-events.test.ts @@ -0,0 +1,110 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { + openaiCompatReasoningAndTextChunks, + openaiCompatTextOnlyChunks, + openaiCompatToolCallStartChunks, +} from '@/providers/__fixtures__/openai-compat' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +describe('createOpenAICompatibleAgentEventStream (Step 9)', () => { + it('emits thinking_delta from reasoning_content then text_delta', async () => { + const onComplete = vi.fn() + const stream = createOpenAICompatibleAgentEventStream( + (async function* () { + yield* openaiCompatReasoningAndTextChunks as any + })(), + { providerName: 'DeepSeek', onComplete } + ) + + const events = await collectEvents(stream) + expect(events.filter((e) => e.type === 'thinking_delta').map((e) => e.text)).toEqual([ + 'I should compute carefully. ', + 'Answer is 4.', + ]) + expect(events.filter((e) => e.type === 'text_delta')).toEqual([ + { type: 'text_delta', text: '2+2=', turn: 'final' }, + { type: 'text_delta', text: '4', turn: 'final' }, + ]) + expect(onComplete.mock.calls[0][0]).toMatchObject({ + content: '2+2=4', + thinking: 'I should compute carefully. Answer is 4.', + usage: { prompt_tokens: 10, completion_tokens: 8 }, + }) + }) + + it('stays text-only when no reasoning fields are present', async () => { + const stream = createOpenAICompatibleAgentEventStream( + (async function* () { + yield* openaiCompatTextOnlyChunks as any + })(), + { providerName: 'Groq' } + ) + const events = await collectEvents(stream) + expect(events.every((e) => e.type === 'text_delta')).toBe(true) + expect(events.some((e) => e.type === 'thinking_delta')).toBe(false) + }) + + it('emits tool_call_start when enabled and id+name are known', async () => { + const onComplete = vi.fn() + const stream = createOpenAICompatibleAgentEventStream( + (async function* () { + yield* openaiCompatToolCallStartChunks as any + yield { + choices: [ + { + delta: { + tool_calls: [{ index: 0, function: { arguments: '"https://x"}' } }], + }, + }, + ], + } + })(), + { providerName: 'Groq', emitToolCallStarts: true, onComplete } + ) + const events = await collectEvents(stream) + expect(events).toContainEqual({ + type: 'tool_call_start', + id: 'call_abc', + name: 'http_request', + }) + // Only once even if later deltas omit id + expect(events.filter((e) => e.type === 'tool_call_start')).toHaveLength(1) + expect(onComplete.mock.calls[0][0].toolCalls).toEqual([ + { + id: 'call_abc', + type: 'function', + function: { name: 'http_request', arguments: '{"url":"https://x"}' }, + }, + ]) + }) + + it('buffers text deltas when bufferTextDeltas is true', async () => { + const onComplete = vi.fn() + const stream = createOpenAICompatibleAgentEventStream( + (async function* () { + yield* openaiCompatTextOnlyChunks as any + })(), + { providerName: 'DeepSeek', bufferTextDeltas: true, onComplete } + ) + const events = await collectEvents(stream) + expect(events.some((e) => e.type === 'text_delta')).toBe(false) + expect(onComplete.mock.calls[0][0].content).toBe('Hello world') + }) +}) diff --git a/apps/sim/providers/openai-compat/stream-events.ts b/apps/sim/providers/openai-compat/stream-events.ts new file mode 100644 index 00000000000..0fd1b45dc32 --- /dev/null +++ b/apps/sim/providers/openai-compat/stream-events.ts @@ -0,0 +1,196 @@ +/** + * OpenAI Chat Completions → agent-events-v1 (Step 9). + * + * Capability-honest: emits `thinking_delta` only when the vendor streams a + * reasoning field on the delta (`reasoning_content`, `reasoning`, etc.). + * Non-reasoning models stay text-only. Tool_call starts emit when id+name known. + * Tool-call argument deltas are accumulated for tool-loop history. + */ + +import type { ChatCompletionChunk } from 'openai/resources/chat/completions' +import type { CompletionUsage } from 'openai/resources/completions' +import { createLogger } from '@sim/logger' +import type { AgentStreamEvent, TextDeltaTurn } from '@/providers/stream-events' + +export interface OpenAICompatAssembledToolCall { + id: string + type: 'function' + function: { name: string; arguments: string } +} + +export interface OpenAICompatStreamComplete { + content: string + thinking: string + /** DeepSeek-style CoT field accumulated from deltas. */ + reasoning_content?: string + /** Groq-style reasoning field accumulated from deltas. */ + reasoning?: string + usage: CompletionUsage + /** Assembled when emitToolCallStarts is true (id + name + args). */ + toolCalls?: OpenAICompatAssembledToolCall[] +} + +export interface CreateOpenAICompatibleAgentEventStreamOptions { + providerName: string + /** Tag for answer text (default `final`). Use `intermediate` inside tool loops. */ + turn?: TextDeltaTurn + /** Emit tool_call_start from delta.tool_calls when id+name known. Default false for no-tools path. */ + emitToolCallStarts?: boolean + /** + * When true, accumulate text but do not enqueue text_delta until the caller + * flushes with a known turn tag (tool loops need this). + */ + bufferTextDeltas?: boolean + onComplete?: (result: OpenAICompatStreamComplete) => void +} + +function extractDeltaReasoning(delta: Record | undefined): { + text: string + reasoning_content?: string + reasoning?: string +} { + if (!delta) return { text: '' } + + if (typeof delta.reasoning_content === 'string' && delta.reasoning_content) { + return { text: delta.reasoning_content, reasoning_content: delta.reasoning_content } + } + if (typeof delta.reasoning === 'string' && delta.reasoning) { + return { text: delta.reasoning, reasoning: delta.reasoning } + } + const nested = delta.reasoning + if ( + nested && + typeof nested === 'object' && + typeof (nested as { text?: string }).text === 'string' + ) { + const text = (nested as { text: string }).text + return { text, reasoning: text } + } + return { text: '' } +} + +/** + * Converts an OpenAI-compatible chat.completions stream into an in-process + * {@link AgentStreamEvent} object stream. + */ +export function createOpenAICompatibleAgentEventStream( + stream: AsyncIterable, + options: CreateOpenAICompatibleAgentEventStreamOptions +): ReadableStream { + const { + providerName, + turn = 'final', + emitToolCallStarts = false, + bufferTextDeltas = false, + onComplete, + } = options + const streamLogger = createLogger(`${providerName}Utils`) + + return new ReadableStream({ + async start(controller) { + let fullContent = '' + let fullThinking = '' + let reasoningContent = '' + let reasoning = '' + let promptTokens = 0 + let completionTokens = 0 + let totalTokens = 0 + const seenToolIds = new Set() + const toolBuffers = new Map< + number, + { id?: string; name?: string; args: string; started: boolean } + >() + + try { + for await (const chunk of stream) { + if (chunk.usage) { + promptTokens = chunk.usage.prompt_tokens ?? 0 + completionTokens = chunk.usage.completion_tokens ?? 0 + totalTokens = chunk.usage.total_tokens ?? 0 + } + + const choice = chunk.choices?.[0] + const delta = choice?.delta as Record | undefined + + const extracted = extractDeltaReasoning(delta) + if (extracted.text) { + fullThinking += extracted.text + if (extracted.reasoning_content) reasoningContent += extracted.reasoning_content + if (extracted.reasoning) reasoning += extracted.reasoning + controller.enqueue({ type: 'thinking_delta', text: extracted.text }) + } + + const content = typeof delta?.content === 'string' ? delta.content : '' + if (content) { + fullContent += content + if (!bufferTextDeltas) { + controller.enqueue({ type: 'text_delta', text: content, turn }) + } + } + + if (emitToolCallStarts && Array.isArray(delta?.tool_calls)) { + for (const tc of delta.tool_calls as Array<{ + index?: number + id?: string + type?: string + function?: { name?: string; arguments?: string } + }>) { + const index = typeof tc.index === 'number' ? tc.index : 0 + const buf = toolBuffers.get(index) ?? { + id: undefined, + name: undefined, + args: '', + started: false, + } + if (tc.id) buf.id = tc.id + if (tc.function?.name) buf.name = tc.function.name + if (typeof tc.function?.arguments === 'string') { + buf.args += tc.function.arguments + } + toolBuffers.set(index, buf) + + if (buf.id && buf.name && !buf.started && !seenToolIds.has(buf.id)) { + buf.started = true + seenToolIds.add(buf.id) + controller.enqueue({ type: 'tool_call_start', id: buf.id, name: buf.name }) + } + } + } + } + + if (onComplete) { + if (promptTokens === 0 && completionTokens === 0) { + streamLogger.warn(`${providerName} stream completed without usage data`) + } + const toolCalls: OpenAICompatAssembledToolCall[] = [] + if (emitToolCallStarts) { + for (const [, buf] of [...toolBuffers.entries()].sort(([a], [b]) => a - b)) { + if (!buf.id || !buf.name) continue + toolCalls.push({ + id: buf.id, + type: 'function', + function: { name: buf.name, arguments: buf.args || '{}' }, + }) + } + } + onComplete({ + content: fullContent, + thinking: fullThinking, + ...(reasoningContent ? { reasoning_content: reasoningContent } : {}), + ...(reasoning ? { reasoning } : {}), + usage: { + prompt_tokens: promptTokens, + completion_tokens: completionTokens, + total_tokens: totalTokens || promptTokens + completionTokens, + }, + ...(toolCalls.length > 0 ? { toolCalls } : {}), + }) + } + + controller.close() + } catch (error) { + controller.error(error) + } + }, + }) +} diff --git a/apps/sim/providers/openai-compat/streaming-tool-loop.test.ts b/apps/sim/providers/openai-compat/streaming-tool-loop.test.ts new file mode 100644 index 00000000000..32f60ebdfcc --- /dev/null +++ b/apps/sim/providers/openai-compat/streaming-tool-loop.test.ts @@ -0,0 +1,297 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { openaiCompatToolCallStartChunks } from '@/providers/__fixtures__/openai-compat' +import { createOpenAICompatStreamingToolLoopStream } from '@/providers/openai-compat/streaming-tool-loop' +import type { AgentStreamEvent } from '@/providers/stream-events' +import type { TimeSegment } from '@/providers/types' + +const { mockExecuteTool, mockPrepareToolExecution } = vi.hoisted(() => ({ + mockExecuteTool: vi.fn(), + mockPrepareToolExecution: vi.fn(), +})) + +vi.mock('@/tools', () => ({ + executeTool: mockExecuteTool, +})) + +vi.mock('@/providers/utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + prepareToolExecution: mockPrepareToolExecution, + calculateCost: () => ({ input: 0.01, output: 0.02, total: 0.03 }), + sumToolCosts: () => 0, + } +}) + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 5 })) + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +function toolThenAnswerChunks(toolName: string, args: string, answer: string) { + return [ + { + choices: [ + { + delta: { + reasoning_content: 'I should call the tool. ', + tool_calls: [ + { index: 0, id: 'call_1', type: 'function', function: { name: toolName, arguments: '' } }, + ], + }, + }, + ], + }, + { + choices: [ + { + delta: { + tool_calls: [{ index: 0, function: { arguments: args } }], + }, + }, + ], + usage: { prompt_tokens: 5, completion_tokens: 3, total_tokens: 8 }, + }, + // Second turn (final answer) — yielded by a separate createStream call + ] as const +} + +describe('createOpenAICompatStreamingToolLoopStream', () => { + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + } as any + + beforeEach(() => { + mockExecuteTool.mockReset() + mockPrepareToolExecution.mockReset() + logger.info.mockReset() + mockPrepareToolExecution.mockImplementation((_tool: unknown, args: unknown) => ({ + toolParams: args, + executionParams: args, + })) + mockExecuteTool.mockResolvedValue({ + success: true, + output: { answer: 42 }, + }) + }) + + it('keeps reasoning_content on assistant history when preserveAssistantReasoning is true', async () => { + const messageHistory: unknown[][] = [] + let call = 0 + + const createStream = vi.fn(async (params: { messages: unknown[] }) => { + messageHistory.push(params.messages) + call += 1 + if (call === 1) { + return (async function* () { + yield* toolThenAnswerChunks('lookup', '{}', '') + })() + } + return (async function* () { + yield { + choices: [{ delta: { content: 'done', reasoning_content: 'final thought' } }], + usage: { prompt_tokens: 8, completion_tokens: 2, total_tokens: 10 }, + } + })() + }) + + const timeSegments: TimeSegment[] = [] + const stream = createOpenAICompatStreamingToolLoopStream({ + providerName: 'Deepseek', + request: { + model: 'deepseek-chat', + apiKey: 'k', + messages: [], + tools: [{ id: 'lookup', name: 'lookup', description: 'd', parameters: {} } as any], + thinkingLevel: 'enabled', + }, + basePayload: { model: 'deepseek-chat' }, + messages: [{ role: 'user', content: 'use the tool' }], + createStream: createStream as any, + logger, + timeSegments, + preserveAssistantReasoning: true, + onComplete: () => {}, + }) + + await collectEvents(stream) + + expect(createStream).toHaveBeenCalledTimes(2) + const secondTurnMessages = messageHistory[1] as Array> + const assistantWithTools = secondTurnMessages.find( + (m) => m.role === 'assistant' && Array.isArray(m.tool_calls) + ) + expect(assistantWithTools).toMatchObject({ + role: 'assistant', + reasoning_content: 'I should call the tool. ', + }) + }) + + it('omits reasoning_content when preserveAssistantReasoning is false', async () => { + const messageHistory: unknown[][] = [] + let call = 0 + + const createStream = vi.fn(async (params: { messages: unknown[] }) => { + messageHistory.push(params.messages) + call += 1 + if (call === 1) { + return (async function* () { + yield* toolThenAnswerChunks('lookup', '{}', '') + })() + } + return (async function* () { + yield { + choices: [{ delta: { content: 'done' } }], + usage: { prompt_tokens: 8, completion_tokens: 2, total_tokens: 10 }, + } + })() + }) + + const stream = createOpenAICompatStreamingToolLoopStream({ + providerName: 'Groq', + request: { + model: 'groq/openai/gpt-oss-120b', + apiKey: 'k', + messages: [], + tools: [{ id: 'lookup', name: 'lookup', description: 'd', parameters: {} } as any], + }, + basePayload: { model: 'openai/gpt-oss-120b' }, + messages: [{ role: 'user', content: 'use the tool' }], + createStream: createStream as any, + logger, + timeSegments: [], + preserveAssistantReasoning: false, + onComplete: () => {}, + }) + + await collectEvents(stream) + + const secondTurnMessages = messageHistory[1] as Array> + const assistantWithTools = secondTurnMessages.find( + (m) => m.role === 'assistant' && Array.isArray(m.tool_calls) + ) + expect(assistantWithTools).toBeDefined() + expect(assistantWithTools?.reasoning_content).toBeUndefined() + }) + + it('rotates forced tool_choice to auto after the forced tool is used', async () => { + const toolChoices: unknown[] = [] + let call = 0 + + const createStream = vi.fn(async (params: { tool_choice?: unknown }) => { + toolChoices.push(params.tool_choice) + call += 1 + if (call === 1) { + return (async function* () { + yield* toolThenAnswerChunks('lookup', '{}', '') + })() + } + return (async function* () { + yield { + choices: [{ delta: { content: 'final answer' } }], + usage: { prompt_tokens: 8, completion_tokens: 2, total_tokens: 10 }, + } + })() + }) + + const events = await collectEvents( + createOpenAICompatStreamingToolLoopStream({ + providerName: 'Deepseek', + request: { + model: 'deepseek-chat', + apiKey: 'k', + messages: [], + tools: [{ id: 'lookup', name: 'lookup', description: 'd', parameters: {} } as any], + }, + basePayload: { + model: 'deepseek-chat', + tool_choice: { type: 'function', function: { name: 'lookup' } }, + }, + messages: [{ role: 'user', content: 'force lookup then answer' }], + createStream: createStream as any, + logger, + timeSegments: [], + forcedTools: ['lookup'], + onComplete: () => {}, + }) + ) + + expect(createStream).toHaveBeenCalledTimes(2) + expect(toolChoices[0]).toEqual({ type: 'function', function: { name: 'lookup' } }) + expect(toolChoices[1]).toBe('auto') + expect(events.some((e) => e.type === 'text_delta' && e.text === 'final answer')).toBe(true) + expect(logger.info).toHaveBeenCalledWith( + 'All forced tools have been used, switching to auto tool_choice' + ) + }) + + it('streams thinking live and assembles tool args from deltas', async () => { + let call = 0 + const createStream = vi.fn(async () => { + call += 1 + if (call === 1) { + return (async function* () { + yield* openaiCompatToolCallStartChunks as any + yield { + choices: [ + { + delta: { + tool_calls: [{ index: 0, function: { arguments: '"https://example.com"}' } }], + }, + }, + ], + usage: { prompt_tokens: 4, completion_tokens: 6, total_tokens: 10 }, + } + })() + } + return (async function* () { + yield { + choices: [{ delta: { content: 'fetched' } }], + usage: { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 }, + } + })() + }) + + await collectEvents( + createOpenAICompatStreamingToolLoopStream({ + providerName: 'Deepseek', + request: { + model: 'deepseek-chat', + apiKey: 'k', + messages: [], + tools: [ + { id: 'http_request', name: 'http_request', description: 'd', parameters: {} } as any, + ], + }, + basePayload: { model: 'deepseek-chat' }, + messages: [{ role: 'user', content: 'fetch' }], + createStream: createStream as any, + logger, + timeSegments: [], + onComplete: () => {}, + }) + ) + + expect(mockExecuteTool).toHaveBeenCalledWith( + 'http_request', + { url: 'https://example.com' }, + expect.objectContaining({ skipPostProcess: true }) + ) + }) +}) diff --git a/apps/sim/providers/openai-compat/streaming-tool-loop.ts b/apps/sim/providers/openai-compat/streaming-tool-loop.ts new file mode 100644 index 00000000000..f18de36e98e --- /dev/null +++ b/apps/sim/providers/openai-compat/streaming-tool-loop.ts @@ -0,0 +1,451 @@ +/** + * Shared OpenAI Chat Completions streaming tool loop (Step 9). + * + * Capability-honest: reasoning deltas only when the vendor streams them. + * Final-turn-only answer projection via `turn` tags. Tool ends in completion + * order; abort → cancelled. + * + * Streams each model turn live (thinking + tool_call_start). Answer text is + * buffered until the turn is classified intermediate vs final — same contract + * as Anthropic/Gemini/Bedrock loops. Tool args are assembled from streamed + * `tool_calls` deltas (no blocking hybrid). + */ + +import type OpenAI from 'openai' +import type { ChatCompletionChunk } from 'openai/resources/chat/completions' +import type { Logger } from '@sim/logger' +import { getErrorMessage, toError } from '@sim/utils/errors' +import type { NormalizedBlockOutput } from '@/executor/types' +import { MAX_TOOL_ITERATIONS } from '@/providers' +import { + createOpenAICompatibleAgentEventStream, + type OpenAICompatAssembledToolCall, +} from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent, ToolCallEndStatus } from '@/providers/stream-events' +import type { ProviderRequest, TimeSegment } from '@/providers/types' +import { + calculateCost, + prepareToolExecution, + sumToolCosts, + trackForcedToolUsage, +} from '@/providers/utils' +import { executeTool } from '@/tools' + +export interface OpenAICompatStreamingToolLoopComplete { + content: string + tokens: { input: number; output: number; total: number } + cost: NormalizedBlockOutput['cost'] + toolCalls?: { list: unknown[]; count: number } + modelTime: number + toolsTime: number + firstResponseTime: number + iterations: number +} + +export type OpenAICompatCreateCompletion = ( + params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, + options?: { signal?: AbortSignal } +) => Promise> + +/** @deprecated Prefer streamed tool-arg assembly; retained for callers that still pass it. */ +export type OpenAICompatCreateCompletionBlocking = ( + params: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming, + options?: { signal?: AbortSignal } +) => Promise + +export interface CreateOpenAICompatStreamingToolLoopOptions { + providerName: string + request: ProviderRequest + /** Base chat.completions payload (messages, tools, model, …) without stream. */ + basePayload: Record + messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] + createStream: OpenAICompatCreateCompletion + /** Ignored — tool args come from streamed deltas. Kept for call-site compatibility. */ + createBlocking?: OpenAICompatCreateCompletionBlocking + logger: Logger + timeSegments: TimeSegment[] + forcedTools?: string[] + /** + * When true, keep vendor reasoning fields on assistant history messages + * during the tool loop (required by DeepSeek thinking + tools). + */ + preserveAssistantReasoning?: boolean + onComplete: (result: OpenAICompatStreamingToolLoopComplete) => void +} + +function isAbortError(error: unknown): boolean { + if (!error || typeof error !== 'object') return false + const name = (error as { name?: string }).name + return name === 'AbortError' || name === 'APIUserAbortError' +} + +function settleOpenTools( + controller: ReadableStreamDefaultController, + openTools: Map, + status: ToolCallEndStatus +): void { + for (const [id, name] of openTools) { + controller.enqueue({ type: 'tool_call_end', id, name, status }) + } + openTools.clear() +} + +function nextForcedToolChoice( + forcedTools: string[], + usedForcedTools: string[] +): 'auto' | { type: 'function'; function: { name: string } } { + const remaining = forcedTools.filter((tool) => !usedForcedTools.includes(tool)) + if (remaining.length === 0) return 'auto' + return { type: 'function', function: { name: remaining[0] } } +} + +/** + * Multi-turn OpenAI-compat tool loop as an agent-events-v1 object stream. + */ +export function createOpenAICompatStreamingToolLoopStream( + options: CreateOpenAICompatStreamingToolLoopOptions +): ReadableStream { + const { + providerName, + request, + basePayload, + messages, + createStream, + logger, + timeSegments, + onComplete, + preserveAssistantReasoning = false, + } = options + const forcedTools = options.forcedTools ?? [] + + return new ReadableStream({ + async start(controller) { + const currentMessages = [...messages] + let content = '' + let iterationCount = 0 + let modelTime = 0 + let toolsTime = 0 + let firstResponseTime = 0 + const tokens = { input: 0, output: 0, total: 0 } + const toolCalls: unknown[] = [] + const toolResults: Record[] = [] + const openToolStarts = new Map() + const streamOpts = request.abortSignal ? { signal: request.abortSignal } : undefined + + let currentToolChoice = basePayload.tool_choice + let usedForcedTools: string[] = [] + let hasUsedForcedTool = false + + try { + while (iterationCount < MAX_TOOL_ITERATIONS) { + if (request.abortSignal?.aborted) { + settleOpenTools(controller, openToolStarts, 'cancelled') + throw new DOMException('Stream aborted', 'AbortError') + } + + const modelStart = Date.now() + const turnPayload = { + ...basePayload, + messages: currentMessages, + ...(currentToolChoice !== undefined ? { tool_choice: currentToolChoice } : {}), + stream: true as const, + } + + const stream = await createStream( + turnPayload as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, + streamOpts + ) + + let turnUsage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } + let turnContent = '' + let turnReasoningContent = '' + let turnReasoning = '' + let assembledTools: OpenAICompatAssembledToolCall[] = [] + const bufferedText: string[] = [] + + const eventStream = createOpenAICompatibleAgentEventStream(stream, { + providerName, + emitToolCallStarts: true, + bufferTextDeltas: true, + onComplete: (result) => { + turnUsage = { + prompt_tokens: result.usage.prompt_tokens ?? 0, + completion_tokens: result.usage.completion_tokens ?? 0, + total_tokens: result.usage.total_tokens ?? 0, + } + turnContent = result.content || '' + turnReasoningContent = result.reasoning_content || '' + turnReasoning = result.reasoning || '' + assembledTools = result.toolCalls ?? [] + }, + }) + + { + const reader = eventStream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + if (value.type === 'thinking_delta' || value.type === 'tool_call_start') { + if (value.type === 'tool_call_start') { + openToolStarts.set(value.id, value.name) + } + controller.enqueue(value) + } else if (value.type === 'text_delta') { + // Should not arrive when bufferTextDeltas is true; keep for safety. + bufferedText.push(value.text) + } + } + } + + const pendingTools = assembledTools.filter((tc) => tc.id && tc.function?.name) + const turnTag = pendingTools.length > 0 ? 'intermediate' : 'final' + const textToFlush = turnContent || bufferedText.join('') + if (textToFlush) { + controller.enqueue({ type: 'text_delta', text: textToFlush, turn: turnTag }) + } + + const modelEnd = Date.now() + const thisModelTime = modelEnd - modelStart + modelTime += thisModelTime + if (iterationCount === 0) firstResponseTime = thisModelTime + timeSegments.push({ + type: 'model', + name: request.model, + startTime: modelStart, + endTime: modelEnd, + duration: thisModelTime, + }) + tokens.input += turnUsage.prompt_tokens + tokens.output += turnUsage.completion_tokens + tokens.total += + turnUsage.total_tokens || turnUsage.prompt_tokens + turnUsage.completion_tokens + + if (pendingTools.length === 0) { + content = textToFlush || content + break + } + + if ( + typeof currentToolChoice === 'object' && + currentToolChoice !== null && + pendingTools.length > 0 + ) { + const tracked = trackForcedToolUsage( + pendingTools, + currentToolChoice, + logger, + providerName.toLowerCase(), + forcedTools, + usedForcedTools + ) + hasUsedForcedTool = tracked.hasUsedForcedTool + usedForcedTools = tracked.usedForcedTools + } + + const assistantHistory: Record = { + role: 'assistant', + content: textToFlush || null, + tool_calls: pendingTools, + } + if (preserveAssistantReasoning) { + if (turnReasoningContent) { + assistantHistory.reasoning_content = turnReasoningContent + } + if (turnReasoning) { + assistantHistory.reasoning = turnReasoning + } + } + currentMessages.push( + assistantHistory as OpenAI.Chat.Completions.ChatCompletionMessageParam + ) + + const toolsStartTime = Date.now() + const orderedResults = await Promise.all( + pendingTools.map(async (tc) => { + const toolCallStartTime = Date.now() + const toolName = tc.function.name + const toolUseId = tc.id + let toolArgs: Record = {} + try { + toolArgs = JSON.parse(tc.function.arguments || '{}') + } catch { + toolArgs = {} + } + + try { + if (request.abortSignal?.aborted) { + throw new DOMException('Stream aborted', 'AbortError') + } + const tool = request.tools?.find((t) => t.id === toolName) + if (!tool) { + const value = { + toolUseId, + toolName, + toolArgs, + toolParams: {} as Record, + result: { + success: false as const, + output: undefined, + error: `Tool not found: ${toolName}`, + }, + startTime: toolCallStartTime, + endTime: Date.now(), + duration: Date.now() - toolCallStartTime, + status: 'error' as ToolCallEndStatus, + } + openToolStarts.delete(toolUseId) + controller.enqueue({ + type: 'tool_call_end', + id: toolUseId, + name: toolName, + status: 'error', + }) + return value + } + + const { toolParams, executionParams } = prepareToolExecution( + tool, + toolArgs, + request + ) + const result = await executeTool(toolName, executionParams, { + skipPostProcess: true, + signal: request.abortSignal, + }) + const toolCallEndTime = Date.now() + const value = { + toolUseId, + toolName, + toolArgs, + toolParams, + result, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + status: (result.success ? 'success' : 'error') as ToolCallEndStatus, + } + openToolStarts.delete(toolUseId) + controller.enqueue({ + type: 'tool_call_end', + id: toolUseId, + name: toolName, + status: value.status, + }) + return value + } catch (error) { + const cancelled = isAbortError(error) || !!request.abortSignal?.aborted + if (!cancelled) { + logger.error('Error processing tool call:', { error, toolName }) + } + const toolCallEndTime = Date.now() + const value = { + toolUseId, + toolName, + toolArgs, + toolParams: {} as Record, + result: { + success: false as const, + output: undefined, + error: getErrorMessage(error, 'Tool execution failed'), + }, + startTime: toolCallStartTime, + endTime: toolCallEndTime, + duration: toolCallEndTime - toolCallStartTime, + status: (cancelled ? 'cancelled' : 'error') as ToolCallEndStatus, + } + openToolStarts.delete(toolUseId) + controller.enqueue({ + type: 'tool_call_end', + id: toolUseId, + name: toolName, + status: value.status, + }) + return value + } + }) + ) + + for (const value of orderedResults) { + timeSegments.push({ + type: 'tool', + name: value.toolName, + startTime: value.startTime, + endTime: value.endTime, + duration: value.duration, + toolCallId: value.toolUseId, + }) + + let resultContent: unknown + if (value.result.success && value.result.output) { + toolResults.push(value.result.output as Record) + resultContent = value.result.output + } else { + resultContent = { + error: true, + message: value.result.error || 'Tool execution failed', + tool: value.toolName, + } + } + + toolCalls.push({ + name: value.toolName, + arguments: value.toolParams, + startTime: new Date(value.startTime).toISOString(), + endTime: new Date(value.endTime).toISOString(), + duration: value.duration, + result: resultContent, + success: value.result.success, + }) + + currentMessages.push({ + role: 'tool', + tool_call_id: value.toolUseId, + content: JSON.stringify(resultContent), + }) + } + + toolsTime += Date.now() - toolsStartTime + + // Rotate / clear forced tool_choice so the model can answer after forced tools. + if (typeof currentToolChoice === 'object' && currentToolChoice !== null) { + if (hasUsedForcedTool && forcedTools.length > 0) { + const next = nextForcedToolChoice(forcedTools, usedForcedTools) + currentToolChoice = next + if (next === 'auto') { + logger.info('All forced tools have been used, switching to auto tool_choice') + } else { + logger.info(`Forcing next tool: ${next.function.name}`) + } + } + } + + iterationCount++ + } + + const modelCost = calculateCost(request.model, tokens.input, tokens.output) + const toolCostTotal = sumToolCosts(toolResults) + onComplete({ + content, + tokens, + cost: { + input: modelCost.input, + output: modelCost.output, + total: modelCost.total + (toolCostTotal || 0), + ...(toolCostTotal ? { toolCost: toolCostTotal } : {}), + }, + toolCalls: + toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + modelTime, + toolsTime, + firstResponseTime, + iterations: iterationCount + 1, + }) + controller.close() + } catch (error) { + const cancelled = isAbortError(error) || !!request.abortSignal?.aborted + settleOpenTools(controller, openToolStarts, cancelled ? 'cancelled' : 'error') + controller.error(toError(error)) + } + }, + }) +} diff --git a/apps/sim/providers/openai/core.reasoning.test.ts b/apps/sim/providers/openai/core.reasoning.test.ts new file mode 100644 index 00000000000..2b4f481e727 --- /dev/null +++ b/apps/sim/providers/openai/core.reasoning.test.ts @@ -0,0 +1,107 @@ +/** + * @vitest-environment node + * + * OpenAI Responses harden: reasoning models always request summary: 'auto' + * even when effort is auto / unset. + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { executeResponsesProviderRequest } from '@/providers/openai/core' +import type { ProviderRequest } from '@/providers/types' + +vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 5 })) + +vi.mock('@/providers/utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + calculateCost: () => ({ input: 0, output: 0, total: 0 }), + sumToolCosts: () => 0, + prepareToolsWithUsageControl: () => ({ + tools: [], + toolChoice: undefined, + forcedTools: [], + hasFilteredTools: false, + }), + } +}) + +vi.mock('@/tools', () => ({ executeTool: vi.fn() })) + +function jsonResponse(body: unknown, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }) +} + +describe('executeResponsesProviderRequest reasoning harden', () => { + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + } as any + + let fetchMock: ReturnType + + beforeEach(() => { + fetchMock = vi.fn().mockResolvedValue( + jsonResponse({ + id: 'resp_1', + status: 'completed', + output: [ + { + type: 'message', + role: 'assistant', + content: [{ type: 'output_text', text: 'hello' }], + }, + ], + usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 }, + }) + ) + }) + + function run(request: Partial & { model: string }) { + return executeResponsesProviderRequest( + { + apiKey: 'k', + messages: [{ role: 'user', content: 'hi' }], + ...request, + }, + { + providerId: 'openai', + providerLabel: 'OpenAI', + modelName: request.model, + endpoint: 'https://api.openai.com/v1/responses', + headers: { Authorization: 'Bearer k' }, + logger, + fetch: fetchMock as unknown as typeof fetch, + } + ) + } + + it('includes reasoning.summary auto when effort is auto', async () => { + await run({ model: 'gpt-5.5', reasoningEffort: 'auto' }) + expect(fetchMock).toHaveBeenCalled() + const body = JSON.parse(fetchMock.mock.calls[0][1].body as string) + expect(body.reasoning).toEqual({ summary: 'auto' }) + }) + + it('includes reasoning.summary auto when effort is unset', async () => { + await run({ model: 'o3' }) + const body = JSON.parse(fetchMock.mock.calls[0][1].body as string) + expect(body.reasoning).toEqual({ summary: 'auto' }) + }) + + it('includes effort and summary when effort is explicit', async () => { + await run({ model: 'gpt-5.5', reasoningEffort: 'high' }) + const body = JSON.parse(fetchMock.mock.calls[0][1].body as string) + expect(body.reasoning).toEqual({ summary: 'auto', effort: 'high' }) + }) + + it('omits reasoning for non-reasoning models', async () => { + await run({ model: 'gpt-4o' }) + const body = JSON.parse(fetchMock.mock.calls[0][1].body as string) + expect(body.reasoning).toBeUndefined() + }) +}) diff --git a/apps/sim/providers/openai/core.ts b/apps/sim/providers/openai/core.ts index 913700ef5d7..6938c337a91 100644 --- a/apps/sim/providers/openai/core.ts +++ b/apps/sim/providers/openai/core.ts @@ -14,6 +14,7 @@ import { prepareToolExecution, prepareToolsWithUsageControl, sumToolCosts, + supportsReasoningEffort, trackForcedToolUsage, } from '@/providers/utils' import { executeTool } from '@/tools' @@ -98,10 +99,14 @@ export async function executeResponsesProviderRequest( if (request.temperature !== undefined) basePayload.temperature = request.temperature if (request.maxTokens != null) basePayload.max_output_tokens = request.maxTokens - if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') { + // Always request reasoning summaries on reasoning models so Thinking chrome works + // even when effort is `auto` / unset (API defaults effort). + if (supportsReasoningEffort(config.modelName)) { basePayload.reasoning = { - effort: request.reasoningEffort, summary: 'auto', + ...(request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto' + ? { effort: request.reasoningEffort } + : {}), } } @@ -255,8 +260,9 @@ export async function executeResponsesProviderRequest( timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => - createReadableStreamFromResponses(streamResponse, (content, usage) => { + createReadableStreamFromResponses(streamResponse, (content, usage, thinking) => { output.content = content output.tokens = { input: usage?.promptTokens || 0, @@ -275,6 +281,14 @@ export async function executeResponsesProviderRequest( total: costResult.total, } + if (thinking) { + const segment = output.providerTiming?.timeSegments?.[0] + if (segment) { + // Label honestly: these are reasoning *summaries*, not raw CoT. + segment.thinkingContent = thinking + } + } + finalizeTiming() }), }) @@ -582,10 +596,12 @@ export async function executeResponsesProviderRequest( // Copy over non-tool related settings if (request.temperature !== undefined) finalPayload.temperature = request.temperature if (request.maxTokens != null) finalPayload.max_output_tokens = request.maxTokens - if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') { + if (supportsReasoningEffort(config.modelName)) { finalPayload.reasoning = { - effort: request.reasoningEffort, summary: 'auto', + ...(request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto' + ? { effort: request.reasoningEffort } + : {}), } } if (request.verbosity !== undefined && request.verbosity !== 'auto') { @@ -681,8 +697,9 @@ export async function executeResponsesProviderRequest( total: accumulatedCost.total, }, toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output }) => - createReadableStreamFromResponses(streamResponse, (content, usage) => { + createReadableStreamFromResponses(streamResponse, (content, usage, thinking) => { output.content = content output.tokens = { input: tokens.input + (usage?.promptTokens || 0), @@ -702,6 +719,13 @@ export async function executeResponsesProviderRequest( toolCost: tc || undefined, total: accumulatedCost.total + streamCost.total + tc, } + + if (thinking) { + const lastModel = [...timeSegments].reverse().find((s) => s.type === 'model') + if (lastModel) { + lastModel.thinkingContent = thinking + } + } }), }) diff --git a/apps/sim/providers/openai/utils.stream.test.ts b/apps/sim/providers/openai/utils.stream.test.ts new file mode 100644 index 00000000000..c044584c583 --- /dev/null +++ b/apps/sim/providers/openai/utils.stream.test.ts @@ -0,0 +1,75 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { createReadableStreamFromResponses } from '@/providers/openai/utils' +import type { AgentStreamEvent } from '@/providers/stream-events' + +function sseResponse(events: Array<{ event?: string; data: unknown }>): Response { + const body = events + .map((e) => { + const lines = [] + if (e.event) lines.push(`event: ${e.event}`) + lines.push(`data: ${JSON.stringify(e.data)}`) + return `${lines.join('\n')}\n\n` + }) + .join('') + return new Response(body, { headers: { 'Content-Type': 'text/event-stream' } }) +} + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +describe('createReadableStreamFromResponses (Step 9)', () => { + it('emits reasoning summary deltas as thinking and output_text as final text', async () => { + const onComplete = vi.fn() + const response = sseResponse([ + { + event: 'response.reasoning_summary_text.delta', + data: { type: 'response.reasoning_summary_text.delta', delta: 'Summary thought. ' }, + }, + { + event: 'response.output_text.delta', + data: { type: 'response.output_text.delta', delta: 'Answer' }, + }, + { + event: 'response.completed', + data: { + type: 'response.completed', + response: { + usage: { input_tokens: 4, output_tokens: 6 }, + }, + }, + }, + ]) + + const events = await collectEvents(createReadableStreamFromResponses(response, onComplete)) + expect(events).toEqual([ + { type: 'thinking_delta', text: 'Summary thought. ' }, + { type: 'text_delta', text: 'Answer', turn: 'final' }, + ]) + expect(onComplete.mock.calls[0][0]).toBe('Answer') + expect(onComplete.mock.calls[0][2]).toBe('Summary thought. ') + }) + + it('stays text-only when no reasoning summary events arrive', async () => { + const response = sseResponse([ + { + event: 'response.output_text.delta', + data: { type: 'response.output_text.delta', delta: 'Hi' }, + }, + ]) + const events = await collectEvents(createReadableStreamFromResponses(response)) + expect(events).toEqual([{ type: 'text_delta', text: 'Hi', turn: 'final' }]) + }) +}) diff --git a/apps/sim/providers/openai/utils.ts b/apps/sim/providers/openai/utils.ts index 495a0eae05b..65d5d5da5ab 100644 --- a/apps/sim/providers/openai/utils.ts +++ b/apps/sim/providers/openai/utils.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import type OpenAI from 'openai' import { buildOpenAIMessageContent } from '@/providers/attachments' +import type { AgentStreamEvent } from '@/providers/stream-events' import type { Message } from '@/providers/types' const logger = createLogger('ResponsesUtils') @@ -411,18 +412,23 @@ export function parseResponsesUsage( } /** - * Creates a ReadableStream from a Responses API SSE stream. + * Creates an agent-events-v1 stream from a Responses API SSE stream. + * + * Capability-honest: emits `thinking_delta` only for streamable reasoning + * *summary* deltas (not encrypted_content / raw CoT). If the API only + * surfaces reasoning at completion, live thinking may be empty — traces still + * use extractResponseReasoning post-hoc. */ export function createReadableStreamFromResponses( response: Response, - onComplete?: (content: string, usage?: ResponsesUsageTokens) => void -): ReadableStream { + onComplete?: (content: string, usage?: ResponsesUsageTokens, thinking?: string) => void +): ReadableStream { let fullContent = '' + let fullThinking = '' let finalUsage: ResponsesUsageTokens | undefined let activeEventType: string | undefined - const encoder = new TextEncoder() - return new ReadableStream({ + return new ReadableStream({ async start(controller) { const reader = response.body?.getReader() if (!reader) { @@ -488,6 +494,28 @@ export function createReadableStreamFromResponses( return } + // Reasoning *summaries* only — never stream encrypted_content. + if ( + eventType === 'response.reasoning_summary_text.delta' || + eventType === 'response.reasoning_summary.delta' || + eventType === 'response.reasoning.delta' + ) { + let deltaText = '' + const delta = event.delta as string | Record | undefined + if (typeof delta === 'string') { + deltaText = delta + } else if (delta && typeof delta.text === 'string') { + deltaText = delta.text + } else if (typeof event.text === 'string') { + deltaText = event.text + } + if (deltaText.length > 0) { + fullThinking += deltaText + controller.enqueue({ type: 'thinking_delta', text: deltaText }) + } + continue + } + if ( eventType === 'response.output_text.delta' || eventType === 'response.output_json.delta' @@ -508,7 +536,7 @@ export function createReadableStreamFromResponses( if (deltaText.length > 0) { fullContent += deltaText - controller.enqueue(encoder.encode(deltaText)) + controller.enqueue({ type: 'text_delta', text: deltaText, turn: 'final' }) } } @@ -523,7 +551,7 @@ export function createReadableStreamFromResponses( } if (onComplete) { - onComplete(fullContent, finalUsage) + onComplete(fullContent, finalUsage, fullThinking || undefined) } controller.close() diff --git a/apps/sim/providers/openrouter/index.ts b/apps/sim/providers/openrouter/index.ts index 8821483fa4e..0508b28c2ce 100644 --- a/apps/sim/providers/openrouter/index.ts +++ b/apps/sim/providers/openrouter/index.ts @@ -169,6 +169,7 @@ export const openRouterProvider: ProviderConfig = { timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { output.content = content @@ -471,6 +472,7 @@ export const openRouterProvider: ProviderConfig = { }, toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { output.content = content diff --git a/apps/sim/providers/openrouter/utils.ts b/apps/sim/providers/openrouter/utils.ts index 51637f5148d..2f8a7850fc9 100644 --- a/apps/sim/providers/openrouter/utils.ts +++ b/apps/sim/providers/openrouter/utils.ts @@ -2,7 +2,9 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' +import { checkForForcedToolUsageOpenAI } from '@/providers/utils' const logger = createLogger('OpenRouterUtils') @@ -86,9 +88,14 @@ export async function supportsNativeStructuredOutputs(modelId: string): Promise< export function createReadableStreamFromOpenAIStream( openaiStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(openaiStream, 'OpenRouter', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(openaiStream, { + providerName: 'OpenRouter', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } export function checkForForcedToolUsage( diff --git a/apps/sim/providers/sakana/index.ts b/apps/sim/providers/sakana/index.ts index 3988d3e96db..39e61746ad1 100644 --- a/apps/sim/providers/sakana/index.ts +++ b/apps/sim/providers/sakana/index.ts @@ -150,6 +150,7 @@ export const sakanaProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromSakanaStream(streamResponse as any, (content, usage) => { output.content = content @@ -516,6 +517,7 @@ export const sakanaProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromSakanaStream(streamResponse as any, (content, usage) => { output.content = content diff --git a/apps/sim/providers/sakana/utils.ts b/apps/sim/providers/sakana/utils.ts index ede98301a12..ba8b42329cf 100644 --- a/apps/sim/providers/sakana/utils.ts +++ b/apps/sim/providers/sakana/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from a Sakana AI streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Sakana AI streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromSakanaStream( sakanaStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(sakanaStream, 'Sakana', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(sakanaStream, { + providerName: 'Sakana', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/providers/step-10-smokes.test.ts b/apps/sim/providers/step-10-smokes.test.ts new file mode 100644 index 00000000000..cb1ac833f9a --- /dev/null +++ b/apps/sim/providers/step-10-smokes.test.ts @@ -0,0 +1,67 @@ +/** + * @vitest-environment node + * + * Step 10 smoke: Anthropic thinking+tool fixture, openai-compat thinking, + * and a non-thinking text-only stream — capability-honest expectations. + */ +import { describe, expect, it } from 'vitest' +import { anthropicThinkingTextToolStreamEvents } from '@/providers/__fixtures__/anthropic' +import { + openaiCompatReasoningAndTextChunks, + openaiCompatTextOnlyChunks, +} from '@/providers/__fixtures__/openai-compat' +import { createReadableStreamFromAnthropicStream } from '@/providers/anthropic/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' + +async function collectEvents( + stream: ReadableStream +): Promise { + const events: AgentStreamEvent[] = [] + const reader = stream.getReader() + while (true) { + const { done, value } = await reader.read() + if (done) break + events.push(value) + } + return events +} + +describe('Step 10 provider smokes', () => { + it('Anthropic fixture emits thinking then text (tools ignored on simple util)', async () => { + const stream = createReadableStreamFromAnthropicStream( + (async function* () { + yield* anthropicThinkingTextToolStreamEvents as any + })() + ) + const events = await collectEvents(stream) + expect(events.some((e) => e.type === 'thinking_delta')).toBe(true) + expect(events.some((e) => e.type === 'text_delta')).toBe(true) + // Simple Anthropic util does not emit tool_call_* (loop does). + expect(events.some((e) => e.type === 'tool_call_start')).toBe(false) + }) + + it('openai-compat reasoning model emits thinking_delta', async () => { + const stream = createOpenAICompatibleAgentEventStream( + (async function* () { + yield* openaiCompatReasoningAndTextChunks as any + })(), + { providerName: 'DeepSeek' } + ) + const events = await collectEvents(stream) + expect(events.filter((e) => e.type === 'thinking_delta').length).toBeGreaterThan(0) + expect(events.some((e) => e.type === 'text_delta')).toBe(true) + }) + + it('openai-compat non-thinking model stays text-only', async () => { + const stream = createOpenAICompatibleAgentEventStream( + (async function* () { + yield* openaiCompatTextOnlyChunks as any + })(), + { providerName: 'OpenAI' } + ) + const events = await collectEvents(stream) + expect(events.every((e) => e.type === 'text_delta')).toBe(true) + expect(events.some((e) => e.type === 'thinking_delta')).toBe(false) + }) +}) diff --git a/apps/sim/providers/stream-events.test.ts b/apps/sim/providers/stream-events.test.ts new file mode 100644 index 00000000000..477ed4abc42 --- /dev/null +++ b/apps/sim/providers/stream-events.test.ts @@ -0,0 +1,110 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + createAgentEventReadableStream, + isAgentEventReadableStream, + isAgentStreamEvent, + isAgentStreamFormat, + isTextDeltaTurn, + isToolCallEndStatus, + type AgentStreamEvent, +} from '@/providers/stream-events' + +describe('stream-events contract', () => { + describe('isAgentStreamFormat', () => { + it('accepts text and agent-events-v1 only', () => { + expect(isAgentStreamFormat('text')).toBe(true) + expect(isAgentStreamFormat('agent-events-v1')).toBe(true) + expect(isAgentStreamFormat('ndjson')).toBe(false) + expect(isAgentStreamFormat(undefined)).toBe(false) + }) + }) + + describe('isAgentStreamEvent', () => { + it('accepts valid event variants', () => { + const events: AgentStreamEvent[] = [ + { type: 'text_delta', text: 'hi' }, + { type: 'text_delta', text: 'mid', turn: 'intermediate' }, + { type: 'text_delta', text: 'bye', turn: 'final' }, + { type: 'thinking_delta', text: 'hmm' }, + { type: 'tool_call_start', id: 't1', name: 'search' }, + { type: 'tool_call_end', id: 't1', name: 'search', status: 'success' }, + { type: 'tool_call_end', id: 't1', name: 'search', status: 'error' }, + { type: 'tool_call_end', id: 't1', name: 'search', status: 'cancelled' }, + ] + + for (const event of events) { + expect(isAgentStreamEvent(event)).toBe(true) + } + }) + + it('rejects malformed events', () => { + expect(isAgentStreamEvent(null)).toBe(false) + expect(isAgentStreamEvent({ type: 'error', message: 'x' })).toBe(false) + expect(isAgentStreamEvent({ type: 'text_delta' })).toBe(false) + expect(isAgentStreamEvent({ type: 'text_delta', text: 'x', turn: 'other' })).toBe(false) + expect(isAgentStreamEvent({ type: 'tool_call_start', id: 't1' })).toBe(false) + expect( + isAgentStreamEvent({ type: 'tool_call_end', id: 't1', name: 'search', status: 'ok' }) + ).toBe(false) + }) + }) + + describe('status and turn guards', () => { + it('validates tool end statuses and text turns', () => { + expect(isToolCallEndStatus('success')).toBe(true) + expect(isToolCallEndStatus('failed')).toBe(false) + expect(isTextDeltaTurn('final')).toBe(true) + expect(isTextDeltaTurn('first')).toBe(false) + }) + }) + + describe('isAgentEventReadableStream', () => { + it('requires streamFormat agent-events-v1 and does not sniff', () => { + const stream = new ReadableStream() + expect(isAgentEventReadableStream(stream, 'agent-events-v1')).toBe(true) + expect(isAgentEventReadableStream(stream, 'text')).toBe(false) + }) + }) + + describe('createAgentEventReadableStream', () => { + it('enqueues events in order and closes', async () => { + const events: AgentStreamEvent[] = [ + { type: 'thinking_delta', text: 'a' }, + { type: 'text_delta', text: 'b', turn: 'final' }, + { type: 'tool_call_start', id: '1', name: 'lookup' }, + { type: 'tool_call_end', id: '1', name: 'lookup', status: 'success' }, + ] + + const stream = createAgentEventReadableStream(events) + const reader = stream.getReader() + const received: AgentStreamEvent[] = [] + + while (true) { + const { done, value } = await reader.read() + if (done) break + received.push(value) + } + + expect(received).toEqual(events) + }) + + it('errors when an invalid object is yielded', async () => { + async function* bad() { + yield { type: 'text_delta', text: 'ok' } as AgentStreamEvent + yield { type: 'nope' } as unknown as AgentStreamEvent + } + + const stream = createAgentEventReadableStream(bad()) + const reader = stream.getReader() + + await expect(reader.read()).resolves.toMatchObject({ + done: false, + value: { type: 'text_delta', text: 'ok' }, + }) + await expect(reader.read()).rejects.toThrow(/Invalid AgentStreamEvent/) + }) + }) +}) diff --git a/apps/sim/providers/stream-events.ts b/apps/sim/providers/stream-events.ts new file mode 100644 index 00000000000..08d73fbe6c9 --- /dev/null +++ b/apps/sim/providers/stream-events.ts @@ -0,0 +1,142 @@ +/** + * Canonical agent stream event contract (provider → executor). + * + * Providers with `streamFormat: 'agent-events-v1'` return a + * `ReadableStream` (in-process object stream — not NDJSON). + * Legacy providers keep `streamFormat: 'text'` and `ReadableStream`. + * + * The executor is the only consumer that projects these events into a text + * stream + optional sink; downstream SSE/chat must not re-parse provider streams. + */ + +export const AGENT_STREAM_FORMATS = ['text', 'agent-events-v1'] as const + +export type AgentStreamFormat = (typeof AGENT_STREAM_FORMATS)[number] + +export const AGENT_STREAM_EVENT_TYPES = [ + 'text_delta', + 'thinking_delta', + 'tool_call_start', + 'tool_call_end', +] as const + +export type AgentStreamEventType = (typeof AGENT_STREAM_EVENT_TYPES)[number] + +export const TOOL_CALL_END_STATUSES = ['success', 'error', 'cancelled'] as const + +export type ToolCallEndStatus = (typeof TOOL_CALL_END_STATUSES)[number] + +export type TextDeltaTurn = 'intermediate' | 'final' + +export type AgentStreamEvent = + | { type: 'text_delta'; text: string; turn?: TextDeltaTurn } + | { type: 'thinking_delta'; text: string } + | { type: 'tool_call_start'; id: string; name: string } + | { + type: 'tool_call_end' + id: string + name: string + status: ToolCallEndStatus + } + +/** Optional sink the executor pump pushes the full ordered timeline into. */ +export type AgentStreamSink = { + onEvent: (event: AgentStreamEvent) => void | Promise +} + +export type UnsubscribeAgentStreamSink = () => void + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +export function isAgentStreamFormat(value: unknown): value is AgentStreamFormat { + return value === 'text' || value === 'agent-events-v1' +} + +export function isToolCallEndStatus(value: unknown): value is ToolCallEndStatus { + return value === 'success' || value === 'error' || value === 'cancelled' +} + +export function isTextDeltaTurn(value: unknown): value is TextDeltaTurn { + return value === 'intermediate' || value === 'final' +} + +export function isAgentStreamEvent(value: unknown): value is AgentStreamEvent { + if (!isRecord(value) || typeof value.type !== 'string') { + return false + } + + switch (value.type) { + case 'text_delta': + return ( + typeof value.text === 'string' && + (value.turn === undefined || isTextDeltaTurn(value.turn)) + ) + case 'thinking_delta': + return typeof value.text === 'string' + case 'tool_call_start': + return typeof value.id === 'string' && typeof value.name === 'string' + case 'tool_call_end': + return ( + typeof value.id === 'string' && + typeof value.name === 'string' && + isToolCallEndStatus(value.status) + ) + default: + return false + } +} + +/** + * Narrows a {@link ReadableStream} to an agent-events object stream. + * Callers must also check `streamFormat === 'agent-events-v1'` on the envelope — + * do not sniff chunk types from the stream. + */ +export function isAgentEventReadableStream( + stream: ReadableStream, + streamFormat: AgentStreamFormat +): stream is ReadableStream { + return streamFormat === 'agent-events-v1' +} + +/** + * Builds a {@link ReadableStream} that enqueues the given agent events in order. + * Intended for tests and provider adapters that already have an event sequence. + */ +export function createAgentEventReadableStream( + events: Iterable | AsyncIterable +): ReadableStream { + const iterator = + Symbol.asyncIterator in events + ? events[Symbol.asyncIterator]() + : (async function* () { + for (const event of events as Iterable) { + yield event + } + })() + + return new ReadableStream({ + async pull(controller) { + try { + const { done, value } = await iterator.next() + if (done) { + controller.close() + return + } + if (!isAgentStreamEvent(value)) { + controller.error(new Error('Invalid AgentStreamEvent enqueued on object stream')) + return + } + controller.enqueue(value) + } catch (error) { + controller.error(error) + } + }, + async cancel(reason) { + if (typeof iterator.return === 'function') { + await iterator.return(reason) + } + }, + }) +} diff --git a/apps/sim/providers/stream-pump.test.ts b/apps/sim/providers/stream-pump.test.ts new file mode 100644 index 00000000000..98fd5bd7ad2 --- /dev/null +++ b/apps/sim/providers/stream-pump.test.ts @@ -0,0 +1,494 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it, vi } from 'vitest' +import { + createAgentEventReadableStream, + type AgentStreamEvent, + type AgentStreamSink, +} from '@/providers/stream-events' +import { createAgentStreamPump, DEFAULT_MAX_THINKING_BYTES } from '@/providers/stream-pump' + +async function readAllText(stream: ReadableStream | null): Promise { + if (!stream) return '' + const reader = stream.getReader() + const decoder = new TextDecoder() + let text = '' + while (true) { + const { done, value } = await reader.read() + if (done) break + text += decoder.decode(value, { stream: true }) + } + text += decoder.decode() + return text +} + +function collectingSink() { + const events: AgentStreamEvent[] = [] + const sink: AgentStreamSink = { + onEvent: async (event) => { + events.push(event) + }, + } + return { sink, events } +} + +describe('createAgentStreamPump', () => { + it('projects agent-events to final-turn answer text and full sink timeline', async () => { + const events: AgentStreamEvent[] = [ + { type: 'thinking_delta', text: 'plan ' }, + { type: 'thinking_delta', text: 'it' }, + { type: 'text_delta', text: 'Looking up…', turn: 'intermediate' }, + { type: 'tool_call_start', id: '1', name: 'search' }, + { type: 'tool_call_end', id: '1', name: 'search', status: 'success' }, + { type: 'text_delta', text: 'Done.', turn: 'final' }, + ] + + const pump = createAgentStreamPump({ + source: createAgentEventReadableStream(events), + streamFormat: 'agent-events-v1', + }) + const { sink, events: seen } = collectingSink() + pump.subscribe(sink) + + const textPromise = readAllText(pump.textStream) + const result = await pump.run() + const text = await textPromise + + expect(result.answerText).toBe('Done.') + expect(result.fullyDrained).toBe(true) + expect(text).toBe('Done.') + expect(seen).toEqual(events) + }) + + it('treats legacy text streams as final-turn answer bytes and sink text_delta', async () => { + const encoder = new TextEncoder() + const source = new ReadableStream({ + start(controller) { + controller.enqueue(encoder.encode('Hel')) + controller.enqueue(encoder.encode('lo')) + controller.close() + }, + }) + + const pump = createAgentStreamPump({ source, streamFormat: 'text' }) + const { sink, events } = collectingSink() + pump.subscribe(sink) + + const textPromise = readAllText(pump.textStream) + const result = await pump.run() + + expect(result.answerText).toBe('Hello') + expect(await textPromise).toBe('Hello') + expect(events).toEqual([ + { type: 'text_delta', text: 'Hel', turn: 'final' }, + { type: 'text_delta', text: 'lo', turn: 'final' }, + ]) + }) + + it('handles UTF-8 characters split across byte chunks', async () => { + // € in UTF-8 is E2 82 AC — split across two chunks + const source = new ReadableStream({ + start(controller) { + controller.enqueue(Uint8Array.of(0xe2, 0x82)) + controller.enqueue(Uint8Array.of(0xac, 0x20, 0x6f, 0x6b)) + controller.close() + }, + }) + + const pump = createAgentStreamPump({ source, streamFormat: 'text' }) + const textPromise = readAllText(pump.textStream) + const result = await pump.run() + + expect(result.answerText).toBe('€ ok') + expect(await textPromise).toBe('€ ok') + }) + + it('drops thinking when no sink is subscribed (no unbounded thinking buffer)', async () => { + const hugeThinking = 'x'.repeat(50_000) + const pump = createAgentStreamPump({ + source: createAgentEventReadableStream([ + { type: 'thinking_delta', text: hugeThinking }, + { type: 'text_delta', text: 'hi', turn: 'final' }, + ]), + streamFormat: 'agent-events-v1', + }) + + const textPromise = readAllText(pump.textStream) + const result = await pump.run() + + expect(result.answerText).toBe('hi') + expect(await textPromise).toBe('hi') + }) + + it('caps thinking forwarded to sinks', async () => { + const pump = createAgentStreamPump({ + source: createAgentEventReadableStream([ + { type: 'thinking_delta', text: 'abcdef' }, + { type: 'thinking_delta', text: 'ghijkl' }, + { type: 'text_delta', text: 'out', turn: 'final' }, + ]), + streamFormat: 'agent-events-v1', + maxThinkingBytes: 4, + }) + const { sink, events } = collectingSink() + pump.subscribe(sink) + + const textPromise = readAllText(pump.textStream) + await pump.run() + await textPromise + + expect(events.filter((e) => e.type === 'thinking_delta')).toEqual([ + { type: 'thinking_delta', text: 'abcd' }, + ]) + expect(DEFAULT_MAX_THINKING_BYTES).toBeGreaterThan(0) + }) + + it('allows late subscribers to receive only future events', async () => { + let pullCount = 0 + let releaseFirst!: () => void + const firstGate = new Promise((resolve) => { + releaseFirst = resolve + }) + + const source = new ReadableStream({ + async pull(controller) { + pullCount += 1 + if (pullCount === 1) { + controller.enqueue({ type: 'thinking_delta', text: 'early' }) + await firstGate + return + } + if (pullCount === 2) { + controller.enqueue({ type: 'text_delta', text: 'late', turn: 'final' }) + return + } + controller.close() + }, + }) + + const pump = createAgentStreamPump({ source, streamFormat: 'agent-events-v1' }) + const early = collectingSink() + pump.subscribe(early.sink) + + const runPromise = pump.run() + const textPromise = readAllText(pump.textStream) + + // Wait until first event has been dispatched to early sink + await vi.waitFor(() => { + expect(early.events.length).toBe(1) + }) + + const late = collectingSink() + pump.subscribe(late.sink) + releaseFirst() + + const result = await runPromise + await textPromise + + expect(early.events.map((e) => e.type)).toEqual(['thinking_delta', 'text_delta']) + expect(late.events.map((e) => e.type)).toEqual(['text_delta']) + expect(result.answerText).toBe('late') + }) + + it('unsubscribe mid-stream detaches without failing the pump', async () => { + const seen: AgentStreamEvent[] = [] + const sink: AgentStreamSink = { + onEvent: async (event) => { + seen.push(event) + if (event.type === 'tool_call_start') { + unsubscribe() + } + }, + } + + const pump = createAgentStreamPump({ + source: createAgentEventReadableStream([ + { type: 'tool_call_start', id: '1', name: 'x' }, + { type: 'tool_call_end', id: '1', name: 'x', status: 'success' }, + { type: 'text_delta', text: 'ok', turn: 'final' }, + ]), + streamFormat: 'agent-events-v1', + }) + const unsubscribe = pump.subscribe(sink) + + const textPromise = readAllText(pump.textStream) + const result = await pump.run() + await textPromise + + expect(result.answerText).toBe('ok') + expect(result.fullyDrained).toBe(true) + expect(seen.map((e) => e.type)).toEqual(['tool_call_start']) + }) + + it('sinkMode skips text stream and still drains', async () => { + const pump = createAgentStreamPump({ + source: createAgentEventReadableStream([ + { type: 'text_delta', text: 'only-sink', turn: 'final' }, + ]), + streamFormat: 'agent-events-v1', + sinkMode: true, + }) + expect(pump.textStream).toBeNull() + + const { sink, events } = collectingSink() + pump.subscribe(sink) + const result = await pump.run() + + expect(result.answerText).toBe('only-sink') + expect(events).toEqual([{ type: 'text_delta', text: 'only-sink', turn: 'final' }]) + }) + + it('awaits each sink before pulling the next event (per-event sync)', async () => { + let release!: () => void + const gate = new Promise((resolve) => { + release = resolve + }) + let inFlight = 0 + let maxInFlight = 0 + + const sink: AgentStreamSink = { + onEvent: async () => { + inFlight += 1 + maxInFlight = Math.max(maxInFlight, inFlight) + await gate + inFlight -= 1 + }, + } + + const pump = createAgentStreamPump({ + source: createAgentEventReadableStream([ + { type: 'text_delta', text: '1', turn: 'final' }, + { type: 'text_delta', text: '2', turn: 'final' }, + { type: 'text_delta', text: '3', turn: 'final' }, + ]), + streamFormat: 'agent-events-v1', + sinkMode: true, + }) + pump.subscribe(sink) + + const runPromise = pump.run() + await vi.waitFor(() => { + expect(maxInFlight).toBe(1) + }) + release() + await runPromise + expect(maxInFlight).toBe(1) + }) + + it('fails the pump on invalid agent-events chunks (no soft success)', async () => { + const source = new ReadableStream({ + start(controller) { + controller.enqueue({ type: 'nope' }) + controller.close() + }, + }) + + const pump = createAgentStreamPump({ + source, + streamFormat: 'agent-events-v1', + sinkMode: true, + }) + + await expect(pump.run()).rejects.toThrow(/Invalid AgentStreamEvent/) + }) + + it('fails the pump when the provider source errors', async () => { + const source = new ReadableStream({ + start(controller) { + controller.error(new Error('provider down')) + }, + }) + + const pump = createAgentStreamPump({ + source, + streamFormat: 'agent-events-v1', + sinkMode: true, + }) + + await expect(pump.run()).rejects.toThrow(/provider down/) + }) + + it('maps abort to cancelled result and distinguishes timeout reason', async () => { + const controller = new AbortController() + let pullCount = 0 + const source = new ReadableStream({ + async pull(ctrl) { + pullCount += 1 + if (pullCount === 1) { + ctrl.enqueue({ type: 'thinking_delta', text: '…' }) + controller.abort('timeout') + return + } + ctrl.close() + }, + }) + + const pump = createAgentStreamPump({ + source, + streamFormat: 'agent-events-v1', + sinkMode: true, + abortSignal: controller.signal, + }) + + const result = await pump.run() + expect(result.cancelled).toBe(true) + expect(result.cancelReason).toBe('timeout') + expect(result.fullyDrained).toBe(false) + }) + + it('does not start until run() — subscribe-before-pull', async () => { + let pulled = false + const source = new ReadableStream({ + pull(controller) { + pulled = true + controller.enqueue({ type: 'text_delta', text: 'x', turn: 'final' }) + controller.close() + }, + }) + + const pump = createAgentStreamPump({ + source, + streamFormat: 'agent-events-v1', + sinkMode: true, + }) + const { sink, events } = collectingSink() + pump.subscribe(sink) + + expect(pulled).toBe(false) + await pump.run() + expect(pulled).toBe(true) + expect(events).toHaveLength(1) + }) + + it('rejects double run()', async () => { + const pump = createAgentStreamPump({ + source: createAgentEventReadableStream([{ type: 'text_delta', text: 'a', turn: 'final' }]), + streamFormat: 'agent-events-v1', + sinkMode: true, + }) + await pump.run() + await expect(pump.run()).rejects.toThrow(/already started/) + }) + + it('detaches a sink that throws without failing the pump', async () => { + const bad: AgentStreamSink = { + onEvent: async () => { + throw new Error('sink exploded') + }, + } + const good = collectingSink() + + const pump = createAgentStreamPump({ + source: createAgentEventReadableStream([ + { type: 'text_delta', text: 'a', turn: 'final' }, + { type: 'text_delta', text: 'b', turn: 'final' }, + ]), + streamFormat: 'agent-events-v1', + sinkMode: true, + }) + pump.subscribe(bad) + pump.subscribe(good.sink) + + const result = await pump.run() + expect(result.answerText).toBe('ab') + expect(good.events.length).toBe(2) + }) + + it('synthesizes tool_call_end cancelled for open tools on abort', async () => { + const controller = new AbortController() + const source = new ReadableStream({ + start(c) { + c.enqueue({ type: 'tool_call_start', id: 't1', name: 'search' }) + c.enqueue({ type: 'thinking_delta', text: 'working' }) + // Never emits tool_call_end — abort mid-flight. + setTimeout(() => controller.abort('user'), 5) + }, + }) + + const pump = createAgentStreamPump({ + source, + streamFormat: 'agent-events-v1', + sinkMode: true, + abortSignal: controller.signal, + }) + const { sink, events } = collectingSink() + pump.subscribe(sink) + + const result = await pump.run() + expect(result.cancelled).toBe(true) + expect(events).toContainEqual({ + type: 'tool_call_end', + id: 't1', + name: 'search', + status: 'cancelled', + }) + }) + + it('synthesizes tool_call_end error for open tools on source failure', async () => { + let pulled = 0 + const source = new ReadableStream({ + pull(c) { + pulled += 1 + if (pulled === 1) { + c.enqueue({ type: 'tool_call_start', id: 't1', name: 'search' }) + return + } + c.error(new Error('provider reset')) + }, + }) + + const pump = createAgentStreamPump({ + source, + streamFormat: 'agent-events-v1', + sinkMode: true, + }) + const { sink, events } = collectingSink() + pump.subscribe(sink) + + await expect(pump.run()).rejects.toThrow('provider reset') + expect(events).toContainEqual({ + type: 'tool_call_start', + id: 't1', + name: 'search', + }) + expect(events).toContainEqual({ + type: 'tool_call_end', + id: 't1', + name: 'search', + status: 'error', + }) + }) +}) + +describe('projectStreamingExecutionToByteStream', () => { + it('projects agent-events final-turn text to UTF-8 bytes', async () => { + const { projectStreamingExecutionToByteStream } = await import('@/providers/stream-pump') + const byteStream = projectStreamingExecutionToByteStream({ + stream: createAgentEventReadableStream([ + { type: 'thinking_delta', text: 'secret' }, + { type: 'text_delta', text: 'Looking…', turn: 'intermediate' }, + { type: 'text_delta', text: 'Answer', turn: 'final' }, + ]), + streamFormat: 'agent-events-v1', + }) + + expect(await readAllText(byteStream)).toBe('Answer') + }) + + it('passes through legacy text byte streams unchanged', async () => { + const { projectStreamingExecutionToByteStream } = await import('@/providers/stream-pump') + const encoder = new TextEncoder() + const source = new ReadableStream({ + start(c) { + c.enqueue(encoder.encode('raw')) + c.close() + }, + }) + const byteStream = projectStreamingExecutionToByteStream({ + stream: source, + streamFormat: 'text', + }) + expect(await readAllText(byteStream)).toBe('raw') + }) +}) diff --git a/apps/sim/providers/stream-pump.ts b/apps/sim/providers/stream-pump.ts new file mode 100644 index 00000000000..21cb0194d28 --- /dev/null +++ b/apps/sim/providers/stream-pump.ts @@ -0,0 +1,424 @@ +/** + * Executor-facing agent stream pump (Step 2). + * + * Owns a single drain of a provider {@link StreamingExecution} source and: + * - projects final-turn answer text to an optional legacy byte stream + * - pushes the full ordered timeline (including text) to optional sinks + * - awaits each sink per event (simple backpressure; SSE enqueues are cheap) + * + * Wired into {@link BlockExecutor} `handleStreamingExecution` (Step 3). + */ + +import { + type AgentStreamEvent, + type AgentStreamFormat, + type AgentStreamSink, + type UnsubscribeAgentStreamSink, + isAgentStreamEvent, +} from '@/providers/stream-events' + +/** Default cap on thinking text forwarded to sinks (UTF-16 code units via `.length`). */ +export const DEFAULT_MAX_THINKING_BYTES = 512 * 1024 + +export type AgentStreamPumpCancelReason = 'user' | 'timeout' | 'unknown' + +export interface CreateAgentStreamPumpOptions { + source: ReadableStream + streamFormat: AgentStreamFormat + /** + * When true, no legacy text {@link ReadableStream} is created — upgraded + * consumers read only the sink. Avoids unbounded buffering into an unread stream. + */ + sinkMode?: boolean + maxThinkingBytes?: number + abortSignal?: AbortSignal +} + +export interface AgentStreamPumpResult { + /** Final-turn answer text only (`turn: 'intermediate'` excluded). */ + answerText: string + fullyDrained: boolean + cancelled: boolean + cancelReason?: AgentStreamPumpCancelReason +} + +export interface AgentStreamPump { + subscribe: (sink: AgentStreamSink) => UnsubscribeAgentStreamSink + /** Final-turn answer bytes, or `null` when {@link CreateAgentStreamPumpOptions.sinkMode}. */ + readonly textStream: ReadableStream | null + /** + * Starts draining the provider source. Call only after the synchronous + * subscribe window (e.g. after `onStream` returns). + */ + run: () => Promise +} + +interface SinkState { + sink: AgentStreamSink + detached: boolean +} + +function resolveCancelReason(signal: AbortSignal): AgentStreamPumpCancelReason { + const reason = signal.reason + if (reason === 'timeout' || reason === 'user') { + return reason + } + if (typeof reason === 'string') { + const lower = reason.toLowerCase() + if (lower.includes('timeout')) return 'timeout' + if (lower.includes('abort') || lower.includes('cancel') || lower.includes('user')) { + return 'user' + } + } + if (reason && typeof reason === 'object' && 'name' in reason) { + const name = String((reason as { name?: unknown }).name) + if (name === 'TimeoutError') return 'timeout' + } + return 'unknown' +} + +function isFinalTurnText(event: Extract): boolean { + return event.turn !== 'intermediate' +} + +/** + * Creates a pump over a provider stream. Subscribe synchronously before {@link AgentStreamPump.run}. + */ +export function createAgentStreamPump(options: CreateAgentStreamPumpOptions): AgentStreamPump { + const { + source, + streamFormat, + sinkMode = false, + maxThinkingBytes = DEFAULT_MAX_THINKING_BYTES, + abortSignal, + } = options + + const sinks = new Map() + let started = false + let closedTextStream = false + + let answerText = '' + let thinkingBytesForwarded = 0 + + let textController: ReadableStreamDefaultController | null = null + let textBackpressureWait: Promise | null = null + let resolveTextBackpressure: (() => void) | null = null + const textEncoder = new TextEncoder() + + const textStream = sinkMode + ? null + : new ReadableStream({ + start(controller) { + textController = controller + }, + pull() { + if (resolveTextBackpressure) { + const resolve = resolveTextBackpressure + resolveTextBackpressure = null + textBackpressureWait = null + resolve() + } + }, + cancel() { + closedTextStream = true + textController = null + if (resolveTextBackpressure) { + const resolve = resolveTextBackpressure + resolveTextBackpressure = null + textBackpressureWait = null + resolve() + } + }, + }) + + function subscribe(sink: AgentStreamSink): UnsubscribeAgentStreamSink { + if (sinks.has(sink)) { + return () => { + detachSink(sink) + } + } + + sinks.set(sink, { sink, detached: false }) + return () => detachSink(sink) + } + + function detachSink(sink: AgentStreamSink): void { + const state = sinks.get(sink) + if (!state || state.detached) return + state.detached = true + sinks.delete(sink) + } + + async function dispatchToSinks(event: AgentStreamEvent): Promise { + const active = [...sinks.values()].filter((s) => !s.detached) + if (active.length === 0) return + + await Promise.all( + active.map(async (state) => { + if (state.detached) return + try { + await state.sink.onEvent(event) + } catch { + detachSink(state.sink) + } + }) + ) + } + + async function enqueueAnswerText(text: string): Promise { + if (!text) return + + answerText += text + + if (sinkMode || closedTextStream || !textController) return + + const chunk = textEncoder.encode(text) + + while (!closedTextStream && textController) { + const desired = textController.desiredSize + if (desired === null) return + if (desired > 0) { + try { + textController.enqueue(chunk) + } catch { + closedTextStream = true + textController = null + } + return + } + if (!textBackpressureWait) { + textBackpressureWait = new Promise((resolve) => { + resolveTextBackpressure = resolve + }) + } + await textBackpressureWait + } + } + + function closeTextStream(error?: unknown): void { + if (sinkMode || closedTextStream) return + closedTextStream = true + try { + if (error !== undefined && textController) { + textController.error(error) + } else { + textController?.close() + } + } catch { + // already closed + } + textController = null + if (resolveTextBackpressure) { + resolveTextBackpressure() + resolveTextBackpressure = null + textBackpressureWait = null + } + } + + /** Open tool_call_start ids → names; settled on abort/error so sinks never hang. */ + const openTools = new Map() + + async function settleOpenTools(status: 'cancelled' | 'error'): Promise { + if (openTools.size === 0) return + const pending = [...openTools.entries()] + openTools.clear() + for (const [id, name] of pending) { + await dispatchToSinks({ type: 'tool_call_end', id, name, status }) + } + } + + async function handleEvent(event: AgentStreamEvent): Promise { + if (event.type === 'thinking_delta') { + const remaining = Math.max(0, maxThinkingBytes - thinkingBytesForwarded) + if (remaining <= 0) return + const forwarded = + event.text.length > remaining ? event.text.slice(0, remaining) : event.text + thinkingBytesForwarded += forwarded.length + if (forwarded.length > 0) { + await dispatchToSinks({ type: 'thinking_delta', text: forwarded }) + } + return + } + + if (event.type === 'text_delta') { + await dispatchToSinks(event) + if (isFinalTurnText(event)) { + await enqueueAnswerText(event.text) + } + return + } + + if (event.type === 'tool_call_start') { + openTools.set(event.id, event.name) + await dispatchToSinks(event) + return + } + + if (event.type === 'tool_call_end') { + openTools.delete(event.id) + await dispatchToSinks(event) + return + } + + await dispatchToSinks(event) + } + + async function run(): Promise { + if (started) { + throw new Error('Agent stream pump already started') + } + started = true + + let cancelled = false + let cancelReason: AgentStreamPumpCancelReason | undefined + let fullyDrained = false + let drainError: unknown + + const reader = source.getReader() + const decoder = new TextDecoder() + + const onAbort = () => { + cancelled = true + if (abortSignal) { + cancelReason = resolveCancelReason(abortSignal) + } else { + cancelReason = 'unknown' + } + void reader.cancel(abortSignal?.reason ?? 'aborted') + } + + if (abortSignal) { + if (abortSignal.aborted) { + onAbort() + } else { + abortSignal.addEventListener('abort', onAbort, { once: true }) + } + } + + try { + while (!cancelled) { + const { done, value } = await reader.read() + if (done) { + if (streamFormat === 'text') { + const tail = decoder.decode() + if (tail) { + await handleEvent({ type: 'text_delta', text: tail, turn: 'final' }) + } + } + fullyDrained = true + break + } + + if (streamFormat === 'text') { + if (!(value instanceof Uint8Array)) { + throw new Error('text streamFormat expected Uint8Array chunks') + } + const text = decoder.decode(value, { stream: true }) + if (text) { + await handleEvent({ type: 'text_delta', text, turn: 'final' }) + } + continue + } + + if (!isAgentStreamEvent(value)) { + throw new Error('Invalid AgentStreamEvent on agent-events-v1 stream') + } + await handleEvent(value) + } + } catch (error) { + drainError = error + } finally { + abortSignal?.removeEventListener('abort', onAbort) + try { + reader.releaseLock() + } catch { + // ignore + } + } + + // Synthesize tool ends — enqueue-then-error in provider loops can drop + // settlement frames (WHATWG resets the queue on error). + if (cancelled) { + await settleOpenTools('cancelled') + } else if (drainError) { + await settleOpenTools('error') + } + + if (drainError) { + closeTextStream(drainError) + throw drainError instanceof Error ? drainError : new Error(String(drainError)) + } + + if (cancelled) { + closeTextStream() + return { + answerText, + fullyDrained: false, + cancelled: true, + cancelReason: cancelReason ?? 'unknown', + } + } + + closeTextStream() + return { + answerText, + fullyDrained, + cancelled: false, + } + } + + return { + subscribe, + textStream, + run, + } +} + +/** + * Project a {@link StreamingExecution} to a byte stream suitable for an HTTP + * `Response` body. Agent-events object streams are pumped to final-turn answer + * bytes; legacy `text` streams pass through unchanged. + */ +export function projectStreamingExecutionToByteStream(streamingExec: { + stream: ReadableStream + streamFormat?: AgentStreamFormat +}): ReadableStream { + const streamFormat = streamingExec.streamFormat ?? 'text' + if (streamFormat === 'text') { + return streamingExec.stream as ReadableStream + } + + const pump = createAgentStreamPump({ + source: streamingExec.stream, + streamFormat, + }) + const textStream = pump.textStream + if (!textStream) { + throw new Error('Agent stream pump expected a text projection stream') + } + + return new ReadableStream({ + async start(controller) { + const runPromise = pump.run() + const reader = textStream.getReader() + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + controller.enqueue(value) + } + await runPromise + controller.close() + } catch (error) { + try { + controller.error(error instanceof Error ? error : new Error(String(error))) + } catch { + // already closed/errored + } + } + }, + cancel(reason) { + void textStream.cancel(reason) + }, + }) +} diff --git a/apps/sim/providers/streaming-execution.test.ts b/apps/sim/providers/streaming-execution.test.ts index 25e7708b1f0..f5d4c678932 100644 --- a/apps/sim/providers/streaming-execution.test.ts +++ b/apps/sim/providers/streaming-execution.test.ts @@ -3,6 +3,7 @@ */ import { describe, expect, it, vi } from 'vitest' import type { NormalizedBlockOutput } from '@/executor/types' +import { createAgentEventReadableStream } from '@/providers/stream-events' import { createStreamingExecution } from '@/providers/streaming-execution' /** @@ -207,4 +208,42 @@ describe('createStreamingExecution', () => { vi.restoreAllMocks() }) + + it('defaults streamFormat to text and can attach agent-events-v1 object streams', async () => { + const textResult = createStreamingExecution({ + model: 'm', + providerStartTime, + providerStartTimeISO, + timing: { kind: 'simple', segmentName: 'm' }, + initialTokens: { input: 0, output: 0, total: 0 }, + initialCost: { input: 0, output: 0, total: 0 }, + createStream: () => new ReadableStream(), + }) + expect(textResult.streamFormat).toBe('text') + + const events = [ + { type: 'thinking_delta' as const, text: 'reason' }, + { type: 'text_delta' as const, text: 'answer', turn: 'final' as const }, + ] + const eventResult = createStreamingExecution({ + model: 'm', + providerStartTime, + providerStartTimeISO, + timing: { kind: 'simple', segmentName: 'm' }, + initialTokens: { input: 0, output: 0, total: 0 }, + initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', + createStream: () => createAgentEventReadableStream(events), + }) + + expect(eventResult.streamFormat).toBe('agent-events-v1') + const reader = eventResult.stream.getReader() + const received = [] + while (true) { + const { done, value } = await reader.read() + if (done) break + received.push(value) + } + expect(received).toEqual(events) + }) }) diff --git a/apps/sim/providers/streaming-execution.ts b/apps/sim/providers/streaming-execution.ts index 7a217af283d..ef020c5d328 100644 --- a/apps/sim/providers/streaming-execution.ts +++ b/apps/sim/providers/streaming-execution.ts @@ -1,4 +1,5 @@ import type { NormalizedBlockOutput, StreamingExecution } from '@/executor/types' +import type { AgentStreamEvent, AgentStreamFormat } from '@/providers/stream-events' import type { TimeSegment } from '@/providers/types' /** @@ -78,13 +79,21 @@ interface CreateStreamingExecutionOptions { toolCalls?: ToolCallSlice /** Marks `execution.isStreaming = true` when set. */ isStreaming?: boolean + /** + * Declares whether {@link createStream} returns UTF-8 answer bytes (`text`) + * or an in-process {@link AgentStreamEvent} object stream (`agent-events-v1`). + * Defaults to `'text'` so existing providers stay unchanged. + */ + streamFormat?: AgentStreamFormat /** * Builds the provider stream. Receives the live `output` object and a * `finalizeTiming` hook. The provider wires its native stream factory and, in * the drain callback, writes final content/tokens/cost onto `output` then * calls `finalizeTiming()`. */ - createStream: (handles: StreamFinalizer) => ReadableStream + createStream: ( + handles: StreamFinalizer + ) => ReadableStream | ReadableStream } /** @@ -104,6 +113,7 @@ export function createStreamingExecution( initialCost, toolCalls, isStreaming, + streamFormat = 'text', createStream, } = options @@ -155,6 +165,7 @@ export function createStreamingExecution( return { stream, + streamFormat, execution: { success: true, output, diff --git a/apps/sim/providers/together/index.ts b/apps/sim/providers/together/index.ts index 34c22f5c18c..c52f383d2ff 100644 --- a/apps/sim/providers/together/index.ts +++ b/apps/sim/providers/together/index.ts @@ -167,6 +167,7 @@ export const togetherProvider: ProviderConfig = { timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { output.content = content @@ -469,6 +470,7 @@ export const togetherProvider: ProviderConfig = { }, toolCalls: toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => { output.content = content diff --git a/apps/sim/providers/together/utils.ts b/apps/sim/providers/together/utils.ts index 22b7823f342..e187e881490 100644 --- a/apps/sim/providers/together/utils.ts +++ b/apps/sim/providers/together/utils.ts @@ -1,6 +1,8 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' +import { checkForForcedToolUsageOpenAI } from '@/providers/utils' /** * Together gates native `json_schema` per-model, so we use the broadly supported @@ -11,14 +13,19 @@ export async function supportsNativeStructuredOutputs(_modelId: string): Promise } /** - * Creates a ReadableStream from a Together AI streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Together AI streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromOpenAIStream( openaiStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(openaiStream, 'Together', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(openaiStream, { + providerName: 'Together', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } /** diff --git a/apps/sim/providers/tool-call-id.ts b/apps/sim/providers/tool-call-id.ts new file mode 100644 index 00000000000..c9cdf875e5d --- /dev/null +++ b/apps/sim/providers/tool-call-id.ts @@ -0,0 +1,27 @@ +/** + * Stable execution-local tool call ids when a provider omits them (e.g. Gemini). + * Ensures `${blockId}:${toolCallId}` cannot collide within a run. + */ + +import { randomFloat } from '@sim/utils/random' + +let localToolIdCounter = 0 + +/** Reset counter — tests only. */ +export function resetLocalToolIdCounterForTests(): void { + localToolIdCounter = 0 +} + +export function allocateExecutionLocalToolId(prefix = 'local'): string { + localToolIdCounter += 1 + const rand = randomFloat().toString(36).slice(2, 8) + return `${prefix}_${localToolIdCounter}_${rand}` +} + +/** Returns provider id if non-empty, otherwise allocates a local one. */ +export function ensureToolCallId(providerId: string | null | undefined, prefix = 'local'): string { + if (typeof providerId === 'string' && providerId.trim().length > 0) { + return providerId + } + return allocateExecutionLocalToolId(prefix) +} diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index 1697c12f80d..b999804f54f 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -367,6 +367,8 @@ describe('Model Capabilities', () => { expect(supportsReasoningEffort('o4-mini')).toBe(true) expect(supportsReasoningEffort('azure/gpt-5')).toBe(true) expect(supportsReasoningEffort('azure/o3')).toBe(true) + expect(supportsReasoningEffort('groq/openai/gpt-oss-120b')).toBe(true) + expect(supportsReasoningEffort('groq/openai/gpt-oss-20b')).toBe(true) }) it.concurrent('should return false for models without reasoning effort capability', () => { @@ -416,6 +418,9 @@ describe('Model Capabilities', () => { expect(supportsThinking('claude-sonnet-4-5')).toBe(true) expect(supportsThinking('claude-haiku-4-5')).toBe(true) expect(supportsThinking('gemini-3-flash-preview')).toBe(true) + expect(supportsThinking('deepseek-chat')).toBe(true) + expect(supportsThinking('deepseek-reasoner')).toBe(true) + expect(supportsThinking('groq/qwen/qwen3.6-27b')).toBe(true) }) it.concurrent('should return false for models without thinking capability', () => { @@ -497,6 +502,8 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-4o') expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-5') + expect(MODELS_WITH_REASONING_EFFORT).toContain('groq/openai/gpt-oss-120b') + expect(MODELS_WITH_REASONING_EFFORT).toContain('groq/openai/gpt-oss-20b') }) it.concurrent('should have correct models in MODELS_WITH_VERBOSITY', () => { @@ -538,6 +545,10 @@ describe('Model Capabilities', () => { expect(MODELS_WITH_THINKING).toContain('claude-haiku-4-5') + expect(MODELS_WITH_THINKING).toContain('deepseek-chat') + expect(MODELS_WITH_THINKING).toContain('deepseek-reasoner') + expect(MODELS_WITH_THINKING).toContain('groq/qwen/qwen3.6-27b') + expect(MODELS_WITH_THINKING).not.toContain('gpt-4o') expect(MODELS_WITH_THINKING).not.toContain('gpt-5') expect(MODELS_WITH_THINKING).not.toContain('o3') diff --git a/apps/sim/providers/vllm/index.ts b/apps/sim/providers/vllm/index.ts index 936bf3af633..34ee8d9f256 100644 --- a/apps/sim/providers/vllm/index.ts +++ b/apps/sim/providers/vllm/index.ts @@ -240,6 +240,7 @@ export const vllmProvider: ProviderConfig = { timing: { kind: 'simple', segmentName: request.model }, initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, + streamFormat: 'agent-events-v1', createStream: ({ output, finalizeTiming }) => createReadableStreamFromVLLMStream(streamResponse, (content, usage) => { let cleanContent = content @@ -582,6 +583,7 @@ export const vllmProvider: ProviderConfig = { count: toolCalls.length, } : undefined, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromVLLMStream(streamResponse, (content, usage) => { let cleanContent = content diff --git a/apps/sim/providers/vllm/utils.ts b/apps/sim/providers/vllm/utils.ts index 2b1db5bf553..f2580ab481b 100644 --- a/apps/sim/providers/vllm/utils.ts +++ b/apps/sim/providers/vllm/utils.ts @@ -1,16 +1,23 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' +import { checkForForcedToolUsageOpenAI } from '@/providers/utils' /** - * Creates a ReadableStream from a vLLM streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a vLLM streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromVLLMStream( vllmStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(vllmStream, 'vLLM', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(vllmStream, { + providerName: 'vLLM', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } /** diff --git a/apps/sim/providers/xai/index.ts b/apps/sim/providers/xai/index.ts index 42ebeb1254c..4e9c2b13b09 100644 --- a/apps/sim/providers/xai/index.ts +++ b/apps/sim/providers/xai/index.ts @@ -128,6 +128,7 @@ export const xAIProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromXAIStream(streamResponse, (content, usage) => { output.content = content @@ -523,6 +524,7 @@ export const xAIProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromXAIStream(streamResponse as any, (content, usage) => { output.content = content diff --git a/apps/sim/providers/xai/utils.ts b/apps/sim/providers/xai/utils.ts index 3a2acd56eca..76a21c7505f 100644 --- a/apps/sim/providers/xai/utils.ts +++ b/apps/sim/providers/xai/utils.ts @@ -1,16 +1,23 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { checkForForcedToolUsageOpenAI, createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' +import { checkForForcedToolUsageOpenAI } from '@/providers/utils' /** - * Creates a ReadableStream from an xAI streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from an xAI streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromXAIStream( xaiStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(xaiStream, 'xAI', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(xaiStream, { + providerName: 'xAI', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } /** diff --git a/apps/sim/providers/zai/index.ts b/apps/sim/providers/zai/index.ts index 779b18942ea..430f3667b71 100644 --- a/apps/sim/providers/zai/index.ts +++ b/apps/sim/providers/zai/index.ts @@ -183,6 +183,7 @@ export const zaiProvider: ProviderConfig = { initialTokens: { input: 0, output: 0, total: 0 }, initialCost: { input: 0, output: 0, total: 0 }, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromZaiStream(streamResponse as any, (content, usage) => { output.content = content @@ -547,6 +548,7 @@ export const zaiProvider: ProviderConfig = { } : undefined, isStreaming: true, + streamFormat: 'agent-events-v1', createStream: ({ output }) => createReadableStreamFromZaiStream(streamResponse as any, (content, usage) => { output.content = content diff --git a/apps/sim/providers/zai/utils.ts b/apps/sim/providers/zai/utils.ts index 4b86c6b7619..28ab12b1cd0 100644 --- a/apps/sim/providers/zai/utils.ts +++ b/apps/sim/providers/zai/utils.ts @@ -1,14 +1,20 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createOpenAICompatibleStream } from '@/providers/utils' +import { createOpenAICompatibleAgentEventStream } from '@/providers/openai-compat/stream-events' +import type { AgentStreamEvent } from '@/providers/stream-events' /** - * Creates a ReadableStream from a Z.ai streaming response. - * Uses the shared OpenAI-compatible streaming utility. + * Creates an agent-events stream from a Z.ai streaming response. + * Uses the shared OpenAI-compatible agent event streaming utility. */ export function createReadableStreamFromZaiStream( zaiStream: AsyncIterable, - onComplete?: (content: string, usage: CompletionUsage) => void -): ReadableStream { - return createOpenAICompatibleStream(zaiStream, 'Z.ai', onComplete) + onComplete?: (content: string, usage: CompletionUsage, thinking?: string) => void +): ReadableStream { + return createOpenAICompatibleAgentEventStream(zaiStream, { + providerName: 'Z.ai', + onComplete: onComplete + ? (result) => onComplete(result.content, result.usage, result.thinking) + : undefined, + }) } diff --git a/apps/sim/stores/terminal/console/store.ts b/apps/sim/stores/terminal/console/store.ts index a3262620c3c..185f4d89639 100644 --- a/apps/sim/stores/terminal/console/store.ts +++ b/apps/sim/stores/terminal/console/store.ts @@ -612,6 +612,18 @@ export const useTerminalConsoleStore = create()( updatedEntry.childWorkflowInstanceId = update.childWorkflowInstanceId } + if (update.agentStreamThinking !== undefined) { + updatedEntry.agentStreamThinking = update.agentStreamThinking + } + + if (update.agentStreamToolCalls !== undefined) { + updatedEntry.agentStreamToolCalls = update.agentStreamToolCalls + } + + if (update.agentStreamActive !== undefined) { + updatedEntry.agentStreamActive = update.agentStreamActive + } + nextEntries[location.index] = updatedEntry } diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts index d7a0fdadf2c..079a8d91a74 100644 --- a/apps/sim/stores/terminal/console/types.ts +++ b/apps/sim/stores/terminal/console/types.ts @@ -1,5 +1,6 @@ import type { ParentIteration } from '@/executor/execution/types' import type { NormalizedBlockOutput } from '@/executor/types' +import type { AgentStreamToolCall } from '@/components/agent-stream/agent-stream-chrome' import type { SubflowType } from '@/stores/workflows/workflow/types' export interface ConsoleEntry { @@ -32,6 +33,12 @@ export interface ConsoleEntry { childWorkflowName?: string /** Per-invocation unique ID linking this workflow block to its child block events */ childWorkflowInstanceId?: string + /** Live agent thinking text (canvas stream:thinking). Not part of answer content. */ + agentStreamThinking?: string + /** Live tool chips (canvas stream:tool). Name + status only. */ + agentStreamToolCalls?: AgentStreamToolCall[] + /** True while thinking/tool live updates may still arrive for this entry. */ + agentStreamActive?: boolean } export interface ConsoleUpdate { @@ -58,6 +65,9 @@ export interface ConsoleUpdate { childWorkflowBlockId?: string childWorkflowName?: string childWorkflowInstanceId?: string + agentStreamThinking?: string + agentStreamToolCalls?: AgentStreamToolCall[] + agentStreamActive?: boolean } export interface ConsoleEntryLocation { diff --git a/packages/db/migrations/0261_chat_include_thinking.sql b/packages/db/migrations/0261_chat_include_thinking.sql new file mode 100644 index 00000000000..4d3e8bd8049 --- /dev/null +++ b/packages/db/migrations/0261_chat_include_thinking.sql @@ -0,0 +1 @@ +ALTER TABLE "chat" ADD COLUMN IF NOT EXISTS "include_thinking" boolean DEFAULT false NOT NULL; diff --git a/packages/db/migrations/meta/0261_snapshot.json b/packages/db/migrations/meta/0261_snapshot.json new file mode 100644 index 00000000000..c9534b9e70d --- /dev/null +++ b/packages/db/migrations/meta/0261_snapshot.json @@ -0,0 +1,17701 @@ +{ + "id": "0e7737c0-3a5e-49b0-b162-736c5550d1bd", + "prevId": "d708558f-4e3a-4d38-96e5-ca0d65c83f45", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.academy_certificate": { + "name": "academy_certificate", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "academy_cert_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "certificate_number": { + "name": "certificate_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "academy_certificate_user_id_idx": { + "name": "academy_certificate_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_course_id_idx": { + "name": "academy_certificate_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_user_course_unique": { + "name": "academy_certificate_user_course_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "academy_certificate_status_idx": { + "name": "academy_certificate_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "academy_certificate_user_id_user_id_fk": { + "name": "academy_certificate_user_id_user_id_fk", + "tableFrom": "academy_certificate", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "academy_certificate_certificate_number_unique": { + "name": "academy_certificate_certificate_number_unique", + "nullsNotDistinct": false, + "columns": [ + "certificate_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_key_hash_idx": { + "name": "api_key_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_pending_run_at_idx": { + "name": "async_jobs_schedule_pending_run_at_idx", + "columns": [ + { + "expression": "run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_schedule_processing_started_at_idx": { + "name": "async_jobs_schedule_processing_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_log": { + "name": "audit_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_name": { + "name": "actor_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_name": { + "name": "resource_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audit_log_workspace_created_idx": { + "name": "audit_log_workspace_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_workspace_created_at_id_idx": { + "name": "audit_log_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_actor_created_idx": { + "name": "audit_log_actor_created_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_resource_idx": { + "name": "audit_log_resource_idx", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_log_action_idx": { + "name": "audit_log_action_idx", + "columns": [ + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_log_workspace_id_workspace_id_fk": { + "name": "audit_log_workspace_id_workspace_id_fk", + "tableFrom": "audit_log", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "audit_log_actor_id_user_id_fk": { + "name": "audit_log_actor_id_user_id_fk", + "tableFrom": "audit_log", + "tableTo": "user", + "columnsFrom": [ + "actor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_work_status": { + "name": "background_work_status", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "background_work_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_work_status_value", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "background_work_status_workspace_status_idx": { + "name": "background_work_status_workspace_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_workflow_status_idx": { + "name": "background_work_status_workflow_status_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_child_ws_idx": { + "name": "background_work_status_meta_child_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'childWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_work_status_meta_other_ws_idx": { + "name": "background_work_status_meta_other_ws_idx", + "columns": [ + { + "expression": "(\"metadata\" ->> 'otherWorkspaceId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_work_status_workspace_id_workspace_id_fk": { + "name": "background_work_status_workspace_id_workspace_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "background_work_status_workflow_id_workflow_id_fk": { + "name": "background_work_status_workflow_id_workflow_id_fk", + "tableFrom": "background_work_status", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "include_thinking": { + "name": "include_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"chat\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_archived_at_partial_idx": { + "name": "chat_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"chat\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_chat_on_workflow_id_archived_at": { + "name": "idx_chat_on_workflow_id_archived_at", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_async_tool_calls": { + "name": "copilot_async_tool_calls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tool_call_id": { + "name": "tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "args": { + "name": "args", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "status": { + "name": "status", + "type": "copilot_async_tool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "claimed_by": { + "name": "claimed_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_async_tool_calls_run_id_idx": { + "name": "copilot_async_tool_calls_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_checkpoint_id_idx": { + "name": "copilot_async_tool_calls_checkpoint_id_idx", + "columns": [ + { + "expression": "checkpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_status_idx": { + "name": "copilot_async_tool_calls_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_run_status_idx": { + "name": "copilot_async_tool_calls_run_status_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_async_tool_calls_tool_call_id_unique": { + "name": "copilot_async_tool_calls_tool_call_id_unique", + "columns": [ + { + "expression": "tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_async_tool_calls_run_id_copilot_runs_id_fk": { + "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": { + "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", + "tableFrom": "copilot_async_tool_calls", + "tableTo": "copilot_run_checkpoints", + "columnsFrom": [ + "checkpoint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "chat_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'copilot'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workspace_idx": { + "name": "copilot_chats_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workspace_created_at_id_idx": { + "name": "copilot_chats_workspace_created_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workspace_id_workspace_id_fk": { + "name": "copilot_chats_workspace_id_workspace_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_messages": { + "name": "copilot_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_message_id": { + "name": "parent_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens_in": { + "name": "tokens_in", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "tokens_out": { + "name": "tokens_out", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_messages_chat_message_unique": { + "name": "copilot_messages_chat_message_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_created_at_idx": { + "name": "copilot_messages_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_seq_idx": { + "name": "copilot_messages_chat_seq_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_chat_stream_idx": { + "name": "copilot_messages_chat_stream_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_messages_user_created_at_idx": { + "name": "copilot_messages_user_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_messages_chat_id_copilot_chats_id_fk": { + "name": "copilot_messages_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_messages", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_run_checkpoints": { + "name": "copilot_run_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pending_tool_call_id": { + "name": "pending_tool_call_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "conversation_snapshot": { + "name": "conversation_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "agent_state": { + "name": "agent_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "provider_request": { + "name": "provider_request", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_run_checkpoints_run_id_idx": { + "name": "copilot_run_checkpoints_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_pending_tool_call_id_idx": { + "name": "copilot_run_checkpoints_pending_tool_call_id_idx", + "columns": [ + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_run_checkpoints_run_pending_tool_unique": { + "name": "copilot_run_checkpoints_run_pending_tool_unique", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pending_tool_call_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_run_checkpoints_run_id_copilot_runs_id_fk": { + "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", + "tableFrom": "copilot_run_checkpoints", + "tableTo": "copilot_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_runs": { + "name": "copilot_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_run_id": { + "name": "parent_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream_id": { + "name": "stream_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "copilot_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "request_context": { + "name": "request_context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "copilot_runs_execution_id_idx": { + "name": "copilot_runs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_parent_run_id_idx": { + "name": "copilot_runs_parent_run_id_idx", + "columns": [ + { + "expression": "parent_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_id_idx": { + "name": "copilot_runs_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_user_id_idx": { + "name": "copilot_runs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workflow_id_idx": { + "name": "copilot_runs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_id_idx": { + "name": "copilot_runs_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_status_idx": { + "name": "copilot_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_chat_execution_idx": { + "name": "copilot_runs_chat_execution_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_execution_started_at_idx": { + "name": "copilot_runs_execution_started_at_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_workspace_completed_at_id_idx": { + "name": "copilot_runs_workspace_completed_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"completed_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_runs_stream_id_unique": { + "name": "copilot_runs_stream_id_unique", + "columns": [ + { + "expression": "stream_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_runs_chat_id_copilot_chats_id_fk": { + "name": "copilot_runs_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_user_id_user_id_fk": { + "name": "copilot_runs_user_id_user_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workflow_id_workflow_id_fk": { + "name": "copilot_runs_workflow_id_workflow_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_runs_workspace_id_workspace_id_fk": { + "name": "copilot_runs_workspace_id_workspace_id_fk", + "tableFrom": "copilot_runs", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_workflow_read_hashes": { + "name": "copilot_workflow_read_hashes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_workflow_read_hashes_chat_id_idx": { + "name": "copilot_workflow_read_hashes_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_workflow_id_idx": { + "name": "copilot_workflow_read_hashes_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_workflow_read_hashes_chat_workflow_unique": { + "name": "copilot_workflow_read_hashes_chat_workflow_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": { + "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": { + "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", + "tableFrom": "copilot_workflow_read_hashes", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_service_account_key": { + "name": "encrypted_service_account_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": [ + "env_owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": [ + "invited_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_block": { + "name": "custom_block", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inputs": { + "name": "inputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "outputs": { + "name": "outputs", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": [ + "drain_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "user_excluded": { + "name": "user_excluded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_external_id_idx": { + "name": "doc_connector_external_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"document\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_connector_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_storage_key_idx": { + "name": "doc_storage_key_idx", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"storage_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_archived_at_partial_idx": { + "name": "doc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_deleted_at_partial_idx": { + "name": "doc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"document\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_connector_id_knowledge_connector_id_fk": { + "name": "document_connector_id_knowledge_connector_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_connector", + "columnsFrom": [ + "connector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_uploaded_by_user_id_fk": { + "name": "document_uploaded_by_user_id_fk", + "tableFrom": "document", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_dependencies": { + "name": "execution_large_value_dependencies", + "schema": "", + "columns": { + "parent_key": { + "name": "parent_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_key": { + "name": "child_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_dependencies_workspace_parent_key_idx": { + "name": "execution_large_value_dependencies_workspace_parent_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_value_dependencies_workspace_child_key_idx": { + "name": "execution_large_value_dependencies_workspace_child_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_dependencies_workspace_id_workspace_id_fk": { + "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_dependencies", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_dependencies_parent_key_child_key_pk": { + "name": "execution_large_value_dependencies_parent_key_child_key_pk", + "columns": [ + "parent_key", + "child_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_value_references": { + "name": "execution_large_value_references", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "execution_large_value_reference_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_large_value_references_workspace_execution_source_idx": { + "name": "execution_large_value_references_workspace_execution_source_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_value_references_workspace_id_workspace_id_fk": { + "name": "execution_large_value_references_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_value_references_workflow_id_workflow_id_fk": { + "name": "execution_large_value_references_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_value_references", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "execution_large_value_references_key_execution_id_source_pk": { + "name": "execution_large_value_references_key_execution_id_source_pk", + "columns": [ + "key", + "execution_id", + "source" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_large_values": { + "name": "execution_large_values", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_execution_id": { + "name": "owner_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "execution_large_values_owner_execution_id_idx": { + "name": "execution_large_values_owner_execution_id_idx", + "columns": [ + { + "expression": "owner_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_cleanup_idx": { + "name": "execution_large_values_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_large_values_tombstone_cleanup_idx": { + "name": "execution_large_values_tombstone_cleanup_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_large_values_workspace_id_workspace_id_fk": { + "name": "execution_large_values_workspace_id_workspace_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_large_values_workflow_id_workflow_id_fk": { + "name": "execution_large_values_workflow_id_workflow_id_fk", + "tableFrom": "execution_large_values", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "invitation_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "membership_intent": { + "name": "membership_intent", + "type": "invitation_membership_intent", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'internal'" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_status_idx": { + "name": "invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_pending_email_org_unique": { + "name": "invitation_pending_email_org_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invitation_token_unique": { + "name": "invitation_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation_workspace_grant": { + "name": "invitation_workspace_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invitation_id": { + "name": "invitation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_workspace_grant_unique": { + "name": "invitation_workspace_grant_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_workspace_grant_workspace_id_idx": { + "name": "invitation_workspace_grant_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_workspace_grant_invitation_id_invitation_id_fk": { + "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "invitation", + "columnsFrom": [ + "invitation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_workspace_grant_workspace_id_workspace_id_fk": { + "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", + "tableFrom": "invitation_workspace_grant", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_execution_logs": { + "name": "job_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "job_execution_logs_schedule_id_idx": { + "name": "job_execution_logs_schedule_id_idx", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_started_at_idx": { + "name": "job_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_workspace_ended_at_id_idx": { + "name": "job_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_execution_id_unique": { + "name": "job_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_execution_logs_trigger_idx": { + "name": "job_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_execution_logs_schedule_id_workflow_schedule_id_fk": { + "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workflow_schedule", + "columnsFrom": [ + "schedule_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "job_execution_logs_workspace_id_workspace_id_fk": { + "name": "job_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "job_execution_logs", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_deleted_partial_idx": { + "name": "kb_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_name_active_unique": { + "name": "kb_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"knowledge_base\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": [ + "knowledge_base_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": [ + "connector_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_server_oauth": { + "name": "mcp_server_oauth", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_server_id": { + "name": "mcp_server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_information": { + "name": "client_information", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tokens": { + "name": "tokens", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_created_at": { + "name": "state_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_refreshed_at": { + "name": "last_refreshed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_server_oauth_server_unique": { + "name": "mcp_server_oauth_server_unique", + "columns": [ + { + "expression": "mcp_server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_server_oauth_state_idx": { + "name": "mcp_server_oauth_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": { + "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "mcp_servers", + "columnsFrom": [ + "mcp_server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_server_oauth_user_id_user_id_fk": { + "name": "mcp_server_oauth_user_id_user_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "mcp_server_oauth_workspace_id_workspace_id_fk": { + "name": "mcp_server_oauth_workspace_id_workspace_id_fk", + "tableFrom": "mcp_server_oauth", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'headers'" + }, + "oauth_client_id": { + "name": "oauth_client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "oauth_client_secret": { + "name": "oauth_client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_partial_idx": { + "name": "mcp_servers_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_deleted_partial_idx": { + "name": "memory_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"memory\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_allowed_sender": { + "name": "mothership_inbox_allowed_sender", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "added_by": { + "name": "added_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_sender_ws_email_idx": { + "name": "inbox_sender_ws_email_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_allowed_sender_added_by_user_id_fk": { + "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", + "tableFrom": "mothership_inbox_allowed_sender", + "tableTo": "user", + "columnsFrom": [ + "added_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_task": { + "name": "mothership_inbox_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_preview": { + "name": "body_preview", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_text": { + "name": "body_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_message_id": { + "name": "response_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agentmail_message_id": { + "name": "agentmail_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_job_id": { + "name": "trigger_job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "has_attachments": { + "name": "has_attachments", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "cc_recipients": { + "name": "cc_recipients", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "inbox_task_ws_created_at_idx": { + "name": "inbox_task_ws_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_ws_status_idx": { + "name": "inbox_task_ws_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_response_msg_id_idx": { + "name": "inbox_task_response_msg_id_idx", + "columns": [ + { + "expression": "response_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_task_email_msg_id_idx": { + "name": "inbox_task_email_msg_id_idx", + "columns": [ + { + "expression": "email_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_inbox_task_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_task_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mothership_inbox_task_chat_id_copilot_chats_id_fk": { + "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", + "tableFrom": "mothership_inbox_task", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_inbox_webhook": { + "name": "mothership_inbox_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "webhook_id": { + "name": "webhook_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "mothership_inbox_webhook_workspace_id_workspace_id_fk": { + "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", + "tableFrom": "mothership_inbox_webhook", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mothership_inbox_webhook_workspace_id_unique": { + "name": "mothership_inbox_webhook_workspace_id_unique", + "nullsNotDistinct": false, + "columns": [ + "workspace_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mothership_settings": { + "name": "mothership_settings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "mcp_tool_refs": { + "name": "mcp_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "custom_tool_refs": { + "name": "custom_tool_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "skill_refs": { + "name": "skill_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mothership_settings_workspace_id_workspace_id_fk": { + "name": "mothership_settings_workspace_id_workspace_id_fk", + "tableFrom": "mothership_settings", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "whitelabel_settings": { + "name": "whitelabel_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "data_retention_settings": { + "name": "data_retention_settings", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_member_usage_limit": { + "name": "organization_member_usage_limit", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_limit": { + "name": "usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "set_by": { + "name": "set_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_member_usage_limit_org_user_unique": { + "name": "org_member_usage_limit_org_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_member_usage_limit_organization_id_idx": { + "name": "org_member_usage_limit_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_member_usage_limit_organization_id_organization_id_fk": { + "name": "organization_member_usage_limit_organization_id_organization_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_user_id_user_id_fk": { + "name": "organization_member_usage_limit_user_id_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_member_usage_limit_set_by_user_id_fk": { + "name": "organization_member_usage_limit_set_by_user_id_fk", + "tableFrom": "organization_member_usage_limit", + "tableTo": "user", + "columnsFrom": [ + "set_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_event": { + "name": "outbox_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "available_at": { + "name": "available_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_event_status_available_idx": { + "name": "outbox_event_status_available_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_locked_at_idx": { + "name": "outbox_event_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_event_type_created_idx": { + "name": "outbox_event_type_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "automatic_resume_retry_count": { + "name": "automatic_resume_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_resume_at": { + "name": "next_resume_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_next_resume_at_idx": { + "name": "paused_executions_next_resume_at_idx", + "columns": [ + { + "expression": "next_resume_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'paused' AND next_resume_at IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pending_credential_draft": { + "name": "pending_credential_draft", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pending_draft_user_provider_ws": { + "name": "pending_draft_user_provider_ws", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "pending_credential_draft_user_id_user_id_fk": { + "name": "pending_credential_draft_user_id_user_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_workspace_id_workspace_id_fk": { + "name": "pending_credential_draft_workspace_id_workspace_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pending_credential_draft_credential_id_credential_id_fk": { + "name": "pending_credential_draft_credential_id_credential_id_fk", + "tableFrom": "pending_credential_draft", + "tableTo": "credential", + "columnsFrom": [ + "credential_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_name_unique": { + "name": "permission_group_organization_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_organization_default_unique": { + "name": "permission_group_organization_default_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "is_default = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_group_user_unique": { + "name": "permission_group_member_group_user_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_organization_user_idx": { + "name": "permission_group_member_organization_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": [ + "permission_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_organization_id_organization_id_fk": { + "name": "permission_group_member_organization_id_organization_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_workspace": { + "name": "permission_group_workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_workspace_workspace_id_idx": { + "name": "permission_group_workspace_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_workspace_group_workspace_unique": { + "name": "permission_group_workspace_group_workspace_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_permission_group_id_permission_group_id_fk": { + "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "permission_group", + "columnsFrom": [ + "permission_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_workspace_organization_id_organization_id_fk": { + "name": "permission_group_workspace_organization_id_organization_id_fk", + "tableFrom": "permission_group_workspace", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.public_share": { + "name": "public_share", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "public_share_token_unique": { + "name": "public_share_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_unique": { + "name": "public_share_resource_unique", + "columns": [ + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_resource_id_idx": { + "name": "public_share_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "public_share_workspace_id_idx": { + "name": "public_share_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "public_share_workspace_id_workspace_id_fk": { + "name": "public_share_workspace_id_workspace_id_fk", + "tableFrom": "public_share", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "public_share_created_by_user_id_fk": { + "name": "public_share_created_by_user_id_fk", + "tableFrom": "public_share", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": [ + "paused_execution_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": [ + "active_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mothership_environment": { + "name": "mothership_environment", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sim_trigger_state": { + "name": "sim_trigger_state", + "schema": "", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_key": { + "name": "scope_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sim_trigger_state_workflow_id_workflow_id_fk": { + "name": "sim_trigger_state_workflow_id_workflow_id_fk", + "tableFrom": "sim_trigger_state", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sim_trigger_state_workflow_id_block_id_scope_key_pk": { + "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", + "columns": [ + "workflow_id", + "block_id", + "scope_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.table_jobs": { + "name": "table_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "rows_processed": { + "name": "rows_processed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_jobs_one_active_per_table": { + "name": "table_jobs_one_active_per_table", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_watchdog_idx": { + "name": "table_jobs_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_jobs_table_started_idx": { + "name": "table_jobs_table_started_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_jobs_table_id_user_table_definitions_id_fk": { + "name": "table_jobs_table_id_user_table_definitions_id_fk", + "tableFrom": "table_jobs", + "tableTo": "user_table_definitions", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_jobs_workspace_id_workspace_id_fk": { + "name": "table_jobs_workspace_id_workspace_id_fk", + "tableFrom": "table_jobs", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_row_executions": { + "name": "table_row_executions", + "schema": "", + "columns": { + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "row_id": { + "name": "row_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "running_block_ids": { + "name": "running_block_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "block_errors": { + "name": "block_errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enrichment_details": { + "name": "enrichment_details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "table_row_executions_table_status_idx": { + "name": "table_row_executions_table_status_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_execution_id_idx": { + "name": "table_row_executions_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_row_executions_table_group_idx": { + "name": "table_row_executions_table_group_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_row_executions_table_id_user_table_definitions_id_fk": { + "name": "table_row_executions_table_id_user_table_definitions_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_definitions", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_row_executions_row_id_user_table_rows_id_fk": { + "name": "table_row_executions_row_id_user_table_rows_id_fk", + "tableFrom": "table_row_executions", + "tableTo": "user_table_rows", + "columnsFrom": [ + "row_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "table_row_executions_row_id_group_id_pk": { + "name": "table_row_executions_row_id_group_id_pk", + "columns": [ + "row_id", + "group_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.table_run_dispatches": { + "name": "table_run_dispatches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor": { + "name": "cursor", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "limit": { + "name": "limit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processed_count": { + "name": "processed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_manual_run": { + "name": "is_manual_run", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "triggered_by_user_id": { + "name": "triggered_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "table_run_dispatches_table_id_user_table_definitions_id_fk": { + "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user_table_definitions", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_workspace_id_workspace_id_fk": { + "name": "table_run_dispatches_workspace_id_workspace_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "table_run_dispatches_triggered_by_user_id_user_id_fk": { + "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", + "tableFrom": "table_run_dispatches", + "tableTo": "user", + "columnsFrom": [ + "triggered_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_entity_type": { + "name": "billing_entity_type", + "type": "billing_entity_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "billing_entity_id": { + "name": "billing_entity_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_period_start": { + "name": "billing_period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "billing_period_end": { + "name": "billing_period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_event_key_unique": { + "name": "usage_log_event_key_unique", + "columns": [ + { + "expression": "event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_log\".\"event_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_billing_entity_period_idx": { + "name": "usage_log_billing_entity_period_idx", + "columns": [ + { + "expression": "billing_entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_created_at_idx": { + "name": "usage_log_workspace_created_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_execution_id_idx": { + "name": "usage_log_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_log_billing_scope_all_or_none": { + "name": "usage_log_billing_scope_all_or_none", + "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )" + } + }, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_email": { + "name": "normalized_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'user'" + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_normalized_email_unique": { + "name": "user_normalized_email_unique", + "nullsNotDistinct": false, + "columns": [ + "normalized_email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'5'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "pro_period_cost_snapshot_at": { + "name": "pro_period_cost_snapshot_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "limit_notifications": { + "name": "limit_notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_definitions": { + "name": "user_table_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "max_rows": { + "name": "max_rows", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10000 + }, + "row_count": { + "name": "row_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "rows_version": { + "name": "rows_version", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_table_def_workspace_id_idx": { + "name": "user_table_def_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_name_unique": { + "name": "user_table_def_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"user_table_definitions\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_archived_at_idx": { + "name": "user_table_def_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_def_workspace_archived_partial_idx": { + "name": "user_table_def_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_definitions_workspace_id_workspace_id_fk": { + "name": "user_table_definitions_workspace_id_workspace_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_definitions_created_by_user_id_fk": { + "name": "user_table_definitions_created_by_user_id_fk", + "tableFrom": "user_table_definitions", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_table_rows": { + "name": "user_table_rows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "table_id": { + "name": "table_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_table_rows_tenant_data_gin_idx": { + "name": "user_table_rows_tenant_data_gin_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"data\" jsonb_path_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "user_table_rows_workspace_table_idx": { + "name": "user_table_rows_workspace_table_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_position_idx": { + "name": "user_table_rows_table_position_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_order_key_idx": { + "name": "user_table_rows_table_order_key_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "order_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_table_id_id_idx": { + "name": "user_table_rows_table_id_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "user_table_rows_table_id_user_table_definitions_id_fk": { + "name": "user_table_rows_table_id_user_table_definitions_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user_table_definitions", + "columnsFrom": [ + "table_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_workspace_id_workspace_id_fk": { + "name": "user_table_rows_workspace_id_workspace_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_table_rows_created_by_user_id_fk": { + "name": "user_table_rows_created_by_user_id_fk", + "tableFrom": "user_table_rows", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "routing_key": { + "name": "routing_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_routing_key_active_idx": { + "name": "webhook_routing_key_active_idx", + "columns": [ + { + "expression": "routing_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_archived_at_partial_idx": { + "name": "webhook_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": { + "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_tiktok_credential_id_idx": { + "name": "webhook_tiktok_credential_id_idx", + "columns": [ + { + "expression": "((\"provider_config\")::jsonb ->> 'credentialId')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook\".\"provider\" = 'tiktok' AND \"webhook\".\"is_active\" = true AND \"webhook\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id_updated_at_desc": { + "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": [ + "deployment_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_folder_name_active_unique": { + "name": "workflow_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_archived_at_idx": { + "name": "workflow_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_archived_partial_idx": { + "name": "workflow_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": [ + "source_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": [ + "target_block_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost_total": { + "name": "cost_total", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "models_used": { + "name": "models_used", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_id_desc_idx": { + "name": "workflow_execution_logs_workspace_started_at_id_desc_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"started_at\" DESC NULLS LAST", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "\"id\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_cost_total_idx": { + "name": "workflow_execution_logs_workspace_cost_total_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_total", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_models_used_idx": { + "name": "workflow_execution_logs_models_used_idx", + "columns": [ + { + "expression": "models_used", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "workflow_execution_logs_workspace_ended_at_id_idx": { + "name": "workflow_execution_logs_workspace_ended_at_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "date_trunc('milliseconds', \"ended_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_running_started_at_idx": { + "name": "workflow_execution_logs_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "status = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_completed_ended_at_idx": { + "name": "workflow_execution_logs_completed_ended_at_idx", + "columns": [ + { + "expression": "ended_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": [ + "state_snapshot_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": [ + "deployment_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_workspace_archived_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_deleted_at_idx": { + "name": "workflow_mcp_server_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_workspace_deleted_partial_idx": { + "name": "workflow_mcp_server_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "parameter_description_overrides": { + "name": "parameter_description_overrides", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'::json" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_archived_at_partial_idx": { + "name": "workflow_mcp_tool_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": [ + "server_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "infra_retry_count": { + "name": "infra_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'workflow'" + }, + "job_title": { + "name": "job_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'persistent'" + }, + "success_condition": { + "name": "success_condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_runs": { + "name": "max_runs", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "source_chat_id": { + "name": "source_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_task_name": { + "name": "source_task_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_user_id": { + "name": "source_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contexts": { + "name": "contexts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "excluded_dates": { + "name": "excluded_dates", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_archived_at_partial_idx": { + "name": "workflow_schedule_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": { + "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6", + "columns": [ + { + "expression": "source_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_workflow_idx": { + "name": "workflow_schedule_due_workflow_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_due_job_idx": { + "name": "workflow_schedule_due_job_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": [ + "deployment_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_user_id_user_id_fk": { + "name": "workflow_schedule_source_user_id_user_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "user", + "columnsFrom": [ + "source_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_source_workspace_id_workspace_id_fk": { + "name": "workflow_schedule_source_workspace_id_workspace_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workspace", + "columnsFrom": [ + "source_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#33C482'" + }, + "logo_url": { + "name": "logo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_mode": { + "name": "workspace_mode", + "type": "workspace_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'grandfathered_shared'" + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "inbox_enabled": { + "name": "inbox_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "inbox_address": { + "name": "inbox_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "inbox_provider_id": { + "name": "inbox_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "organization_assigned_at": { + "name": "organization_assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "forked_from_workspace_id": { + "name": "forked_from_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_owner_id_idx": { + "name": "workspace_owner_id_idx", + "columns": [ + { + "expression": "owner_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_organization_id_idx": { + "name": "workspace_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_mode_idx": { + "name": "workspace_mode_idx", + "columns": [ + { + "expression": "workspace_mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_forked_from_workspace_id_idx": { + "name": "workspace_forked_from_workspace_id_idx", + "columns": [ + { + "expression": "forked_from_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_organization_id_organization_id_fk": { + "name": "workspace_organization_id_organization_id_fk", + "tableFrom": "workspace", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": [ + "billed_account_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_forked_from_workspace_id_workspace_id_fk": { + "name": "workspace_forked_from_workspace_id_workspace_id_fk", + "tableFrom": "workspace", + "tableTo": "workspace", + "columnsFrom": [ + "forked_from_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "workspace_storage_used_bytes_non_negative": { + "name": "workspace_storage_used_bytes_non_negative", + "value": "\"workspace\".\"storage_used_bytes\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_workspace_provider_idx": { + "name": "workspace_byok_workspace_provider_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_deleted_at_idx": { + "name": "workspace_file_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_workspace_deleted_partial_idx": { + "name": "workspace_file_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file_folders": { + "name": "workspace_file_folders", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_folders_workspace_parent_idx": { + "name": "workspace_file_folders_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_parent_sort_idx": { + "name": "workspace_file_folders_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_deleted_at_idx": { + "name": "workspace_file_folders_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_deleted_partial_idx": { + "name": "workspace_file_folders_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_folders_workspace_parent_name_active_unique": { + "name": "workspace_file_folders_workspace_parent_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"parent_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_file_folders\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_folders_user_id_user_id_fk": { + "name": "workspace_file_folders_user_id_user_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_workspace_id_workspace_id_fk": { + "name": "workspace_file_folders_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_folders_parent_id_workspace_file_folders_id_fk": { + "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_file_folders", + "tableTo": "workspace_file_folders", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_active_unique": { + "name": "workspace_files_key_active_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_folder_name_active_unique": { + "name": "workspace_files_workspace_folder_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"folder_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + }, + { + "expression": "original_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_display_name_unique": { + "name": "workspace_files_chat_display_name_unique", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_folder_id_idx": { + "name": "workspace_files_folder_id_idx", + "columns": [ + { + "expression": "folder_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_chat_id_idx": { + "name": "workspace_files_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_deleted_at_idx": { + "name": "workspace_files_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_deleted_partial_idx": { + "name": "workspace_files_workspace_deleted_partial_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_folder_id_workspace_file_folders_id_fk": { + "name": "workspace_files_folder_id_workspace_file_folders_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace_file_folders", + "columnsFrom": [ + "folder_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_files_chat_id_copilot_chats_id_fk": { + "name": "workspace_files_chat_id_copilot_chats_id_fk", + "tableFrom": "workspace_files", + "tableTo": "copilot_chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_block_map": { + "name": "workspace_fork_block_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_workflow_id": { + "name": "parent_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_block_id": { + "name": "parent_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_workflow_id": { + "name": "child_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_block_id": { + "name": "child_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_block_map_child_ws_parent_unique": { + "name": "workspace_fork_block_map_child_ws_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_unique": { + "name": "workspace_fork_block_map_child_ws_child_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_parent_wf_idx": { + "name": "workspace_fork_block_map_child_ws_parent_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_block_map_child_ws_child_wf_idx": { + "name": "workspace_fork_block_map_child_ws_child_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "child_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_block_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_block_map", + "tableTo": "workspace", + "columnsFrom": [ + "child_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_dependent_value": { + "name": "workspace_fork_dependent_value", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workflow_id": { + "name": "target_workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sub_block_key": { + "name": "sub_block_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_dependent_value_child_ws_wf_idx": { + "name": "workspace_fork_dependent_value_child_ws_wf_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_dependent_value_field_unique": { + "name": "workspace_fork_dependent_value_field_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sub_block_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_dependent_value", + "tableTo": "workspace", + "columnsFrom": [ + "child_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_promote_run": { + "name": "workspace_fork_promote_run", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_workspace_id": { + "name": "source_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_workspace_id": { + "name": "target_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "workspace_fork_promote_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_promote_run_child_ws_target_unique": { + "name": "workspace_fork_promote_run_child_ws_target_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_promote_run_target_ws_idx": { + "name": "workspace_fork_promote_run_target_ws_idx", + "columns": [ + { + "expression": "target_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "workspace", + "columnsFrom": [ + "child_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_promote_run_created_by_user_id_fk": { + "name": "workspace_fork_promote_run_created_by_user_id_fk", + "tableFrom": "workspace_fork_promote_run", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_fork_resource_map": { + "name": "workspace_fork_resource_map", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "child_workspace_id": { + "name": "child_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "workspace_fork_resource_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_resource_id": { + "name": "parent_resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "child_resource_id": { + "name": "child_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_fork_resource_map_child_ws_idx": { + "name": "workspace_fork_resource_map_child_ws_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_ws_type_idx": { + "name": "workspace_fork_resource_map_child_ws_type_idx", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_fork_resource_map_child_type_parent_unique": { + "name": "workspace_fork_resource_map_child_type_parent_unique", + "columns": [ + { + "expression": "child_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": { + "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "workspace", + "columnsFrom": [ + "child_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_fork_resource_map_created_by_user_id_fk": { + "name": "workspace_fork_resource_map_created_by_user_id_fk", + "tableFrom": "workspace_fork_resource_map", + "tableTo": "user", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": [ + "active", + "revoked", + "expired" + ] + }, + "public.background_work_kind": { + "name": "background_work_kind", + "schema": "public", + "values": [ + "deployment_side_effects", + "fork_content_copy", + "fork_sync", + "fork_rollback" + ] + }, + "public.background_work_status_value": { + "name": "background_work_status_value", + "schema": "public", + "values": [ + "pending", + "processing", + "completed", + "completed_with_warnings", + "failed" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": [ + "payment_failed", + "dispute" + ] + }, + "public.billing_entity_type": { + "name": "billing_entity_type", + "schema": "public", + "values": [ + "user", + "organization" + ] + }, + "public.chat_type": { + "name": "chat_type", + "schema": "public", + "values": [ + "mothership", + "copilot" + ] + }, + "public.copilot_async_tool_status": { + "name": "copilot_async_tool_status", + "schema": "public", + "values": [ + "pending", + "running", + "completed", + "failed", + "cancelled", + "delivered" + ] + }, + "public.copilot_run_status": { + "name": "copilot_run_status", + "schema": "public", + "values": [ + "active", + "paused_waiting_for_tool", + "resuming", + "complete", + "error", + "cancelled" + ] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": [ + "admin", + "member" + ] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": [ + "active", + "pending", + "revoked" + ] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": [ + "oauth", + "env_workspace", + "env_personal", + "service_account" + ] + }, + "public.data_drain_cadence": { + "name": "data_drain_cadence", + "schema": "public", + "values": [ + "hourly", + "daily" + ] + }, + "public.data_drain_destination": { + "name": "data_drain_destination", + "schema": "public", + "values": [ + "s3", + "gcs", + "azure_blob", + "datadog", + "bigquery", + "snowflake", + "webhook" + ] + }, + "public.data_drain_run_status": { + "name": "data_drain_run_status", + "schema": "public", + "values": [ + "running", + "success", + "failed" + ] + }, + "public.data_drain_run_trigger": { + "name": "data_drain_run_trigger", + "schema": "public", + "values": [ + "cron", + "manual" + ] + }, + "public.data_drain_source": { + "name": "data_drain_source", + "schema": "public", + "values": [ + "workflow_logs", + "job_logs", + "audit_logs", + "copilot_chats", + "copilot_runs" + ] + }, + "public.execution_large_value_reference_source": { + "name": "execution_large_value_reference_source", + "schema": "public", + "values": [ + "execution_log", + "paused_snapshot" + ] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": [ + "organization", + "workspace" + ] + }, + "public.invitation_membership_intent": { + "name": "invitation_membership_intent", + "schema": "public", + "values": [ + "internal", + "external" + ] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "rejected", + "cancelled", + "expired" + ] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": [ + "admin", + "write", + "read" + ] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": [ + "model", + "fixed", + "tool" + ] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input", + "enrichment" + ] + }, + "public.workspace_fork_promote_direction": { + "name": "workspace_fork_promote_direction", + "schema": "public", + "values": [ + "push", + "pull" + ] + }, + "public.workspace_fork_resource_type": { + "name": "workspace_fork_resource_type", + "schema": "public", + "values": [ + "workflow", + "oauth_credential", + "service_account_credential", + "env_var", + "table", + "knowledge_base", + "knowledge_document", + "file", + "mcp_server", + "workflow_mcp_server", + "custom_tool", + "skill" + ] + }, + "public.workspace_mode": { + "name": "workspace_mode", + "schema": "public", + "values": [ + "personal", + "organization", + "grandfathered_shared" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 1cad973fa4f..b7dc44c1642 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1821,6 +1821,13 @@ "when": 1783810442774, "tag": "0260_unknown_sinister_six", "breakpoints": true + }, + { + "idx": 261, + "version": "7", + "when": 1783900000000, + "tag": "0261_chat_include_thinking", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 089cdf4c997..f71689e637b 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1082,6 +1082,13 @@ export const chat = pgTable( // Output configuration outputConfigs: json('output_configs').default('[]'), // Array of {blockId, path} objects + /** + * When true, public chat SSE may expose provider thinking/tool events if the + * client also opts in via `X-Sim-Stream-Protocol: agent-events-v1`. + * Default off — never derived from auth type or isSecureMode. + */ + includeThinking: boolean('include_thinking').notNull().default(false), + archivedAt: timestamp('archived_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index f3470bf55a5..9257ac6f480 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -428,6 +428,7 @@ export const schemaMock = { password: 'password', allowedEmails: 'allowedEmails', outputConfigs: 'outputConfigs', + includeThinking: 'includeThinking', archivedAt: 'archivedAt', createdAt: 'createdAt', updatedAt: 'updatedAt', From a241ec198abc63a028e9f74303d4aea8c1a9a307 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Mon, 13 Jul 2026 20:36:32 -0700 Subject: [PATCH 2/5] fix(agent-stream): clear stuck streaming UI and format db snapshot Biome was failing CI on migrations/meta/0261_snapshot.json. Also settle assistant streaming/tool flags when SSE ends without a terminal frame, without clobbering Stop's finalized content. Co-authored-by: Cursor --- .../chat/hooks/use-chat-streaming.test.tsx | 41 + .../chat/hooks/use-chat-streaming.ts | 39 +- .../db/migrations/meta/0261_snapshot.json | 1598 ++++------------- 3 files changed, 463 insertions(+), 1215 deletions(-) diff --git a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx index 997ed22b654..3c7807ea77a 100644 --- a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx +++ b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx @@ -196,6 +196,47 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { expect(assistant?.isStreaming).toBe(false) }) + it('clears streaming flags when SSE ends without a terminal final/error frame', async () => { + mockReadSSEEvents.mockImplementation(async (_source, options) => { + await options.onEvent({ + blockId: 'agent-1', + event: 'thinking', + data: 'Halfway…', + }) + await options.onEvent({ + blockId: 'agent-1', + chunk: 'Partial answer', + }) + await options.onEvent({ + blockId: 'agent-1', + event: 'tool', + phase: 'start', + id: 't1', + name: 'search', + }) + // Stream closes abruptly — no final or error event. + }) + + await act(async () => { + await handle.latest().handleStreamedResponse( + makeSseResponse(), + setMessages, + vi.fn(), + vi.fn(), + false + ) + }) + await flushUiBatch() + + const assistant = messages.find((m) => m.id === 'msg-assistant-1') + expect(assistant?.content).toBe('Partial answer') + expect(assistant?.thinking).toBe('Halfway…') + expect(assistant?.isStreaming).toBe(false) + expect(assistant?.isThinkingStreaming).toBe(false) + expect(assistant?.isToolStreaming).toBe(false) + expect(assistant?.toolCalls?.some((t) => t.status === 'error')).toBe(true) + }) + it('does not append thinking payload into answer when mislabeled as chunk', async () => { mockReadSSEEvents.mockImplementation(async (_source, options) => { await options.onEvent({ diff --git a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts index 312586be90a..05e0e3bbd63 100644 --- a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts +++ b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts @@ -308,10 +308,13 @@ export function useChatStreaming() { setIsLoading(false) let terminated = false + // Capture before Stop nulls abortControllerRef; needed when the reader + // resolves on abort instead of throwing AbortError. + const streamAbortSignal = abortControllerRef.current!.signal try { await readSSEEvents(response.body, { - signal: abortControllerRef.current!.signal, + signal: streamAbortSignal, onParseError: (_data, parseError) => { logger.error('Error parsing stream data:', parseError) }, @@ -630,7 +633,41 @@ export function useChatStreaming() { if (!terminated) { flushUI() + // Stream closed without a terminal final/error frame (abrupt disconnect, + // or only non-terminal stream_error). Clear live chrome so the UI does not + // stay stuck in a streaming/loading state. + const wasAborted = streamAbortSignal.aborted + settleInFlightTools(toolCallsMap, wasAborted ? 'cancelled' : 'error') + syncToolCallsRef() + isThinkingStreaming = false + const toolsSnapshot = snapshotToolCalls(toolCallOrder, toolCallsMap) + setMessages((prev) => + prev.map((msg) => { + if (msg.id !== messageId) return msg + // stopStreaming already wrote the stop notice into content; do not clobber it. + if (wasAborted) { + return { + ...msg, + isStreaming: false, + isThinkingStreaming: false, + isToolStreaming: false, + thinking: accumulatedThinking || msg.thinking, + toolCalls: toolsSnapshot ?? msg.toolCalls, + } + } + return { + ...msg, + isStreaming: false, + isThinkingStreaming: false, + isToolStreaming: false, + content: accumulatedText || msg.content, + thinking: accumulatedThinking || msg.thinking, + toolCalls: toolsSnapshot ?? msg.toolCalls, + } + }) + ) if ( + !wasAborted && shouldPlayAudio && streamingOptions?.audioStreamHandler && accumulatedText.length > lastAudioPosition diff --git a/packages/db/migrations/meta/0261_snapshot.json b/packages/db/migrations/meta/0261_snapshot.json index c9534b9e70d..499afb0848f 100644 --- a/packages/db/migrations/meta/0261_snapshot.json +++ b/packages/db/migrations/meta/0261_snapshot.json @@ -155,12 +155,8 @@ "name": "academy_certificate_user_id_user_id_fk", "tableFrom": "academy_certificate", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -170,9 +166,7 @@ "academy_certificate_certificate_number_unique": { "name": "academy_certificate_certificate_number_unique", "nullsNotDistinct": false, - "columns": [ - "certificate_number" - ] + "columns": ["certificate_number"] } }, "policies": {}, @@ -305,12 +299,8 @@ "name": "account_user_id_user_id_fk", "tableFrom": "account", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -465,12 +455,8 @@ "name": "api_key_user_id_user_id_fk", "tableFrom": "api_key", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -478,12 +464,8 @@ "name": "api_key_workspace_id_workspace_id_fk", "tableFrom": "api_key", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -491,12 +473,8 @@ "name": "api_key_created_by_user_id_fk", "tableFrom": "api_key", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -506,9 +484,7 @@ "api_key_key_unique": { "name": "api_key_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -919,12 +895,8 @@ "name": "audit_log_workspace_id_workspace_id_fk", "tableFrom": "audit_log", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -932,12 +904,8 @@ "name": "audit_log_actor_id_user_id_fk", "tableFrom": "audit_log", "tableTo": "user", - "columnsFrom": [ - "actor_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["actor_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -1102,12 +1070,8 @@ "name": "background_work_status_workspace_id_workspace_id_fk", "tableFrom": "background_work_status", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1115,12 +1079,8 @@ "name": "background_work_status_workflow_id_workflow_id_fk", "tableFrom": "background_work_status", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1300,12 +1260,8 @@ "name": "chat_workflow_id_workflow_id_fk", "tableFrom": "chat", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1313,12 +1269,8 @@ "name": "chat_user_id_user_id_fk", "tableFrom": "chat", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1527,12 +1479,8 @@ "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk", "tableFrom": "copilot_async_tool_calls", "tableTo": "copilot_runs", - "columnsFrom": [ - "run_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["run_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1540,12 +1488,8 @@ "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk", "tableFrom": "copilot_async_tool_calls", "tableTo": "copilot_run_checkpoints", - "columnsFrom": [ - "checkpoint_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["checkpoint_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1801,12 +1745,8 @@ "name": "copilot_chats_user_id_user_id_fk", "tableFrom": "copilot_chats", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1814,12 +1754,8 @@ "name": "copilot_chats_workflow_id_workflow_id_fk", "tableFrom": "copilot_chats", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -1827,12 +1763,8 @@ "name": "copilot_chats_workspace_id_workspace_id_fk", "tableFrom": "copilot_chats", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -1999,12 +1931,8 @@ "name": "copilot_feedback_user_id_user_id_fk", "tableFrom": "copilot_feedback", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2012,12 +1940,8 @@ "name": "copilot_feedback_chat_id_copilot_chats_id_fk", "tableFrom": "copilot_feedback", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2248,12 +2172,8 @@ "name": "copilot_messages_chat_id_copilot_chats_id_fk", "tableFrom": "copilot_messages", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2381,12 +2301,8 @@ "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk", "tableFrom": "copilot_run_checkpoints", "tableTo": "copilot_runs", - "columnsFrom": [ - "run_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["run_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2713,12 +2629,8 @@ "name": "copilot_runs_chat_id_copilot_chats_id_fk", "tableFrom": "copilot_runs", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2726,12 +2638,8 @@ "name": "copilot_runs_user_id_user_id_fk", "tableFrom": "copilot_runs", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2739,12 +2647,8 @@ "name": "copilot_runs_workflow_id_workflow_id_fk", "tableFrom": "copilot_runs", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2752,12 +2656,8 @@ "name": "copilot_runs_workspace_id_workspace_id_fk", "tableFrom": "copilot_runs", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -2870,12 +2770,8 @@ "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk", "tableFrom": "copilot_workflow_read_hashes", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -2883,12 +2779,8 @@ "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk", "tableFrom": "copilot_workflow_read_hashes", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3151,12 +3043,8 @@ "name": "credential_workspace_id_workspace_id_fk", "tableFrom": "credential", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3164,12 +3052,8 @@ "name": "credential_account_id_account_id_fk", "tableFrom": "credential", "tableTo": "account", - "columnsFrom": [ - "account_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["account_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3177,12 +3061,8 @@ "name": "credential_env_owner_user_id_user_id_fk", "tableFrom": "credential", "tableTo": "user", - "columnsFrom": [ - "env_owner_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3190,12 +3070,8 @@ "name": "credential_created_by_user_id_fk", "tableFrom": "credential", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -3357,12 +3233,8 @@ "name": "credential_member_credential_id_credential_id_fk", "tableFrom": "credential_member", "tableTo": "credential", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3370,12 +3242,8 @@ "name": "credential_member_user_id_user_id_fk", "tableFrom": "credential_member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3383,12 +3251,8 @@ "name": "credential_member_invited_by_user_id_fk", "tableFrom": "credential_member", "tableTo": "user", - "columnsFrom": [ - "invited_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3544,12 +3408,8 @@ "name": "custom_block_organization_id_organization_id_fk", "tableFrom": "custom_block", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3557,12 +3417,8 @@ "name": "custom_block_workflow_id_workflow_id_fk", "tableFrom": "custom_block", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3570,12 +3426,8 @@ "name": "custom_block_created_by_user_id_fk", "tableFrom": "custom_block", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3684,12 +3536,8 @@ "name": "custom_tools_workspace_id_workspace_id_fk", "tableFrom": "custom_tools", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -3697,12 +3545,8 @@ "name": "custom_tools_user_id_user_id_fk", "tableFrom": "custom_tools", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -3824,12 +3668,8 @@ "name": "data_drain_runs_drain_id_data_drains_id_fk", "tableFrom": "data_drain_runs", "tableTo": "data_drains", - "columnsFrom": [ - "drain_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -4005,12 +3845,8 @@ "name": "data_drains_organization_id_organization_id_fk", "tableFrom": "data_drains", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4018,12 +3854,8 @@ "name": "data_drains_created_by_user_id_fk", "tableFrom": "data_drains", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" } @@ -4930,12 +4762,8 @@ "name": "document_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "document", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -4943,12 +4771,8 @@ "name": "document_connector_id_knowledge_connector_id_fk", "tableFrom": "document", "tableTo": "knowledge_connector", - "columnsFrom": [ - "connector_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -4956,12 +4780,8 @@ "name": "document_uploaded_by_user_id_fk", "tableFrom": "document", "tableTo": "user", - "columnsFrom": [ - "uploaded_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -5593,12 +5413,8 @@ "name": "embedding_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "embedding", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5606,12 +5422,8 @@ "name": "embedding_document_id_document_id_fk", "tableFrom": "embedding", "tableTo": "document", - "columnsFrom": [ - "document_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["document_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5663,12 +5475,8 @@ "name": "environment_user_id_user_id_fk", "tableFrom": "environment", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5678,9 +5486,7 @@ "environment_user_id_unique": { "name": "environment_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -5766,12 +5572,8 @@ "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk", "tableFrom": "execution_large_value_dependencies", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -5779,10 +5581,7 @@ "compositePrimaryKeys": { "execution_large_value_dependencies_parent_key_child_key_pk": { "name": "execution_large_value_dependencies_parent_key_child_key_pk", - "columns": [ - "parent_key", - "child_key" - ] + "columns": ["parent_key", "child_key"] } }, "uniqueConstraints": {}, @@ -5867,12 +5666,8 @@ "name": "execution_large_value_references_workspace_id_workspace_id_fk", "tableFrom": "execution_large_value_references", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -5880,12 +5675,8 @@ "name": "execution_large_value_references_workflow_id_workflow_id_fk", "tableFrom": "execution_large_value_references", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -5893,11 +5684,7 @@ "compositePrimaryKeys": { "execution_large_value_references_key_execution_id_source_pk": { "name": "execution_large_value_references_key_execution_id_source_pk", - "columns": [ - "key", - "execution_id", - "source" - ] + "columns": ["key", "execution_id", "source"] } }, "uniqueConstraints": {}, @@ -6031,12 +5818,8 @@ "name": "execution_large_values_workspace_id_workspace_id_fk", "tableFrom": "execution_large_values", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6044,12 +5827,8 @@ "name": "execution_large_values_workflow_id_workflow_id_fk", "tableFrom": "execution_large_values", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -6267,12 +6046,8 @@ "name": "invitation_inviter_id_user_id_fk", "tableFrom": "invitation", "tableTo": "user", - "columnsFrom": [ - "inviter_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6280,12 +6055,8 @@ "name": "invitation_organization_id_organization_id_fk", "tableFrom": "invitation", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6295,9 +6066,7 @@ "invitation_token_unique": { "name": "invitation_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -6391,12 +6160,8 @@ "name": "invitation_workspace_grant_invitation_id_invitation_id_fk", "tableFrom": "invitation_workspace_grant", "tableTo": "invitation", - "columnsFrom": [ - "invitation_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["invitation_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6404,12 +6169,8 @@ "name": "invitation_workspace_grant_workspace_id_workspace_id_fk", "tableFrom": "invitation_workspace_grant", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6606,12 +6367,8 @@ "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk", "tableFrom": "job_execution_logs", "tableTo": "workflow_schedule", - "columnsFrom": [ - "schedule_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -6619,12 +6376,8 @@ "name": "job_execution_logs_workspace_id_workspace_id_fk", "tableFrom": "job_execution_logs", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6835,12 +6588,8 @@ "name": "knowledge_base_user_id_user_id_fk", "tableFrom": "knowledge_base", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -6848,12 +6597,8 @@ "name": "knowledge_base_workspace_id_workspace_id_fk", "tableFrom": "knowledge_base", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -6978,12 +6723,8 @@ "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "knowledge_base_tag_definitions", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7188,12 +6929,8 @@ "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", "tableFrom": "knowledge_connector", "tableTo": "knowledge_base", - "columnsFrom": [ - "knowledge_base_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7303,12 +7040,8 @@ "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", "tableFrom": "knowledge_connector_sync_log", "tableTo": "knowledge_connector", - "columnsFrom": [ - "connector_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7435,12 +7168,8 @@ "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk", "tableFrom": "mcp_server_oauth", "tableTo": "mcp_servers", - "columnsFrom": [ - "mcp_server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["mcp_server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7448,12 +7177,8 @@ "name": "mcp_server_oauth_user_id_user_id_fk", "tableFrom": "mcp_server_oauth", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -7461,12 +7186,8 @@ "name": "mcp_server_oauth_workspace_id_workspace_id_fk", "tableFrom": "mcp_server_oauth", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7693,12 +7414,8 @@ "name": "mcp_servers_workspace_id_workspace_id_fk", "tableFrom": "mcp_servers", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7706,12 +7423,8 @@ "name": "mcp_servers_created_by_user_id_fk", "tableFrom": "mcp_servers", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -7795,12 +7508,8 @@ "name": "member_user_id_user_id_fk", "tableFrom": "member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -7808,12 +7517,8 @@ "name": "member_organization_id_organization_id_fk", "tableFrom": "member", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -7953,12 +7658,8 @@ "name": "memory_workspace_id_workspace_id_fk", "tableFrom": "memory", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8039,12 +7740,8 @@ "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk", "tableFrom": "mothership_inbox_allowed_sender", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8052,12 +7749,8 @@ "name": "mothership_inbox_allowed_sender_added_by_user_id_fk", "tableFrom": "mothership_inbox_allowed_sender", "tableTo": "user", - "columnsFrom": [ - "added_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["added_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8293,12 +7986,8 @@ "name": "mothership_inbox_task_workspace_id_workspace_id_fk", "tableFrom": "mothership_inbox_task", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8306,12 +7995,8 @@ "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk", "tableFrom": "mothership_inbox_task", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -8364,12 +8049,8 @@ "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk", "tableFrom": "mothership_inbox_webhook", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8379,9 +8060,7 @@ "mothership_inbox_webhook_workspace_id_unique": { "name": "mothership_inbox_webhook_workspace_id_unique", "nullsNotDistinct": false, - "columns": [ - "workspace_id" - ] + "columns": ["workspace_id"] } }, "policies": {}, @@ -8456,12 +8135,8 @@ "name": "mothership_settings_workspace_id_workspace_id_fk", "tableFrom": "mothership_settings", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -8667,12 +8342,8 @@ "name": "organization_member_usage_limit_organization_id_organization_id_fk", "tableFrom": "organization_member_usage_limit", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8680,12 +8351,8 @@ "name": "organization_member_usage_limit_user_id_user_id_fk", "tableFrom": "organization_member_usage_limit", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -8693,12 +8360,8 @@ "name": "organization_member_usage_limit_set_by_user_id_fk", "tableFrom": "organization_member_usage_limit", "tableTo": "user", - "columnsFrom": [ - "set_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["set_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9014,12 +8677,8 @@ "name": "paused_executions_workflow_id_workflow_id_fk", "tableFrom": "paused_executions", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9124,12 +8783,8 @@ "name": "pending_credential_draft_user_id_user_id_fk", "tableFrom": "pending_credential_draft", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9137,12 +8792,8 @@ "name": "pending_credential_draft_workspace_id_workspace_id_fk", "tableFrom": "pending_credential_draft", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9150,12 +8801,8 @@ "name": "pending_credential_draft_credential_id_credential_id_fk", "tableFrom": "pending_credential_draft", "tableTo": "credential", - "columnsFrom": [ - "credential_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9288,12 +8935,8 @@ "name": "permission_group_organization_id_organization_id_fk", "tableFrom": "permission_group", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9301,12 +8944,8 @@ "name": "permission_group_created_by_user_id_fk", "tableFrom": "permission_group", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9423,12 +9062,8 @@ "name": "permission_group_member_permission_group_id_permission_group_id_fk", "tableFrom": "permission_group_member", "tableTo": "permission_group", - "columnsFrom": [ - "permission_group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9436,12 +9071,8 @@ "name": "permission_group_member_organization_id_organization_id_fk", "tableFrom": "permission_group_member", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9449,12 +9080,8 @@ "name": "permission_group_member_user_id_user_id_fk", "tableFrom": "permission_group_member", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9462,12 +9089,8 @@ "name": "permission_group_member_assigned_by_user_id_fk", "tableFrom": "permission_group_member", "tableTo": "user", - "columnsFrom": [ - "assigned_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -9557,12 +9180,8 @@ "name": "permission_group_workspace_permission_group_id_permission_group_id_fk", "tableFrom": "permission_group_workspace", "tableTo": "permission_group", - "columnsFrom": [ - "permission_group_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9570,12 +9189,8 @@ "name": "permission_group_workspace_workspace_id_workspace_id_fk", "tableFrom": "permission_group_workspace", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9583,12 +9198,8 @@ "name": "permission_group_workspace_organization_id_organization_id_fk", "tableFrom": "permission_group_workspace", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9794,12 +9405,8 @@ "name": "permissions_user_id_user_id_fk", "tableFrom": "permissions", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -9965,12 +9572,8 @@ "name": "public_share_workspace_id_workspace_id_fk", "tableFrom": "public_share", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -9978,12 +9581,8 @@ "name": "public_share_created_by_user_id_fk", "tableFrom": "public_share", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10154,12 +9753,8 @@ "name": "resume_queue_paused_execution_id_paused_executions_id_fk", "tableFrom": "resume_queue", "tableTo": "paused_executions", - "columnsFrom": [ - "paused_execution_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10272,12 +9867,8 @@ "name": "session_user_id_user_id_fk", "tableFrom": "session", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10285,12 +9876,8 @@ "name": "session_active_organization_id_organization_id_fk", "tableFrom": "session", "tableTo": "organization", - "columnsFrom": [ - "active_organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10300,9 +9887,7 @@ "session_token_unique": { "name": "session_token_unique", "nullsNotDistinct": false, - "columns": [ - "token" - ] + "columns": ["token"] } }, "policies": {}, @@ -10442,12 +10027,8 @@ "name": "settings_user_id_user_id_fk", "tableFrom": "settings", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10457,9 +10038,7 @@ "settings_user_id_unique": { "name": "settings_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -10509,12 +10088,8 @@ "name": "sim_trigger_state_workflow_id_workflow_id_fk", "tableFrom": "sim_trigger_state", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -10522,11 +10097,7 @@ "compositePrimaryKeys": { "sim_trigger_state_workflow_id_block_id_scope_key_pk": { "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk", - "columns": [ - "workflow_id", - "block_id", - "scope_key" - ] + "columns": ["workflow_id", "block_id", "scope_key"] } }, "uniqueConstraints": {}, @@ -10617,12 +10188,8 @@ "name": "skill_workspace_id_workspace_id_fk", "tableFrom": "skill", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10630,12 +10197,8 @@ "name": "skill_user_id_user_id_fk", "tableFrom": "skill", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -10766,12 +10329,8 @@ "name": "sso_provider_user_id_user_id_fk", "tableFrom": "sso_provider", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -10779,12 +10338,8 @@ "name": "sso_provider_organization_id_organization_id_fk", "tableFrom": "sso_provider", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11083,12 +10638,8 @@ "name": "table_jobs_table_id_user_table_definitions_id_fk", "tableFrom": "table_jobs", "tableTo": "user_table_definitions", - "columnsFrom": [ - "table_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["table_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11096,12 +10647,8 @@ "name": "table_jobs_workspace_id_workspace_id_fk", "tableFrom": "table_jobs", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11264,12 +10811,8 @@ "name": "table_row_executions_table_id_user_table_definitions_id_fk", "tableFrom": "table_row_executions", "tableTo": "user_table_definitions", - "columnsFrom": [ - "table_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["table_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11277,12 +10820,8 @@ "name": "table_row_executions_row_id_user_table_rows_id_fk", "tableFrom": "table_row_executions", "tableTo": "user_table_rows", - "columnsFrom": [ - "row_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["row_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -11290,10 +10829,7 @@ "compositePrimaryKeys": { "table_row_executions_row_id_group_id_pk": { "name": "table_row_executions_row_id_group_id_pk", - "columns": [ - "row_id", - "group_id" - ] + "columns": ["row_id", "group_id"] } }, "uniqueConstraints": {}, @@ -11450,12 +10986,8 @@ "name": "table_run_dispatches_table_id_user_table_definitions_id_fk", "tableFrom": "table_run_dispatches", "tableTo": "user_table_definitions", - "columnsFrom": [ - "table_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["table_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11463,12 +10995,8 @@ "name": "table_run_dispatches_workspace_id_workspace_id_fk", "tableFrom": "table_run_dispatches", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11476,12 +11004,8 @@ "name": "table_run_dispatches_triggered_by_user_id_user_id_fk", "tableFrom": "table_run_dispatches", "tableTo": "user", - "columnsFrom": [ - "triggered_by_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["triggered_by_user_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -11756,12 +11280,8 @@ "name": "usage_log_user_id_user_id_fk", "tableFrom": "usage_log", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -11769,12 +11289,8 @@ "name": "usage_log_workspace_id_workspace_id_fk", "tableFrom": "usage_log", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -11782,12 +11298,8 @@ "name": "usage_log_workflow_id_workflow_id_fk", "tableFrom": "usage_log", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -11895,16 +11407,12 @@ "user_email_unique": { "name": "user_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] }, "user_normalized_email_unique": { "name": "user_normalized_email_unique", "nullsNotDistinct": false, - "columns": [ - "normalized_email" - ] + "columns": ["normalized_email"] } }, "policies": {}, @@ -12136,12 +11644,8 @@ "name": "user_stats_user_id_user_id_fk", "tableFrom": "user_stats", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -12151,9 +11655,7 @@ "user_stats_user_id_unique": { "name": "user_stats_user_id_unique", "nullsNotDistinct": false, - "columns": [ - "user_id" - ] + "columns": ["user_id"] } }, "policies": {}, @@ -12329,12 +11831,8 @@ "name": "user_table_definitions_workspace_id_workspace_id_fk", "tableFrom": "user_table_definitions", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12342,12 +11840,8 @@ "name": "user_table_definitions_created_by_user_id_fk", "tableFrom": "user_table_definitions", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -12538,12 +12032,8 @@ "name": "user_table_rows_table_id_user_table_definitions_id_fk", "tableFrom": "user_table_rows", "tableTo": "user_table_definitions", - "columnsFrom": [ - "table_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["table_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12551,12 +12041,8 @@ "name": "user_table_rows_workspace_id_workspace_id_fk", "tableFrom": "user_table_rows", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12564,12 +12050,8 @@ "name": "user_table_rows_created_by_user_id_fk", "tableFrom": "user_table_rows", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -12705,9 +12187,7 @@ "waitlist_email_unique": { "name": "waitlist_email_unique", "nullsNotDistinct": false, - "columns": [ - "email" - ] + "columns": ["email"] } }, "policies": {}, @@ -12971,12 +12451,8 @@ "name": "webhook_workflow_id_workflow_id_fk", "tableFrom": "webhook", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -12984,12 +12460,8 @@ "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", "tableFrom": "webhook", "tableTo": "workflow_deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13263,12 +12735,8 @@ "name": "workflow_user_id_user_id_fk", "tableFrom": "workflow", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13276,12 +12744,8 @@ "name": "workflow_workspace_id_workspace_id_fk", "tableFrom": "workflow", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13289,12 +12753,8 @@ "name": "workflow_folder_id_workflow_folder_id_fk", "tableFrom": "workflow", "tableTo": "workflow_folder", - "columnsFrom": [ - "folder_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -13467,12 +12927,8 @@ "name": "workflow_blocks_workflow_id_workflow_id_fk", "tableFrom": "workflow_blocks", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13684,12 +13140,8 @@ "name": "workflow_checkpoints_user_id_user_id_fk", "tableFrom": "workflow_checkpoints", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13697,12 +13149,8 @@ "name": "workflow_checkpoints_workflow_id_workflow_id_fk", "tableFrom": "workflow_checkpoints", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13710,12 +13158,8 @@ "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", "tableFrom": "workflow_checkpoints", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13851,12 +13295,8 @@ "name": "workflow_deployment_version_workflow_id_workflow_id_fk", "tableFrom": "workflow_deployment_version", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -13979,12 +13419,8 @@ "name": "workflow_edges_workflow_id_workflow_id_fk", "tableFrom": "workflow_edges", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -13992,12 +13428,8 @@ "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", "tableFrom": "workflow_edges", "tableTo": "workflow_blocks", - "columnsFrom": [ - "source_block_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14005,12 +13437,8 @@ "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", "tableFrom": "workflow_edges", "tableTo": "workflow_blocks", - "columnsFrom": [ - "target_block_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14425,12 +13853,8 @@ "name": "workflow_execution_logs_workflow_id_workflow_id_fk", "tableFrom": "workflow_execution_logs", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -14438,12 +13862,8 @@ "name": "workflow_execution_logs_workspace_id_workspace_id_fk", "tableFrom": "workflow_execution_logs", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14451,12 +13871,8 @@ "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", "tableFrom": "workflow_execution_logs", "tableTo": "workflow_execution_snapshots", - "columnsFrom": [ - "state_snapshot_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -14464,12 +13880,8 @@ "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", "tableFrom": "workflow_execution_logs", "tableTo": "workflow_deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -14589,12 +14001,8 @@ "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", "tableFrom": "workflow_execution_snapshots", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -14789,12 +14197,8 @@ "name": "workflow_folder_user_id_user_id_fk", "tableFrom": "workflow_folder", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14802,12 +14206,8 @@ "name": "workflow_folder_workspace_id_workspace_id_fk", "tableFrom": "workflow_folder", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -14954,12 +14354,8 @@ "name": "workflow_mcp_server_workspace_id_workspace_id_fk", "tableFrom": "workflow_mcp_server", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -14967,12 +14363,8 @@ "name": "workflow_mcp_server_created_by_user_id_fk", "tableFrom": "workflow_mcp_server", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15127,12 +14519,8 @@ "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", "tableFrom": "workflow_mcp_tool", "tableTo": "workflow_mcp_server", - "columnsFrom": [ - "server_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["server_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15140,12 +14528,8 @@ "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", "tableFrom": "workflow_mcp_tool", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15523,12 +14907,8 @@ "name": "workflow_schedule_workflow_id_workflow_id_fk", "tableFrom": "workflow_schedule", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15536,12 +14916,8 @@ "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", "tableFrom": "workflow_schedule", "tableTo": "workflow_deployment_version", - "columnsFrom": [ - "deployment_version_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15549,12 +14925,8 @@ "name": "workflow_schedule_source_user_id_user_id_fk", "tableFrom": "workflow_schedule", "tableTo": "user", - "columnsFrom": [ - "source_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15562,12 +14934,8 @@ "name": "workflow_schedule_source_workspace_id_workspace_id_fk", "tableFrom": "workflow_schedule", "tableTo": "workspace", - "columnsFrom": [ - "source_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["source_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15665,12 +15033,8 @@ "name": "workflow_subflows_workflow_id_workflow_id_fk", "tableFrom": "workflow_subflows", "tableTo": "workflow", - "columnsFrom": [ - "workflow_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -15869,12 +15233,8 @@ "name": "workspace_owner_id_user_id_fk", "tableFrom": "workspace", "tableTo": "user", - "columnsFrom": [ - "owner_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -15882,12 +15242,8 @@ "name": "workspace_organization_id_organization_id_fk", "tableFrom": "workspace", "tableTo": "organization", - "columnsFrom": [ - "organization_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -15895,12 +15251,8 @@ "name": "workspace_billed_account_user_id_user_id_fk", "tableFrom": "workspace", "tableTo": "user", - "columnsFrom": [ - "billed_account_user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], "onDelete": "no action", "onUpdate": "no action" }, @@ -15908,12 +15260,8 @@ "name": "workspace_forked_from_workspace_id_workspace_id_fk", "tableFrom": "workspace", "tableTo": "workspace", - "columnsFrom": [ - "forked_from_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["forked_from_workspace_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16012,12 +15360,8 @@ "name": "workspace_byok_keys_workspace_id_workspace_id_fk", "tableFrom": "workspace_byok_keys", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16025,12 +15369,8 @@ "name": "workspace_byok_keys_created_by_user_id_fk", "tableFrom": "workspace_byok_keys", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16101,12 +15441,8 @@ "name": "workspace_environment_workspace_id_workspace_id_fk", "tableFrom": "workspace_environment", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16251,12 +15587,8 @@ "name": "workspace_file_workspace_id_workspace_id_fk", "tableFrom": "workspace_file", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16264,12 +15596,8 @@ "name": "workspace_file_uploaded_by_user_id_fk", "tableFrom": "workspace_file", "tableTo": "user", - "columnsFrom": [ - "uploaded_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16279,9 +15607,7 @@ "workspace_file_key_unique": { "name": "workspace_file_key_unique", "nullsNotDistinct": false, - "columns": [ - "key" - ] + "columns": ["key"] } }, "policies": {}, @@ -16464,12 +15790,8 @@ "name": "workspace_file_folders_user_id_user_id_fk", "tableFrom": "workspace_file_folders", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16477,12 +15799,8 @@ "name": "workspace_file_folders_workspace_id_workspace_id_fk", "tableFrom": "workspace_file_folders", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16490,12 +15808,8 @@ "name": "workspace_file_folders_parent_id_workspace_file_folders_id_fk", "tableFrom": "workspace_file_folders", "tableTo": "workspace_file_folders", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["parent_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -16797,12 +16111,8 @@ "name": "workspace_files_user_id_user_id_fk", "tableFrom": "workspace_files", "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["user_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16810,12 +16120,8 @@ "name": "workspace_files_workspace_id_workspace_id_fk", "tableFrom": "workspace_files", "tableTo": "workspace", - "columnsFrom": [ - "workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -16823,12 +16129,8 @@ "name": "workspace_files_folder_id_workspace_file_folders_id_fk", "tableFrom": "workspace_files", "tableTo": "workspace_file_folders", - "columnsFrom": [ - "folder_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" }, @@ -16836,12 +16138,8 @@ "name": "workspace_files_chat_id_copilot_chats_id_fk", "tableFrom": "workspace_files", "tableTo": "copilot_chats", - "columnsFrom": [ - "chat_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -16998,12 +16296,8 @@ "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk", "tableFrom": "workspace_fork_block_map", "tableTo": "workspace", - "columnsFrom": [ - "child_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17130,12 +16424,8 @@ "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk", "tableFrom": "workspace_fork_dependent_value", "tableTo": "workspace", - "columnsFrom": [ - "child_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" } @@ -17244,12 +16534,8 @@ "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk", "tableFrom": "workspace_fork_promote_run", "tableTo": "workspace", - "columnsFrom": [ - "child_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17257,12 +16543,8 @@ "name": "workspace_fork_promote_run_created_by_user_id_fk", "tableFrom": "workspace_fork_promote_run", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17399,12 +16681,8 @@ "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk", "tableFrom": "workspace_fork_resource_map", "tableTo": "workspace", - "columnsFrom": [ - "child_workspace_id" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["child_workspace_id"], + "columnsTo": ["id"], "onDelete": "cascade", "onUpdate": "no action" }, @@ -17412,12 +16690,8 @@ "name": "workspace_fork_resource_map_created_by_user_id_fk", "tableFrom": "workspace_fork_resource_map", "tableTo": "user", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], + "columnsFrom": ["created_by"], + "columnsTo": ["id"], "onDelete": "set null", "onUpdate": "no action" } @@ -17433,209 +16707,112 @@ "public.academy_cert_status": { "name": "academy_cert_status", "schema": "public", - "values": [ - "active", - "revoked", - "expired" - ] + "values": ["active", "revoked", "expired"] }, "public.background_work_kind": { "name": "background_work_kind", "schema": "public", - "values": [ - "deployment_side_effects", - "fork_content_copy", - "fork_sync", - "fork_rollback" - ] + "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"] }, "public.background_work_status_value": { "name": "background_work_status_value", "schema": "public", - "values": [ - "pending", - "processing", - "completed", - "completed_with_warnings", - "failed" - ] + "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"] }, "public.billing_blocked_reason": { "name": "billing_blocked_reason", "schema": "public", - "values": [ - "payment_failed", - "dispute" - ] + "values": ["payment_failed", "dispute"] }, "public.billing_entity_type": { "name": "billing_entity_type", "schema": "public", - "values": [ - "user", - "organization" - ] + "values": ["user", "organization"] }, "public.chat_type": { "name": "chat_type", "schema": "public", - "values": [ - "mothership", - "copilot" - ] + "values": ["mothership", "copilot"] }, "public.copilot_async_tool_status": { "name": "copilot_async_tool_status", "schema": "public", - "values": [ - "pending", - "running", - "completed", - "failed", - "cancelled", - "delivered" - ] + "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"] }, "public.copilot_run_status": { "name": "copilot_run_status", "schema": "public", - "values": [ - "active", - "paused_waiting_for_tool", - "resuming", - "complete", - "error", - "cancelled" - ] + "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"] }, "public.credential_member_role": { "name": "credential_member_role", "schema": "public", - "values": [ - "admin", - "member" - ] + "values": ["admin", "member"] }, "public.credential_member_status": { "name": "credential_member_status", "schema": "public", - "values": [ - "active", - "pending", - "revoked" - ] + "values": ["active", "pending", "revoked"] }, "public.credential_type": { "name": "credential_type", "schema": "public", - "values": [ - "oauth", - "env_workspace", - "env_personal", - "service_account" - ] + "values": ["oauth", "env_workspace", "env_personal", "service_account"] }, "public.data_drain_cadence": { "name": "data_drain_cadence", "schema": "public", - "values": [ - "hourly", - "daily" - ] + "values": ["hourly", "daily"] }, "public.data_drain_destination": { "name": "data_drain_destination", "schema": "public", - "values": [ - "s3", - "gcs", - "azure_blob", - "datadog", - "bigquery", - "snowflake", - "webhook" - ] + "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"] }, "public.data_drain_run_status": { "name": "data_drain_run_status", "schema": "public", - "values": [ - "running", - "success", - "failed" - ] + "values": ["running", "success", "failed"] }, "public.data_drain_run_trigger": { "name": "data_drain_run_trigger", "schema": "public", - "values": [ - "cron", - "manual" - ] + "values": ["cron", "manual"] }, "public.data_drain_source": { "name": "data_drain_source", "schema": "public", - "values": [ - "workflow_logs", - "job_logs", - "audit_logs", - "copilot_chats", - "copilot_runs" - ] + "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"] }, "public.execution_large_value_reference_source": { "name": "execution_large_value_reference_source", "schema": "public", - "values": [ - "execution_log", - "paused_snapshot" - ] + "values": ["execution_log", "paused_snapshot"] }, "public.invitation_kind": { "name": "invitation_kind", "schema": "public", - "values": [ - "organization", - "workspace" - ] + "values": ["organization", "workspace"] }, "public.invitation_membership_intent": { "name": "invitation_membership_intent", "schema": "public", - "values": [ - "internal", - "external" - ] + "values": ["internal", "external"] }, "public.invitation_status": { "name": "invitation_status", "schema": "public", - "values": [ - "pending", - "accepted", - "rejected", - "cancelled", - "expired" - ] + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] }, "public.permission_type": { "name": "permission_type", "schema": "public", - "values": [ - "admin", - "write", - "read" - ] + "values": ["admin", "write", "read"] }, "public.usage_log_category": { "name": "usage_log_category", "schema": "public", - "values": [ - "model", - "fixed", - "tool" - ] + "values": ["model", "fixed", "tool"] }, "public.usage_log_source": { "name": "usage_log_source", @@ -17655,10 +16832,7 @@ "public.workspace_fork_promote_direction": { "name": "workspace_fork_promote_direction", "schema": "public", - "values": [ - "push", - "pull" - ] + "values": ["push", "pull"] }, "public.workspace_fork_resource_type": { "name": "workspace_fork_resource_type", @@ -17681,11 +16855,7 @@ "public.workspace_mode": { "name": "workspace_mode", "schema": "public", - "values": [ - "personal", - "organization", - "grandfathered_shared" - ] + "values": ["personal", "organization", "grandfathered_shared"] } }, "schemas": {}, From 6f58d91b6055e321b945905da90372fd490f2c63 Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Mon, 13 Jul 2026 20:42:00 -0700 Subject: [PATCH 3/5] fix(agent-stream): satisfy biome format and import order Auto-format the sim package for CI lint:check, and repair the Anthropic streaming tool-loop payload after an unsafe delete-to-undefined rewrite. Co-authored-by: Cursor --- .../chat/components/message/message.test.tsx | 2 +- .../chat/hooks/use-chat-streaming.test.tsx | 109 ++++++------------ .../chat/hooks/use-chat-streaming.ts | 5 +- .../app/api/workflows/[id]/execute/route.ts | 5 +- .../hooks/use-workflow-execution.ts | 8 +- .../agent-stream/agent-stream-chrome.test.tsx | 9 +- .../agent-stream/agent-stream-chrome.tsx | 2 +- .../executor/execution/block-executor.test.ts | 13 ++- apps/sim/executor/execution/block-executor.ts | 4 +- apps/sim/executor/types.ts | 5 +- .../executor/human-in-the-loop-manager.ts | 2 +- .../attach-agent-stream-sink.test.ts | 2 +- apps/sim/lib/workflows/streaming/streaming.ts | 9 +- .../__fixtures__/anthropic/fixtures.test.ts | 8 +- .../providers/__fixtures__/anthropic/index.ts | 13 ++- .../anthropic/redacted-thinking-signature.ts | 3 +- .../anthropic/thinking-text-tool.ts | 3 +- .../__fixtures__/openai-compat/index.ts | 4 +- apps/sim/providers/anthropic/core.ts | 7 +- .../anthropic/streaming-tool-loop.test.ts | 5 +- .../anthropic/streaming-tool-loop.ts | 14 +-- apps/sim/providers/bedrock/index.ts | 2 +- .../providers/bedrock/streaming-tool-loop.ts | 35 ++---- apps/sim/providers/deepseek/index.ts | 10 +- apps/sim/providers/gemini/core.ts | 10 +- .../providers/gemini/streaming-tool-loop.ts | 3 +- .../sim/providers/google/utils.stream.test.ts | 9 +- apps/sim/providers/groq/index.ts | 57 ++++----- apps/sim/providers/ollama/core.ts | 2 +- .../providers/openai-compat/stream-events.ts | 2 +- .../openai-compat/streaming-tool-loop.test.ts | 7 +- .../openai-compat/streaming-tool-loop.ts | 4 +- apps/sim/providers/stream-events.test.ts | 2 +- apps/sim/providers/stream-events.ts | 3 +- apps/sim/providers/stream-pump.test.ts | 2 +- apps/sim/providers/stream-pump.ts | 5 +- apps/sim/stores/terminal/console/types.ts | 2 +- 37 files changed, 161 insertions(+), 226 deletions(-) diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx index e0e48731e65..43daf5cd018 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx @@ -31,9 +31,9 @@ vi.mock('@/app/(interfaces)/chat/components/message/components/markdown-renderer })) import { + type ChatMessage, ClientChatMessage, escapeHtml, - type ChatMessage, } from '@/app/(interfaces)/chat/components/message/message' describe('escapeHtml', () => { diff --git a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx index 3c7807ea77a..e78023dfb5c 100644 --- a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx +++ b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx @@ -139,13 +139,9 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { }) await act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false) }) await flushUiBatch() @@ -179,13 +175,9 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { }) await act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false) }) await flushUiBatch() @@ -218,13 +210,9 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { }) await act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false) }) await flushUiBatch() @@ -256,13 +244,9 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { }) await act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false) }) await flushUiBatch() @@ -291,21 +275,16 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { }) await act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false, - { + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false, { voiceSettings: { isVoiceEnabled: true, voiceId: 'voice-1', autoPlayResponses: true, }, audioStreamHandler, - } - ) + }) }) await flushUiBatch() @@ -343,14 +322,11 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { }) const streamPromise = act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false, - { abortController } - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false, { + abortController, + }) }) await flushUiBatch() @@ -384,13 +360,9 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { }) await act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false) }) await flushUiBatch() @@ -451,13 +423,9 @@ describe('useChatStreaming tool lifecycle (Step 8)', () => { }) await act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false) }) await flushUiBatch() @@ -515,14 +483,11 @@ describe('useChatStreaming tool lifecycle (Step 8)', () => { }) const streamPromise = act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false, - { abortController } - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false, { + abortController, + }) }) await flushUiBatch() @@ -556,13 +521,9 @@ describe('useChatStreaming tool lifecycle (Step 8)', () => { }) await act(async () => { - await handle.latest().handleStreamedResponse( - makeSseResponse(), - setMessages, - vi.fn(), - vi.fn(), - false - ) + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false) }) await flushUiBatch() diff --git a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts index 05e0e3bbd63..329d5a580c0 100644 --- a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts +++ b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts @@ -3,9 +3,9 @@ import { useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { humanizeToolName } from '@/lib/copilot/tools/tool-display' import { readSSEEvents } from '@/lib/core/utils/sse' import { isUserFileWithMetadata } from '@/lib/core/utils/user-file' -import { humanizeToolName } from '@/lib/copilot/tools/tool-display' import type { ChatFile, ChatMessage, @@ -357,8 +357,7 @@ export function useChatStreaming() { error: errText, }) // Non-terminal: keep streaming; surface in thinking chrome so it is visible. - accumulatedThinking += - (accumulatedThinking ? '\n\n' : '') + `[Stream error] ${errText}` + accumulatedThinking += `${accumulatedThinking ? '\n\n' : ''}[Stream error] ${errText}` accumulatedThinkingRef.current = accumulatedThinking isThinkingStreaming = true uiDirty = true diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 309e16c43c2..c71506f1263 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -87,8 +87,11 @@ import { loadDeployedWorkflowState, loadWorkflowFromNormalizedTables, } from '@/lib/workflows/persistence/utils' -import { createStreamingResponse, agentStreamProtocolResponseHeaders } from '@/lib/workflows/streaming/streaming' import { attachAgentStreamSink } from '@/lib/workflows/streaming/attach-agent-stream-sink' +import { + agentStreamProtocolResponseHeaders, + createStreamingResponse, +} from '@/lib/workflows/streaming/streaming' import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils' import { getWorkspaceBillingSettings } from '@/lib/workspaces/utils' import { executeWorkflowJob, type WorkflowExecutionPayload } from '@/background/workflow-execution' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index c1ad48bf5e2..ceac34db1ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -9,9 +9,9 @@ import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' import { requestJson } from '@/lib/api/client/request' import { cancelWorkflowExecutionContract, workflowLogContract } from '@/lib/api/contracts/workflows' +import { humanizeToolName } from '@/lib/copilot/tools/tool-display' import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans' import { processStreamingBlockLogs } from '@/lib/tokenization' -import { humanizeToolName } from '@/lib/copilot/tools/tool-display' import type { ExecutionPausedData } from '@/lib/workflows/executor/execution-events' import { collectInputFormatFiles, isFileFieldType } from '@/lib/workflows/input-format' import { @@ -1255,11 +1255,7 @@ export function useWorkflowExecution() { executionIdRef.current ) } else { - updateConsole( - data.blockId, - { agentStreamActive: false }, - executionIdRef.current - ) + updateConsole(data.blockId, { agentStreamActive: false }, executionIdRef.current) } }, diff --git a/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx b/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx index cdb0e7e6ef6..af9ddd245d4 100644 --- a/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx +++ b/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx @@ -15,10 +15,7 @@ vi.mock('@/lib/copilot/tools/tool-display', () => ({ import { AgentStreamThinkingChrome } from '@/components/agent-stream/agent-stream-chrome' -function renderChrome(props: { - thinking: string - isStreaming?: boolean -}): { +function renderChrome(props: { thinking: string; isStreaming?: boolean }): { container: HTMLDivElement rerender: (next: { thinking: string; isStreaming?: boolean }) => void unmount: () => void @@ -30,9 +27,7 @@ function renderChrome(props: { const mount = (p: { thinking: string; isStreaming?: boolean }) => { act(() => { - root.render( - - ) + root.render() }) } diff --git a/apps/sim/components/agent-stream/agent-stream-chrome.tsx b/apps/sim/components/agent-stream/agent-stream-chrome.tsx index 24dd39f1476..d6790255a57 100644 --- a/apps/sim/components/agent-stream/agent-stream-chrome.tsx +++ b/apps/sim/components/agent-stream/agent-stream-chrome.tsx @@ -1,8 +1,8 @@ 'use client' import { useEffect, useLayoutEffect, useRef, useState } from 'react' -import { Check, ChevronDown, Circle, Square, X } from 'lucide-react' import { cn } from '@sim/emcn' +import { Check, ChevronDown, Circle, Square, X } from 'lucide-react' import { humanizeToolName } from '@/lib/copilot/tools/tool-display' export type AgentStreamToolStatus = 'running' | 'success' | 'error' | 'cancelled' diff --git a/apps/sim/executor/execution/block-executor.test.ts b/apps/sim/executor/execution/block-executor.test.ts index 5c71be6c9a6..4593adbe502 100644 --- a/apps/sim/executor/execution/block-executor.test.ts +++ b/apps/sim/executor/execution/block-executor.test.ts @@ -549,9 +549,9 @@ describe('BlockExecutor streaming pump (Step 3)', () => { { type: 'text_delta', text: 'Hello ', turn: 'final' }, { type: 'text_delta', text: 'world', turn: 'final' }, ]) - expect( - state.getBlockOutput(block.id)?.providerTiming?.timeSegments?.[0]?.thinkingContent - ).toBe('hmm yes') + expect(state.getBlockOutput(block.id)?.providerTiming?.timeSegments?.[0]?.thinkingContent).toBe( + 'hmm yes' + ) }) it('drains without onStream and still persists answer content', async () => { @@ -602,7 +602,12 @@ describe('BlockExecutor streaming pump (Step 3)', () => { const result = (await originalExecute(ctx, block, inputs)) as { stream: ReadableStream streamFormat: 'agent-events-v1' - execution: { success: boolean; output: Record; logs: unknown[]; metadata: Record } + execution: { + success: boolean + output: Record + logs: unknown[] + metadata: Record + } } // Abort after a tick so the pump can project the first text_delta. queueMicrotask(() => abortController.abort('user')) diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 1dd2466557e..29ad84e9d6e 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -48,7 +48,6 @@ import { getIterationContext, } from '@/executor/utils/iteration-context' import { isJSONString } from '@/executor/utils/json' -import { createAgentStreamPump } from '@/providers/stream-pump' import { filterOutputForLog } from '@/executor/utils/output-filter' import { buildBranchNodeId, @@ -60,6 +59,7 @@ import { FUNCTION_BLOCK_DISPLAY_CODE_KEY, type VariableResolver, } from '@/executor/variables/resolver' +import { createAgentStreamPump } from '@/providers/stream-pump' import type { SerializedBlock } from '@/serializer/types' import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' @@ -974,7 +974,7 @@ function stripThinkingContentFromOutput(output: unknown): void { if (!Array.isArray(segments)) return for (const segment of segments) { if (segment && typeof segment === 'object' && 'thinkingContent' in segment) { - delete (segment as { thinkingContent?: string }).thinkingContent + ;(segment as { thinkingContent?: string }).thinkingContent = undefined } } } diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index 8f0c9dcf0cd..ec478b447b4 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -10,10 +10,7 @@ import type { SerializableExecutionState, } from '@/executor/execution/types' import type { RunFromBlockContext } from '@/executor/utils/run-from-block' -import type { - AgentStreamSink, - UnsubscribeAgentStreamSink, -} from '@/providers/stream-events' +import type { AgentStreamSink, UnsubscribeAgentStreamSink } from '@/providers/stream-events' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' import type { SubflowType } from '@/stores/workflows/workflow/types' diff --git a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts index 9df3a818cdf..b82d2a52f2e 100644 --- a/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts +++ b/apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts @@ -26,7 +26,6 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session' import { cleanupExecutionBase64Cache } from '@/lib/uploads/utils/user-file-base64.server' import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core' import type { ExecutionEvent } from '@/lib/workflows/executor/execution-events' -import { attachAgentStreamSink } from '@/lib/workflows/streaming/attach-agent-stream-sink' import { createPausedExecutionResumeMetadata, parsePausedExecutionResumeMetadata, @@ -36,6 +35,7 @@ import { normalizeAutomaticResumeWaitingReason, resolveAutomaticResumeAdmissionFailure, } from '@/lib/workflows/executor/resume-policy' +import { attachAgentStreamSink } from '@/lib/workflows/streaming/attach-agent-stream-sink' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ChildWorkflowContext, diff --git a/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.test.ts b/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.test.ts index 4e999cce476..e9d2f18063c 100644 --- a/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.test.ts +++ b/apps/sim/lib/workflows/streaming/attach-agent-stream-sink.test.ts @@ -3,8 +3,8 @@ */ import { describe, expect, it, vi } from 'vitest' import { attachAgentStreamSink } from '@/lib/workflows/streaming/attach-agent-stream-sink' -import type { AgentStreamEvent } from '@/providers/stream-events' import type { StreamingExecution } from '@/executor/types' +import type { AgentStreamEvent } from '@/providers/stream-events' describe('attachAgentStreamSink (Step 10)', () => { it('subscribes in sync window and routes thinking/tool events', async () => { diff --git a/apps/sim/lib/workflows/streaming/streaming.ts b/apps/sim/lib/workflows/streaming/streaming.ts index 6dced6c7e3b..db468d6d82b 100644 --- a/apps/sim/lib/workflows/streaming/streaming.ts +++ b/apps/sim/lib/workflows/streaming/streaming.ts @@ -24,13 +24,13 @@ import { cleanupExecutionBase64Cache, hydrateUserFilesWithBase64, } from '@/lib/uploads/utils/user-file-base64.server' -import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' -import { navigatePathAsync } from '@/executor/variables/resolvers/reference-async.server' import { AGENT_STREAM_PROTOCOL_HEADER, AGENT_STREAM_PROTOCOL_V1, shouldEmitAgentStreamEvents, } from '@/lib/workflows/streaming/agent-stream-protocol' +import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' +import { navigatePathAsync } from '@/executor/variables/resolvers/reference-async.server' import { DEFAULT_MAX_THINKING_BYTES } from '@/providers/stream-pump' /** @@ -117,7 +117,6 @@ export function agentStreamProtocolResponseHeaders(options: { return { [AGENT_STREAM_PROTOCOL_HEADER]: AGENT_STREAM_PROTOCOL_V1 } } - interface StreamingState { streamedChunks: Map processedOutputs: Set @@ -689,9 +688,7 @@ export async function createStreamingResponse( logger.info(`[${requestId}] Streaming execution aborted by client disconnect`) if (result._streamingMetadata?.loggingSession) { // LoggingSession has no cancelled status; match workflow execute route wording. - await result._streamingMetadata.loggingSession.markAsFailed( - 'Client cancelled request' - ) + await result._streamingMetadata.loggingSession.markAsFailed('Client cancelled request') } // No `final` after abort; clients that already disconnected ignore these. controller.enqueue(encodeSSE({ event: 'error', error: 'Client cancelled request' })) diff --git a/apps/sim/providers/__fixtures__/anthropic/fixtures.test.ts b/apps/sim/providers/__fixtures__/anthropic/fixtures.test.ts index eb6434d92c0..b5c90d6cd85 100644 --- a/apps/sim/providers/__fixtures__/anthropic/fixtures.test.ts +++ b/apps/sim/providers/__fixtures__/anthropic/fixtures.test.ts @@ -19,7 +19,13 @@ import { type StreamEvent = { type: string index?: number - delta?: { type: string; thinking?: string; text?: string; signature?: string; partial_json?: string } + delta?: { + type: string + thinking?: string + text?: string + signature?: string + partial_json?: string + } content_block?: { type: string thinking?: string diff --git a/apps/sim/providers/__fixtures__/anthropic/index.ts b/apps/sim/providers/__fixtures__/anthropic/index.ts index 117a8fb7769..222e7af8c71 100644 --- a/apps/sim/providers/__fixtures__/anthropic/index.ts +++ b/apps/sim/providers/__fixtures__/anthropic/index.ts @@ -2,15 +2,16 @@ * Re-exports Anthropic stream fixtures used by agent-stream-events work. * Keep fixture data in dedicated modules so adapters can import without pulling tests. */ -export { - anthropicThinkingTextToolAssembledContent, - anthropicThinkingTextToolExpectedText, - anthropicThinkingTextToolExpectedThinking, - anthropicThinkingTextToolStreamEvents, -} from '@/providers/__fixtures__/anthropic/thinking-text-tool' + export { anthropicRedactedThinkingAssembledContent, anthropicRedactedThinkingExpectedText, anthropicRedactedThinkingExpectedTraceThinking, anthropicRedactedThinkingStreamEvents, } from '@/providers/__fixtures__/anthropic/redacted-thinking-signature' +export { + anthropicThinkingTextToolAssembledContent, + anthropicThinkingTextToolExpectedText, + anthropicThinkingTextToolExpectedThinking, + anthropicThinkingTextToolStreamEvents, +} from '@/providers/__fixtures__/anthropic/thinking-text-tool' diff --git a/apps/sim/providers/__fixtures__/anthropic/redacted-thinking-signature.ts b/apps/sim/providers/__fixtures__/anthropic/redacted-thinking-signature.ts index 1f489756ddf..7e70d0a44e5 100644 --- a/apps/sim/providers/__fixtures__/anthropic/redacted-thinking-signature.ts +++ b/apps/sim/providers/__fixtures__/anthropic/redacted-thinking-signature.ts @@ -95,5 +95,4 @@ export const anthropicRedactedThinkingAssembledContent = [ export const anthropicRedactedThinkingExpectedTraceThinking = '[redacted]\n\nVisible follow-up reasoning after redaction.' -export const anthropicRedactedThinkingExpectedText = - 'Here is the answer after redacted thinking.' +export const anthropicRedactedThinkingExpectedText = 'Here is the answer after redacted thinking.' diff --git a/apps/sim/providers/__fixtures__/anthropic/thinking-text-tool.ts b/apps/sim/providers/__fixtures__/anthropic/thinking-text-tool.ts index ec183158186..5840906a7a2 100644 --- a/apps/sim/providers/__fixtures__/anthropic/thinking-text-tool.ts +++ b/apps/sim/providers/__fixtures__/anthropic/thinking-text-tool.ts @@ -115,5 +115,4 @@ export const anthropicThinkingTextToolExpectedThinking = 'I should check the weather before answering. Calling get_weather for SF.' /** Concatenated answer text (what output.content / live stream should contain today). */ -export const anthropicThinkingTextToolExpectedText = - 'Let me check the weather in San Francisco.' +export const anthropicThinkingTextToolExpectedText = 'Let me check the weather in San Francisco.' diff --git a/apps/sim/providers/__fixtures__/openai-compat/index.ts b/apps/sim/providers/__fixtures__/openai-compat/index.ts index 76ab6500b47..806c8578dc7 100644 --- a/apps/sim/providers/__fixtures__/openai-compat/index.ts +++ b/apps/sim/providers/__fixtures__/openai-compat/index.ts @@ -43,7 +43,9 @@ export const openaiCompatToolCallStartChunks = [ choices: [ { delta: { - tool_calls: [{ index: 0, id: 'call_abc', function: { name: 'http_request', arguments: '' } }], + tool_calls: [ + { index: 0, id: 'call_abc', function: { name: 'http_request', arguments: '' } }, + ], }, }, ], diff --git a/apps/sim/providers/anthropic/core.ts b/apps/sim/providers/anthropic/core.ts index 64bc7ba86fa..23fb6bc6e86 100644 --- a/apps/sim/providers/anthropic/core.ts +++ b/apps/sim/providers/anthropic/core.ts @@ -3,7 +3,12 @@ import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages/messages' import type { Logger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' -import type { BlockTokens, IterationToolCall, NormalizedBlockOutput, StreamingExecution } from '@/executor/types' +import type { + BlockTokens, + IterationToolCall, + NormalizedBlockOutput, + StreamingExecution, +} from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { createAnthropicStreamingToolLoopStream } from '@/providers/anthropic/streaming-tool-loop' import { diff --git a/apps/sim/providers/anthropic/streaming-tool-loop.test.ts b/apps/sim/providers/anthropic/streaming-tool-loop.test.ts index e4e4b7824da..681b030c6bb 100644 --- a/apps/sim/providers/anthropic/streaming-tool-loop.test.ts +++ b/apps/sim/providers/anthropic/streaming-tool-loop.test.ts @@ -62,10 +62,7 @@ function makeFinalMessage(overrides: { } } -function makeMessageStream( - events: unknown[], - finalMessage: ReturnType -) { +function makeMessageStream(events: unknown[], finalMessage: ReturnType) { return { async *[Symbol.asyncIterator]() { for (const event of events) { diff --git a/apps/sim/providers/anthropic/streaming-tool-loop.ts b/apps/sim/providers/anthropic/streaming-tool-loop.ts index 12e191d5b7d..1d0899b3116 100644 --- a/apps/sim/providers/anthropic/streaming-tool-loop.ts +++ b/apps/sim/providers/anthropic/streaming-tool-loop.ts @@ -18,11 +18,7 @@ import { checkForForcedToolUsage } from '@/providers/anthropic/utils' import type { AgentStreamEvent, ToolCallEndStatus } from '@/providers/stream-events' import { enrichLastModelSegment } from '@/providers/trace-enrichment' import type { ProviderRequest, TimeSegment } from '@/providers/types' -import { - calculateCost, - prepareToolExecution, - sumToolCosts, -} from '@/providers/utils' +import { calculateCost, prepareToolExecution, sumToolCosts } from '@/providers/utils' import { executeTool } from '@/tools' /** Custom payload fields shared with `core.ts` (adaptive thinking, output_format). */ @@ -181,7 +177,7 @@ export function createAnthropicStreamingToolLoopStream( messages: currentMessages, } // Streaming tool loop always streams each turn; never pass stream:true twice. - delete (turnPayload as { stream?: boolean }).stream + ;(turnPayload as { stream?: boolean }).stream = undefined // Forced tool_choice vs thinking — same rules as silent loop. const thinkingEnabled = !!payload.thinking @@ -522,11 +518,7 @@ export function createAnthropicStreamingToolLoopStream( throw new DOMException('Stream aborted', 'AbortError') } } catch (error) { - settleOpenTools( - controller, - openToolStarts, - isAbortError(error) ? 'cancelled' : 'error' - ) + settleOpenTools(controller, openToolStarts, isAbortError(error) ? 'cancelled' : 'error') throw error } } diff --git a/apps/sim/providers/bedrock/index.ts b/apps/sim/providers/bedrock/index.ts index 8bda948d84d..6a0b7621285 100644 --- a/apps/sim/providers/bedrock/index.ts +++ b/apps/sim/providers/bedrock/index.ts @@ -18,13 +18,13 @@ import { getErrorMessage, toError } from '@sim/utils/errors' import type { IterationToolCall, NormalizedBlockOutput, StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { buildBedrockMessageContent } from '@/providers/attachments' +import { createBedrockStreamingToolLoopStream } from '@/providers/bedrock/streaming-tool-loop' import { checkForForcedToolUsage, createReadableStreamFromBedrockStream, generateToolUseId, getBedrockInferenceProfileId, } from '@/providers/bedrock/utils' -import { createBedrockStreamingToolLoopStream } from '@/providers/bedrock/streaming-tool-loop' import { getCachedProviderClient } from '@/providers/client-cache' import { getProviderDefaultModel, getProviderModels } from '@/providers/models' import { createStreamingExecution } from '@/providers/streaming-execution' diff --git a/apps/sim/providers/bedrock/streaming-tool-loop.ts b/apps/sim/providers/bedrock/streaming-tool-loop.ts index b3e39816ea3..2c8e1a1cd8e 100644 --- a/apps/sim/providers/bedrock/streaming-tool-loop.ts +++ b/apps/sim/providers/bedrock/streaming-tool-loop.ts @@ -6,11 +6,11 @@ */ import { + type Message as BedrockMessage, + type BedrockRuntimeClient, type ContentBlock, type ConversationRole, ConverseStreamCommand, - type BedrockRuntimeClient, - type Message as BedrockMessage, type SystemContentBlock, type Tool, type ToolConfiguration, @@ -25,11 +25,7 @@ import { checkForForcedToolUsage, generateToolUseId } from '@/providers/bedrock/ import type { AgentStreamEvent, ToolCallEndStatus } from '@/providers/stream-events' import { enrichLastModelSegment } from '@/providers/trace-enrichment' import type { ProviderRequest, TimeSegment } from '@/providers/types' -import { - calculateCost, - prepareToolExecution, - sumToolCosts, -} from '@/providers/utils' +import { calculateCost, prepareToolExecution, sumToolCosts } from '@/providers/utils' import { executeTool } from '@/tools' export interface BedrockStreamingToolLoopComplete { @@ -238,11 +234,7 @@ export function createBedrockStreamingToolLoopStream( throw new Error('No stream returned from Bedrock') } - const drained = await drainBedrockTurn( - streamResponse.stream, - controller, - openToolStarts - ) + const drained = await drainBedrockTurn(streamResponse.stream, controller, openToolStarts) const modelEnd = Date.now() const thisModelTime = modelEnd - modelStart modelTime += thisModelTime @@ -441,15 +433,7 @@ export function createBedrockStreamingToolLoopStream( const toolResultContent: ContentBlock[] = [] for (const value of orderedResults) { - const { - toolUseId, - toolName, - toolParams, - result, - startTime, - endTime, - duration, - } = value + const { toolUseId, toolName, toolParams, result, startTime, endTime, duration } = value timeSegments.push({ type: 'tool', @@ -496,9 +480,14 @@ export function createBedrockStreamingToolLoopStream( }) } - if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) { + if ( + typeof originalToolChoice === 'object' && + hasUsedForcedTool && + forcedTools.length > 0 + ) { const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool)) - toolChoice = remainingTools.length > 0 ? { tool: { name: remainingTools[0] } } : { auto: {} } + toolChoice = + remainingTools.length > 0 ? { tool: { name: remainingTools[0] } } : { auto: {} } } else if (hasUsedForcedTool && typeof originalToolChoice === 'object') { toolChoice = { auto: {} } } diff --git a/apps/sim/providers/deepseek/index.ts b/apps/sim/providers/deepseek/index.ts index 109592c41e7..8e9812319d6 100644 --- a/apps/sim/providers/deepseek/index.ts +++ b/apps/sim/providers/deepseek/index.ts @@ -148,15 +148,9 @@ export const deepseekProvider: ProviderConfig = { basePayload: payload, messages: formattedMessages as any, createStream: async (params, options) => - deepseek.chat.completions.create( - { ...params, stream: true }, - options - ), + deepseek.chat.completions.create({ ...params, stream: true }, options), createBlocking: async (params, options) => - deepseek.chat.completions.create( - { ...params, stream: false }, - options - ), + deepseek.chat.completions.create({ ...params, stream: false }, options), logger, timeSegments, forcedTools, diff --git a/apps/sim/providers/gemini/core.ts b/apps/sim/providers/gemini/core.ts index 67b2d8f5815..a7d2690be22 100644 --- a/apps/sim/providers/gemini/core.ts +++ b/apps/sim/providers/gemini/core.ts @@ -15,6 +15,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import type { IterationToolCall, NormalizedBlockOutput, StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' +import { createGeminiStreamingToolLoopStream } from '@/providers/gemini/streaming-tool-loop' import { checkForForcedToolUsage, cleanSchemaForGemini, @@ -28,10 +29,9 @@ import { mapToThinkingLevel, supportsDisablingGemini25Thinking, } from '@/providers/google/utils' -import { enrichLastModelSegment } from '@/providers/trace-enrichment' -import { ensureToolCallId } from '@/providers/tool-call-id' import { createStreamingExecution } from '@/providers/streaming-execution' -import { createGeminiStreamingToolLoopStream } from '@/providers/gemini/streaming-tool-loop' +import { ensureToolCallId } from '@/providers/tool-call-id' +import { enrichLastModelSegment } from '@/providers/trace-enrichment' import type { FunctionCallResponse, ProviderRequest, @@ -1280,7 +1280,9 @@ export async function executeGeminiRequest( if (thinking) { const segments = streamingResult.execution.output.providerTiming?.timeSegments - const lastModel = segments ? [...segments].reverse().find((s) => s.type === 'model') : undefined + const lastModel = segments + ? [...segments].reverse().find((s) => s.type === 'model') + : undefined if (lastModel) { lastModel.thinkingContent = thinking } diff --git a/apps/sim/providers/gemini/streaming-tool-loop.ts b/apps/sim/providers/gemini/streaming-tool-loop.ts index b9de301ae05..21eb5229f75 100644 --- a/apps/sim/providers/gemini/streaming-tool-loop.ts +++ b/apps/sim/providers/gemini/streaming-tool-loop.ts @@ -91,8 +91,7 @@ function buildNextConfig( model: string ): GenerateContentConfig { const nextConfig = { ...baseConfig } - const allForcedToolsUsed = - forcedTools.length > 0 && usedForcedTools.length === forcedTools.length + const allForcedToolsUsed = forcedTools.length > 0 && usedForcedTools.length === forcedTools.length if (allForcedToolsUsed && request.responseFormat) { nextConfig.tools = undefined diff --git a/apps/sim/providers/google/utils.stream.test.ts b/apps/sim/providers/google/utils.stream.test.ts index b9085efc92e..570be2733e2 100644 --- a/apps/sim/providers/google/utils.stream.test.ts +++ b/apps/sim/providers/google/utils.stream.test.ts @@ -67,8 +67,11 @@ describe('createReadableStreamFromGeminiStream (Step 9)', () => { ) const events = await collectEvents(stream) expect(events.some((e) => e.type === 'thinking_delta')).toBe(false) - expect(events.filter((e) => e.type === 'text_delta').map((e) => e.text).join('')).toContain( - 'Just text' - ) + expect( + events + .filter((e) => e.type === 'text_delta') + .map((e) => e.text) + .join('') + ).toContain('Just text') }) }) diff --git a/apps/sim/providers/groq/index.ts b/apps/sim/providers/groq/index.ts index fdac6a74a3b..4d8c71ced6f 100644 --- a/apps/sim/providers/groq/index.ts +++ b/apps/sim/providers/groq/index.ts @@ -161,15 +161,9 @@ export const groqProvider: ProviderConfig = { basePayload: payload, messages: formattedMessages as any, createStream: async (params, options) => - groq.chat.completions.create( - { ...params, stream: true }, - options - ) as any, + groq.chat.completions.create({ ...params, stream: true }, options) as any, createBlocking: async (params, options) => - groq.chat.completions.create( - { ...params, stream: false }, - options - ) as any, + groq.chat.completions.create({ ...params, stream: false }, options) as any, logger, timeSegments, forcedTools, @@ -217,35 +211,32 @@ export const groqProvider: ProviderConfig = { isStreaming: true, streamFormat: 'agent-events-v1', createStream: ({ output }) => - createReadableStreamFromGroqStream( - streamResponse as any, - (content, usage, thinking) => { - output.content = content - output.tokens = { - input: usage.prompt_tokens, - output: usage.completion_tokens, - total: usage.total_tokens, - } + createReadableStreamFromGroqStream(streamResponse as any, (content, usage, thinking) => { + output.content = content + output.tokens = { + input: usage.prompt_tokens, + output: usage.completion_tokens, + total: usage.total_tokens, + } - const costResult = calculateCost( - request.model, - usage.prompt_tokens, - usage.completion_tokens - ) - output.cost = { - input: costResult.input, - output: costResult.output, - total: costResult.total, - } + const costResult = calculateCost( + request.model, + usage.prompt_tokens, + usage.completion_tokens + ) + output.cost = { + input: costResult.input, + output: costResult.output, + total: costResult.total, + } - if (thinking) { - const segment = output.providerTiming?.timeSegments?.[0] - if (segment) { - segment.thinkingContent = thinking - } + if (thinking) { + const segment = output.providerTiming?.timeSegments?.[0] + if (segment) { + segment.thinkingContent = thinking } } - ), + }), }) return streamingResult diff --git a/apps/sim/providers/ollama/core.ts b/apps/sim/providers/ollama/core.ts index 3d5d23295e8..c41134fac80 100644 --- a/apps/sim/providers/ollama/core.ts +++ b/apps/sim/providers/ollama/core.ts @@ -9,8 +9,8 @@ import type { CompletionUsage } from 'openai/resources/completions' import type { StreamingExecution } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { formatMessagesForProvider } from '@/providers/attachments' -import { createStreamingExecution } from '@/providers/streaming-execution' import type { AgentStreamEvent } from '@/providers/stream-events' +import { createStreamingExecution } from '@/providers/streaming-execution' import { adaptOpenAIChatToolSchema } from '@/providers/tool-schema-adapter' import { enrichLastModelSegmentFromChatCompletions } from '@/providers/trace-enrichment' import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types' diff --git a/apps/sim/providers/openai-compat/stream-events.ts b/apps/sim/providers/openai-compat/stream-events.ts index 0fd1b45dc32..9097595e095 100644 --- a/apps/sim/providers/openai-compat/stream-events.ts +++ b/apps/sim/providers/openai-compat/stream-events.ts @@ -7,9 +7,9 @@ * Tool-call argument deltas are accumulated for tool-loop history. */ +import { createLogger } from '@sim/logger' import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' -import { createLogger } from '@sim/logger' import type { AgentStreamEvent, TextDeltaTurn } from '@/providers/stream-events' export interface OpenAICompatAssembledToolCall { diff --git a/apps/sim/providers/openai-compat/streaming-tool-loop.test.ts b/apps/sim/providers/openai-compat/streaming-tool-loop.test.ts index 32f60ebdfcc..e867454a0b2 100644 --- a/apps/sim/providers/openai-compat/streaming-tool-loop.test.ts +++ b/apps/sim/providers/openai-compat/streaming-tool-loop.test.ts @@ -49,7 +49,12 @@ function toolThenAnswerChunks(toolName: string, args: string, answer: string) { delta: { reasoning_content: 'I should call the tool. ', tool_calls: [ - { index: 0, id: 'call_1', type: 'function', function: { name: toolName, arguments: '' } }, + { + index: 0, + id: 'call_1', + type: 'function', + function: { name: toolName, arguments: '' }, + }, ], }, }, diff --git a/apps/sim/providers/openai-compat/streaming-tool-loop.ts b/apps/sim/providers/openai-compat/streaming-tool-loop.ts index f18de36e98e..958b431bd4b 100644 --- a/apps/sim/providers/openai-compat/streaming-tool-loop.ts +++ b/apps/sim/providers/openai-compat/streaming-tool-loop.ts @@ -11,10 +11,10 @@ * `tool_calls` deltas (no blocking hybrid). */ -import type OpenAI from 'openai' -import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { Logger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' +import type OpenAI from 'openai' +import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { NormalizedBlockOutput } from '@/executor/types' import { MAX_TOOL_ITERATIONS } from '@/providers' import { diff --git a/apps/sim/providers/stream-events.test.ts b/apps/sim/providers/stream-events.test.ts index 477ed4abc42..0cf8624eb5a 100644 --- a/apps/sim/providers/stream-events.test.ts +++ b/apps/sim/providers/stream-events.test.ts @@ -3,13 +3,13 @@ */ import { describe, expect, it } from 'vitest' import { + type AgentStreamEvent, createAgentEventReadableStream, isAgentEventReadableStream, isAgentStreamEvent, isAgentStreamFormat, isTextDeltaTurn, isToolCallEndStatus, - type AgentStreamEvent, } from '@/providers/stream-events' describe('stream-events contract', () => { diff --git a/apps/sim/providers/stream-events.ts b/apps/sim/providers/stream-events.ts index 08d73fbe6c9..45c35eefb19 100644 --- a/apps/sim/providers/stream-events.ts +++ b/apps/sim/providers/stream-events.ts @@ -70,8 +70,7 @@ export function isAgentStreamEvent(value: unknown): value is AgentStreamEvent { switch (value.type) { case 'text_delta': return ( - typeof value.text === 'string' && - (value.turn === undefined || isTextDeltaTurn(value.turn)) + typeof value.text === 'string' && (value.turn === undefined || isTextDeltaTurn(value.turn)) ) case 'thinking_delta': return typeof value.text === 'string' diff --git a/apps/sim/providers/stream-pump.test.ts b/apps/sim/providers/stream-pump.test.ts index 98fd5bd7ad2..9fc05304199 100644 --- a/apps/sim/providers/stream-pump.test.ts +++ b/apps/sim/providers/stream-pump.test.ts @@ -3,9 +3,9 @@ */ import { describe, expect, it, vi } from 'vitest' import { - createAgentEventReadableStream, type AgentStreamEvent, type AgentStreamSink, + createAgentEventReadableStream, } from '@/providers/stream-events' import { createAgentStreamPump, DEFAULT_MAX_THINKING_BYTES } from '@/providers/stream-pump' diff --git a/apps/sim/providers/stream-pump.ts b/apps/sim/providers/stream-pump.ts index 21cb0194d28..1e4ff84f4d1 100644 --- a/apps/sim/providers/stream-pump.ts +++ b/apps/sim/providers/stream-pump.ts @@ -13,8 +13,8 @@ import { type AgentStreamEvent, type AgentStreamFormat, type AgentStreamSink, - type UnsubscribeAgentStreamSink, isAgentStreamEvent, + type UnsubscribeAgentStreamSink, } from '@/providers/stream-events' /** Default cap on thinking text forwarded to sinks (UTF-16 code units via `.length`). */ @@ -231,8 +231,7 @@ export function createAgentStreamPump(options: CreateAgentStreamPumpOptions): Ag if (event.type === 'thinking_delta') { const remaining = Math.max(0, maxThinkingBytes - thinkingBytesForwarded) if (remaining <= 0) return - const forwarded = - event.text.length > remaining ? event.text.slice(0, remaining) : event.text + const forwarded = event.text.length > remaining ? event.text.slice(0, remaining) : event.text thinkingBytesForwarded += forwarded.length if (forwarded.length > 0) { await dispatchToSinks({ type: 'thinking_delta', text: forwarded }) diff --git a/apps/sim/stores/terminal/console/types.ts b/apps/sim/stores/terminal/console/types.ts index 079a8d91a74..7939ee826e6 100644 --- a/apps/sim/stores/terminal/console/types.ts +++ b/apps/sim/stores/terminal/console/types.ts @@ -1,6 +1,6 @@ +import type { AgentStreamToolCall } from '@/components/agent-stream/agent-stream-chrome' import type { ParentIteration } from '@/executor/execution/types' import type { NormalizedBlockOutput } from '@/executor/types' -import type { AgentStreamToolCall } from '@/components/agent-stream/agent-stream-chrome' import type { SubflowType } from '@/stores/workflows/workflow/types' export interface ConsoleEntry { From 3e139eb990cf522eaee238690be6d8409f1d9e9b Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Mon, 13 Jul 2026 20:46:50 -0700 Subject: [PATCH 4/5] fix(agent-stream): keep drained answer on abort and update migration journal test Treat AbortError from reader.cancel as a cancelled pump result so soft-complete retains answerText. Point the workspace storage migration journal assertion at 0261_chat_include_thinking. Co-authored-by: Cursor --- .../executor/execution/block-executor.test.ts | 24 +++++------------- apps/sim/providers/stream-pump.test.ts | 25 +++++++++++++++++++ apps/sim/providers/stream-pump.ts | 12 ++++++++- ...space-storage-accounting-migration.test.ts | 2 +- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/apps/sim/executor/execution/block-executor.test.ts b/apps/sim/executor/execution/block-executor.test.ts index 4593adbe502..768429a0954 100644 --- a/apps/sim/executor/execution/block-executor.test.ts +++ b/apps/sim/executor/execution/block-executor.test.ts @@ -596,23 +596,6 @@ describe('BlockExecutor streaming pump (Step 3)', () => { { type: 'thinking_delta', text: 'more' }, ], }) - // Override execute to abort mid-stream after first text chunk is projected. - const originalExecute = handler.execute.bind(handler) - handler.execute = async (ctx, block, inputs) => { - const result = (await originalExecute(ctx, block, inputs)) as { - stream: ReadableStream - streamFormat: 'agent-events-v1' - execution: { - success: boolean - output: Record - logs: unknown[] - metadata: Record - } - } - // Abort after a tick so the pump can project the first text_delta. - queueMicrotask(() => abortController.abort('user')) - return result - } const { executor, block, state } = createExecutor(handler) const ctx = createContext(state) @@ -621,6 +604,10 @@ describe('BlockExecutor streaming pump (Step 3)', () => { streamingExec.subscribe?.({ onEvent: async () => {} }) const reader = streamingExec.stream.getReader() try { + // Drain the first projected answer chunk, then Stop — pump must keep it. + const first = await reader.read() + expect(first.done).toBe(false) + abortController.abort('user') while (true) { const { done } = await reader.read() if (done) break @@ -634,7 +621,8 @@ describe('BlockExecutor streaming pump (Step 3)', () => { const output = state.getBlockOutput(block.id) expect(output?.error).toBeUndefined() - // May or may not have content depending on race; must not be a failed error output. + // Soft-complete must keep text already projected before Stop — not empty content. + expect(output?.content).toBe('partial answer') expect(output).not.toMatchObject({ error: expect.any(String) }) }) diff --git a/apps/sim/providers/stream-pump.test.ts b/apps/sim/providers/stream-pump.test.ts index 9fc05304199..c3826bd4703 100644 --- a/apps/sim/providers/stream-pump.test.ts +++ b/apps/sim/providers/stream-pump.test.ts @@ -337,6 +337,31 @@ describe('createAgentStreamPump', () => { expect(result.fullyDrained).toBe(false) }) + it('preserves drained answerText when user abort surfaces as AbortError from reader', async () => { + const controller = new AbortController() + const source = new ReadableStream({ + start(c) { + c.enqueue({ type: 'text_delta', text: 'kept answer', turn: 'final' }) + c.enqueue({ type: 'thinking_delta', text: 'still thinking' }) + // Abort while the reader is waiting for more — cancel() rejects read(). + queueMicrotask(() => controller.abort('user')) + }, + }) + + const pump = createAgentStreamPump({ + source, + streamFormat: 'agent-events-v1', + sinkMode: true, + abortSignal: controller.signal, + }) + + const result = await pump.run() + expect(result.cancelled).toBe(true) + expect(result.cancelReason).toBe('user') + expect(result.answerText).toBe('kept answer') + expect(result.fullyDrained).toBe(false) + }) + it('does not start until run() — subscribe-before-pull', async () => { let pulled = false const source = new ReadableStream({ diff --git a/apps/sim/providers/stream-pump.ts b/apps/sim/providers/stream-pump.ts index 1e4ff84f4d1..c8bc2ef3b7c 100644 --- a/apps/sim/providers/stream-pump.ts +++ b/apps/sim/providers/stream-pump.ts @@ -335,6 +335,16 @@ export function createAgentStreamPump(options: CreateAgentStreamPumpOptions): Ag } } + // reader.cancel() / aborted read often surfaces as AbortError. That is the + // cancel path, not a hard drain failure — keep accumulated answerText. + const isAbortError = + (drainError instanceof DOMException && drainError.name === 'AbortError') || + (drainError instanceof Error && drainError.name === 'AbortError') + if (!cancelled && isAbortError && abortSignal?.aborted) { + cancelled = true + cancelReason = resolveCancelReason(abortSignal) + } + // Synthesize tool ends — enqueue-then-error in provider loops can drop // settlement frames (WHATWG resets the queue on error). if (cancelled) { @@ -343,7 +353,7 @@ export function createAgentStreamPump(options: CreateAgentStreamPumpOptions): Ag await settleOpenTools('error') } - if (drainError) { + if (drainError && !cancelled) { closeTextStream(drainError) throw drainError instanceof Error ? drainError : new Error(String(drainError)) } diff --git a/packages/db/workspace-storage-accounting-migration.test.ts b/packages/db/workspace-storage-accounting-migration.test.ts index 2b86c008d9d..9f9e4858bac 100644 --- a/packages/db/workspace-storage-accounting-migration.test.ts +++ b/packages/db/workspace-storage-accounting-migration.test.ts @@ -68,6 +68,6 @@ describe('workspace storage accounting migration', () => { readFileSync(new URL('./migrations/meta/_journal.json', import.meta.url), 'utf8') ) as { entries: Array<{ idx: number; tag: string }> } const lastEntry = journal.entries[journal.entries.length - 1] - expect(lastEntry).toMatchObject({ idx: 260, tag: '0260_unknown_sinister_six' }) + expect(lastEntry).toMatchObject({ idx: 261, tag: '0261_chat_include_thinking' }) }) }) From 3b9e24dfc78d9bdefcee19a267b7be99d363659d Mon Sep 17 00:00:00 2001 From: Bill Leoutsakos Date: Mon, 13 Jul 2026 20:55:34 -0700 Subject: [PATCH 5/5] fix(chat): keep Stop notice when server emits cancel error Ignore terminal SSE error frames after the user aborts so "Client cancelled request" cannot overwrite "Response stopped by user". Co-authored-by: Cursor --- .../chat/hooks/use-chat-streaming.test.tsx | 52 +++++++++++++++++++ .../chat/hooks/use-chat-streaming.ts | 26 ++++++++++ 2 files changed, 78 insertions(+) diff --git a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx index e78023dfb5c..652fee8b7dc 100644 --- a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx +++ b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.test.tsx @@ -347,6 +347,58 @@ describe('useChatStreaming thinking + abort (Step 6)', () => { expect(assistant?.isThinkingStreaming).toBe(false) }) + it('does not replace Stop notice with server Client cancelled request error', async () => { + const abortController = new AbortController() + let resolveStream!: () => void + const streamDone = new Promise((resolve) => { + resolveStream = resolve + }) + + mockReadSSEEvents.mockImplementation(async (_source, options) => { + await options.onEvent({ + blockId: 'agent-1', + chunk: 'partial answer', + }) + await new Promise((resolve) => { + options.signal?.addEventListener( + 'abort', + () => { + // Server still emits terminal cancel error while the reader finishes. + void options.onEvent({ + event: 'error', + error: 'Client cancelled request', + }) + resolve() + }, + { once: true } + ) + streamDone.then(() => resolve()) + }) + }) + + const streamPromise = act(async () => { + await handle + .latest() + .handleStreamedResponse(makeSseResponse(), setMessages, vi.fn(), vi.fn(), false, { + abortController, + }) + }) + + await flushUiBatch() + + act(() => { + handle.latest().stopStreaming(setMessages) + }) + resolveStream() + await streamPromise + + const assistant = messages.find((m) => m.id === 'msg-assistant-1') + expect(String(assistant?.content)).toContain('partial answer') + expect(String(assistant?.content)).toContain('Response stopped by user') + expect(String(assistant?.content)).not.toContain('Client cancelled request') + expect(assistant?.isStreaming).toBe(false) + }) + it('leaves thinking undefined when no thinking events arrive', async () => { mockReadSSEEvents.mockImplementation(async (_source, options) => { await options.onEvent({ diff --git a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts index 329d5a580c0..5ddbe789ccd 100644 --- a/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts +++ b/apps/sim/app/(interfaces)/chat/hooks/use-chat-streaming.ts @@ -322,6 +322,32 @@ export function useChatStreaming() { const { blockId, chunk: contentChunk, event: eventType } = json if (eventType === 'error' || json.event === 'error') { + // User Stop aborts the fetch; the server often still emits a terminal + // `{ event: 'error', error: 'Client cancelled request' }` before the + // SSE reader finishes. Do not overwrite the stop notice. + if (streamAbortSignal.aborted) { + settleInFlightTools(toolCallsMap, 'cancelled') + syncToolCallsRef() + const toolsSnapshot = snapshotToolCalls(toolCallOrder, toolCallsMap) + setMessages((prev) => + prev.map((msg) => + msg.id === messageId + ? { + ...msg, + isStreaming: false, + isThinkingStreaming: false, + isToolStreaming: false, + thinking: accumulatedThinking || msg.thinking, + toolCalls: toolsSnapshot ?? msg.toolCalls, + } + : msg + ) + ) + setIsLoading(false) + terminated = true + return true + } + const errorMessage = json.error || CHAT_ERROR_MESSAGES.GENERIC_ERROR settleInFlightTools(toolCallsMap, 'error') syncToolCallsRef()