diff --git a/extensions/copilot/src/attempt.ts b/extensions/copilot/src/attempt.ts index 91f8565c524..7db862dc890 100644 --- a/extensions/copilot/src/attempt.ts +++ b/extensions/copilot/src/attempt.ts @@ -31,10 +31,6 @@ import { } from "openclaw/plugin-sdk/agent-harness-runtime"; import { createCopilotByokAuth, resolveCopilotAuth } from "./auth-bridge.js"; import { createCopilotByokProxy } from "./byok-proxy.js"; -import { - createInfiniteSessionConfig, - type CopilotInfiniteSessionOptions, -} from "./compaction-bridge.js"; import { attachCopilotMirrorIdentity, dualWriteCopilotTranscriptBestEffort, @@ -104,7 +100,7 @@ type AttemptParamsLike = AgentHarnessAttemptParams & { cwd?: string; enableSessionTelemetry?: boolean; hooksConfig?: CopilotHooksConfig; - infiniteSessionConfig?: CopilotInfiniteSessionOptions; + infiniteSessionConfig?: SessionConfig["infiniteSessions"]; initialReplayState?: AgentHarnessAttemptParams["initialReplayState"] & { sdkSessionId?: string }; messages?: AgentMessage[]; model?: string | { api?: string; id?: string; input?: string[]; provider?: string }; @@ -1331,7 +1327,6 @@ function createSessionConfig( ): CopilotSessionConfig { const permissionPolicy = params.permissionPolicy ?? rejectAllPolicy; const hooks = createHooksBridge(params.hooksConfig, options.hooksBridgeOptions); - const infiniteSessions = createInfiniteSessionConfig(params.infiniteSessionConfig); return { model: sdkModelId, // Permission decisions for SDK built-in tool kinds (shell, write, @@ -1371,11 +1366,8 @@ function createSessionConfig( ...(typeof params.enableSessionTelemetry === "boolean" ? { enableSessionTelemetry: params.enableSessionTelemetry } : {}), - // Infinite sessions / background compaction: only attach when the - // host provided an InfiniteSessionConfig. SDK defaults - // (`enabled: true`, background 0.80, buffer 0.95) apply when - // omitted. See compaction-bridge.ts. - ...(infiniteSessions ? { infiniteSessions } : {}), + // The SDK owns defaulting and validation for this native config block. + ...(params.infiniteSessionConfig ? { infiniteSessions: params.infiniteSessionConfig } : {}), reasoningEffort: params.reasoningEffort, tools: sdkTools, // Restrict the SDK's tool catalog to the bridged tool names returned diff --git a/extensions/copilot/src/compaction-bridge.test.ts b/extensions/copilot/src/compaction-bridge.test.ts deleted file mode 100755 index d789f07f065..00000000000 --- a/extensions/copilot/src/compaction-bridge.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Copilot tests cover compaction bridge plugin behavior. -import { describe, expect, it } from "vitest"; -import { createInfiniteSessionConfig } from "./compaction-bridge.js"; - -describe("createInfiniteSessionConfig", () => { - it("returns undefined when no options provided", () => { - expect(createInfiniteSessionConfig()).toBeUndefined(); - expect(createInfiniteSessionConfig(undefined)).toBeUndefined(); - }); - - it("returns undefined when options is an empty object", () => { - expect(createInfiniteSessionConfig({})).toBeUndefined(); - }); - - it("preserves explicit enabled:false to disable infinite sessions", () => { - expect(createInfiniteSessionConfig({ enabled: false })).toEqual({ enabled: false }); - }); - - it("preserves explicit enabled:true", () => { - expect(createInfiniteSessionConfig({ enabled: true })).toEqual({ enabled: true }); - }); - - it("forwards threshold fields when set", () => { - expect( - createInfiniteSessionConfig({ - backgroundCompactionThreshold: 0.7, - bufferExhaustionThreshold: 0.9, - }), - ).toEqual({ - backgroundCompactionThreshold: 0.7, - bufferExhaustionThreshold: 0.9, - }); - }); - - it("combines enabled and thresholds", () => { - expect( - createInfiniteSessionConfig({ - enabled: true, - backgroundCompactionThreshold: 0.5, - bufferExhaustionThreshold: 0.85, - }), - ).toEqual({ - enabled: true, - backgroundCompactionThreshold: 0.5, - bufferExhaustionThreshold: 0.85, - }); - }); - - it("omits undefined fields without coercing them", () => { - const result = createInfiniteSessionConfig({ - enabled: undefined, - backgroundCompactionThreshold: 0.6, - bufferExhaustionThreshold: undefined, - }); - expect(result).toEqual({ backgroundCompactionThreshold: 0.6 }); - expect(result).not.toHaveProperty("enabled"); - expect(result).not.toHaveProperty("bufferExhaustionThreshold"); - }); -}); diff --git a/extensions/copilot/src/compaction-bridge.ts b/extensions/copilot/src/compaction-bridge.ts deleted file mode 100755 index 5e18d0ada62..00000000000 --- a/extensions/copilot/src/compaction-bridge.ts +++ /dev/null @@ -1,51 +0,0 @@ -// Copilot plugin module implements compaction bridge behavior. -import type { SessionConfig } from "@github/copilot-sdk"; - -// Compaction bridge for the GitHub Copilot agent runtime. -// -// Shapes `SessionConfig.infiniteSessions` from a typed options bag so -// attempt.ts can opt the SDK in to background auto-compaction at session -// creation. The SDK manages the actual compaction under the `infiniteSessions` -// config and the session-scoped history compaction RPC. -// -// Host back-pointers (NOT imported here to keep the package boundary -// clean): -// - `src/agents/pi-embedded-runner/compact.types.ts` — canonical -// `CompactEmbeddedPiSessionParams`. -// - `src/agents/pi-embedded-runner/types.ts` — canonical -// `EmbeddedPiCompactResult`. - -type SdkInfiniteSessionConfig = NonNullable; - -export interface CopilotInfiniteSessionOptions { - enabled?: boolean; - backgroundCompactionThreshold?: number; - bufferExhaustionThreshold?: number; -} - -/** - * Shape an `InfiniteSessionConfig` for `SessionConfig.infiniteSessions`. - * Returns `undefined` when no fields were supplied so callers can - * spread conditionally and let the SDK apply its own defaults - * (`enabled: true`, background 0.80, buffer 0.95). Any explicitly-set - * value (including `enabled: false` to disable infinite sessions) is - * preserved. - */ -export function createInfiniteSessionConfig( - options?: CopilotInfiniteSessionOptions, -): SdkInfiniteSessionConfig | undefined { - if (!options) { - return undefined; - } - const result: SdkInfiniteSessionConfig = {}; - if (options.enabled !== undefined) { - result.enabled = options.enabled; - } - if (options.backgroundCompactionThreshold !== undefined) { - result.backgroundCompactionThreshold = options.backgroundCompactionThreshold; - } - if (options.bufferExhaustionThreshold !== undefined) { - result.bufferExhaustionThreshold = options.bufferExhaustionThreshold; - } - return Object.keys(result).length > 0 ? result : undefined; -} diff --git a/extensions/copilot/src/tool-bridge.test.ts b/extensions/copilot/src/tool-bridge.test.ts index be7e123dec6..e44039ed53d 100644 --- a/extensions/copilot/src/tool-bridge.test.ts +++ b/extensions/copilot/src/tool-bridge.test.ts @@ -1475,15 +1475,6 @@ describe("convertOpenClawToolToSdkTool", () => { expect(getError(result as ToolResultObject)).toBe(error.message); }); - it("returns success with empty text when content is missing", async () => { - const sourceTool = makeTool({}, { details: null }); - const sdkTool = convertOpenClawToolToSdkTool(sourceTool, {}); - - const result = await runSdkTool(sdkTool, {}); - - expect(result).toEqual({ resultType: "success", textResultForLlm: "" }); - }); - it("converts single text content to an exact textResultForLlm", async () => { const onAgentToolResult = vi.fn(); const sourceResult = { @@ -1650,7 +1641,6 @@ describe("convertOpenClawToolToSdkTool", () => { expect(result).toEqual({ binaryResultsForLlm: [ { - base64Data: "base64-data", data: "base64-data", mimeType: "image/png", type: "image", @@ -1661,30 +1651,6 @@ describe("convertOpenClawToolToSdkTool", () => { }); }); - it("returns a failure result for unsupported content shapes", async () => { - const onAgentToolResult = vi.fn(); - const sourceResult = { - content: [{ type: "resource" }], - details: null, - }; - const sdkTool = convertOpenClawToolToSdkTool(makeTool({}, sourceResult), { onAgentToolResult }); - - const result = await runSdkTool(sdkTool, {}); - - expect(result).toMatchObject({ - resultType: "failure", - textResultForLlm: "[copilot-tool-bridge] unsupported AgentToolResult content shape: resource", - }); - expect(getError(result as ToolResultObject)).toBe( - "[copilot-tool-bridge] unsupported AgentToolResult content shape: resource", - ); - expect(onAgentToolResult).toHaveBeenCalledWith({ - toolName: "tool-a", - result: sourceResult, - isError: true, - }); - }); - it("returns a failure result when execute throws and preserves the error", async () => { const error = new Error("tool exploded"); const sourceTool = makeTool({ diff --git a/extensions/copilot/src/tool-bridge.ts b/extensions/copilot/src/tool-bridge.ts index 4f521b3045d..e80ec6b1f7b 100644 --- a/extensions/copilot/src/tool-bridge.ts +++ b/extensions/copilot/src/tool-bridge.ts @@ -1,5 +1,10 @@ // Copilot plugin module implements tool bridge behavior. -import type { Tool as SdkTool, ToolInvocation, ToolResultObject } from "@github/copilot-sdk"; +import { + convertMcpCallToolResult, + type Tool as SdkTool, + type ToolInvocation, + type ToolResultObject, +} from "@github/copilot-sdk"; import type { AnyAgentTool, EmbeddedRunAttemptParams, @@ -27,10 +32,6 @@ type CatalogExecuteParams = Parameters< NonNullable >[0]; -type AgentToolResultLike = { - content?: unknown; -}; - /** * Mutable holder populated by `attempt.ts` *after* `client.createSession()` * (or `client.resumeSession()`) succeeds, so that the tool bridge — which is @@ -552,7 +553,7 @@ export function convertOpenClawToolToSdkTool( ); } - let result: AgentToolResultLike; + let result: Awaited>; try { result = await sourceTool.execute( invocation.toolCallId, @@ -570,7 +571,9 @@ export function convertOpenClawToolToSdkTool( ); } - const sdkResult = agentToolResultToSdk(result); + // OpenClaw tools throw for execution failures. Error-shaped details remain + // lifecycle metadata; successful content uses the SDK's MCP converter. + const sdkResult = convertMcpCallToolResult({ content: result.content }); const sanitizedResult = sanitizeToolResult(result); const resultIsError = sdkResult.resultType === "failure" || isToolResultError(sanitizedResult); const resultError = resultIsError ? extractToolErrorMessage(sanitizedResult) : undefined; @@ -694,72 +697,6 @@ function toToolStartArgs(args: unknown): Record { : { value: args }; } -function agentToolResultToSdk(result: AgentToolResultLike | undefined): ToolResultObject { - const content = result?.content; - if (content == null) { - return createSuccessResult(""); - } - - if (!Array.isArray(content)) { - return createUnsupportedContentFailure(typeof content); - } - - const textParts: string[] = []; - const binaryResults: Array> = []; - for (const block of content) { - if (!block || typeof block !== "object") { - return createUnsupportedContentFailure(typeof block); - } - - const kind = readString((block as { type?: unknown }).type); - if (kind === "text") { - const text = readString((block as { text?: unknown }).text, { allowEmpty: true }); - if (text === undefined) { - return createUnsupportedContentFailure(kind); - } - textParts.push(text); - continue; - } - - if (kind === "image") { - const base64Data = readString((block as { data?: unknown }).data); - const mimeType = readString((block as { mimeType?: unknown }).mimeType); - if (!base64Data || !mimeType) { - return createUnsupportedContentFailure(kind); - } - binaryResults.push({ - base64Data, - data: base64Data, - mimeType, - type: "image", - }); - continue; - } - - return createUnsupportedContentFailure(kind ?? typeof block); - } - - return { - ...(binaryResults.length > 0 - ? { binaryResultsForLlm: binaryResults as ToolResultObject["binaryResultsForLlm"] } - : {}), - resultType: "success", - textResultForLlm: textParts.join("\n"), - }; -} - -function createUnsupportedContentFailure(kind: string): ToolResultObject { - const message = `[copilot-tool-bridge] unsupported AgentToolResult content shape: ${kind}`; - return createFailureResult(message, new Error(message)); -} - -function createSuccessResult(textResultForLlm: string): ToolResultObject { - return { - resultType: "success", - textResultForLlm, - }; -} - function createFailureResult(message: string, error: unknown): ToolResultObject { // ToolResultObject.error is typed as `string | undefined` in the SDK contract // (see `node_modules/@github/copilot-sdk/dist/types.d.ts`). Returning an @@ -871,16 +808,6 @@ function findDuplicateToolNames(sourceTools: AnyAgentTool[]): string[] { .toSorted(); } -function readString(value: unknown, options: { allowEmpty?: boolean } = {}): string | undefined { - if (typeof value !== "string") { - return undefined; - } - if (options.allowEmpty || value.length > 0) { - return value; - } - return undefined; -} - function toError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)); }