Skip to content

Commit 28f0075

Browse files
committed
fix(mothership): execute display-named frames by their execName
Companion to mothership b4910ed8. The worker's CLI frames carry cli_* display identities; execution now dispatches on the frame's execName (ToolCallState.execName) while rendering and persistence keep the display name. Regenerated stream contract (execName + ui.simExecutable); the generator's formatter step also un-broken (biome refuses stdin paths its config excludes, so generated output formats under a neutral path). Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent e8b15e2 commit 28f0075

7 files changed

Lines changed: 55 additions & 4 deletions

File tree

‎apps/sim/lib/mothership/generated/mothership-stream-v1-schema.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,9 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = {
11791179
arguments: {
11801180
$ref: '#/$defs/MothershipStreamV1AdditionalPropertiesMap',
11811181
},
1182+
execName: {
1183+
type: 'string',
1184+
},
11821185
executor: {
11831186
$ref: '#/$defs/MothershipStreamV1ToolExecutor',
11841187
},
@@ -1351,6 +1354,9 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = {
13511354
internal: {
13521355
type: 'boolean',
13531356
},
1357+
simExecutable: {
1358+
type: 'boolean',
1359+
},
13541360
},
13551361
type: 'object',
13561362
},

‎apps/sim/lib/mothership/generated/mothership-stream-v1.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export interface MothershipStreamV1ToolCallEventEnvelope {
145145
}
146146
export interface MothershipStreamV1ToolCallDescriptor {
147147
arguments?: MothershipStreamV1AdditionalPropertiesMap
148+
execName?: string
148149
executor: MothershipStreamV1ToolExecutor
149150
mode: MothershipStreamV1ToolMode
150151
partial?: boolean
@@ -162,6 +163,7 @@ export interface MothershipStreamV1ToolUI {
162163
hidden?: boolean
163164
inbandOwned?: boolean
164165
internal?: boolean
166+
simExecutable?: boolean
165167
}
166168
export interface MothershipStreamV1ToolArgsDeltaEventEnvelope {
167169
payload: MothershipStreamV1ToolArgsDeltaPayload

‎apps/sim/lib/mothership/request/handlers/handlers.test.ts‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,36 @@ describe('sse-handlers tool lifecycle', () => {
444444
expect(context.finalAssistantContent).toBe('Final answer only.')
445445
})
446446

447+
it('executes a display-named frame by its execName (the worker CLI class)', async () => {
448+
// The worker wires sim_cli frames with toolName cli_* (display identity) and
449+
// execName sim_cli. Dispatching by the display name sent every CLI call into
450+
// the app registry as "Tool not found" — caught by the bench suite.
451+
executeTool.mockResolvedValueOnce({ success: true, output: { exitCode: 0, stdout: '[]' } })
452+
await sseHandlers.tool(
453+
{
454+
type: MothershipStreamV1EventType.tool,
455+
payload: {
456+
toolCallId: 'cli-1',
457+
toolName: 'cli_workflows_list',
458+
execName: 'sim_cli',
459+
arguments: { args: ['workflows', 'list'] },
460+
executor: MothershipStreamV1ToolExecutor.sim,
461+
mode: MothershipStreamV1ToolMode.async,
462+
phase: MothershipStreamV1ToolPhase.call,
463+
ui: { simExecutable: true },
464+
},
465+
} satisfies StreamEvent,
466+
context,
467+
execContext,
468+
{ interactive: false, timeout: 1000 }
469+
)
470+
await sleep(0)
471+
expect(executeTool).toHaveBeenCalledTimes(1)
472+
expect(executeTool.mock.calls[0][0]).toBe('sim_cli')
473+
// Rendering and persistence keep the display identity.
474+
expect(context.toolCalls.get('cli-1')?.name).toBe('cli_workflows_list')
475+
})
476+
447477
it('executes tool_call and emits tool_result', async () => {
448478
executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } })
449479
const onEvent = vi.fn()

‎apps/sim/lib/mothership/request/handlers/tool.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ async function handleCallPhase(
509509
// into the server tool context — this is what scopes the prepare_file_edit ->
510510
// apply_file_edit intent handoff to one file subagent under concurrency.
511511
if (parentToolCallId) toolCall.parentToolCallId = parentToolCallId
512+
if (data.execName) toolCall.execName = data.execName
512513

513514
const readPath = typeof args?.path === 'string' ? args.path : undefined
514515
if (toolName === 'read' && readPath?.startsWith('internal/')) return

‎apps/sim/lib/mothership/request/tools/executor.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,8 +298,11 @@ export function buildToolExecutionContext(
298298
* eventual settlement is ignored.
299299
*/
300300
async function executeToolWithWatchdog(toolCall: ToolCallState, toolContext: ExecutionContext) {
301-
const timeoutMs = toolWatchdogTimeoutMs(toolCall.name)
302-
const execution = executeTool(toolCall.name, toolCall.params || {}, toolContext)
301+
// The frame's wire name can be a display identity (the worker's cli_* names);
302+
// execution always dispatches on the model's real tool name.
303+
const executableName = toolCall.execName ?? toolCall.name
304+
const timeoutMs = toolWatchdogTimeoutMs(executableName)
305+
const execution = executeTool(executableName, toolCall.params || {}, toolContext)
303306
let timer: ReturnType<typeof setTimeout> | undefined
304307
try {
305308
return await Promise.race([

‎apps/sim/lib/mothership/request/types.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export function isTerminalToolCallStatus(status?: string): boolean {
2727
export interface ToolCallState {
2828
id: string
2929
name: string
30+
/** The model's tool name when `name` is a display identity (the worker's cli_*
31+
* names). Execution dispatches on this; rendering and persistence keep `name`. */
32+
execName?: string
3033
status: ToolCallStatus
3134
/** Bounded registry ID of the agent that invoked this tool. */
3235
agentId?: string

‎scripts/format-generated-source.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
import { spawnSync } from 'node:child_process'
22
import { localBin } from './local-bin'
3+
import { join } from 'node:path'
34

45
export function formatGeneratedSource(source: string, stdinFilePath: string, cwd: string): string {
5-
const result = spawnSync(localBin('biome'), ['format', '--stdin-file-path', stdinFilePath], {
6+
// biome.json excludes the generated output dirs, and biome refuses to format a
7+
// stdin whose declared path is excluded — so declare a neutral path instead;
8+
// formatting rules do not vary by location, only ignores do.
9+
void stdinFilePath
10+
const neutralPath = join(cwd, 'scripts', '.generated-format-buffer.ts')
11+
const result = spawnSync(localBin('biome'), ['format', '--stdin-file-path', neutralPath], {
612
cwd,
713
encoding: 'utf8',
814
input: source,
915
})
1016

1117
if (result.status !== 0) {
1218
throw new Error(
13-
`Failed to format generated source for ${stdinFilePath}:\n${
19+
`Failed to format generated source for ${neutralPath}:\n${
1420
result.stderr || result.stdout || 'unknown error'
1521
}`
1622
)

0 commit comments

Comments
 (0)