file/folder tools

This commit is contained in:
Siddharth Ganesan
2026-04-06 18:09:52 -07:00
parent 2da0cbe365
commit c52d63303d
11 changed files with 126 additions and 125 deletions

View File

@@ -122,12 +122,10 @@ Sim is a workflow automation platform. Workflows are visual pipelines of connect
1. \`list_workspaces\` → know where to work
2. \`create_workflow(name, workspaceId)\` → get a workflowId
3. \`sim_build(request, workflowId)\` → plan and build in one pass
3. \`sim_workflow(request, workflowId)\` → plan and build in one pass
4. \`sim_test(request, workflowId)\` → verify it works
5. \`sim_deploy("deploy as api", workflowId)\` → make it accessible externally (optional)
For fine-grained control, use \`sim_plan\`\`sim_edit\` instead of \`sim_build\`. Pass the plan object from sim_plan EXACTLY as-is to sim_edit's context.plan field.
### Working with Existing Workflows
When the user refers to a workflow by name or description ("the email one", "my Slack bot"):
@@ -670,7 +668,7 @@ async function handleDirectToolCall(
/**
* Build mode uses the main chat orchestrator with the 'fast' command instead of
* the subagent endpoint. In Go, 'build' is not a registered subagent — it's a mode
* the subagent endpoint. In Go, 'workflow' is not a registered subagent — it's a mode
* (ModeFast) on the main chat processor that bypasses subagent orchestration and
* executes all tools directly.
*/
@@ -768,7 +766,7 @@ async function handleSubagentToolCall(
userId: string,
abortSignal?: AbortSignal
): Promise<CallToolResult> {
if (toolDef.agentId === 'build') {
if (toolDef.agentId === 'workflow') {
return handleBuildToolCall(args, userId, abortSignal)
}

View File

@@ -43,7 +43,7 @@ const TOOL_ICONS: Record<string, IconComponent> = {
workspace_file: File,
create_workflow: Layout,
edit_workflow: Pencil,
build: Hammer,
workflow: Hammer,
run: PlayOutline,
deploy: Rocket,
auth: Integration,

View File

@@ -1,7 +1,6 @@
import {
Agent,
Auth,
Build,
CreateWorkflow,
Debug,
Deploy,
@@ -26,6 +25,7 @@ import {
Table,
UserMemory,
UserTable,
Workflow,
WorkspaceFile,
} from '@/lib/copilot/generated/tool-catalog-v1'
import type { ChatContext } from '@/stores/panel'
@@ -178,7 +178,7 @@ export interface ChatMessage {
}
export const SUBAGENT_LABELS: Record<string, string> = {
build: 'Build agent',
workflow: 'Workflow agent',
deploy: 'Deploy agent',
auth: 'Integration agent',
research: 'Research agent',
@@ -281,7 +281,7 @@ export const TOOL_UI_METADATA: Record<string, ToolUIMetadata> = {
phaseLabel: 'Resource',
phase: 'resource',
},
[Build.id]: { title: 'Building', phaseLabel: 'Build', phase: 'subagent' },
[Workflow.id]: { title: 'Managing workflow', phaseLabel: 'Workflow', phase: 'subagent' },
[Run.id]: { title: 'Running', phaseLabel: 'Run', phase: 'subagent' },
[Deploy.id]: { title: 'Deploying', phaseLabel: 'Deploy', phase: 'subagent' },
[Auth.id]: {

View File

@@ -20,7 +20,7 @@ describe('persisted-message', () => {
{
type: 'tool_call',
timestamp: Date.now(),
calledBy: 'build',
calledBy: 'workflow',
toolCall: {
id: 'tool-1',
name: 'read',
@@ -46,7 +46,7 @@ describe('persisted-message', () => {
state: 'success',
params: { path: 'foo.txt' },
result: { success: true, output: { ok: true } },
calledBy: 'build',
calledBy: 'workflow',
},
},
{

View File

@@ -45,6 +45,7 @@ export interface WorkspaceMdData {
description?: string | null
isDeployed: boolean
lastRunAt?: Date | null
folderPath?: string | null
}>
knowledgeBases: Array<{
id: string
@@ -92,15 +93,40 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string {
}
if (data.workflows.length > 0) {
const lines = data.workflows.map((wf) => {
const parts = [`- **${wf.name}** (${wf.id})`]
if (wf.description) parts.push(` ${wf.description}`)
const rootWorkflows: typeof data.workflows = []
const folderWorkflows = new Map<string, typeof data.workflows>()
for (const wf of data.workflows) {
if (wf.folderPath) {
const existing = folderWorkflows.get(wf.folderPath) ?? []
existing.push(wf)
folderWorkflows.set(wf.folderPath, existing)
} else {
rootWorkflows.push(wf)
}
}
const formatWf = (wf: (typeof data.workflows)[0], indent: string) => {
const parts = [`${indent}- **${wf.name}** (${wf.id})`]
if (wf.description) parts.push(`${indent} ${wf.description}`)
const flags: string[] = []
if (wf.isDeployed) flags.push('deployed')
if (wf.lastRunAt) flags.push(`last run: ${wf.lastRunAt.toISOString().split('T')[0]}`)
if (flags.length > 0) parts[0] += `${flags.join(', ')}`
return parts.join('\n')
})
}
const lines: string[] = []
for (const wf of rootWorkflows) {
lines.push(formatWf(wf, ''))
}
const sortedFolders = [...folderWorkflows.entries()].sort((a, b) => a[0].localeCompare(b[0]))
for (const [folder, wfs] of sortedFolders) {
lines.push(`- 📁 **${folder}/**`)
for (const wf of wfs) {
lines.push(formatWf(wf, ' '))
}
}
sections.push(`## Workflows (${data.workflows.length})\n${lines.join('\n')}`)
} else {
sections.push('## Workflows (0)\n(none)')

View File

@@ -9,7 +9,6 @@ export interface ToolCatalogEntry {
id:
| 'agent'
| 'auth'
| 'build'
| 'check_deployment_status'
| 'complete_job'
| 'context_write'
@@ -84,13 +83,13 @@ export interface ToolCatalogEntry {
| 'update_workspace_mcp_server'
| 'user_memory'
| 'user_table'
| 'workflow'
| 'workspace_file'
internal?: boolean
mode: 'async' | 'sync'
name:
| 'agent'
| 'auth'
| 'build'
| 'check_deployment_status'
| 'complete_job'
| 'context_write'
@@ -165,13 +164,13 @@ export interface ToolCatalogEntry {
| 'update_workspace_mcp_server'
| 'user_memory'
| 'user_table'
| 'workflow'
| 'workspace_file'
requiredPermission?: 'admin' | 'write'
requiresConfirmation?: boolean
subagentId?:
| 'agent'
| 'auth'
| 'build'
| 'debug'
| 'deploy'
| 'file_write'
@@ -181,6 +180,7 @@ export interface ToolCatalogEntry {
| 'run'
| 'superagent'
| 'table'
| 'workflow'
}
export const Agent: ToolCatalogEntry = {
@@ -202,15 +202,6 @@ export const Auth: ToolCatalogEntry = {
internal: true,
}
export const Build: ToolCatalogEntry = {
id: 'build',
name: 'build',
executor: 'subagent',
mode: 'async',
subagentId: 'build',
internal: true,
}
export const CheckDeploymentStatus: ToolCatalogEntry = {
id: 'check_deployment_status',
name: 'check_deployment_status',
@@ -801,6 +792,15 @@ export const UserTable: ToolCatalogEntry = {
requiresConfirmation: true,
}
export const Workflow: ToolCatalogEntry = {
id: 'workflow',
name: 'workflow',
executor: 'subagent',
mode: 'async',
subagentId: 'workflow',
internal: true,
}
export const WorkspaceFile: ToolCatalogEntry = {
id: 'workspace_file',
name: 'workspace_file',
@@ -812,7 +812,6 @@ export const WorkspaceFile: ToolCatalogEntry = {
export const TOOL_CATALOG: Record<string, ToolCatalogEntry> = {
[Agent.id]: Agent,
[Auth.id]: Auth,
[Build.id]: Build,
[CheckDeploymentStatus.id]: CheckDeploymentStatus,
[CompleteJob.id]: CompleteJob,
[ContextWrite.id]: ContextWrite,
@@ -887,5 +886,6 @@ export const TOOL_CATALOG: Record<string, ToolCatalogEntry> = {
[UpdateWorkspaceMcpServer.id]: UpdateWorkspaceMcpServer,
[UserMemory.id]: UserMemory,
[UserTable.id]: UserTable,
[Workflow.id]: Workflow,
[WorkspaceFile.id]: WorkspaceFile,
}

View File

@@ -132,7 +132,7 @@ describe('sse-handlers tool lifecycle', () => {
context.subAgentParentStack = ['parent-1']
context.toolCalls.set('parent-1', {
id: 'parent-1',
name: 'build',
name: 'workflow',
status: 'pending',
startTime: Date.now(),
})
@@ -140,7 +140,7 @@ describe('sse-handlers tool lifecycle', () => {
await subAgentHandlers.tool(
{
type: MothershipStreamV1EventType.tool,
scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'build' },
scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'workflow' },
payload: {
toolCallId: 'sub-tool-1',
toolName: 'create_workflow',
@@ -158,7 +158,7 @@ describe('sse-handlers tool lifecycle', () => {
await subAgentHandlers.tool(
{
type: MothershipStreamV1EventType.tool,
scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'build' },
scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'workflow' },
payload: {
toolCallId: 'sub-tool-1',
toolName: 'create_workflow',

View File

@@ -371,20 +371,20 @@ const META_manage_skill: ToolMetadata = {
},
}
const META_build: ToolMetadata = {
const META_workflow: ToolMetadata = {
displayNames: {
[ClientToolCallState.generating]: { text: 'Building', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Building', icon: Loader2 },
[ClientToolCallState.executing]: { text: 'Building', icon: Loader2 },
[ClientToolCallState.success]: { text: 'Built', icon: Wrench },
[ClientToolCallState.error]: { text: 'Failed to build', icon: XCircle },
[ClientToolCallState.rejected]: { text: 'Skipped build', icon: XCircle },
[ClientToolCallState.aborted]: { text: 'Aborted build', icon: XCircle },
[ClientToolCallState.generating]: { text: 'Managing workflow', icon: Loader2 },
[ClientToolCallState.pending]: { text: 'Managing workflow', icon: Loader2 },
[ClientToolCallState.executing]: { text: 'Managing workflow', icon: Loader2 },
[ClientToolCallState.success]: { text: 'Done', icon: Wrench },
[ClientToolCallState.error]: { text: 'Failed', icon: XCircle },
[ClientToolCallState.rejected]: { text: 'Skipped', icon: XCircle },
[ClientToolCallState.aborted]: { text: 'Aborted', icon: XCircle },
},
uiConfig: {
subagent: {
streamingLabel: 'Building',
completedLabel: 'Built',
streamingLabel: 'Managing workflow',
completedLabel: 'Done',
shouldCollapse: true,
outputArtifacts: [],
},
@@ -2298,7 +2298,7 @@ const TOOL_METADATA_BY_ID: Record<string, ToolMetadata> = {
checkoff_todo: META_checkoff_todo,
crawl_website: META_crawl_website,
create_workspace_mcp_server: META_create_workspace_mcp_server,
build: META_build,
workflow: META_workflow,
create_folder: META_create_folder,
create_workflow: META_create_workflow,
agent: META_agent,

View File

@@ -58,7 +58,7 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [
name: 'create_workflow',
toolId: 'create_workflow',
description:
'Create a new empty workflow. Returns the new workflow ID. Always call this FIRST before sim_build for new workflows. Use workspaceId to place it in a specific workspace.',
'Create a new empty workflow. Returns the new workflow ID. Always call this FIRST before sim_workflow for new workflows. Use workspaceId to place it in a specific workspace.',
inputSchema: {
type: 'object',
properties: {
@@ -263,14 +263,15 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [
export const SUBAGENT_TOOL_DEFS: SubagentToolDef[] = [
{
name: 'sim_build',
agentId: 'build',
description: `Build a workflow end-to-end in a single step. This is the fast mode equivalent for headless/MCP usage.
name: 'sim_workflow',
agentId: 'workflow',
description: `Create, modify, test, debug, and organize workflows end-to-end in a single step.
USE THIS WHEN:
- Building a new workflow from scratch
- Modifying an existing workflow
- You want to gather information and build in one pass without separate plan→edit steps
- You want to gather information and build in one pass
- Moving, renaming, or organizing workflows and folders
WORKFLOW ID (REQUIRED):
- For NEW workflows: First call create_workflow to get a workflowId, then pass it here
@@ -282,6 +283,7 @@ CAN DO:
- Add, modify, or remove blocks
- Configure block settings and connections
- Set environment variables and workflow variables
- Move, rename, delete workflows and folders
CANNOT DO:
- Run or test workflows (use sim_test separately)
@@ -289,8 +291,8 @@ CANNOT DO:
WORKFLOW:
1. Call create_workflow to get a workflowId (for new workflows)
2. Call sim_build with the request and workflowId
3. Build agent gathers info and builds in one pass
2. Call sim_workflow with the request and workflowId
3. Workflow agent gathers info and builds in one pass
4. Call sim_test to verify it works
5. Optionally call sim_deploy to make it externally accessible`,
inputSchema: {
@@ -298,7 +300,7 @@ WORKFLOW:
properties: {
request: {
type: 'string',
description: 'What you want to build or modify in the workflow.',
description: 'What you want to build, modify, or organize.',
},
workflowId: {
type: 'string',
@@ -337,77 +339,6 @@ DO NOT USE (use direct tools instead):
},
annotations: { readOnlyHint: true },
},
{
name: 'sim_plan',
agentId: 'plan',
description: `Plan workflow changes by gathering required information. For most cases, prefer sim_build which combines planning and editing in one step.
USE THIS WHEN:
- You need fine-grained control over the build process
- You want to inspect the plan before executing it
WORKFLOW ID (REQUIRED):
- For NEW workflows: First call create_workflow to get a workflowId, then pass it here
- For EXISTING workflows: Always pass the workflowId parameter
This tool gathers information about available blocks, credentials, and the current workflow state.
RETURNS: A plan object containing block configurations, connections, and technical details.
IMPORTANT: Pass the returned plan EXACTLY to sim_edit - do not modify or summarize it.`,
inputSchema: {
type: 'object',
properties: {
request: {
type: 'string',
description: 'What you want to build or modify in the workflow.',
},
workflowId: {
type: 'string',
description:
'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.',
},
context: { type: 'object' },
},
required: ['request', 'workflowId'],
},
annotations: { readOnlyHint: true },
},
{
name: 'sim_edit',
agentId: 'edit',
description: `Execute a workflow plan from sim_plan. For most cases, prefer sim_build which combines planning and editing in one step.
WORKFLOW ID (REQUIRED):
- You MUST provide the workflowId parameter
PLAN (REQUIRED):
- Pass the EXACT plan object from sim_plan in the context.plan field
- Do NOT modify, summarize, or interpret the plan - pass it verbatim
After sim_edit completes, you can test immediately with sim_test, or deploy with sim_deploy to make it accessible externally.`,
inputSchema: {
type: 'object',
properties: {
message: { type: 'string', description: 'Optional additional instructions for the edit.' },
workflowId: {
type: 'string',
description:
'REQUIRED. The workflow ID to edit. Get this from create_workflow for new workflows.',
},
plan: {
type: 'object',
description: 'The plan object from sim_plan. Pass it EXACTLY as returned, do not modify.',
},
context: {
type: 'object',
description:
'Additional context. Put the plan in context.plan if not using the plan field directly.',
},
},
required: ['workflowId'],
},
annotations: { destructiveHint: false, openWorldHint: true },
},
{
name: 'sim_deploy',
agentId: 'deploy',

View File

@@ -12,6 +12,7 @@ export function serializeWorkflowMeta(wf: {
id: string
name: string
description?: string | null
folderId?: string | null
isDeployed: boolean
deployedAt?: Date | null
runCount: number
@@ -24,6 +25,7 @@ export function serializeWorkflowMeta(wf: {
id: wf.id,
name: wf.name,
description: wf.description || undefined,
folderId: wf.folderId || undefined,
isDeployed: wf.isDeployed,
deployedAt: wf.deployedAt?.toISOString(),
runCount: wf.runCount,

View File

@@ -66,7 +66,7 @@ import { listCustomTools } from '@/lib/workflows/custom-tools/operations'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
import { listSkills } from '@/lib/workflows/skills/operations'
import { listWorkflows } from '@/lib/workflows/utils'
import { listFolders, listWorkflows } from '@/lib/workflows/utils'
import {
assertActiveWorkspaceAccess,
getUsersWithPermissions,
@@ -262,10 +262,11 @@ function getStaticComponentFiles(): Map<string, string> {
*
* Structure:
* WORKSPACE.md — workspace identity, members, inventory (auto-generated)
* workflows/{name}/meta.json
* workflows/{name}/meta.json (root-level workflows)
* workflows/{name}/state.json (sanitized blocks with embedded connections)
* workflows/{name}/executions.json
* workflows/{name}/deployment.json
* workflows/{folder}/{name}/... (workflows inside folders, nested folders supported)
* knowledgebases/{name}/meta.json
* knowledgebases/{name}/documents.json
* knowledgebases/{name}/connectors.json
@@ -415,20 +416,62 @@ export class WorkspaceVFS {
}
}
/**
* Build a map from folderId to its full VFS path segment (e.g. "My Folder/Sub Folder").
* Handles nested folders via parentId traversal.
*/
private buildFolderPaths(
folders: Array<{ folderId: string; folderName: string; parentId: string | null }>
): Map<string, string> {
const folderMap = new Map<string, { name: string; parentId: string | null }>()
for (const f of folders) {
folderMap.set(f.folderId, { name: f.folderName, parentId: f.parentId })
}
const cache = new Map<string, string>()
const resolve = (id: string): string => {
if (cache.has(id)) return cache.get(id)!
const folder = folderMap.get(id)
if (!folder) return ''
const parentPath = folder.parentId ? resolve(folder.parentId) : ''
const path = parentPath
? `${parentPath}/${sanitizeName(folder.name)}`
: sanitizeName(folder.name)
cache.set(id, path)
return path
}
for (const id of folderMap.keys()) {
resolve(id)
}
return cache
}
/**
* Materialize all workflows using the shared listWorkflows function.
* Workflows are nested under their folder paths in the VFS:
* workflows/{folder}/{name}/ (if in a folder)
* workflows/{name}/ (if at workspace root)
* Returns a summary for WORKSPACE.md generation.
*/
private async materializeWorkflows(
workspaceId: string,
_userId: string
): Promise<WorkspaceMdData['workflows']> {
const workflowRows = await listWorkflows(workspaceId)
const [workflowRows, folderRows] = await Promise.all([
listWorkflows(workspaceId),
listFolders(workspaceId),
])
const folderPaths = this.buildFolderPaths(folderRows)
await Promise.all(
workflowRows.map(async (wf) => {
const safeName = sanitizeName(wf.name)
const prefix = `workflows/${safeName}/`
const folderPath = wf.folderId ? folderPaths.get(wf.folderId) : null
const prefix = folderPath
? `workflows/${folderPath}/${safeName}/`
: `workflows/${safeName}/`
this.files.set(`${prefix}meta.json`, serializeWorkflowMeta(wf))
@@ -506,6 +549,7 @@ export class WorkspaceVFS {
description: wf.description,
isDeployed: wf.isDeployed,
lastRunAt: wf.lastRunAt,
folderPath: wf.folderId ? (folderPaths.get(wf.folderId) ?? null) : null,
}))
}