|
|
|
|
@@ -25,7 +25,10 @@ import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
|
|
|
|
import type { ClawdisConfig } from "../config/config.js";
|
|
|
|
|
import { getMachineDisplayName } from "../infra/machine-name.js";
|
|
|
|
|
import { splitMediaFromOutput } from "../media/parse.js";
|
|
|
|
|
import { enqueueCommand } from "../process/command-queue.js";
|
|
|
|
|
import {
|
|
|
|
|
enqueueCommand,
|
|
|
|
|
enqueueCommandInLane,
|
|
|
|
|
} from "../process/command-queue.js";
|
|
|
|
|
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
|
|
|
|
import { resolveClawdisAgentDir } from "./agent-paths.js";
|
|
|
|
|
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
|
|
|
|
@@ -90,6 +93,16 @@ const DEFAULT_OAUTH_DIR = path.join(CONFIG_DIR, "credentials");
|
|
|
|
|
let oauthStorageConfigured = false;
|
|
|
|
|
let cachedDefaultApiKey: ReturnType<typeof defaultGetApiKey> | null = null;
|
|
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveClawdisOAuthPath(): string {
|
|
|
|
|
const overrideDir =
|
|
|
|
|
process.env.CLAWDIS_OAUTH_DIR?.trim() || DEFAULT_OAUTH_DIR;
|
|
|
|
|
@@ -242,6 +255,7 @@ function resolvePromptSkills(
|
|
|
|
|
|
|
|
|
|
export async function runEmbeddedPiAgent(params: {
|
|
|
|
|
sessionId: string;
|
|
|
|
|
sessionKey?: string;
|
|
|
|
|
sessionFile: string;
|
|
|
|
|
workspaceDir: string;
|
|
|
|
|
config?: ClawdisConfig;
|
|
|
|
|
@@ -267,268 +281,277 @@ export async function runEmbeddedPiAgent(params: {
|
|
|
|
|
stream: string;
|
|
|
|
|
data: Record<string, unknown>;
|
|
|
|
|
}) => void;
|
|
|
|
|
lane?: string;
|
|
|
|
|
enqueue?: typeof enqueueCommand;
|
|
|
|
|
extraSystemPrompt?: string;
|
|
|
|
|
ownerNumbers?: string[];
|
|
|
|
|
enforceFinalTag?: boolean;
|
|
|
|
|
}): Promise<EmbeddedPiRunResult> {
|
|
|
|
|
const enqueue = params.enqueue ?? enqueueCommand;
|
|
|
|
|
return enqueue(async () => {
|
|
|
|
|
const started = Date.now();
|
|
|
|
|
const resolvedWorkspace = resolveUserPath(params.workspaceDir);
|
|
|
|
|
const prevCwd = process.cwd();
|
|
|
|
|
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 started = Date.now();
|
|
|
|
|
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 ensureClawdisModelsJson(params.config);
|
|
|
|
|
const agentDir = resolveClawdisAgentDir();
|
|
|
|
|
const { model, error } = resolveModel(provider, modelId, agentDir);
|
|
|
|
|
if (!model) {
|
|
|
|
|
throw new Error(error ?? `Unknown model: ${provider}/${modelId}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const thinkingLevel = mapThinkingLevel(params.thinkLevel);
|
|
|
|
|
|
|
|
|
|
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
|
|
|
|
await ensureSessionHeader({
|
|
|
|
|
sessionFile: params.sessionFile,
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
cwd: resolvedWorkspace,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let restoreSkillEnv: (() => void) | undefined;
|
|
|
|
|
process.chdir(resolvedWorkspace);
|
|
|
|
|
try {
|
|
|
|
|
const shouldLoadSkillEntries =
|
|
|
|
|
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
|
|
|
|
|
const skillEntries = shouldLoadSkillEntries
|
|
|
|
|
? loadWorkspaceSkillEntries(resolvedWorkspace)
|
|
|
|
|
: [];
|
|
|
|
|
const skillsSnapshot =
|
|
|
|
|
params.skillsSnapshot ??
|
|
|
|
|
buildWorkspaceSkillSnapshot(resolvedWorkspace, {
|
|
|
|
|
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(resolvedWorkspace);
|
|
|
|
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
|
|
|
|
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
|
|
|
|
const tools = createClawdisCodingTools({
|
|
|
|
|
bash: params.config?.agent?.bash,
|
|
|
|
|
});
|
|
|
|
|
const machineName = await getMachineDisplayName();
|
|
|
|
|
const runtimeInfo = {
|
|
|
|
|
host: machineName,
|
|
|
|
|
os: `${os.type()} ${os.release()}`,
|
|
|
|
|
arch: os.arch(),
|
|
|
|
|
node: process.version,
|
|
|
|
|
model: `${provider}/${modelId}`,
|
|
|
|
|
};
|
|
|
|
|
const reasoningTagHint = provider === "lmstudio" || provider === "ollama";
|
|
|
|
|
const systemPrompt = buildSystemPrompt({
|
|
|
|
|
appendPrompt: buildAgentSystemPromptAppend({
|
|
|
|
|
workspaceDir: resolvedWorkspace,
|
|
|
|
|
defaultThinkLevel: params.thinkLevel,
|
|
|
|
|
extraSystemPrompt: params.extraSystemPrompt,
|
|
|
|
|
ownerNumbers: params.ownerNumbers,
|
|
|
|
|
reasoningTagHint,
|
|
|
|
|
runtimeInfo,
|
|
|
|
|
}),
|
|
|
|
|
contextFiles,
|
|
|
|
|
skills: promptSkills,
|
|
|
|
|
cwd: resolvedWorkspace,
|
|
|
|
|
tools,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const sessionManager = SessionManager.open(params.sessionFile, agentDir);
|
|
|
|
|
const settingsManager = SettingsManager.create(
|
|
|
|
|
resolvedWorkspace,
|
|
|
|
|
agentDir,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const { session } = await createAgentSession({
|
|
|
|
|
cwd: resolvedWorkspace,
|
|
|
|
|
agentDir,
|
|
|
|
|
model,
|
|
|
|
|
thinkingLevel,
|
|
|
|
|
systemPrompt,
|
|
|
|
|
// TODO(steipete): Once pi-mono publishes file-magic MIME detection in `read` image payloads,
|
|
|
|
|
// remove `createClawdisCodingTools()` and use upstream `codingTools` again.
|
|
|
|
|
tools,
|
|
|
|
|
sessionManager,
|
|
|
|
|
settingsManager,
|
|
|
|
|
getApiKey: async (m) => {
|
|
|
|
|
return await getApiKeyForModel(m as Model<Api>);
|
|
|
|
|
},
|
|
|
|
|
skills: promptSkills,
|
|
|
|
|
contextFiles,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const prior = await sanitizeSessionMessagesImages(
|
|
|
|
|
session.messages,
|
|
|
|
|
"session:history",
|
|
|
|
|
);
|
|
|
|
|
if (prior.length > 0) {
|
|
|
|
|
session.agent.replaceMessages(prior);
|
|
|
|
|
const provider =
|
|
|
|
|
(params.provider ?? DEFAULT_PROVIDER).trim() || DEFAULT_PROVIDER;
|
|
|
|
|
const modelId = (params.model ?? DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
|
|
|
|
await ensureClawdisModelsJson(params.config);
|
|
|
|
|
const agentDir = resolveClawdisAgentDir();
|
|
|
|
|
const { model, error } = resolveModel(provider, modelId, agentDir);
|
|
|
|
|
if (!model) {
|
|
|
|
|
throw new Error(error ?? `Unknown model: ${provider}/${modelId}`);
|
|
|
|
|
}
|
|
|
|
|
const queueHandle: EmbeddedPiQueueHandle = {
|
|
|
|
|
queueMessage: async (text: string) => {
|
|
|
|
|
await session.queueMessage(text);
|
|
|
|
|
},
|
|
|
|
|
isStreaming: () => session.isStreaming,
|
|
|
|
|
};
|
|
|
|
|
ACTIVE_EMBEDDED_RUNS.set(params.sessionId, queueHandle);
|
|
|
|
|
let aborted = Boolean(params.abortSignal?.aborted);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
assistantTexts,
|
|
|
|
|
toolMetas,
|
|
|
|
|
unsubscribe,
|
|
|
|
|
flush: flushToolDebouncer,
|
|
|
|
|
} = subscribeEmbeddedPiSession({
|
|
|
|
|
session,
|
|
|
|
|
runId: params.runId,
|
|
|
|
|
verboseLevel: params.verboseLevel,
|
|
|
|
|
shouldEmitToolResult: params.shouldEmitToolResult,
|
|
|
|
|
onToolResult: params.onToolResult,
|
|
|
|
|
onPartialReply: params.onPartialReply,
|
|
|
|
|
onAgentEvent: params.onAgentEvent,
|
|
|
|
|
enforceFinalTag: params.enforceFinalTag,
|
|
|
|
|
const thinkingLevel = mapThinkingLevel(params.thinkLevel);
|
|
|
|
|
|
|
|
|
|
await fs.mkdir(resolvedWorkspace, { recursive: true });
|
|
|
|
|
await ensureSessionHeader({
|
|
|
|
|
sessionFile: params.sessionFile,
|
|
|
|
|
sessionId: params.sessionId,
|
|
|
|
|
cwd: resolvedWorkspace,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const abortTimer = setTimeout(
|
|
|
|
|
() => {
|
|
|
|
|
aborted = true;
|
|
|
|
|
void session.abort();
|
|
|
|
|
},
|
|
|
|
|
Math.max(1, params.timeoutMs),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let messagesSnapshot: AppMessage[] = [];
|
|
|
|
|
let sessionIdUsed = session.sessionId;
|
|
|
|
|
const onAbort = () => {
|
|
|
|
|
aborted = true;
|
|
|
|
|
void session.abort();
|
|
|
|
|
};
|
|
|
|
|
if (params.abortSignal) {
|
|
|
|
|
if (params.abortSignal.aborted) {
|
|
|
|
|
onAbort();
|
|
|
|
|
} else {
|
|
|
|
|
params.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let promptError: unknown = null;
|
|
|
|
|
let restoreSkillEnv: (() => void) | undefined;
|
|
|
|
|
process.chdir(resolvedWorkspace);
|
|
|
|
|
try {
|
|
|
|
|
try {
|
|
|
|
|
await session.prompt(params.prompt);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
promptError = err;
|
|
|
|
|
} finally {
|
|
|
|
|
messagesSnapshot = session.messages.slice();
|
|
|
|
|
sessionIdUsed = session.sessionId;
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(abortTimer);
|
|
|
|
|
unsubscribe();
|
|
|
|
|
flushToolDebouncer();
|
|
|
|
|
if (ACTIVE_EMBEDDED_RUNS.get(params.sessionId) === queueHandle) {
|
|
|
|
|
ACTIVE_EMBEDDED_RUNS.delete(params.sessionId);
|
|
|
|
|
}
|
|
|
|
|
session.dispose();
|
|
|
|
|
params.abortSignal?.removeEventListener?.("abort", onAbort);
|
|
|
|
|
}
|
|
|
|
|
if (promptError && !aborted) {
|
|
|
|
|
throw promptError;
|
|
|
|
|
}
|
|
|
|
|
const shouldLoadSkillEntries =
|
|
|
|
|
!params.skillsSnapshot || !params.skillsSnapshot.resolvedSkills;
|
|
|
|
|
const skillEntries = shouldLoadSkillEntries
|
|
|
|
|
? loadWorkspaceSkillEntries(resolvedWorkspace)
|
|
|
|
|
: [];
|
|
|
|
|
const skillsSnapshot =
|
|
|
|
|
params.skillsSnapshot ??
|
|
|
|
|
buildWorkspaceSkillSnapshot(resolvedWorkspace, {
|
|
|
|
|
config: params.config,
|
|
|
|
|
entries: skillEntries,
|
|
|
|
|
});
|
|
|
|
|
restoreSkillEnv = params.skillsSnapshot
|
|
|
|
|
? applySkillEnvOverridesFromSnapshot({
|
|
|
|
|
snapshot: params.skillsSnapshot,
|
|
|
|
|
config: params.config,
|
|
|
|
|
})
|
|
|
|
|
: applySkillEnvOverrides({
|
|
|
|
|
skills: skillEntries ?? [],
|
|
|
|
|
config: params.config,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const lastAssistant = messagesSnapshot
|
|
|
|
|
.slice()
|
|
|
|
|
.reverse()
|
|
|
|
|
.find((m) => (m as AppMessage)?.role === "assistant") as
|
|
|
|
|
| AssistantMessage
|
|
|
|
|
| undefined;
|
|
|
|
|
const bootstrapFiles =
|
|
|
|
|
await loadWorkspaceBootstrapFiles(resolvedWorkspace);
|
|
|
|
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
|
|
|
|
const promptSkills = resolvePromptSkills(skillsSnapshot, skillEntries);
|
|
|
|
|
const tools = createClawdisCodingTools({
|
|
|
|
|
bash: params.config?.agent?.bash,
|
|
|
|
|
});
|
|
|
|
|
const machineName = await getMachineDisplayName();
|
|
|
|
|
const runtimeInfo = {
|
|
|
|
|
host: machineName,
|
|
|
|
|
os: `${os.type()} ${os.release()}`,
|
|
|
|
|
arch: os.arch(),
|
|
|
|
|
node: process.version,
|
|
|
|
|
model: `${provider}/${modelId}`,
|
|
|
|
|
};
|
|
|
|
|
const reasoningTagHint = provider === "lmstudio" || provider === "ollama";
|
|
|
|
|
const systemPrompt = buildSystemPrompt({
|
|
|
|
|
appendPrompt: buildAgentSystemPromptAppend({
|
|
|
|
|
workspaceDir: resolvedWorkspace,
|
|
|
|
|
defaultThinkLevel: params.thinkLevel,
|
|
|
|
|
extraSystemPrompt: params.extraSystemPrompt,
|
|
|
|
|
ownerNumbers: params.ownerNumbers,
|
|
|
|
|
reasoningTagHint,
|
|
|
|
|
runtimeInfo,
|
|
|
|
|
}),
|
|
|
|
|
contextFiles,
|
|
|
|
|
skills: promptSkills,
|
|
|
|
|
cwd: resolvedWorkspace,
|
|
|
|
|
tools,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const usage = lastAssistant?.usage;
|
|
|
|
|
const agentMeta: EmbeddedPiAgentMeta = {
|
|
|
|
|
sessionId: sessionIdUsed,
|
|
|
|
|
provider: lastAssistant?.provider ?? provider,
|
|
|
|
|
model: lastAssistant?.model ?? model.id,
|
|
|
|
|
usage: usage
|
|
|
|
|
? {
|
|
|
|
|
input: usage.input,
|
|
|
|
|
output: usage.output,
|
|
|
|
|
cacheRead: usage.cacheRead,
|
|
|
|
|
cacheWrite: usage.cacheWrite,
|
|
|
|
|
total: usage.totalTokens,
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const replyItems: Array<{ text: string; media?: string[] }> = [];
|
|
|
|
|
|
|
|
|
|
const errorText = lastAssistant
|
|
|
|
|
? formatAssistantErrorText(lastAssistant)
|
|
|
|
|
: undefined;
|
|
|
|
|
if (errorText) replyItems.push({ text: errorText });
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const text of assistantTexts.length
|
|
|
|
|
? assistantTexts
|
|
|
|
|
: lastAssistant
|
|
|
|
|
? [extractAssistantText(lastAssistant)]
|
|
|
|
|
: []) {
|
|
|
|
|
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text);
|
|
|
|
|
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) continue;
|
|
|
|
|
replyItems.push({ text: cleanedText, media: mediaUrls });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payloads = replyItems
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
text: item.text?.trim() ? item.text.trim() : undefined,
|
|
|
|
|
mediaUrls: item.media?.length ? item.media : undefined,
|
|
|
|
|
mediaUrl: item.media?.[0],
|
|
|
|
|
}))
|
|
|
|
|
.filter(
|
|
|
|
|
(p) =>
|
|
|
|
|
p.text || p.mediaUrl || (p.mediaUrls && p.mediaUrls.length > 0),
|
|
|
|
|
const sessionManager = SessionManager.open(params.sessionFile, agentDir);
|
|
|
|
|
const settingsManager = SettingsManager.create(
|
|
|
|
|
resolvedWorkspace,
|
|
|
|
|
agentDir,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
payloads: payloads.length ? payloads : undefined,
|
|
|
|
|
meta: {
|
|
|
|
|
durationMs: Date.now() - started,
|
|
|
|
|
agentMeta,
|
|
|
|
|
aborted,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
restoreSkillEnv?.();
|
|
|
|
|
process.chdir(prevCwd);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const { session } = await createAgentSession({
|
|
|
|
|
cwd: resolvedWorkspace,
|
|
|
|
|
agentDir,
|
|
|
|
|
model,
|
|
|
|
|
thinkingLevel,
|
|
|
|
|
systemPrompt,
|
|
|
|
|
// TODO(steipete): Once pi-mono publishes file-magic MIME detection in `read` image payloads,
|
|
|
|
|
// remove `createClawdisCodingTools()` and use upstream `codingTools` again.
|
|
|
|
|
tools,
|
|
|
|
|
sessionManager,
|
|
|
|
|
settingsManager,
|
|
|
|
|
getApiKey: async (m) => {
|
|
|
|
|
return await getApiKeyForModel(m as Model<Api>);
|
|
|
|
|
},
|
|
|
|
|
skills: promptSkills,
|
|
|
|
|
contextFiles,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const prior = await sanitizeSessionMessagesImages(
|
|
|
|
|
session.messages,
|
|
|
|
|
"session:history",
|
|
|
|
|
);
|
|
|
|
|
if (prior.length > 0) {
|
|
|
|
|
session.agent.replaceMessages(prior);
|
|
|
|
|
}
|
|
|
|
|
const queueHandle: EmbeddedPiQueueHandle = {
|
|
|
|
|
queueMessage: async (text: string) => {
|
|
|
|
|
await session.queueMessage(text);
|
|
|
|
|
},
|
|
|
|
|
isStreaming: () => session.isStreaming,
|
|
|
|
|
};
|
|
|
|
|
ACTIVE_EMBEDDED_RUNS.set(params.sessionId, queueHandle);
|
|
|
|
|
let aborted = Boolean(params.abortSignal?.aborted);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
assistantTexts,
|
|
|
|
|
toolMetas,
|
|
|
|
|
unsubscribe,
|
|
|
|
|
flush: flushToolDebouncer,
|
|
|
|
|
} = subscribeEmbeddedPiSession({
|
|
|
|
|
session,
|
|
|
|
|
runId: params.runId,
|
|
|
|
|
verboseLevel: params.verboseLevel,
|
|
|
|
|
shouldEmitToolResult: params.shouldEmitToolResult,
|
|
|
|
|
onToolResult: params.onToolResult,
|
|
|
|
|
onPartialReply: params.onPartialReply,
|
|
|
|
|
onAgentEvent: params.onAgentEvent,
|
|
|
|
|
enforceFinalTag: params.enforceFinalTag,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const abortTimer = setTimeout(
|
|
|
|
|
() => {
|
|
|
|
|
aborted = true;
|
|
|
|
|
void session.abort();
|
|
|
|
|
},
|
|
|
|
|
Math.max(1, params.timeoutMs),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let messagesSnapshot: AppMessage[] = [];
|
|
|
|
|
let sessionIdUsed = session.sessionId;
|
|
|
|
|
const onAbort = () => {
|
|
|
|
|
aborted = true;
|
|
|
|
|
void session.abort();
|
|
|
|
|
};
|
|
|
|
|
if (params.abortSignal) {
|
|
|
|
|
if (params.abortSignal.aborted) {
|
|
|
|
|
onAbort();
|
|
|
|
|
} else {
|
|
|
|
|
params.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let promptError: unknown = null;
|
|
|
|
|
try {
|
|
|
|
|
try {
|
|
|
|
|
await session.prompt(params.prompt);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
promptError = err;
|
|
|
|
|
} finally {
|
|
|
|
|
messagesSnapshot = session.messages.slice();
|
|
|
|
|
sessionIdUsed = session.sessionId;
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
clearTimeout(abortTimer);
|
|
|
|
|
unsubscribe();
|
|
|
|
|
flushToolDebouncer();
|
|
|
|
|
if (ACTIVE_EMBEDDED_RUNS.get(params.sessionId) === queueHandle) {
|
|
|
|
|
ACTIVE_EMBEDDED_RUNS.delete(params.sessionId);
|
|
|
|
|
}
|
|
|
|
|
session.dispose();
|
|
|
|
|
params.abortSignal?.removeEventListener?.("abort", onAbort);
|
|
|
|
|
}
|
|
|
|
|
if (promptError && !aborted) {
|
|
|
|
|
throw promptError;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lastAssistant = messagesSnapshot
|
|
|
|
|
.slice()
|
|
|
|
|
.reverse()
|
|
|
|
|
.find((m) => (m as AppMessage)?.role === "assistant") as
|
|
|
|
|
| AssistantMessage
|
|
|
|
|
| undefined;
|
|
|
|
|
|
|
|
|
|
const usage = lastAssistant?.usage;
|
|
|
|
|
const agentMeta: EmbeddedPiAgentMeta = {
|
|
|
|
|
sessionId: sessionIdUsed,
|
|
|
|
|
provider: lastAssistant?.provider ?? provider,
|
|
|
|
|
model: lastAssistant?.model ?? model.id,
|
|
|
|
|
usage: usage
|
|
|
|
|
? {
|
|
|
|
|
input: usage.input,
|
|
|
|
|
output: usage.output,
|
|
|
|
|
cacheRead: usage.cacheRead,
|
|
|
|
|
cacheWrite: usage.cacheWrite,
|
|
|
|
|
total: usage.totalTokens,
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const replyItems: Array<{ text: string; media?: string[] }> = [];
|
|
|
|
|
|
|
|
|
|
const errorText = lastAssistant
|
|
|
|
|
? formatAssistantErrorText(lastAssistant)
|
|
|
|
|
: undefined;
|
|
|
|
|
if (errorText) replyItems.push({ text: errorText });
|
|
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const text of assistantTexts.length
|
|
|
|
|
? assistantTexts
|
|
|
|
|
: lastAssistant
|
|
|
|
|
? [extractAssistantText(lastAssistant)]
|
|
|
|
|
: []) {
|
|
|
|
|
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text);
|
|
|
|
|
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) continue;
|
|
|
|
|
replyItems.push({ text: cleanedText, media: mediaUrls });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const payloads = replyItems
|
|
|
|
|
.map((item) => ({
|
|
|
|
|
text: item.text?.trim() ? item.text.trim() : undefined,
|
|
|
|
|
mediaUrls: item.media?.length ? item.media : undefined,
|
|
|
|
|
mediaUrl: item.media?.[0],
|
|
|
|
|
}))
|
|
|
|
|
.filter(
|
|
|
|
|
(p) =>
|
|
|
|
|
p.text || p.mediaUrl || (p.mediaUrls && p.mediaUrls.length > 0),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
payloads: payloads.length ? payloads : undefined,
|
|
|
|
|
meta: {
|
|
|
|
|
durationMs: Date.now() - started,
|
|
|
|
|
agentMeta,
|
|
|
|
|
aborted,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
} finally {
|
|
|
|
|
restoreSkillEnv?.();
|
|
|
|
|
process.chdir(prevCwd);
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|