@sema-agent/core
Version:
Stateless, task-oriented AI agent core
132 lines (131 loc) • 7.95 kB
JavaScript
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);
}