From 17a033ef156ced42fbf2def2bd9f6ac483944241 Mon Sep 17 00:00:00 2001 From: akhisud3195 Date: Tue, 8 Jul 2025 12:25:31 +0530 Subject: [PATCH] Fix control flow logic in JS --- apps/rowboat/app/lib/agent_instructions.ts | 4 +- apps/rowboat/app/lib/agents.ts | 119 ++++++++++++++------- apps/rowboat/app/lib/feature_flags.ts | 2 +- 3 files changed, 82 insertions(+), 43 deletions(-) diff --git a/apps/rowboat/app/lib/agent_instructions.ts b/apps/rowboat/app/lib/agent_instructions.ts index e2d4b3f3..76ed7bc3 100644 --- a/apps/rowboat/app/lib/agent_instructions.ts +++ b/apps/rowboat/app/lib/agent_instructions.ts @@ -31,8 +31,8 @@ ${candidateParentsNameDescriptionTools}. */ export const TRANSFER_GIVE_UP_CONTROL_INSTRUCTIONS = (candidateParentsNameDescriptionTools: string): string => ` # 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. -${candidateParentsNameDescriptionTools} +- 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}. +- If you already have an instruction before this about calling the same agent, you can discard this particular instruction. ## 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. diff --git a/apps/rowboat/app/lib/agents.ts b/apps/rowboat/app/lib/agents.ts index 24b1621b..0a114990 100644 --- a/apps/rowboat/app/lib/agents.ts +++ b/apps/rowboat/app/lib/agents.ts @@ -347,7 +347,7 @@ ${CHILD_TRANSFER_RELATED_INSTRUCTIONS} agentLogger.log(`added rag instructions`); } - // Create the agent + // Create the agent with the dynamic instructions const agent = new Agent({ name: config.name, instructions: sanitized, @@ -397,41 +397,35 @@ function convertMsgsInput(messages: z.infer[]): AgentInputItem[] } // Helper to determine the next agent name based on control settings -function getNextAgentName( +function getStartOfTurnAgentName( logger: PrefixLogger, stack: string[], agentConfig: Record>, workflow: z.infer, ): string { - logger = logger.child(`getNextAgentName`); + logger = logger.child(`getStartOfTurnAgentName`); 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 - // const lastAgentName = stack.pop() || workflow.startAgent; - // const lastAgentConfig = agentConfig[lastAgentName]; - // if (!lastAgentConfig) { - // logger.log(`last agent ${lastAgentName} not found in agent config, returning start agent: ${workflow.startAgent}`); - // return workflow.startAgent; - // } - // switch (lastAgentConfig.controlType) { - // case 'retain': - // logger.log(`last agent ${lastAgentName} control type is retain, returning last agent: ${lastAgentName}`); - // return lastAgentName; - // case 'relinquish_to_parent': - // const parentAgentName = stack.pop() || workflow.startAgent; - // logger.log(`last agent ${lastAgentName} control type is relinquish_to_parent, returning most recent parent: ${parentAgentName}`); - // return parentAgentName; - // case 'relinquish_to_start': - // logger.log(`last agent ${lastAgentName} control type is relinquish_to_start, returning start agent: ${workflow.startAgent}`); - // return workflow.startAgent; - // } + const lastAgentName = stack.pop() || workflow.startAgent; + const lastAgentConfig = agentConfig[lastAgentName]; + if (!lastAgentConfig) { + logger.log(`last agent ${lastAgentName} not found in agent config, returning start agent: ${workflow.startAgent}`); + return workflow.startAgent; + } + switch (lastAgentConfig.controlType) { + case 'retain': + logger.log(`last agent ${lastAgentName} control type is retain, returning last agent: ${lastAgentName}`); + return lastAgentName; + case 'relinquish_to_parent': + const parentAgentName = stack.pop() || workflow.startAgent; + logger.log(`last agent ${lastAgentName} control type is relinquish_to_parent, returning most recent parent: ${parentAgentName}`); + return parentAgentName; + case 'relinquish_to_start': + logger.log(`last agent ${lastAgentName} control type is relinquish_to_start, returning start agent: ${workflow.startAgent}`); + return workflow.startAgent; + } } // Logs an event and then yields it @@ -573,6 +567,7 @@ async function* emitGreetingTurn(logger: PrefixLogger, workflow: z.infer[]): string[] { + console.log(`createAgentCallStack: Messages: ${JSON.stringify(messages)}`); const stack: string[] = []; for (const msg of messages) { if (msg.role === 'assistant' && msg.agentName) { @@ -584,6 +579,7 @@ function createAgentCallStack(messages: z.infer[]): string[] { stack.push(msg.agentName); } } + console.log(`createAgentCallStack: Stack: ${JSON.stringify(stack)}`); return stack; } @@ -639,6 +635,28 @@ function createAgents( 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 // using OpenAI Agents SDK export async function* streamResponse( @@ -646,6 +664,8 @@ export async function* streamResponse( projectTools: z.infer[], messages: z.infer[], ): AsyncIterable | z.infer> { + // Divider log for tracking agent loop start + console.log('-------------------- AGENT LOOP START --------------------'); // set up logging let logger = new PrefixLogger(`agent-loop`) logger.log('projectId', workflow.projectId); @@ -679,7 +699,7 @@ export async function* streamResponse( const usageTracker = new UsageTracker(); // get next agent name - let agentName = getNextAgentName(logger, stack, agentConfig, workflow); + let agentName = getStartOfTurnAgentName(logger, stack, agentConfig, workflow); // set up initial state for loop logger.log('@@ starting agent turn @@'); @@ -702,18 +722,36 @@ export async function* streamResponse( } 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 const inputs = convertMsgsInput(turnMsgs); // run the agent - const result = await run(agent, inputs, { - stream: true, - }); + const result = await run( + agent, + inputs, + { + stream: true, + } + ); // handle streaming events for await (const event of result) { const eventLogger = loopLogger.child(event.type); - // eventLogger.log(`----------> event: ${JSON.stringify(event)}`); + eventLogger.log(`----------> stack: ${JSON.stringify(stack)}`); switch (event.type) { case 'raw_model_stream_event': @@ -776,11 +814,14 @@ export async function* streamResponse( // update transfer counter transferCounter.increment(agentName, event.item.targetAgent.name); - // add current agent to stack - stack.push(agentName); + // add current agent to stack only if new agent is internal + const newAgentName = event.item.targetAgent.name; + if (agentConfig[newAgentName]?.outputVisibility === 'internal') { + stack.push(newAgentName); + } // set this as the new agent name - agentName = event.item.targetAgent.name; + agentName = newAgentName; 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 (isInternal) { const current = agentName; - agentName = getNextAgentName(logger, stack, agentConfig, workflow); + // pop the stack + agentName = stack.pop()!; // emit transfer tool call invocation const [transferStart, transferComplete] = createTransferEvents(current, agentName); @@ -846,9 +888,6 @@ export async function* streamResponse( // update transfer counter transferCounter.increment(current, agentName); - // add current agent to stack - stack.push(current); - // set this as the new agent name loopLogger.log(`switched to agent (reason: internal agent put out a message): ${agentName}`); diff --git a/apps/rowboat/app/lib/feature_flags.ts b/apps/rowboat/app/lib/feature_flags.ts index 8a530ee2..9c45c8ae 100644 --- a/apps/rowboat/app/lib/feature_flags.ts +++ b/apps/rowboat/app/lib/feature_flags.ts @@ -11,6 +11,6 @@ export const USE_BILLING = process.env.USE_BILLING === 'true'; export const USE_MULTIPLE_PROJECTS = true; export const USE_TESTING_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 SHOW_COPILOT_MARQUEE = false; \ No newline at end of file