diff --git a/.github/workflows/daily-rig-decomposition-bench.lock.yml b/.github/workflows/daily-rig-decomposition-bench.lock.yml index f68c0d8..402db24 100644 --- a/.github/workflows/daily-rig-decomposition-bench.lock.yml +++ b/.github/workflows/daily-rig-decomposition-bench.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d34bf384104a4974f29e4cb25d5d547879e1a3711ca518f4e647741e7ce23507","body_hash":"7a75a58f8c2a385cf9e795a402950623a81d84ca2cdda7dec5ade50873c96ffd","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73","copilot-sdk":"1.0.7"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"71e29caa23e306fcfd04b7520c66c8729a333295ba1dde1140b695c739911bf2","body_hash":"7a75a58f8c2a385cf9e795a402950623a81d84ca2cdda7dec5ade50873c96ffd","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73","copilot-sdk":"1.0.7"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.83.1","version":"v0.83.1"}],"skills":["githubnext/rig/skills/rig@e7d6ad85cd93946a8c09ebbdc1280ca62abd9de5"],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} # This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -759,7 +759,7 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): - timeout-minutes: 45 + timeout-minutes: 55 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt @@ -810,7 +810,7 @@ jobs: GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} - GH_AW_TIMEOUT_MINUTES: 45 + GH_AW_TIMEOUT_MINUTES: 55 GH_AW_VERSION: v0.83.1 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true @@ -1217,7 +1217,7 @@ jobs: GH_AW_FAILURE_REPORT_AS_ISSUE: "true" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" - GH_AW_TIMEOUT_MINUTES: "45" + GH_AW_TIMEOUT_MINUTES: "55" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/daily-rig-decomposition-bench.md b/.github/workflows/daily-rig-decomposition-bench.md index 9717ec1..2cae1e6 100644 --- a/.github/workflows/daily-rig-decomposition-bench.md +++ b/.github/workflows/daily-rig-decomposition-bench.md @@ -20,7 +20,7 @@ engine: id: copilot copilot-sdk: true strict: true -timeout-minutes: 45 +timeout-minutes: 55 checkout: false skills: - githubnext/rig/skills/rig@e7d6ad85cd93946a8c09ebbdc1280ca62abd9de5 diff --git a/skills/rig/rig.ts b/skills/rig/rig.ts index 6c5b4b4..76567e0 100644 --- a/skills/rig/rig.ts +++ b/skills/rig/rig.ts @@ -498,6 +498,39 @@ function resolveDefaultCopilotUri(): string { return process.env["COPILOT_SDK_URI"] ?? "localhost:7777"; } +function resolveDefaultCopilotConnection(): NonNullable { + const connectionToken = process.env["COPILOT_CONNECTION_TOKEN"]; + return connectionToken + ? RuntimeConnection.forUri(resolveDefaultCopilotUri(), { connectionToken }) + : RuntimeConnection.forUri(resolveDefaultCopilotUri()); +} + +type CopilotMultiProvider = { + model: string; + providers: unknown[]; + models: unknown[]; +}; + +function resolveCopilotMultiProvider(): CopilotMultiProvider | undefined { + const raw = process.env["GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON"]; + if (!raw) { + return undefined; + } + try { + const value = JSON.parse(raw) as Partial; + return typeof value.model === "string" && Array.isArray(value.providers) && Array.isArray(value.models) + ? { model: value.model, providers: value.providers, models: value.models } + : undefined; + } catch { + return undefined; + } +} + +function copilotSendTimeout(): number { + const timeout = Number(process.env["COPILOT_SDK_SEND_TIMEOUT_MS"]); + return Number.isFinite(timeout) && timeout > 0 ? timeout : 24 * 60 * 60 * 1000; +} + type DefaultEngineKind = "copilot" | "anthropic" | "codex" | "gemini"; type DefaultEngineOptions = { @@ -570,16 +603,18 @@ function defaultAgentFactory(options: DefaultEngineOptions = {}): AgentFactory { export function copilotEngine(options: CopilotEngineOptions = {}): AgentFactory { const { server, connection, ...clientOptions } = options; + const multiProvider = resolveCopilotMultiProvider(); return async (agentOptions) => { debugCopilotCreate({ model: agentOptions.model, transport: connection ? "custom" : server ? "stdio" : "uri" }); const client = new CopilotClient({ ...clientOptions, - connection: connection ?? (server ? RuntimeConnection.forStdio() : RuntimeConnection.forUri(resolveDefaultCopilotUri())), + connection: connection ?? (server ? RuntimeConnection.forStdio() : resolveDefaultCopilotConnection()), }); const session = await client.createSession({ - model: agentOptions.model, + model: multiProvider?.model ?? agentOptions.model, streaming: false, onPermissionRequest: approveAll, + ...(multiProvider ? { providers: multiProvider.providers, models: multiProvider.models } as any : {}), ...(agentOptions.systemMessage !== undefined && { systemMessage: agentOptions.systemMessage as any }), ...(agentOptions.tools !== undefined && { tools: agentOptions.tools as any }), }); @@ -590,12 +625,18 @@ export function copilotEngine(options: CopilotEngineOptions = {}): AgentFactory return { async ask(prompt, askOptions = {}) { debugCopilotAsk({ prompt, structured: askOptions.outputSchema !== undefined }); - const response = await (session.sendAndWait as any)( - { - prompt, - ...(askOptions.signal ? { signal: askOptions.signal } : {}), - ...(askOptions.outputSchema !== undefined ? { outputSchema: askOptions.outputSchema } : {}), - }, + throwIfAborted(askOptions.signal); + const response = await abortable( + (session.sendAndWait as any)( + { + prompt, + ...(askOptions.signal ? { signal: askOptions.signal } : {}), + ...(askOptions.outputSchema !== undefined ? { outputSchema: askOptions.outputSchema } : {}), + }, + copilotSendTimeout(), + ), + askOptions.signal, + () => (session as any).abort?.(), ); const text = responseText(response); debugCopilotResponse({ response: text }); @@ -3283,6 +3324,24 @@ function throwIfAborted(signal?: AbortSignal): void { } } +function abortable(promise: Promise, signal?: AbortSignal, onAbort?: () => void): Promise { + throwIfAborted(signal); + if (!signal) { + return promise; + } + return new Promise((resolve, reject) => { + const abort = () => { + try { + onAbort?.(); + } finally { + reject(signal.reason ?? new DOMException("Aborted", "AbortError")); + } + }; + signal.addEventListener("abort", abort, { once: true }); + promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)); + }); +} + function timeoutSignal(parent?: AbortSignal, timeout?: number): AbortSignal | undefined { if (!timeout) { return parent; diff --git a/src/engines/copilot.test.ts b/src/engines/copilot.test.ts index f50e9a4..09f8474 100644 --- a/src/engines/copilot.test.ts +++ b/src/engines/copilot.test.ts @@ -33,6 +33,9 @@ beforeEach(() => { mocks.forStdio.mockImplementation(() => ({ kind: "stdio" })); mocks.copilotClientCtor.mockClear(); delete process.env["COPILOT_SDK_URI"]; + delete process.env["COPILOT_CONNECTION_TOKEN"]; + delete process.env["GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON"]; + delete process.env["COPILOT_SDK_SEND_TIMEOUT_MS"]; vi.restoreAllMocks(); }); @@ -58,6 +61,51 @@ it("uses COPILOT_SDK_URI when set", async () => { expect(mocks.copilotClientCtor).toHaveBeenCalledWith({ connection: { kind: "uri", url: "http://127.0.0.1:4141" } }); }); +it("uses agentic workflow SDK connection and provider settings", async () => { + process.env["COPILOT_CONNECTION_TOKEN"] = "connection-token"; + process.env["GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON"] = JSON.stringify({ + model: "claude-sonnet-4.6", + providers: [{ name: "copilot" }], + models: [{ id: "claude-sonnet-4.6" }], + }); + + await copilotEngine()({ model: "small" }); + + expect(mocks.forUri).toHaveBeenCalledWith("localhost:7777", { connectionToken: "connection-token" }); + expect(mocks.createSession).toHaveBeenCalledWith({ + model: "claude-sonnet-4.6", + streaming: false, + onPermissionRequest: mocks.approveAll, + providers: [{ name: "copilot" }], + models: [{ id: "claude-sonnet-4.6" }], + }); +}); + +it("uses the configured SDK send timeout", async () => { + process.env["COPILOT_SDK_SEND_TIMEOUT_MS"] = "120000"; + const sendAndWait = vi.fn().mockResolvedValue({ data: { content: "ok" } }); + mocks.createSession.mockResolvedValue({ sendAndWait, disconnect: vi.fn() }); + const implementation = await copilotEngine()({ model: "small" }); + + await implementation.ask("hello"); + + expect(sendAndWait).toHaveBeenCalledWith({ prompt: "hello" }, 120000); +}); + +it("aborts an in-flight SDK request when its signal aborts", async () => { + const abort = vi.fn(); + const sendAndWait = vi.fn(() => new Promise(() => {})); + mocks.createSession.mockResolvedValue({ sendAndWait, abort, disconnect: vi.fn() }); + const implementation = await copilotEngine()({ model: "small" }); + const controller = new AbortController(); + const request = implementation.ask("hello", { signal: controller.signal }); + + controller.abort(new Error("cancelled")); + + await expect(request).rejects.toThrow("cancelled"); + expect(abort).toHaveBeenCalledOnce(); +}); + it("preserves explicit client options", async () => { const connection = { kind: "uri", url: "127.0.0.1:8765" } as const;