Fix control flow logic in JS

This commit is contained in:
akhisud3195 2025-07-08 12:25:31 +05:30
parent f60850fe12
commit 17a033ef15
3 changed files with 82 additions and 43 deletions

View file

@ -31,8 +31,8 @@ ${candidateParentsNameDescriptionTools}.
*/ */
export const TRANSFER_GIVE_UP_CONTROL_INSTRUCTIONS = (candidateParentsNameDescriptionTools: string): string => ` export const TRANSFER_GIVE_UP_CONTROL_INSTRUCTIONS = (candidateParentsNameDescriptionTools: string): string => `
# Instructions about giving up chat control # Instructions about giving up chat control
If you are unable to handle the chat (e.g. if it is not in your scope of instructions), you should use the tool call provided to give up control of the chat. - If you are unable to handle the chat (e.g. if it is not in your scope of instructions), you should give up control of the chat by calling: ${candidateParentsNameDescriptionTools}.
${candidateParentsNameDescriptionTools} - If you already have an instruction before this about calling the same agent, you can discard this particular instruction.
## Notes: ## Notes:
- When you give up control of the chat, you should not provide any response to the user. Just invoke the tool call to give up control. - When you give up control of the chat, you should not provide any response to the user. Just invoke the tool call to give up control.

View file

@ -347,7 +347,7 @@ ${CHILD_TRANSFER_RELATED_INSTRUCTIONS}
agentLogger.log(`added rag instructions`); agentLogger.log(`added rag instructions`);
} }
// Create the agent // Create the agent with the dynamic instructions
const agent = new Agent({ const agent = new Agent({
name: config.name, name: config.name,
instructions: sanitized, instructions: sanitized,
@ -397,41 +397,35 @@ function convertMsgsInput(messages: z.infer<typeof Message>[]): AgentInputItem[]
} }
// Helper to determine the next agent name based on control settings // Helper to determine the next agent name based on control settings
function getNextAgentName( function getStartOfTurnAgentName(
logger: PrefixLogger, logger: PrefixLogger,
stack: string[], stack: string[],
agentConfig: Record<string, z.infer<typeof WorkflowAgent>>, agentConfig: Record<string, z.infer<typeof WorkflowAgent>>,
workflow: z.infer<typeof Workflow>, workflow: z.infer<typeof Workflow>,
): string { ): string {
logger = logger.child(`getNextAgentName`); logger = logger.child(`getStartOfTurnAgentName`);
logger.log(`stack: ${stack.join(', ')}`); logger.log(`stack: ${stack.join(', ')}`);
// get the last agent from the stack
// if stack is empty, use the start agent
const lastAgentName = stack.pop() || workflow.startAgent;
return lastAgentName;
// TODO: control-type logic is being ignored for now
// if control type is retain, return last agent // if control type is retain, return last agent
// const lastAgentName = stack.pop() || workflow.startAgent; const lastAgentName = stack.pop() || workflow.startAgent;
// const lastAgentConfig = agentConfig[lastAgentName]; const lastAgentConfig = agentConfig[lastAgentName];
// if (!lastAgentConfig) { if (!lastAgentConfig) {
// logger.log(`last agent ${lastAgentName} not found in agent config, returning start agent: ${workflow.startAgent}`); logger.log(`last agent ${lastAgentName} not found in agent config, returning start agent: ${workflow.startAgent}`);
// return workflow.startAgent; return workflow.startAgent;
// } }
// switch (lastAgentConfig.controlType) { switch (lastAgentConfig.controlType) {
// case 'retain': case 'retain':
// logger.log(`last agent ${lastAgentName} control type is retain, returning last agent: ${lastAgentName}`); logger.log(`last agent ${lastAgentName} control type is retain, returning last agent: ${lastAgentName}`);
// return lastAgentName; return lastAgentName;
// case 'relinquish_to_parent': case 'relinquish_to_parent':
// const parentAgentName = stack.pop() || workflow.startAgent; const parentAgentName = stack.pop() || workflow.startAgent;
// logger.log(`last agent ${lastAgentName} control type is relinquish_to_parent, returning most recent parent: ${parentAgentName}`); logger.log(`last agent ${lastAgentName} control type is relinquish_to_parent, returning most recent parent: ${parentAgentName}`);
// return parentAgentName; return parentAgentName;
// case 'relinquish_to_start': case 'relinquish_to_start':
// logger.log(`last agent ${lastAgentName} control type is relinquish_to_start, returning start agent: ${workflow.startAgent}`); logger.log(`last agent ${lastAgentName} control type is relinquish_to_start, returning start agent: ${workflow.startAgent}`);
// return workflow.startAgent; return workflow.startAgent;
// } }
} }
// Logs an event and then yields it // Logs an event and then yields it
@ -573,6 +567,7 @@ async function* emitGreetingTurn(logger: PrefixLogger, workflow: z.infer<typeof
} }
function createAgentCallStack(messages: z.infer<typeof Message>[]): string[] { function createAgentCallStack(messages: z.infer<typeof Message>[]): string[] {
console.log(`createAgentCallStack: Messages: ${JSON.stringify(messages)}`);
const stack: string[] = []; const stack: string[] = [];
for (const msg of messages) { for (const msg of messages) {
if (msg.role === 'assistant' && msg.agentName) { if (msg.role === 'assistant' && msg.agentName) {
@ -584,6 +579,7 @@ function createAgentCallStack(messages: z.infer<typeof Message>[]): string[] {
stack.push(msg.agentName); stack.push(msg.agentName);
} }
} }
console.log(`createAgentCallStack: Stack: ${JSON.stringify(stack)}`);
return stack; return stack;
} }
@ -639,6 +635,28 @@ function createAgents(
return { agents, mentions }; return { agents, mentions };
} }
// Helper to get give up control instructions for child agents
function getGiveUpControlInstructions(
agent: Agent,
parentAgentName: string,
logger: PrefixLogger
): string {
let dynamicInstructions: string;
if (typeof agent.instructions === 'string') {
dynamicInstructions = agent.instructions;
} else {
throw new Error('Agent instructions must be a string for dynamic injection.');
}
// Only include the @mention for the parent, not the tool call format
const parentBlock = `@agent:${parentAgentName}`;
// Import the template
const { TRANSFER_GIVE_UP_CONTROL_INSTRUCTIONS } = require('./agent_instructions');
dynamicInstructions = dynamicInstructions + '\n\n' + TRANSFER_GIVE_UP_CONTROL_INSTRUCTIONS(parentBlock);
// For tracking
logger.log(`[inject] Added give up control instructions for ${agent.name} with parent ${parentAgentName}`);
return dynamicInstructions;
}
// Main function to stream an agentic response // Main function to stream an agentic response
// using OpenAI Agents SDK // using OpenAI Agents SDK
export async function* streamResponse( export async function* streamResponse(
@ -646,6 +664,8 @@ export async function* streamResponse(
projectTools: z.infer<typeof WorkflowTool>[], projectTools: z.infer<typeof WorkflowTool>[],
messages: z.infer<typeof Message>[], messages: z.infer<typeof Message>[],
): AsyncIterable<z.infer<typeof ZOutMessage> | z.infer<typeof ZUsage>> { ): AsyncIterable<z.infer<typeof ZOutMessage> | z.infer<typeof ZUsage>> {
// Divider log for tracking agent loop start
console.log('-------------------- AGENT LOOP START --------------------');
// set up logging // set up logging
let logger = new PrefixLogger(`agent-loop`) let logger = new PrefixLogger(`agent-loop`)
logger.log('projectId', workflow.projectId); logger.log('projectId', workflow.projectId);
@ -679,7 +699,7 @@ export async function* streamResponse(
const usageTracker = new UsageTracker(); const usageTracker = new UsageTracker();
// get next agent name // get next agent name
let agentName = getNextAgentName(logger, stack, agentConfig, workflow); let agentName = getStartOfTurnAgentName(logger, stack, agentConfig, workflow);
// set up initial state for loop // set up initial state for loop
logger.log('@@ starting agent turn @@'); logger.log('@@ starting agent turn @@');
@ -702,18 +722,36 @@ export async function* streamResponse(
} }
const agent: Agent = agents[agentName]!; const agent: Agent = agents[agentName]!;
// --- DYNAMICALLY INJECT GIVE UP CONTROL INSTRUCTIONS FOR CHILD AGENTS ---
// Only inject for non-internal agents with controlType 'retain'
const agentConfigObj = agentConfig[agentName];
const isInternal = agentConfigObj?.outputVisibility === 'internal';
const isRetain = agentConfigObj?.controlType === 'retain';
loopLogger.log(`isInternal: ${isInternal}`);
loopLogger.log(`isRetain: ${isRetain}`);
loopLogger.log(`stack.length: ${stack.length}`);
if (!isInternal && isRetain && stack.length > 0) {
const parentAgentName = stack[stack.length - 1];
agents[agentName].instructions = getGiveUpControlInstructions(agent, parentAgentName, loopLogger);
}
// --- END DYNAMIC INJECTION ---
// convert messages to agents sdk compatible input // convert messages to agents sdk compatible input
const inputs = convertMsgsInput(turnMsgs); const inputs = convertMsgsInput(turnMsgs);
// run the agent // run the agent
const result = await run(agent, inputs, { const result = await run(
stream: true, agent,
}); inputs,
{
stream: true,
}
);
// handle streaming events // handle streaming events
for await (const event of result) { for await (const event of result) {
const eventLogger = loopLogger.child(event.type); const eventLogger = loopLogger.child(event.type);
// eventLogger.log(`----------> event: ${JSON.stringify(event)}`); eventLogger.log(`----------> stack: ${JSON.stringify(stack)}`);
switch (event.type) { switch (event.type) {
case 'raw_model_stream_event': case 'raw_model_stream_event':
@ -776,11 +814,14 @@ export async function* streamResponse(
// update transfer counter // update transfer counter
transferCounter.increment(agentName, event.item.targetAgent.name); transferCounter.increment(agentName, event.item.targetAgent.name);
// add current agent to stack // add current agent to stack only if new agent is internal
stack.push(agentName); const newAgentName = event.item.targetAgent.name;
if (agentConfig[newAgentName]?.outputVisibility === 'internal') {
stack.push(newAgentName);
}
// set this as the new agent name // set this as the new agent name
agentName = event.item.targetAgent.name; agentName = newAgentName;
loopLogger.log(`switched to agent: ${agentName}`); loopLogger.log(`switched to agent: ${agentName}`);
} }
@ -830,7 +871,8 @@ export async function* streamResponse(
// if this is an internal agent, switch to previous agent // if this is an internal agent, switch to previous agent
if (isInternal) { if (isInternal) {
const current = agentName; const current = agentName;
agentName = getNextAgentName(logger, stack, agentConfig, workflow); // pop the stack
agentName = stack.pop()!;
// emit transfer tool call invocation // emit transfer tool call invocation
const [transferStart, transferComplete] = createTransferEvents(current, agentName); const [transferStart, transferComplete] = createTransferEvents(current, agentName);
@ -846,9 +888,6 @@ export async function* streamResponse(
// update transfer counter // update transfer counter
transferCounter.increment(current, agentName); transferCounter.increment(current, agentName);
// add current agent to stack
stack.push(current);
// set this as the new agent name // set this as the new agent name
loopLogger.log(`switched to agent (reason: internal agent put out a message): ${agentName}`); loopLogger.log(`switched to agent (reason: internal agent put out a message): ${agentName}`);

View file

@ -11,6 +11,6 @@ export const USE_BILLING = process.env.USE_BILLING === 'true';
export const USE_MULTIPLE_PROJECTS = true; export const USE_MULTIPLE_PROJECTS = true;
export const USE_TESTING_FEATURE = false; export const USE_TESTING_FEATURE = false;
export const USE_VOICE_FEATURE = false; export const USE_VOICE_FEATURE = false;
export const USE_TRANSFER_CONTROL_OPTIONS = false; export const USE_TRANSFER_CONTROL_OPTIONS = true;
export const USE_PRODUCT_TOUR = true; export const USE_PRODUCT_TOUR = true;
export const SHOW_COPILOT_MARQUEE = false; export const SHOW_COPILOT_MARQUEE = false;