Skip to content

Commit 08331e6

Browse files
committed
Applying returnGuidance in orchestrator
1 parent fa590ab commit 08331e6

File tree

3 files changed

+99
-24
lines changed

3 files changed

+99
-24
lines changed

packages/mcp-workflow/src/common/metadata.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,19 @@ export interface NodeGuidanceData<TResultSchema extends z.ZodObject<z.ZodRawShap
7878
*/
7979
exampleOutput?: string;
8080
/**
81-
* Optional guidance for the LLM to return to the orchestrator.
82-
* When provided, this guidance will replace the default "return to orchestrator"
83-
* guidance that the orchestrator normally provides to the LLM, to return the
84-
* workflow to the orchestrator after task completion. Make sure this custom
85-
* guidance can properly return the workflow to the orchestrator, or the workflow
86-
* will likely be broken.
81+
* Optional custom guidance for the LLM to return results to the orchestrator.
82+
*
83+
* When provided, this replaces the orchestrator's default "return to orchestrator"
84+
* prompt. The function receives only `workflowStateData` (the runtime session state
85+
* that the producer doesn't have at construction time). The producer already owns
86+
* `resultSchema` and `exampleOutput` as sibling properties on this same struct,
87+
* so they can be captured in the closure if needed.
88+
*
89+
* Ensure this custom guidance properly instructs the LLM to return the workflow
90+
* to the orchestrator, or the workflow will likely be broken.
91+
*
92+
* @param workflowStateData - The workflow state data to round-trip back to the orchestrator
93+
* @returns The return guidance prompt string
8794
*/
8895
returnGuidance?: (workflowStateData: WorkflowStateData) => string;
8996
}

packages/mcp-workflow/src/tools/orchestrator/orchestratorTool.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -364,22 +364,13 @@ instructions for continuing the workflow.
364364
nodeGuidanceData: NodeGuidanceData<z.ZodObject<z.ZodRawShape>>,
365365
workflowStateData: WorkflowStateData
366366
): string {
367-
const resultSchemaJson = JSON.stringify(
368-
zodToJsonSchema(nodeGuidanceData.resultSchema),
369-
null,
370-
2
371-
);
372-
373-
// Build example section if provided
374-
const exampleSection = nodeGuidanceData.exampleOutput
375-
? `
376-
For example, a properly formatted result should look like:
377-
378-
\`\`\`json
379-
${nodeGuidanceData.exampleOutput}
380-
\`\`\`
381-
`
382-
: '';
367+
const returnGuidance = nodeGuidanceData.returnGuidance
368+
? nodeGuidanceData.returnGuidance(workflowStateData)
369+
: this.defaultReturnGuidance(
370+
workflowStateData,
371+
nodeGuidanceData.resultSchema,
372+
nodeGuidanceData.exampleOutput
373+
);
383374

384375
return `
385376
# ROLE
@@ -391,7 +382,28 @@ you with direct guidance for the current task.
391382
392383
${nodeGuidanceData.taskGuidance}
393384
394-
# CRITICAL: REQUIRED NEXT STEP
385+
${returnGuidance}
386+
`;
387+
}
388+
389+
private defaultReturnGuidance(
390+
workflowStateData: WorkflowStateData,
391+
resultSchema: z.ZodObject<z.ZodRawShape>,
392+
exampleOutput?: string
393+
): string {
394+
const resultSchemaJson = JSON.stringify(zodToJsonSchema(resultSchema), null, 2);
395+
396+
// Build example section if provided
397+
const exampleSection = exampleOutput
398+
? `
399+
For example, a properly formatted result should look like:
400+
401+
\`\`\`json
402+
${exampleOutput}
403+
\`\`\`
404+
`
405+
: '';
406+
return `# CRITICAL: REQUIRED NEXT STEP
395407
396408
After completing the task above, you **MUST** invoke the \`${this.toolMetadata.toolId}\` tool
397409
to continue the workflow. The workflow CANNOT proceed without this tool call.
@@ -424,7 +436,7 @@ Here is an example of the EXACT format your tool call should follow:
424436
\`\`\`
425437
Tool: ${this.toolMetadata.toolId}
426438
Parameters:
427-
${WORKFLOW_PROPERTY_NAMES.userInput}: ${nodeGuidanceData.exampleOutput ? nodeGuidanceData.exampleOutput.replaceAll('\n', '\n ') : '{ /* your result object here */ }'}
439+
${WORKFLOW_PROPERTY_NAMES.userInput}: ${exampleOutput ? exampleOutput.replaceAll('\n', '\n ') : '{ /* your result object here */ }'}
428440
${WORKFLOW_PROPERTY_NAMES.workflowStateData}: ${JSON.stringify(workflowStateData)}
429441
\`\`\`
430442
`;

packages/mcp-workflow/tests/tools/orchestrator/orchestratorTool.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,62 @@ Gather user input for platform and projectName.
578578
expect(prompt).not.toContain('Invoke the following MCP server tool');
579579
});
580580

581+
it('should use custom returnGuidance when provided in NodeGuidanceData', async () => {
582+
const orchestratorToolId = 'test-return-guidance-orchestrator';
583+
const customReturnMarker = '## CUSTOM RETURN GUIDANCE MARKER';
584+
585+
const workflow = new StateGraph(TestState)
586+
.addNode('guidedNode', (_state: State) => {
587+
const nodeGuidanceData: NodeGuidanceData<typeof GET_INPUT_WORKFLOW_RESULT_SCHEMA> = {
588+
nodeId: 'test-custom-return',
589+
taskGuidance: 'Do something interesting.',
590+
resultSchema: GET_INPUT_WORKFLOW_RESULT_SCHEMA,
591+
exampleOutput: '{ "userUtterance": "example" }',
592+
returnGuidance: workflowStateData =>
593+
`${customReturnMarker}\nReturn to orchestrator with thread: ${workflowStateData.thread_id}`,
594+
};
595+
return interrupt(nodeGuidanceData);
596+
})
597+
.addEdge(START, 'guidedNode')
598+
.addEdge('guidedNode', END);
599+
600+
const config: OrchestratorConfig = {
601+
toolId: orchestratorToolId,
602+
title: 'Return Guidance Test',
603+
description: 'Tests custom returnGuidance in NodeGuidanceData',
604+
workflow,
605+
stateManager: new WorkflowStateManager({ environment: 'test' }),
606+
logger: mockLogger,
607+
};
608+
609+
const orchestrator = new OrchestratorTool(server, config);
610+
611+
const result = await orchestrator.handleRequest(
612+
{
613+
userInput: {},
614+
workflowStateData: { thread_id: '' },
615+
},
616+
createMockExtra()
617+
);
618+
619+
expect(result.structuredContent).toBeDefined();
620+
const output = result.structuredContent as { orchestrationInstructionsPrompt: string };
621+
const prompt = output.orchestrationInstructionsPrompt;
622+
623+
// Should contain the custom return guidance
624+
expect(prompt).toContain(customReturnMarker);
625+
expect(prompt).toContain('Return to orchestrator with thread: mmw-');
626+
627+
// Should still contain the task guidance structure
628+
expect(prompt).toContain('# TASK GUIDANCE');
629+
expect(prompt).toContain('Do something interesting.');
630+
631+
// Should NOT contain the default return guidance sections
632+
expect(prompt).not.toContain('# CRITICAL: REQUIRED NEXT STEP');
633+
expect(prompt).not.toContain('# OUTPUT FORMAT');
634+
expect(prompt).not.toContain('# EXAMPLE TOOL CALL');
635+
});
636+
581637
it('should use delegate mode (orchestration prompt) when MCPToolInvocationData is used', async () => {
582638
const workflow = new StateGraph(TestState)
583639
.addNode('regularInterrupt', (_state: State) => {

0 commit comments

Comments
 (0)