feat: add background task notification system (#827)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-02-04 22:45:16 -08:00
committed by GitHub
parent 84e9a6d744
commit 48ccd8f220
44 changed files with 2244 additions and 234 deletions

View File

@@ -124,6 +124,7 @@ export async function handleHeadlessCommand(
"no-skills": { type: "boolean" },
memfs: { type: "boolean" },
"no-memfs": { type: "boolean" },
"max-turns": { type: "string" }, // Maximum number of agentic turns
},
strict: false,
allowPositionals: true,
@@ -262,6 +263,20 @@ export async function handleHeadlessCommand(
const memfsFlag = values.memfs as boolean | undefined;
const noMemfsFlag = values["no-memfs"] as boolean | undefined;
const fromAfFile = values["from-af"] as string | undefined;
const maxTurnsRaw = values["max-turns"] as string | undefined;
// Parse and validate max-turns if provided
let maxTurns: number | undefined;
if (maxTurnsRaw !== undefined) {
const parsed = parseInt(maxTurnsRaw, 10);
if (Number.isNaN(parsed) || parsed <= 0) {
console.error(
`Error: --max-turns must be a positive integer, got: ${maxTurnsRaw}`,
);
process.exit(1);
}
maxTurns = parsed;
}
// Handle --conv {agent-id} shorthand: --conv agent-xyz → --agent agent-xyz --conv default
if (specifiedConversationId?.startsWith("agent-")) {
@@ -1005,7 +1020,11 @@ export async function handleHeadlessCommand(
// Build message content with reminders (plan mode first, then skill unload)
const { permissionMode } = await import("./permissions/mode");
const { hasLoadedSkills } = await import("./agent/context");
let messageContent = "";
const contentParts: MessageCreate["content"] = [];
const pushPart = (text: string) => {
if (!text) return;
contentParts.push({ type: "text", text });
};
if (fromAgentId) {
const senderAgentId = fromAgentId;
@@ -1017,29 +1036,29 @@ If you need to share detailed information, include it in your response text.
${SYSTEM_REMINDER_CLOSE}
`;
messageContent += systemReminder;
pushPart(systemReminder);
}
// Add plan mode reminder if in plan mode (highest priority)
if (permissionMode.getMode() === "plan") {
const { PLAN_MODE_REMINDER } = await import("./agent/promptAssets");
messageContent += PLAN_MODE_REMINDER;
pushPart(PLAN_MODE_REMINDER);
}
// Add skill unload reminder if skills are loaded (using cached flag)
if (hasLoadedSkills()) {
const { SKILL_UNLOAD_REMINDER } = await import("./agent/promptAssets");
messageContent += SKILL_UNLOAD_REMINDER;
pushPart(SKILL_UNLOAD_REMINDER);
}
// Add user prompt
messageContent += prompt;
pushPart(prompt);
// Start with the user message
let currentInput: Array<MessageCreate | ApprovalCreate> = [
{
role: "user",
content: [{ type: "text", text: messageContent }],
content: contentParts,
},
];
@@ -1047,12 +1066,35 @@ ${SYSTEM_REMINDER_CLOSE}
let lastKnownRunId: string | null = null;
let llmApiErrorRetries = 0;
let conversationBusyRetries = 0;
markMilestone("HEADLESS_FIRST_STREAM_START");
measureSinceMilestone("headless-setup-total", "HEADLESS_CLIENT_READY");
// Helper to check max turns limit using server-side step count from buffers
const checkMaxTurns = () => {
if (maxTurns !== undefined && buffers.usage.stepCount >= maxTurns) {
if (outputFormat === "stream-json") {
const errorMsg: ErrorMessage = {
type: "error",
message: `Maximum turns limit reached (${buffers.usage.stepCount}/${maxTurns} steps)`,
stop_reason: "max_steps",
session_id: sessionId,
uuid: `error-max-turns-${crypto.randomUUID()}`,
};
console.log(JSON.stringify(errorMsg));
} else {
console.error(
`Maximum turns limit reached (${buffers.usage.stepCount}/${maxTurns} steps)`,
);
}
process.exit(1);
}
};
try {
while (true) {
// Check max turns limit before starting a new turn (uses server-side step count)
checkMaxTurns();
// Wrap sendMessageStream in try-catch to handle pre-stream errors (e.g., 409)
let stream: Awaited<ReturnType<typeof sendMessageStream>>;
try {
@@ -1283,6 +1325,10 @@ ${SYSTEM_REMINDER_CLOSE}
// Track API duration for this stream
sessionStats.endTurn(apiDurationMs);
// Check max turns after each turn (server may have taken multiple steps)
checkMaxTurns();
if (approvalPendingRecovery) {
await resolveAllPendingApprovals();
continue;