327 lines
12 KiB
TypeScript
327 lines
12 KiB
TypeScript
import { resolveSessionAgentIds } from "../../agents/agent-scope.js";
|
|
import { resolveBootstrapMaxChars } from "../../agents/pi-embedded-helpers.js";
|
|
import { createClawdbotCodingTools } from "../../agents/pi-tools.js";
|
|
import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js";
|
|
import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js";
|
|
import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
|
|
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
|
|
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
|
|
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
|
|
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
|
|
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
|
|
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
|
|
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
|
|
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
|
import type { ReplyPayload } from "../types.js";
|
|
import type { HandleCommandsParams } from "./commands-types.js";
|
|
|
|
function estimateTokensFromChars(chars: number): number {
|
|
return Math.ceil(Math.max(0, chars) / 4);
|
|
}
|
|
|
|
function formatInt(n: number): string {
|
|
return new Intl.NumberFormat("en-US").format(n);
|
|
}
|
|
|
|
function formatCharsAndTokens(chars: number): string {
|
|
return `${formatInt(chars)} chars (~${formatInt(estimateTokensFromChars(chars))} tok)`;
|
|
}
|
|
|
|
function parseContextArgs(commandBodyNormalized: string): string {
|
|
if (commandBodyNormalized === "/context") return "";
|
|
if (commandBodyNormalized.startsWith("/context ")) return commandBodyNormalized.slice(8).trim();
|
|
return "";
|
|
}
|
|
|
|
function formatListTop(
|
|
entries: Array<{ name: string; value: number }>,
|
|
cap: number,
|
|
): { lines: string[]; omitted: number } {
|
|
const sorted = [...entries].sort((a, b) => b.value - a.value);
|
|
const top = sorted.slice(0, cap);
|
|
const omitted = Math.max(0, sorted.length - top.length);
|
|
const lines = top.map((e) => `- ${e.name}: ${formatCharsAndTokens(e.value)}`);
|
|
return { lines, omitted };
|
|
}
|
|
|
|
async function resolveContextReport(
|
|
params: HandleCommandsParams,
|
|
): Promise<SessionSystemPromptReport> {
|
|
const existing = params.sessionEntry?.systemPromptReport;
|
|
if (existing && existing.source === "run") return existing;
|
|
|
|
const workspaceDir = params.workspaceDir;
|
|
const bootstrapMaxChars = resolveBootstrapMaxChars(params.cfg);
|
|
const { bootstrapFiles, contextFiles: injectedFiles } = await resolveBootstrapContextForRun({
|
|
workspaceDir,
|
|
config: params.cfg,
|
|
sessionKey: params.sessionKey,
|
|
sessionId: params.sessionEntry?.sessionId,
|
|
});
|
|
const skillsSnapshot = (() => {
|
|
try {
|
|
return buildWorkspaceSkillSnapshot(workspaceDir, {
|
|
config: params.cfg,
|
|
eligibility: { remote: getRemoteSkillEligibility() },
|
|
snapshotVersion: getSkillsSnapshotVersion(workspaceDir),
|
|
});
|
|
} catch {
|
|
return { prompt: "", skills: [], resolvedSkills: [] };
|
|
}
|
|
})();
|
|
const skillsPrompt = skillsSnapshot.prompt ?? "";
|
|
const sandboxRuntime = resolveSandboxRuntimeStatus({
|
|
cfg: params.cfg,
|
|
sessionKey: params.ctx.SessionKey ?? params.sessionKey,
|
|
});
|
|
const tools = (() => {
|
|
try {
|
|
return createClawdbotCodingTools({
|
|
config: params.cfg,
|
|
workspaceDir,
|
|
sessionKey: params.sessionKey,
|
|
messageProvider: params.command.channel,
|
|
groupId: params.sessionEntry?.groupId ?? undefined,
|
|
groupChannel: params.sessionEntry?.groupChannel ?? undefined,
|
|
groupSpace: params.sessionEntry?.space ?? undefined,
|
|
spawnedBy: params.sessionEntry?.spawnedBy ?? undefined,
|
|
modelProvider: params.provider,
|
|
modelId: params.model,
|
|
});
|
|
} catch {
|
|
return [];
|
|
}
|
|
})();
|
|
const toolSummaries = buildToolSummaryMap(tools);
|
|
const toolNames = tools.map((t) => t.name);
|
|
const { sessionAgentId } = resolveSessionAgentIds({
|
|
sessionKey: params.sessionKey,
|
|
config: params.cfg,
|
|
});
|
|
const defaultModelRef = resolveDefaultModelForAgent({
|
|
cfg: params.cfg,
|
|
agentId: sessionAgentId,
|
|
});
|
|
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
|
|
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
|
|
config: params.cfg,
|
|
agentId: sessionAgentId,
|
|
workspaceDir,
|
|
cwd: process.cwd(),
|
|
runtime: {
|
|
host: "unknown",
|
|
os: "unknown",
|
|
arch: "unknown",
|
|
node: process.version,
|
|
model: `${params.provider}/${params.model}`,
|
|
defaultModel: defaultModelLabel,
|
|
},
|
|
});
|
|
const sandboxInfo = sandboxRuntime.sandboxed
|
|
? {
|
|
enabled: true,
|
|
workspaceDir,
|
|
workspaceAccess: "rw" as const,
|
|
elevated: {
|
|
allowed: params.elevated.allowed,
|
|
defaultLevel: (params.resolvedElevatedLevel ?? "off") as "on" | "off" | "ask" | "full",
|
|
},
|
|
}
|
|
: { enabled: false };
|
|
|
|
const systemPrompt = buildAgentSystemPrompt({
|
|
workspaceDir,
|
|
defaultThinkLevel: params.resolvedThinkLevel,
|
|
reasoningLevel: params.resolvedReasoningLevel,
|
|
extraSystemPrompt: undefined,
|
|
ownerNumbers: undefined,
|
|
reasoningTagHint: false,
|
|
toolNames,
|
|
toolSummaries,
|
|
modelAliasLines: [],
|
|
userTimezone,
|
|
userTime,
|
|
userTimeFormat,
|
|
contextFiles: injectedFiles,
|
|
skillsPrompt,
|
|
heartbeatPrompt: undefined,
|
|
runtimeInfo,
|
|
sandboxInfo,
|
|
});
|
|
|
|
return buildSystemPromptReport({
|
|
source: "estimate",
|
|
generatedAt: Date.now(),
|
|
sessionId: params.sessionEntry?.sessionId,
|
|
sessionKey: params.sessionKey,
|
|
provider: params.provider,
|
|
model: params.model,
|
|
workspaceDir,
|
|
bootstrapMaxChars,
|
|
sandbox: { mode: sandboxRuntime.mode, sandboxed: sandboxRuntime.sandboxed },
|
|
systemPrompt,
|
|
bootstrapFiles,
|
|
injectedFiles,
|
|
skillsPrompt,
|
|
tools,
|
|
});
|
|
}
|
|
|
|
export async function buildContextReply(params: HandleCommandsParams): Promise<ReplyPayload> {
|
|
const args = parseContextArgs(params.command.commandBodyNormalized);
|
|
const sub = args.split(/\s+/).filter(Boolean)[0]?.toLowerCase() ?? "";
|
|
|
|
if (!sub || sub === "help") {
|
|
return {
|
|
text: [
|
|
"🧠 /context",
|
|
"",
|
|
"What counts as context (high-level), plus a breakdown mode.",
|
|
"",
|
|
"Try:",
|
|
"- /context list (short breakdown)",
|
|
"- /context detail (per-file + per-tool + per-skill + system prompt size)",
|
|
"- /context json (same, machine-readable)",
|
|
"",
|
|
"Inline shortcut = a command token inside a normal message (e.g. “hey /status”). It runs immediately (allowlisted senders only) and is stripped before the model sees the remaining text.",
|
|
].join("\n"),
|
|
};
|
|
}
|
|
|
|
const report = await resolveContextReport(params);
|
|
const session = {
|
|
totalTokens: params.sessionEntry?.totalTokens ?? null,
|
|
inputTokens: params.sessionEntry?.inputTokens ?? null,
|
|
outputTokens: params.sessionEntry?.outputTokens ?? null,
|
|
contextTokens: params.contextTokens ?? null,
|
|
} as const;
|
|
|
|
if (sub === "json") {
|
|
return { text: JSON.stringify({ report, session }, null, 2) };
|
|
}
|
|
|
|
if (sub !== "list" && sub !== "show" && sub !== "detail" && sub !== "deep") {
|
|
return {
|
|
text: [
|
|
"Unknown /context mode.",
|
|
"Use: /context, /context list, /context detail, or /context json",
|
|
].join("\n"),
|
|
};
|
|
}
|
|
|
|
const fileLines = report.injectedWorkspaceFiles.map((f) => {
|
|
const status = f.missing ? "MISSING" : f.truncated ? "TRUNCATED" : "OK";
|
|
const raw = f.missing ? "0" : formatCharsAndTokens(f.rawChars);
|
|
const injected = f.missing ? "0" : formatCharsAndTokens(f.injectedChars);
|
|
return `- ${f.name}: ${status} | raw ${raw} | injected ${injected}`;
|
|
});
|
|
|
|
const sandboxLine = `Sandbox: mode=${report.sandbox?.mode ?? "unknown"} sandboxed=${report.sandbox?.sandboxed ?? false}`;
|
|
const toolSchemaLine = `Tool schemas (JSON): ${formatCharsAndTokens(report.tools.schemaChars)} (counts toward context; not shown as text)`;
|
|
const toolListLine = `Tool list (system prompt text): ${formatCharsAndTokens(report.tools.listChars)}`;
|
|
const skillNameSet = new Set(report.skills.entries.map((s) => s.name));
|
|
const skillNames = Array.from(skillNameSet);
|
|
const toolNames = report.tools.entries.map((t) => t.name);
|
|
const formatNameList = (names: string[], cap: number) =>
|
|
names.length <= cap
|
|
? names.join(", ")
|
|
: `${names.slice(0, cap).join(", ")}, … (+${names.length - cap} more)`;
|
|
const skillsLine = `Skills list (system prompt text): ${formatCharsAndTokens(report.skills.promptChars)} (${skillNameSet.size} skills)`;
|
|
const skillsNamesLine = skillNameSet.size
|
|
? `Skills: ${formatNameList(skillNames, 20)}`
|
|
: "Skills: (none)";
|
|
const toolsNamesLine = toolNames.length
|
|
? `Tools: ${formatNameList(toolNames, 30)}`
|
|
: "Tools: (none)";
|
|
const systemPromptLine = `System prompt (${report.source}): ${formatCharsAndTokens(report.systemPrompt.chars)} (Project Context ${formatCharsAndTokens(report.systemPrompt.projectContextChars)})`;
|
|
const workspaceLabel = report.workspaceDir ?? params.workspaceDir;
|
|
const bootstrapMaxLabel =
|
|
typeof report.bootstrapMaxChars === "number"
|
|
? `${formatInt(report.bootstrapMaxChars)} chars`
|
|
: "? chars";
|
|
|
|
const totalsLine =
|
|
session.totalTokens != null
|
|
? `Session tokens (cached): ${formatInt(session.totalTokens)} total / ctx=${session.contextTokens ?? "?"}`
|
|
: `Session tokens (cached): unknown / ctx=${session.contextTokens ?? "?"}`;
|
|
|
|
if (sub === "detail" || sub === "deep") {
|
|
const perSkill = formatListTop(
|
|
report.skills.entries.map((s) => ({ name: s.name, value: s.blockChars })),
|
|
30,
|
|
);
|
|
const perToolSchema = formatListTop(
|
|
report.tools.entries.map((t) => ({ name: t.name, value: t.schemaChars })),
|
|
30,
|
|
);
|
|
const perToolSummary = formatListTop(
|
|
report.tools.entries.map((t) => ({ name: t.name, value: t.summaryChars })),
|
|
30,
|
|
);
|
|
const toolPropsLines = report.tools.entries
|
|
.filter((t) => t.propertiesCount != null)
|
|
.sort((a, b) => (b.propertiesCount ?? 0) - (a.propertiesCount ?? 0))
|
|
.slice(0, 30)
|
|
.map((t) => `- ${t.name}: ${t.propertiesCount} params`);
|
|
|
|
return {
|
|
text: [
|
|
"🧠 Context breakdown (detailed)",
|
|
`Workspace: ${workspaceLabel}`,
|
|
`Bootstrap max/file: ${bootstrapMaxLabel}`,
|
|
sandboxLine,
|
|
systemPromptLine,
|
|
"",
|
|
"Injected workspace files:",
|
|
...fileLines,
|
|
"",
|
|
skillsLine,
|
|
skillsNamesLine,
|
|
...(perSkill.lines.length ? ["Top skills (prompt entry size):", ...perSkill.lines] : []),
|
|
...(perSkill.omitted ? [`… (+${perSkill.omitted} more skills)`] : []),
|
|
"",
|
|
toolListLine,
|
|
toolSchemaLine,
|
|
toolsNamesLine,
|
|
"Top tools (schema size):",
|
|
...perToolSchema.lines,
|
|
...(perToolSchema.omitted ? [`… (+${perToolSchema.omitted} more tools)`] : []),
|
|
"",
|
|
"Top tools (summary text size):",
|
|
...perToolSummary.lines,
|
|
...(perToolSummary.omitted ? [`… (+${perToolSummary.omitted} more tools)`] : []),
|
|
...(toolPropsLines.length ? ["", "Tools (param count):", ...toolPropsLines] : []),
|
|
"",
|
|
totalsLine,
|
|
"",
|
|
"Inline shortcut: a command token inside normal text (e.g. “hey /status”) that runs immediately (allowlisted senders only) and is stripped before the model sees the remaining message.",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n"),
|
|
};
|
|
}
|
|
|
|
return {
|
|
text: [
|
|
"🧠 Context breakdown",
|
|
`Workspace: ${workspaceLabel}`,
|
|
`Bootstrap max/file: ${bootstrapMaxLabel}`,
|
|
sandboxLine,
|
|
systemPromptLine,
|
|
"",
|
|
"Injected workspace files:",
|
|
...fileLines,
|
|
"",
|
|
skillsLine,
|
|
skillsNamesLine,
|
|
toolListLine,
|
|
toolSchemaLine,
|
|
toolsNamesLine,
|
|
"",
|
|
totalsLine,
|
|
"",
|
|
"Inline shortcut: a command token inside normal text (e.g. “hey /status”) that runs immediately (allowlisted senders only) and is stripped before the model sees the remaining message.",
|
|
].join("\n"),
|
|
};
|
|
}
|