Skip to content

Commit 1225250

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 7ccc86c commit 1225250

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
@@ -1173,6 +1173,9 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = {
11731173
arguments: {
11741174
$ref: '#/$defs/MothershipStreamV1AdditionalPropertiesMap',
11751175
},
1176+
execName: {
1177+
type: 'string',
1178+
},
11761179
executor: {
11771180
$ref: '#/$defs/MothershipStreamV1ToolExecutor',
11781181
},
@@ -1345,6 +1348,9 @@ export const MOTHERSHIP_STREAM_V1_SCHEMA: JsonSchema = {
13451348
internal: {
13461349
type: 'boolean',
13471350
},
1351+
simExecutable: {
1352+
type: 'boolean',
1353+
},
13481354
},
13491355
type: 'object',
13501356
},

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
@@ -433,6 +433,36 @@ describe('sse-handlers tool lifecycle', () => {
433433
expect(context.finalAssistantContent).toBe('Final answer only.')
434434
})
435435

436+
it('executes a display-named frame by its execName (the worker CLI class)', async () => {
437+
// The worker wires sim_cli frames with toolName cli_* (display identity) and
438+
// execName sim_cli. Dispatching by the display name sent every CLI call into
439+
// the app registry as "Tool not found" — caught by the bench suite.
440+
executeTool.mockResolvedValueOnce({ success: true, output: { exitCode: 0, stdout: '[]' } })
441+
await sseHandlers.tool(
442+
{
443+
type: MothershipStreamV1EventType.tool,
444+
payload: {
445+
toolCallId: 'cli-1',
446+
toolName: 'cli_workflows_list',
447+
execName: 'sim_cli',
448+
arguments: { args: ['workflows', 'list'] },
449+
executor: MothershipStreamV1ToolExecutor.sim,
450+
mode: MothershipStreamV1ToolMode.async,
451+
phase: MothershipStreamV1ToolPhase.call,
452+
ui: { simExecutable: true },
453+
},
454+
} satisfies StreamEvent,
455+
context,
456+
execContext,
457+
{ interactive: false, timeout: 1000 }
458+
)
459+
await sleep(0)
460+
expect(executeTool).toHaveBeenCalledTimes(1)
461+
expect(executeTool.mock.calls[0][0]).toBe('sim_cli')
462+
// Rendering and persistence keep the display identity.
463+
expect(context.toolCalls.get('cli-1')?.name).toBe('cli_workflows_list')
464+
})
465+
436466
it('executes tool_call and emits tool_result', async () => {
437467
executeTool.mockResolvedValueOnce({ success: true, output: { ok: true } })
438468
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
@@ -304,8 +304,11 @@ export function buildToolExecutionContext(
304304
* eventual settlement is ignored.
305305
*/
306306
async function executeToolWithWatchdog(toolCall: ToolCallState, toolContext: ExecutionContext) {
307-
const timeoutMs = toolWatchdogTimeoutMs(toolCall.name)
308-
const execution = executeTool(toolCall.name, toolCall.params || {}, toolContext)
307+
// The frame's wire name can be a display identity (the worker's cli_* names);
308+
// execution always dispatches on the model's real tool name.
309+
const executableName = toolCall.execName ?? toolCall.name
310+
const timeoutMs = toolWatchdogTimeoutMs(executableName)
311+
const execution = executeTool(executableName, toolCall.params || {}, toolContext)
309312
let timer: ReturnType<typeof setTimeout> | undefined
310313
try {
311314
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,15 +1,21 @@
11
import { spawnSync } from 'node:child_process'
2+
import { join } from 'node:path'
23

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

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

0 commit comments

Comments
 (0)