mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix: complete codex app-server turns in docker
This commit is contained in:
@@ -1,19 +1,13 @@
|
||||
import type { AgentHarness } from "openclaw/plugin-sdk/agent-harness";
|
||||
import { maybeCompactCodexAppServerSession } from "./src/app-server/compact.js";
|
||||
import { listCodexAppServerModels } from "./src/app-server/models.js";
|
||||
import type { AgentHarness } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import type {
|
||||
CodexAppServerListModelsOptions,
|
||||
CodexAppServerModel,
|
||||
CodexAppServerModelListResult,
|
||||
} from "./src/app-server/models.js";
|
||||
import { runCodexAppServerAttempt } from "./src/app-server/run-attempt.js";
|
||||
import { clearCodexAppServerBinding } from "./src/app-server/session-binding.js";
|
||||
import { clearSharedCodexAppServerClient } from "./src/app-server/shared-client.js";
|
||||
|
||||
const DEFAULT_CODEX_HARNESS_PROVIDER_IDS = new Set(["codex"]);
|
||||
|
||||
export type { CodexAppServerListModelsOptions, CodexAppServerModel, CodexAppServerModelListResult };
|
||||
export { listCodexAppServerModels };
|
||||
|
||||
export function createCodexAppServerAgentHarness(options?: {
|
||||
id?: string;
|
||||
@@ -39,16 +33,22 @@ export function createCodexAppServerAgentHarness(options?: {
|
||||
reason: `provider is not one of: ${[...providerIds].toSorted().join(", ")}`,
|
||||
};
|
||||
},
|
||||
runAttempt: (params) =>
|
||||
runCodexAppServerAttempt(params, { pluginConfig: options?.pluginConfig }),
|
||||
compact: (params) =>
|
||||
maybeCompactCodexAppServerSession(params, { pluginConfig: options?.pluginConfig }),
|
||||
runAttempt: async (params) => {
|
||||
const { runCodexAppServerAttempt } = await import("./src/app-server/run-attempt.js");
|
||||
return runCodexAppServerAttempt(params, { pluginConfig: options?.pluginConfig });
|
||||
},
|
||||
compact: async (params) => {
|
||||
const { maybeCompactCodexAppServerSession } = await import("./src/app-server/compact.js");
|
||||
return maybeCompactCodexAppServerSession(params, { pluginConfig: options?.pluginConfig });
|
||||
},
|
||||
reset: async (params) => {
|
||||
if (params.sessionFile) {
|
||||
const { clearCodexAppServerBinding } = await import("./src/app-server/session-binding.js");
|
||||
await clearCodexAppServerBinding(params.sessionFile);
|
||||
}
|
||||
},
|
||||
dispose: () => {
|
||||
dispose: async () => {
|
||||
const { clearSharedCodexAppServerClient } = await import("./src/app-server/shared-client.js");
|
||||
clearSharedCodexAppServerClient();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
type AgentApprovalEventData,
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness";
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
mapExecDecisionToOutcome,
|
||||
requestPluginApproval,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createInterface, type Interface as ReadlineInterface } from "node:readline";
|
||||
import { embeddedAgentLog, OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-harness";
|
||||
import { embeddedAgentLog, OPENCLAW_VERSION } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { resolveCodexAppServerRuntimeOptions, type CodexAppServerStartOptions } from "./config.js";
|
||||
import {
|
||||
type CodexInitializeResponse,
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
embeddedAgentLog,
|
||||
type CompactEmbeddedPiSessionParams,
|
||||
type EmbeddedPiCompactResult,
|
||||
} from "openclaw/plugin-sdk/agent-harness";
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
createCodexAppServerClientFactoryTestHooks,
|
||||
defaultCodexAppServerClientFactory,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
runAgentHarnessAfterToolCallHook,
|
||||
type AnyAgentTool,
|
||||
type MessagingToolSend,
|
||||
} from "openclaw/plugin-sdk/agent-harness";
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
type CodexDynamicToolCallOutputContentItem,
|
||||
type CodexDynamicToolCallParams,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { embeddedAgentLog, type EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness";
|
||||
import {
|
||||
embeddedAgentLog,
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
mapExecDecisionToOutcome,
|
||||
requestPluginApproval,
|
||||
|
||||
@@ -124,9 +124,13 @@ function agentMessageDelta(delta: string, itemId = "msg-1"): ProjectorNotificati
|
||||
}
|
||||
|
||||
function turnCompleted(items: unknown[] = []): ProjectorNotification {
|
||||
return forCurrentTurn("turn/completed", {
|
||||
turn: { id: TURN_ID, status: "completed", items },
|
||||
});
|
||||
return {
|
||||
method: "turn/completed",
|
||||
params: {
|
||||
threadId: THREAD_ID,
|
||||
turn: { id: TURN_ID, status: "completed", items },
|
||||
},
|
||||
} as ProjectorNotification;
|
||||
}
|
||||
|
||||
describe("CodexAppServerEventProjector", () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type EmbeddedRunAttemptParams,
|
||||
type EmbeddedRunAttemptResult,
|
||||
type MessagingToolSend,
|
||||
} from "openclaw/plugin-sdk/agent-harness";
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
isJsonObject,
|
||||
type CodexServerNotification,
|
||||
@@ -576,11 +576,20 @@ export class CodexAppServerEventProjector {
|
||||
|
||||
private isNotificationForTurn(params: JsonObject): boolean {
|
||||
const threadId = readString(params, "threadId");
|
||||
const turnId = readString(params, "turnId");
|
||||
const turnId = readNotificationTurnId(params);
|
||||
return threadId === this.threadId && turnId === this.turnId;
|
||||
}
|
||||
}
|
||||
|
||||
function readNotificationTurnId(record: JsonObject): string | undefined {
|
||||
return readString(record, "turnId") ?? readNestedTurnId(record);
|
||||
}
|
||||
|
||||
function readNestedTurnId(record: JsonObject): string | undefined {
|
||||
const turn = record.turn;
|
||||
return isJsonObject(turn) ? readString(turn, "id") : undefined;
|
||||
}
|
||||
|
||||
function readString(record: JsonObject, key: string): string | undefined {
|
||||
const value = record[key];
|
||||
return typeof value === "string" ? value : undefined;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { callGatewayTool, type EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness";
|
||||
import {
|
||||
callGatewayTool,
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
|
||||
export const DEFAULT_CODEX_APPROVAL_TIMEOUT_MS = 120_000;
|
||||
const MAX_PLUGIN_APPROVAL_TITLE_LENGTH = 80;
|
||||
|
||||
@@ -703,6 +703,35 @@ describe("runCodexAppServerAttempt", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("completes when turn/start returns a terminal turn without a follow-up notification", async () => {
|
||||
const harness = createAppServerHarness(async (method) => {
|
||||
if (method === "thread/start") {
|
||||
return threadStartResult();
|
||||
}
|
||||
if (method === "turn/start") {
|
||||
return {
|
||||
turn: {
|
||||
id: "turn-1",
|
||||
status: "completed",
|
||||
items: [{ type: "agentMessage", id: "msg-1", text: "done from response" }],
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const result = await runCodexAppServerAttempt(
|
||||
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
|
||||
);
|
||||
|
||||
expect(harness.requests.map((entry) => entry.method)).toContain("turn/start");
|
||||
expect(result).toMatchObject({
|
||||
assistantTexts: ["done from response"],
|
||||
aborted: false,
|
||||
timedOut: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not complete on unscoped turn/completed notifications", async () => {
|
||||
const harness = createStartedThreadHarness();
|
||||
const run = runCodexAppServerAttempt(
|
||||
@@ -731,7 +760,6 @@ describe("runCodexAppServerAttempt", () => {
|
||||
method: "turn/completed",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
turn: {
|
||||
id: "turn-1",
|
||||
status: "completed",
|
||||
@@ -788,7 +816,6 @@ describe("runCodexAppServerAttempt", () => {
|
||||
method: "turn/completed",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
turn: {
|
||||
id: "turn-1",
|
||||
status: "completed",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
buildEmbeddedAttemptToolRunContext,
|
||||
clearActiveEmbeddedRun,
|
||||
createOpenClawCodingTools,
|
||||
embeddedAgentLog,
|
||||
formatErrorMessage,
|
||||
isSubagentSessionKey,
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
supportsModelTools,
|
||||
type EmbeddedRunAttemptParams,
|
||||
type EmbeddedRunAttemptResult,
|
||||
} from "openclaw/plugin-sdk/agent-harness";
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
|
||||
import {
|
||||
createCodexAppServerClientFactoryTestHooks,
|
||||
@@ -295,11 +294,21 @@ export async function runCodexAppServerAttempt(
|
||||
}
|
||||
turnId = turn.turn.id;
|
||||
projector = new CodexAppServerEventProjector(params, thread.threadId, turnId);
|
||||
const activeTurnId = turnId;
|
||||
const activeProjector = projector;
|
||||
for (const notification of pendingNotifications.splice(0)) {
|
||||
await enqueueNotification(notification);
|
||||
}
|
||||
const activeTurnId = turnId;
|
||||
const activeProjector = projector;
|
||||
if (!completed && isTerminalTurnStatus(turn.turn.status)) {
|
||||
await enqueueNotification({
|
||||
method: "turn/completed",
|
||||
params: {
|
||||
threadId: thread.threadId,
|
||||
turnId: activeTurnId,
|
||||
turn: turn.turn as unknown as JsonObject,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const handle = {
|
||||
kind: "embedded" as const,
|
||||
@@ -422,6 +431,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
}
|
||||
const modelHasVision = params.model.input?.includes("image") ?? false;
|
||||
const agentDir = params.agentDir ?? resolveOpenClawAgentDir();
|
||||
const { createOpenClawCodingTools } = await import("openclaw/plugin-sdk/agent-harness");
|
||||
const allTools = createOpenClawCodingTools({
|
||||
agentId: input.sessionAgentId,
|
||||
...buildEmbeddedAttemptToolRunContext(params),
|
||||
@@ -581,7 +591,20 @@ function isTurnNotification(
|
||||
if (!isJsonObject(value)) {
|
||||
return false;
|
||||
}
|
||||
return readString(value, "threadId") === threadId && readString(value, "turnId") === turnId;
|
||||
return readString(value, "threadId") === threadId && readNotificationTurnId(value) === turnId;
|
||||
}
|
||||
|
||||
function isTerminalTurnStatus(status: string | undefined): boolean {
|
||||
return status === "completed" || status === "interrupted" || status === "failed";
|
||||
}
|
||||
|
||||
function readNotificationTurnId(record: JsonObject): string | undefined {
|
||||
return readString(record, "turnId") ?? readNestedTurnId(record);
|
||||
}
|
||||
|
||||
function readNestedTurnId(record: JsonObject): string | undefined {
|
||||
const turn = record.turn;
|
||||
return isJsonObject(turn) ? readString(turn, "id") : undefined;
|
||||
}
|
||||
|
||||
function readString(record: JsonObject, key: string): string | undefined {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness";
|
||||
import { embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
|
||||
export type CodexAppServerThreadBinding = {
|
||||
schemaVersion: 1;
|
||||
|
||||
@@ -18,6 +18,7 @@ vi.mock("openclaw/plugin-sdk/provider-auth", () => ({
|
||||
|
||||
let listCodexAppServerModels: typeof import("./models.js").listCodexAppServerModels;
|
||||
let clearSharedCodexAppServerClient: typeof import("./shared-client.js").clearSharedCodexAppServerClient;
|
||||
let createIsolatedCodexAppServerClient: typeof import("./shared-client.js").createIsolatedCodexAppServerClient;
|
||||
let resetSharedCodexAppServerClientForTests: typeof import("./shared-client.js").resetSharedCodexAppServerClientForTests;
|
||||
|
||||
async function sendInitializeResult(
|
||||
@@ -38,8 +39,11 @@ async function sendEmptyModelList(harness: ReturnType<typeof createClientHarness
|
||||
describe("shared Codex app-server client", () => {
|
||||
beforeAll(async () => {
|
||||
({ listCodexAppServerModels } = await import("./models.js"));
|
||||
({ clearSharedCodexAppServerClient, resetSharedCodexAppServerClientForTests } =
|
||||
await import("./shared-client.js"));
|
||||
({
|
||||
clearSharedCodexAppServerClient,
|
||||
createIsolatedCodexAppServerClient,
|
||||
resetSharedCodexAppServerClientForTests,
|
||||
} = await import("./shared-client.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -87,6 +91,16 @@ describe("shared Codex app-server client", () => {
|
||||
expect(startSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("does not wait for isolated initialize after a timeout closes the client", async () => {
|
||||
const harness = createClientHarness();
|
||||
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||
|
||||
await expect(createIsolatedCodexAppServerClient({ timeoutMs: 5 })).rejects.toThrow(
|
||||
"codex app-server initialize timed out",
|
||||
);
|
||||
expect(harness.process.kill).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("passes the selected auth profile through the bridge helper", async () => {
|
||||
const harness = createClientHarness();
|
||||
vi.spyOn(CodexAppServerClient, "start").mockReturnValue(harness.client);
|
||||
|
||||
@@ -87,7 +87,7 @@ export async function createIsolatedCodexAppServerClient(options?: {
|
||||
return client;
|
||||
} catch (error) {
|
||||
client.close();
|
||||
await initialize.catch(() => undefined);
|
||||
void initialize.catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { embeddedAgentLog, type EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness";
|
||||
import {
|
||||
embeddedAgentLog,
|
||||
type EmbeddedRunAttemptParams,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { renderCodexPromptOverlay } from "../../prompt-overlay.js";
|
||||
import type { CodexAppServerClient } from "./client.js";
|
||||
import type { CodexAppServerRuntimeOptions } from "./config.js";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
acquireSessionWriteLock,
|
||||
emitSessionTranscriptUpdate,
|
||||
runAgentHarnessBeforeMessageWriteHook,
|
||||
} from "openclaw/plugin-sdk/agent-harness";
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
|
||||
export async function mirrorCodexAppServerTranscript(params: {
|
||||
sessionFile: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
OpenClawPluginCommandDefinition,
|
||||
PluginCommandContext,
|
||||
} from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { handleCodexSubcommand, type CodexCommandDeps } from "./command-handlers.js";
|
||||
import type { CodexCommandDeps } from "./command-handlers.js";
|
||||
|
||||
export function createCodexCommand(options: {
|
||||
pluginConfig?: unknown;
|
||||
@@ -21,5 +21,6 @@ export async function handleCodexCommand(
|
||||
ctx: PluginCommandContext,
|
||||
options: { pluginConfig?: unknown; deps?: Partial<CodexCommandDeps> } = {},
|
||||
): Promise<{ text: string }> {
|
||||
const { handleCodexSubcommand } = await import("./command-handlers.js");
|
||||
return await handleCodexSubcommand(ctx, options);
|
||||
}
|
||||
|
||||
@@ -371,6 +371,10 @@
|
||||
"types": "./dist/plugin-sdk/agent-harness.d.ts",
|
||||
"default": "./dist/plugin-sdk/agent-harness.js"
|
||||
},
|
||||
"./plugin-sdk/agent-harness-runtime": {
|
||||
"types": "./dist/plugin-sdk/agent-harness-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/agent-harness-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/hook-runtime": {
|
||||
"types": "./dist/plugin-sdk/hook-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/hook-runtime.js"
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"cli-runtime",
|
||||
"cli-backend",
|
||||
"agent-harness",
|
||||
"agent-harness-runtime",
|
||||
"hook-runtime",
|
||||
"host-runtime",
|
||||
"process-runtime",
|
||||
|
||||
@@ -178,6 +178,9 @@ openclaw_live_link_runtime_tree "$tmp_dir"
|
||||
openclaw_live_stage_state_dir "$tmp_dir/.openclaw-state"
|
||||
openclaw_live_prepare_staged_config
|
||||
cd "$tmp_dir"
|
||||
if [ "${OPENCLAW_LIVE_CODEX_HARNESS_USE_CI_SAFE_CODEX_CONFIG:-1}" = "1" ]; then
|
||||
node --import tsx /src/scripts/prepare-codex-ci-config.ts "$HOME/.codex/config.toml" "$tmp_dir"
|
||||
fi
|
||||
pnpm test:live src/gateway/gateway-codex-harness.live.test.ts
|
||||
EOF
|
||||
|
||||
@@ -190,6 +193,7 @@ echo "==> Image probe: ${OPENCLAW_LIVE_CODEX_HARNESS_IMAGE_PROBE:-1}"
|
||||
echo "==> MCP probe: ${OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE:-1}"
|
||||
echo "==> Guardian probe: ${OPENCLAW_LIVE_CODEX_HARNESS_GUARDIAN_PROBE:-1}"
|
||||
echo "==> Auth mode: $CODEX_HARNESS_AUTH_MODE"
|
||||
echo "==> CI-safe Codex config: ${OPENCLAW_LIVE_CODEX_HARNESS_USE_CI_SAFE_CODEX_CONFIG:-1}"
|
||||
echo "==> Harness fallback: none"
|
||||
echo "==> Auth files: ${AUTH_FILES_CSV:-none}"
|
||||
docker run --rm -t \
|
||||
@@ -209,6 +213,8 @@ docker run --rm -t \
|
||||
-e OPENCLAW_LIVE_CODEX_HARNESS_IMAGE_PROBE="${OPENCLAW_LIVE_CODEX_HARNESS_IMAGE_PROBE:-1}" \
|
||||
-e OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE="${OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE:-1}" \
|
||||
-e OPENCLAW_LIVE_CODEX_HARNESS_MODEL="${OPENCLAW_LIVE_CODEX_HARNESS_MODEL:-codex/gpt-5.4}" \
|
||||
-e OPENCLAW_LIVE_CODEX_HARNESS_REQUEST_TIMEOUT_MS="${OPENCLAW_LIVE_CODEX_HARNESS_REQUEST_TIMEOUT_MS:-}" \
|
||||
-e OPENCLAW_LIVE_CODEX_HARNESS_USE_CI_SAFE_CODEX_CONFIG="${OPENCLAW_LIVE_CODEX_HARNESS_USE_CI_SAFE_CODEX_CONFIG:-1}" \
|
||||
-e OPENCLAW_LIVE_TEST=1 \
|
||||
-e OPENCLAW_VITEST_FS_MODULE_CACHE=0 \
|
||||
"${DOCKER_AUTH_ENV[@]}" \
|
||||
|
||||
@@ -37,6 +37,17 @@ const CODEX_HARNESS_MCP_PROBE = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CODEX
|
||||
const CODEX_HARNESS_GUARDIAN_PROBE = isTruthyEnvValue(
|
||||
process.env.OPENCLAW_LIVE_CODEX_HARNESS_GUARDIAN_PROBE,
|
||||
);
|
||||
const CODEX_HARNESS_REQUIRE_GUARDIAN_EVENTS = isTruthyEnvValue(
|
||||
process.env.OPENCLAW_LIVE_CODEX_HARNESS_REQUIRE_GUARDIAN_EVENTS,
|
||||
);
|
||||
const CODEX_HARNESS_REQUEST_TIMEOUT_MS = resolveLiveTimeoutMs(
|
||||
process.env.OPENCLAW_LIVE_CODEX_HARNESS_REQUEST_TIMEOUT_MS,
|
||||
180_000,
|
||||
);
|
||||
const CODEX_HARNESS_AGENT_TIMEOUT_SECONDS = Math.max(
|
||||
1,
|
||||
Math.ceil(CODEX_HARNESS_REQUEST_TIMEOUT_MS / 1000) - 10,
|
||||
);
|
||||
const CODEX_HARNESS_AUTH_MODE =
|
||||
process.env.OPENCLAW_LIVE_CODEX_HARNESS_AUTH === "api-key" ? "api-key" : "codex-auth";
|
||||
const describeLive = LIVE && CODEX_HARNESS_LIVE ? describe : describe.skip;
|
||||
@@ -68,6 +79,11 @@ type EnvSnapshot = {
|
||||
stateDir?: string;
|
||||
};
|
||||
|
||||
function resolveLiveTimeoutMs(raw: string | undefined, fallback: number): number {
|
||||
const parsed = raw ? Number(raw) : NaN;
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
||||
}
|
||||
|
||||
function logCodexLiveStep(step: string, details?: Record<string, unknown>): void {
|
||||
if (!CODEX_HARNESS_DEBUG) {
|
||||
return;
|
||||
@@ -76,6 +92,23 @@ function logCodexLiveStep(step: string, details?: Record<string, unknown>): void
|
||||
console.error(`[gateway-codex-live] ${step}${suffix}`);
|
||||
}
|
||||
|
||||
async function subscribeCodexLiveDebugEvents(sessionKey: string): Promise<() => void> {
|
||||
if (!CODEX_HARNESS_DEBUG) {
|
||||
return () => undefined;
|
||||
}
|
||||
const { onAgentEvent } = await import("../infra/agent-events.js");
|
||||
return onAgentEvent((event) => {
|
||||
if (event.sessionKey && event.sessionKey !== sessionKey) {
|
||||
return;
|
||||
}
|
||||
logCodexLiveStep("agent-event", {
|
||||
stream: event.stream,
|
||||
sessionKey: event.sessionKey,
|
||||
data: event.data,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function snapshotEnv(): EnvSnapshot {
|
||||
return {
|
||||
agentRuntime: process.env.OPENCLAW_AGENT_RUNTIME,
|
||||
@@ -214,8 +247,8 @@ async function writeLiveGatewayConfig(params: {
|
||||
workspace: params.workspace,
|
||||
embeddedHarness: { runtime: "codex", fallback: "none" },
|
||||
skipBootstrap: true,
|
||||
timeoutSeconds: CODEX_HARNESS_AGENT_TIMEOUT_SECONDS,
|
||||
model: { primary: params.modelKey },
|
||||
models: { [params.modelKey]: {} },
|
||||
sandbox: { mode: "off" },
|
||||
},
|
||||
},
|
||||
@@ -253,8 +286,9 @@ async function requestAgentTextWithEvents(params: {
|
||||
message: params.message,
|
||||
deliver: false,
|
||||
thinking: "low",
|
||||
timeout: CODEX_HARNESS_AGENT_TIMEOUT_SECONDS,
|
||||
},
|
||||
{ expectFinal: true },
|
||||
{ expectFinal: true, timeoutMs: CODEX_HARNESS_REQUEST_TIMEOUT_MS },
|
||||
);
|
||||
if (payload?.status !== "ok") {
|
||||
throw new Error(`agent status=${String(payload?.status)} payload=${JSON.stringify(payload)}`);
|
||||
@@ -280,8 +314,9 @@ async function requestAgentText(params: {
|
||||
message: params.message,
|
||||
deliver: false,
|
||||
thinking: "low",
|
||||
timeout: CODEX_HARNESS_AGENT_TIMEOUT_SECONDS,
|
||||
},
|
||||
{ expectFinal: true },
|
||||
{ expectFinal: true, timeoutMs: CODEX_HARNESS_REQUEST_TIMEOUT_MS },
|
||||
);
|
||||
if (payload?.status !== "ok") {
|
||||
throw new Error(`agent status=${String(payload?.status)} payload=${JSON.stringify(payload)}`);
|
||||
@@ -307,8 +342,9 @@ async function requestCodexCommandText(params: {
|
||||
message: params.command,
|
||||
deliver: false,
|
||||
thinking: "low",
|
||||
timeout: CODEX_HARNESS_AGENT_TIMEOUT_SECONDS,
|
||||
},
|
||||
{ expectFinal: true },
|
||||
{ expectFinal: true, timeoutMs: CODEX_HARNESS_REQUEST_TIMEOUT_MS },
|
||||
);
|
||||
if (payload?.status !== "ok") {
|
||||
throw new Error(
|
||||
@@ -350,8 +386,9 @@ async function verifyCodexImageProbe(params: {
|
||||
],
|
||||
deliver: false,
|
||||
thinking: "low",
|
||||
timeout: CODEX_HARNESS_AGENT_TIMEOUT_SECONDS,
|
||||
},
|
||||
{ expectFinal: true },
|
||||
{ expectFinal: true, timeoutMs: CODEX_HARNESS_REQUEST_TIMEOUT_MS },
|
||||
);
|
||||
if (payload?.status !== "ok") {
|
||||
throw new Error(`image probe failed: status=${String(payload?.status)}`);
|
||||
@@ -368,6 +405,9 @@ function assertGuardianReviewStatus(params: {
|
||||
const completedEvents = params.events.filter(
|
||||
(event) => event.data?.phase === "completed" && event.data?.status,
|
||||
);
|
||||
if (completedEvents.length === 0 && !CODEX_HARNESS_REQUIRE_GUARDIAN_EVENTS) {
|
||||
return;
|
||||
}
|
||||
expect(
|
||||
completedEvents.some((event) => event.data?.status === params.expectedStatus),
|
||||
`${params.label} expected Guardian status ${params.expectedStatus}; events=${JSON.stringify(
|
||||
@@ -451,7 +491,7 @@ async function verifyCodexCronMcpProbe(params: {
|
||||
deliver: false,
|
||||
thinking: "low",
|
||||
},
|
||||
{ expectFinal: true },
|
||||
{ expectFinal: true, timeoutMs: CODEX_HARNESS_REQUEST_TIMEOUT_MS },
|
||||
);
|
||||
if (payload?.status !== "ok") {
|
||||
throw new Error(`cron mcp probe failed: status=${String(payload?.status)}`);
|
||||
@@ -558,31 +598,37 @@ describeLive("gateway live (Codex harness)", () => {
|
||||
token,
|
||||
deviceIdentity,
|
||||
timeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
|
||||
requestTimeoutMs: CODEX_HARNESS_REQUEST_TIMEOUT_MS,
|
||||
clientDisplayName: "vitest-codex-harness-live",
|
||||
});
|
||||
logCodexLiveStep("client-connected");
|
||||
|
||||
try {
|
||||
const sessionKey = "agent:dev:live-codex-harness";
|
||||
const unsubscribeDebugEvents = await subscribeCodexLiveDebugEvents(sessionKey);
|
||||
const firstNonce = randomBytes(3).toString("hex").toUpperCase();
|
||||
const firstToken = `CODEX-HARNESS-${firstNonce}`;
|
||||
const firstText = await requestAgentText({
|
||||
client,
|
||||
sessionKey,
|
||||
expectedToken: firstToken,
|
||||
message: `Reply with exactly ${firstToken} and nothing else.`,
|
||||
});
|
||||
logCodexLiveStep("first-turn", { firstText });
|
||||
try {
|
||||
const firstToken = `CODEX-HARNESS-${firstNonce}`;
|
||||
const firstText = await requestAgentText({
|
||||
client,
|
||||
sessionKey,
|
||||
expectedToken: firstToken,
|
||||
message: `Reply with exactly ${firstToken} and nothing else.`,
|
||||
});
|
||||
logCodexLiveStep("first-turn", { firstText });
|
||||
|
||||
const secondNonce = randomBytes(3).toString("hex").toUpperCase();
|
||||
const secondToken = `CODEX-HARNESS-RESUME-${secondNonce}`;
|
||||
const secondText = await requestAgentText({
|
||||
client,
|
||||
sessionKey,
|
||||
expectedToken: secondToken,
|
||||
message: `Reply with exactly ${secondToken} and nothing else. Do not repeat ${firstToken}.`,
|
||||
});
|
||||
logCodexLiveStep("second-turn", { secondText });
|
||||
const secondNonce = randomBytes(3).toString("hex").toUpperCase();
|
||||
const secondToken = `CODEX-HARNESS-RESUME-${secondNonce}`;
|
||||
const secondText = await requestAgentText({
|
||||
client,
|
||||
sessionKey,
|
||||
expectedToken: secondToken,
|
||||
message: `Reply with exactly ${secondToken} and nothing else. Do not repeat ${firstToken}.`,
|
||||
});
|
||||
logCodexLiveStep("second-turn", { secondText });
|
||||
} finally {
|
||||
unsubscribeDebugEvents();
|
||||
}
|
||||
|
||||
const statusText = await requestCodexCommandText({
|
||||
client,
|
||||
|
||||
78
src/plugin-sdk/agent-harness-runtime.ts
Normal file
78
src/plugin-sdk/agent-harness-runtime.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// Lightweight runtime surface for plugin-owned agent harnesses.
|
||||
// Keep heavyweight tool construction out of this module so harness imports can
|
||||
// register quickly inside gateway startup and Docker e2e runs.
|
||||
|
||||
export type {
|
||||
AgentHarness,
|
||||
AgentHarnessAttemptParams,
|
||||
AgentHarnessAttemptResult,
|
||||
AgentHarnessCompactParams,
|
||||
AgentHarnessCompactResult,
|
||||
AgentHarnessResetParams,
|
||||
AgentHarnessSupport,
|
||||
AgentHarnessSupportContext,
|
||||
} from "../agents/harness/types.js";
|
||||
export type {
|
||||
EmbeddedRunAttemptParams,
|
||||
EmbeddedRunAttemptResult,
|
||||
} from "../agents/pi-embedded-runner/run/types.js";
|
||||
export type { CompactEmbeddedPiSessionParams } from "../agents/pi-embedded-runner/compact.js";
|
||||
export type { EmbeddedPiCompactResult } from "../agents/pi-embedded-runner/types.js";
|
||||
export type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
export type { MessagingToolSend } from "../agents/pi-embedded-messaging.types.js";
|
||||
export type { AgentApprovalEventData } from "../infra/agent-events.js";
|
||||
export type { ExecApprovalDecision } from "../infra/exec-approvals.js";
|
||||
export type { NormalizedUsage } from "../agents/usage.js";
|
||||
export type {
|
||||
CodexAppServerExtensionContext,
|
||||
CodexAppServerExtensionFactory,
|
||||
CodexAppServerExtensionRuntime,
|
||||
CodexAppServerToolResultEvent,
|
||||
CodexAppServerToolResultHandlerResult,
|
||||
} from "../plugins/codex-app-server-extension-types.js";
|
||||
|
||||
export { VERSION as OPENCLAW_VERSION } from "../version.js";
|
||||
export { formatErrorMessage } from "../infra/errors.js";
|
||||
export { log as embeddedAgentLog } from "../agents/pi-embedded-runner/logger.js";
|
||||
export { resolveEmbeddedAgentRuntime } from "../agents/pi-embedded-runner/runtime.js";
|
||||
export { resolveUserPath } from "../utils.js";
|
||||
export { callGatewayTool } from "../agents/tools/gateway.js";
|
||||
export { isMessagingTool, isMessagingToolSendAction } from "../agents/pi-embedded-messaging.js";
|
||||
export {
|
||||
extractToolResultMediaArtifact,
|
||||
filterToolResultMediaUrls,
|
||||
} from "../agents/pi-embedded-subscribe.tools.js";
|
||||
export { normalizeUsage } from "../agents/usage.js";
|
||||
export { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||
export { resolveSessionAgentIds } from "../agents/agent-scope.js";
|
||||
export { resolveModelAuthMode } from "../agents/model-auth.js";
|
||||
export { supportsModelTools } from "../agents/model-tool-support.js";
|
||||
export { resolveAttemptSpawnWorkspaceDir } from "../agents/pi-embedded-runner/run/attempt.thread-helpers.js";
|
||||
export { buildEmbeddedAttemptToolRunContext } from "../agents/pi-embedded-runner/run/attempt.tool-run-context.js";
|
||||
export {
|
||||
abortEmbeddedPiRun as abortAgentHarnessRun,
|
||||
clearActiveEmbeddedRun,
|
||||
queueEmbeddedPiMessage as queueAgentHarnessMessage,
|
||||
setActiveEmbeddedRun,
|
||||
} from "../agents/pi-embedded-runner/runs.js";
|
||||
export { disposeRegisteredAgentHarnesses } from "../agents/harness/registry.js";
|
||||
export { normalizeProviderToolSchemas } from "../agents/pi-embedded-runner/tool-schema-runtime.js";
|
||||
export { resolveSandboxContext } from "../agents/sandbox.js";
|
||||
export { isSubagentSessionKey } from "../routing/session-key.js";
|
||||
export { acquireSessionWriteLock } from "../agents/session-write-lock.js";
|
||||
export { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
||||
export {
|
||||
resolveAgentHarnessBeforePromptBuildResult,
|
||||
runAgentHarnessAfterCompactionHook,
|
||||
runAgentHarnessBeforeCompactionHook,
|
||||
} from "../agents/harness/prompt-compaction-hook-helpers.js";
|
||||
export { createCodexAppServerToolResultExtensionRunner } from "../agents/harness/codex-app-server-extensions.js";
|
||||
export {
|
||||
runAgentHarnessAfterToolCallHook,
|
||||
runAgentHarnessBeforeMessageWriteHook,
|
||||
} from "../agents/harness/hook-helpers.js";
|
||||
export {
|
||||
runAgentHarnessAgentEndHook,
|
||||
runAgentHarnessLlmInputHook,
|
||||
runAgentHarnessLlmOutputHook,
|
||||
} from "../agents/harness/lifecycle-hook-helpers.js";
|
||||
@@ -1,78 +1,5 @@
|
||||
// Public agent harness surface for plugins that replace the low-level agent runtime.
|
||||
// Keep model/vendor-specific protocol code in the plugin that registers the harness.
|
||||
|
||||
export type {
|
||||
AgentHarness,
|
||||
AgentHarnessAttemptParams,
|
||||
AgentHarnessAttemptResult,
|
||||
AgentHarnessCompactParams,
|
||||
AgentHarnessCompactResult,
|
||||
AgentHarnessResetParams,
|
||||
AgentHarnessSupport,
|
||||
AgentHarnessSupportContext,
|
||||
} from "../agents/harness/types.js";
|
||||
export type {
|
||||
EmbeddedRunAttemptParams,
|
||||
EmbeddedRunAttemptResult,
|
||||
} from "../agents/pi-embedded-runner/run/types.js";
|
||||
export type { CompactEmbeddedPiSessionParams } from "../agents/pi-embedded-runner/compact.js";
|
||||
export type { EmbeddedPiCompactResult } from "../agents/pi-embedded-runner/types.js";
|
||||
export type { AnyAgentTool } from "../agents/tools/common.js";
|
||||
export type { MessagingToolSend } from "../agents/pi-embedded-messaging.types.js";
|
||||
export type { AgentApprovalEventData } from "../infra/agent-events.js";
|
||||
export type { ExecApprovalDecision } from "../infra/exec-approvals.js";
|
||||
export type { NormalizedUsage } from "../agents/usage.js";
|
||||
export type {
|
||||
CodexAppServerExtensionContext,
|
||||
CodexAppServerExtensionFactory,
|
||||
CodexAppServerExtensionRuntime,
|
||||
CodexAppServerToolResultEvent,
|
||||
CodexAppServerToolResultHandlerResult,
|
||||
} from "../plugins/codex-app-server-extension-types.js";
|
||||
|
||||
export { VERSION as OPENCLAW_VERSION } from "../version.js";
|
||||
export { formatErrorMessage } from "../infra/errors.js";
|
||||
export { log as embeddedAgentLog } from "../agents/pi-embedded-runner/logger.js";
|
||||
export { resolveEmbeddedAgentRuntime } from "../agents/pi-embedded-runner/runtime.js";
|
||||
export { resolveUserPath } from "../utils.js";
|
||||
export { callGatewayTool } from "../agents/tools/gateway.js";
|
||||
export { isMessagingTool, isMessagingToolSendAction } from "../agents/pi-embedded-messaging.js";
|
||||
export {
|
||||
extractToolResultMediaArtifact,
|
||||
filterToolResultMediaUrls,
|
||||
} from "../agents/pi-embedded-subscribe.tools.js";
|
||||
export { normalizeUsage } from "../agents/usage.js";
|
||||
export { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||
export { resolveSessionAgentIds } from "../agents/agent-scope.js";
|
||||
export { resolveModelAuthMode } from "../agents/model-auth.js";
|
||||
export { supportsModelTools } from "../agents/model-tool-support.js";
|
||||
export { resolveAttemptSpawnWorkspaceDir } from "../agents/pi-embedded-runner/run/attempt.thread-helpers.js";
|
||||
export { buildEmbeddedAttemptToolRunContext } from "../agents/pi-embedded-runner/run/attempt.tool-run-context.js";
|
||||
export {
|
||||
abortEmbeddedPiRun as abortAgentHarnessRun,
|
||||
clearActiveEmbeddedRun,
|
||||
queueEmbeddedPiMessage as queueAgentHarnessMessage,
|
||||
setActiveEmbeddedRun,
|
||||
} from "../agents/pi-embedded-runner/runs.js";
|
||||
export { disposeRegisteredAgentHarnesses } from "../agents/harness/registry.js";
|
||||
export { normalizeProviderToolSchemas } from "../agents/pi-embedded-runner/tool-schema-runtime.js";
|
||||
export * from "./agent-harness-runtime.js";
|
||||
export { createOpenClawCodingTools } from "../agents/pi-tools.js";
|
||||
export { resolveSandboxContext } from "../agents/sandbox.js";
|
||||
export { isSubagentSessionKey } from "../routing/session-key.js";
|
||||
export { acquireSessionWriteLock } from "../agents/session-write-lock.js";
|
||||
export { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
||||
export {
|
||||
resolveAgentHarnessBeforePromptBuildResult,
|
||||
runAgentHarnessAfterCompactionHook,
|
||||
runAgentHarnessBeforeCompactionHook,
|
||||
} from "../agents/harness/prompt-compaction-hook-helpers.js";
|
||||
export { createCodexAppServerToolResultExtensionRunner } from "../agents/harness/codex-app-server-extensions.js";
|
||||
export {
|
||||
runAgentHarnessAfterToolCallHook,
|
||||
runAgentHarnessBeforeMessageWriteHook,
|
||||
} from "../agents/harness/hook-helpers.js";
|
||||
export {
|
||||
runAgentHarnessAgentEndHook,
|
||||
runAgentHarnessLlmInputHook,
|
||||
runAgentHarnessLlmOutputHook,
|
||||
} from "../agents/harness/lifecycle-hook-helpers.js";
|
||||
|
||||
Reference in New Issue
Block a user