mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-01 10:03:30 +00:00
* refactor: extract agent core package Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts. * refactor: extract shared llm runtime Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout. * refactor: remove pi runtime internals Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code. * refactor: tighten agent session runtime Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts. * refactor: remove static model and pi auth paths Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities. * refactor: remove legacy provider compat paths * docs: remove agent parity notes * fix: skip provider wildcard metadata parsing * refactor: share session extension sdk loading * refactor: inline acpx proxy error formatter * refactor: fold edit recovery into edit tool * fix: accept extension batch separator * test: align startup provider plugin expectations * fix: restore provider-scoped release discovery * test: align static asset packaging expectations * fix: run static provider catalogs during scoped discovery * fix: add provider entry catalogs for scoped live discovery * fix: load lightweight provider catalog entries * fix: refresh provider-scoped plugin metadata * fix: keep provider catalog entries on release live path * fix: keep static manifest models in release live checks * fix: harden release model discovery * fix: reduce OpenAI live cache probe reasoning * fix: disable OpenAI cache probe reasoning * ci: extend OpenAI gateway live timeout * fix: extend live gateway model budget * fix: stabilize release validation regressions * fix: honor provider aliases in model rows * fix: stabilize release validation lanes * fix: stabilize release memory qa * ci: stabilize release validation lanes * ci: prefer ipv4 for live docker node calls * fix: restore shared tool-call stream wrapper * ci: remove legacy pi test shard alias * fix: clean up embedded agent test drift * fix: stabilize runtime alias status * fix: clean up embedded agent ci drift * fix: restore release ci invariants * fix: clean up post-rebase runtime drift * fix: restore release ci checks * fix: restore release ci after rebase * fix: remove stale pi runtime path * test: align compaction runtime expectations * test: update plugin prerelease expectations * fix: handle claude live tool approvals * fix: stabilize release validation gates * fix: finish agent runtime import * test: finish post-rebase agent runtime mocks * fix: keep codex compaction native * fix: stabilize codex app-server hook tests * test: isolate codex diagnostic active run * test: remove codex diagnostic completion race # Conflicts: # extensions/codex/src/app-server/run-attempt.test.ts * ci: fix full release manifest performance run id * refactor: narrow llm plugin sdk boundary * chore: drop generated google boundary stamps * fix: repair rebase fallout * fix: clean up rebased runtime references * fix: decode codex jwt payloads as base64url * fix: preserve shipped pi runtime alias * fix: add scoped sdk virtual modules * fix: decode llm codex oauth jwt as base64url * fix: avoid stale vertex adc negative cache * fix: harden tool arg decoding and codeql path * fix: keep vertex adc negative checks live * refactor: consolidate codex jwt and edit helpers * fix: await codex oauth node runtime imports * fix: preserve sdk tool and notice contracts * fix: preserve shipped compat config boundaries * fix: align codex oauth callback host * fix: terminate agent-core loop streams on failure * fix: keep codex oauth callback alive during fallback * ci: include session tools in critical codeql scans * fix: keep Cloudflare Anthropic provider auth header * docs: redirect legacy pi runtime pages * fix: honor bundled web provider compat discovery * fix: protect session output spill files * fix: keep legacy agent dir env blocked * fix: contain auto-discovered skill symlinks * fix: harden agent core sdk proxy surfaces * fix: restore approval reaction sdk compat * fix: keep live docker runs bounded * fix: keep codex oauth redirect host aligned * fix: resolve post-rebase agent runtime drift * fix: redact anthropic oauth parse failures * fix: preserve responses strict tool shaping * fix: repair agent runtime rebase cleanup * docs: redirect retired parity pages * fix: bound auto-discovered resources to roots * fix: repair post-rebase agent test drift * fix: preserve bundled provider allowlist migration * fix: preserve manifest-owned provider aliases * fix: declare photon image dependency * fix: keep provider headers out of proxy body * fix: preserve shipped env aliases * fix: refresh control ui i18n generated state * fix: quote read fallback paths * fix: preview edits through configured backend * test: satisfy core test typecheck * fix: preserve ZAI usage auth fallback * test: repair codex diagnostic test * fix: repair agent runtime rebase drift * test: finish embedded runner import rename * fix: repair agent runtime rebase integrations * test: align compaction oauth fallback expectations * fix: allow sdk-auth session models * fix: update doctor tool schema import * fix: preserve bedrock plugin region * fix: stream harmony-like prose immediately * ci: include session runtime in codeql shards * fix: repair latest rebase integrations * fix: honor explicit codex websocket transport * fix: keep openai-compatible credentials provider-scoped * fix: refresh sdk api baseline after rebase * fix: route cli runtime aliases through openclaw harness * test: rename stale harness mock expectation * test: rename embedded agent overflow calls * test: clean embedded auth test wording * test: use openclaw stream types in deepinfra cache test * fix: refresh sdk api baseline on latest main * fix: honor bundled discovery compat allowlists * fix: refresh sdk api baseline after latest rebase * fix: remove stale rebase imports * test: rename stale model catalog mock * test: mock renamed doctor runtime modules * fix: map canonical kimi env auth * fix: use internal model registry in bench script * fix: migrate deepinfra provider catalog entry * fix: enforce builtin tool suppression * fix: route compaction auth and proxy payloads safely * refactor: prune unused llm registry leftovers * test: update codex hooks session import * test: fix model picker ci coverage * test: align model picker auth mock types
301 lines
9.7 KiB
TypeScript
301 lines
9.7 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import path from "node:path";
|
|
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { resolveStateDir } from "../config/paths.js";
|
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
|
import { resolveCommitmentTimezone, resolveCommitmentsConfig } from "./config.js";
|
|
import {
|
|
buildCommitmentExtractionPrompt,
|
|
hydrateCommitmentExtractionItem,
|
|
parseCommitmentExtractionOutput,
|
|
persistCommitmentExtractionResult,
|
|
} from "./extraction.js";
|
|
import type {
|
|
CommitmentExtractionBatchResult,
|
|
CommitmentExtractionItem,
|
|
CommitmentScope,
|
|
} from "./types.js";
|
|
|
|
type TimerHandle = ReturnType<typeof setTimeout>;
|
|
type ModelRef = { provider: string; model: string };
|
|
type EmbeddedAgentPayloadResult = { payloads?: Array<{ text?: string }> };
|
|
|
|
type CommitmentExtractionEnqueueInput = CommitmentScope & {
|
|
cfg?: OpenClawConfig;
|
|
nowMs?: number;
|
|
userText: string;
|
|
assistantText?: string;
|
|
sourceMessageId?: string;
|
|
sourceRunId?: string;
|
|
};
|
|
|
|
type CommitmentExtractionRuntime = {
|
|
extractBatch?: (params: {
|
|
cfg?: OpenClawConfig;
|
|
items: CommitmentExtractionItem[];
|
|
}) => Promise<CommitmentExtractionBatchResult>;
|
|
resolveDefaultModel?: (params: { cfg: OpenClawConfig; agentId?: string }) => ModelRef;
|
|
setTimer?: (callback: () => void, delayMs: number) => TimerHandle;
|
|
clearTimer?: (timer: TimerHandle) => void;
|
|
forceInTests?: boolean;
|
|
};
|
|
|
|
const log = createSubsystemLogger("commitments");
|
|
const TERMINAL_EXTRACTION_FAILURE_COOLDOWN_MS = 15 * 60_000;
|
|
|
|
let runtime: CommitmentExtractionRuntime = {};
|
|
let queue: Array<Omit<CommitmentExtractionItem, "existingPending"> & { cfg?: OpenClawConfig }> = [];
|
|
let timer: TimerHandle | null = null;
|
|
let draining = false;
|
|
let queueOverflowWarned = false;
|
|
let terminalFailureCooldownUntilByAgent = new Map<string, number>();
|
|
|
|
function shouldDisableBackgroundExtractionForTests(): boolean {
|
|
if (runtime.forceInTests) {
|
|
return false;
|
|
}
|
|
return process.env.VITEST === "true" || process.env.NODE_ENV === "test";
|
|
}
|
|
|
|
function setTimer(callback: () => void, delayMs: number): TimerHandle {
|
|
const handle = runtime.setTimer
|
|
? runtime.setTimer(callback, delayMs)
|
|
: setTimeout(callback, delayMs);
|
|
if (typeof handle === "object" && "unref" in handle && typeof handle.unref === "function") {
|
|
handle.unref();
|
|
}
|
|
return handle;
|
|
}
|
|
|
|
function clearTimer(handle: TimerHandle): void {
|
|
(runtime.clearTimer ?? clearTimeout)(handle);
|
|
}
|
|
|
|
export function configureCommitmentExtractionRuntime(next: CommitmentExtractionRuntime): void {
|
|
runtime = next;
|
|
}
|
|
|
|
export function resetCommitmentExtractionRuntimeForTests(): void {
|
|
if (timer) {
|
|
clearTimer(timer);
|
|
}
|
|
runtime = {};
|
|
queue = [];
|
|
timer = null;
|
|
draining = false;
|
|
queueOverflowWarned = false;
|
|
terminalFailureCooldownUntilByAgent = new Map();
|
|
}
|
|
|
|
function buildItemId(params: CommitmentExtractionEnqueueInput, nowMs: number): string {
|
|
const source = normalizeOptionalString(params.sourceMessageId) ? "message" : "turn";
|
|
return `${source}:${nowMs.toString(36)}:${randomUUID()}`;
|
|
}
|
|
|
|
function isUsefulText(value: string | undefined): boolean {
|
|
return Boolean(value?.trim());
|
|
}
|
|
|
|
export function enqueueCommitmentExtraction(input: CommitmentExtractionEnqueueInput): boolean {
|
|
const resolved = resolveCommitmentsConfig(input.cfg);
|
|
const nowMs = input.nowMs ?? Date.now();
|
|
const agentId = normalizeOptionalString(input.agentId) ?? "";
|
|
const sessionKey = normalizeOptionalString(input.sessionKey) ?? "";
|
|
const channel = normalizeOptionalString(input.channel) ?? "";
|
|
if (
|
|
!resolved.enabled ||
|
|
shouldDisableBackgroundExtractionForTests() ||
|
|
(agentId ? nowMs < (terminalFailureCooldownUntilByAgent.get(agentId) ?? 0) : false) ||
|
|
!isUsefulText(input.userText) ||
|
|
!isUsefulText(input.assistantText) ||
|
|
!agentId ||
|
|
!sessionKey ||
|
|
!channel
|
|
) {
|
|
return false;
|
|
}
|
|
if (queue.length >= resolved.extraction.queueMaxItems) {
|
|
if (!queueOverflowWarned) {
|
|
log.warn("commitment extraction queue full; dropping hidden extraction request", {
|
|
queued: queue.length,
|
|
max: resolved.extraction.queueMaxItems,
|
|
});
|
|
queueOverflowWarned = true;
|
|
}
|
|
return false;
|
|
}
|
|
queue.push({
|
|
itemId: buildItemId(input, nowMs),
|
|
nowMs,
|
|
timezone: resolveCommitmentTimezone(input.cfg),
|
|
agentId,
|
|
sessionKey,
|
|
channel,
|
|
...(input.accountId?.trim() ? { accountId: input.accountId.trim() } : {}),
|
|
...(input.to?.trim() ? { to: input.to.trim() } : {}),
|
|
...(input.threadId?.trim() ? { threadId: input.threadId.trim() } : {}),
|
|
...(input.senderId?.trim() ? { senderId: input.senderId.trim() } : {}),
|
|
userText: input.userText.trim(),
|
|
...(input.assistantText?.trim() ? { assistantText: input.assistantText.trim() } : {}),
|
|
...(input.sourceMessageId?.trim() ? { sourceMessageId: input.sourceMessageId.trim() } : {}),
|
|
...(input.sourceRunId?.trim() ? { sourceRunId: input.sourceRunId.trim() } : {}),
|
|
cfg: input.cfg,
|
|
});
|
|
if (!timer) {
|
|
timer = setTimer(() => {
|
|
timer = null;
|
|
void drainCommitmentExtractionQueue().catch((err) => {
|
|
log.warn("commitment extraction failed", { error: String(err) });
|
|
});
|
|
}, resolved.extraction.debounceMs);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isTerminalExtractionError(error: unknown): boolean {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return (
|
|
/\bNo API key found\b/i.test(message) ||
|
|
/\bUnknown model\b/i.test(message) ||
|
|
/\bAuth profile credentials are missing or expired\b/i.test(message) ||
|
|
/\bOAuth token refresh failed\b/i.test(message) ||
|
|
/\bmissing credential\b/i.test(message) ||
|
|
/\bmissing credentials\b/i.test(message) ||
|
|
/\bmissing_api_key\b/i.test(message) ||
|
|
/\binvalid_grant\b/i.test(message)
|
|
);
|
|
}
|
|
|
|
function openTerminalFailureCooldown(agentId: string, error: unknown): void {
|
|
terminalFailureCooldownUntilByAgent.set(
|
|
agentId,
|
|
Date.now() + TERMINAL_EXTRACTION_FAILURE_COOLDOWN_MS,
|
|
);
|
|
queue = queue.filter((item) => item.agentId !== agentId);
|
|
log.warn("commitment extraction disabled temporarily after terminal model/auth failure", {
|
|
agentId,
|
|
cooldownMs: TERMINAL_EXTRACTION_FAILURE_COOLDOWN_MS,
|
|
error: String(error),
|
|
});
|
|
}
|
|
|
|
function resolveExtractionSessionFile(agentId: string, runId: string): string {
|
|
return path.join(
|
|
resolveStateDir(),
|
|
"commitments",
|
|
"extractor-sessions",
|
|
agentId,
|
|
`${runId}.jsonl`,
|
|
);
|
|
}
|
|
|
|
function joinPayloadText(result: EmbeddedAgentPayloadResult): string {
|
|
return (
|
|
result.payloads
|
|
?.map((payload) => payload.text)
|
|
.filter((text): text is string => Boolean(text?.trim()))
|
|
.join("\n")
|
|
.trim() ?? ""
|
|
);
|
|
}
|
|
|
|
async function resolveDefaultModel(params: {
|
|
cfg: OpenClawConfig;
|
|
agentId?: string;
|
|
}): Promise<ModelRef> {
|
|
if (runtime.resolveDefaultModel) {
|
|
return runtime.resolveDefaultModel(params);
|
|
}
|
|
const { resolveCommitmentDefaultModelRef } = await import("./model-selection.runtime.js");
|
|
return resolveCommitmentDefaultModelRef(params);
|
|
}
|
|
|
|
async function defaultExtractBatch(params: {
|
|
cfg?: OpenClawConfig;
|
|
items: CommitmentExtractionItem[];
|
|
}): Promise<CommitmentExtractionBatchResult> {
|
|
const cfg = params.cfg ?? {};
|
|
const first = params.items[0];
|
|
if (!first) {
|
|
return { candidates: [] };
|
|
}
|
|
const resolved = resolveCommitmentsConfig(cfg);
|
|
const runId = `commitments-${randomUUID()}`;
|
|
const modelRef = await resolveDefaultModel({ cfg, agentId: first.agentId });
|
|
const { runEmbeddedAgent } = await import("../agents/embedded-agent.js");
|
|
const result = await runEmbeddedAgent({
|
|
sessionId: runId,
|
|
sessionKey: `agent:${first.agentId}:commitments:${runId}`,
|
|
agentId: first.agentId,
|
|
trigger: "manual",
|
|
sessionFile: resolveExtractionSessionFile(first.agentId, runId),
|
|
workspaceDir: resolveAgentWorkspaceDir(cfg, first.agentId),
|
|
config: cfg,
|
|
provider: modelRef.provider,
|
|
model: modelRef.model,
|
|
prompt: buildCommitmentExtractionPrompt({ cfg, items: params.items }),
|
|
disableTools: true,
|
|
thinkLevel: "off",
|
|
verboseLevel: "off",
|
|
reasoningLevel: "off",
|
|
fastMode: true,
|
|
timeoutMs: resolved.extraction.timeoutSeconds * 1000,
|
|
runId,
|
|
bootstrapContextMode: "lightweight",
|
|
skillsSnapshot: { prompt: "", skills: [] },
|
|
suppressToolErrorWarnings: true,
|
|
});
|
|
return parseCommitmentExtractionOutput(joinPayloadText(result));
|
|
}
|
|
|
|
async function hydrateBatch(
|
|
batch: Array<Omit<CommitmentExtractionItem, "existingPending"> & { cfg?: OpenClawConfig }>,
|
|
): Promise<CommitmentExtractionItem[]> {
|
|
return Promise.all(
|
|
batch.map(async (item) =>
|
|
hydrateCommitmentExtractionItem({
|
|
cfg: item.cfg,
|
|
item,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
export async function drainCommitmentExtractionQueue(): Promise<number> {
|
|
if (draining) {
|
|
return 0;
|
|
}
|
|
draining = true;
|
|
try {
|
|
let processed = 0;
|
|
while (queue.length > 0) {
|
|
const firstCfg = queue[0]?.cfg;
|
|
const resolved = resolveCommitmentsConfig(firstCfg);
|
|
const batch = queue.splice(0, resolved.extraction.batchMaxItems);
|
|
const items = await hydrateBatch(batch);
|
|
const extractor = runtime.extractBatch ?? defaultExtractBatch;
|
|
let result: CommitmentExtractionBatchResult;
|
|
try {
|
|
result = await extractor({ cfg: firstCfg, items });
|
|
} catch (error) {
|
|
if (isTerminalExtractionError(error)) {
|
|
openTerminalFailureCooldown(items[0]?.agentId ?? "", error);
|
|
}
|
|
throw error;
|
|
}
|
|
await persistCommitmentExtractionResult({
|
|
cfg: firstCfg,
|
|
items,
|
|
result,
|
|
nowMs: Date.now(),
|
|
});
|
|
processed += items.length;
|
|
}
|
|
return processed;
|
|
} finally {
|
|
draining = false;
|
|
}
|
|
}
|