fix: bump pi deps and fix lint
This commit is contained in:
@@ -67,9 +67,9 @@
|
||||
"dependencies": {
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@homebridge/ciao": "^1.3.4",
|
||||
"@mariozechner/pi-agent-core": "^0.26.0",
|
||||
"@mariozechner/pi-ai": "^0.26.0",
|
||||
"@mariozechner/pi-coding-agent": "^0.26.0",
|
||||
"@mariozechner/pi-agent-core": "^0.27.1",
|
||||
"@mariozechner/pi-ai": "^0.27.1",
|
||||
"@mariozechner/pi-coding-agent": "^0.27.1",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||
"ajv": "^8.17.1",
|
||||
|
||||
46
pnpm-lock.yaml
generated
46
pnpm-lock.yaml
generated
@@ -15,14 +15,14 @@ importers:
|
||||
specifier: ^1.3.4
|
||||
version: 1.3.4
|
||||
'@mariozechner/pi-agent-core':
|
||||
specifier: ^0.26.0
|
||||
version: 0.26.1(ws@8.18.3)(zod@4.2.1)
|
||||
specifier: ^0.27.1
|
||||
version: 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@mariozechner/pi-ai':
|
||||
specifier: ^0.26.0
|
||||
version: 0.26.1(ws@8.18.3)(zod@4.2.1)
|
||||
specifier: ^0.27.1
|
||||
version: 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@mariozechner/pi-coding-agent':
|
||||
specifier: ^0.26.0
|
||||
version: 0.26.1(ws@8.18.3)(zod@4.2.1)
|
||||
specifier: ^0.27.1
|
||||
version: 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@sinclair/typebox':
|
||||
specifier: ^0.34.41
|
||||
version: 0.34.41
|
||||
@@ -659,21 +659,21 @@ packages:
|
||||
peerDependencies:
|
||||
lit: ^3.3.1
|
||||
|
||||
'@mariozechner/pi-agent-core@0.26.1':
|
||||
resolution: {integrity: sha512-yH15oPK9l8F2vGrz2mXl0dRydKkw0x4p1WChVuQALqDaFOf48V2XbLS7SvTE3qx095ylNp/Q+RQ+NiB5I2myFA==}
|
||||
'@mariozechner/pi-agent-core@0.27.1':
|
||||
resolution: {integrity: sha512-EctMh4kIVc4HqsX6vbqkLqTey6/TmyxFg/vi2GQe03dkHYJkufiSOWp3Kei5emMGNjaNL0kPeJ3H0nwfobsWCg==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@mariozechner/pi-ai@0.26.1':
|
||||
resolution: {integrity: sha512-VEH9kwQoo0N1KtBQnAHDZaIwe0nLwikGytNvjCV3RltQirywwUUsw0xQ/2YUXaN3vl3nqDO/VY1qgdSnVZE5iA==}
|
||||
'@mariozechner/pi-ai@0.27.1':
|
||||
resolution: {integrity: sha512-Y9eMs1vdHCIzpVZdg4MBMWlz9LKsyXlGDZVShPoSXgYU8Kg3/3DbkBVwBsEA7mTedKS6FDueWv9TYsFPydWMww==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@mariozechner/pi-coding-agent@0.26.1':
|
||||
resolution: {integrity: sha512-o1WOhzwPQTiUBNxlANDXJ9bTOIIpxxkwRh9+nnz9F28uEzkSfTrJLTgJoWxuRAU7Xvj5//pkKYaUPfhCd69R9g==}
|
||||
'@mariozechner/pi-coding-agent@0.27.1':
|
||||
resolution: {integrity: sha512-dbuXgrz1b0O4GWiqyDuzurxH30PSwdb8gbHGb4NnHxTP8ECwsDbC2ykTQpg12iLBbzwB2Ze0Q+iuvnjoaonuAw==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
hasBin: true
|
||||
|
||||
'@mariozechner/pi-tui@0.26.1':
|
||||
resolution: {integrity: sha512-qGKS4SwxJw4pinttl3UvzylC1IuB31QpuoM3X36mz/GmLq52RNYnriK4si52GpeTrqNm8vXDpeevI0zhPQPjYw==}
|
||||
'@mariozechner/pi-tui@0.27.1':
|
||||
resolution: {integrity: sha512-rTVQ031UR/KKJ3/6f/pRLH7Fg/hOqVnXnVEY37J2txBWd6r9uZClAL0RThGlH2xHsB0+qgER2kgiS/Kwf9I76g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@mistralai/mistralai@1.10.0':
|
||||
@@ -2991,10 +2991,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- tailwindcss
|
||||
|
||||
'@mariozechner/pi-agent-core@0.26.1(ws@8.18.3)(zod@4.2.1)':
|
||||
'@mariozechner/pi-agent-core@0.27.1(ws@8.18.3)(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@mariozechner/pi-ai': 0.26.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@mariozechner/pi-tui': 0.26.1
|
||||
'@mariozechner/pi-ai': 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@mariozechner/pi-tui': 0.27.1
|
||||
transitivePeerDependencies:
|
||||
- '@modelcontextprotocol/sdk'
|
||||
- bufferutil
|
||||
@@ -3003,7 +3003,7 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-ai@0.26.1(ws@8.18.3)(zod@4.2.1)':
|
||||
'@mariozechner/pi-ai@0.27.1(ws@8.18.3)(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.71.2(zod@4.2.1)
|
||||
'@google/genai': 1.34.0
|
||||
@@ -3023,11 +3023,11 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-coding-agent@0.26.1(ws@8.18.3)(zod@4.2.1)':
|
||||
'@mariozechner/pi-coding-agent@0.27.1(ws@8.18.3)(zod@4.2.1)':
|
||||
dependencies:
|
||||
'@mariozechner/pi-agent-core': 0.26.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@mariozechner/pi-ai': 0.26.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@mariozechner/pi-tui': 0.26.1
|
||||
'@mariozechner/pi-agent-core': 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@mariozechner/pi-ai': 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||
'@mariozechner/pi-tui': 0.27.1
|
||||
chalk: 5.6.2
|
||||
cli-highlight: 2.1.11
|
||||
diff: 8.0.2
|
||||
@@ -3042,7 +3042,7 @@ snapshots:
|
||||
- ws
|
||||
- zod
|
||||
|
||||
'@mariozechner/pi-tui@0.26.1':
|
||||
'@mariozechner/pi-tui@0.27.1':
|
||||
dependencies:
|
||||
'@types/mime-types': 2.1.4
|
||||
chalk: 5.6.2
|
||||
|
||||
106
src/agents/pi-embedded-helpers.ts
Normal file
106
src/agents/pi-embedded-helpers.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import type { AppMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { AgentToolResult, AssistantMessage } from "@mariozechner/pi-ai";
|
||||
|
||||
import { sanitizeContentBlocksImages } from "./pi-tools.js";
|
||||
import type { WorkspaceBootstrapFile } from "./workspace.js";
|
||||
|
||||
export type EmbeddedContextFile = { path: string; content: string };
|
||||
|
||||
export async function ensureSessionHeader(params: {
|
||||
sessionFile: string;
|
||||
sessionId: string;
|
||||
cwd: string;
|
||||
}) {
|
||||
const file = params.sessionFile;
|
||||
try {
|
||||
await fs.stat(file);
|
||||
return;
|
||||
} catch {
|
||||
// create
|
||||
}
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
const entry = {
|
||||
type: "session",
|
||||
id: params.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: params.cwd,
|
||||
};
|
||||
await fs.writeFile(file, `${JSON.stringify(entry)}\n`, "utf-8");
|
||||
}
|
||||
|
||||
type ContentBlock = AgentToolResult<unknown>["content"][number];
|
||||
|
||||
export async function sanitizeSessionMessagesImages(
|
||||
messages: AppMessage[],
|
||||
label: string,
|
||||
): Promise<AppMessage[]> {
|
||||
// We sanitize historical session messages because Anthropic can reject a request
|
||||
// if the transcript contains oversized base64 images (see MAX_IMAGE_DIMENSION_PX).
|
||||
const out: AppMessage[] = [];
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== "object") {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const role = (msg as { role?: unknown }).role;
|
||||
if (role === "toolResult") {
|
||||
const toolMsg = msg as Extract<AppMessage, { role: "toolResult" }>;
|
||||
const content = Array.isArray(toolMsg.content) ? toolMsg.content : [];
|
||||
const nextContent = (await sanitizeContentBlocksImages(
|
||||
content as ContentBlock[],
|
||||
label,
|
||||
)) as unknown as typeof toolMsg.content;
|
||||
out.push({ ...toolMsg, content: nextContent });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role === "user") {
|
||||
const userMsg = msg as Extract<AppMessage, { role: "user" }>;
|
||||
const content = userMsg.content;
|
||||
if (Array.isArray(content)) {
|
||||
const nextContent = (await sanitizeContentBlocksImages(
|
||||
content as unknown as ContentBlock[],
|
||||
label,
|
||||
)) as unknown as typeof userMsg.content;
|
||||
out.push({ ...userMsg, content: nextContent });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
out.push(msg);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function buildBootstrapContextFiles(
|
||||
files: WorkspaceBootstrapFile[],
|
||||
): EmbeddedContextFile[] {
|
||||
return files.map((file) => ({
|
||||
path: file.name,
|
||||
content: file.missing
|
||||
? `[MISSING] Expected at: ${file.path}`
|
||||
: (file.content ?? ""),
|
||||
}));
|
||||
}
|
||||
|
||||
export function formatAssistantErrorText(
|
||||
msg: AssistantMessage,
|
||||
): string | undefined {
|
||||
if (msg.stopReason !== "error") return undefined;
|
||||
const raw = (msg.errorMessage ?? "").trim();
|
||||
if (!raw) return "LLM request failed with an unknown error.";
|
||||
|
||||
const invalidRequest = raw.match(
|
||||
/"type":"invalid_request_error".*?"message":"([^"]+)"/,
|
||||
);
|
||||
if (invalidRequest?.[1]) {
|
||||
return `LLM request rejected: ${invalidRequest[1]}`;
|
||||
}
|
||||
|
||||
// Keep it short for WhatsApp.
|
||||
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
|
||||
}
|
||||
413
src/agents/pi-embedded-runner.ts
Normal file
413
src/agents/pi-embedded-runner.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import type { AppMessage, ThinkingLevel } from "@mariozechner/pi-agent-core";
|
||||
import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai";
|
||||
import {
|
||||
buildSystemPrompt,
|
||||
createAgentSession,
|
||||
defaultGetApiKey,
|
||||
findModelByProviderAndId,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
type Skill,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js";
|
||||
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||
import type { ClawdisConfig } from "../config/config.js";
|
||||
import { splitMediaFromOutput } from "../media/parse.js";
|
||||
import { enqueueCommand } from "../process/command-queue.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import {
|
||||
buildBootstrapContextFiles,
|
||||
ensureSessionHeader,
|
||||
formatAssistantErrorText,
|
||||
sanitizeSessionMessagesImages,
|
||||
} from "./pi-embedded-helpers.js";
|
||||
import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js";
|
||||
import { extractAssistantText } from "./pi-embedded-utils.js";
|
||||
import { createClawdisCodingTools } from "./pi-tools.js";
|
||||
import {
|
||||
applySkillEnvOverrides,
|
||||
applySkillEnvOverridesFromSnapshot,
|
||||
buildWorkspaceSkillSnapshot,
|
||||
loadWorkspaceSkillEntries,
|
||||
type SkillEntry,
|
||||
type SkillSnapshot,
|
||||
} from "./skills.js";
|
||||
import { buildAgentSystemPromptAppend } from "./system-prompt.js";
|
||||
import { loadWorkspaceBootstrapFiles } from "./workspace.js";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export type EmbeddedPiRunResult = {
|
||||
payloads?: Array<{
|
||||
text?: string;
|
||||
mediaUrl?: string;
|
||||
mediaUrls?: string[];
|
||||
}>;
|
||||
meta: EmbeddedPiRunMeta;
|
||||
};
|
||||
|
||||
type EmbeddedPiQueueHandle = {
|
||||
queueMessage: (text: string) => Promise<void>;
|
||||
isStreaming: () => boolean;
|
||||
};
|
||||
|
||||
const ACTIVE_EMBEDDED_RUNS = new Map<string, EmbeddedPiQueueHandle>();
|
||||
|
||||
export function queueEmbeddedPiMessage(
|
||||
sessionId: string,
|
||||
text: string,
|
||||
): boolean {
|
||||
const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId);
|
||||
if (!handle) return false;
|
||||
if (!handle.isStreaming()) return false;
|
||||
void handle.queueMessage(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {
|
||||
// pi-agent-core supports "xhigh" too; Clawdis doesn't surface it for now.
|
||||
if (!level) return "off";
|
||||
return level;
|
||||
}
|
||||
|
||||
function resolveModel(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
agentDir?: string,
|
||||
): { model?: Model<Api>; error?: string } {
|
||||
const model = findModelByProviderAndId(
|
||||
provider,
|
||||
modelId,
|
||||
agentDir,
|
||||
) as Model<Api> | null;
|
||||
if (!model) return { error: `Unknown model: ${provider}/${modelId}` };
|
||||
return { model };
|
||||
}
|
||||
|
||||
const defaultApiKey = defaultGetApiKey();
|
||||
|
||||
async function getApiKeyForModel(model: Model<Api>): Promise<string> {
|
||||
if (model.provider === "anthropic") {
|
||||
const oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;
|
||||
if (oauthEnv?.trim()) return oauthEnv.trim();
|
||||
}
|
||||
const key = await defaultApiKey(model);
|
||||
if (key) return key;
|
||||
throw new Error(`No API key found for provider "${model.provider}"`);
|
||||
}
|
||||
|
||||
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 runEmbeddedPiAgent(params: {
|
||||
sessionId: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
config?: ClawdisConfig;
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
prompt: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
thinkLevel?: ThinkLevel;
|
||||
verboseLevel?: VerboseLevel;
|
||||
timeoutMs: number;
|
||||
runId: string;
|
||||
abortSignal?: AbortSignal;
|
||||
shouldEmitToolResult?: () => boolean;
|
||||
onPartialReply?: (payload: {
|
||||
text?: string;
|
||||
mediaUrls?: string[];
|
||||
}) => void | Promise<void>;
|
||||
onToolResult?: (payload: {
|
||||
text?: string;
|
||||
mediaUrls?: string[];
|
||||
}) => void | Promise<void>;
|
||||
onAgentEvent?: (evt: {
|
||||
stream: string;
|
||||
data: Record<string, unknown>;
|
||||
}) => void;
|
||||
enqueue?: typeof enqueueCommand;
|
||||
}): Promise<EmbeddedPiRunResult> {
|
||||
const enqueue = params.enqueue ?? enqueueCommand;
|
||||
return enqueue(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;
|
||||
const agentDir =
|
||||
process.env.PI_CODING_AGENT_DIR ??
|
||||
path.join(os.homedir(), ".pi", "agent");
|
||||
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();
|
||||
const systemPrompt = buildSystemPrompt({
|
||||
appendPrompt: buildAgentSystemPromptAppend({
|
||||
workspaceDir: resolvedWorkspace,
|
||||
defaultThinkLevel: params.thinkLevel,
|
||||
}),
|
||||
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 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,
|
||||
});
|
||||
|
||||
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 = 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
215
src/agents/pi-embedded-subscribe.ts
Normal file
215
src/agents/pi-embedded-subscribe.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
import type { AgentEvent, AppMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { AssistantMessage } from "@mariozechner/pi-ai";
|
||||
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
import {
|
||||
createToolDebouncer,
|
||||
formatToolAggregate,
|
||||
} from "../auto-reply/tool-meta.js";
|
||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||
import { splitMediaFromOutput } from "../media/parse.js";
|
||||
import {
|
||||
extractAssistantText,
|
||||
inferToolMetaFromArgs,
|
||||
} from "./pi-embedded-utils.js";
|
||||
|
||||
export function subscribeEmbeddedPiSession(params: {
|
||||
session: AgentSession;
|
||||
runId: string;
|
||||
verboseLevel?: "off" | "on";
|
||||
shouldEmitToolResult?: () => boolean;
|
||||
onToolResult?: (payload: {
|
||||
text?: string;
|
||||
mediaUrls?: string[];
|
||||
}) => void | Promise<void>;
|
||||
onPartialReply?: (payload: {
|
||||
text?: string;
|
||||
mediaUrls?: string[];
|
||||
}) => void | Promise<void>;
|
||||
onAgentEvent?: (evt: {
|
||||
stream: string;
|
||||
data: Record<string, unknown>;
|
||||
}) => void;
|
||||
}) {
|
||||
const assistantTexts: string[] = [];
|
||||
const toolMetas: Array<{ toolName?: string; meta?: string }> = [];
|
||||
const toolMetaById = new Map<string, string | undefined>();
|
||||
let deltaBuffer = "";
|
||||
let lastStreamedAssistant: string | undefined;
|
||||
|
||||
const toolDebouncer = createToolDebouncer((toolName, metas) => {
|
||||
if (!params.onPartialReply) return;
|
||||
const text = formatToolAggregate(toolName, metas);
|
||||
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text);
|
||||
void params.onPartialReply({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
const unsubscribe = params.session.subscribe(
|
||||
(evt: AgentEvent | { type: string; [k: string]: unknown }) => {
|
||||
if (evt.type === "tool_execution_start") {
|
||||
const toolName = String(
|
||||
(evt as AgentEvent & { toolName: string }).toolName,
|
||||
);
|
||||
const toolCallId = String(
|
||||
(evt as AgentEvent & { toolCallId: string }).toolCallId,
|
||||
);
|
||||
const args = (evt as AgentEvent & { args: unknown }).args;
|
||||
const meta = inferToolMetaFromArgs(toolName, args);
|
||||
toolMetaById.set(toolCallId, meta);
|
||||
|
||||
emitAgentEvent({
|
||||
runId: params.runId,
|
||||
stream: "tool",
|
||||
data: {
|
||||
phase: "start",
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
args: args as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
params.onAgentEvent?.({
|
||||
stream: "tool",
|
||||
data: { phase: "start", name: toolName, toolCallId },
|
||||
});
|
||||
}
|
||||
|
||||
if (evt.type === "tool_execution_end") {
|
||||
const toolName = String(
|
||||
(evt as AgentEvent & { toolName: string }).toolName,
|
||||
);
|
||||
const toolCallId = String(
|
||||
(evt as AgentEvent & { toolCallId: string }).toolCallId,
|
||||
);
|
||||
const isError = Boolean(
|
||||
(evt as AgentEvent & { isError: boolean }).isError,
|
||||
);
|
||||
const meta = toolMetaById.get(toolCallId);
|
||||
toolMetas.push({ toolName, meta });
|
||||
toolDebouncer.push(toolName, meta);
|
||||
|
||||
emitAgentEvent({
|
||||
runId: params.runId,
|
||||
stream: "tool",
|
||||
data: {
|
||||
phase: "result",
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
meta,
|
||||
isError,
|
||||
},
|
||||
});
|
||||
params.onAgentEvent?.({
|
||||
stream: "tool",
|
||||
data: {
|
||||
phase: "result",
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
meta,
|
||||
isError,
|
||||
},
|
||||
});
|
||||
|
||||
const emitToolResult =
|
||||
typeof params.shouldEmitToolResult === "function"
|
||||
? params.shouldEmitToolResult()
|
||||
: params.verboseLevel === "on";
|
||||
if (emitToolResult && params.onToolResult) {
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined);
|
||||
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(agg);
|
||||
if (cleanedText || (mediaUrls && mediaUrls.length > 0)) {
|
||||
try {
|
||||
void params.onToolResult({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
});
|
||||
} catch {
|
||||
// ignore tool result delivery failures
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evt.type === "message_update") {
|
||||
const msg = (evt as AgentEvent & { message: AppMessage }).message;
|
||||
if (msg?.role === "assistant") {
|
||||
const assistantEvent = (
|
||||
evt as AgentEvent & { assistantMessageEvent?: unknown }
|
||||
).assistantMessageEvent;
|
||||
const assistantRecord =
|
||||
assistantEvent && typeof assistantEvent === "object"
|
||||
? (assistantEvent as Record<string, unknown>)
|
||||
: undefined;
|
||||
const evtType =
|
||||
typeof assistantRecord?.type === "string"
|
||||
? assistantRecord.type
|
||||
: "";
|
||||
if (
|
||||
evtType === "text_delta" ||
|
||||
evtType === "text_start" ||
|
||||
evtType === "text_end"
|
||||
) {
|
||||
const chunk =
|
||||
typeof assistantRecord?.delta === "string"
|
||||
? assistantRecord.delta
|
||||
: typeof assistantRecord?.content === "string"
|
||||
? assistantRecord.content
|
||||
: "";
|
||||
if (chunk) {
|
||||
deltaBuffer += chunk;
|
||||
const next = deltaBuffer.trim();
|
||||
if (next && next !== lastStreamedAssistant) {
|
||||
lastStreamedAssistant = next;
|
||||
const { text: cleanedText, mediaUrls } =
|
||||
splitMediaFromOutput(next);
|
||||
emitAgentEvent({
|
||||
runId: params.runId,
|
||||
stream: "assistant",
|
||||
data: {
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
},
|
||||
});
|
||||
params.onAgentEvent?.({
|
||||
stream: "assistant",
|
||||
data: {
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
},
|
||||
});
|
||||
if (params.onPartialReply) {
|
||||
void params.onPartialReply({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evt.type === "message_end") {
|
||||
const msg = (evt as AgentEvent & { message: AppMessage }).message;
|
||||
if (msg?.role === "assistant") {
|
||||
const text = extractAssistantText(msg as AssistantMessage);
|
||||
if (text) assistantTexts.push(text);
|
||||
deltaBuffer = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (evt.type === "agent_end") {
|
||||
toolDebouncer.flush();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
assistantTexts,
|
||||
toolMetas,
|
||||
unsubscribe,
|
||||
flush: () => toolDebouncer.flush(),
|
||||
};
|
||||
}
|
||||
@@ -1,691 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import type {
|
||||
AgentEvent,
|
||||
AppMessage,
|
||||
ThinkingLevel,
|
||||
} from "@mariozechner/pi-agent-core";
|
||||
import type {
|
||||
AgentToolResult,
|
||||
Api,
|
||||
AssistantMessage,
|
||||
Model,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import {
|
||||
buildSystemPrompt,
|
||||
createAgentSession,
|
||||
defaultGetApiKey,
|
||||
findModel,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
type Skill,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import type { ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js";
|
||||
import {
|
||||
createToolDebouncer,
|
||||
formatToolAggregate,
|
||||
} from "../auto-reply/tool-meta.js";
|
||||
import type { ClawdisConfig } from "../config/config.js";
|
||||
import { emitAgentEvent } from "../infra/agent-events.js";
|
||||
import { splitMediaFromOutput } from "../media/parse.js";
|
||||
import { enqueueCommand } from "../process/command-queue.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
|
||||
import {
|
||||
extractAssistantText,
|
||||
inferToolMetaFromArgs,
|
||||
} from "./pi-embedded-utils.js";
|
||||
import {
|
||||
createClawdisCodingTools,
|
||||
sanitizeContentBlocksImages,
|
||||
} from "./pi-tools.js";
|
||||
import {
|
||||
applySkillEnvOverrides,
|
||||
applySkillEnvOverridesFromSnapshot,
|
||||
buildWorkspaceSkillSnapshot,
|
||||
loadWorkspaceSkillEntries,
|
||||
type SkillEntry,
|
||||
type SkillSnapshot,
|
||||
} from "./skills.js";
|
||||
import { buildAgentSystemPromptAppend } from "./system-prompt.js";
|
||||
import {
|
||||
loadWorkspaceBootstrapFiles,
|
||||
type WorkspaceBootstrapFile,
|
||||
} from "./workspace.js";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export type EmbeddedPiRunResult = {
|
||||
payloads?: Array<{
|
||||
text?: string;
|
||||
mediaUrl?: string;
|
||||
mediaUrls?: string[];
|
||||
}>;
|
||||
meta: EmbeddedPiRunMeta;
|
||||
};
|
||||
|
||||
type EmbeddedPiQueueHandle = {
|
||||
queueMessage: (text: string) => Promise<void>;
|
||||
isStreaming: () => boolean;
|
||||
};
|
||||
|
||||
const ACTIVE_EMBEDDED_RUNS = new Map<string, EmbeddedPiQueueHandle>();
|
||||
|
||||
export function queueEmbeddedPiMessage(
|
||||
sessionId: string,
|
||||
text: string,
|
||||
): boolean {
|
||||
const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId);
|
||||
if (!handle) return false;
|
||||
if (!handle.isStreaming()) return false;
|
||||
void handle.queueMessage(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {
|
||||
// pi-agent-core supports "xhigh" too; Clawdis doesn't surface it for now.
|
||||
if (!level) return "off";
|
||||
return level;
|
||||
}
|
||||
|
||||
function resolveModel(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
agentDir?: string,
|
||||
): { model?: Model<Api>; error?: string } {
|
||||
const result = findModel(provider, modelId, agentDir);
|
||||
return {
|
||||
model: (result.model ?? undefined) as Model<Api> | undefined,
|
||||
error: result.error ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureSessionHeader(params: {
|
||||
sessionFile: string;
|
||||
sessionId: string;
|
||||
cwd: string;
|
||||
provider: string;
|
||||
modelId: string;
|
||||
thinkingLevel: ThinkingLevel;
|
||||
}) {
|
||||
const file = params.sessionFile;
|
||||
try {
|
||||
await fs.stat(file);
|
||||
return;
|
||||
} catch {
|
||||
// create
|
||||
}
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
const entry = {
|
||||
type: "session",
|
||||
id: params.sessionId,
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: params.cwd,
|
||||
provider: params.provider,
|
||||
modelId: params.modelId,
|
||||
thinkingLevel: params.thinkingLevel,
|
||||
};
|
||||
await fs.writeFile(file, `${JSON.stringify(entry)}\n`, "utf-8");
|
||||
}
|
||||
|
||||
const defaultApiKey = defaultGetApiKey();
|
||||
|
||||
async function getApiKeyForModel(model: { provider: string }): Promise<string> {
|
||||
if (model.provider === "anthropic") {
|
||||
const oauthEnv = process.env.ANTHROPIC_OAUTH_TOKEN;
|
||||
if (oauthEnv?.trim()) return oauthEnv.trim();
|
||||
}
|
||||
const key = await defaultApiKey(model as unknown as Model<Api>);
|
||||
if (key) return key;
|
||||
throw new Error(`No API key found for provider "${model.provider}"`);
|
||||
}
|
||||
|
||||
type ContentBlock = AgentToolResult<unknown>["content"][number];
|
||||
|
||||
type ContextFile = { path: string; content: string };
|
||||
|
||||
async function sanitizeSessionMessagesImages(
|
||||
messages: AppMessage[],
|
||||
label: string,
|
||||
): Promise<AppMessage[]> {
|
||||
// We sanitize historical session messages because Anthropic can reject a request
|
||||
// if the transcript contains oversized base64 images (see MAX_IMAGE_DIMENSION_PX).
|
||||
const out: AppMessage[] = [];
|
||||
for (const msg of messages) {
|
||||
if (!msg || typeof msg !== "object") {
|
||||
out.push(msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
const role = (msg as { role?: unknown }).role;
|
||||
if (role === "toolResult") {
|
||||
const toolMsg = msg as Extract<AppMessage, { role: "toolResult" }>;
|
||||
const content = Array.isArray(toolMsg.content) ? toolMsg.content : [];
|
||||
const nextContent = (await sanitizeContentBlocksImages(
|
||||
content as ContentBlock[],
|
||||
label,
|
||||
)) as unknown as typeof toolMsg.content;
|
||||
out.push({ ...toolMsg, content: nextContent });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (role === "user") {
|
||||
const userMsg = msg as Extract<AppMessage, { role: "user" }>;
|
||||
const content = userMsg.content;
|
||||
if (Array.isArray(content)) {
|
||||
const nextContent = (await sanitizeContentBlocksImages(
|
||||
content as unknown as ContentBlock[],
|
||||
label,
|
||||
)) as unknown as typeof userMsg.content;
|
||||
out.push({ ...userMsg, content: nextContent });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
out.push(msg);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildBootstrapContextFiles(
|
||||
files: WorkspaceBootstrapFile[],
|
||||
): ContextFile[] {
|
||||
return files.map((file) => ({
|
||||
path: file.name,
|
||||
content: file.missing
|
||||
? `[MISSING] Expected at: ${file.path}`
|
||||
: (file.content ?? ""),
|
||||
}));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
function formatAssistantErrorText(msg: AssistantMessage): string | undefined {
|
||||
if (msg.stopReason !== "error") return undefined;
|
||||
const raw = (msg.errorMessage ?? "").trim();
|
||||
if (!raw) return "LLM request failed with an unknown error.";
|
||||
|
||||
const invalidRequest = raw.match(
|
||||
/"type":"invalid_request_error".*?"message":"([^"]+)"/,
|
||||
);
|
||||
if (invalidRequest?.[1]) {
|
||||
return `LLM request rejected: ${invalidRequest[1]}`;
|
||||
}
|
||||
|
||||
// Keep it short for WhatsApp.
|
||||
return raw.length > 600 ? `${raw.slice(0, 600)}…` : raw;
|
||||
}
|
||||
|
||||
export async function runEmbeddedPiAgent(params: {
|
||||
sessionId: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
config?: ClawdisConfig;
|
||||
skillsSnapshot?: SkillSnapshot;
|
||||
prompt: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
thinkLevel?: ThinkLevel;
|
||||
verboseLevel?: VerboseLevel;
|
||||
timeoutMs: number;
|
||||
runId: string;
|
||||
abortSignal?: AbortSignal;
|
||||
shouldEmitToolResult?: () => boolean;
|
||||
onPartialReply?: (payload: {
|
||||
text?: string;
|
||||
mediaUrls?: string[];
|
||||
}) => void | Promise<void>;
|
||||
onToolResult?: (payload: {
|
||||
text?: string;
|
||||
mediaUrls?: string[];
|
||||
}) => void | Promise<void>;
|
||||
onAgentEvent?: (evt: {
|
||||
stream: string;
|
||||
data: Record<string, unknown>;
|
||||
}) => void;
|
||||
enqueue?: typeof enqueueCommand;
|
||||
}): Promise<EmbeddedPiRunResult> {
|
||||
const enqueue = params.enqueue ?? enqueueCommand;
|
||||
return enqueue(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;
|
||||
const agentDir =
|
||||
process.env.PI_CODING_AGENT_DIR ??
|
||||
path.join(os.homedir(), ".pi", "agent");
|
||||
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,
|
||||
provider,
|
||||
modelId,
|
||||
thinkingLevel,
|
||||
});
|
||||
|
||||
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();
|
||||
const systemPrompt = buildSystemPrompt({
|
||||
appendPrompt: buildAgentSystemPromptAppend({
|
||||
workspaceDir: resolvedWorkspace,
|
||||
defaultThinkLevel: params.thinkLevel,
|
||||
}),
|
||||
contextFiles,
|
||||
skills: promptSkills,
|
||||
cwd: resolvedWorkspace,
|
||||
});
|
||||
|
||||
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: getApiKeyForModel,
|
||||
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);
|
||||
|
||||
const assistantTexts: string[] = [];
|
||||
const toolDebouncer = createToolDebouncer((toolName, metas) => {
|
||||
if (!params.onPartialReply) return;
|
||||
const text = formatToolAggregate(toolName, metas);
|
||||
const { text: cleanedText, mediaUrls } = splitMediaFromOutput(text);
|
||||
void params.onPartialReply({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
const toolMetas: Array<{ toolName?: string; meta?: string }> = [];
|
||||
const toolMetaById = new Map<string, string | undefined>();
|
||||
let deltaBuffer = "";
|
||||
let lastStreamedAssistant: string | undefined;
|
||||
let aborted = Boolean(params.abortSignal?.aborted);
|
||||
|
||||
const unsubscribe = session.subscribe(
|
||||
(evt: AgentEvent | { type: string; [k: string]: unknown }) => {
|
||||
if (evt.type === "tool_execution_start") {
|
||||
const toolName = String(
|
||||
(evt as AgentEvent & { toolName: string }).toolName,
|
||||
);
|
||||
const toolCallId = String(
|
||||
(evt as AgentEvent & { toolCallId: string }).toolCallId,
|
||||
);
|
||||
const args = (evt as AgentEvent & { args: unknown }).args;
|
||||
const meta = inferToolMetaFromArgs(toolName, args);
|
||||
toolMetaById.set(toolCallId, meta);
|
||||
|
||||
emitAgentEvent({
|
||||
runId: params.runId,
|
||||
stream: "tool",
|
||||
data: {
|
||||
phase: "start",
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
args: args as Record<string, unknown>,
|
||||
},
|
||||
});
|
||||
params.onAgentEvent?.({
|
||||
stream: "tool",
|
||||
data: { phase: "start", name: toolName, toolCallId },
|
||||
});
|
||||
}
|
||||
|
||||
if (evt.type === "tool_execution_end") {
|
||||
const toolName = String(
|
||||
(evt as AgentEvent & { toolName: string }).toolName,
|
||||
);
|
||||
const toolCallId = String(
|
||||
(evt as AgentEvent & { toolCallId: string }).toolCallId,
|
||||
);
|
||||
const isError = Boolean(
|
||||
(evt as AgentEvent & { isError: boolean }).isError,
|
||||
);
|
||||
const meta = toolMetaById.get(toolCallId);
|
||||
toolMetas.push({ toolName, meta });
|
||||
toolDebouncer.push(toolName, meta);
|
||||
|
||||
emitAgentEvent({
|
||||
runId: params.runId,
|
||||
stream: "tool",
|
||||
data: {
|
||||
phase: "result",
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
meta,
|
||||
isError,
|
||||
},
|
||||
});
|
||||
params.onAgentEvent?.({
|
||||
stream: "tool",
|
||||
data: {
|
||||
phase: "result",
|
||||
name: toolName,
|
||||
toolCallId,
|
||||
meta,
|
||||
isError,
|
||||
},
|
||||
});
|
||||
const emitToolResult =
|
||||
typeof params.shouldEmitToolResult === "function"
|
||||
? params.shouldEmitToolResult()
|
||||
: params.verboseLevel === "on";
|
||||
if (emitToolResult && params.onToolResult) {
|
||||
const agg = formatToolAggregate(
|
||||
toolName,
|
||||
meta ? [meta] : undefined,
|
||||
);
|
||||
const { text: cleanedText, mediaUrls } =
|
||||
splitMediaFromOutput(agg);
|
||||
if (cleanedText || (mediaUrls && mediaUrls.length > 0)) {
|
||||
try {
|
||||
void params.onToolResult({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
});
|
||||
} catch {
|
||||
// ignore tool result delivery failures
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evt.type === "message_update") {
|
||||
const msg = (evt as AgentEvent & { message: AppMessage }).message;
|
||||
if (msg?.role === "assistant") {
|
||||
const assistantEvent = (
|
||||
evt as AgentEvent & { assistantMessageEvent?: unknown }
|
||||
).assistantMessageEvent;
|
||||
const assistantRecord =
|
||||
assistantEvent && typeof assistantEvent === "object"
|
||||
? (assistantEvent as Record<string, unknown>)
|
||||
: undefined;
|
||||
const evtType =
|
||||
typeof assistantRecord?.type === "string"
|
||||
? assistantRecord.type
|
||||
: "";
|
||||
if (
|
||||
evtType === "text_delta" ||
|
||||
evtType === "text_start" ||
|
||||
evtType === "text_end"
|
||||
) {
|
||||
const chunk =
|
||||
typeof assistantRecord?.delta === "string"
|
||||
? assistantRecord.delta
|
||||
: typeof assistantRecord?.content === "string"
|
||||
? assistantRecord.content
|
||||
: "";
|
||||
if (chunk) {
|
||||
deltaBuffer += chunk;
|
||||
const next = deltaBuffer.trim();
|
||||
if (next && next !== lastStreamedAssistant) {
|
||||
lastStreamedAssistant = next;
|
||||
const { text: cleanedText, mediaUrls } =
|
||||
splitMediaFromOutput(next);
|
||||
emitAgentEvent({
|
||||
runId: params.runId,
|
||||
stream: "assistant",
|
||||
data: {
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
},
|
||||
});
|
||||
params.onAgentEvent?.({
|
||||
stream: "assistant",
|
||||
data: {
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
},
|
||||
});
|
||||
if (params.onPartialReply) {
|
||||
void params.onPartialReply({
|
||||
text: cleanedText,
|
||||
mediaUrls: mediaUrls?.length ? mediaUrls : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evt.type === "message_end") {
|
||||
const msg = (evt as AgentEvent & { message: AppMessage }).message;
|
||||
if (msg?.role === "assistant") {
|
||||
const text = extractAssistantText(msg as AssistantMessage);
|
||||
if (text) assistantTexts.push(text);
|
||||
deltaBuffer = "";
|
||||
}
|
||||
}
|
||||
|
||||
if (evt.type === "agent_end") {
|
||||
toolDebouncer.flush();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
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 = null;
|
||||
try {
|
||||
try {
|
||||
await session.prompt(params.prompt);
|
||||
} catch (err) {
|
||||
promptError = err;
|
||||
} finally {
|
||||
messagesSnapshot = session.messages.slice();
|
||||
sessionIdUsed = session.sessionId;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(abortTimer);
|
||||
unsubscribe();
|
||||
toolDebouncer.flush();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
export type {
|
||||
EmbeddedPiAgentMeta,
|
||||
EmbeddedPiRunMeta,
|
||||
EmbeddedPiRunResult,
|
||||
} from "./pi-embedded-runner.js";
|
||||
export {
|
||||
queueEmbeddedPiMessage,
|
||||
runEmbeddedPiAgent,
|
||||
} from "./pi-embedded-runner.js";
|
||||
|
||||
@@ -35,8 +35,8 @@ import { normalizeE164 } from "../utils.js";
|
||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
||||
import {
|
||||
parseActivationCommand,
|
||||
normalizeGroupActivation,
|
||||
parseActivationCommand,
|
||||
} from "./group-activation.js";
|
||||
import { buildStatusMessage } from "./status.js";
|
||||
import type { MsgContext, TemplateContext } from "./templating.js";
|
||||
@@ -46,9 +46,9 @@ import {
|
||||
type ThinkLevel,
|
||||
type VerboseLevel,
|
||||
} from "./thinking.js";
|
||||
import { SILENT_REPLY_TOKEN } from "./tokens.js";
|
||||
import { isAudio, transcribeInboundAudio } from "./transcription.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "./types.js";
|
||||
import { SILENT_REPLY_TOKEN } from "./tokens.js";
|
||||
|
||||
export type { GetReplyOptions, ReplyPayload } from "./types.js";
|
||||
|
||||
|
||||
@@ -181,10 +181,9 @@ export function buildStatusMessage(args: StatusArgs): string {
|
||||
.filter(Boolean)
|
||||
.join(" • ");
|
||||
|
||||
const groupActivationLine =
|
||||
args.sessionKey?.startsWith("group:")
|
||||
? `Group activation: ${entry?.groupActivation ?? "mention"}`
|
||||
: undefined;
|
||||
const groupActivationLine = args.sessionKey?.startsWith("group:")
|
||||
? `Group activation: ${entry?.groupActivation ?? "mention"}`
|
||||
: undefined;
|
||||
|
||||
const contextLine = `Context: ${formatTokens(
|
||||
totalTokens,
|
||||
|
||||
@@ -71,12 +71,12 @@ import {
|
||||
type ResponseFrame,
|
||||
ResponseFrameSchema,
|
||||
SendParamsSchema,
|
||||
type SessionsListParams,
|
||||
SessionsListParamsSchema,
|
||||
type SessionsCompactParams,
|
||||
SessionsCompactParamsSchema,
|
||||
type SessionsDeleteParams,
|
||||
SessionsDeleteParamsSchema,
|
||||
type SessionsListParams,
|
||||
SessionsListParamsSchema,
|
||||
type SessionsPatchParams,
|
||||
SessionsPatchParamsSchema,
|
||||
type SessionsResetParams,
|
||||
|
||||
@@ -292,7 +292,11 @@ export const SessionsPatchParamsSchema = Type.Object(
|
||||
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||
groupActivation: Type.Optional(
|
||||
Type.Union([Type.Literal("mention"), Type.Literal("always"), Type.Null()]),
|
||||
Type.Union([
|
||||
Type.Literal("mention"),
|
||||
Type.Literal("always"),
|
||||
Type.Null(),
|
||||
]),
|
||||
),
|
||||
syncing: Type.Optional(
|
||||
Type.Union([Type.Boolean(), NonEmptyString, Type.Null()]),
|
||||
|
||||
@@ -3390,14 +3390,16 @@ describe("gateway server", () => {
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-main.jsonl"),
|
||||
Array.from({ length: 10 })
|
||||
.map((_, idx) => JSON.stringify({ role: "user", content: `line ${idx}` }))
|
||||
.join("\n") + "\n",
|
||||
`${Array.from({ length: 10 })
|
||||
.map((_, idx) =>
|
||||
JSON.stringify({ role: "user", content: `line ${idx}` }),
|
||||
)
|
||||
.join("\n")}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(dir, "sess-group.jsonl"),
|
||||
JSON.stringify({ role: "user", content: "group line 0" }) + "\n",
|
||||
`${JSON.stringify({ role: "user", content: "group line 0" })}\n`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
@@ -3532,8 +3534,9 @@ describe("gateway server", () => {
|
||||
.filter((l) => l.trim().length > 0);
|
||||
expect(compactedLines).toHaveLength(3);
|
||||
const filesAfterCompact = await fs.readdir(dir);
|
||||
expect(filesAfterCompact.some((f) => f.startsWith("sess-main.jsonl.bak.")))
|
||||
.toBe(true);
|
||||
expect(
|
||||
filesAfterCompact.some((f) => f.startsWith("sess-main.jsonl.bak.")),
|
||||
).toBe(true);
|
||||
|
||||
const deleted = await rpcReq<{ ok: true; deleted: boolean }>(
|
||||
ws,
|
||||
@@ -3546,17 +3549,19 @@ describe("gateway server", () => {
|
||||
sessions: Array<{ key: string }>;
|
||||
}>(ws, "sessions.list", {});
|
||||
expect(listAfterDelete.ok).toBe(true);
|
||||
expect(listAfterDelete.payload?.sessions.some((s) => s.key === "group:dev"))
|
||||
.toBe(false);
|
||||
expect(
|
||||
listAfterDelete.payload?.sessions.some((s) => s.key === "group:dev"),
|
||||
).toBe(false);
|
||||
const filesAfterDelete = await fs.readdir(dir);
|
||||
expect(filesAfterDelete.some((f) => f.startsWith("sess-group.jsonl.deleted.")))
|
||||
.toBe(true);
|
||||
expect(
|
||||
filesAfterDelete.some((f) => f.startsWith("sess-group.jsonl.deleted.")),
|
||||
).toBe(true);
|
||||
|
||||
const reset = await rpcReq<{ ok: true; key: string; entry: { sessionId: string } }>(
|
||||
ws,
|
||||
"sessions.reset",
|
||||
{ key: "main" },
|
||||
);
|
||||
const reset = await rpcReq<{
|
||||
ok: true;
|
||||
key: string;
|
||||
entry: { sessionId: string };
|
||||
}>(ws, "sessions.reset", { key: "main" });
|
||||
expect(reset.ok).toBe(true);
|
||||
expect(reset.payload?.key).toBe("main");
|
||||
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
|
||||
|
||||
@@ -271,9 +271,9 @@ import {
|
||||
formatValidationErrors,
|
||||
PROTOCOL_VERSION,
|
||||
type RequestFrame,
|
||||
type SessionsListParams,
|
||||
type SessionsCompactParams,
|
||||
type SessionsDeleteParams,
|
||||
type SessionsListParams,
|
||||
type SessionsPatchParams,
|
||||
type SessionsResetParams,
|
||||
type Snapshot,
|
||||
@@ -303,9 +303,9 @@ import {
|
||||
validateProvidersStatusParams,
|
||||
validateRequestFrame,
|
||||
validateSendParams,
|
||||
validateSessionsListParams,
|
||||
validateSessionsCompactParams,
|
||||
validateSessionsDeleteParams,
|
||||
validateSessionsListParams,
|
||||
validateSessionsPatchParams,
|
||||
validateSessionsResetParams,
|
||||
validateSkillsInstallParams,
|
||||
@@ -718,7 +718,6 @@ function readSessionMessages(
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
// pi/tau logs either raw message or wrapper { message }
|
||||
if (parsed?.message) {
|
||||
messages.push(parsed.message);
|
||||
} else if (parsed?.role && parsed?.content) {
|
||||
@@ -2183,8 +2182,10 @@ export async function startGatewayServer(
|
||||
};
|
||||
}
|
||||
|
||||
const filePath = resolveSessionTranscriptCandidates(sessionId, storePath)
|
||||
.find((candidate) => fs.existsSync(candidate));
|
||||
const filePath = resolveSessionTranscriptCandidates(
|
||||
sessionId,
|
||||
storePath,
|
||||
).find((candidate) => fs.existsSync(candidate));
|
||||
if (!filePath) {
|
||||
return {
|
||||
ok: true,
|
||||
|
||||
@@ -19,11 +19,11 @@ import * as commandQueue from "../process/command-queue.js";
|
||||
import {
|
||||
HEARTBEAT_PROMPT,
|
||||
HEARTBEAT_TOKEN,
|
||||
SILENT_REPLY_TOKEN,
|
||||
monitorWebProvider,
|
||||
resolveHeartbeatRecipients,
|
||||
resolveReplyHeartbeatMinutes,
|
||||
runWebHeartbeatOnce,
|
||||
SILENT_REPLY_TOKEN,
|
||||
stripHeartbeatToken,
|
||||
} from "./auto-reply.js";
|
||||
import type { sendMessageWhatsApp } from "./outbound.js";
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { chunkText } from "../auto-reply/chunk.js";
|
||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import {
|
||||
normalizeGroupActivation,
|
||||
parseActivationCommand,
|
||||
} from "../auto-reply/group-activation.js";
|
||||
import {
|
||||
HEARTBEAT_TOKEN,
|
||||
SILENT_REPLY_TOKEN,
|
||||
} from "../auto-reply/tokens.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
import { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import { waitForever } from "../cli/wait.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
@@ -1244,10 +1241,7 @@ export async function monitorWebProvider(
|
||||
|
||||
if (msg.chatType === "group") {
|
||||
noteGroupMember(conversationId, msg.senderE164, msg.senderName);
|
||||
const commandBody = stripMentionsForCommand(
|
||||
msg.body,
|
||||
msg.selfE164,
|
||||
);
|
||||
const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
|
||||
const activationCommand = parseActivationCommand(commandBody);
|
||||
const isOwner = isOwnerSender(msg);
|
||||
const statusCommand = isStatusCommand(commandBody);
|
||||
|
||||
Reference in New Issue
Block a user