openclaw/src/agents/pi-embedded-runner.ts

1393 lines
45 KiB
TypeScript
Raw Normal View History

2025-12-22 20:45:22 +00:00
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
2025-12-22 20:45:22 +00:00
2026-01-07 06:12:56 +00:00
import type {
AgentMessage,
AgentTool,
ThinkingLevel,
} from "@mariozechner/pi-agent-core";
2026-01-04 21:56:16 +01:00
import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai";
2025-12-22 20:45:22 +00:00
import {
buildSystemPrompt,
createAgentSession,
2025-12-26 10:16:50 +01:00
discoverAuthStorage,
discoverModels,
2025-12-22 20:45:22 +00:00
SessionManager,
SettingsManager,
type Skill,
} from "@mariozechner/pi-coding-agent";
2026-01-06 21:54:19 +00:00
import { resolveHeartbeatPrompt } from "../auto-reply/heartbeat.js";
import type {
ReasoningLevel,
ThinkLevel,
VerboseLevel,
} from "../auto-reply/thinking.js";
2025-12-22 20:45:22 +00:00
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
2026-01-04 14:32:47 +00:00
import type { ClawdbotConfig } from "../config/config.js";
import { getMachineDisplayName } from "../infra/machine-name.js";
import { createSubsystemLogger } from "../logging.js";
2025-12-22 20:45:22 +00:00
import { splitMediaFromOutput } from "../media/parse.js";
2025-12-25 23:50:52 +01:00
import {
2025-12-25 23:58:37 +00:00
type enqueueCommand,
2025-12-25 23:50:52 +01:00
enqueueCommandInLane,
} from "../process/command-queue.js";
import { resolveUserPath } from "../utils.js";
2026-01-04 14:32:47 +00:00
import { resolveClawdbotAgentDir } from "./agent-paths.js";
2026-01-06 07:18:06 +01:00
import {
markAuthProfileCooldown,
markAuthProfileGood,
markAuthProfileUsed,
} from "./auth-profiles.js";
2026-01-04 05:15:42 +00:00
import type { BashElevatedDefaults } from "./bash-tools.js";
import {
DEFAULT_CONTEXT_TOKENS,
DEFAULT_MODEL,
DEFAULT_PROVIDER,
} from "./defaults.js";
import {
ensureAuthProfileStore,
getApiKeyForModel,
resolveAuthProfileOrder,
} from "./model-auth.js";
2026-01-04 14:32:47 +00:00
import { ensureClawdbotModelsJson } from "./models-config.js";
2025-12-22 20:45:22 +00:00
import {
buildBootstrapContextFiles,
ensureSessionHeader,
formatAssistantErrorText,
isAuthAssistantError,
isAuthErrorMessage,
isContextOverflowError,
isRateLimitAssistantError,
isRateLimitErrorMessage,
pickFallbackThinkingLevel,
sanitizeGoogleTurnOrdering,
2025-12-22 20:45:22 +00:00
sanitizeSessionMessagesImages,
} from "./pi-embedded-helpers.js";
2026-01-03 16:45:53 +01:00
import {
type BlockReplyChunking,
2026-01-03 12:35:16 -06:00
subscribeEmbeddedPiSession,
2026-01-03 16:45:53 +01:00
} from "./pi-embedded-subscribe.js";
import {
extractAssistantText,
extractAssistantThinking,
formatReasoningMarkdown,
} from "./pi-embedded-utils.js";
import { setContextPruningRuntime } from "./pi-extensions/context-pruning/runtime.js";
import { computeEffectiveSettings } from "./pi-extensions/context-pruning/settings.js";
import { makeToolPrunablePredicate } from "./pi-extensions/context-pruning/tools.js";
import { toToolDefinitions } from "./pi-tool-definition-adapter.js";
2026-01-04 14:32:47 +00:00
import { createClawdbotCodingTools } from "./pi-tools.js";
import { resolveSandboxContext } from "./sandbox.js";
2025-12-22 20:45:22 +00:00
import {
applySkillEnvOverrides,
applySkillEnvOverridesFromSnapshot,
buildWorkspaceSkillSnapshot,
loadWorkspaceSkillEntries,
type SkillEntry,
type SkillSnapshot,
} from "./skills.js";
import { buildAgentSystemPromptAppend } from "./system-prompt.js";
import { normalizeUsage, type UsageLike } from "./usage.js";
2025-12-22 20:45:22 +00:00
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
// Optional features can be implemented as Pi extensions that run in the same Node process.
// We configure context pruning per-session via a WeakMap registry keyed by the SessionManager instance.
function resolvePiExtensionPath(id: string): string {
const self = fileURLToPath(import.meta.url);
const dir = path.dirname(self);
// In dev this file is `.ts` (tsx), in production it's `.js`.
const ext = path.extname(self) === ".ts" ? "ts" : "js";
return path.join(dir, "pi-extensions", `${id}.${ext}`);
}
function resolveContextWindowTokens(params: {
cfg: ClawdbotConfig | undefined;
provider: string;
modelId: string;
model: Model<Api> | undefined;
}): number {
const fromModel =
typeof params.model?.contextWindow === "number" &&
Number.isFinite(params.model.contextWindow) &&
params.model.contextWindow > 0
? params.model.contextWindow
: undefined;
if (fromModel) return fromModel;
const fromModelsConfig = (() => {
const providers = params.cfg?.models?.providers as
| Record<
string,
{ models?: Array<{ id?: string; contextWindow?: number }> }
>
| undefined;
const providerEntry = providers?.[params.provider];
const models = Array.isArray(providerEntry?.models)
? providerEntry.models
: [];
const match = models.find((m) => m?.id === params.modelId);
return typeof match?.contextWindow === "number" && match.contextWindow > 0
? match.contextWindow
: undefined;
})();
if (fromModelsConfig) return fromModelsConfig;
const fromAgentConfig =
typeof params.cfg?.agent?.contextTokens === "number" &&
Number.isFinite(params.cfg.agent.contextTokens) &&
params.cfg.agent.contextTokens > 0
? Math.floor(params.cfg.agent.contextTokens)
: undefined;
if (fromAgentConfig) return fromAgentConfig;
return DEFAULT_CONTEXT_TOKENS;
}
function buildContextPruningExtension(params: {
cfg: ClawdbotConfig | undefined;
sessionManager: SessionManager;
provider: string;
modelId: string;
model: Model<Api> | undefined;
}): { additionalExtensionPaths?: string[] } {
const raw = params.cfg?.agent?.contextPruning;
if (raw?.mode !== "adaptive" && raw?.mode !== "aggressive") return {};
const settings = computeEffectiveSettings(raw);
if (!settings) return {};
setContextPruningRuntime(params.sessionManager, {
settings,
contextWindowTokens: resolveContextWindowTokens(params),
isToolPrunable: makeToolPrunablePredicate(settings.tools),
});
return {
additionalExtensionPaths: [resolvePiExtensionPath("context-pruning")],
};
}
2025-12-22 20:45:22 +00:00
export type EmbeddedPiAgentMeta = {
sessionId: string;
provider: string;
model: string;
usage?: {
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
total?: number;
};
};
export type EmbeddedPiRunMeta = {
durationMs: number;
agentMeta?: EmbeddedPiAgentMeta;
aborted?: boolean;
};
function buildModelAliasLines(cfg?: ClawdbotConfig) {
const models = cfg?.agent?.models ?? {};
const entries: Array<{ alias: string; model: string }> = [];
for (const [keyRaw, entryRaw] of Object.entries(models)) {
const model = String(keyRaw ?? "").trim();
if (!model) continue;
const alias = String(
(entryRaw as { alias?: string } | undefined)?.alias ?? "",
).trim();
if (!alias) continue;
entries.push({ alias, model });
}
return entries
.sort((a, b) => a.alias.localeCompare(b.alias))
.map((entry) => `- ${entry.alias}: ${entry.model}`);
}
2026-01-06 01:08:36 +00:00
type ApiKeyInfo = {
apiKey: string;
profileId?: string;
source: string;
};
2025-12-22 20:45:22 +00:00
export type EmbeddedPiRunResult = {
payloads?: Array<{
text?: string;
mediaUrl?: string;
mediaUrls?: string[];
2026-01-02 23:18:41 +01:00
replyToId?: string;
isError?: boolean;
2025-12-22 20:45:22 +00:00
}>;
meta: EmbeddedPiRunMeta;
};
export type EmbeddedPiCompactResult = {
ok: boolean;
compacted: boolean;
reason?: string;
result?: {
summary: string;
firstKeptEntryId: string;
tokensBefore: number;
details?: unknown;
};
};
2025-12-22 20:45:22 +00:00
type EmbeddedPiQueueHandle = {
queueMessage: (text: string) => Promise<void>;
isStreaming: () => boolean;
isCompacting: () => boolean;
abort: () => void;
2025-12-22 20:45:22 +00:00
};
const log = createSubsystemLogger("agent/embedded");
2025-12-22 20:45:22 +00:00
const ACTIVE_EMBEDDED_RUNS = new Map<string, EmbeddedPiQueueHandle>();
2026-01-03 23:57:17 +00:00
type EmbeddedRunWaiter = {
resolve: (ended: boolean) => void;
timer: NodeJS.Timeout;
};
const EMBEDDED_RUN_WAITERS = new Map<string, Set<EmbeddedRunWaiter>>();
2025-12-22 20:45:22 +00:00
2026-01-07 01:00:47 +01:00
const isAbortError = (err: unknown): boolean => {
if (!err || typeof err !== "object") return false;
const name = "name" in err ? String(err.name) : "";
if (name === "AbortError") return true;
const message =
"message" in err && typeof err.message === "string"
? err.message.toLowerCase()
: "";
return message.includes("aborted");
};
type EmbeddedSandboxInfo = {
enabled: boolean;
workspaceDir?: string;
workspaceAccess?: "none" | "ro" | "rw";
agentWorkspaceMount?: string;
browserControlUrl?: string;
browserNoVncUrl?: string;
};
2025-12-25 23:50:52 +01:00
function resolveSessionLane(key: string) {
const cleaned = key.trim() || "main";
return cleaned.startsWith("session:") ? cleaned : `session:${cleaned}`;
}
function resolveGlobalLane(lane?: string) {
const cleaned = lane?.trim();
return cleaned ? cleaned : "main";
}
2026-01-05 23:02:13 +00:00
function resolveUserTimezone(configured?: string): string {
const trimmed = configured?.trim();
if (trimmed) {
try {
new Intl.DateTimeFormat("en-US", { timeZone: trimmed }).format(
new Date(),
);
return trimmed;
} catch {
// ignore invalid timezone
}
}
const host = Intl.DateTimeFormat().resolvedOptions().timeZone;
return host?.trim() || "UTC";
}
function formatUserTime(date: Date, timeZone: string): string | undefined {
try {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone,
2026-01-07 11:02:39 +01:00
weekday: "long",
2026-01-05 23:02:13 +00:00
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
}).formatToParts(date);
const map: Record<string, string> = {};
for (const part of parts) {
if (part.type !== "literal") map[part.type] = part.value;
}
2026-01-07 11:02:39 +01:00
if (
!map.weekday ||
!map.year ||
!map.month ||
!map.day ||
!map.hour ||
!map.minute
) {
2026-01-05 23:02:13 +00:00
return undefined;
}
2026-01-07 11:02:39 +01:00
return `${map.weekday} ${map.year}-${map.month}-${map.day} ${map.hour}:${map.minute}`;
2026-01-05 23:02:13 +00:00
} catch {
return undefined;
}
}
2026-01-05 23:05:57 +00:00
function describeUnknownError(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "string") return error;
try {
const serialized = JSON.stringify(error);
return serialized ?? "Unknown error";
} catch {
return "Unknown error";
}
}
export function buildEmbeddedSandboxInfo(
sandbox?: Awaited<ReturnType<typeof resolveSandboxContext>>,
): EmbeddedSandboxInfo | undefined {
if (!sandbox?.enabled) return undefined;
return {
enabled: true,
workspaceDir: sandbox.workspaceDir,
workspaceAccess: sandbox.workspaceAccess,
agentWorkspaceMount:
sandbox.workspaceAccess === "ro" ? "/agent" : undefined,
browserControlUrl: sandbox.browser?.controlUrl,
browserNoVncUrl: sandbox.browser?.noVncUrl,
};
}
2026-01-07 06:12:56 +00:00
const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]);
type AnyAgentTool = AgentTool;
2026-01-07 06:12:56 +00:00
export function splitSdkTools(options: {
tools: AnyAgentTool[];
sandboxEnabled: boolean;
}): {
builtInTools: AnyAgentTool[];
customTools: ReturnType<typeof toToolDefinitions>;
} {
// SDK rebuilds built-ins from cwd; route sandboxed versions as custom tools.
const { tools, sandboxEnabled } = options;
if (sandboxEnabled) {
return {
builtInTools: [],
customTools: toToolDefinitions(tools),
};
}
return {
builtInTools: tools.filter((tool) => BUILT_IN_TOOL_NAMES.has(tool.name)),
customTools: toToolDefinitions(
tools.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool.name)),
),
};
}
2025-12-22 20:45:22 +00:00
export function queueEmbeddedPiMessage(
sessionId: string,
text: string,
): boolean {
const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId);
if (!handle) return false;
if (!handle.isStreaming()) return false;
if (handle.isCompacting()) return false;
2025-12-22 20:45:22 +00:00
void handle.queueMessage(text);
return true;
}
export function abortEmbeddedPiRun(sessionId: string): boolean {
const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId);
if (!handle) return false;
handle.abort();
return true;
}
export function isEmbeddedPiRunActive(sessionId: string): boolean {
return ACTIVE_EMBEDDED_RUNS.has(sessionId);
}
export function isEmbeddedPiRunStreaming(sessionId: string): boolean {
const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId);
if (!handle) return false;
return handle.isStreaming();
}
2026-01-03 23:57:17 +00:00
export function waitForEmbeddedPiRunEnd(
sessionId: string,
timeoutMs = 15_000,
): Promise<boolean> {
if (!sessionId || !ACTIVE_EMBEDDED_RUNS.has(sessionId))
return Promise.resolve(true);
return new Promise((resolve) => {
const waiters = EMBEDDED_RUN_WAITERS.get(sessionId) ?? new Set();
const waiter: EmbeddedRunWaiter = {
resolve,
2026-01-04 01:16:53 +01:00
timer: setTimeout(
() => {
waiters.delete(waiter);
if (waiters.size === 0) EMBEDDED_RUN_WAITERS.delete(sessionId);
resolve(false);
},
Math.max(100, timeoutMs),
),
2026-01-03 23:57:17 +00:00
};
waiters.add(waiter);
EMBEDDED_RUN_WAITERS.set(sessionId, waiters);
if (!ACTIVE_EMBEDDED_RUNS.has(sessionId)) {
waiters.delete(waiter);
if (waiters.size === 0) EMBEDDED_RUN_WAITERS.delete(sessionId);
clearTimeout(waiter.timer);
resolve(true);
}
});
}
function notifyEmbeddedRunEnded(sessionId: string) {
const waiters = EMBEDDED_RUN_WAITERS.get(sessionId);
if (!waiters || waiters.size === 0) return;
EMBEDDED_RUN_WAITERS.delete(sessionId);
for (const waiter of waiters) {
clearTimeout(waiter.timer);
waiter.resolve(true);
}
}
export function resolveEmbeddedSessionLane(key: string) {
return resolveSessionLane(key);
}
2025-12-22 20:45:22 +00:00
function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {
2026-01-04 14:32:47 +00:00
// pi-agent-core supports "xhigh" too; Clawdbot doesn't surface it for now.
2025-12-22 20:45:22 +00:00
if (!level) return "off";
return level;
}
function resolveModel(
provider: string,
modelId: string,
agentDir?: string,
): {
model?: Model<Api>;
error?: string;
authStorage: ReturnType<typeof discoverAuthStorage>;
modelRegistry: ReturnType<typeof discoverModels>;
} {
2026-01-04 14:32:47 +00:00
const resolvedAgentDir = agentDir ?? resolveClawdbotAgentDir();
2025-12-26 10:16:50 +01:00
const authStorage = discoverAuthStorage(resolvedAgentDir);
const modelRegistry = discoverModels(authStorage, resolvedAgentDir);
const model = modelRegistry.find(provider, modelId) as Model<Api> | null;
if (!model) {
return {
error: `Unknown model: ${provider}/${modelId}`,
authStorage,
modelRegistry,
};
}
return { model, authStorage, modelRegistry };
2025-12-22 20:45:22 +00:00
}
function resolvePromptSkills(
snapshot: SkillSnapshot,
entries: SkillEntry[],
): Skill[] {
if (snapshot.resolvedSkills?.length) {
return snapshot.resolvedSkills;
}
const snapshotNames = snapshot.skills.map((entry) => entry.name);
if (snapshotNames.length === 0) return [];
const entryByName = new Map(
entries.map((entry) => [entry.skill.name, entry.skill]),
);
return snapshotNames
.map((name) => entryByName.get(name))
.filter((skill): skill is Skill => Boolean(skill));
}
export async function compactEmbeddedPiSession(params: {
sessionId: string;
sessionKey?: string;
messageProvider?: string;
sessionFile: string;
workspaceDir: string;
agentDir?: string;
config?: ClawdbotConfig;
skillsSnapshot?: SkillSnapshot;
provider?: string;
model?: string;
thinkLevel?: ThinkLevel;
bashElevated?: BashElevatedDefaults;
customInstructions?: string;
lane?: string;
enqueue?: typeof enqueueCommand;
extraSystemPrompt?: string;
ownerNumbers?: string[];
}): Promise<EmbeddedPiCompactResult> {
const sessionLane = resolveSessionLane(
params.sessionKey?.trim() || params.sessionId,
);
const globalLane = resolveGlobalLane(params.lane);
const enqueueGlobal =
params.enqueue ??
((task, opts) => enqueueCommandInLane(globalLane, task, opts));
return enqueueCommandInLane(sessionLane, () =>
enqueueGlobal(async () => {
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
const prevCwd = process.cwd();
const provider =
(params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
await ensureClawdbotModelsJson(params.config);
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
const { model, error, authStorage, modelRegistry } = resolveModel(
provider,
modelId,
agentDir,
);
if (!model) {
return {
ok: false,
compacted: false,
reason: error ?? `Unknown model: ${provider}/${modelId}`,
};
}
try {
2026-01-06 02:43:35 +01:00
const apiKeyInfo = await getApiKeyForModel({
2026-01-06 01:38:09 +00:00
model,
cfg: params.config,
});
2026-01-06 02:43:35 +01:00
authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey);
} catch (err) {
return {
ok: false,
compacted: false,
reason: describeUnknownError(err),
};
}
await fs.mkdir(resolvedWorkspace, { recursive: true });
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
const sandbox = await resolveSandboxContext({
config: params.config,
sessionKey: sandboxSessionKey,
workspaceDir: resolvedWorkspace,
});
const effectiveWorkspace = sandbox?.enabled
? sandbox.workspaceAccess === "rw"
? resolvedWorkspace
: sandbox.workspaceDir
: resolvedWorkspace;
await fs.mkdir(effectiveWorkspace, { recursive: true });
await ensureSessionHeader({
sessionFile: params.sessionFile,
sessionId: params.sessionId,
cwd: effectiveWorkspace,
});
let restoreSkillEnv: (() => void) | undefined;
process.chdir(effectiveWorkspace);
try {
const shouldLoadSkillEntries =
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
const skillEntries = shouldLoadSkillEntries
? loadWorkspaceSkillEntries(effectiveWorkspace)
: [];
const skillsSnapshot =
params.skillsSnapshot ??
buildWorkspaceSkillSnapshot(effectiveWorkspace, {
config: params.config,
entries: skillEntries,
});
restoreSkillEnv = params.skillsSnapshot
? applySkillEnvOverridesFromSnapshot({
snapshot: params.skillsSnapshot,
config: params.config,
})
: applySkillEnvOverrides({
skills: skillEntries ?? [],
config: params.config,
});
const bootstrapFiles =
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
const tools = createClawdbotCodingTools({
bash: {
...params.config?.agent?.bash,
elevated: params.bashElevated,
},
sandbox,
messageProvider: params.messageProvider,
sessionKey: params.sessionKey ?? params.sessionId,
agentDir,
config: params.config,
});
const machineName = await getMachineDisplayName();
const runtimeInfo = {
host: machineName,
os: `${os.type()} ${os.release()}`,
arch: os.arch(),
node: process.version,
model: `${provider}/${modelId}`,
};
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
const reasoningTagHint = provider === "ollama";
const userTimezone = resolveUserTimezone(
params.config?.agent?.userTimezone,
);
const userTime = formatUserTime(new Date(), userTimezone);
const systemPrompt = buildSystemPrompt({
appendPrompt: buildAgentSystemPromptAppend({
workspaceDir: effectiveWorkspace,
defaultThinkLevel: params.thinkLevel,
extraSystemPrompt: params.extraSystemPrompt,
ownerNumbers: params.ownerNumbers,
reasoningTagHint,
2026-01-06 21:54:19 +00:00
heartbeatPrompt: resolveHeartbeatPrompt(
params.config?.agent?.heartbeat?.prompt,
),
runtimeInfo,
sandboxInfo,
toolNames: tools.map((tool) => tool.name),
modelAliasLines: buildModelAliasLines(params.config),
userTimezone,
userTime,
}),
contextFiles,
skills: promptSkills,
cwd: effectiveWorkspace,
tools,
});
const sessionManager = SessionManager.open(params.sessionFile);
const settingsManager = SettingsManager.create(
effectiveWorkspace,
agentDir,
);
const pruning = buildContextPruningExtension({
cfg: params.config,
sessionManager,
provider,
modelId,
model,
});
const additionalExtensionPaths = pruning.additionalExtensionPaths;
2026-01-07 06:12:56 +00:00
const { builtInTools, customTools } = splitSdkTools({
tools,
sandboxEnabled: !!sandbox?.enabled,
});
let session: Awaited<ReturnType<typeof createAgentSession>>["session"];
({ session } = await createAgentSession({
cwd: resolvedWorkspace,
agentDir,
authStorage,
modelRegistry,
model,
thinkingLevel: mapThinkingLevel(params.thinkLevel),
systemPrompt,
tools: builtInTools,
customTools,
sessionManager,
settingsManager,
skills: promptSkills,
contextFiles,
additionalExtensionPaths,
}));
try {
const sanitizedImages = await sanitizeSessionMessagesImages(
session.messages,
"session:history",
);
const needsGoogleBootstrap =
(model.api === "google-gemini-cli" ||
model.api === "google-generative-ai") &&
sanitizedImages[0] &&
typeof sanitizedImages[0] === "object" &&
"role" in sanitizedImages[0] &&
sanitizedImages[0].role === "assistant";
const prior =
model.api === "google-gemini-cli" ||
model.api === "google-generative-ai"
? sanitizeGoogleTurnOrdering(sanitizedImages)
: sanitizedImages;
if (needsGoogleBootstrap) {
log.warn(
`google turn ordering fixup: prepended user bootstrap (sessionId=${params.sessionId})`,
);
}
if (prior.length > 0) {
session.agent.replaceMessages(prior);
}
const result = await session.compact(params.customInstructions);
return {
ok: true,
compacted: true,
result: {
summary: result.summary,
firstKeptEntryId: result.firstKeptEntryId,
tokensBefore: result.tokensBefore,
details: result.details,
},
};
} finally {
session.dispose();
}
} catch (err) {
return {
ok: false,
compacted: false,
reason: describeUnknownError(err),
};
} finally {
restoreSkillEnv?.();
process.chdir(prevCwd);
}
}),
);
}
2025-12-22 20:45:22 +00:00
export async function runEmbeddedPiAgent(params: {
sessionId: string;
2025-12-25 23:50:52 +01:00
sessionKey?: string;
messageProvider?: string;
2025-12-22 20:45:22 +00:00
sessionFile: string;
workspaceDir: string;
agentDir?: string;
2026-01-04 14:32:47 +00:00
config?: ClawdbotConfig;
2025-12-22 20:45:22 +00:00
skillsSnapshot?: SkillSnapshot;
prompt: string;
provider?: string;
model?: string;
authProfileId?: string;
2025-12-22 20:45:22 +00:00
thinkLevel?: ThinkLevel;
verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
2026-01-04 05:15:42 +00:00
bashElevated?: BashElevatedDefaults;
2025-12-22 20:45:22 +00:00
timeoutMs: number;
runId: string;
abortSignal?: AbortSignal;
shouldEmitToolResult?: () => boolean;
onPartialReply?: (payload: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
2026-01-03 00:28:33 +01:00
onBlockReply?: (payload: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
blockReplyBreak?: "text_end" | "message_end";
2026-01-03 16:45:53 +01:00
blockReplyChunking?: BlockReplyChunking;
2026-01-07 11:08:11 +01:00
onReasoningStream?: (payload: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
2025-12-22 20:45:22 +00:00
onToolResult?: (payload: {
text?: string;
mediaUrls?: string[];
}) => void | Promise<void>;
onAgentEvent?: (evt: {
stream: string;
data: Record<string, unknown>;
}) => void;
2025-12-25 23:50:52 +01:00
lane?: string;
2025-12-22 20:45:22 +00:00
enqueue?: typeof enqueueCommand;
extraSystemPrompt?: string;
ownerNumbers?: string[];
enforceFinalTag?: boolean;
2025-12-22 20:45:22 +00:00
}): Promise<EmbeddedPiRunResult> {
const sessionLane = resolveSessionLane(
params.sessionKey?.trim() || params.sessionId,
);
2025-12-25 23:50:52 +01:00
const globalLane = resolveGlobalLane(params.lane);
const enqueueGlobal =
params.enqueue ??
((task, opts) => enqueueCommandInLane(globalLane, task, opts));
return enqueueCommandInLane(sessionLane, () =>
enqueueGlobal(async () => {
const started = Date.now();
2026-01-03 20:16:53 +00:00
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
2025-12-25 23:50:52 +01:00
const prevCwd = process.cwd();
const provider =
(params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
2026-01-04 14:32:47 +00:00
await ensureClawdbotModelsJson(params.config);
const agentDir = params.agentDir ?? resolveClawdbotAgentDir();
const { model, error, authStorage, modelRegistry } = resolveModel(
provider,
modelId,
agentDir,
);
2025-12-25 23:50:52 +01:00
if (!model) {
throw new Error(error ?? `Unknown model: ${provider}/${modelId}`);
}
const authStore = ensureAuthProfileStore(agentDir);
const explicitProfileId = params.authProfileId?.trim();
const profileOrder = resolveAuthProfileOrder({
cfg: params.config,
store: authStore,
provider,
preferredProfile: explicitProfileId,
});
if (explicitProfileId && !profileOrder.includes(explicitProfileId)) {
throw new Error(
`Auth profile "${explicitProfileId}" is not configured for ${provider}.`,
);
}
const profileCandidates =
profileOrder.length > 0 ? profileOrder : [undefined];
let profileIndex = 0;
const initialThinkLevel = params.thinkLevel ?? "off";
let thinkLevel = initialThinkLevel;
const attemptedThinking = new Set<ThinkLevel>();
2026-01-06 01:08:36 +00:00
let apiKeyInfo: ApiKeyInfo | null = null;
let lastProfileId: string | undefined;
const resolveApiKeyForCandidate = async (candidate?: string) => {
return getApiKeyForModel({
model,
cfg: params.config,
profileId: candidate,
store: authStore,
});
};
const applyApiKeyInfo = async (candidate?: string): Promise<void> => {
apiKeyInfo = await resolveApiKeyForCandidate(candidate);
authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey);
2026-01-06 01:08:36 +00:00
lastProfileId = apiKeyInfo.profileId;
};
const advanceAuthProfile = async (): Promise<boolean> => {
let nextIndex = profileIndex + 1;
while (nextIndex < profileCandidates.length) {
const candidate = profileCandidates[nextIndex];
try {
await applyApiKeyInfo(candidate);
profileIndex = nextIndex;
thinkLevel = initialThinkLevel;
attemptedThinking.clear();
return true;
} catch (err) {
if (candidate && candidate === explicitProfileId) throw err;
nextIndex += 1;
}
}
return false;
};
try {
await applyApiKeyInfo(profileCandidates[profileIndex]);
} catch (err) {
if (profileCandidates[profileIndex] === explicitProfileId) throw err;
const advanced = await advanceAuthProfile();
if (!advanced) throw err;
}
2025-12-25 23:50:52 +01:00
while (true) {
const thinkingLevel = mapThinkingLevel(thinkLevel);
attemptedThinking.add(thinkLevel);
log.debug(
`embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${provider} model=${modelId} thinking=${thinkLevel} messageProvider=${params.messageProvider ?? "unknown"}`,
);
2025-12-22 20:45:22 +00:00
await fs.mkdir(resolvedWorkspace, { recursive: true });
const sandboxSessionKey = params.sessionKey?.trim() || params.sessionId;
const sandbox = await resolveSandboxContext({
config: params.config,
sessionKey: sandboxSessionKey,
workspaceDir: resolvedWorkspace,
});
const effectiveWorkspace = sandbox?.enabled
? sandbox.workspaceAccess === "rw"
? resolvedWorkspace
: sandbox.workspaceDir
: resolvedWorkspace;
await fs.mkdir(effectiveWorkspace, { recursive: true });
await ensureSessionHeader({
sessionFile: params.sessionFile,
sessionId: params.sessionId,
cwd: effectiveWorkspace,
});
let restoreSkillEnv: (() => void) | undefined;
process.chdir(effectiveWorkspace);
try {
const shouldLoadSkillEntries =
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
const skillEntries = shouldLoadSkillEntries
? loadWorkspaceSkillEntries(effectiveWorkspace)
: [];
const skillsSnapshot =
params.skillsSnapshot ??
buildWorkspaceSkillSnapshot(effectiveWorkspace, {
2025-12-25 23:50:52 +01:00
config: params.config,
entries: skillEntries,
2025-12-25 23:50:52 +01:00
});
restoreSkillEnv = params.skillsSnapshot
? applySkillEnvOverridesFromSnapshot({
snapshot: params.skillsSnapshot,
config: params.config,
})
: applySkillEnvOverrides({
skills: skillEntries ?? [],
config: params.config,
});
const bootstrapFiles =
await loadWorkspaceBootstrapFiles(effectiveWorkspace);
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
const promptSkills = resolvePromptSkills(
skillsSnapshot,
skillEntries,
);
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
const tools = createClawdbotCodingTools({
bash: {
...params.config?.agent?.bash,
elevated: params.bashElevated,
},
sandbox,
messageProvider: params.messageProvider,
sessionKey: params.sessionKey ?? params.sessionId,
agentDir,
config: params.config,
});
const machineName = await getMachineDisplayName();
const runtimeInfo = {
host: machineName,
os: `${os.type()} ${os.release()}`,
arch: os.arch(),
node: process.version,
model: `${provider}/${modelId}`,
};
const sandboxInfo = buildEmbeddedSandboxInfo(sandbox);
const reasoningTagHint = provider === "ollama";
2026-01-05 23:02:13 +00:00
const userTimezone = resolveUserTimezone(
params.config?.agent?.userTimezone,
);
const userTime = formatUserTime(new Date(), userTimezone);
const systemPrompt = buildSystemPrompt({
appendPrompt: buildAgentSystemPromptAppend({
workspaceDir: effectiveWorkspace,
defaultThinkLevel: thinkLevel,
extraSystemPrompt: params.extraSystemPrompt,
ownerNumbers: params.ownerNumbers,
reasoningTagHint,
2026-01-06 21:54:19 +00:00
heartbeatPrompt: resolveHeartbeatPrompt(
params.config?.agent?.heartbeat?.prompt,
),
runtimeInfo,
sandboxInfo,
toolNames: tools.map((tool) => tool.name),
modelAliasLines: buildModelAliasLines(params.config),
2026-01-05 23:02:13 +00:00
userTimezone,
userTime,
}),
contextFiles,
skills: promptSkills,
cwd: effectiveWorkspace,
tools,
});
2025-12-22 20:45:22 +00:00
const sessionManager = SessionManager.open(params.sessionFile);
const settingsManager = SettingsManager.create(
effectiveWorkspace,
agentDir,
);
const pruning = buildContextPruningExtension({
cfg: params.config,
sessionManager,
provider,
modelId,
model,
});
const additionalExtensionPaths = pruning.additionalExtensionPaths;
2025-12-22 20:45:22 +00:00
2026-01-07 06:12:56 +00:00
const { builtInTools, customTools } = splitSdkTools({
tools,
sandboxEnabled: !!sandbox?.enabled,
});
let session: Awaited<
ReturnType<typeof createAgentSession>
>["session"];
({ session } = await createAgentSession({
cwd: resolvedWorkspace,
agentDir,
authStorage,
modelRegistry,
model,
thinkingLevel,
systemPrompt,
// Built-in tools recognized by pi-coding-agent SDK
tools: builtInTools,
// Custom clawdbot tools (browser, canvas, nodes, cron, etc.)
customTools,
sessionManager,
settingsManager,
skills: promptSkills,
contextFiles,
additionalExtensionPaths,
}));
2025-12-25 23:50:52 +01:00
try {
const prior = await sanitizeSessionMessagesImages(
session.messages,
"session:history",
);
const needsGoogleBootstrap =
(model.api === "google-gemini-cli" ||
model.api === "google-generative-ai") &&
prior[0] &&
typeof prior[0] === "object" &&
"role" in prior[0] &&
prior[0].role === "assistant";
const sanitizedPrior =
model.api === "google-gemini-cli" ||
model.api === "google-generative-ai"
? sanitizeGoogleTurnOrdering(prior)
: prior;
if (needsGoogleBootstrap) {
log.warn(
`google turn ordering fixup: prepended user bootstrap (sessionId=${params.sessionId})`,
);
}
if (sanitizedPrior.length > 0) {
session.agent.replaceMessages(sanitizedPrior);
}
} catch (err) {
session.dispose();
throw err;
}
let aborted = Boolean(params.abortSignal?.aborted);
let timedOut = false;
const abortRun = (isTimeout = false) => {
aborted = true;
if (isTimeout) timedOut = true;
void session.abort();
};
let subscription: ReturnType<typeof subscribeEmbeddedPiSession>;
try {
subscription = subscribeEmbeddedPiSession({
session,
runId: params.runId,
verboseLevel: params.verboseLevel,
reasoningMode: params.reasoningLevel ?? "off",
shouldEmitToolResult: params.shouldEmitToolResult,
onToolResult: params.onToolResult,
onReasoningStream: params.onReasoningStream,
onBlockReply: params.onBlockReply,
blockReplyBreak: params.blockReplyBreak,
blockReplyChunking: params.blockReplyChunking,
onPartialReply: params.onPartialReply,
onAgentEvent: params.onAgentEvent,
enforceFinalTag: params.enforceFinalTag,
});
} catch (err) {
session.dispose();
throw err;
}
const {
assistantTexts,
toolMetas,
unsubscribe,
waitForCompactionRetry,
} = subscription;
const queueHandle: EmbeddedPiQueueHandle = {
queueMessage: async (text: string) => {
await session.steer(text);
},
isStreaming: () => session.isStreaming,
isCompacting: () => subscription.isCompacting(),
abort: abortRun,
};
ACTIVE_EMBEDDED_RUNS.set(params.sessionId, queueHandle);
2025-12-22 20:45:22 +00:00
let abortWarnTimer: NodeJS.Timeout | undefined;
const abortTimer = setTimeout(
() => {
log.warn(
`embedded run timeout: runId=${params.runId} sessionId=${params.sessionId} timeoutMs=${params.timeoutMs}`,
);
abortRun(true);
if (!abortWarnTimer) {
abortWarnTimer = setTimeout(() => {
if (!session.isStreaming) return;
log.warn(
`embedded run abort still streaming: runId=${params.runId} sessionId=${params.sessionId}`,
);
}, 10_000);
}
},
Math.max(1, params.timeoutMs),
);
let messagesSnapshot: AgentMessage[] = [];
let sessionIdUsed = session.sessionId;
const onAbort = () => {
2026-01-03 14:57:49 +00:00
abortRun();
};
if (params.abortSignal) {
if (params.abortSignal.aborted) {
onAbort();
} else {
params.abortSignal.addEventListener("abort", onAbort, {
once: true,
});
2026-01-03 14:57:49 +00:00
}
2025-12-25 23:50:52 +01:00
}
let promptError: unknown = null;
2025-12-25 23:50:52 +01:00
try {
const promptStartedAt = Date.now();
log.debug(
`embedded run prompt start: runId=${params.runId} sessionId=${params.sessionId}`,
);
try {
await session.prompt(params.prompt);
} catch (err) {
promptError = err;
} finally {
log.debug(
`embedded run prompt end: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - promptStartedAt}`,
);
}
fix(auth): improve multi-account round-robin rotation and 429 handling This commit fixes several issues with multi-account OAuth rotation that were causing slow responses and inefficient account cycling. ## Changes ### 1. Fix usageStats race condition (auth-profiles.ts) The `markAuthProfileUsed`, `markAuthProfileCooldown`, `markAuthProfileGood`, and `clearAuthProfileCooldown` functions were using a stale in-memory store passed as a parameter. Long-running sessions would overwrite usageStats updates from concurrent sessions when saving. **Fix:** Re-read the store from disk before each update to get fresh usageStats from other sessions, then merge the update. ### 2. Capture AbortError from waitForCompactionRetry (pi-embedded-runner.ts) When a request timed out, `session.abort()` was called which throws an `AbortError`. The code structure was: ```javascript try { await session.prompt(params.prompt); } catch (err) { promptError = err; // Catches AbortError here } await waitForCompactionRetry(); // But THIS also throws AbortError! ``` The second `AbortError` from `waitForCompactionRetry()` escaped and bypassed the rotation/fallback logic entirely. **Fix:** Wrap `waitForCompactionRetry()` in its own try/catch to capture the error as `promptError`, enabling proper timeout handling. Root cause analysis and fix proposed by @erikpr1994 in #313. Fixes #313 ### 3. Fail fast on 429 rate limits (pi-ai patch) The pi-ai library was retrying 429 errors up to 3 times with exponential backoff before throwing. This meant a rate-limited account would waste 30+ seconds retrying before our rotation code could try the next account. **Fix:** Patch google-gemini-cli.js to: - Throw immediately on first 429 (no retries) - Not catch and retry 429 errors in the network error handler This allows the caller to rotate to the next account instantly on rate limit. Note: We submitted this fix upstream (https://github.com/badlogic/pi-mono/pull/504) but it was closed without merging. Keeping as a local patch for now. ## Testing With 6 Antigravity accounts configured: - Accounts rotate properly based on lastUsed (round-robin) - 429s trigger immediate rotation to next account - usageStats persist correctly across concurrent sessions - Cooldown tracking works as expected ## Before/After **Before:** Multiple 429 retries on same account, 30-90s delays **After:** Instant rotation on 429, responses in seconds
2026-01-06 22:44:19 +00:00
try {
await waitForCompactionRetry();
} catch (err) {
// Capture AbortError from waitForCompactionRetry to enable fallback/rotation.
2026-01-07 01:00:47 +01:00
if (isAbortError(err)) {
if (!promptError) promptError = err;
} else {
throw err;
}
fix(auth): improve multi-account round-robin rotation and 429 handling This commit fixes several issues with multi-account OAuth rotation that were causing slow responses and inefficient account cycling. ## Changes ### 1. Fix usageStats race condition (auth-profiles.ts) The `markAuthProfileUsed`, `markAuthProfileCooldown`, `markAuthProfileGood`, and `clearAuthProfileCooldown` functions were using a stale in-memory store passed as a parameter. Long-running sessions would overwrite usageStats updates from concurrent sessions when saving. **Fix:** Re-read the store from disk before each update to get fresh usageStats from other sessions, then merge the update. ### 2. Capture AbortError from waitForCompactionRetry (pi-embedded-runner.ts) When a request timed out, `session.abort()` was called which throws an `AbortError`. The code structure was: ```javascript try { await session.prompt(params.prompt); } catch (err) { promptError = err; // Catches AbortError here } await waitForCompactionRetry(); // But THIS also throws AbortError! ``` The second `AbortError` from `waitForCompactionRetry()` escaped and bypassed the rotation/fallback logic entirely. **Fix:** Wrap `waitForCompactionRetry()` in its own try/catch to capture the error as `promptError`, enabling proper timeout handling. Root cause analysis and fix proposed by @erikpr1994 in #313. Fixes #313 ### 3. Fail fast on 429 rate limits (pi-ai patch) The pi-ai library was retrying 429 errors up to 3 times with exponential backoff before throwing. This meant a rate-limited account would waste 30+ seconds retrying before our rotation code could try the next account. **Fix:** Patch google-gemini-cli.js to: - Throw immediately on first 429 (no retries) - Not catch and retry 429 errors in the network error handler This allows the caller to rotate to the next account instantly on rate limit. Note: We submitted this fix upstream (https://github.com/badlogic/pi-mono/pull/504) but it was closed without merging. Keeping as a local patch for now. ## Testing With 6 Antigravity accounts configured: - Accounts rotate properly based on lastUsed (round-robin) - 429s trigger immediate rotation to next account - usageStats persist correctly across concurrent sessions - Cooldown tracking works as expected ## Before/After **Before:** Multiple 429 retries on same account, 30-90s delays **After:** Instant rotation on 429, responses in seconds
2026-01-06 22:44:19 +00:00
}
messagesSnapshot = session.messages.slice();
sessionIdUsed = session.sessionId;
} finally {
clearTimeout(abortTimer);
if (abortWarnTimer) {
clearTimeout(abortWarnTimer);
abortWarnTimer = undefined;
}
unsubscribe();
if (ACTIVE_EMBEDDED_RUNS.get(params.sessionId) === queueHandle) {
ACTIVE_EMBEDDED_RUNS.delete(params.sessionId);
notifyEmbeddedRunEnded(params.sessionId);
}
session.dispose();
params.abortSignal?.removeEventListener?.("abort", onAbort);
2025-12-25 23:50:52 +01:00
}
if (promptError && !aborted) {
const errorText = describeUnknownError(promptError);
if (isContextOverflowError(errorText)) {
return {
payloads: [
{
text:
"Context overflow: the conversation history is too large for the model. " +
"Use /new or /reset to start a fresh session, or try a model with a larger context window.",
isError: true,
},
],
meta: {
durationMs: Date.now() - started,
agentMeta: {
sessionId: sessionIdUsed,
provider,
model: model.id,
},
},
};
}
if (
(isAuthErrorMessage(errorText) ||
isRateLimitErrorMessage(errorText)) &&
(await advanceAuthProfile())
) {
continue;
}
const fallbackThinking = pickFallbackThinkingLevel({
message: errorText,
attempted: attemptedThinking,
});
if (fallbackThinking) {
log.warn(
`unsupported thinking level for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
);
thinkLevel = fallbackThinking;
continue;
}
throw promptError;
2025-12-25 23:50:52 +01:00
}
const lastAssistant = messagesSnapshot
.slice()
.reverse()
.find((m) => (m as AgentMessage)?.role === "assistant") as
| AssistantMessage
| undefined;
const fallbackThinking = pickFallbackThinkingLevel({
message: lastAssistant?.errorMessage,
attempted: attemptedThinking,
});
if (fallbackThinking && !aborted) {
log.warn(
`unsupported thinking level for ${provider}/${modelId}; retrying with ${fallbackThinking}`,
);
thinkLevel = fallbackThinking;
continue;
}
2025-12-25 23:50:52 +01:00
const fallbackConfigured =
(params.config?.agent?.model?.fallbacks?.length ?? 0) > 0;
const authFailure = isAuthAssistantError(lastAssistant);
const rateLimitFailure = isRateLimitAssistantError(lastAssistant);
2026-01-06 07:18:06 +01:00
// Treat timeout as potential rate limit (Antigravity hangs on rate limit)
2026-01-06 07:18:06 +01:00
const shouldRotate =
(!aborted && (authFailure || rateLimitFailure)) || timedOut;
if (shouldRotate) {
// Mark current profile for cooldown before rotating
if (lastProfileId) {
await markAuthProfileCooldown({
2026-01-06 07:18:06 +01:00
store: authStore,
profileId: lastProfileId,
});
if (timedOut) {
log.warn(
`Profile ${lastProfileId} timed out (possible rate limit). Trying next account...`,
);
}
}
const rotated = await advanceAuthProfile();
if (rotated) {
continue;
}
2026-01-06 07:18:06 +01:00
if (fallbackConfigured) {
const message =
lastAssistant?.errorMessage?.trim() ||
(lastAssistant
? formatAssistantErrorText(lastAssistant)
: "") ||
2026-01-06 07:18:06 +01:00
(timedOut
? "LLM request timed out."
: rateLimitFailure
? "LLM request rate limited."
: "LLM request unauthorized.");
throw new Error(message);
}
}
const usage = normalizeUsage(lastAssistant?.usage as UsageLike);
const agentMeta: EmbeddedPiAgentMeta = {
sessionId: sessionIdUsed,
provider: lastAssistant?.provider ?? provider,
model: lastAssistant?.model ?? model.id,
usage,
};
const replyItems: Array<{
text: string;
media?: string[];
isError?: boolean;
}> = [];
const errorText = lastAssistant
? formatAssistantErrorText(lastAssistant)
: undefined;
if (errorText) replyItems.push({ text: errorText, isError: true });
const inlineToolResults =
params.verboseLevel === "on" &&
!params.onPartialReply &&
!params.onToolResult &&
toolMetas.length > 0;
if (inlineToolResults) {
for (const { toolName, meta } of toolMetas) {
const agg = formatToolAggregate(toolName, meta ? [meta] : []);
const { text: cleanedText, mediaUrls } =
splitMediaFromOutput(agg);
if (cleanedText)
replyItems.push({ text: cleanedText, media: mediaUrls });
}
}
const fallbackText = lastAssistant
? (() => {
const base = extractAssistantText(lastAssistant);
if (params.reasoningLevel !== "on") return base;
const thinking = extractAssistantThinking(lastAssistant);
const formatted = thinking
? formatReasoningMarkdown(thinking)
: "";
if (!formatted) return base;
2026-01-07 07:05:07 +01:00
return base ? `${formatted}\n\n${base}` : formatted;
})()
: "";
for (const text of assistantTexts.length
? assistantTexts
: fallbackText
? [fallbackText]
: []) {
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text);
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0))
continue;
replyItems.push({ text: cleanedText, media: mediaUrls });
2025-12-25 23:50:52 +01:00
}
2025-12-22 20:45:22 +00:00
const payloads = replyItems
.map((item) => ({
text: item.text?.trim() ? item.text.trim() : undefined,
mediaUrls: item.media?.length ? item.media : undefined,
mediaUrl: item.media?.[0],
isError: item.isError,
}))
.filter(
(p) =>
p.text || p.mediaUrl || (p.mediaUrls && p.mediaUrls.length > 0),
);
2025-12-22 20:45:22 +00:00
log.debug(
`embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`,
2025-12-25 23:50:52 +01:00
);
2026-01-06 01:08:36 +00:00
if (lastProfileId) {
await markAuthProfileGood({
store: authStore,
provider,
2026-01-06 01:08:36 +00:00
profileId: lastProfileId,
});
// Track usage for round-robin rotation
await markAuthProfileUsed({
store: authStore,
profileId: lastProfileId,
});
}
return {
payloads: payloads.length ? payloads : undefined,
meta: {
durationMs: Date.now() - started,
agentMeta,
aborted,
},
};
} finally {
restoreSkillEnv?.();
process.chdir(prevCwd);
}
2025-12-25 23:50:52 +01:00
}
}),
);
2025-12-22 20:45:22 +00:00
}