fix: bump pi deps and fix lint
This commit is contained in:
@@ -67,9 +67,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||||
"@homebridge/ciao": "^1.3.4",
|
"@homebridge/ciao": "^1.3.4",
|
||||||
"@mariozechner/pi-agent-core": "^0.26.0",
|
"@mariozechner/pi-agent-core": "^0.27.1",
|
||||||
"@mariozechner/pi-ai": "^0.26.0",
|
"@mariozechner/pi-ai": "^0.27.1",
|
||||||
"@mariozechner/pi-coding-agent": "^0.26.0",
|
"@mariozechner/pi-coding-agent": "^0.27.1",
|
||||||
"@sinclair/typebox": "^0.34.41",
|
"@sinclair/typebox": "^0.34.41",
|
||||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||||
"ajv": "^8.17.1",
|
"ajv": "^8.17.1",
|
||||||
|
|||||||
46
pnpm-lock.yaml
generated
46
pnpm-lock.yaml
generated
@@ -15,14 +15,14 @@ importers:
|
|||||||
specifier: ^1.3.4
|
specifier: ^1.3.4
|
||||||
version: 1.3.4
|
version: 1.3.4
|
||||||
'@mariozechner/pi-agent-core':
|
'@mariozechner/pi-agent-core':
|
||||||
specifier: ^0.26.0
|
specifier: ^0.27.1
|
||||||
version: 0.26.1(ws@8.18.3)(zod@4.2.1)
|
version: 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||||
'@mariozechner/pi-ai':
|
'@mariozechner/pi-ai':
|
||||||
specifier: ^0.26.0
|
specifier: ^0.27.1
|
||||||
version: 0.26.1(ws@8.18.3)(zod@4.2.1)
|
version: 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||||
'@mariozechner/pi-coding-agent':
|
'@mariozechner/pi-coding-agent':
|
||||||
specifier: ^0.26.0
|
specifier: ^0.27.1
|
||||||
version: 0.26.1(ws@8.18.3)(zod@4.2.1)
|
version: 0.27.1(ws@8.18.3)(zod@4.2.1)
|
||||||
'@sinclair/typebox':
|
'@sinclair/typebox':
|
||||||
specifier: ^0.34.41
|
specifier: ^0.34.41
|
||||||
version: 0.34.41
|
version: 0.34.41
|
||||||
@@ -659,21 +659,21 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
lit: ^3.3.1
|
lit: ^3.3.1
|
||||||
|
|
||||||
'@mariozechner/pi-agent-core@0.26.1':
|
'@mariozechner/pi-agent-core@0.27.1':
|
||||||
resolution: {integrity: sha512-yH15oPK9l8F2vGrz2mXl0dRydKkw0x4p1WChVuQALqDaFOf48V2XbLS7SvTE3qx095ylNp/Q+RQ+NiB5I2myFA==}
|
resolution: {integrity: sha512-EctMh4kIVc4HqsX6vbqkLqTey6/TmyxFg/vi2GQe03dkHYJkufiSOWp3Kei5emMGNjaNL0kPeJ3H0nwfobsWCg==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@mariozechner/pi-ai@0.26.1':
|
'@mariozechner/pi-ai@0.27.1':
|
||||||
resolution: {integrity: sha512-VEH9kwQoo0N1KtBQnAHDZaIwe0nLwikGytNvjCV3RltQirywwUUsw0xQ/2YUXaN3vl3nqDO/VY1qgdSnVZE5iA==}
|
resolution: {integrity: sha512-Y9eMs1vdHCIzpVZdg4MBMWlz9LKsyXlGDZVShPoSXgYU8Kg3/3DbkBVwBsEA7mTedKS6FDueWv9TYsFPydWMww==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@mariozechner/pi-coding-agent@0.26.1':
|
'@mariozechner/pi-coding-agent@0.27.1':
|
||||||
resolution: {integrity: sha512-o1WOhzwPQTiUBNxlANDXJ9bTOIIpxxkwRh9+nnz9F28uEzkSfTrJLTgJoWxuRAU7Xvj5//pkKYaUPfhCd69R9g==}
|
resolution: {integrity: sha512-dbuXgrz1b0O4GWiqyDuzurxH30PSwdb8gbHGb4NnHxTP8ECwsDbC2ykTQpg12iLBbzwB2Ze0Q+iuvnjoaonuAw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@mariozechner/pi-tui@0.26.1':
|
'@mariozechner/pi-tui@0.27.1':
|
||||||
resolution: {integrity: sha512-qGKS4SwxJw4pinttl3UvzylC1IuB31QpuoM3X36mz/GmLq52RNYnriK4si52GpeTrqNm8vXDpeevI0zhPQPjYw==}
|
resolution: {integrity: sha512-rTVQ031UR/KKJ3/6f/pRLH7Fg/hOqVnXnVEY37J2txBWd6r9uZClAL0RThGlH2xHsB0+qgER2kgiS/Kwf9I76g==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@mistralai/mistralai@1.10.0':
|
'@mistralai/mistralai@1.10.0':
|
||||||
@@ -2991,10 +2991,10 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- tailwindcss
|
- 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:
|
dependencies:
|
||||||
'@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)
|
||||||
'@mariozechner/pi-tui': 0.26.1
|
'@mariozechner/pi-tui': 0.27.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@modelcontextprotocol/sdk'
|
- '@modelcontextprotocol/sdk'
|
||||||
- bufferutil
|
- bufferutil
|
||||||
@@ -3003,7 +3003,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- 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:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.71.2(zod@4.2.1)
|
'@anthropic-ai/sdk': 0.71.2(zod@4.2.1)
|
||||||
'@google/genai': 1.34.0
|
'@google/genai': 1.34.0
|
||||||
@@ -3023,11 +3023,11 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- 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:
|
dependencies:
|
||||||
'@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)
|
||||||
'@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)
|
||||||
'@mariozechner/pi-tui': 0.26.1
|
'@mariozechner/pi-tui': 0.27.1
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
cli-highlight: 2.1.11
|
cli-highlight: 2.1.11
|
||||||
diff: 8.0.2
|
diff: 8.0.2
|
||||||
@@ -3042,7 +3042,7 @@ snapshots:
|
|||||||
- ws
|
- ws
|
||||||
- zod
|
- zod
|
||||||
|
|
||||||
'@mariozechner/pi-tui@0.26.1':
|
'@mariozechner/pi-tui@0.27.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/mime-types': 2.1.4
|
'@types/mime-types': 2.1.4
|
||||||
chalk: 5.6.2
|
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";
|
export type {
|
||||||
import os from "node:os";
|
EmbeddedPiAgentMeta,
|
||||||
import path from "node:path";
|
EmbeddedPiRunMeta,
|
||||||
|
EmbeddedPiRunResult,
|
||||||
import type {
|
} from "./pi-embedded-runner.js";
|
||||||
AgentEvent,
|
export {
|
||||||
AppMessage,
|
queueEmbeddedPiMessage,
|
||||||
ThinkingLevel,
|
runEmbeddedPiAgent,
|
||||||
} from "@mariozechner/pi-agent-core";
|
} from "./pi-embedded-runner.js";
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ import { normalizeE164 } from "../utils.js";
|
|||||||
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
import { resolveHeartbeatSeconds } from "../web/reconnect.js";
|
||||||
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
import { getWebAuthAgeMs, webAuthExists } from "../web/session.js";
|
||||||
import {
|
import {
|
||||||
parseActivationCommand,
|
|
||||||
normalizeGroupActivation,
|
normalizeGroupActivation,
|
||||||
|
parseActivationCommand,
|
||||||
} from "./group-activation.js";
|
} from "./group-activation.js";
|
||||||
import { buildStatusMessage } from "./status.js";
|
import { buildStatusMessage } from "./status.js";
|
||||||
import type { MsgContext, TemplateContext } from "./templating.js";
|
import type { MsgContext, TemplateContext } from "./templating.js";
|
||||||
@@ -46,9 +46,9 @@ import {
|
|||||||
type ThinkLevel,
|
type ThinkLevel,
|
||||||
type VerboseLevel,
|
type VerboseLevel,
|
||||||
} from "./thinking.js";
|
} from "./thinking.js";
|
||||||
|
import { SILENT_REPLY_TOKEN } from "./tokens.js";
|
||||||
import { isAudio, transcribeInboundAudio } from "./transcription.js";
|
import { isAudio, transcribeInboundAudio } from "./transcription.js";
|
||||||
import type { GetReplyOptions, ReplyPayload } from "./types.js";
|
import type { GetReplyOptions, ReplyPayload } from "./types.js";
|
||||||
import { SILENT_REPLY_TOKEN } from "./tokens.js";
|
|
||||||
|
|
||||||
export type { GetReplyOptions, ReplyPayload } from "./types.js";
|
export type { GetReplyOptions, ReplyPayload } from "./types.js";
|
||||||
|
|
||||||
|
|||||||
@@ -181,10 +181,9 @@ export function buildStatusMessage(args: StatusArgs): string {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(" • ");
|
.join(" • ");
|
||||||
|
|
||||||
const groupActivationLine =
|
const groupActivationLine = args.sessionKey?.startsWith("group:")
|
||||||
args.sessionKey?.startsWith("group:")
|
? `Group activation: ${entry?.groupActivation ?? "mention"}`
|
||||||
? `Group activation: ${entry?.groupActivation ?? "mention"}`
|
: undefined;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const contextLine = `Context: ${formatTokens(
|
const contextLine = `Context: ${formatTokens(
|
||||||
totalTokens,
|
totalTokens,
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ import {
|
|||||||
type ResponseFrame,
|
type ResponseFrame,
|
||||||
ResponseFrameSchema,
|
ResponseFrameSchema,
|
||||||
SendParamsSchema,
|
SendParamsSchema,
|
||||||
type SessionsListParams,
|
|
||||||
SessionsListParamsSchema,
|
|
||||||
type SessionsCompactParams,
|
type SessionsCompactParams,
|
||||||
SessionsCompactParamsSchema,
|
SessionsCompactParamsSchema,
|
||||||
type SessionsDeleteParams,
|
type SessionsDeleteParams,
|
||||||
SessionsDeleteParamsSchema,
|
SessionsDeleteParamsSchema,
|
||||||
|
type SessionsListParams,
|
||||||
|
SessionsListParamsSchema,
|
||||||
type SessionsPatchParams,
|
type SessionsPatchParams,
|
||||||
SessionsPatchParamsSchema,
|
SessionsPatchParamsSchema,
|
||||||
type SessionsResetParams,
|
type SessionsResetParams,
|
||||||
|
|||||||
@@ -292,7 +292,11 @@ export const SessionsPatchParamsSchema = Type.Object(
|
|||||||
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
thinkingLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
verboseLevel: Type.Optional(Type.Union([NonEmptyString, Type.Null()])),
|
||||||
groupActivation: Type.Optional(
|
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(
|
syncing: Type.Optional(
|
||||||
Type.Union([Type.Boolean(), NonEmptyString, Type.Null()]),
|
Type.Union([Type.Boolean(), NonEmptyString, Type.Null()]),
|
||||||
|
|||||||
@@ -3390,14 +3390,16 @@ describe("gateway server", () => {
|
|||||||
|
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(dir, "sess-main.jsonl"),
|
path.join(dir, "sess-main.jsonl"),
|
||||||
Array.from({ length: 10 })
|
`${Array.from({ length: 10 })
|
||||||
.map((_, idx) => JSON.stringify({ role: "user", content: `line ${idx}` }))
|
.map((_, idx) =>
|
||||||
.join("\n") + "\n",
|
JSON.stringify({ role: "user", content: `line ${idx}` }),
|
||||||
|
)
|
||||||
|
.join("\n")}\n`,
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
path.join(dir, "sess-group.jsonl"),
|
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",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3532,8 +3534,9 @@ describe("gateway server", () => {
|
|||||||
.filter((l) => l.trim().length > 0);
|
.filter((l) => l.trim().length > 0);
|
||||||
expect(compactedLines).toHaveLength(3);
|
expect(compactedLines).toHaveLength(3);
|
||||||
const filesAfterCompact = await fs.readdir(dir);
|
const filesAfterCompact = await fs.readdir(dir);
|
||||||
expect(filesAfterCompact.some((f) => f.startsWith("sess-main.jsonl.bak.")))
|
expect(
|
||||||
.toBe(true);
|
filesAfterCompact.some((f) => f.startsWith("sess-main.jsonl.bak.")),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
const deleted = await rpcReq<{ ok: true; deleted: boolean }>(
|
const deleted = await rpcReq<{ ok: true; deleted: boolean }>(
|
||||||
ws,
|
ws,
|
||||||
@@ -3546,17 +3549,19 @@ describe("gateway server", () => {
|
|||||||
sessions: Array<{ key: string }>;
|
sessions: Array<{ key: string }>;
|
||||||
}>(ws, "sessions.list", {});
|
}>(ws, "sessions.list", {});
|
||||||
expect(listAfterDelete.ok).toBe(true);
|
expect(listAfterDelete.ok).toBe(true);
|
||||||
expect(listAfterDelete.payload?.sessions.some((s) => s.key === "group:dev"))
|
expect(
|
||||||
.toBe(false);
|
listAfterDelete.payload?.sessions.some((s) => s.key === "group:dev"),
|
||||||
|
).toBe(false);
|
||||||
const filesAfterDelete = await fs.readdir(dir);
|
const filesAfterDelete = await fs.readdir(dir);
|
||||||
expect(filesAfterDelete.some((f) => f.startsWith("sess-group.jsonl.deleted.")))
|
expect(
|
||||||
.toBe(true);
|
filesAfterDelete.some((f) => f.startsWith("sess-group.jsonl.deleted.")),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
const reset = await rpcReq<{ ok: true; key: string; entry: { sessionId: string } }>(
|
const reset = await rpcReq<{
|
||||||
ws,
|
ok: true;
|
||||||
"sessions.reset",
|
key: string;
|
||||||
{ key: "main" },
|
entry: { sessionId: string };
|
||||||
);
|
}>(ws, "sessions.reset", { key: "main" });
|
||||||
expect(reset.ok).toBe(true);
|
expect(reset.ok).toBe(true);
|
||||||
expect(reset.payload?.key).toBe("main");
|
expect(reset.payload?.key).toBe("main");
|
||||||
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
|
expect(reset.payload?.entry.sessionId).not.toBe("sess-main");
|
||||||
|
|||||||
@@ -271,9 +271,9 @@ import {
|
|||||||
formatValidationErrors,
|
formatValidationErrors,
|
||||||
PROTOCOL_VERSION,
|
PROTOCOL_VERSION,
|
||||||
type RequestFrame,
|
type RequestFrame,
|
||||||
type SessionsListParams,
|
|
||||||
type SessionsCompactParams,
|
type SessionsCompactParams,
|
||||||
type SessionsDeleteParams,
|
type SessionsDeleteParams,
|
||||||
|
type SessionsListParams,
|
||||||
type SessionsPatchParams,
|
type SessionsPatchParams,
|
||||||
type SessionsResetParams,
|
type SessionsResetParams,
|
||||||
type Snapshot,
|
type Snapshot,
|
||||||
@@ -303,9 +303,9 @@ import {
|
|||||||
validateProvidersStatusParams,
|
validateProvidersStatusParams,
|
||||||
validateRequestFrame,
|
validateRequestFrame,
|
||||||
validateSendParams,
|
validateSendParams,
|
||||||
validateSessionsListParams,
|
|
||||||
validateSessionsCompactParams,
|
validateSessionsCompactParams,
|
||||||
validateSessionsDeleteParams,
|
validateSessionsDeleteParams,
|
||||||
|
validateSessionsListParams,
|
||||||
validateSessionsPatchParams,
|
validateSessionsPatchParams,
|
||||||
validateSessionsResetParams,
|
validateSessionsResetParams,
|
||||||
validateSkillsInstallParams,
|
validateSkillsInstallParams,
|
||||||
@@ -718,7 +718,6 @@ function readSessionMessages(
|
|||||||
if (!line.trim()) continue;
|
if (!line.trim()) continue;
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(line);
|
const parsed = JSON.parse(line);
|
||||||
// pi/tau logs either raw message or wrapper { message }
|
|
||||||
if (parsed?.message) {
|
if (parsed?.message) {
|
||||||
messages.push(parsed.message);
|
messages.push(parsed.message);
|
||||||
} else if (parsed?.role && parsed?.content) {
|
} else if (parsed?.role && parsed?.content) {
|
||||||
@@ -2183,8 +2182,10 @@ export async function startGatewayServer(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = resolveSessionTranscriptCandidates(sessionId, storePath)
|
const filePath = resolveSessionTranscriptCandidates(
|
||||||
.find((candidate) => fs.existsSync(candidate));
|
sessionId,
|
||||||
|
storePath,
|
||||||
|
).find((candidate) => fs.existsSync(candidate));
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ import * as commandQueue from "../process/command-queue.js";
|
|||||||
import {
|
import {
|
||||||
HEARTBEAT_PROMPT,
|
HEARTBEAT_PROMPT,
|
||||||
HEARTBEAT_TOKEN,
|
HEARTBEAT_TOKEN,
|
||||||
SILENT_REPLY_TOKEN,
|
|
||||||
monitorWebProvider,
|
monitorWebProvider,
|
||||||
resolveHeartbeatRecipients,
|
resolveHeartbeatRecipients,
|
||||||
resolveReplyHeartbeatMinutes,
|
resolveReplyHeartbeatMinutes,
|
||||||
runWebHeartbeatOnce,
|
runWebHeartbeatOnce,
|
||||||
|
SILENT_REPLY_TOKEN,
|
||||||
stripHeartbeatToken,
|
stripHeartbeatToken,
|
||||||
} from "./auto-reply.js";
|
} from "./auto-reply.js";
|
||||||
import type { sendMessageWhatsApp } from "./outbound.js";
|
import type { sendMessageWhatsApp } from "./outbound.js";
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText } from "../auto-reply/chunk.js";
|
||||||
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
import { formatAgentEnvelope } from "../auto-reply/envelope.js";
|
||||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
|
||||||
import {
|
import {
|
||||||
normalizeGroupActivation,
|
normalizeGroupActivation,
|
||||||
parseActivationCommand,
|
parseActivationCommand,
|
||||||
} from "../auto-reply/group-activation.js";
|
} from "../auto-reply/group-activation.js";
|
||||||
import {
|
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||||
HEARTBEAT_TOKEN,
|
import { HEARTBEAT_TOKEN, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||||
SILENT_REPLY_TOKEN,
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
} from "../auto-reply/tokens.js";
|
|
||||||
import { waitForever } from "../cli/wait.js";
|
import { waitForever } from "../cli/wait.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
@@ -1244,10 +1241,7 @@ export async function monitorWebProvider(
|
|||||||
|
|
||||||
if (msg.chatType === "group") {
|
if (msg.chatType === "group") {
|
||||||
noteGroupMember(conversationId, msg.senderE164, msg.senderName);
|
noteGroupMember(conversationId, msg.senderE164, msg.senderName);
|
||||||
const commandBody = stripMentionsForCommand(
|
const commandBody = stripMentionsForCommand(msg.body, msg.selfE164);
|
||||||
msg.body,
|
|
||||||
msg.selfE164,
|
|
||||||
);
|
|
||||||
const activationCommand = parseActivationCommand(commandBody);
|
const activationCommand = parseActivationCommand(commandBody);
|
||||||
const isOwner = isOwnerSender(msg);
|
const isOwner = isOwnerSender(msg);
|
||||||
const statusCommand = isStatusCommand(commandBody);
|
const statusCommand = isStatusCommand(commandBody);
|
||||||
|
|||||||
Reference in New Issue
Block a user