mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-10 17:16:55 +00:00
refactor(copilot): use SDK contracts (#106208)
This commit is contained in:
committed by
GitHub
parent
bd9bedb201
commit
fa963eee06
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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<SessionConfig["infiniteSessions"]>;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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<AgentHarnessToolSurfaceRuntime["toolSearchCatalogExecutor"]>
|
||||
>[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<ReturnType<AnyAgentTool["execute"]>>;
|
||||
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<string, unknown> {
|
||||
: { 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<Record<string, string>> = [];
|
||||
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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user