@botpress/adk-cli
Version:
Command-line interface for the Botpress Agent Development Kit (ADK)
632 lines (629 loc) • 21.6 kB
JavaScript
// @bun
import {
parseDuration
} from "./chunk-wk6ynbab.js";
import {
SqliteTraceStore
} from "./chunk-2re833ev.js";
import {
ensureJsonOnlyFormat
} from "./chunk-7sfagm12.js";
import {
findAgentRootOrFail
} from "./chunk-kk3h6qaj.js";
import {
createCliLogger,
source_default
} from "./chunk-gzwt1qdr.js";
import"./chunk-nxy2ya5r.js";
import"./chunk-wzj4dc7n.js";
import {
AdkError
} from "./chunk-p0hjqn4r.js";
import"./chunk-np5wcwfv.js";
import"./chunk-dq2xpa24.js";
import"./chunk-6w0knnta.js";
import"./chunk-40x04ckt.js";
import"./chunk-t76d8fxx.js";
import"./chunk-nh2akp42.js";
import"./chunk-0fdvzjbh.js";
import"./chunk-2a5b6azq.js";
import"./chunk-vay209b5.js";
import"./chunk-3xrpxgq4.js";
import"./chunk-rfm3jr1m.js";
import"./chunk-w346ejn9.js";
import"./chunk-knvm2anf.js";
import"./chunk-65h5trb5.js";
import"./chunk-s2akeqpw.js";
import"./chunk-6771vrjp.js";
import"./chunk-g8mm42v1.js";
import"./chunk-50hzjdck.js";
import"./chunk-nn2jb0x0.js";
import"./chunk-v8xvth6j.js";
import"./chunk-kkk13rcb.js";
import"./chunk-ytpp1kam.js";
import"./chunk-na956zz3.js";
import"./chunk-f4bw8q7c.js";
import"./chunk-0v8vgrns.js";
import"./chunk-54qt5g7m.js";
import"./chunk-dhs2bg35.js";
// src/commands/adk-traces.ts
import { createInterface } from "readline";
// src/traces/trace-filter-tokens.ts
var VALID_BARE_KEYWORDS = "error";
var VALID_KV_KEYS = "workflow, conversation, action, trigger, trace, since, until, limit";
var DURATION_HINT = "Use number + unit: 30s, 5m, 1h, 2d, 1w";
var POSITIVE_INT_HINT = "Must be a positive integer";
var ARRAY_KEYS = new Set(["workflow", "conversation", "action", "trigger", "trace"]);
function parseTraceFilterTokens(tokens) {
const opts = {};
const errors = [];
for (const token of tokens) {
const lower = token.toLowerCase();
if (lower === "error" || lower === "errors") {
opts.status = "error";
continue;
}
const eqIdx = token.indexOf("=");
if (eqIdx === -1) {
errors.push({
token,
message: `Unknown filter \`${token}\``,
suggestion: `Valid filters: ${VALID_BARE_KEYWORDS}, ${VALID_KV_KEYS}`
});
continue;
}
const key = token.slice(0, eqIdx).toLowerCase();
const val = token.slice(eqIdx + 1);
if (ARRAY_KEYS.has(key)) {
const values = val.split(",").map((v) => v.trim()).filter(Boolean);
if (values.length === 0) {
errors.push({
token,
message: `Empty value for \`${key}\``,
suggestion: `Provide one or more values separated by commas: ${key}=val1,val2`
});
continue;
}
switch (key) {
case "workflow":
opts.workflows = (opts.workflows ?? []).concat(values);
break;
case "conversation":
opts.conversations = (opts.conversations ?? []).concat(values);
break;
case "action":
opts.actions = (opts.actions ?? []).concat(values);
break;
case "trigger":
opts.triggers = (opts.triggers ?? []).concat(values);
break;
case "trace":
opts.traceIds = (opts.traceIds ?? []).concat(values);
break;
}
continue;
}
if (key === "since") {
const ms = parseDuration(val);
if (ms !== null) {
opts.since = Date.now() - ms;
} else {
errors.push({
token,
message: `Invalid duration \`${val}\` for \`since\``,
suggestion: DURATION_HINT
});
}
continue;
}
if (key === "until") {
const ms = parseDuration(val);
if (ms !== null) {
opts.until = Date.now() - ms;
} else {
errors.push({
token,
message: `Invalid duration \`${val}\` for \`until\``,
suggestion: DURATION_HINT
});
}
continue;
}
if (key === "limit") {
const n = parseInt(val, 10);
if (!isNaN(n) && n > 0) {
opts.limit = n;
} else {
errors.push({
token,
message: `Invalid value \`${val}\` for \`limit\``,
suggestion: POSITIVE_INT_HINT
});
}
continue;
}
errors.push({
token,
message: `Unknown filter key \`${key}\``,
suggestion: `Valid keys: ${VALID_KV_KEYS}`
});
}
return { options: opts, errors };
}
// src/traces/trace-summary.ts
var HIGH_IMPORTANCE_SPANS = new Set([
"request.incoming",
"cognitive.request",
"handler.conversation",
"handler.trigger",
"handler.workflow",
"handler.workflow.step",
"handler.action",
"handler.event",
"autonomous.execution",
"autonomous.iteration",
"autonomous.tool",
"chat.sendMessage"
]);
var MAX_PATH_HOPS = 5;
function buildTraceSummary(spans) {
if (spans.length === 0) {
throw new AdkError({ code: "EMPTY_SPANS", message: "Cannot build summary from empty spans array" });
}
const traceId = spans[0].id.trace;
const roots = spans.filter((s) => s.id.parent === null);
const root = roots.sort((a, b) => a.timing.startedAt - b.timing.startedAt)[0] ?? spans[0];
const childrenMap = new Map;
for (const span of spans) {
if (span.id.parent) {
const children = childrenMap.get(span.id.parent) ?? [];
children.push(span);
childrenMap.set(span.id.parent, children);
}
}
const pathNames = [];
let current = root;
while (current && pathNames.length < MAX_PATH_HOPS) {
const displayName = current.label && current.label !== current.name ? current.label : current.name;
pathNames.push(displayName);
const children = childrenMap.get(current.id.span);
if (!children || children.length === 0)
break;
current = pickCriticalChild(children);
}
const hasError = spans.some((s) => s.status === "error");
const hasRunning = spans.some((s) => s.status === "running");
const status = hasError ? "error" : hasRunning ? "running" : "ok";
const startTime = Math.min(...spans.map((s) => s.timing.startedAt));
const duration = root.timing.duration ?? null;
const conversationId = spans.find((s) => s.context.conversationId)?.context.conversationId;
const summary = `${pathNames.join(" \u2192 ")} [${status}]`;
return {
traceId,
startTime: new Date(startTime).toISOString(),
duration,
status,
spanCount: spans.length,
conversationId,
summary
};
}
function pickCriticalChild(children) {
return children.sort((a, b) => {
const aHigh = HIGH_IMPORTANCE_SPANS.has(a.name) ? 1 : 0;
const bHigh = HIGH_IMPORTANCE_SPANS.has(b.name) ? 1 : 0;
if (aHigh !== bHigh)
return bHigh - aHigh;
return (b.timing.duration ?? 0) - (a.timing.duration ?? 0);
})[0];
}
// src/traces/trace-formatter.ts
function formatDuration(ms) {
if (ms == null)
return "---";
if (ms < 1)
return "<1ms";
if (ms < 1000)
return `${Math.round(ms)}ms`;
if (ms < 60000)
return `${(ms / 1000).toFixed(1)}s`;
return `${(ms / 60000).toFixed(1)}m`;
}
function padDuration(ms, width = 6) {
return formatDuration(ms).padEnd(width);
}
function statusIcon(status) {
switch (status) {
case "ok":
return source_default.green("\u2713");
case "error":
return source_default.red("\u2717");
case "running":
return source_default.yellow("\u25CC");
}
}
function statusWord(status) {
switch (status) {
case "ok":
return source_default.green("ok");
case "error":
return source_default.red("error");
case "running":
return source_default.yellow("running");
}
}
function formatTraceSummary(summary) {
const time = new Date(summary.startTime).toLocaleTimeString("en-US", { hour12: false });
const dur = padDuration(summary.duration);
const icon = statusIcon(summary.status);
const id = source_default.dim(summary.traceId.slice(0, 8));
return `[${time}] ${dur} ${icon} ${id} ${summary.summary}`;
}
function formatTraceDetail(traceId, spans, opts = {}) {
if (spans.length === 0)
return source_default.dim(`Trace ${traceId}: no spans found`);
const roots = spans.filter((s) => s.id.parent === null);
const root = roots.sort((a, b) => a.timing.startedAt - b.timing.startedAt)[0] ?? spans[0];
const hasError = spans.some((s) => s.status === "error");
const hasRunning = spans.some((s) => s.status === "running");
const traceStatus = hasError ? "error" : hasRunning ? "running" : "ok";
const lines = [];
const totalDuration = root.timing.duration;
lines.push(`Trace ${source_default.bold(traceId)} ${statusIcon(traceStatus)} ${statusWord(traceStatus)} ${formatDuration(totalDuration)}`);
const childrenMap = new Map;
for (const span of spans) {
if (span.id.parent) {
const children = childrenMap.get(span.id.parent) ?? [];
children.push(span);
childrenMap.set(span.id.parent, children);
}
}
for (const [, children] of childrenMap) {
children.sort((a, b) => a.timing.startedAt - b.timing.startedAt);
}
renderSpanTree(root, "", true, childrenMap, lines, opts);
return lines.join(`
`);
}
function renderSpanTree(span, prefix, isLast, childrenMap, lines, opts) {
const connector = prefix === "" ? "" : isLast ? "\u2514\u2500 " : "\u251C\u2500 ";
const displayName = span.label && span.label !== span.name ? `${span.name} (${span.label})` : span.name;
const dur = formatDuration(span.timing.duration);
const errorMark = span.status === "error" ? ` ${source_default.red("\u2717")}` : "";
const cachedMark = isCachedStep(span) ? ` ${source_default.dim("(cached)")}` : "";
const tokenInfo = formatTokenInfo(span);
lines.push(`${prefix}${connector}${displayName}${errorMark}${cachedMark} ${source_default.dim(dur)}${tokenInfo}`);
if (span.error) {
const errPrefix = prefix === "" ? " " : prefix + (isLast ? " " : "\u2502 ");
lines.push(`${errPrefix}${source_default.red(span.error)}`);
}
if (opts.includeLlm) {
const detailPrefix = prefix === "" ? " " : prefix + (isLast ? " " : "\u2502 ");
const llmLines = formatLlmDetails(span);
for (const line of llmLines) {
lines.push(`${detailPrefix}${line}`);
}
}
const children = childrenMap.get(span.id.span);
if (!children)
return;
const childPrefix = prefix === "" ? "" : prefix + (isLast ? " " : "\u2502 ");
for (let i = 0;i < children.length; i++) {
renderSpanTree(children[i], childPrefix, i === children.length - 1, childrenMap, lines, opts);
}
}
function formatTokenInfo(span) {
if (span.name !== "cognitive.request")
return "";
const data = span.data;
const inputTokens = data?.["ai.input_tokens"];
const outputTokens = data?.["ai.output_tokens"];
if (inputTokens != null && outputTokens != null) {
return source_default.dim(` ${inputTokens}\u2192${outputTokens} tokens`);
}
return "";
}
function isCachedStep(span) {
if (span.name !== "handler.workflow.step")
return false;
const data = span.data;
return data?.["workflow.step.cached"] === true;
}
function formatLlmDetails(span) {
const data = span.data;
const lines = [];
if (span.name === "cognitive.request") {
const instructions = data?.["ai.instructions"];
if (typeof instructions === "string") {
const truncated = instructions.length > 200 ? instructions.slice(0, 200) + "..." : instructions;
lines.push(source_default.dim(`instructions: ${truncated}`));
}
const tools = data?.["ai.tools"];
if (tools) {
lines.push(source_default.dim(`tools: ${JSON.stringify(tools)}`));
}
}
if (span.name === "autonomous.iteration") {
const instructions = data?.["autonomous.instructions"];
if (typeof instructions === "string") {
const truncated = instructions.length > 200 ? instructions.slice(0, 200) + "..." : instructions;
lines.push(source_default.dim(`instructions: ${truncated}`));
}
const code = data?.["autonomous.code"];
if (typeof code === "string") {
const truncated = code.length > 300 ? code.slice(0, 300) + "..." : code;
lines.push(source_default.dim(`code: ${truncated}`));
}
const thoughts = data?.["autonomous.thoughts"];
if (typeof thoughts === "string") {
const truncated = thoughts.length > 200 ? thoughts.slice(0, 200) + "..." : thoughts;
lines.push(source_default.dim(`thoughts: ${truncated}`));
}
}
if (span.name === "botpress.client" || span.name === "http.client") {
const bp = span.name === "botpress.client";
const method = data?.[bp ? "botpress.method" : "http.method"];
const url = data?.[bp ? "botpress.url" : "http.url"];
if (method || url) {
lines.push(source_default.dim(`${method ?? ""} ${url ?? ""}`.trim()));
}
const requestBody = data?.[bp ? "botpress.request.body" : "http.request.body"];
if (typeof requestBody === "string" && requestBody) {
lines.push(source_default.dim(`request: ${requestBody.length > 200 ? requestBody.slice(0, 200) + "..." : requestBody}`));
}
const responseBody = data?.[bp ? "botpress.response.body" : "http.response.body"];
if (typeof responseBody === "string" && responseBody) {
lines.push(source_default.dim(`response: ${responseBody.length > 200 ? responseBody.slice(0, 200) + "..." : responseBody}`));
}
}
return lines;
}
// src/traces/trace-query.ts
function queryTraceOverviews(store, filters) {
const traceIds = store.queryTraceIds({
since: filters.since,
until: filters.until,
status: filters.status,
conversations: filters.conversations,
workflows: filters.workflows,
actions: filters.actions,
triggers: filters.triggers,
limit: filters.limit ?? 20
});
const summaries = [];
for (const traceId of traceIds) {
const spans = store.getTraceById(traceId);
if (spans.length > 0) {
summaries.push(buildTraceSummary(spans));
}
}
return summaries.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
}
function queryTraceDetails(store, traceIds) {
const result = new Map;
for (const traceId of traceIds) {
const spans = store.getTraceById(traceId);
if (spans.length > 0) {
result.set(traceId, spans);
}
}
return result;
}
function createTraceFollower(store, filters) {
let lastPollTs = Date.now();
let stopped = false;
const emittedTraceIds = new Set;
let resolveNext = null;
const buffer = [];
let intervalId = null;
function poll() {
if (stopped)
return;
const prevTs = lastPollTs;
lastPollTs = Date.now();
const completedRoots = store.getCompletedRootSpansSince(prevTs);
const traceIds = new Set;
for (const span of completedRoots) {
if (!emittedTraceIds.has(span.id.trace)) {
traceIds.add(span.id.trace);
}
}
for (const traceId of traceIds) {
const spans = store.getTraceById(traceId);
if (spans.length === 0)
continue;
const summary = buildTraceSummary(spans);
if (filters.status === "error" && summary.status !== "error")
continue;
if (filters.workflows && filters.workflows.length > 0) {
const hasWorkflow = spans.some((s) => s.name === "handler.workflow" && filters.workflows.includes(s.data?.["workflow.name"]));
if (!hasWorkflow)
continue;
}
if (filters.conversations && filters.conversations.length > 0) {
const hasConv = spans.some((s) => s.context.conversationId && filters.conversations.includes(s.context.conversationId));
if (!hasConv)
continue;
}
if (filters.actions && filters.actions.length > 0) {
const hasAction = spans.some((s) => {
const data = s.data;
return s.name === "handler.action" && filters.actions.includes(data?.["action.name"]) || s.name === "autonomous.tool" && filters.actions.includes(data?.["autonomous.tool.name"]);
});
if (!hasAction)
continue;
}
if (filters.triggers && filters.triggers.length > 0) {
const hasTrigger = spans.some((s) => s.name === "handler.trigger" && filters.triggers.includes(s.data?.["trigger.name"]));
if (!hasTrigger)
continue;
}
emittedTraceIds.add(traceId);
if (resolveNext) {
const resolve = resolveNext;
resolveNext = null;
resolve({ value: summary, done: false });
} else {
buffer.push(summary);
}
}
}
function stop() {
stopped = true;
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (resolveNext) {
const resolve = resolveNext;
resolveNext = null;
resolve({ value: undefined, done: true });
}
}
const summaries = {
[Symbol.asyncIterator]() {
if (!intervalId && !stopped) {
intervalId = setInterval(poll, 1000);
}
return {
next() {
if (buffer.length > 0) {
return Promise.resolve({ value: buffer.shift(), done: false });
}
if (stopped) {
return Promise.resolve({ value: undefined, done: true });
}
return new Promise((resolve) => {
resolveNext = resolve;
});
}
};
}
};
return { summaries, stop };
}
// src/commands/adk-traces.ts
async function resolveTraceId(store, partial, logger) {
const exact = store.getTraceById(partial);
if (exact.length > 0)
return partial;
const candidates = store.findTraceIdsByPrefix(partial);
if (candidates.length === 0)
return null;
if (candidates.length === 1)
return candidates[0];
logger.warn(`"${partial}" matches ${candidates.length} traces:
`);
for (let i = 0;i < candidates.length; i++) {
const spans = store.getTraceById(candidates[i]);
if (spans.length > 0) {
const summary = buildTraceSummary(spans);
const time = new Date(summary.startTime).toLocaleTimeString("en-US", { hour12: false });
const statusMark = summary.status === "error" ? "\u2717" : summary.status === "running" ? "\u25CC" : "\u2713";
logger.info(` ${i + 1}. ${candidates[i]} [${time}] ${statusMark} ${summary.summary}`, "gray");
} else {
logger.info(` ${i + 1}. ${candidates[i]} (no spans)`, "gray");
}
}
logger.newline();
const rl = createInterface({ input: process.stdin, output: process.stderr });
const answer = await new Promise((resolve) => {
rl.question(`Pick a trace (1-${candidates.length}): `, resolve);
});
rl.close();
const choice = parseInt(answer.trim(), 10);
if (isNaN(choice) || choice < 1 || choice > candidates.length) {
return null;
}
return candidates[choice - 1];
}
async function adkTraces(tokens, options) {
const logger = createCliLogger({ format: options?.format });
ensureJsonOnlyFormat(options.format);
const agentRoot = await findAgentRootOrFail(process.cwd());
const { options: filters, errors } = parseTraceFilterTokens(tokens);
if (errors.length > 0) {
throw new AdkError({
code: "INVALID_FLAGS",
message: errors.map((e) => e.suggestion ? `${e.message} (${e.suggestion})` : e.message).join("; "),
expected: true
});
}
const store = new SqliteTraceStore(agentRoot);
try {
if (filters.traceIds && filters.traceIds.length > 0) {
if (options.follow) {
logger.warn("Warning: --follow is not supported in drill-in mode, ignoring.");
}
const resolvedIds = [];
for (const partial of filters.traceIds) {
const resolved = await resolveTraceId(store, partial, logger);
if (resolved) {
resolvedIds.push(resolved);
} else {
logger.info(`No trace found matching "${partial}".`);
}
}
if (resolvedIds.length === 0) {
return;
}
const details = queryTraceDetails(store, resolvedIds);
if (details.size === 0) {
logger.info("No traces found for the given IDs.");
return;
}
for (const [traceId, spans] of details) {
const jsonSpans = spans.map((s) => {
const cached = isCachedStep(s);
const base = options.includeLlm ? s : (({ data: _, ...rest }) => rest)(s);
return cached ? { ...base, cached: true } : base;
});
logger.info(formatTraceDetail(traceId, spans, { includeLlm: options.includeLlm })).result({ traceId, spans: jsonSpans });
logger.newline();
}
} else {
if (options.follow) {
const existing = queryTraceOverviews(store, { ...filters, limit: filters.limit });
const cap = filters.limit;
let printed = 0;
for (const summary of existing) {
if (cap && printed >= cap)
break;
logger.info(formatTraceSummary(summary)).result(summary);
printed++;
}
if (cap && printed >= cap)
return;
const { summaries, stop } = createTraceFollower(store, filters);
process.on("SIGINT", stop);
process.on("SIGTERM", stop);
for await (const summary of summaries) {
logger.info(formatTraceSummary(summary)).result(summary);
printed++;
if (cap && printed >= cap) {
stop();
break;
}
}
} else {
if (!filters.limit) {
filters.limit = 20;
}
const summaries = queryTraceOverviews(store, filters);
if (summaries.length === 0) {
logger.info("No traces match the given filters.");
return;
}
for (const summary of summaries) {
logger.info(formatTraceSummary(summary)).result(summary);
}
}
}
} finally {
store.close();
}
}
export {
adkTraces
};