Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/sim/lib/copilot/chat/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toError } from '@sim/utils/errors'
import { LRUCache } from 'lru-cache'
import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription'
import { isPaid } from '@/lib/billing/plan-helpers'
import type { VfsSnapshotV1 } from '@/lib/copilot/generated/vfs-snapshot-v1'
import { getExposedIntegrationTools } from '@/lib/copilot/integration-tools'
import { getToolEntry } from '@/lib/copilot/tool-executor/router'
import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions'
Expand Down Expand Up @@ -33,6 +34,7 @@ interface BuildPayloadParams {
prefetch?: boolean
implicitFeedback?: string
workspaceContext?: string
vfs?: VfsSnapshotV1
userPermission?: string
userTimezone?: string
userMetadata?: {
Expand Down Expand Up @@ -366,6 +368,7 @@ export async function buildCopilotRequestPayload(
...(mothershipTools.length > 0 ? { mothershipTools } : {}),
...(commands && commands.length > 0 ? { commands } : {}),
...(params.workspaceContext ? { workspaceContext: params.workspaceContext } : {}),
...(params.vfs ? { vfs: params.vfs } : {}),
...(params.userPermission ? { userPermission: params.userPermission } : {}),
...(params.userTimezone ? { userTimezone: params.userTimezone } : {}),
...(params.userMetadata &&
Expand Down
17 changes: 12 additions & 5 deletions apps/sim/lib/copilot/chat/post.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const getUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions

const {
getEffectiveDecryptedEnv,
generateWorkspaceContext,
generateWorkspaceSnapshot,
processContextsServer,
resolveActiveResourceContext,
buildCopilotRequestPayload,
Expand All @@ -31,7 +31,7 @@ const {
mockPublishStatusChanged,
} = vi.hoisted(() => ({
getEffectiveDecryptedEnv: vi.fn(),
generateWorkspaceContext: vi.fn(),
generateWorkspaceSnapshot: vi.fn(),
processContextsServer: vi.fn(),
resolveActiveResourceContext: vi.fn(),
buildCopilotRequestPayload: vi.fn(),
Expand All @@ -56,7 +56,7 @@ vi.mock('@/lib/environment/utils', () => ({
}))

vi.mock('@/lib/copilot/chat/workspace-context', () => ({
generateWorkspaceContext,
generateWorkspaceSnapshot,
}))

vi.mock('@/lib/copilot/chat/process-contents', () => ({
Expand Down Expand Up @@ -142,7 +142,10 @@ describe('handleUnifiedChatPost', () => {
})
getUserEntityPermissions.mockResolvedValue('write')
getEffectiveDecryptedEnv.mockResolvedValue({ API_KEY: 'secret' })
generateWorkspaceContext.mockResolvedValue('workspace context')
generateWorkspaceSnapshot.mockResolvedValue({
markdown: 'workspace context',
snapshot: { workflows: [{ id: 'wf-1', name: 'Alpha', path: 'workflows/Alpha' }] },
})
processContextsServer.mockResolvedValue([])
resolveActiveResourceContext.mockResolvedValue(null)
buildCopilotRequestPayload.mockImplementation(async (params: Record<string, unknown>) => params)
Expand Down Expand Up @@ -178,11 +181,13 @@ describe('handleUnifiedChatPost', () => {
)

expect(response.status).toBe(200)
expect(generateWorkspaceContext).toHaveBeenCalledWith('ws-1', 'user-1')
expect(generateWorkspaceSnapshot).toHaveBeenCalledWith('ws-1', 'user-1')
expect(buildCopilotRequestPayload).toHaveBeenCalledWith(
expect.objectContaining({
model: 'claude-opus-4-8',
workspaceContext: 'workspace context',
// Regression guard: the branch must forward the typed snapshot, not drop it.
vfs: expect.objectContaining({ workflows: expect.any(Array) }),
}),
{ selectedModel: 'claude-opus-4-8' }
)
Expand Down Expand Up @@ -221,6 +226,8 @@ describe('handleUnifiedChatPost', () => {
expect.objectContaining({
workspaceId: 'ws-1',
workspaceContext: 'workspace context',
// Regression guard: the branch must forward the typed snapshot, not drop it.
vfs: expect.objectContaining({ workflows: expect.any(Array) }),
}),
{ selectedModel: '' }
)
Expand Down
18 changes: 15 additions & 3 deletions apps/sim/lib/copilot/chat/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
resolveActiveResourceContext,
} from '@/lib/copilot/chat/process-contents'
import { finalizeAssistantTurn } from '@/lib/copilot/chat/terminal-state'
import { generateWorkspaceContext } from '@/lib/copilot/chat/workspace-context'
import { generateWorkspaceSnapshot } from '@/lib/copilot/chat/workspace-context'
import { chatPubSub } from '@/lib/copilot/chat-status'
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/constants'
import {
Expand All @@ -32,6 +32,7 @@ import {
} from '@/lib/copilot/generated/trace-attribute-values-v1'
import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1'
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
import type { VfsSnapshotV1 } from '@/lib/copilot/generated/vfs-snapshot-v1'
import { createBadRequestResponse, createUnauthorizedResponse } from '@/lib/copilot/request/http'
import { createSSEStream, SSE_RESPONSE_HEADERS } from '@/lib/copilot/request/lifecycle/start'
import { startCopilotOtelRoot, withCopilotSpan } from '@/lib/copilot/request/otel'
Expand Down Expand Up @@ -183,6 +184,7 @@ type UnifiedChatBranch =
prefetch?: boolean
implicitFeedback?: string
workspaceContext?: string
vfs?: VfsSnapshotV1
}) => Promise<Record<string, unknown>>
buildExecutionContext: (params: {
userId: string
Expand Down Expand Up @@ -210,6 +212,7 @@ type UnifiedChatBranch =
userTimezone?: string
userMetadata?: { name?: string; email?: string; timezone?: string }
workspaceContext?: string
vfs?: VfsSnapshotV1
}) => Promise<Record<string, unknown>>
buildExecutionContext: (params: {
userId: string
Expand Down Expand Up @@ -616,6 +619,7 @@ async function resolveBranch(params: {
prefetch: payloadParams.prefetch,
implicitFeedback: payloadParams.implicitFeedback,
workspaceContext: payloadParams.workspaceContext,
vfs: payloadParams.vfs,
userPermission: payloadParams.userPermission,
userTimezone: payloadParams.userTimezone,
userMetadata: payloadParams.userMetadata,
Expand Down Expand Up @@ -675,6 +679,7 @@ async function resolveBranch(params: {
fileAttachments: payloadParams.fileAttachments,
chatId: payloadParams.chatId,
workspaceContext: payloadParams.workspaceContext,
vfs: payloadParams.vfs,
userPermission: payloadParams.userPermission,
userTimezone: payloadParams.userTimezone,
userMetadata: payloadParams.userMetadata,
Expand Down Expand Up @@ -898,7 +903,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
? withCopilotSpan(
TraceSpan.CopilotChatBuildWorkspaceContext,
{ [TraceAttr.WorkspaceId]: workspaceId },
() => generateWorkspaceContext(workspaceId, authenticatedUserId),
() => generateWorkspaceSnapshot(workspaceId, authenticatedUserId),
activeOtelRoot.context
)
: Promise.resolve(undefined)
Expand Down Expand Up @@ -943,14 +948,19 @@ export async function handleUnifiedChatPost(req: NextRequest) {
activeOtelRoot.context
)

const [agentContexts, userPermission, workspaceContext, , executionContext] =
const [agentContexts, userPermission, workspaceSnapshot, , executionContext] =
await Promise.all([
agentContextsPromise,
userPermissionPromise,
workspaceContextPromise,
persistUserMessagePromise,
executionContextPromise,
])
// Both halves come from one primary-db fetch (workspace-context.ts):
// `workspaceContext` is the markdown transition fallback, `vfs` is the
// typed snapshot Go diffs into baseline+delta messages.
const workspaceContext = workspaceSnapshot?.markdown
const vfs = workspaceSnapshot?.snapshot

executionContext.userPermission = userPermission ?? undefined

Expand Down Expand Up @@ -987,6 +997,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
prefetch: body.prefetch,
implicitFeedback: body.implicitFeedback,
workspaceContext,
vfs,
})
: branch.buildPayload({
message: body.message,
Expand All @@ -999,6 +1010,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
userTimezone: body.userTimezone,
userMetadata,
workspaceContext,
vfs,
}),
activeOtelRoot.context
)
Expand Down
154 changes: 154 additions & 0 deletions apps/sim/lib/copilot/chat/workspace-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,157 @@ describe('buildWorkspaceMd - connected integrations / credentials', () => {
expect(md).toContain('## Connected Integrations\n(none)')
})
})

describe('buildWorkspaceMd - determinism (prompt-cache stability)', () => {
it('is byte-identical regardless of input row order', () => {
const a = buildWorkspaceMd(
baseData({
members: [
{ name: 'Bob', email: 'bob@x.com', permissionType: 'admin' },
{ name: 'Amy', email: 'amy@x.com', permissionType: 'write' },
],
workflows: [
{ id: 'wf-2', name: 'Zeta', isDeployed: false, folderPath: null },
{ id: 'wf-1', name: 'Alpha', isDeployed: true, folderPath: null },
],
tables: [
{ id: 't-2', name: 'Orders', description: null, rowCount: 5 },
{ id: 't-1', name: 'Customers', description: null, rowCount: 9 },
],
knowledgeBases: [
{ id: 'kb-2', name: 'Docs', connectorTypes: ['notion', 'github'] },
{ id: 'kb-1', name: 'Articles', connectorTypes: ['github', 'notion'] },
],
oauthIntegrations: [
{ id: 'c-2', providerId: 'slack', displayName: null, role: null },
{ id: 'c-1', providerId: 'github', displayName: null, role: null },
],
envVariables: ['ZED', 'API_KEY'],
customTools: [
{ id: 'ct-2', name: 'Beta Tool' },
{ id: 'ct-1', name: 'Alpha Tool' },
],
mcpServers: [
{ id: 'mcp-2', name: 'Zulu', url: null, enabled: false },
{ id: 'mcp-1', name: 'Mike', url: 'https://x', enabled: true },
],
skills: [
{ id: 'sk-2', name: 'Writer', description: 'writes' },
{ id: 'sk-1', name: 'Editor', description: 'edits' },
],
jobs: [
{
id: 'j-2',
title: 'Nightly',
prompt: 'run nightly',
cronExpression: '0 0 * * *',
status: 'active',
lifecycle: 'persistent',
sourceTaskName: null,
},
{
id: 'j-1',
title: 'Hourly',
prompt: 'run hourly',
cronExpression: '0 * * * *',
status: 'active',
lifecycle: 'persistent',
sourceTaskName: null,
},
],
})
)
const b = buildWorkspaceMd(
baseData({
members: [
{ name: 'Amy', email: 'amy@x.com', permissionType: 'write' },
{ name: 'Bob', email: 'bob@x.com', permissionType: 'admin' },
],
workflows: [
{ id: 'wf-1', name: 'Alpha', isDeployed: true, folderPath: null },
{ id: 'wf-2', name: 'Zeta', isDeployed: false, folderPath: null },
],
tables: [
{ id: 't-1', name: 'Customers', description: null, rowCount: 9 },
{ id: 't-2', name: 'Orders', description: null, rowCount: 5 },
],
knowledgeBases: [
{ id: 'kb-1', name: 'Articles', connectorTypes: ['notion', 'github'] },
{ id: 'kb-2', name: 'Docs', connectorTypes: ['github', 'notion'] },
],
oauthIntegrations: [
{ id: 'c-1', providerId: 'github', displayName: null, role: null },
{ id: 'c-2', providerId: 'slack', displayName: null, role: null },
],
envVariables: ['API_KEY', 'ZED'],
customTools: [
{ id: 'ct-1', name: 'Alpha Tool' },
{ id: 'ct-2', name: 'Beta Tool' },
],
mcpServers: [
{ id: 'mcp-1', name: 'Mike', url: 'https://x', enabled: true },
{ id: 'mcp-2', name: 'Zulu', url: null, enabled: false },
],
skills: [
{ id: 'sk-1', name: 'Editor', description: 'edits' },
{ id: 'sk-2', name: 'Writer', description: 'writes' },
],
jobs: [
{
id: 'j-1',
title: 'Hourly',
prompt: 'run hourly',
cronExpression: '0 * * * *',
status: 'active',
lifecycle: 'persistent',
sourceTaskName: null,
},
{
id: 'j-2',
title: 'Nightly',
prompt: 'run nightly',
cronExpression: '0 0 * * *',
status: 'active',
lifecycle: 'persistent',
sourceTaskName: null,
},
],
})
)
expect(a).toBe(b)
})

it('ignores volatile workflow run timestamps', () => {
const withRun = buildWorkspaceMd(
baseData({
workflows: [
{
id: 'wf-1',
name: 'Alpha',
isDeployed: false,
folderPath: null,
lastRunAt: new Date('2026-06-18T12:00:00Z'),
},
],
})
)
const withoutRun = buildWorkspaceMd(
baseData({
workflows: [{ id: 'wf-1', name: 'Alpha', isDeployed: false, folderPath: null }],
})
)
expect(withRun).toBe(withoutRun)
expect(withRun).not.toContain('last run')
})

it('ignores volatile table row counts', () => {
const a = buildWorkspaceMd(
baseData({ tables: [{ id: 't-1', name: 'Customers', description: null, rowCount: 1 }] })
)
const b = buildWorkspaceMd(
baseData({ tables: [{ id: 't-1', name: 'Customers', description: null, rowCount: 9999 }] })
)
expect(a).toBe(b)
expect(a).not.toContain('rows')
})
})
Loading
Loading