Mothership block

This commit is contained in:
Siddharth Ganesan
2026-03-02 14:54:54 -08:00
parent ae080f125c
commit fce10241a5
14 changed files with 13525 additions and 38 deletions

View File

@@ -18,7 +18,7 @@ const UpdateCostSchema = z.object({
model: z.string().min(1, 'Model is required'),
inputTokens: z.number().min(0).default(0),
outputTokens: z.number().min(0).default(0),
source: z.enum(['copilot', 'workspace-chat', 'mcp_copilot']).default('copilot'),
source: z.enum(['copilot', 'workspace-chat', 'mcp_copilot', 'mothership_block']).default('copilot'),
})
/**

View File

@@ -0,0 +1,106 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { buildIntegrationToolSchemas } from '@/lib/copilot/chat-payload'
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
import { generateWorkspaceContext } from '@/lib/copilot/workspace-context'
const logger = createLogger('MothershipExecuteAPI')
const MessageSchema = z.object({
role: z.enum(['system', 'user', 'assistant']),
content: z.string(),
})
const ExecuteRequestSchema = z.object({
messages: z.array(MessageSchema).min(1, 'At least one message is required'),
responseFormat: z.any().optional(),
workspaceId: z.string().min(1, 'workspaceId is required'),
userId: z.string().min(1, 'userId is required'),
chatId: z.string().optional(),
})
/**
* POST /api/mothership/execute
*
* Non-streaming endpoint for Mothership block execution within workflows.
* Called by the executor via internal JWT auth, not by the browser directly.
* Consumes the Go SSE stream internally and returns a single JSON response.
*/
export async function POST(req: NextRequest) {
try {
const auth = await checkInternalAuth(req, { requireWorkflowId: false })
if (!auth.success) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const body = await req.json()
const { messages, responseFormat, workspaceId, userId, chatId } =
ExecuteRequestSchema.parse(body)
const effectiveChatId = chatId || crypto.randomUUID()
const [workspaceContext, integrationTools] = await Promise.all([
generateWorkspaceContext(workspaceId, userId),
buildIntegrationToolSchemas(),
])
const requestPayload: Record<string, unknown> = {
messages,
responseFormat,
userId,
chatId: effectiveChatId,
mode: 'agent',
messageId: crypto.randomUUID(),
isHosted: true,
workspaceContext,
...(integrationTools.length > 0 ? { integrationTools } : {}),
}
const result = await orchestrateCopilotStream(requestPayload, {
userId,
workspaceId,
chatId: effectiveChatId,
goRoute: '/api/mothership/execute',
autoExecuteTools: true,
interactive: false,
})
if (!result.success) {
logger.error('Mothership execute failed', {
error: result.error,
errors: result.errors,
})
return NextResponse.json(
{
error: result.error || 'Mothership execution failed',
content: result.content || '',
},
{ status: 500 }
)
}
return NextResponse.json({
content: result.content,
model: 'mothership',
tokens: {},
toolCalls: result.toolCalls,
})
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}
logger.error('Mothership execute error', {
error: error instanceof Error ? error.message : 'Unknown error',
})
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Internal server error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,99 @@
import { Rocket } from 'lucide-react'
import type { BlockConfig } from '@/blocks/types'
import type { ToolResponse } from '@/tools/types'
interface MothershipResponse extends ToolResponse {
output: {
content: string
model: string
tokens?: {
prompt?: number
completion?: number
total?: number
}
}
}
export const MothershipBlock: BlockConfig<MothershipResponse> = {
type: 'mothership',
name: 'Mothership',
description: 'Query the Mothership AI agent',
longDescription:
'The Mothership block sends messages to the Mothership AI agent, which has access to subagents, integration tools, memory, and workspace context. Use it to perform complex multi-step reasoning, cross-service queries, or any task that benefits from the full Mothership intelligence within a workflow.',
bestPractices: `
- Use for tasks that require multi-step reasoning, tool use, or cross-service coordination.
- Response Format should be a valid JSON Schema. When present, structured fields are returned at root level (e.g. <mothership1.field>). Without it, the block returns content, model, and tokens.
- The Mothership picks its own model and tools internally — you only provide messages and an optional response format.
`,
category: 'blocks',
bgColor: '#802FDE',
icon: Rocket,
subBlocks: [
{
id: 'messages',
title: 'Messages',
type: 'messages-input',
placeholder: 'Enter messages...',
},
{
id: 'responseFormat',
title: 'Response Format',
type: 'code',
placeholder: 'Enter JSON schema...',
language: 'json',
mode: 'advanced',
},
{
id: 'memoryType',
title: 'Memory',
type: 'dropdown',
placeholder: 'Select memory...',
options: [
{ label: 'None', id: 'none' },
{ label: 'Conversation', id: 'conversation' },
],
mode: 'advanced',
},
{
id: 'conversationId',
title: 'Conversation ID',
type: 'short-input',
placeholder: 'e.g., user-123, session-abc',
required: {
field: 'memoryType',
value: ['conversation'],
},
condition: {
field: 'memoryType',
value: ['conversation'],
},
},
],
tools: {
access: [],
},
inputs: {
messages: {
type: 'json',
description:
'Array of message objects with role and content: [{ role: "system", content: "..." }, { role: "user", content: "..." }]',
},
responseFormat: {
type: 'json',
description: 'JSON response format schema for structured output',
},
memoryType: {
type: 'string',
description: 'Type of memory: none (default) or conversation',
},
conversationId: {
type: 'string',
description: 'Persistent conversation ID for memory across executions',
},
},
outputs: {
content: { type: 'string', description: 'Generated response content' },
model: { type: 'string', description: 'Model used for generation' },
tokens: { type: 'json', description: 'Token usage statistics' },
},
}

View File

@@ -82,6 +82,7 @@ import { MailchimpBlock } from '@/blocks/blocks/mailchimp'
import { MailgunBlock } from '@/blocks/blocks/mailgun'
import { ManualTriggerBlock } from '@/blocks/blocks/manual_trigger'
import { McpBlock } from '@/blocks/blocks/mcp'
import { MothershipBlock } from '@/blocks/blocks/mothership'
import { Mem0Block } from '@/blocks/blocks/mem0'
import { MemoryBlock } from '@/blocks/blocks/memory'
import { MicrosoftDataverseBlock } from '@/blocks/blocks/microsoft_dataverse'
@@ -285,6 +286,7 @@ export const registry: Record<string, BlockConfig> = {
mistral_parse_v2: MistralParseV2Block,
mistral_parse_v3: MistralParseV3Block,
mongodb: MongoDBBlock,
mothership: MothershipBlock,
mysql: MySQLBlock,
neo4j: Neo4jBlock,
note: NoteBlock,

View File

@@ -25,6 +25,7 @@ export enum BlockType {
FUNCTION = 'function',
AGENT = 'agent',
MOTHERSHIP = 'mothership',
API = 'api',
EVALUATOR = 'evaluator',
VARIABLES = 'variables',

View File

@@ -0,0 +1,152 @@
import { createLogger } from '@sim/logger'
import type { BlockOutput } from '@/blocks/types'
import { BlockType } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http'
import type { SerializedBlock } from '@/serializer/types'
const logger = createLogger('MothershipBlockHandler')
/**
* Handler for Mothership blocks that proxy requests to the Mothership AI agent.
*
* Unlike the Agent block (which calls LLM providers directly), the Mothership
* block delegates to the full Mothership infrastructure: main agent, subagents,
* integration tools, memory, and workspace context.
*/
export class MothershipBlockHandler implements BlockHandler {
canHandle(block: SerializedBlock): boolean {
return block.metadata?.id === BlockType.MOTHERSHIP
}
async execute(
ctx: ExecutionContext,
block: SerializedBlock,
inputs: Record<string, any>
): Promise<BlockOutput> {
const messages = this.resolveMessages(inputs)
const responseFormat = this.parseResponseFormat(inputs.responseFormat)
const memoryType = inputs.memoryType || 'none'
const chatId =
memoryType === 'conversation' && inputs.conversationId
? inputs.conversationId
: crypto.randomUUID()
const url = buildAPIUrl('/api/mothership/execute')
const headers = await buildAuthHeaders()
const body: Record<string, unknown> = {
messages,
workspaceId: ctx.workspaceId || '',
userId: ctx.userId || '',
chatId,
}
if (responseFormat) {
body.responseFormat = responseFormat
}
logger.info('Executing Mothership block', {
blockId: block.id,
messageCount: messages.length,
hasResponseFormat: !!responseFormat,
memoryType,
hasConversationId: memoryType === 'conversation',
})
const response = await fetch(url.toString(), {
method: 'POST',
headers,
body: JSON.stringify(body),
})
if (!response.ok) {
const errorMsg = await extractAPIErrorMessage(response)
throw new Error(`Mothership execution failed: ${errorMsg}`)
}
const result = await response.json()
if (responseFormat && result.content) {
return this.processStructuredResponse(result)
}
return {
content: result.content || '',
model: result.model || 'mothership',
tokens: result.tokens || {},
}
}
private resolveMessages(
inputs: Record<string, any>
): Array<{ role: string; content: string }> {
const raw = inputs.messages
if (!raw) {
throw new Error('Messages input is required for the Mothership block')
}
let messages: unknown[]
if (typeof raw === 'string') {
try {
messages = JSON.parse(raw)
} catch {
throw new Error('Messages must be a valid JSON array')
}
} else if (Array.isArray(raw)) {
messages = raw
} else {
throw new Error('Messages must be an array of {role, content} objects')
}
return messages.map((msg: any, i: number) => {
if (!msg.role || typeof msg.content !== 'string') {
throw new Error(
`Message at index ${i} must have "role" (string) and "content" (string)`
)
}
return { role: String(msg.role), content: msg.content }
})
}
private parseResponseFormat(responseFormat?: string | object): any {
if (!responseFormat || responseFormat === '') return undefined
if (typeof responseFormat === 'object') return responseFormat
if (typeof responseFormat === 'string') {
const trimmed = responseFormat.trim()
if (!trimmed) return undefined
if (trimmed.startsWith('<') || trimmed.startsWith('{{')) return undefined
try {
return JSON.parse(trimmed)
} catch {
logger.warn('Failed to parse responseFormat as JSON', {
preview: trimmed.slice(0, 100),
})
return undefined
}
}
return undefined
}
private processStructuredResponse(result: any): BlockOutput {
const content = result.content
try {
const parsed = JSON.parse(content.trim())
return {
...parsed,
model: result.model || 'mothership',
tokens: result.tokens || {},
}
} catch {
logger.warn('Failed to parse structured response, returning raw content')
return {
content,
model: result.model || 'mothership',
tokens: result.tokens || {},
}
}
}
}

View File

@@ -11,6 +11,7 @@ import { ConditionBlockHandler } from '@/executor/handlers/condition/condition-h
import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler'
import { FunctionBlockHandler } from '@/executor/handlers/function/function-handler'
import { GenericBlockHandler } from '@/executor/handlers/generic/generic-handler'
import { MothershipBlockHandler } from '@/executor/handlers/mothership/mothership-handler'
import { HumanInTheLoopBlockHandler } from '@/executor/handlers/human-in-the-loop/human-in-the-loop-handler'
import { ResponseBlockHandler } from '@/executor/handlers/response/response-handler'
import { RouterBlockHandler } from '@/executor/handlers/router/router-handler'
@@ -36,6 +37,7 @@ export function createBlockHandlers(): BlockHandler[] {
new ResponseBlockHandler(),
new HumanInTheLoopBlockHandler(),
new AgentBlockHandler(),
new MothershipBlockHandler(),
new VariablesBlockHandler(),
new WorkflowBlockHandler(),
new WaitBlockHandler(),

View File

@@ -14,7 +14,13 @@ export type UsageLogCategory = 'model' | 'fixed'
/**
* Usage log source types
*/
export type UsageLogSource = 'workflow' | 'wand' | 'copilot' | 'workspace-chat' | 'mcp_copilot'
export type UsageLogSource =
| 'workflow'
| 'wand'
| 'copilot'
| 'workspace-chat'
| 'mcp_copilot'
| 'mothership_block'
/**
* Metadata for 'model' category charges

View File

@@ -27,7 +27,7 @@ export interface BuildPayloadParams {
workspaceContext?: string
}
interface ToolSchema {
export interface ToolSchema {
name: string
description: string
input_schema: Record<string, unknown>
@@ -36,6 +36,48 @@ interface ToolSchema {
oauth?: { required: boolean; provider: string }
}
/**
* Build deferred integration tool schemas from the Sim tool registry.
* Shared by the interactive chat payload builder and the non-interactive
* block execution route so both paths send the same tool definitions to Go.
*/
export async function buildIntegrationToolSchemas(): Promise<ToolSchema[]> {
const integrationTools: ToolSchema[] = []
try {
const { createUserToolSchema } = await import('@/tools/params')
const latestTools = getLatestVersionTools(tools)
for (const [toolId, toolConfig] of Object.entries(latestTools)) {
try {
const userSchema = createUserToolSchema(toolConfig)
const strippedName = stripVersionSuffix(toolId)
integrationTools.push({
name: strippedName,
description: toolConfig.description || toolConfig.name || strippedName,
input_schema: userSchema as unknown as Record<string, unknown>,
defer_loading: true,
...(toolConfig.oauth?.required && {
oauth: {
required: true,
provider: toolConfig.oauth.provider,
},
}),
})
} catch (toolError) {
logger.warn('Failed to build schema for tool, skipping', {
toolId,
error: toolError instanceof Error ? toolError.message : String(toolError),
})
}
}
} catch (error) {
logger.warn('Failed to build tool schemas', {
error: error instanceof Error ? error.message : String(error),
})
}
return integrationTools
}
/**
* Build the request payload for the copilot backend.
*/
@@ -68,41 +110,10 @@ export async function buildCopilotRequestPayload(
const processedFileContents = await processFileAttachments(fileAttachments ?? [], userId)
const integrationTools: ToolSchema[] = []
let integrationTools: ToolSchema[] = []
if (effectiveMode === 'build') {
try {
const { createUserToolSchema } = await import('@/tools/params')
const latestTools = getLatestVersionTools(tools)
for (const [toolId, toolConfig] of Object.entries(latestTools)) {
try {
const userSchema = createUserToolSchema(toolConfig)
const strippedName = stripVersionSuffix(toolId)
integrationTools.push({
name: strippedName,
description: toolConfig.description || toolConfig.name || strippedName,
input_schema: userSchema as unknown as Record<string, unknown>,
defer_loading: true,
...(toolConfig.oauth?.required && {
oauth: {
required: true,
provider: toolConfig.oauth.provider,
},
}),
})
} catch (toolError) {
logger.warn('Failed to build schema for tool, skipping', {
toolId,
error: toolError instanceof Error ? toolError.message : String(toolError),
})
}
}
} catch (error) {
logger.warn('Failed to build tool schemas for payload', {
error: error instanceof Error ? error.message : String(error),
})
}
integrationTools = await buildIntegrationToolSchemas()
// Discover MCP tools from workspace servers and include as deferred tools
if (workflowId) {

View File

@@ -27,10 +27,17 @@ function addToSet(set: Set<string>, id: string): void {
const parseEventData = (data: unknown): EventDataObject => {
if (!data) return undefined
if (typeof data !== 'string') {
return data as EventDataObject
if (typeof data === 'object' && !Array.isArray(data)) {
return data as EventDataObject
}
return undefined
}
try {
return JSON.parse(data) as EventDataObject
const parsed = JSON.parse(data)
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
return parsed as EventDataObject
}
return undefined
} catch {
return undefined
}

View File

@@ -0,0 +1,2 @@
ALTER TYPE "public"."usage_log_source" ADD VALUE 'mothership_block';--> statement-breakpoint
ALTER TABLE "mcp_servers" DROP COLUMN "copilot_enabled";

File diff suppressed because it is too large Load Diff

View File

@@ -1128,6 +1128,13 @@
"when": 1772217895286,
"tag": "0161_true_songbird",
"breakpoints": true
},
{
"idx": 162,
"version": "7",
"when": 1772482049606,
"tag": "0162_early_bloodscream",
"breakpoints": true
}
]
}

View File

@@ -2063,6 +2063,7 @@ export const usageLogSourceEnum = pgEnum('usage_log_source', [
'wand',
'copilot',
'mcp_copilot',
'mothership_block',
])
export const usageLog = pgTable(