feat(agent): adopt AI SDK v7 — crash safety, context pruning, HITL approvals (#3362)

* docs(adr): propose agent AI SDK v7 adoption plan

* fix(ai): bind analyzeResume through aiService in service test

The test destructured analyzeResume as a named export that does not exist; main was red.

* test(agent): keep pure ai helpers real via spread-actual mock factory

* feat(agent): add run guards, patch version guard, run wall-clock timeout

* feat(agent): validate UI messages at the send boundary

* feat(agent): crash-safe draft-row persistence and server-side cancellation

* feat(agent): reap stale run claims at boot, on send, and on thread open

* feat(agent): fresh-document patch output and tiered context pruning

* feat(ai): shared agent tool contracts and message metadata schema

* feat(agent): add per-thread review-patches setting with update endpoint

* feat(agent): gate resume patches behind hmac-signed tool approval

* feat(agent): merge question answers and approval decisions before run claim

* feat(agent): approval ui with composed auto-send and fixture-driven tests

* feat(agent): usage metadata, tool activity cards, smoother streaming

* feat(agent): tool-call repair, input examples, structured step logging

* chore(i18n): translate new agent workspace strings across all locales

* fix(agent): gate stale-run draft cancellation on winning the claim clear

Snapshot streaming drafts before the conditional clear and skip the flip entirely when another reaper or a replacement run already cleared the claim. Also address review nits in eleven locale catalogs.

* fix(agent): flip reaped drafts only when their snapshotted state is unchanged

* fix(agent): address review findings across run lifecycle, context budget, and approval flow

- bind patches to the revision the model read via signed baseUpdatedAt
- claim the run before consuming a continuation; recorded-but-unexecuted approvals retry as pending continuations
- keep run ownership on stop() until cancellation persists; preserve the claim for the reaper when final persistence fails
- estimate tokens without serializing binary attachments (tokenx) and enforce the budget by dropping oldest whole turns
- mark crash-recovered patch results as snapshot boundaries; strip /data prefixes at execution time
- retry failed continuations without regenerate; mount a single AgentChat; disable response controls on read-only threads; freeze review toggle during runs (client+server)
- accumulate usage across continuations and match the SDK's nested usage shape; label-form token strings; reorderable source label; accessible note field; state-neutral web-search label

* chore(i18n): translate revised agent strings across all locales

* fix(agent): harden baseUpdatedAt validation and address review follow-ups

- bundle tokenx in the server runtime dependencies (e2e boot failure)
- strict ISO schema for baseUpdatedAt plus loud executor rejection of unparseable values
- it-IT source label consistency (Fonte)
- prove penultimate-turn retention in the context pruning test

* chore(deps): exempt tokenx from knip for the externalized server bundle
This commit is contained in:
Amruth Pillai
2026-08-20 08:06:53 +02:00
committed by GitHub
parent dbbab6fd76
commit c8081ac2fe
95 changed files with 14919 additions and 497 deletions
+56
View File
@@ -0,0 +1,56 @@
import type { ToolCallRepairFunction, ToolSet } from "ai";
import { jsonrepair } from "jsonrepair";
import { applyResumePatchInputSchema } from "@reactive-resume/ai/tools/agent-tool-contracts";
// Repairs sloppy apply_resume_patch calls from weaker BYOK models: fix broken JSON with
// jsonrepair, strip the common `/data` path prefix, then re-validate against the shared schema.
// Returning null falls back to the SDK's re-ask. Section-shortcut normalization stays in
// normalizeAgentResumePatchOperations (it needs the resume's section ids at execute time).
function stripDataPrefix(path: unknown): unknown {
if (typeof path !== "string") return path;
if (path === "/data") return "";
return path.startsWith("/data/") ? path.slice("/data".length) : path;
}
function normalizeRepairedInput(parsed: unknown): unknown {
if (!parsed || typeof parsed !== "object" || !Array.isArray((parsed as { operations?: unknown }).operations)) {
return parsed;
}
const record = parsed as Record<string, unknown> & { operations: unknown[] };
return {
...record,
operations: record.operations.map((operation) => {
if (!operation || typeof operation !== "object") return operation;
const op = operation as Record<string, unknown>;
return {
...op,
path: stripDataPrefix(op.path),
...("from" in op ? { from: stripDataPrefix(op.from) } : {}),
};
}),
};
}
// Pure core, exported for tests: returns the repaired stringified input or null.
export function repairAgentPatchToolCallInput(rawInput: string): string | null {
let parsed: unknown;
try {
parsed = JSON.parse(jsonrepair(rawInput));
} catch {
return null;
}
const validated = applyResumePatchInputSchema.safeParse(normalizeRepairedInput(parsed));
return validated.success ? JSON.stringify(validated.data) : null;
}
export const repairAgentToolCall: ToolCallRepairFunction<ToolSet> = ({ toolCall }) => {
if (toolCall.toolName !== "apply_resume_patch") return Promise.resolve(null);
const repairedInput = repairAgentPatchToolCallInput(toolCall.input);
if (repairedInput === null || repairedInput === toolCall.input) return Promise.resolve(null);
return Promise.resolve({ ...toolCall, input: repairedInput });
};