UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

132 lines (131 loc) • 7.95 kB
import { ASK_USER_QUESTION_TOOL_NAME, isLiveQuestionFace, markBoundOnlyQuestionFace } from "../ask-question.js"; import { boundInputHashOf } from "../canonical-json.js"; import { CheckpointError } from "../checkpoint-store.js"; import { formatHookFeedback } from "../hooks.js"; import { delimitUntrusted, REVIEWER_NOTE_MAX_BODY } from "../untrusted-text.js"; import { wireOf } from "../tool-name-readings.js"; export function resumeDecisionWasNegative(resume) { const o = resume.outcome; return o.decision === "reject" || o.decision === "deny"; } export function deepJsonEqual(a, b) { if (a === b) return true; if (typeof a !== typeof b || a === null || b === null || typeof a !== "object") { return false; } const aArr = Array.isArray(a); const bArr = Array.isArray(b); if (aArr !== bArr) return false; if (aArr && bArr) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!deepJsonEqual(a[i], b[i])) return false; } const aExtra = Object.keys(a).filter((k) => !isCanonicalIndexKey(k, a.length)); const bExtra = Object.keys(b).filter((k) => !isCanonicalIndexKey(k, b.length)); if (aExtra.length !== bExtra.length) return false; return aExtra.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepJsonEqual(Reflect.get(a, k), Reflect.get(b, k))); } const ao = a; const bo = b; const aKeys = Object.keys(ao); const bKeys = Object.keys(bo); if (aKeys.length !== bKeys.length) return false; return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k])); } function isCanonicalIndexKey(key, length) { return /^(0|[1-9]\d*)$/.test(key) && Number(key) < length; } export function sameWinner(incoming, persisted) { return (incoming.boundCallId === persisted.boundCallId && incoming.decision === persisted.decision && deepJsonEqual(incoming.updatedInput, persisted.updatedInput) && deepJsonEqual(incoming.answer, persisted.answer) && (persisted.reason === undefined || incoming.reason === persisted.reason)); } export function pendingContentAskCallId(cp) { return cp.pendingAction.kind === "tool_approval" && wireOf("resume.contentAskRow", cp.pendingAction) === ASK_USER_QUESTION_TOOL_NAME ? cp.pendingAction.toolCallId : undefined; } export function answerFaceForRedeemedCall(answer, redeemedCallId, base, redeemedQuestionsHash) { let consumed = false; const face = async (req, signal) => { if (!consumed && req.toolCallId === redeemedCallId && boundInputHashOf(req.questions) === redeemedQuestionsHash) { consumed = true; return answer; } if (base !== undefined) return base(req, signal); throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg"); }; return isLiveQuestionFace(base) ? face : markBoundOnlyQuestionFace(face); } export function describeSuppliedValue(value) { return typeof value === "string" ? value : value === null ? "null" : typeof value; } export function assertOutcomeText(value, field) { if (value !== undefined && typeof value !== "string") { throw new CheckpointError("checkpoint.invalid_outcome", `resume \`${field}\` is not a plain string (got ${typeof value}) — a decide's text payload is an operator's plain data, not a live object; refusing pre-CAS, the checkpoint stays pending`); } } function reorientClause(fullShellReachable) { const source = fullShellReachable ? "re-orient from the workspace: run `git status` and review your recent changes / last commits" : "re-orient from the conversation: re-read your most recent messages and tool results"; return `Before continuing, ${source} to confirm what is already done, then continue the remaining work. ` + "Do NOT restart the task or re-run work that is already committed."; } export function resumeContinuation(resume, mark, fullShellReachable) { if (resume.outcome.gate === "wake") { return formatHookFeedback(`You were WOKEN from a parked pause by an operator. ${reorientClause(fullShellReachable)}`, mark); } if (resume.outcome.gate === "resource_limit") { return formatHookFeedback(`You were resumed after a pause. ${reorientClause(fullShellReachable)}`, mark); } if (resume.outcome.gate === "dry_run_review") { const verdict = resume.outcome.decision === "approve" ? "Your predicted change was REVIEWED and APPROVED; it has been applied" : `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}; it was NOT applied`; return formatHookFeedback(`You were resumed after a dry-run review. ${verdict}. ${reorientClause(fullShellReachable)}`, mark); } if (resume.outcome.gate === "plan_review") { if (resume.outcome.decision === "approve") { return formatHookFeedback("Your proposed PLAN was REVIEWED and APPROVED. Proceed with that plan now — begin executing it. " + "This is a RESUMED task; do NOT re-plan or restart from scratch, just carry out the approved plan.", mark); } if (resume.outcome.decision === "edit") { if (!resume.outcome.editedPlan) { return formatHookFeedback("Your proposed PLAN was REVIEWED and EDITED, but no revised plan text was supplied — proceed with your " + "ORIGINAL plan as-is, begin executing it now. This is a RESUMED task; do NOT re-plan or restart from scratch.", mark); } return formatHookFeedback("Your proposed PLAN was REVIEWED and EDITED by a human reviewer. Proceed with the REVISED plan below " + "(it supersedes your earlier plan); begin executing it now. This is a RESUMED task; do NOT re-plan " + "or restart from scratch, just carry out the revised plan.\n\nThe REVISED plan to follow is:\n" + delimitUntrusted("revised plan", resume.outcome.editedPlan), mark); } const why = resume.outcome.reason ? ` Reviewer note: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""; return formatHookFeedback(`Your proposed PLAN was REVIEWED and REJECTED; it was NOT executed.${why} Produce a NEW plan that ` + "addresses the concern, then continue. This is a RESUMED task — re-plan from the current state; do " + "NOT execute the rejected plan.", mark); } const { pendingAction } = resume.cp; if (pendingAction.kind !== "tool_approval") { return formatHookFeedback("This is a RESUMED task — continue from where you left off without restarting or re-running prior work.", mark); } const decided = resume.outcome.decision === "allow" ? "was APPROVED and has now been executed — its result is in the tool results above" : resume.outcome.hostDecision.decidedBy === "policy" ? "was DENIED by this deployment's own policy, not by a person — the refusal and its reason are in the tool results above" : `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}`; return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}. ` + `This is a RESUMED task — every step before this point is already done and its results are in the ` + `conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` + `exact point, building on the existing results, and finish the remaining work.`, mark); }