-
Notifications
You must be signed in to change notification settings - Fork 368
Use --prompt-file for Copilot execution and add Copilot driver fallback handling
#26492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
51862b0
0231c3f
de5c3d1
eea7123
c934094
9f3d6be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ | |
| * - Maximum 3 retry attempts after the initial run. | ||
| * | ||
| * Usage: node copilot_driver.cjs <command> [args...] | ||
| * Example: node copilot_driver.cjs copilot --add-dir /tmp/ --prompt "..." | ||
| * Example: node copilot_driver.cjs copilot --add-dir /tmp/ --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt | ||
| */ | ||
|
|
||
| "use strict"; | ||
|
|
@@ -37,6 +37,9 @@ const INITIAL_DELAY_MS = 5000; | |
| const BACKOFF_MULTIPLIER = 2; | ||
| // Maximum delay cap in milliseconds | ||
| const MAX_DELAY_MS = 60000; | ||
| // If prompt files are larger than this threshold, avoid inlining into argv. | ||
| const PROMPT_FILE_INLINE_THRESHOLD_BYTES = 100 * 1024; | ||
| const PROMPT_FILE_INLINE_THRESHOLD_LABEL = "100KB"; | ||
|
|
||
| // Pattern to detect transient CAPIError 400 in copilot output | ||
| const CAPI_ERROR_400_PATTERN = /CAPIError:\s*400/; | ||
|
|
@@ -167,7 +170,7 @@ function runProcess(command, args, attempt) { | |
| return new Promise(resolve => { | ||
| const startTime = Date.now(); | ||
| // Redact --prompt value from logs to avoid leaking prompt content | ||
| const safeArgs = args.map((arg, i) => (args[i - 1] === "--prompt" ? "<redacted>" : arg)); | ||
| const safeArgs = args.map((arg, i) => (args[i - 1] === "--prompt" || args[i - 1] === "-p" ? "<redacted>" : arg)); | ||
| log(`attempt ${attempt + 1}: spawning: ${command} ${safeArgs.join(" ")}`); | ||
|
|
||
| const child = spawn(command, args, { | ||
|
|
@@ -235,6 +238,63 @@ function runProcess(command, args, attempt) { | |
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Build a compact fallback prompt that asks the agent to read instructions from disk. | ||
| * @param {string} promptFile | ||
| * @returns {string} | ||
| */ | ||
| function buildPromptFileFallbackInstruction(promptFile) { | ||
| return `Read the full instructions from ${promptFile} and execute them exactly as written.`; | ||
| } | ||
|
|
||
| /** | ||
| * Replace --prompt-file arguments with -p prompt text to support older Copilot CLIs. | ||
| * For files over 100KB, emit a compact fallback prompt that instructs the agent to | ||
| * read and execute the full prompt file from disk. | ||
| * @param {string[]} args | ||
| * @returns {string[]} | ||
| */ | ||
| function resolvePromptFileArgs(args) { | ||
| /** @type {string[]} */ | ||
| const resolvedArgs = []; | ||
|
|
||
| for (let i = 0; i < args.length; i++) { | ||
| const arg = args[i]; | ||
| if (arg !== "--prompt-file") { | ||
| resolvedArgs.push(arg); | ||
| continue; | ||
| } | ||
|
|
||
| if (i + 1 >= args.length) { | ||
| log("warning: --prompt-file provided without a path; leaving arguments unchanged"); | ||
| resolvedArgs.push(arg); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Smoke Test Observation: The |
||
| continue; | ||
| } | ||
| const promptFile = args[i + 1]; | ||
|
|
||
| try { | ||
| const stat = fs.statSync(promptFile); | ||
| log(`resolved --prompt-file: path=${promptFile} size=${stat.size}B`); | ||
|
|
||
| if (stat.size > PROMPT_FILE_INLINE_THRESHOLD_BYTES) { | ||
| log(`prompt file exceeds ${PROMPT_FILE_INLINE_THRESHOLD_LABEL}; using compact fallback prompt`); | ||
| resolvedArgs.push("-p", buildPromptFileFallbackInstruction(promptFile)); | ||
| } else { | ||
| const promptText = fs.readFileSync(promptFile, "utf8"); | ||
| resolvedArgs.push("-p", promptText); | ||
| } | ||
| i++; // Skip the prompt-file path argument | ||
| } catch (error) { | ||
| const err = /** @type {Error} */ error; | ||
| log(`warning: failed to resolve --prompt-file ${promptFile}: ${err.message}; leaving arguments unchanged`); | ||
| resolvedArgs.push(arg, promptFile); | ||
| i++; // Skip the prompt-file path argument | ||
| } | ||
| } | ||
|
|
||
| return resolvedArgs; | ||
| } | ||
|
|
||
| /** | ||
| * Main entry point: run copilot with retry logic for partially-executed sessions. | ||
| */ | ||
|
|
@@ -249,14 +309,15 @@ async function main() { | |
| log(`starting: command=${command} maxRetries=${MAX_RETRIES} initialDelayMs=${INITIAL_DELAY_MS}` + ` backoffMultiplier=${BACKOFF_MULTIPLIER} maxDelayMs=${MAX_DELAY_MS}` + ` nodeVersion=${process.version} platform=${process.platform}`); | ||
|
|
||
| await checkCommandAccessible(command); | ||
| const resolvedArgs = resolvePromptFileArgs(args); | ||
|
|
||
| let delay = INITIAL_DELAY_MS; | ||
| let lastExitCode = 1; | ||
| const driverStartTime = Date.now(); | ||
|
|
||
| for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { | ||
| // Add --continue flag on retries so the copilot session continues from where it left off | ||
| const currentArgs = attempt > 0 ? [...args, "--continue"] : args; | ||
| const currentArgs = attempt > 0 ? [...resolvedArgs, "--continue"] : resolvedArgs; | ||
|
|
||
| if (attempt > 0) { | ||
| log(`retry ${attempt}/${MAX_RETRIES}: sleeping ${delay}ms before next attempt with --continue`); | ||
|
|
@@ -335,7 +396,17 @@ async function main() { | |
| process.exit(lastExitCode); | ||
| } | ||
|
|
||
| main().catch(err => { | ||
| log(`unexpected error: ${err.message}`); | ||
| process.exit(1); | ||
| }); | ||
| if (typeof module !== "undefined" && module.exports) { | ||
| module.exports = { | ||
| PROMPT_FILE_INLINE_THRESHOLD_BYTES, | ||
| buildPromptFileFallbackInstruction, | ||
| resolvePromptFileArgs, | ||
| }; | ||
| } | ||
|
|
||
| if (require.main === module) { | ||
| main().catch(err => { | ||
| log(`unexpected error: ${err.message}`); | ||
| process.exit(1); | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -175,8 +175,8 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st | |
| } | ||
|
|
||
| // Build the command - model is always passed via COPILOT_MODEL env var (see env block below). | ||
| // The --add-dir "${GITHUB_WORKSPACE}" and --prompt args are appended raw (not through | ||
| // shellJoinArgs) because they contain shell variable references that must expand at runtime. | ||
| // The --add-dir "${GITHUB_WORKSPACE}" arg is appended raw (not through shellJoinArgs) | ||
| // because it contains a shell variable reference that must expand at runtime. | ||
| // | ||
| // When a driver script is provided (GetDriverScriptName), wrap the copilot invocation with | ||
| // `node <driver> <commandName> <args>` to enable retry logic for transient CAPIError 400 errors. | ||
|
|
@@ -196,11 +196,11 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st | |
| } | ||
|
|
||
| if sandboxEnabled { | ||
| // Sandbox mode: add workspace dir and inline prompt (read inside AWF container) | ||
| copilotCommand = fmt.Sprintf(`%s %s --add-dir "${GITHUB_WORKSPACE}" --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"`, execPrefix, shellJoinArgs(copilotArgs)) | ||
| // Sandbox mode: add workspace dir and pass prompt file path directly | ||
| copilotCommand = fmt.Sprintf(`%s %s --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt`, execPrefix, shellJoinArgs(copilotArgs)) | ||
| } else { | ||
| // Non-sandbox mode: prompt is read from a shell variable set earlier in the script | ||
| copilotCommand = fmt.Sprintf(`%s %s --prompt "$COPILOT_CLI_INSTRUCTION"`, execPrefix, shellJoinArgs(copilotArgs)) | ||
| // Non-sandbox mode: pass prompt file path directly | ||
| copilotCommand = fmt.Sprintf(`%s %s --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt`, execPrefix, shellJoinArgs(copilotArgs)) | ||
| } | ||
|
Comment on lines
198
to
204
|
||
|
|
||
| // Conditionally wrap with sandbox (AWF only) | ||
|
|
@@ -262,7 +262,6 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st | |
| command = fmt.Sprintf(`set -o pipefail | ||
| touch %s | ||
| (umask 177 && touch %s) | ||
| COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)" | ||
| %s%s 2>&1 | tee %s`, AgentStepSummaryPath, logFile, mkdirCommands.String(), copilotCommand, logFile) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
buildPromptFileFallbackInstructionreturns a bare instruction string telling the agent to read from disk. However, if the prompt file path contains spaces or special characters, this instruction might not convey the path accurately. Consider quoting the path in the instruction string or documenting that prompt file paths must not contain spaces.