Files
sim/apps/sim/app/api/copilot/confirm/route.ts
Siddharth Ganesan 775daed2ea fix(mothership): tool call loop (#3729)
* v0

* Fix ppt load

* Fixes

* Fixes

* Fix lint

* Fix wid

* Download image

* Update tools

* Fix lint

* Fix error msg

* Tool fixes

* Reenable subagent stream

* Subagent stream

* Fix edit workflow hydration

* Throw func execute error on error

* Rewrite

* Remove promptForToolApproval flag, fix workflow terminal logs

* Fixes

* Fix buffer

* Fix

* Fix claimed by

* Cleanup v1

* Tool call loop

* Fixes

* Fixes

* Fix subaget aborts

* Fix diff

* Add delegating state to subagents

* Fix build

* Fix sandbox

* Fix lint

---------

Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Lakee Sivaraya <71339072+lakeesiv@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Theodore Li <teddy@zenobiapay.com>
2026-03-23 18:11:06 -07:00

170 lines
4.9 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import {
completeAsyncToolCall,
getAsyncToolCall,
getRunSegment,
upsertAsyncToolCall,
} from '@/lib/copilot/async-runs/repository'
import { publishToolConfirmation } from '@/lib/copilot/orchestrator/persistence'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
createInternalServerErrorResponse,
createNotFoundResponse,
createRequestTracker,
createUnauthorizedResponse,
type NotificationStatus,
} from '@/lib/copilot/request-helpers'
const logger = createLogger('CopilotConfirmAPI')
// Schema for confirmation request
const ConfirmationSchema = z.object({
toolCallId: z.string().min(1, 'Tool call ID is required'),
status: z.enum(['success', 'error', 'accepted', 'rejected', 'background', 'cancelled'] as const, {
errorMap: () => ({ message: 'Invalid notification status' }),
}),
message: z.string().optional(),
data: z.record(z.unknown()).optional(),
})
/**
* Persist the durable tool status, then publish a wakeup event.
*/
async function updateToolCallStatus(
existing: NonNullable<Awaited<ReturnType<typeof getAsyncToolCall>>>,
status: NotificationStatus,
message?: string,
data?: Record<string, unknown>
): Promise<boolean> {
const toolCallId = existing.toolCallId
const durableStatus =
status === 'success'
? 'completed'
: status === 'cancelled'
? 'cancelled'
: status === 'error' || status === 'rejected'
? 'failed'
: 'pending'
try {
if (
durableStatus === 'completed' ||
durableStatus === 'failed' ||
durableStatus === 'cancelled'
) {
await completeAsyncToolCall({
toolCallId,
status: durableStatus,
result: data ?? null,
error: status === 'success' ? null : message || status,
})
} else if (existing.runId) {
await upsertAsyncToolCall({
runId: existing.runId,
checkpointId: existing.checkpointId ?? null,
toolCallId,
toolName: existing.toolName || 'client_tool',
args: (existing.args as Record<string, unknown> | null) ?? {},
status: durableStatus,
})
}
const timestamp = new Date().toISOString()
publishToolConfirmation({
toolCallId,
status,
message: message || undefined,
timestamp,
data,
})
return true
} catch (error) {
logger.error('Failed to update tool call status', {
toolCallId,
status,
error: error instanceof Error ? error.message : String(error),
})
return false
}
}
/**
* POST /api/copilot/confirm
* Update tool call status (Accept/Reject)
*/
export async function POST(req: NextRequest) {
const tracker = createRequestTracker()
try {
// Authenticate user using consolidated helper
const { userId: authenticatedUserId, isAuthenticated } =
await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated) {
return createUnauthorizedResponse()
}
const body = await req.json()
const { toolCallId, status, message, data } = ConfirmationSchema.parse(body)
const existing = await getAsyncToolCall(toolCallId).catch(() => null)
if (!existing) {
return createNotFoundResponse('Tool call not found')
}
const run = await getRunSegment(existing.runId).catch(() => null)
if (!run) {
return createNotFoundResponse('Tool call run not found')
}
if (run.userId !== authenticatedUserId) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
// Update the durable tool call status and wake any waiters.
const updated = await updateToolCallStatus(existing, status, message, data)
if (!updated) {
logger.error(`[${tracker.requestId}] Failed to update tool call status`, {
userId: authenticatedUserId,
toolCallId,
status,
internalStatus: status,
message,
})
return createBadRequestResponse('Failed to update tool call status or tool call not found')
}
const duration = tracker.getDuration()
return NextResponse.json({
success: true,
message: message || `Tool call ${toolCallId} has been ${status.toLowerCase()}`,
toolCallId,
status,
})
} catch (error) {
const duration = tracker.getDuration()
if (error instanceof z.ZodError) {
logger.error(`[${tracker.requestId}] Request validation error:`, {
duration,
errors: error.errors,
})
return createBadRequestResponse(
`Invalid request data: ${error.errors.map((e) => e.message).join(', ')}`
)
}
logger.error(`[${tracker.requestId}] Unexpected error:`, {
duration,
error: error instanceof Error ? error.message : 'Unknown error',
stack: error instanceof Error ? error.stack : undefined,
})
return createInternalServerErrorResponse(
error instanceof Error ? error.message : 'Internal server error'
)
}
}