From 94d9fa40378b4e768959cf80efd61669d2421d0d Mon Sep 17 00:00:00 2001 From: Federico Baldassarre Date: Thu, 8 Oct 2026 22:13:07 +0000 Subject: [PATCH 1/2] fix: emit provider-safe JSON Schema for update_goal_plan tool input The V2 update_goal_plan registration passed raw zod objects from planToolArgs as the tool input, so hosts serialized zod internals (def/checks/shape/optional plus null-valued keywords like maxLength/format) into the JSON Schema sent to providers. Providers that strictly validate tool schemas (e.g. Mistral mistral-large-4) reject those requests with 'Invalid tool schema', breaking every session that includes the goal tools on such models. Convert PlanToolSchema once via z.toJSONSchema and hand each registration a deep copy so hosts or test frameworks that mutate a received tool input cannot corrupt the shared schema. Runtime argument validation is unchanged: planFromTool still parses through PlanToolSchema. --- dist/server.js | 421 +++++++++++++++++++++-------------------- src/server.ts | 31 ++- test/server-v2.test.ts | 59 ++++++ 3 files changed, 303 insertions(+), 208 deletions(-) diff --git a/dist/server.js b/dist/server.js index 2123b6a..5c1d4c1 100644 --- a/dist/server.js +++ b/dist/server.js @@ -31,34 +31,34 @@ var GoalPlanInputSchema = z.object({ const ids = new Set; let runningPhases = 0; let runningTasks = 0; - for (const phase2 of plan.phases) { - if (ids.has(phase2.id)) + for (const phase of plan.phases) { + if (ids.has(phase.id)) ctx.addIssue({ code: "custom", message: "plan IDs must be unique" }); - ids.add(phase2.id); - if (phase2.status === "in_progress") + ids.add(phase.id); + if (phase.status === "in_progress") runningPhases++; - if (phase2.status === "blocked" && !phase2.blocker) + if (phase.status === "blocked" && !phase.blocker) ctx.addIssue({ code: "custom", message: "blocked phases require a blocker" }); - if (phase2.status === "completed" && (!phase2.verification || phase2.tasks.some((task2) => task2.status !== "completed"))) { + if (phase.status === "completed" && (!phase.verification || phase.tasks.some((task) => task.status !== "completed"))) { ctx.addIssue({ code: "custom", message: "completed phases require verified tasks and phase verification" }); } - for (const task2 of phase2.tasks) { - if (ids.has(task2.id)) + for (const task of phase.tasks) { + if (ids.has(task.id)) ctx.addIssue({ code: "custom", message: "plan IDs must be unique" }); - ids.add(task2.id); - if (task2.status === "completed" && !task2.evidence) + ids.add(task.id); + if (task.status === "completed" && !task.evidence) ctx.addIssue({ code: "custom", message: "completed tasks require evidence" }); - if (task2.status === "blocked" && !task2.blocker) + if (task.status === "blocked" && !task.blocker) ctx.addIssue({ code: "custom", message: "blocked tasks require a blocker" }); - if (task2.status === "in_progress") { + if (task.status === "in_progress") { runningTasks++; - if (phase2.status !== "in_progress") + if (phase.status !== "in_progress") ctx.addIssue({ code: "custom", message: "running tasks require a running phase" }); } } } - const firstUnfinished = plan.phases.find((phase2) => phase2.status !== "completed"); - if (plan.phases.some((phase2) => phase2.status === "in_progress" && phase2 !== firstUnfinished)) + const firstUnfinished = plan.phases.find((phase) => phase.status !== "completed"); + if (plan.phases.some((phase) => phase.status === "in_progress" && phase !== firstUnfinished)) ctx.addIssue({ code: "custom", message: "verify the current phase before starting the next phase" }); if (runningPhases > 1 || runningTasks > 1) ctx.addIssue({ code: "custom", message: "choose one current phase and task" }); @@ -86,9 +86,9 @@ function reviseGoalPlan(previous, input, expectedRevision, reason, now, revisitE throw new Error("preserve overall completion criteria; replace the goal for a new scope"); } for (const oldPhase of previous.phases) { - const newPhase = next.phases.find((phase2) => phase2.id === oldPhase.id); + const newPhase = next.phases.find((phase) => phase.id === oldPhase.id); if (!newPhase) { - if (oldPhase.status === "completed" || oldPhase.tasks.some((task2) => task2.status === "completed")) + if (oldPhase.status === "completed" || oldPhase.tasks.some((task) => task.status === "completed")) throw new Error("preserve verified phase and task history"); if (!revisitEvidence?.trim()) throw new Error("removing planned scope requires concrete revisit evidence"); @@ -102,7 +102,7 @@ function reviseGoalPlan(previous, input, expectedRevision, reason, now, revisitE throw new Error("reopening a verified phase requires concrete revisit evidence"); } for (const oldTask of oldPhase.tasks) { - const newTask = newPhase.tasks.find((task2) => task2.id === oldTask.id); + const newTask = newPhase.tasks.find((task) => task.id === oldTask.id); if (oldTask.status !== "completed") { if ((!newTask || newTask.description !== oldTask.description) && !revisitEvidence?.trim()) throw new Error("removing or changing planned task scope requires concrete revisit evidence"); @@ -133,23 +133,23 @@ function reviseGoalPlan(previous, input, expectedRevision, reason, now, revisitE }); } function goalPlanProgress(plan) { - const current = plan.phases.find((phase2) => phase2.status === "in_progress") ?? plan.phases.find((phase2) => phase2.status !== "completed"); - const running = current?.tasks.find((task2) => task2.status === "in_progress"); - const next = current?.tasks.find((task2) => task2.status === "pending"); + const current = plan.phases.find((phase) => phase.status === "in_progress") ?? plan.phases.find((phase) => phase.status !== "completed"); + const running = current?.tasks.find((task) => task.status === "in_progress"); + const next = current?.tasks.find((task) => task.status === "pending"); return { currentPhaseID: current?.id ?? null, currentTaskID: running?.id ?? null, nextTaskID: next?.id ?? null, - nextPhaseID: plan.phases.find((phase2) => phase2.id !== current?.id && phase2.status !== "completed")?.id ?? null, - completedPhaseIDs: plan.phases.filter((phase2) => phase2.status === "completed").map((phase2) => phase2.id), - completedTaskIDs: plan.phases.flatMap((phase2) => phase2.tasks.filter((task2) => task2.status === "completed").map((task2) => task2.id)) + nextPhaseID: plan.phases.find((phase) => phase.id !== current?.id && phase.status !== "completed")?.id ?? null, + completedPhaseIDs: plan.phases.filter((phase) => phase.status === "completed").map((phase) => phase.id), + completedTaskIDs: plan.phases.flatMap((phase) => phase.tasks.filter((task) => task.status === "completed").map((task) => task.id)) }; } function goalPlanEntries(plan) { - return plan.phases.flatMap((phase2) => phase2.tasks.map((task2) => ({ - content: `${phase2.objective}: ${task2.description}${task2.blocker ? ` \u2014 Blocked: ${task2.blocker}` : ""}`, + return plan.phases.flatMap((phase) => phase.tasks.map((task) => ({ + content: `${phase.objective}: ${task.description}${task.blocker ? ` \u2014 Blocked: ${task.blocker}` : ""}`, priority: "medium", - status: task2.status === "blocked" ? "pending" : task2.status + status: task.status === "blocked" ? "pending" : task.status }))); } @@ -691,11 +691,11 @@ function nonNegativeInteger(value, fallback) { function nonNegativeIntegerOrNull(value) { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null; } -function isClosed(status2) { - return status2 === "complete" || status2 === "unmet" || status2 === "cancelled"; +function isClosed(status) { + return status === "complete" || status === "unmet" || status === "cancelled"; } -function canContinue(status2) { - return status2 === "active"; +function canContinue(status) { + return status === "active"; } function remainingTokens(goal) { return goal.tokenBudget == null ? null : Math.max(0, goal.tokenBudget - goal.tokensUsed); @@ -913,7 +913,7 @@ async function createGoal(sessionID, objective, options) { return snapshot(goal); }); } -async function updateGoalObjective(sessionID, objective, status2 = "active", options) { +async function updateGoalObjective(sessionID, objective, status = "active", options) { const value = validateObjective(objective, resolveMaxObjectiveChars(options?.maxObjectiveChars)); const agent = typeof options?.agent === "string" && options.agent.trim() ? options.agent.trim() : null; const planModePause = options?.planModePause === true; @@ -932,7 +932,7 @@ async function updateGoalObjective(sessionID, objective, status2 = "active", opt goal.planRevision += 1; } goal.objective = value; - goal.status = planModePause ? "paused" : status2; + goal.status = planModePause ? "paused" : status; goal.updatedAt = nowSeconds(); goal.lastAccountedAt = goal.status === "active" ? goal.updatedAt : null; goal.completionEvidence = null; @@ -985,7 +985,7 @@ async function pauseGoalForPlanMode(sessionID) { return snapshot(goal); }); } -async function setGoalStatus(sessionID, status2, agent, options) { +async function setGoalStatus(sessionID, status, agent, options) { const agentValue = typeof agent === "string" && agent.trim() ? agent.trim() : null; return mutate((state) => { const goal = state.goals[sessionID]; @@ -993,26 +993,26 @@ async function setGoalStatus(sessionID, status2, agent, options) { throw new Error("cannot update goal because this session has no goal"); if (isClosed(goal.status)) throw new Error("cannot update goal status because this goal is closed"); - if (goal.status === status2) + if (goal.status === status) return snapshot(goal); - if (status2 === "paused" && goal.status !== "active") + if (status === "paused" && goal.status !== "active") return snapshot(goal); - const resumesAutoTurnLimit = options?.resetAutoTurnLimit === true && status2 === "active" && goal.status === "usageLimited" && goal.stopReason?.startsWith(MAX_AUTO_CONTINUES_STOP_REASON_PREFIX) === true; + const resumesAutoTurnLimit = options?.resetAutoTurnLimit === true && status === "active" && goal.status === "usageLimited" && goal.stopReason?.startsWith(MAX_AUTO_CONTINUES_STOP_REASON_PREFIX) === true; accountWallClock(goal); - goal.status = status2; + goal.status = status; goal.updatedAt = nowSeconds(); - goal.lastAccountedAt = status2 === "active" ? goal.updatedAt : null; + goal.lastAccountedAt = status === "active" ? goal.updatedAt : null; goal.autoTurns = resumesAutoTurnLimit ? 0 : goal.autoTurns; - goal.continuationFailures = status2 === "active" ? 0 : goal.continuationFailures; - goal.pendingAttempt = status2 === "active" ? null : goal.pendingAttempt; - goal.noProgressTurns = status2 === "active" ? 0 : goal.noProgressTurns; - goal.stopReason = status2 === "active" ? null : "paused"; - goal.budgetWrapupSent = status2 === "active" ? false : goal.budgetWrapupSent; - goal.blocker = status2 === "active" ? null : goal.blocker; + goal.continuationFailures = status === "active" ? 0 : goal.continuationFailures; + goal.pendingAttempt = status === "active" ? null : goal.pendingAttempt; + goal.noProgressTurns = status === "active" ? 0 : goal.noProgressTurns; + goal.stopReason = status === "active" ? null : "paused"; + goal.budgetWrapupSent = status === "active" ? false : goal.budgetWrapupSent; + goal.blocker = status === "active" ? null : goal.blocker; if (agentValue) goal.lastPromptAgent = agentValue; - goal.lastStatus = status2 === "active" ? "Goal resumed." : "Goal paused."; - pushHistory(goal, status2 === "active" ? "resumed" : "paused", goal.lastStatus); + goal.lastStatus = status === "active" ? "Goal resumed." : "Goal paused."; + pushHistory(goal, status === "active" ? "resumed" : "paused", goal.lastStatus); return snapshot(goal); }); } @@ -1024,7 +1024,7 @@ async function closeGoal(sessionID, input, maxObjectiveChars = DEFAULT_MAX_OBJEC throw new Error("cannot update goal because this session has no goal"); if (isClosed(goal.status)) throw new Error("cannot close goal because this goal is already closed"); - if (input.status === "complete" && goal.plan?.phases.some((phase2) => phase2.status !== "completed")) { + if (input.status === "complete" && goal.plan?.phases.some((phase) => phase.status !== "completed")) { throw new Error("cannot complete the overall goal while planned phases still require work or verification"); } accountWallClock(goal); @@ -1186,20 +1186,20 @@ async function recordAssistantProgress(sessionID, input) { const goal = state.goals[sessionID]; if (!goal || goal.status !== "active") return goal ? snapshot(goal) : null; - const text2 = input.text?.trim() ?? ""; + const text = input.text?.trim() ?? ""; const messageID = input.messageID?.trim() ?? ""; const outputTokens = positiveIntegerOrNull(input.outputTokens) ?? 0; const threshold = positiveIntegerOrNull(input.noProgressTokenThreshold) ?? goal.noProgressTokenThreshold; const maxNoProgressTurns = positiveIntegerOrNull(input.maxNoProgressTurns) ?? goal.maxNoProgressTurns; - const summary = summarizeText(text2); - const substantive = /[\p{L}\p{N}]/u.test(text2); + const summary = summarizeText(text); + const substantive = /[\p{L}\p{N}]/u.test(text); const previousSummary = summarizeText(goal.lastAssistantText); const repeatedMessage = Boolean(messageID && messageID === goal.lastAssistantMessageID); const changed = Boolean(summary && summary !== previousSummary); if (summary && (!repeatedMessage || changed)) recordCheckpoint(goal, summary); - if (text2) - goal.lastAssistantText = text2; + if (text) + goal.lastAssistantText = text; if (messageID) goal.lastAssistantMessageID = messageID; if (substantive && summary && (!repeatedMessage || changed)) { @@ -1361,8 +1361,8 @@ async function markPendingContinuationStarted(sessionID) { return current ? snapshotInternal(current) : null; if (current.pendingAttempt == null || current.pendingAttempt.started) return snapshotInternal(current); - return mutate((state2) => { - const goal = state2.goals[sessionID]; + return mutate((state) => { + const goal = state.goals[sessionID]; if (!goal || goal.status !== "active") return goal ? snapshotInternal(goal) : null; if (goal.pendingAttempt == null || goal.pendingAttempt.started) @@ -1372,12 +1372,12 @@ async function markPendingContinuationStarted(sessionID) { return snapshotInternal(goal); }); } -async function recordToolProgress(sessionID, text2, expectedAttemptID) { +async function recordToolProgress(sessionID, text, expectedAttemptID) { return mutate((state) => { const goal = state.goals[sessionID]; if (!goal || goal.status !== "active") return goal ? snapshotInternal(goal) : null; - const value = text2?.trim() ?? ""; + const value = text?.trim() ?? ""; if (!value) return snapshotInternal(goal); if (goal.continuationFailures === 0 && goal.pendingAttempt == null) @@ -1458,8 +1458,8 @@ function pushHistory(goal, type, detail) { return; goal.history = [...goal.history, { type, detail: value, timestamp: nowSeconds() }].slice(-MAX_HISTORY_ENTRIES); } -function summarizeText(text2, limit = CHECKPOINT_CHAR_LIMIT) { - const normalized = text2.replace(/\s+/g, " ").trim(); +function summarizeText(text, limit = CHECKPOINT_CHAR_LIMIT) { + const normalized = text.replace(/\s+/g, " ").trim(); if (!normalized) return ""; return normalized.length > limit ? `${normalized.slice(0, Math.max(0, limit - 3))}...` : normalized; @@ -1472,8 +1472,8 @@ function goalLimitSummary(goal) { ].filter(Boolean); return limits.length ? `Goal set with ${limits.join(", ")}.` : "Goal set with default continuation limits."; } -function estimateTokensFromText(text2) { - return Math.ceil(text2.length / 4); +function estimateTokensFromText(text) { + return Math.ceil(text.length / 4); } // src/i18n.ts @@ -1714,8 +1714,8 @@ var STATUS_PRESENTATIONS = { cancelled: "\u5DF2\u53D6\u6D88" } }; -function presentGoalStatus(status2, locale) { - return STATUS_PRESENTATIONS[locale][status2] ?? status2; +function presentGoalStatus(status, locale) { + return STATUS_PRESENTATIONS[locale][status] ?? status; } function presentGoalStopReason(reason, locale) { if (locale !== "zh-CN") @@ -1747,9 +1747,9 @@ function presentGoalStopReason(reason, locale) { return `\u5DF2\u8FBE\u5230\u6301\u7EED\u65F6\u95F4\u4E0A\u9650\uFF08${duration[1]} \u79D2\uFF09`; return reason; } -function presentGoalLastStatus(status2, locale) { +function presentGoalLastStatus(status, locale) { if (locale !== "zh-CN") - return status2; + return status; const direct = { "Goal set.": "\u76EE\u6807\u5DF2\u8BBE\u7F6E\u3002", "Goal recorded from Plan mode; execution paused until resumed from Build mode.": "\u76EE\u6807\u5DF2\u5728 Plan \u6A21\u5F0F\u4E0B\u8BB0\u5F55\uFF1B\u6267\u884C\u5DF2\u6682\u505C\uFF0C\u9700\u5728 Build \u6A21\u5F0F\u4E0B\u7EE7\u7EED\u3002", @@ -1768,30 +1768,30 @@ function presentGoalLastStatus(status2, locale) { "Auto-continue prompt failed repeatedly. Resume the goal to retry.": "\u81EA\u52A8\u7EE7\u7EED\u63D0\u793A\u53CD\u590D\u5931\u8D25\u3002\u8BF7\u7EE7\u7EED\u76EE\u6807\u540E\u91CD\u8BD5\u3002", "Goal execution is paused while the session is in Plan mode. Switch to Build mode and resume the goal to continue.": "\u4F1A\u8BDD\u5904\u4E8E Plan \u6A21\u5F0F\uFF0C\u56E0\u6B64\u76EE\u6807\u6267\u884C\u5DF2\u6682\u505C\u3002\u8BF7\u5207\u6362\u5230 Build \u6A21\u5F0F\u5E76\u7EE7\u7EED\u76EE\u6807\u3002" }; - if (direct[status2]) - return direct[status2]; + if (direct[status]) + return direct[status]; const lowProgressPausePattern = /^Auto-continue paused after (\d+) low-progress continuation turn\(s\)\. Resume the goal to retry\.$/; - const lowProgressPause = lowProgressPausePattern.exec(status2); + const lowProgressPause = lowProgressPausePattern.exec(status); if (lowProgressPause) return `\u81EA\u52A8\u7EE7\u7EED\u5DF2\u5728 ${lowProgressPause[1]} \u4E2A\u4F4E\u8FDB\u5C55\u8F6E\u6B21\u540E\u6682\u505C\u3002\u8BF7\u7EE7\u7EED\u76EE\u6807\u540E\u91CD\u8BD5\u3002`; - const lowProgress = /^Low-progress continuation turn detected \((\d+)\/(\d+|unbounded)\)\.$/.exec(status2); + const lowProgress = /^Low-progress continuation turn detected \((\d+)\/(\d+|unbounded)\)\.$/.exec(status); if (lowProgress) { const limit = lowProgress[2] === "unbounded" ? "\u4E0D\u9650" : lowProgress[2]; return `\u68C0\u6D4B\u5230\u4F4E\u8FDB\u5C55\u7684\u7EE7\u7EED\u8F6E\u6B21\uFF08${lowProgress[1]}/${limit}\uFF09\u3002`; } - const reserved = /^Auto-continue (\d+) reserved\.$/.exec(status2); + const reserved = /^Auto-continue (\d+) reserved\.$/.exec(status); if (reserved) return `\u5DF2\u9884\u7559\u7B2C ${reserved[1]} \u6B21\u81EA\u52A8\u7EE7\u7EED\u3002`; - const failed = /^Auto-continue failed (\d+) time\(s\)\.$/.exec(status2); + const failed = /^Auto-continue failed (\d+) time\(s\)\.$/.exec(status); if (failed) return `\u81EA\u52A8\u7EE7\u7EED\u5DF2\u5931\u8D25 ${failed[1]} \u6B21\u3002`; - const pausedAfterFailures = /^Paused after (\d+) auto-continue failure\(s\)\.$/.exec(status2); + const pausedAfterFailures = /^Paused after (\d+) auto-continue failure\(s\)\.$/.exec(status); if (pausedAfterFailures) return `\u5DF2\u5728 ${pausedAfterFailures[1]} \u6B21\u81EA\u52A8\u7EE7\u7EED\u5931\u8D25\u540E\u6682\u505C\u3002`; - const wrapUp = /^(.*); wrap-up required\.$/.exec(status2); + const wrapUp = /^(.*); wrap-up required\.$/.exec(status); if (wrapUp) return `${presentGoalStopReason(wrapUp[1], locale)}\uFF1B\u9700\u8981\u6536\u5C3E\u3002`; - return status2; + return status; } var HISTORY_TYPE_PRESENTATIONS = { en: {}, @@ -2053,7 +2053,7 @@ function compactionContextPrefix(locale = "en") { var COMPACTION_CONTEXT_PREFIX = compactionContextPrefix(); function formatCompactionSnapshot(goal, locale) { if (locale === "zh-CN") { - const lines2 = [ + const lines = [ `\u76EE\u6807\uFF1A${goal.objective}`, `\u72B6\u6001\uFF1A${presentGoalStatus(goal.status, locale)}`, `\u5DF2\u7528\u65F6\u95F4\uFF1A${goal.timeUsedSeconds} \u79D2`, @@ -2061,24 +2061,24 @@ function formatCompactionSnapshot(goal, locale) { `\u81EA\u52A8\u7EE7\u7EED\u6B21\u6570\uFF1A${goal.autoTurns}${goal.maxAutoTurns == null ? "" : `/${goal.maxAutoTurns}`}` ]; if (goal.remainingTokens != null) - lines2.push(`\u5269\u4F59 Token\uFF1A${goal.remainingTokens}`); + lines.push(`\u5269\u4F59 Token\uFF1A${goal.remainingTokens}`); if (goal.maxDurationSeconds != null) - lines2.push(`\u6301\u7EED\u65F6\u95F4\u4E0A\u9650\uFF1A${goal.maxDurationSeconds} \u79D2`); + lines.push(`\u6301\u7EED\u65F6\u95F4\u4E0A\u9650\uFF1A${goal.maxDurationSeconds} \u79D2`); if (goal.noProgressTurns > 0) - lines2.push(`\u65E0\u8FDB\u5C55\u8F6E\u6570\uFF1A${goal.noProgressTurns}`); + lines.push(`\u65E0\u8FDB\u5C55\u8F6E\u6570\uFF1A${goal.noProgressTurns}`); if (goal.lastCheckpoint) - lines2.push(`\u6700\u65B0\u68C0\u67E5\u70B9\uFF1A${goal.lastCheckpoint.summary}`); + lines.push(`\u6700\u65B0\u68C0\u67E5\u70B9\uFF1A${goal.lastCheckpoint.summary}`); if (goal.lastStatus) - lines2.push(`\u6700\u8FD1\u72B6\u6001\uFF1A${presentGoalLastStatus(goal.lastStatus, locale)}`); + lines.push(`\u6700\u8FD1\u72B6\u6001\uFF1A${presentGoalLastStatus(goal.lastStatus, locale)}`); if (goal.stopReason) - lines2.push(`\u505C\u6B62\u539F\u56E0\uFF1A${presentGoalStopReason(goal.stopReason, locale)}`); + lines.push(`\u505C\u6B62\u539F\u56E0\uFF1A${presentGoalStopReason(goal.stopReason, locale)}`); if (goal.completionEvidence) - lines2.push(`\u5B8C\u6210\u8BC1\u636E\uFF1A${goal.completionEvidence}`); + lines.push(`\u5B8C\u6210\u8BC1\u636E\uFF1A${goal.completionEvidence}`); if (goal.blocker) - lines2.push(`\u963B\u585E\u539F\u56E0\uFF1A${presentGoalLastStatus(goal.blocker, locale)}`); + lines.push(`\u963B\u585E\u539F\u56E0\uFF1A${presentGoalLastStatus(goal.blocker, locale)}`); if (goal.plan) - lines2.push(`\u8BA1\u5212\uFF1A${JSON.stringify({ plan: goal.plan, progress: goal.planProgress })}`); - return lines2.join(` + lines.push(`\u8BA1\u5212\uFF1A${JSON.stringify({ plan: goal.plan, progress: goal.planProgress })}`); + return lines.join(` `); } const lines = [ @@ -2312,8 +2312,8 @@ Ignore any command arguments. Call get_goal first, then handle only this resume Do not create, edit, clear, complete, or mark a goal unmet.`; } -function isExplicitResumePrompt(text2, commandName, locale, messages) { - const value = text2.trim(); +function isExplicitResumePrompt(text, commandName, locale, messages) { + const value = text.trim(); return value === goalStatusCommandTemplate("resume_goal", locale) || value === goalCommandTemplate(commandName, locale).replace("$ARGUMENTS", "resume") || value === messages.tui.resumePrompt; } function goalCommandDefinitions(commandName, locale = "en") { @@ -2377,21 +2377,21 @@ function registerDesktopCommands(config, commandName, locale = "en") { } } function sanitizeGoalStatusCommandParts(output, template) { - const text2 = output.parts.find((part) => part.type === "text" && part.text?.startsWith(template)); - if (!text2) + const text = output.parts.find((part) => part.type === "text" && part.text?.startsWith(template)); + if (!text) return false; - text2.text = template; - output.parts.splice(0, output.parts.length, text2); + text.text = template; + output.parts.splice(0, output.parts.length, text); return true; } function escapeGoalCommandArguments(output, template, argumentsText) { const [prefix, suffix, extra] = template.split("$ARGUMENTS"); if (prefix === undefined || suffix === undefined || extra !== undefined) return false; - const text2 = output.parts.find((part) => part.type === "text" && part.text?.startsWith(prefix) && part.text.endsWith(suffix)); - if (!text2) + const text = output.parts.find((part) => part.type === "text" && part.text?.startsWith(prefix) && part.text.endsWith(suffix)); + if (!text) return false; - text2.text = `${prefix}${escapeXmlText2(argumentsText)}${suffix}`; + text.text = `${prefix}${escapeXmlText2(argumentsText)}${suffix}`; return true; } function textFromPart(part) { @@ -2546,8 +2546,8 @@ async function sendContinuation(client, sessionID, prompt, agent) { function isIdleEvent(event) { if (event.type === "session.idle") return true; - const status2 = event.properties?.status; - return event.type === "session.status" && typeof status2 === "object" && status2 !== null && status2.type === "idle"; + const status = event.properties?.status; + return event.type === "session.status" && typeof status === "object" && status !== null && status.type === "idle"; } function isTransportError(error) { const message = error instanceof Error ? error.message : typeof error === "string" ? error : ""; @@ -2623,7 +2623,7 @@ function toolOutputFailed(output) { return true; if (output.success === false) return true; - const text2 = typeof output.output === "string" ? output.output.trim() : ""; + const text = typeof output.output === "string" ? output.output.trim() : ""; const state = output.state ?? output.status; if (typeof state === "string") { const normalized = state.trim().toLowerCase(); @@ -2637,19 +2637,19 @@ function toolOutputFailed(output) { if (typeof metaState === "string" && TOOL_FAILURE_STATES.has(metaState.trim().toLowerCase())) return true; } - const taskState = parseTaskState(text2); + const taskState = parseTaskState(text); if (taskState) return taskState !== "completed"; - if (/^state:\s*(failed|failure|error|cancelled|canceled|aborted|abort|interrupted|running|pending|incomplete|partial|timeout|timed_out)\b/im.test(text2)) + if (/^state:\s*(failed|failure|error|cancelled|canceled|aborted|abort|interrupted|running|pending|incomplete|partial|timeout|timed_out)\b/im.test(text)) return true; - if (/^/i.test(text2) || /^/i.test(text2) || /^error:/i.test(text2)) + if (/^/i.test(text) || /^/i.test(text) || /^error:/i.test(text)) return true; return false; } -function taskBlockExpired(task2, maxBlockMs, now) { +function taskBlockExpired(task, maxBlockMs, now) { if (maxBlockMs == null) return false; - const blockingSince = task2.state === "running" ? task2.runningSince : task2.terminalAt; + const blockingSince = task.state === "running" ? task.runningSince : task.terminalAt; return blockingSince != null && now - blockingSince >= maxBlockMs; } function sessionIDFromEvent(event) { @@ -2716,14 +2716,14 @@ class TaskTracker { this.pendingTaskCalls.delete(input.callID); if (typeof parentSessionID !== "string") return; - const status2 = parseTaskStatus(output.output); - if (!status2) + const status = parseTaskStatus(output.output); + if (!status) return; - if (status2.state === "running") { - this.markRunning(parentSessionID, status2.taskID); + if (status.state === "running") { + this.markRunning(parentSessionID, status.taskID); return; } - this.markTerminal(status2.taskID, status2.state, parentSessionID, { resetReconciled: true }); + this.markTerminal(status.taskID, status.state, parentSessionID, { resetReconciled: true }); } observeSessionCreated(event) { const info = event.properties?.info; @@ -2731,22 +2731,22 @@ class TaskTracker { return; this.markRunning(info.parentID, info.id); } - observeSessionStatus(sessionID, status2) { - const task2 = this.tasks.get(sessionID); - if (!task2) + observeSessionStatus(sessionID, status) { + const task = this.tasks.get(sessionID); + if (!task) return; - if (status2 === "busy") { - this.markRunning(task2.parentSessionID, sessionID); + if (status === "busy") { + this.markRunning(task.parentSessionID, sessionID); return; } - if (status2 === "idle") - this.markTerminal(sessionID, "completed", task2.parentSessionID); + if (status === "idle") + this.markTerminal(sessionID, "completed", task.parentSessionID); } observeSessionDeleted(sessionID) { this.tasks.delete(sessionID); - for (const task2 of this.tasks.values()) { - if (task2.parentSessionID === sessionID) - this.tasks.delete(task2.taskID); + for (const task of this.tasks.values()) { + if (task.parentSessionID === sessionID) + this.tasks.delete(task.taskID); } this.latestAssistantBySession.delete(sessionID); this.clearSnapshotIdleForSession(sessionID); @@ -2762,13 +2762,13 @@ class TaskTracker { continue; } for (const part of message.parts ?? []) { - const status2 = parseTaskStatus(textFromPart(part)); - if (!status2) + const status = parseTaskStatus(textFromPart(part)); + if (!status) continue; - if (status2.state === "running") - this.markRunning(sessionID, status2.taskID); + if (status.state === "running") + this.markRunning(sessionID, status.taskID); else - this.markTerminal(status2.taskID, status2.state, sessionID, { resetReconciled: true }); + this.markTerminal(status.taskID, status.state, sessionID, { resetReconciled: true }); } } } @@ -2792,25 +2792,25 @@ class TaskTracker { continue; if (entry.state.status === "streaming" || entry.state.status === "running") continue; - const status2 = parseTaskStatus(toolTextFromV2Content(entry.state.content ?? [])); - if (!status2) + const status = parseTaskStatus(toolTextFromV2Content(entry.state.content ?? [])); + if (!status) continue; - if (status2.state === "running") - this.markRunning(parentSessionID, status2.taskID); + if (status.state === "running") + this.markRunning(parentSessionID, status.taskID); else - this.markTerminal(status2.taskID, status2.state, parentSessionID, { resetReconciled: true, terminalAt }); + this.markTerminal(status.taskID, status.state, parentSessionID, { resetReconciled: true, terminalAt }); } } } hasBlockingTasks(parentSessionID, maxBlockMs = null) { this.pruneExpiredSnapshotIdleHolds(); const now = Date.now(); - for (const task2 of this.tasks.values()) { - if (task2.parentSessionID !== parentSessionID) + for (const task of this.tasks.values()) { + if (task.parentSessionID !== parentSessionID) continue; - if (task2.state !== "running" && !task2.terminalUnreconciled) + if (task.state !== "running" && !task.terminalUnreconciled) continue; - if (taskBlockExpired(task2, maxBlockMs, now)) + if (taskBlockExpired(task, maxBlockMs, now)) continue; return true; } @@ -2853,8 +2853,8 @@ class TaskTracker { return; } for (const childID of childIDs) { - const status2 = statuses[childID]; - const statusType = isRecord(status2) && typeof status2.type === "string" ? status2.type : undefined; + const status = statuses[childID]; + const statusType = isRecord(status) && typeof status.type === "string" ? status.type : undefined; if (statusType === "busy") this.markRunning(parentSessionID, childID); else if (statusType === "idle") { @@ -2932,16 +2932,16 @@ class TaskTracker { continue; this.snapshotIdleHolds.delete(key); this.settledSnapshotIdleTasks.add(key); - const task2 = this.tasks.get(hold.taskID); - if (task2?.parentSessionID === hold.parentSessionID && task2.state === "running") + const task = this.tasks.get(hold.taskID); + if (task?.parentSessionID === hold.parentSessionID && task.state === "running") this.tasks.delete(hold.taskID); } } markAbsentRunningChildren(parentSessionID, liveChildIDs) { - for (const task2 of this.tasks.values()) { - if (task2.parentSessionID !== parentSessionID || task2.state !== "running" || liveChildIDs.has(task2.taskID)) + for (const task of this.tasks.values()) { + if (task.parentSessionID !== parentSessionID || task.state !== "running" || liveChildIDs.has(task.taskID)) continue; - this.markSnapshotIdle(parentSessionID, task2.taskID); + this.markSnapshotIdle(parentSessionID, task.taskID); } } snapshotIdleKey(parentSessionID, taskID) { @@ -2949,18 +2949,18 @@ class TaskTracker { } observeAssistant(sessionID, marker) { this.latestAssistantBySession.set(sessionID, marker); - for (const task2 of this.tasks.values()) { - if (task2.parentSessionID !== sessionID || !task2.terminalUnreconciled) + for (const task of this.tasks.values()) { + if (task.parentSessionID !== sessionID || !task.terminalUnreconciled) continue; - if (this.assistantReconcilesTask(task2, marker)) { - this.tasks.set(task2.taskID, { ...task2, terminalUnreconciled: false }); + if (this.assistantReconcilesTask(task, marker)) { + this.tasks.set(task.taskID, { ...task, terminalUnreconciled: false }); } } } - assistantReconcilesTask(task2, marker) { - if (marker.id && task2.lastAssistantMessageIDAtTerminal && marker.id !== task2.lastAssistantMessageIDAtTerminal) + assistantReconcilesTask(task, marker) { + if (marker.id && task.lastAssistantMessageIDAtTerminal && marker.id !== task.lastAssistantMessageIDAtTerminal) return true; - if (marker.completedAt != null && task2.terminalAt != null && marker.completedAt >= task2.terminalAt) + if (marker.completedAt != null && task.terminalAt != null && marker.completedAt >= task.terminalAt) return true; return false; } @@ -2969,12 +2969,12 @@ async function recordAssistantMessage(sessionID, message, options, evaluateConti if (!message) return { goal: null, progressed: false }; const before = await getGoal(sessionID); - const id2 = messageID(message) ?? ""; - const text2 = textFromMessage(message); - const progressed = Boolean(/[\p{L}\p{N}]/u.test(text2) && (id2 !== (before?.lastAssistantMessageID ?? "") || text2 !== (before?.lastAssistantText ?? ""))); + const id = messageID(message) ?? ""; + const text = textFromMessage(message); + const progressed = Boolean(/[\p{L}\p{N}]/u.test(text) && (id !== (before?.lastAssistantMessageID ?? "") || text !== (before?.lastAssistantText ?? ""))); const goal = await recordAssistantProgress(sessionID, { - messageID: id2, - text: text2, + messageID: id, + text, outputTokens: outputTokensFromMessage(message) ?? null, noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold), maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns), @@ -3004,6 +3004,13 @@ var planToolArgs = { revisit_evidence: z2.string().trim().min(1).max(2000).optional() }; var PlanToolSchema = z2.object(planToolArgs).strict(); +var planToolInputSchema = z2.toJSONSchema(PlanToolSchema, { + io: "input", + unrepresentable: "any" +}); +function planToolInput() { + return JSON.parse(JSON.stringify(planToolInputSchema)); +} async function planFromTool(args, context) { const input = PlanToolSchema.parse(args); return JSON.stringify({ @@ -3173,10 +3180,10 @@ async function updateGoalObjectiveFromTool(input, context, services) { } async function closeGoalFromTool(input, context, services) { if (input.status === "complete") { - const goal2 = await completeGoal(context.sessionID, input.evidence ?? "", services.maxObjectiveChars); - const budget = goal2.tokenBudget == null ? "" : ` ${services.messages.reports.tokenUsage}: ${goal2.tokensUsed}/${goal2.tokenBudget}.`; - const report2 = `${services.messages.reports.achieved} ${services.messages.reports.timeUsed}: ` + `${goal2.timeUsedSeconds} ${services.messages.reports.seconds}.${budget} ` + `${services.messages.reports.evidence}: ${goal2.completionEvidence}.`; - return JSON.stringify({ goal: goal2, completion_report: report2 }, null, 2); + const goal = await completeGoal(context.sessionID, input.evidence ?? "", services.maxObjectiveChars); + const budget = goal.tokenBudget == null ? "" : ` ${services.messages.reports.tokenUsage}: ${goal.tokensUsed}/${goal.tokenBudget}.`; + const report = `${services.messages.reports.achieved} ${services.messages.reports.timeUsed}: ` + `${goal.timeUsedSeconds} ${services.messages.reports.seconds}.${budget} ` + `${services.messages.reports.evidence}: ${goal.completionEvidence}.`; + return JSON.stringify({ goal, completion_report: report }, null, 2); } const goal = await markGoalUnmet(context.sessionID, input.blocker ?? "", services.maxObjectiveChars); const report = `${services.messages.reports.unmet} ${services.messages.reports.timeUsed}: ` + `${goal.timeUsedSeconds} ${services.messages.reports.seconds}. ` + `${services.messages.reports.blocker}: ${goal.blocker}.`; @@ -3219,9 +3226,9 @@ function textFromToolResult(result) { if (typeof result.content === "string") return result.content; if (Array.isArray(result.content)) { - const text2 = result.content.map(textFromPart).filter(Boolean).join(` + const text = result.content.map(textFromPart).filter(Boolean).join(` `).trim(); - return text2 || undefined; + return text || undefined; } return; } @@ -3743,9 +3750,9 @@ var server = async ({ client }, options) => { const sanitized = escapeGoalCommandArguments(output, goalCommandTemplate(commandName, locale), input.arguments); objectiveEdits.delete(input.sessionID); const edit = /^edit\s+([\s\S]+)$/i.exec(input.arguments.trim()); - const goal2 = edit && sanitized ? await getGoal(input.sessionID) : null; - if (goal2 && edit) - objectiveEdits.set(input.sessionID, { goalID: goal2.id, objective: edit[1].trim() }); + const goal = edit && sanitized ? await getGoal(input.sessionID) : null; + if (goal && edit) + objectiveEdits.set(input.sessionID, { goalID: goal.id, objective: edit[1].trim() }); if (sanitized && input.arguments.trim().toLowerCase() === "resume") { explicitResumeRequests.add(input.sessionID); } @@ -3786,15 +3793,15 @@ var server = async ({ client }, options) => { const toolResult = output; if (toolOutputFailed(toolResult)) return; - const text2 = typeof toolResult.output === "string" ? toolResult.output : undefined; - if (!text2) + const text = typeof toolResult.output === "string" ? toolResult.output : undefined; + if (!text) return; const before = await getGoalInternal(sessionID); const scheduled = scheduledContinuations.get(sessionID); const hasFailureEpisode = Boolean(before && (before.continuationFailures > 0 || before.pendingAttempt != null)); if (!before || !hasFailureEpisode && scheduled?.purpose !== "recovery") return; - const progressed = await recordToolProgress(sessionID, text2, expectedAttemptID); + const progressed = await recordToolProgress(sessionID, text, expectedAttemptID); if (progressed?.continuationFailures === 0 && progressed.pendingAttempt == null) { locallyDeliveredPendingSessions.delete(sessionID); cancelScheduledContinuation(sessionID); @@ -3860,29 +3867,29 @@ var server = async ({ client }, options) => { taskTracker.observeSessionCreated(event); } if (sessionID && eventType === "session.status") { - const status2 = event.properties?.status; - if (isRecord(status2) && typeof status2.type === "string") { - if (status2.type === "busy") { + const status = event.properties?.status; + if (isRecord(status) && typeof status.type === "string") { + if (status.type === "busy") { busySessions.add(sessionID); nativeRetrySessions.delete(sessionID); } - if (status2.type === "busy") + if (status.type === "busy") armTurnWatchdog(sessionID); - if (status2.type === "busy") + if (status.type === "busy") await markPendingContinuationStarted(sessionID); - if (status2.type === "idle") { + if (status.type === "idle") { explicitResumeRequests.delete(sessionID); busySessions.delete(sessionID); nativeRetrySessions.delete(sessionID); clearTurnWatchdog(sessionID); watchdogRescuedSessions.delete(sessionID); } - if (status2.type === "retry") { + if (status.type === "retry") { nativeRetrySessions.add(sessionID); clearTurnWatchdog(sessionID); cancelScheduledContinuation(sessionID); } - taskTracker.observeSessionStatus(sessionID, status2.type); + taskTracker.observeSessionStatus(sessionID, status.type); } } if (sessionID && eventType === "session.idle") { @@ -4036,10 +4043,10 @@ async function setupV2(context) { const registrations = []; let disposed = false; let eventConsumerStopped = false; - function stepKey(sessionID, messageID2) { - return `${sessionID}\x00${messageID2}`; + function stepKey(sessionID, messageID) { + return `${sessionID}\x00${messageID}`; } - async function sendContinuation2(sessionID, prompt, agent) { + async function sendContinuation(sessionID, prompt, agent) { markSessionOwnership(sessionID, true); await context.session.prompt({ sessionID, @@ -4112,7 +4119,7 @@ async function setupV2(context) { watchdogRescuedSessions.add(sessionID); if (!isCurrent()) return; - await sendContinuation2(sessionID, continuationPrompt(current, locale), current.lastPromptAgent ?? latestStep?.agent ?? null); + await sendContinuation(sessionID, continuationPrompt(current, locale), current.lastPromptAgent ?? latestStep?.agent ?? null); if (!isCurrent()) return; const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures, { @@ -4301,7 +4308,7 @@ async function setupV2(context) { await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID }); return; } - await sendContinuation2(sessionID, goal.status === "active" ? continuationPrompt(goal, locale) : limitPrompt(goal, locale), goal.lastPromptAgent ?? latestTurnAgent ?? null); + await sendContinuation(sessionID, goal.status === "active" ? continuationPrompt(goal, locale) : limitPrompt(goal, locale), goal.lastPromptAgent ?? latestTurnAgent ?? null); if (!isCurrent()) { await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID }); return; @@ -4424,29 +4431,29 @@ async function setupV2(context) { case "session.execution.started": case "session.retry.scheduled": case "session.status": { - const status2 = event.type === "session.execution.started" ? { type: "busy" } : event.type === "session.retry.scheduled" ? { type: "retry" } : data.status; - if (sessionID && isRecord(status2) && typeof status2.type === "string") { - if (status2.type === "busy") { + const status = event.type === "session.execution.started" ? { type: "busy" } : event.type === "session.retry.scheduled" ? { type: "retry" } : data.status; + if (sessionID && isRecord(status) && typeof status.type === "string") { + if (status.type === "busy") { stoppedExecutions.delete(sessionID); busySessions.add(sessionID); nativeRetrySessions.delete(sessionID); armTurnWatchdog(sessionID); await markPendingContinuationStarted(sessionID); } - if (status2.type === "idle") { + if (status.type === "idle") { explicitResumeRequests.delete(sessionID); busySessions.delete(sessionID); nativeRetrySessions.delete(sessionID); clearTurnWatchdog(sessionID); watchdogRescuedSessions.delete(sessionID); } - if (status2.type === "retry") { + if (status.type === "retry") { nativeRetrySessions.add(sessionID); clearTurnWatchdog(sessionID); cancelScheduledContinuation(sessionID); } - taskTracker.observeSessionStatus(sessionID, status2.type); - if (status2.type === "idle") { + taskTracker.observeSessionStatus(sessionID, status.type); + if (status.type === "idle") { const goal = await getGoalInternal(sessionID); if (autoContinue || goal?.pendingAttempt != null) await runAutoContinue(sessionID); @@ -4557,16 +4564,16 @@ async function setupV2(context) { case "session.step.started": { if (!sessionID || typeof data.assistantMessageID !== "string") return; - const messageID2 = data.assistantMessageID; + const messageID = data.assistantMessageID; const agent = typeof data.agent === "string" ? data.agent : undefined; if (agent) await recordPromptAgent(sessionID, agent); taskTracker.observeAssistantMessage(sessionID, { - info: { id: messageID2, role: "assistant", time: { completed: event.created } } + info: { id: messageID, role: "assistant", time: { completed: event.created } } }); - if (!stepTextBuffers.has(stepKey(sessionID, messageID2))) - stepTextBuffers.set(stepKey(sessionID, messageID2), ""); - latestStepBySession.set(sessionID, { messageID: messageID2, agent, text: "", outputTokens: null, completedAt: event.created }); + if (!stepTextBuffers.has(stepKey(sessionID, messageID))) + stepTextBuffers.set(stepKey(sessionID, messageID), ""); + latestStepBySession.set(sessionID, { messageID, agent, text: "", outputTokens: null, completedAt: event.created }); return; } case "session.text.delta": { @@ -4585,7 +4592,7 @@ async function setupV2(context) { case "session.step.ended": { if (!sessionID || typeof data.assistantMessageID !== "string") return; - const messageID2 = data.assistantMessageID; + const messageID = data.assistantMessageID; const tokens = tokensFromRecord(data.tokens); if (typeof tokens === "number") { const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens; @@ -4596,27 +4603,27 @@ async function setupV2(context) { initialBaseline: Math.ceil(sum - tokens) }); } - const text2 = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? ""; - stepTextBuffers.delete(stepKey(sessionID, messageID2)); + const text = stepTextBuffers.get(stepKey(sessionID, messageID)) ?? ""; + stepTextBuffers.delete(stepKey(sessionID, messageID)); const outputTokens = outputTokensFromRecord(data.tokens) ?? null; const afterStep = await recordAssistantProgress(sessionID, { - messageID: messageID2, - text: text2, + messageID, + text, outputTokens, noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold), maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns), completedAt: event.created }); await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, afterStep); - if (/[\p{L}\p{N}]/u.test(text2)) { + if (/[\p{L}\p{N}]/u.test(text)) { const scheduled = scheduledContinuations.get(sessionID); if (scheduled?.purpose === "recovery") cancelScheduledContinuation(sessionID); } latestStepBySession.set(sessionID, { - messageID: messageID2, + messageID, agent: latestStepBySession.get(sessionID)?.agent, - text: text2, + text, outputTokens, completedAt: event.created }); @@ -4625,7 +4632,7 @@ async function setupV2(context) { case "session.step.failed": { if (!sessionID || typeof data.assistantMessageID !== "string") return; - const messageID2 = data.assistantMessageID; + const messageID = data.assistantMessageID; const tokens = tokensFromRecord(data.tokens); if (typeof tokens === "number") { const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens; @@ -4636,27 +4643,27 @@ async function setupV2(context) { initialBaseline: Math.ceil(sum - tokens) }); } - const text2 = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? ""; - stepTextBuffers.delete(stepKey(sessionID, messageID2)); + const text = stepTextBuffers.get(stepKey(sessionID, messageID)) ?? ""; + stepTextBuffers.delete(stepKey(sessionID, messageID)); const outputTokens = outputTokensFromRecord(data.tokens) ?? null; const afterStep = await recordAssistantProgress(sessionID, { - messageID: messageID2, - text: text2, + messageID, + text, outputTokens, noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold), maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns), completedAt: event.created }); await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, afterStep); - if (/[\p{L}\p{N}]/u.test(text2)) { + if (/[\p{L}\p{N}]/u.test(text)) { const scheduled = scheduledContinuations.get(sessionID); if (scheduled?.purpose === "recovery") cancelScheduledContinuation(sessionID); } latestStepBySession.set(sessionID, { - messageID: messageID2, + messageID, agent: latestStepBySession.get(sessionID)?.agent, - text: text2, + text, outputTokens, completedAt: event.created }); @@ -4837,7 +4844,7 @@ async function setupV2(context) { if (goal || input.tool === "clear_goal") input.result = { ...input.result, metadata: { ...input.result.metadata, ...acpPlanMetadata(goal) } }; } - const text2 = textFromToolResult(input.result); + const text = textFromToolResult(input.result); taskTracker.noteTaskOutput({ tool: input.tool, sessionID: input.sessionID, callID: input.id }, { output: textFromToolResult(input.result) }); if (!sessionID || typeof input.tool !== "string") return; @@ -4845,14 +4852,14 @@ async function setupV2(context) { return; if (toolOutputFailed(input.result)) return; - if (!text2) + if (!text) return; const before = await getGoalInternal(sessionID); const scheduled = scheduledContinuations.get(sessionID); const hasFailureEpisode = Boolean(before && (before.continuationFailures > 0 || before.pendingAttempt != null)); if (!before || !hasFailureEpisode && scheduled?.purpose !== "recovery") return; - const progressed = await recordToolProgress(sessionID, text2, expectedAttemptID); + const progressed = await recordToolProgress(sessionID, text, expectedAttemptID); if (progressed?.continuationFailures === 0 && progressed.pendingAttempt == null) { locallyDeliveredPendingSessions.delete(sessionID); cancelScheduledContinuation(sessionID); @@ -4948,7 +4955,7 @@ function goalToolsV2(services) { { name: "update_goal_plan", description: services.locale === "zh-CN" ? "\u4FDD\u5B58\u76EE\u6807\u7684\u6574\u4F53\u8BA1\u5212\u3001\u9636\u6BB5\u3001\u4EFB\u52A1\u548C\u9A8C\u8BC1\u8BC1\u636E\u3002\u4FDD\u6301\u6574\u4F53\u76EE\u6807\u4E0D\u53D8\uFF1B\u4F7F\u7528 get_goal \u8FD4\u56DE\u7684\u76EE\u6807 ID \u548C\u8BA1\u5212\u7248\u672C\u3002" : "Persist the overall plan, phases, tasks, verification evidence and decisions. Preserve the goal scope; use the goal ID and planRevision from get_goal. Completed work cannot be silently reopened or removed.", - input: v2ObjectSchema(planToolArgs), + input: planToolInput(), options: { codemode: false }, execute: async (args, context) => ({ content: await planFromTool(args, context) }) }, diff --git a/src/server.ts b/src/server.ts index c0ac50e..4caebfe 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1059,6 +1059,35 @@ const planToolArgs = { } const PlanToolSchema = z.object(planToolArgs).strict() +/** + * JSON Schema for the V2 update_goal_plan tool input. + * + * Passing the raw zod objects from planToolArgs as the V2 tool `input` leaks + * zod internals into the schema hosts send to providers: serialized artifacts + * like `def`, `checks`, `shape`, `optional`, and null-valued keywords such as + * `maxLength: null` or `format: null`. Providers that strictly validate tool + * schemas (for example Mistral mistral-large-4) reject those requests with + * `Invalid tool schema`, breaking every session that includes the goal tools. + * Convert once here; runtime argument validation is unaffected because + * planFromTool still parses through PlanToolSchema. + */ +const planToolInputSchema = z.toJSONSchema(PlanToolSchema, { + io: "input", + unrepresentable: "any", +}) as ToolSchema.ValueSchema + +/** + * Deep copy of {@link planToolInputSchema} for each tool registration. + * + * Registrations share one module instance, so handing out the schema object + * itself would let any host (or test framework) that mutates a received tool + * input corrupt the schema for every other registration. The schema is pure + * JSON, so a JSON round-trip is a sufficient deep copy. + */ +function planToolInput(): ToolSchema.ValueSchema { + return JSON.parse(JSON.stringify(planToolInputSchema)) as ToolSchema.ValueSchema +} + async function planFromTool(args: unknown, context: ToolExecContext) { const input = PlanToolSchema.parse(args) return JSON.stringify( @@ -3356,7 +3385,7 @@ function goalToolsV2(services: GoalServices): ToolV2Info[] { { name: "update_goal_plan", description: services.locale === "zh-CN" ? "保存目标的整体计划、阶段、任务和验证证据。保持整体目标不变;使用 get_goal 返回的目标 ID 和计划版本。" : "Persist the overall plan, phases, tasks, verification evidence and decisions. Preserve the goal scope; use the goal ID and planRevision from get_goal. Completed work cannot be silently reopened or removed.", - input: v2ObjectSchema(planToolArgs), + input: planToolInput(), options: { codemode: false }, execute: async (args, context) => ({ content: await planFromTool(args, context) }), }, diff --git a/test/server-v2.test.ts b/test/server-v2.test.ts index b71f018..90d5f78 100644 --- a/test/server-v2.test.ts +++ b/test/server-v2.test.ts @@ -671,6 +671,65 @@ test("V2 plan tools publish structured ACP metadata and compaction retains the p expect(JSON.stringify(compaction)).toContain("compound") }) +test("V2 update_goal_plan input is provider-safe JSON Schema", async () => { + const mock = makeMockContext() + await setupPlugin(mock as never) + const input = goalTool(mock, "update_goal_plan").input as unknown + + const zodArtifactKeys = ["def", "checks", "shape", "optional", "catchall", "isFinite", "minValue", "maxValue", "isInt"] + const walk = (node: unknown, path: string, visit: (key: string, value: unknown, path: string) => void) => { + if (Array.isArray(node)) { + node.forEach((child, index) => walk(child, `${path}[${index}]`, visit)) + return + } + if (node === null || typeof node !== "object") return + for (const [key, value] of Object.entries(node as Record)) { + visit(key, value, `${path}.${key}`) + walk(value, `${path}.${key}`, visit) + } + } + + const invalid: string[] = [] + walk(input, "input", (key, value, path) => { + if (zodArtifactKeys.includes(key)) invalid.push(`zod artifact key "${key}" at ${path}`) + if (value === null) invalid.push(`null value for "${key}" at ${path}`) + }) + expect(invalid).toEqual([]) + + const root = input as { type?: string; properties?: Record; required?: string[]; additionalProperties?: boolean } + expect(root.type).toBe("object") + expect(root.additionalProperties).toBe(false) + expect(Object.keys(root.properties ?? {}).sort()).toEqual([ + "expected_revision", + "goal_id", + "plan", + "reason", + "revisit_evidence", + ]) + expect(root.required?.sort()).toEqual(["expected_revision", "goal_id", "plan", "reason"]) +}) + +test("V2 update_goal_plan registrations do not share a mutable input schema", async () => { + const first = makeMockContext({ auto_continue: false }) + await setupPlugin(first as never) + const second = makeMockContext({ auto_continue: false }) + await setupPlugin(second as never) + + // Simulate a host or test framework that mutates a received tool input. + const corrupted = (goalTool(first, "update_goal_plan").input as { properties?: Record }) + .properties + if (corrupted) for (const key of Object.keys(corrupted)) delete corrupted[key] + + const survivor = goalTool(second, "update_goal_plan").input as { properties?: Record } + expect(Object.keys(survivor.properties ?? {}).sort()).toEqual([ + "expected_revision", + "goal_id", + "plan", + "reason", + "revisit_evidence", + ]) +}) + test("V2 setup registers /goal, /pause_goal, and /resume_goal via command transform", async () => { const mock = makeMockContext({ auto_continue: false }) const cleanup = await setupPlugin(mock as never) From 716cb375b786659d6c0ca6e78105e5f5a9548d09 Mon Sep 17 00:00:00 2001 From: Federico Baldassarre Date: Fri, 9 Oct 2026 16:09:40 +0000 Subject: [PATCH 2/2] test: port nested plan schema assertions from #68; rebuild dist with minimal diff Co-authored-by: abeisleem --- dist/server.js | 412 ++++++++++++++++++++--------------------- src/server.ts | 9 +- test/server-v2.test.ts | 47 +++++ 3 files changed, 260 insertions(+), 208 deletions(-) diff --git a/dist/server.js b/dist/server.js index 5c1d4c1..4423a37 100644 --- a/dist/server.js +++ b/dist/server.js @@ -31,34 +31,34 @@ var GoalPlanInputSchema = z.object({ const ids = new Set; let runningPhases = 0; let runningTasks = 0; - for (const phase of plan.phases) { - if (ids.has(phase.id)) + for (const phase2 of plan.phases) { + if (ids.has(phase2.id)) ctx.addIssue({ code: "custom", message: "plan IDs must be unique" }); - ids.add(phase.id); - if (phase.status === "in_progress") + ids.add(phase2.id); + if (phase2.status === "in_progress") runningPhases++; - if (phase.status === "blocked" && !phase.blocker) + if (phase2.status === "blocked" && !phase2.blocker) ctx.addIssue({ code: "custom", message: "blocked phases require a blocker" }); - if (phase.status === "completed" && (!phase.verification || phase.tasks.some((task) => task.status !== "completed"))) { + if (phase2.status === "completed" && (!phase2.verification || phase2.tasks.some((task2) => task2.status !== "completed"))) { ctx.addIssue({ code: "custom", message: "completed phases require verified tasks and phase verification" }); } - for (const task of phase.tasks) { - if (ids.has(task.id)) + for (const task2 of phase2.tasks) { + if (ids.has(task2.id)) ctx.addIssue({ code: "custom", message: "plan IDs must be unique" }); - ids.add(task.id); - if (task.status === "completed" && !task.evidence) + ids.add(task2.id); + if (task2.status === "completed" && !task2.evidence) ctx.addIssue({ code: "custom", message: "completed tasks require evidence" }); - if (task.status === "blocked" && !task.blocker) + if (task2.status === "blocked" && !task2.blocker) ctx.addIssue({ code: "custom", message: "blocked tasks require a blocker" }); - if (task.status === "in_progress") { + if (task2.status === "in_progress") { runningTasks++; - if (phase.status !== "in_progress") + if (phase2.status !== "in_progress") ctx.addIssue({ code: "custom", message: "running tasks require a running phase" }); } } } - const firstUnfinished = plan.phases.find((phase) => phase.status !== "completed"); - if (plan.phases.some((phase) => phase.status === "in_progress" && phase !== firstUnfinished)) + const firstUnfinished = plan.phases.find((phase2) => phase2.status !== "completed"); + if (plan.phases.some((phase2) => phase2.status === "in_progress" && phase2 !== firstUnfinished)) ctx.addIssue({ code: "custom", message: "verify the current phase before starting the next phase" }); if (runningPhases > 1 || runningTasks > 1) ctx.addIssue({ code: "custom", message: "choose one current phase and task" }); @@ -86,9 +86,9 @@ function reviseGoalPlan(previous, input, expectedRevision, reason, now, revisitE throw new Error("preserve overall completion criteria; replace the goal for a new scope"); } for (const oldPhase of previous.phases) { - const newPhase = next.phases.find((phase) => phase.id === oldPhase.id); + const newPhase = next.phases.find((phase2) => phase2.id === oldPhase.id); if (!newPhase) { - if (oldPhase.status === "completed" || oldPhase.tasks.some((task) => task.status === "completed")) + if (oldPhase.status === "completed" || oldPhase.tasks.some((task2) => task2.status === "completed")) throw new Error("preserve verified phase and task history"); if (!revisitEvidence?.trim()) throw new Error("removing planned scope requires concrete revisit evidence"); @@ -102,7 +102,7 @@ function reviseGoalPlan(previous, input, expectedRevision, reason, now, revisitE throw new Error("reopening a verified phase requires concrete revisit evidence"); } for (const oldTask of oldPhase.tasks) { - const newTask = newPhase.tasks.find((task) => task.id === oldTask.id); + const newTask = newPhase.tasks.find((task2) => task2.id === oldTask.id); if (oldTask.status !== "completed") { if ((!newTask || newTask.description !== oldTask.description) && !revisitEvidence?.trim()) throw new Error("removing or changing planned task scope requires concrete revisit evidence"); @@ -133,23 +133,23 @@ function reviseGoalPlan(previous, input, expectedRevision, reason, now, revisitE }); } function goalPlanProgress(plan) { - const current = plan.phases.find((phase) => phase.status === "in_progress") ?? plan.phases.find((phase) => phase.status !== "completed"); - const running = current?.tasks.find((task) => task.status === "in_progress"); - const next = current?.tasks.find((task) => task.status === "pending"); + const current = plan.phases.find((phase2) => phase2.status === "in_progress") ?? plan.phases.find((phase2) => phase2.status !== "completed"); + const running = current?.tasks.find((task2) => task2.status === "in_progress"); + const next = current?.tasks.find((task2) => task2.status === "pending"); return { currentPhaseID: current?.id ?? null, currentTaskID: running?.id ?? null, nextTaskID: next?.id ?? null, - nextPhaseID: plan.phases.find((phase) => phase.id !== current?.id && phase.status !== "completed")?.id ?? null, - completedPhaseIDs: plan.phases.filter((phase) => phase.status === "completed").map((phase) => phase.id), - completedTaskIDs: plan.phases.flatMap((phase) => phase.tasks.filter((task) => task.status === "completed").map((task) => task.id)) + nextPhaseID: plan.phases.find((phase2) => phase2.id !== current?.id && phase2.status !== "completed")?.id ?? null, + completedPhaseIDs: plan.phases.filter((phase2) => phase2.status === "completed").map((phase2) => phase2.id), + completedTaskIDs: plan.phases.flatMap((phase2) => phase2.tasks.filter((task2) => task2.status === "completed").map((task2) => task2.id)) }; } function goalPlanEntries(plan) { - return plan.phases.flatMap((phase) => phase.tasks.map((task) => ({ - content: `${phase.objective}: ${task.description}${task.blocker ? ` \u2014 Blocked: ${task.blocker}` : ""}`, + return plan.phases.flatMap((phase2) => phase2.tasks.map((task2) => ({ + content: `${phase2.objective}: ${task2.description}${task2.blocker ? ` \u2014 Blocked: ${task2.blocker}` : ""}`, priority: "medium", - status: task.status === "blocked" ? "pending" : task.status + status: task2.status === "blocked" ? "pending" : task2.status }))); } @@ -691,11 +691,11 @@ function nonNegativeInteger(value, fallback) { function nonNegativeIntegerOrNull(value) { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null; } -function isClosed(status) { - return status === "complete" || status === "unmet" || status === "cancelled"; +function isClosed(status2) { + return status2 === "complete" || status2 === "unmet" || status2 === "cancelled"; } -function canContinue(status) { - return status === "active"; +function canContinue(status2) { + return status2 === "active"; } function remainingTokens(goal) { return goal.tokenBudget == null ? null : Math.max(0, goal.tokenBudget - goal.tokensUsed); @@ -913,7 +913,7 @@ async function createGoal(sessionID, objective, options) { return snapshot(goal); }); } -async function updateGoalObjective(sessionID, objective, status = "active", options) { +async function updateGoalObjective(sessionID, objective, status2 = "active", options) { const value = validateObjective(objective, resolveMaxObjectiveChars(options?.maxObjectiveChars)); const agent = typeof options?.agent === "string" && options.agent.trim() ? options.agent.trim() : null; const planModePause = options?.planModePause === true; @@ -932,7 +932,7 @@ async function updateGoalObjective(sessionID, objective, status = "active", opti goal.planRevision += 1; } goal.objective = value; - goal.status = planModePause ? "paused" : status; + goal.status = planModePause ? "paused" : status2; goal.updatedAt = nowSeconds(); goal.lastAccountedAt = goal.status === "active" ? goal.updatedAt : null; goal.completionEvidence = null; @@ -985,7 +985,7 @@ async function pauseGoalForPlanMode(sessionID) { return snapshot(goal); }); } -async function setGoalStatus(sessionID, status, agent, options) { +async function setGoalStatus(sessionID, status2, agent, options) { const agentValue = typeof agent === "string" && agent.trim() ? agent.trim() : null; return mutate((state) => { const goal = state.goals[sessionID]; @@ -993,26 +993,26 @@ async function setGoalStatus(sessionID, status, agent, options) { throw new Error("cannot update goal because this session has no goal"); if (isClosed(goal.status)) throw new Error("cannot update goal status because this goal is closed"); - if (goal.status === status) + if (goal.status === status2) return snapshot(goal); - if (status === "paused" && goal.status !== "active") + if (status2 === "paused" && goal.status !== "active") return snapshot(goal); - const resumesAutoTurnLimit = options?.resetAutoTurnLimit === true && status === "active" && goal.status === "usageLimited" && goal.stopReason?.startsWith(MAX_AUTO_CONTINUES_STOP_REASON_PREFIX) === true; + const resumesAutoTurnLimit = options?.resetAutoTurnLimit === true && status2 === "active" && goal.status === "usageLimited" && goal.stopReason?.startsWith(MAX_AUTO_CONTINUES_STOP_REASON_PREFIX) === true; accountWallClock(goal); - goal.status = status; + goal.status = status2; goal.updatedAt = nowSeconds(); - goal.lastAccountedAt = status === "active" ? goal.updatedAt : null; + goal.lastAccountedAt = status2 === "active" ? goal.updatedAt : null; goal.autoTurns = resumesAutoTurnLimit ? 0 : goal.autoTurns; - goal.continuationFailures = status === "active" ? 0 : goal.continuationFailures; - goal.pendingAttempt = status === "active" ? null : goal.pendingAttempt; - goal.noProgressTurns = status === "active" ? 0 : goal.noProgressTurns; - goal.stopReason = status === "active" ? null : "paused"; - goal.budgetWrapupSent = status === "active" ? false : goal.budgetWrapupSent; - goal.blocker = status === "active" ? null : goal.blocker; + goal.continuationFailures = status2 === "active" ? 0 : goal.continuationFailures; + goal.pendingAttempt = status2 === "active" ? null : goal.pendingAttempt; + goal.noProgressTurns = status2 === "active" ? 0 : goal.noProgressTurns; + goal.stopReason = status2 === "active" ? null : "paused"; + goal.budgetWrapupSent = status2 === "active" ? false : goal.budgetWrapupSent; + goal.blocker = status2 === "active" ? null : goal.blocker; if (agentValue) goal.lastPromptAgent = agentValue; - goal.lastStatus = status === "active" ? "Goal resumed." : "Goal paused."; - pushHistory(goal, status === "active" ? "resumed" : "paused", goal.lastStatus); + goal.lastStatus = status2 === "active" ? "Goal resumed." : "Goal paused."; + pushHistory(goal, status2 === "active" ? "resumed" : "paused", goal.lastStatus); return snapshot(goal); }); } @@ -1024,7 +1024,7 @@ async function closeGoal(sessionID, input, maxObjectiveChars = DEFAULT_MAX_OBJEC throw new Error("cannot update goal because this session has no goal"); if (isClosed(goal.status)) throw new Error("cannot close goal because this goal is already closed"); - if (input.status === "complete" && goal.plan?.phases.some((phase) => phase.status !== "completed")) { + if (input.status === "complete" && goal.plan?.phases.some((phase2) => phase2.status !== "completed")) { throw new Error("cannot complete the overall goal while planned phases still require work or verification"); } accountWallClock(goal); @@ -1186,20 +1186,20 @@ async function recordAssistantProgress(sessionID, input) { const goal = state.goals[sessionID]; if (!goal || goal.status !== "active") return goal ? snapshot(goal) : null; - const text = input.text?.trim() ?? ""; + const text2 = input.text?.trim() ?? ""; const messageID = input.messageID?.trim() ?? ""; const outputTokens = positiveIntegerOrNull(input.outputTokens) ?? 0; const threshold = positiveIntegerOrNull(input.noProgressTokenThreshold) ?? goal.noProgressTokenThreshold; const maxNoProgressTurns = positiveIntegerOrNull(input.maxNoProgressTurns) ?? goal.maxNoProgressTurns; - const summary = summarizeText(text); - const substantive = /[\p{L}\p{N}]/u.test(text); + const summary = summarizeText(text2); + const substantive = /[\p{L}\p{N}]/u.test(text2); const previousSummary = summarizeText(goal.lastAssistantText); const repeatedMessage = Boolean(messageID && messageID === goal.lastAssistantMessageID); const changed = Boolean(summary && summary !== previousSummary); if (summary && (!repeatedMessage || changed)) recordCheckpoint(goal, summary); - if (text) - goal.lastAssistantText = text; + if (text2) + goal.lastAssistantText = text2; if (messageID) goal.lastAssistantMessageID = messageID; if (substantive && summary && (!repeatedMessage || changed)) { @@ -1361,8 +1361,8 @@ async function markPendingContinuationStarted(sessionID) { return current ? snapshotInternal(current) : null; if (current.pendingAttempt == null || current.pendingAttempt.started) return snapshotInternal(current); - return mutate((state) => { - const goal = state.goals[sessionID]; + return mutate((state2) => { + const goal = state2.goals[sessionID]; if (!goal || goal.status !== "active") return goal ? snapshotInternal(goal) : null; if (goal.pendingAttempt == null || goal.pendingAttempt.started) @@ -1372,12 +1372,12 @@ async function markPendingContinuationStarted(sessionID) { return snapshotInternal(goal); }); } -async function recordToolProgress(sessionID, text, expectedAttemptID) { +async function recordToolProgress(sessionID, text2, expectedAttemptID) { return mutate((state) => { const goal = state.goals[sessionID]; if (!goal || goal.status !== "active") return goal ? snapshotInternal(goal) : null; - const value = text?.trim() ?? ""; + const value = text2?.trim() ?? ""; if (!value) return snapshotInternal(goal); if (goal.continuationFailures === 0 && goal.pendingAttempt == null) @@ -1458,8 +1458,8 @@ function pushHistory(goal, type, detail) { return; goal.history = [...goal.history, { type, detail: value, timestamp: nowSeconds() }].slice(-MAX_HISTORY_ENTRIES); } -function summarizeText(text, limit = CHECKPOINT_CHAR_LIMIT) { - const normalized = text.replace(/\s+/g, " ").trim(); +function summarizeText(text2, limit = CHECKPOINT_CHAR_LIMIT) { + const normalized = text2.replace(/\s+/g, " ").trim(); if (!normalized) return ""; return normalized.length > limit ? `${normalized.slice(0, Math.max(0, limit - 3))}...` : normalized; @@ -1472,8 +1472,8 @@ function goalLimitSummary(goal) { ].filter(Boolean); return limits.length ? `Goal set with ${limits.join(", ")}.` : "Goal set with default continuation limits."; } -function estimateTokensFromText(text) { - return Math.ceil(text.length / 4); +function estimateTokensFromText(text2) { + return Math.ceil(text2.length / 4); } // src/i18n.ts @@ -1714,8 +1714,8 @@ var STATUS_PRESENTATIONS = { cancelled: "\u5DF2\u53D6\u6D88" } }; -function presentGoalStatus(status, locale) { - return STATUS_PRESENTATIONS[locale][status] ?? status; +function presentGoalStatus(status2, locale) { + return STATUS_PRESENTATIONS[locale][status2] ?? status2; } function presentGoalStopReason(reason, locale) { if (locale !== "zh-CN") @@ -1747,9 +1747,9 @@ function presentGoalStopReason(reason, locale) { return `\u5DF2\u8FBE\u5230\u6301\u7EED\u65F6\u95F4\u4E0A\u9650\uFF08${duration[1]} \u79D2\uFF09`; return reason; } -function presentGoalLastStatus(status, locale) { +function presentGoalLastStatus(status2, locale) { if (locale !== "zh-CN") - return status; + return status2; const direct = { "Goal set.": "\u76EE\u6807\u5DF2\u8BBE\u7F6E\u3002", "Goal recorded from Plan mode; execution paused until resumed from Build mode.": "\u76EE\u6807\u5DF2\u5728 Plan \u6A21\u5F0F\u4E0B\u8BB0\u5F55\uFF1B\u6267\u884C\u5DF2\u6682\u505C\uFF0C\u9700\u5728 Build \u6A21\u5F0F\u4E0B\u7EE7\u7EED\u3002", @@ -1768,30 +1768,30 @@ function presentGoalLastStatus(status, locale) { "Auto-continue prompt failed repeatedly. Resume the goal to retry.": "\u81EA\u52A8\u7EE7\u7EED\u63D0\u793A\u53CD\u590D\u5931\u8D25\u3002\u8BF7\u7EE7\u7EED\u76EE\u6807\u540E\u91CD\u8BD5\u3002", "Goal execution is paused while the session is in Plan mode. Switch to Build mode and resume the goal to continue.": "\u4F1A\u8BDD\u5904\u4E8E Plan \u6A21\u5F0F\uFF0C\u56E0\u6B64\u76EE\u6807\u6267\u884C\u5DF2\u6682\u505C\u3002\u8BF7\u5207\u6362\u5230 Build \u6A21\u5F0F\u5E76\u7EE7\u7EED\u76EE\u6807\u3002" }; - if (direct[status]) - return direct[status]; + if (direct[status2]) + return direct[status2]; const lowProgressPausePattern = /^Auto-continue paused after (\d+) low-progress continuation turn\(s\)\. Resume the goal to retry\.$/; - const lowProgressPause = lowProgressPausePattern.exec(status); + const lowProgressPause = lowProgressPausePattern.exec(status2); if (lowProgressPause) return `\u81EA\u52A8\u7EE7\u7EED\u5DF2\u5728 ${lowProgressPause[1]} \u4E2A\u4F4E\u8FDB\u5C55\u8F6E\u6B21\u540E\u6682\u505C\u3002\u8BF7\u7EE7\u7EED\u76EE\u6807\u540E\u91CD\u8BD5\u3002`; - const lowProgress = /^Low-progress continuation turn detected \((\d+)\/(\d+|unbounded)\)\.$/.exec(status); + const lowProgress = /^Low-progress continuation turn detected \((\d+)\/(\d+|unbounded)\)\.$/.exec(status2); if (lowProgress) { const limit = lowProgress[2] === "unbounded" ? "\u4E0D\u9650" : lowProgress[2]; return `\u68C0\u6D4B\u5230\u4F4E\u8FDB\u5C55\u7684\u7EE7\u7EED\u8F6E\u6B21\uFF08${lowProgress[1]}/${limit}\uFF09\u3002`; } - const reserved = /^Auto-continue (\d+) reserved\.$/.exec(status); + const reserved = /^Auto-continue (\d+) reserved\.$/.exec(status2); if (reserved) return `\u5DF2\u9884\u7559\u7B2C ${reserved[1]} \u6B21\u81EA\u52A8\u7EE7\u7EED\u3002`; - const failed = /^Auto-continue failed (\d+) time\(s\)\.$/.exec(status); + const failed = /^Auto-continue failed (\d+) time\(s\)\.$/.exec(status2); if (failed) return `\u81EA\u52A8\u7EE7\u7EED\u5DF2\u5931\u8D25 ${failed[1]} \u6B21\u3002`; - const pausedAfterFailures = /^Paused after (\d+) auto-continue failure\(s\)\.$/.exec(status); + const pausedAfterFailures = /^Paused after (\d+) auto-continue failure\(s\)\.$/.exec(status2); if (pausedAfterFailures) return `\u5DF2\u5728 ${pausedAfterFailures[1]} \u6B21\u81EA\u52A8\u7EE7\u7EED\u5931\u8D25\u540E\u6682\u505C\u3002`; - const wrapUp = /^(.*); wrap-up required\.$/.exec(status); + const wrapUp = /^(.*); wrap-up required\.$/.exec(status2); if (wrapUp) return `${presentGoalStopReason(wrapUp[1], locale)}\uFF1B\u9700\u8981\u6536\u5C3E\u3002`; - return status; + return status2; } var HISTORY_TYPE_PRESENTATIONS = { en: {}, @@ -2053,7 +2053,7 @@ function compactionContextPrefix(locale = "en") { var COMPACTION_CONTEXT_PREFIX = compactionContextPrefix(); function formatCompactionSnapshot(goal, locale) { if (locale === "zh-CN") { - const lines = [ + const lines2 = [ `\u76EE\u6807\uFF1A${goal.objective}`, `\u72B6\u6001\uFF1A${presentGoalStatus(goal.status, locale)}`, `\u5DF2\u7528\u65F6\u95F4\uFF1A${goal.timeUsedSeconds} \u79D2`, @@ -2061,24 +2061,24 @@ function formatCompactionSnapshot(goal, locale) { `\u81EA\u52A8\u7EE7\u7EED\u6B21\u6570\uFF1A${goal.autoTurns}${goal.maxAutoTurns == null ? "" : `/${goal.maxAutoTurns}`}` ]; if (goal.remainingTokens != null) - lines.push(`\u5269\u4F59 Token\uFF1A${goal.remainingTokens}`); + lines2.push(`\u5269\u4F59 Token\uFF1A${goal.remainingTokens}`); if (goal.maxDurationSeconds != null) - lines.push(`\u6301\u7EED\u65F6\u95F4\u4E0A\u9650\uFF1A${goal.maxDurationSeconds} \u79D2`); + lines2.push(`\u6301\u7EED\u65F6\u95F4\u4E0A\u9650\uFF1A${goal.maxDurationSeconds} \u79D2`); if (goal.noProgressTurns > 0) - lines.push(`\u65E0\u8FDB\u5C55\u8F6E\u6570\uFF1A${goal.noProgressTurns}`); + lines2.push(`\u65E0\u8FDB\u5C55\u8F6E\u6570\uFF1A${goal.noProgressTurns}`); if (goal.lastCheckpoint) - lines.push(`\u6700\u65B0\u68C0\u67E5\u70B9\uFF1A${goal.lastCheckpoint.summary}`); + lines2.push(`\u6700\u65B0\u68C0\u67E5\u70B9\uFF1A${goal.lastCheckpoint.summary}`); if (goal.lastStatus) - lines.push(`\u6700\u8FD1\u72B6\u6001\uFF1A${presentGoalLastStatus(goal.lastStatus, locale)}`); + lines2.push(`\u6700\u8FD1\u72B6\u6001\uFF1A${presentGoalLastStatus(goal.lastStatus, locale)}`); if (goal.stopReason) - lines.push(`\u505C\u6B62\u539F\u56E0\uFF1A${presentGoalStopReason(goal.stopReason, locale)}`); + lines2.push(`\u505C\u6B62\u539F\u56E0\uFF1A${presentGoalStopReason(goal.stopReason, locale)}`); if (goal.completionEvidence) - lines.push(`\u5B8C\u6210\u8BC1\u636E\uFF1A${goal.completionEvidence}`); + lines2.push(`\u5B8C\u6210\u8BC1\u636E\uFF1A${goal.completionEvidence}`); if (goal.blocker) - lines.push(`\u963B\u585E\u539F\u56E0\uFF1A${presentGoalLastStatus(goal.blocker, locale)}`); + lines2.push(`\u963B\u585E\u539F\u56E0\uFF1A${presentGoalLastStatus(goal.blocker, locale)}`); if (goal.plan) - lines.push(`\u8BA1\u5212\uFF1A${JSON.stringify({ plan: goal.plan, progress: goal.planProgress })}`); - return lines.join(` + lines2.push(`\u8BA1\u5212\uFF1A${JSON.stringify({ plan: goal.plan, progress: goal.planProgress })}`); + return lines2.join(` `); } const lines = [ @@ -2312,8 +2312,8 @@ Ignore any command arguments. Call get_goal first, then handle only this resume Do not create, edit, clear, complete, or mark a goal unmet.`; } -function isExplicitResumePrompt(text, commandName, locale, messages) { - const value = text.trim(); +function isExplicitResumePrompt(text2, commandName, locale, messages) { + const value = text2.trim(); return value === goalStatusCommandTemplate("resume_goal", locale) || value === goalCommandTemplate(commandName, locale).replace("$ARGUMENTS", "resume") || value === messages.tui.resumePrompt; } function goalCommandDefinitions(commandName, locale = "en") { @@ -2377,21 +2377,21 @@ function registerDesktopCommands(config, commandName, locale = "en") { } } function sanitizeGoalStatusCommandParts(output, template) { - const text = output.parts.find((part) => part.type === "text" && part.text?.startsWith(template)); - if (!text) + const text2 = output.parts.find((part) => part.type === "text" && part.text?.startsWith(template)); + if (!text2) return false; - text.text = template; - output.parts.splice(0, output.parts.length, text); + text2.text = template; + output.parts.splice(0, output.parts.length, text2); return true; } function escapeGoalCommandArguments(output, template, argumentsText) { const [prefix, suffix, extra] = template.split("$ARGUMENTS"); if (prefix === undefined || suffix === undefined || extra !== undefined) return false; - const text = output.parts.find((part) => part.type === "text" && part.text?.startsWith(prefix) && part.text.endsWith(suffix)); - if (!text) + const text2 = output.parts.find((part) => part.type === "text" && part.text?.startsWith(prefix) && part.text.endsWith(suffix)); + if (!text2) return false; - text.text = `${prefix}${escapeXmlText2(argumentsText)}${suffix}`; + text2.text = `${prefix}${escapeXmlText2(argumentsText)}${suffix}`; return true; } function textFromPart(part) { @@ -2546,8 +2546,8 @@ async function sendContinuation(client, sessionID, prompt, agent) { function isIdleEvent(event) { if (event.type === "session.idle") return true; - const status = event.properties?.status; - return event.type === "session.status" && typeof status === "object" && status !== null && status.type === "idle"; + const status2 = event.properties?.status; + return event.type === "session.status" && typeof status2 === "object" && status2 !== null && status2.type === "idle"; } function isTransportError(error) { const message = error instanceof Error ? error.message : typeof error === "string" ? error : ""; @@ -2623,7 +2623,7 @@ function toolOutputFailed(output) { return true; if (output.success === false) return true; - const text = typeof output.output === "string" ? output.output.trim() : ""; + const text2 = typeof output.output === "string" ? output.output.trim() : ""; const state = output.state ?? output.status; if (typeof state === "string") { const normalized = state.trim().toLowerCase(); @@ -2637,19 +2637,19 @@ function toolOutputFailed(output) { if (typeof metaState === "string" && TOOL_FAILURE_STATES.has(metaState.trim().toLowerCase())) return true; } - const taskState = parseTaskState(text); + const taskState = parseTaskState(text2); if (taskState) return taskState !== "completed"; - if (/^state:\s*(failed|failure|error|cancelled|canceled|aborted|abort|interrupted|running|pending|incomplete|partial|timeout|timed_out)\b/im.test(text)) + if (/^state:\s*(failed|failure|error|cancelled|canceled|aborted|abort|interrupted|running|pending|incomplete|partial|timeout|timed_out)\b/im.test(text2)) return true; - if (/^/i.test(text) || /^/i.test(text) || /^error:/i.test(text)) + if (/^/i.test(text2) || /^/i.test(text2) || /^error:/i.test(text2)) return true; return false; } -function taskBlockExpired(task, maxBlockMs, now) { +function taskBlockExpired(task2, maxBlockMs, now) { if (maxBlockMs == null) return false; - const blockingSince = task.state === "running" ? task.runningSince : task.terminalAt; + const blockingSince = task2.state === "running" ? task2.runningSince : task2.terminalAt; return blockingSince != null && now - blockingSince >= maxBlockMs; } function sessionIDFromEvent(event) { @@ -2716,14 +2716,14 @@ class TaskTracker { this.pendingTaskCalls.delete(input.callID); if (typeof parentSessionID !== "string") return; - const status = parseTaskStatus(output.output); - if (!status) + const status2 = parseTaskStatus(output.output); + if (!status2) return; - if (status.state === "running") { - this.markRunning(parentSessionID, status.taskID); + if (status2.state === "running") { + this.markRunning(parentSessionID, status2.taskID); return; } - this.markTerminal(status.taskID, status.state, parentSessionID, { resetReconciled: true }); + this.markTerminal(status2.taskID, status2.state, parentSessionID, { resetReconciled: true }); } observeSessionCreated(event) { const info = event.properties?.info; @@ -2731,22 +2731,22 @@ class TaskTracker { return; this.markRunning(info.parentID, info.id); } - observeSessionStatus(sessionID, status) { - const task = this.tasks.get(sessionID); - if (!task) + observeSessionStatus(sessionID, status2) { + const task2 = this.tasks.get(sessionID); + if (!task2) return; - if (status === "busy") { - this.markRunning(task.parentSessionID, sessionID); + if (status2 === "busy") { + this.markRunning(task2.parentSessionID, sessionID); return; } - if (status === "idle") - this.markTerminal(sessionID, "completed", task.parentSessionID); + if (status2 === "idle") + this.markTerminal(sessionID, "completed", task2.parentSessionID); } observeSessionDeleted(sessionID) { this.tasks.delete(sessionID); - for (const task of this.tasks.values()) { - if (task.parentSessionID === sessionID) - this.tasks.delete(task.taskID); + for (const task2 of this.tasks.values()) { + if (task2.parentSessionID === sessionID) + this.tasks.delete(task2.taskID); } this.latestAssistantBySession.delete(sessionID); this.clearSnapshotIdleForSession(sessionID); @@ -2762,13 +2762,13 @@ class TaskTracker { continue; } for (const part of message.parts ?? []) { - const status = parseTaskStatus(textFromPart(part)); - if (!status) + const status2 = parseTaskStatus(textFromPart(part)); + if (!status2) continue; - if (status.state === "running") - this.markRunning(sessionID, status.taskID); + if (status2.state === "running") + this.markRunning(sessionID, status2.taskID); else - this.markTerminal(status.taskID, status.state, sessionID, { resetReconciled: true }); + this.markTerminal(status2.taskID, status2.state, sessionID, { resetReconciled: true }); } } } @@ -2792,25 +2792,25 @@ class TaskTracker { continue; if (entry.state.status === "streaming" || entry.state.status === "running") continue; - const status = parseTaskStatus(toolTextFromV2Content(entry.state.content ?? [])); - if (!status) + const status2 = parseTaskStatus(toolTextFromV2Content(entry.state.content ?? [])); + if (!status2) continue; - if (status.state === "running") - this.markRunning(parentSessionID, status.taskID); + if (status2.state === "running") + this.markRunning(parentSessionID, status2.taskID); else - this.markTerminal(status.taskID, status.state, parentSessionID, { resetReconciled: true, terminalAt }); + this.markTerminal(status2.taskID, status2.state, parentSessionID, { resetReconciled: true, terminalAt }); } } } hasBlockingTasks(parentSessionID, maxBlockMs = null) { this.pruneExpiredSnapshotIdleHolds(); const now = Date.now(); - for (const task of this.tasks.values()) { - if (task.parentSessionID !== parentSessionID) + for (const task2 of this.tasks.values()) { + if (task2.parentSessionID !== parentSessionID) continue; - if (task.state !== "running" && !task.terminalUnreconciled) + if (task2.state !== "running" && !task2.terminalUnreconciled) continue; - if (taskBlockExpired(task, maxBlockMs, now)) + if (taskBlockExpired(task2, maxBlockMs, now)) continue; return true; } @@ -2853,8 +2853,8 @@ class TaskTracker { return; } for (const childID of childIDs) { - const status = statuses[childID]; - const statusType = isRecord(status) && typeof status.type === "string" ? status.type : undefined; + const status2 = statuses[childID]; + const statusType = isRecord(status2) && typeof status2.type === "string" ? status2.type : undefined; if (statusType === "busy") this.markRunning(parentSessionID, childID); else if (statusType === "idle") { @@ -2932,16 +2932,16 @@ class TaskTracker { continue; this.snapshotIdleHolds.delete(key); this.settledSnapshotIdleTasks.add(key); - const task = this.tasks.get(hold.taskID); - if (task?.parentSessionID === hold.parentSessionID && task.state === "running") + const task2 = this.tasks.get(hold.taskID); + if (task2?.parentSessionID === hold.parentSessionID && task2.state === "running") this.tasks.delete(hold.taskID); } } markAbsentRunningChildren(parentSessionID, liveChildIDs) { - for (const task of this.tasks.values()) { - if (task.parentSessionID !== parentSessionID || task.state !== "running" || liveChildIDs.has(task.taskID)) + for (const task2 of this.tasks.values()) { + if (task2.parentSessionID !== parentSessionID || task2.state !== "running" || liveChildIDs.has(task2.taskID)) continue; - this.markSnapshotIdle(parentSessionID, task.taskID); + this.markSnapshotIdle(parentSessionID, task2.taskID); } } snapshotIdleKey(parentSessionID, taskID) { @@ -2949,18 +2949,18 @@ class TaskTracker { } observeAssistant(sessionID, marker) { this.latestAssistantBySession.set(sessionID, marker); - for (const task of this.tasks.values()) { - if (task.parentSessionID !== sessionID || !task.terminalUnreconciled) + for (const task2 of this.tasks.values()) { + if (task2.parentSessionID !== sessionID || !task2.terminalUnreconciled) continue; - if (this.assistantReconcilesTask(task, marker)) { - this.tasks.set(task.taskID, { ...task, terminalUnreconciled: false }); + if (this.assistantReconcilesTask(task2, marker)) { + this.tasks.set(task2.taskID, { ...task2, terminalUnreconciled: false }); } } } - assistantReconcilesTask(task, marker) { - if (marker.id && task.lastAssistantMessageIDAtTerminal && marker.id !== task.lastAssistantMessageIDAtTerminal) + assistantReconcilesTask(task2, marker) { + if (marker.id && task2.lastAssistantMessageIDAtTerminal && marker.id !== task2.lastAssistantMessageIDAtTerminal) return true; - if (marker.completedAt != null && task.terminalAt != null && marker.completedAt >= task.terminalAt) + if (marker.completedAt != null && task2.terminalAt != null && marker.completedAt >= task2.terminalAt) return true; return false; } @@ -2969,12 +2969,12 @@ async function recordAssistantMessage(sessionID, message, options, evaluateConti if (!message) return { goal: null, progressed: false }; const before = await getGoal(sessionID); - const id = messageID(message) ?? ""; - const text = textFromMessage(message); - const progressed = Boolean(/[\p{L}\p{N}]/u.test(text) && (id !== (before?.lastAssistantMessageID ?? "") || text !== (before?.lastAssistantText ?? ""))); + const id2 = messageID(message) ?? ""; + const text2 = textFromMessage(message); + const progressed = Boolean(/[\p{L}\p{N}]/u.test(text2) && (id2 !== (before?.lastAssistantMessageID ?? "") || text2 !== (before?.lastAssistantText ?? ""))); const goal = await recordAssistantProgress(sessionID, { - messageID: id, - text, + messageID: id2, + text: text2, outputTokens: outputTokensFromMessage(message) ?? null, noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold), maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns), @@ -3180,10 +3180,10 @@ async function updateGoalObjectiveFromTool(input, context, services) { } async function closeGoalFromTool(input, context, services) { if (input.status === "complete") { - const goal = await completeGoal(context.sessionID, input.evidence ?? "", services.maxObjectiveChars); - const budget = goal.tokenBudget == null ? "" : ` ${services.messages.reports.tokenUsage}: ${goal.tokensUsed}/${goal.tokenBudget}.`; - const report = `${services.messages.reports.achieved} ${services.messages.reports.timeUsed}: ` + `${goal.timeUsedSeconds} ${services.messages.reports.seconds}.${budget} ` + `${services.messages.reports.evidence}: ${goal.completionEvidence}.`; - return JSON.stringify({ goal, completion_report: report }, null, 2); + const goal2 = await completeGoal(context.sessionID, input.evidence ?? "", services.maxObjectiveChars); + const budget = goal2.tokenBudget == null ? "" : ` ${services.messages.reports.tokenUsage}: ${goal2.tokensUsed}/${goal2.tokenBudget}.`; + const report2 = `${services.messages.reports.achieved} ${services.messages.reports.timeUsed}: ` + `${goal2.timeUsedSeconds} ${services.messages.reports.seconds}.${budget} ` + `${services.messages.reports.evidence}: ${goal2.completionEvidence}.`; + return JSON.stringify({ goal: goal2, completion_report: report2 }, null, 2); } const goal = await markGoalUnmet(context.sessionID, input.blocker ?? "", services.maxObjectiveChars); const report = `${services.messages.reports.unmet} ${services.messages.reports.timeUsed}: ` + `${goal.timeUsedSeconds} ${services.messages.reports.seconds}. ` + `${services.messages.reports.blocker}: ${goal.blocker}.`; @@ -3226,9 +3226,9 @@ function textFromToolResult(result) { if (typeof result.content === "string") return result.content; if (Array.isArray(result.content)) { - const text = result.content.map(textFromPart).filter(Boolean).join(` + const text2 = result.content.map(textFromPart).filter(Boolean).join(` `).trim(); - return text || undefined; + return text2 || undefined; } return; } @@ -3750,9 +3750,9 @@ var server = async ({ client }, options) => { const sanitized = escapeGoalCommandArguments(output, goalCommandTemplate(commandName, locale), input.arguments); objectiveEdits.delete(input.sessionID); const edit = /^edit\s+([\s\S]+)$/i.exec(input.arguments.trim()); - const goal = edit && sanitized ? await getGoal(input.sessionID) : null; - if (goal && edit) - objectiveEdits.set(input.sessionID, { goalID: goal.id, objective: edit[1].trim() }); + const goal2 = edit && sanitized ? await getGoal(input.sessionID) : null; + if (goal2 && edit) + objectiveEdits.set(input.sessionID, { goalID: goal2.id, objective: edit[1].trim() }); if (sanitized && input.arguments.trim().toLowerCase() === "resume") { explicitResumeRequests.add(input.sessionID); } @@ -3793,15 +3793,15 @@ var server = async ({ client }, options) => { const toolResult = output; if (toolOutputFailed(toolResult)) return; - const text = typeof toolResult.output === "string" ? toolResult.output : undefined; - if (!text) + const text2 = typeof toolResult.output === "string" ? toolResult.output : undefined; + if (!text2) return; const before = await getGoalInternal(sessionID); const scheduled = scheduledContinuations.get(sessionID); const hasFailureEpisode = Boolean(before && (before.continuationFailures > 0 || before.pendingAttempt != null)); if (!before || !hasFailureEpisode && scheduled?.purpose !== "recovery") return; - const progressed = await recordToolProgress(sessionID, text, expectedAttemptID); + const progressed = await recordToolProgress(sessionID, text2, expectedAttemptID); if (progressed?.continuationFailures === 0 && progressed.pendingAttempt == null) { locallyDeliveredPendingSessions.delete(sessionID); cancelScheduledContinuation(sessionID); @@ -3867,29 +3867,29 @@ var server = async ({ client }, options) => { taskTracker.observeSessionCreated(event); } if (sessionID && eventType === "session.status") { - const status = event.properties?.status; - if (isRecord(status) && typeof status.type === "string") { - if (status.type === "busy") { + const status2 = event.properties?.status; + if (isRecord(status2) && typeof status2.type === "string") { + if (status2.type === "busy") { busySessions.add(sessionID); nativeRetrySessions.delete(sessionID); } - if (status.type === "busy") + if (status2.type === "busy") armTurnWatchdog(sessionID); - if (status.type === "busy") + if (status2.type === "busy") await markPendingContinuationStarted(sessionID); - if (status.type === "idle") { + if (status2.type === "idle") { explicitResumeRequests.delete(sessionID); busySessions.delete(sessionID); nativeRetrySessions.delete(sessionID); clearTurnWatchdog(sessionID); watchdogRescuedSessions.delete(sessionID); } - if (status.type === "retry") { + if (status2.type === "retry") { nativeRetrySessions.add(sessionID); clearTurnWatchdog(sessionID); cancelScheduledContinuation(sessionID); } - taskTracker.observeSessionStatus(sessionID, status.type); + taskTracker.observeSessionStatus(sessionID, status2.type); } } if (sessionID && eventType === "session.idle") { @@ -4043,10 +4043,10 @@ async function setupV2(context) { const registrations = []; let disposed = false; let eventConsumerStopped = false; - function stepKey(sessionID, messageID) { - return `${sessionID}\x00${messageID}`; + function stepKey(sessionID, messageID2) { + return `${sessionID}\x00${messageID2}`; } - async function sendContinuation(sessionID, prompt, agent) { + async function sendContinuation2(sessionID, prompt, agent) { markSessionOwnership(sessionID, true); await context.session.prompt({ sessionID, @@ -4119,7 +4119,7 @@ async function setupV2(context) { watchdogRescuedSessions.add(sessionID); if (!isCurrent()) return; - await sendContinuation(sessionID, continuationPrompt(current, locale), current.lastPromptAgent ?? latestStep?.agent ?? null); + await sendContinuation2(sessionID, continuationPrompt(current, locale), current.lastPromptAgent ?? latestStep?.agent ?? null); if (!isCurrent()) return; const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures, { @@ -4308,7 +4308,7 @@ async function setupV2(context) { await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID }); return; } - await sendContinuation(sessionID, goal.status === "active" ? continuationPrompt(goal, locale) : limitPrompt(goal, locale), goal.lastPromptAgent ?? latestTurnAgent ?? null); + await sendContinuation2(sessionID, goal.status === "active" ? continuationPrompt(goal, locale) : limitPrompt(goal, locale), goal.lastPromptAgent ?? latestTurnAgent ?? null); if (!isCurrent()) { await rollbackContinuationAttempt(sessionID, { goalID: attemptGoalID, attemptID }); return; @@ -4431,29 +4431,29 @@ async function setupV2(context) { case "session.execution.started": case "session.retry.scheduled": case "session.status": { - const status = event.type === "session.execution.started" ? { type: "busy" } : event.type === "session.retry.scheduled" ? { type: "retry" } : data.status; - if (sessionID && isRecord(status) && typeof status.type === "string") { - if (status.type === "busy") { + const status2 = event.type === "session.execution.started" ? { type: "busy" } : event.type === "session.retry.scheduled" ? { type: "retry" } : data.status; + if (sessionID && isRecord(status2) && typeof status2.type === "string") { + if (status2.type === "busy") { stoppedExecutions.delete(sessionID); busySessions.add(sessionID); nativeRetrySessions.delete(sessionID); armTurnWatchdog(sessionID); await markPendingContinuationStarted(sessionID); } - if (status.type === "idle") { + if (status2.type === "idle") { explicitResumeRequests.delete(sessionID); busySessions.delete(sessionID); nativeRetrySessions.delete(sessionID); clearTurnWatchdog(sessionID); watchdogRescuedSessions.delete(sessionID); } - if (status.type === "retry") { + if (status2.type === "retry") { nativeRetrySessions.add(sessionID); clearTurnWatchdog(sessionID); cancelScheduledContinuation(sessionID); } - taskTracker.observeSessionStatus(sessionID, status.type); - if (status.type === "idle") { + taskTracker.observeSessionStatus(sessionID, status2.type); + if (status2.type === "idle") { const goal = await getGoalInternal(sessionID); if (autoContinue || goal?.pendingAttempt != null) await runAutoContinue(sessionID); @@ -4564,16 +4564,16 @@ async function setupV2(context) { case "session.step.started": { if (!sessionID || typeof data.assistantMessageID !== "string") return; - const messageID = data.assistantMessageID; + const messageID2 = data.assistantMessageID; const agent = typeof data.agent === "string" ? data.agent : undefined; if (agent) await recordPromptAgent(sessionID, agent); taskTracker.observeAssistantMessage(sessionID, { - info: { id: messageID, role: "assistant", time: { completed: event.created } } + info: { id: messageID2, role: "assistant", time: { completed: event.created } } }); - if (!stepTextBuffers.has(stepKey(sessionID, messageID))) - stepTextBuffers.set(stepKey(sessionID, messageID), ""); - latestStepBySession.set(sessionID, { messageID, agent, text: "", outputTokens: null, completedAt: event.created }); + if (!stepTextBuffers.has(stepKey(sessionID, messageID2))) + stepTextBuffers.set(stepKey(sessionID, messageID2), ""); + latestStepBySession.set(sessionID, { messageID: messageID2, agent, text: "", outputTokens: null, completedAt: event.created }); return; } case "session.text.delta": { @@ -4592,7 +4592,7 @@ async function setupV2(context) { case "session.step.ended": { if (!sessionID || typeof data.assistantMessageID !== "string") return; - const messageID = data.assistantMessageID; + const messageID2 = data.assistantMessageID; const tokens = tokensFromRecord(data.tokens); if (typeof tokens === "number") { const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens; @@ -4603,27 +4603,27 @@ async function setupV2(context) { initialBaseline: Math.ceil(sum - tokens) }); } - const text = stepTextBuffers.get(stepKey(sessionID, messageID)) ?? ""; - stepTextBuffers.delete(stepKey(sessionID, messageID)); + const text2 = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? ""; + stepTextBuffers.delete(stepKey(sessionID, messageID2)); const outputTokens = outputTokensFromRecord(data.tokens) ?? null; const afterStep = await recordAssistantProgress(sessionID, { - messageID, - text, + messageID: messageID2, + text: text2, outputTokens, noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold), maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns), completedAt: event.created }); await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, afterStep); - if (/[\p{L}\p{N}]/u.test(text)) { + if (/[\p{L}\p{N}]/u.test(text2)) { const scheduled = scheduledContinuations.get(sessionID); if (scheduled?.purpose === "recovery") cancelScheduledContinuation(sessionID); } latestStepBySession.set(sessionID, { - messageID, + messageID: messageID2, agent: latestStepBySession.get(sessionID)?.agent, - text, + text: text2, outputTokens, completedAt: event.created }); @@ -4632,7 +4632,7 @@ async function setupV2(context) { case "session.step.failed": { if (!sessionID || typeof data.assistantMessageID !== "string") return; - const messageID = data.assistantMessageID; + const messageID2 = data.assistantMessageID; const tokens = tokensFromRecord(data.tokens); if (typeof tokens === "number") { const sum = (stepTokenSums.get(sessionID) ?? 0) + tokens; @@ -4643,27 +4643,27 @@ async function setupV2(context) { initialBaseline: Math.ceil(sum - tokens) }); } - const text = stepTextBuffers.get(stepKey(sessionID, messageID)) ?? ""; - stepTextBuffers.delete(stepKey(sessionID, messageID)); + const text2 = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? ""; + stepTextBuffers.delete(stepKey(sessionID, messageID2)); const outputTokens = outputTokensFromRecord(data.tokens) ?? null; const afterStep = await recordAssistantProgress(sessionID, { - messageID, - text, + messageID: messageID2, + text: text2, outputTokens, noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold), maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns), completedAt: event.created }); await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, afterStep); - if (/[\p{L}\p{N}]/u.test(text)) { + if (/[\p{L}\p{N}]/u.test(text2)) { const scheduled = scheduledContinuations.get(sessionID); if (scheduled?.purpose === "recovery") cancelScheduledContinuation(sessionID); } latestStepBySession.set(sessionID, { - messageID, + messageID: messageID2, agent: latestStepBySession.get(sessionID)?.agent, - text, + text: text2, outputTokens, completedAt: event.created }); @@ -4844,7 +4844,7 @@ async function setupV2(context) { if (goal || input.tool === "clear_goal") input.result = { ...input.result, metadata: { ...input.result.metadata, ...acpPlanMetadata(goal) } }; } - const text = textFromToolResult(input.result); + const text2 = textFromToolResult(input.result); taskTracker.noteTaskOutput({ tool: input.tool, sessionID: input.sessionID, callID: input.id }, { output: textFromToolResult(input.result) }); if (!sessionID || typeof input.tool !== "string") return; @@ -4852,14 +4852,14 @@ async function setupV2(context) { return; if (toolOutputFailed(input.result)) return; - if (!text) + if (!text2) return; const before = await getGoalInternal(sessionID); const scheduled = scheduledContinuations.get(sessionID); const hasFailureEpisode = Boolean(before && (before.continuationFailures > 0 || before.pendingAttempt != null)); if (!before || !hasFailureEpisode && scheduled?.purpose !== "recovery") return; - const progressed = await recordToolProgress(sessionID, text, expectedAttemptID); + const progressed = await recordToolProgress(sessionID, text2, expectedAttemptID); if (progressed?.continuationFailures === 0 && progressed.pendingAttempt == null) { locallyDeliveredPendingSessions.delete(sessionID); cancelScheduledContinuation(sessionID); diff --git a/src/server.ts b/src/server.ts index 4caebfe..2c53808 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1068,8 +1068,13 @@ const PlanToolSchema = z.object(planToolArgs).strict() * `maxLength: null` or `format: null`. Providers that strictly validate tool * schemas (for example Mistral mistral-large-4) reject those requests with * `Invalid tool schema`, breaking every session that includes the goal tools. - * Convert once here; runtime argument validation is unaffected because - * planFromTool still parses through PlanToolSchema. + * + * The V2 plugin API accepts JSON Schema rather than the zod objects used by + * the V1 tool API. `io: "input"` keeps the schema provider-facing: optional + * `revisit_evidence` stays out of `required`, and `decisions` keeps its + * `default` instead of being pinned as required output. Convert once here; + * runtime argument validation is unaffected because planFromTool still parses + * through PlanToolSchema. */ const planToolInputSchema = z.toJSONSchema(PlanToolSchema, { io: "input", diff --git a/test/server-v2.test.ts b/test/server-v2.test.ts index 90d5f78..cf25eee 100644 --- a/test/server-v2.test.ts +++ b/test/server-v2.test.ts @@ -730,6 +730,53 @@ test("V2 update_goal_plan registrations do not share a mutable input schema", as ]) }) +// Nested plan schema assertions ported from #68 (authored by @abeisleem, commits +// b88c9e2/e44a291) with thanks — they complement the wire-safety checks above by +// pinning the serialized nested plan structure. +test("V2 plan tool publishes JSON Schema with required fields and optional nested evidence", async () => { + const mock = makeMockContext({ auto_continue: false }) + await setupPlugin(mock as never) + // Inspect the serialized wire schema, not only the outer object wrapper. + const input = JSON.parse(JSON.stringify(goalTool(mock, "update_goal_plan").input)) + expect(input.required).toEqual(["goal_id", "expected_revision", "plan", "reason"]) + expect(input.additionalProperties).toBe(false) + expect(input.properties.goal_id).toMatchObject({ type: "string", minLength: 1 }) + expect(input.properties.expected_revision).toMatchObject({ type: "integer", minimum: 0 }) + expect(input.properties.reason).toMatchObject({ type: "string", minLength: 1, maxLength: 2000 }) + expect(input.properties.revisit_evidence).toMatchObject({ type: "string", minLength: 1, maxLength: 2000 }) + + const plan = input.properties.plan + expect(plan.required).toEqual(["summary", "completionCriteria", "phases"]) + expect(plan.additionalProperties).toBe(false) + expect(plan.properties.decisions).toMatchObject({ type: "array", default: [], maxItems: 32 }) + expect(plan.properties.completionCriteria).toMatchObject({ type: "array", minItems: 1, maxItems: 32 }) + const phases = plan.properties.phases + expect(phases).toMatchObject({ type: "array", minItems: 1, maxItems: 64 }) + const phase = phases.items + expect(phase.required).toEqual(["id", "objective", "status", "tasks"]) + expect(phase.additionalProperties).toBe(false) + const tasks = phase.properties.tasks + expect(tasks).toMatchObject({ type: "array", minItems: 1, maxItems: 128 }) + const task = tasks.items + expect(task.required).toEqual(["id", "description", "status"]) + expect(task.additionalProperties).toBe(false) + expect(task.properties.status).toEqual({ + type: "string", + enum: ["pending", "in_progress", "completed", "blocked"], + }) + for (const field of [ + phase.properties.verification, + phase.properties.blocker, + task.properties.evidence, + task.properties.blocker, + ]) { + expect(field.anyOf).toEqual([ + { type: "string", minLength: 1, maxLength: 2000 }, + { type: "null" }, + ]) + } +}) + test("V2 setup registers /goal, /pause_goal, and /resume_goal via command transform", async () => { const mock = makeMockContext({ auto_continue: false }) const cleanup = await setupPlugin(mock as never)