Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
06fbc29
v0
Sg312 Jan 30, 2026
cb3618a
v1
Sg312 Jan 30, 2026
8b7b331
Basic ss tes
Sg312 Jan 31, 2026
301e25c
Ss tests
Sg312 Jan 31, 2026
4c821d0
Stuff
Sg312 Jan 31, 2026
6cd8f1d
Add mcp
Sg312 Jan 31, 2026
793c877
mcp v1
Sg312 Jan 31, 2026
e04f379
Improvement
Sg312 Feb 1, 2026
4d84c54
Fix
Sg312 Feb 3, 2026
8f17bc4
BROKEN
Sg312 Feb 3, 2026
2d9a7c6
Checkpoint
Sg312 Feb 4, 2026
addc760
Streaming
Sg312 Feb 4, 2026
a58a61b
Fix abort
Sg312 Feb 4, 2026
be7fb8f
Things are broken
Sg312 Feb 4, 2026
b034b1c
Streaming seems to work but copilot is dumb
Sg312 Feb 4, 2026
8c48cdd
Fix edge issue
Sg312 Feb 4, 2026
d8daf3a
LUAAAA
Sg312 Feb 4, 2026
cbd7bb6
Fix stream buffer
Sg312 Feb 4, 2026
89782f6
Fix lint
Sg312 Feb 4, 2026
9a5a494
Checkpoint
Sg312 Feb 5, 2026
7402b38
Initial temp state, in the middle of a refactor
Sg312 Feb 5, 2026
4faa939
Initial test shows diff store still working
Sg312 Feb 5, 2026
7183448
Tool refactor
Sg312 Feb 5, 2026
b3e74e4
First cleanup pass complete - untested
Sg312 Feb 5, 2026
6b40d4f
Continued cleanup
Sg312 Feb 5, 2026
39968be
Refactor
Sg312 Feb 6, 2026
ba8f39f
Refactor complete - no testing yet
Sg312 Feb 6, 2026
fb4afeb
Fix - cursor makes me sad
Sg312 Feb 6, 2026
dd02395
Fix mcp
Sg312 Feb 6, 2026
08a8e14
Clean up mcp
Sg312 Feb 6, 2026
fd1e61b
Updated mcp
Sg312 Feb 6, 2026
69bdffa
Add respond to subagents
Sg312 Feb 6, 2026
74f863a
Fix definitions
Sg312 Feb 6, 2026
3dcd008
Add tools
Sg312 Feb 6, 2026
0c51ce5
Add tools
Sg312 Feb 6, 2026
df3523e
Add copilot mcp tracking
Sg312 Feb 6, 2026
6cb112e
Fix lint
Sg312 Feb 6, 2026
18e493e
Fix mcp
Sg312 Feb 6, 2026
a73e351
Fix
Sg312 Feb 6, 2026
67c2271
Updates
Sg312 Feb 6, 2026
a220455
Clean up mcp
Sg312 Feb 7, 2026
6735eaa
Fix copilot mcp tool names to be sim prefixed
Sg312 Feb 7, 2026
4d4d002
Add opus 4.6
Sg312 Feb 7, 2026
b07b812
Fix discovery tool
Sg312 Feb 7, 2026
220a540
Fix
Sg312 Feb 7, 2026
ebf4e90
Remove logs
Sg312 Feb 7, 2026
7e592e8
Fix go side tool rendering
Sg312 Feb 7, 2026
25d255a
Update docs
Sg312 Feb 9, 2026
b4361b8
Fix hydration
Sg312 Feb 9, 2026
d2c028f
Fix tool call resolution
Sg312 Feb 9, 2026
ed613c3
Fix
Sg312 Feb 9, 2026
4698f73
Fix lint
Sg312 Feb 9, 2026
79af303
Fix superagent and autoallow integrations
Sg312 Feb 9, 2026
c086912
Fix always allow
Sg312 Feb 9, 2026
9a47033
Update block
Sg312 Feb 9, 2026
7200421
Remove plan docs
Sg312 Feb 9, 2026
ab39a4f
Fix hardcoded ff
Sg312 Feb 9, 2026
dba4e61
Fix dropped provider
Sg312 Feb 9, 2026
b14e844
Fix lint
Sg312 Feb 9, 2026
395f890
Fix tests
Sg312 Feb 9, 2026
7458bbd
Fix dead messages array
Sg312 Feb 9, 2026
48c9a3a
Fix discovery
Sg312 Feb 9, 2026
7153141
Fix run workflow
Sg312 Feb 10, 2026
7670cdf
Fix run block
Sg312 Feb 10, 2026
1beb35c
Fix run from block in copilot
Sg312 Feb 10, 2026
18f1d76
Fix lint
Sg312 Feb 10, 2026
8a2eacf
Fix skip and mtb
Sg312 Feb 10, 2026
bd6a103
Fix typing
Sg312 Feb 10, 2026
ddc5164
Fix tool call
Sg312 Feb 10, 2026
bb4e072
Bump api version
Sg312 Feb 10, 2026
98df298
Fix bun lock
Sg312 Feb 10, 2026
621dd23
Nuke bad files
Sg312 Feb 10, 2026
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
Prev Previous commit
Next Next commit
LUAAAA
  • Loading branch information
Sg312 committed Feb 9, 2026
commit d8daf3a24879919a96d4ecd1917c96905e480334
49 changes: 24 additions & 25 deletions apps/sim/lib/copilot/orchestrator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
handleSubagentRouting,
markToolCallSeen,
markToolResultSeen,
normalizeSseEvent,
sseHandlers,
subAgentHandlers,
wasToolCallSeen,
Expand Down Expand Up @@ -95,35 +96,33 @@ export async function orchestrateCopilotStream(
break
}

const normalizedEvent = normalizeSseEvent(event)

// Skip tool_result events for tools the sim-side already executed.
// The sim-side emits its own tool_result with complete data.
// For server-side tools (not executed by sim), we still forward the Go backend's tool_result.
const toolCallId = getToolCallIdFromEvent(event)
const eventData =
typeof event.data === 'string'
? (() => {
try {
return JSON.parse(event.data)
} catch {
return undefined
}
})()
: event.data

const isPartialToolCall = event.type === 'tool_call' && eventData?.partial === true
const toolCallId = getToolCallIdFromEvent(normalizedEvent)
const eventData = normalizedEvent.data

const isPartialToolCall = normalizedEvent.type === 'tool_call' && eventData?.partial === true

const shouldSkipToolCall =
event.type === 'tool_call' &&
normalizedEvent.type === 'tool_call' &&
!!toolCallId &&
!isPartialToolCall &&
(wasToolResultSeen(toolCallId) || wasToolCallSeen(toolCallId))

if (event.type === 'tool_call' && toolCallId && !isPartialToolCall && !shouldSkipToolCall) {
if (
normalizedEvent.type === 'tool_call' &&
toolCallId &&
!isPartialToolCall &&
!shouldSkipToolCall
) {
markToolCallSeen(toolCallId)
}

const shouldSkipToolResult =
event.type === 'tool_result' &&
normalizedEvent.type === 'tool_result' &&
(() => {
if (!toolCallId) return false
if (wasToolResultSeen(toolCallId)) return true
Expand All @@ -132,11 +131,11 @@ export async function orchestrateCopilotStream(
})()

if (!shouldSkipToolCall && !shouldSkipToolResult) {
await forwardEvent(event, options)
await forwardEvent(normalizedEvent, options)
}

if (event.type === 'subagent_start') {
const toolCallId = event.data?.tool_call_id
if (normalizedEvent.type === 'subagent_start') {
const toolCallId = normalizedEvent.data?.tool_call_id
if (toolCallId) {
context.subAgentParentToolCallId = toolCallId
context.subAgentContent[toolCallId] = ''
Expand All @@ -145,23 +144,23 @@ export async function orchestrateCopilotStream(
continue
}

if (event.type === 'subagent_end') {
if (normalizedEvent.type === 'subagent_end') {
context.subAgentParentToolCallId = undefined
continue
}

if (handleSubagentRouting(event, context)) {
const handler = subAgentHandlers[event.type]
if (handleSubagentRouting(normalizedEvent, context)) {
const handler = subAgentHandlers[normalizedEvent.type]
if (handler) {
await handler(event, context, execContext, options)
await handler(normalizedEvent, context, execContext, options)
}
if (context.streamComplete) break
continue
}

const handler = sseHandlers[event.type]
const handler = sseHandlers[normalizedEvent.type]
if (handler) {
await handler(event, context, execContext, options)
await handler(normalizedEvent, context, execContext, options)
}
if (context.streamComplete) break
}
Expand Down
44 changes: 42 additions & 2 deletions apps/sim/lib/copilot/orchestrator/sse-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,61 @@ type EventDataObject = Record<string, any> | undefined

const parseEventData = (data: unknown): EventDataObject => {
if (!data) return undefined
if (typeof data !== 'string') return data as EventDataObject
if (typeof data !== 'string') {
return data as EventDataObject
}
try {
return JSON.parse(data) as EventDataObject
} catch {
return undefined
}
}

const getEventData = (event: SSEEvent): EventDataObject => parseEventData(event.data)
const hasToolFields = (data: EventDataObject): boolean => {
if (!data) return false
return (
data.id !== undefined ||
data.toolCallId !== undefined ||
data.name !== undefined ||
data.success !== undefined ||
data.result !== undefined ||
data.arguments !== undefined
)
}

const getEventData = (event: SSEEvent): EventDataObject => {
const topLevel = parseEventData(event.data)
if (!topLevel) return undefined
if (hasToolFields(topLevel)) return topLevel
const nested = parseEventData(topLevel.data)
return nested || topLevel
}

export function getToolCallIdFromEvent(event: SSEEvent): string | undefined {
const data = getEventData(event)
return event.toolCallId || data?.id || data?.toolCallId
}

/** Normalizes SSE events so tool metadata is available at the top level. */
export function normalizeSseEvent(event: SSEEvent): SSEEvent {
if (!event) return event
const data = getEventData(event)
if (!data) return event
const toolCallId = event.toolCallId || data.id || data.toolCallId
const toolName = event.toolName || data.name || data.toolName
const success = event.success ?? data.success
const result = event.result ?? data.result
const normalizedData = typeof event.data === 'string' ? data : event.data
return {
...event,
data: normalizedData,
toolCallId,
toolName,
success,
result,
}
}

/**
* Mark a tool call as executed by the sim-side.
* This prevents the Go backend's duplicate tool_result from being forwarded.
Expand Down
48 changes: 32 additions & 16 deletions apps/sim/lib/copilot/orchestrator/stream-buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ const logger = createLogger('CopilotStreamBuffer')
const STREAM_TTL_SECONDS = 60 * 60
const STREAM_EVENT_LIMIT = 5000

const APPEND_STREAM_EVENT_LUA = `
local seqKey = KEYS[1]
local eventsKey = KEYS[2]
local ttl = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local streamId = ARGV[3]
local eventJson = ARGV[4]

local id = redis.call('INCR', seqKey)
local entry = '{"eventId":' .. id .. ',"streamId":' .. cjson.encode(streamId) .. ',"event":' .. eventJson .. '}'
redis.call('ZADD', eventsKey, id, entry)
redis.call('EXPIRE', eventsKey, ttl)
redis.call('EXPIRE', seqKey, ttl)
if limit > 0 then
redis.call('ZREMRANGEBYRANK', eventsKey, 0, -limit-1)
end
return id
`

function getStreamKeyPrefix(streamId: string) {
return `copilot_stream:${streamId}`
}
Expand Down Expand Up @@ -99,22 +118,19 @@ export async function appendStreamEvent(
}

try {
const nextId = await redis.incr(getSeqKey(streamId))
const entry: StreamEventEntry = { eventId: nextId, streamId, event }
await redis.zadd(getEventsKey(streamId), nextId, JSON.stringify(entry))

const count = await redis.zcard(getEventsKey(streamId))
if (count > STREAM_EVENT_LIMIT) {
const trimCount = count - STREAM_EVENT_LIMIT
if (trimCount > 0) {
await redis.zremrangebyrank(getEventsKey(streamId), 0, trimCount - 1)
}
}

await redis.expire(getEventsKey(streamId), STREAM_TTL_SECONDS)
await redis.expire(getSeqKey(streamId), STREAM_TTL_SECONDS)

return entry
const eventJson = JSON.stringify(event)
const nextId = await redis.eval(
APPEND_STREAM_EVENT_LUA,
2,
getSeqKey(streamId),
getEventsKey(streamId),
STREAM_TTL_SECONDS,
STREAM_EVENT_LIMIT,
streamId,
eventJson
)
const eventId = typeof nextId === 'number' ? nextId : Number(nextId)
return { eventId, streamId, event }
} catch (error) {
logger.warn('Failed to append stream event', {
streamId,
Expand Down
56 changes: 28 additions & 28 deletions apps/sim/lib/copilot/orchestrator/subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
handleSubagentRouting,
markToolCallSeen,
markToolResultSeen,
normalizeSseEvent,
sseHandlers,
subAgentHandlers,
wasToolCallSeen,
Expand Down Expand Up @@ -112,35 +113,33 @@ export async function orchestrateSubagentStream(
break
}

const normalizedEvent = normalizeSseEvent(event)

// Skip tool_result events for tools the sim-side already executed.
// The sim-side emits its own tool_result with complete data.
// For server-side tools (not executed by sim), we still forward the Go backend's tool_result.
const toolCallId = getToolCallIdFromEvent(event)
const eventData =
typeof event.data === 'string'
? (() => {
try {
return JSON.parse(event.data)
} catch {
return undefined
}
})()
: event.data

const isPartialToolCall = event.type === 'tool_call' && eventData?.partial === true
const toolCallId = getToolCallIdFromEvent(normalizedEvent)
const eventData = normalizedEvent.data

const isPartialToolCall = normalizedEvent.type === 'tool_call' && eventData?.partial === true

const shouldSkipToolCall =
event.type === 'tool_call' &&
normalizedEvent.type === 'tool_call' &&
!!toolCallId &&
!isPartialToolCall &&
(wasToolResultSeen(toolCallId) || wasToolCallSeen(toolCallId))

if (event.type === 'tool_call' && toolCallId && !isPartialToolCall && !shouldSkipToolCall) {
if (
normalizedEvent.type === 'tool_call' &&
toolCallId &&
!isPartialToolCall &&
!shouldSkipToolCall
) {
markToolCallSeen(toolCallId)
}

const shouldSkipToolResult =
event.type === 'tool_result' &&
normalizedEvent.type === 'tool_result' &&
(() => {
if (!toolCallId) return false
if (wasToolResultSeen(toolCallId)) return true
Expand All @@ -149,18 +148,18 @@ export async function orchestrateSubagentStream(
})()

if (!shouldSkipToolCall && !shouldSkipToolResult) {
await forwardEvent(event, options)
await forwardEvent(normalizedEvent, options)
}

if (event.type === 'structured_result' || event.type === 'subagent_result') {
structuredResult = normalizeStructuredResult(event.data)
if (normalizedEvent.type === 'structured_result' || normalizedEvent.type === 'subagent_result') {
structuredResult = normalizeStructuredResult(normalizedEvent.data)
context.streamComplete = true
continue
}

// Handle subagent_start/subagent_end events to track nested subagent calls
if (event.type === 'subagent_start') {
const toolCallId = event.data?.tool_call_id
if (normalizedEvent.type === 'subagent_start') {
const toolCallId = normalizedEvent.data?.tool_call_id
if (toolCallId) {
context.subAgentParentToolCallId = toolCallId
context.subAgentContent[toolCallId] = ''
Expand All @@ -169,30 +168,31 @@ export async function orchestrateSubagentStream(
continue
}

if (event.type === 'subagent_end') {
if (normalizedEvent.type === 'subagent_end') {
context.subAgentParentToolCallId = undefined
continue
}

// For direct subagent calls, events may have the subagent field set (e.g., subagent: "discovery")
// but no subagent_start event because this IS the top-level agent. Skip subagent routing
// for events where the subagent field matches the current agentId - these are top-level events.
const isTopLevelSubagentEvent = event.subagent === agentId && !context.subAgentParentToolCallId
const isTopLevelSubagentEvent =
normalizedEvent.subagent === agentId && !context.subAgentParentToolCallId

// Only route to subagent handlers for nested subagent events (not matching current agentId)
if (!isTopLevelSubagentEvent && handleSubagentRouting(event, context)) {
const handler = subAgentHandlers[event.type]
if (!isTopLevelSubagentEvent && handleSubagentRouting(normalizedEvent, context)) {
const handler = subAgentHandlers[normalizedEvent.type]
if (handler) {
await handler(event, context, execContext, options)
await handler(normalizedEvent, context, execContext, options)
}
if (context.streamComplete) break
continue
}

// Process as a regular SSE event (including top-level subagent events)
const handler = sseHandlers[event.type]
const handler = sseHandlers[normalizedEvent.type]
if (handler) {
await handler(event, context, execContext, options)
await handler(normalizedEvent, context, execContext, options)
}
if (context.streamComplete) break
}
Expand Down