@stll/folio-agents
Advanced tools
+50
-5
| import { explainTextTooLong, parseAddCommentInput, parseSuggestChangesInput } from "./parse.js"; | ||
| import { FOLIO_AGENT_TOOL_NAMES } from "./types.js"; | ||
| import { FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, createFolioAITextRangeHandle, getFolioDocumentOutline, readFolioDocumentSection } from "@stll/folio-core/server"; | ||
| import { FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, createFolioAITextRangeHandle, getFolioDocumentOutline, hashFolioAIBlockText, normalizeFolioAIBlockText, readFolioDocumentSection } from "@stll/folio-core/server"; | ||
| //#region src/execute.ts | ||
@@ -16,2 +16,11 @@ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value); | ||
| const VALID_TOOL_NAMES = Object.values(FOLIO_AGENT_TOOL_NAMES); | ||
| /** | ||
| * The same normalized-text hash the core snapshot/apply machinery uses for | ||
| * `precondition.blockTextHash` (see `hashFolioAIBlockText` / | ||
| * `normalizeFolioAIBlockText`). Computed straight from a block's `text` | ||
| * rather than looked up from `snapshot.anchors` so every read path (main | ||
| * document, a section, a find_text match) can attach it without threading | ||
| * the anchors map around — it is defined to produce the exact same value. | ||
| */ | ||
| const blockTextHashOf = (text) => hashFolioAIBlockText(normalizeFolioAIBlockText(text)); | ||
| /** `find_text` requires a short `query` and caps how much of a large match set it returns in one call. */ | ||
@@ -64,3 +73,4 @@ const MAX_QUERY_LENGTH = 1e3; | ||
| kind: block.kind, | ||
| text: block.text | ||
| text: block.text, | ||
| blockTextHash: blockTextHashOf(block.text) | ||
| }))); | ||
@@ -127,3 +137,4 @@ }; | ||
| kind, | ||
| text | ||
| text, | ||
| blockTextHash: blockTextHashOf(text) | ||
| })), | ||
@@ -168,2 +179,12 @@ totalBlocks: resolved.section.blocks.length, | ||
| const WORD_CHARACTER_AT_START = /^[\p{L}\p{M}\p{N}_]/u; | ||
| /** | ||
| * Window (UTF-16 code units) sliced on each side of a match for the | ||
| * whole-word boundary check. The regexes above only ever test the single | ||
| * grapheme adjacent to the match, but a surrogate pair or a base character | ||
| * with stacked combining marks can span a few code units — this window is | ||
| * generous enough to cover that while staying a constant, not | ||
| * `block.text.length`. Without a bound, `.slice(0, at)` / `.slice(at + len)` | ||
| * on every match makes a whole-word search O(n^2) in the block's length. | ||
| */ | ||
| const WORD_BOUNDARY_WINDOW = 8; | ||
| const findTextMatches = (blocks, query, matchCase, wholeWord, getTargetPage, pageFilter) => { | ||
@@ -176,6 +197,7 @@ const matches = []; | ||
| let occurrence = 0; | ||
| let blockTextHash; | ||
| for (const match of block.text.matchAll(expression)) { | ||
| const at = match.index; | ||
| const matchedText = match[0]; | ||
| if (wholeWord && (WORD_CHARACTER_AT_END.test(block.text.slice(0, at)) || WORD_CHARACTER_AT_START.test(block.text.slice(at + matchedText.length)))) continue; | ||
| if (wholeWord && (WORD_CHARACTER_AT_END.test(block.text.slice(Math.max(0, at - WORD_BOUNDARY_WINDOW), at)) || WORD_CHARACTER_AT_START.test(block.text.slice(at + matchedText.length, at + matchedText.length + WORD_BOUNDARY_WINDOW)))) continue; | ||
| const range = createFolioAITextRangeHandle({ | ||
@@ -194,2 +216,3 @@ blockId: block.id, | ||
| const contextEnd = Math.min(block.text.length, at + matchedText.length + CONTEXT_RADIUS); | ||
| blockTextHash ??= blockTextHashOf(block.text); | ||
| matches.push({ | ||
@@ -199,2 +222,3 @@ type: "main", | ||
| blockId: block.id, | ||
| blockTextHash, | ||
| range, | ||
@@ -222,3 +246,3 @@ occurrenceInBlock: occurrence, | ||
| const matchedText = match[0]; | ||
| if (wholeWord && (WORD_CHARACTER_AT_END.test(text.slice(0, at)) || WORD_CHARACTER_AT_START.test(text.slice(at + matchedText.length)))) continue; | ||
| if (wholeWord && (WORD_CHARACTER_AT_END.test(text.slice(Math.max(0, at - WORD_BOUNDARY_WINDOW), at)) || WORD_CHARACTER_AT_START.test(text.slice(at + matchedText.length, at + matchedText.length + WORD_BOUNDARY_WINDOW)))) continue; | ||
| if (matches.length < MAX_FIND_MATCHES) matches.push({ | ||
@@ -329,2 +353,19 @@ type: "story", | ||
| }); | ||
| /** | ||
| * Attach a `precondition.blockTextHash` guard to every operation before | ||
| * handing the batch to the bridge. | ||
| * | ||
| * When the caller (the model) already echoed a `precondition` on the | ||
| * operation — sourced from a `blockTextHash` returned by an earlier | ||
| * `read_document` / `read_section` / `find_text` call — that is honored | ||
| * as-is: it is the only signal that can catch a document edit made | ||
| * BETWEEN that read and this apply call. | ||
| * | ||
| * When no precondition was supplied, fall back to stamping one from this | ||
| * call's own fresh `bridge.snapshot()`, matching the previous behavior. | ||
| * This fallback cannot detect cross-call staleness — the snapshot it reads | ||
| * from is taken right before applying, so it always matches the current | ||
| * live document — but it still guards a later operation in the SAME batch | ||
| * against an earlier operation in that batch touching the same block. | ||
| */ | ||
| const applyOperations = (bridge, operations) => { | ||
@@ -334,2 +375,6 @@ const snapshot = bridge.snapshot(); | ||
| for (const operation of operations) { | ||
| if (operation.precondition !== void 0) { | ||
| guardedOperations.push(operation); | ||
| continue; | ||
| } | ||
| const blockId = operation.type === "replaceRange" || operation.type === "commentOnRange" || operation.type === "formatRange" ? operation.range.blockId : operation.blockId; | ||
@@ -336,0 +381,0 @@ const blockTextHash = snapshot.anchors[blockId]?.textHash; |
@@ -72,9 +72,16 @@ import { FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, parseFolioDocumentOperationBatch } from "@stll/folio-core/server"; | ||
| }; | ||
| const preconditionJsonSchema = { | ||
| /** | ||
| * JSON Schema for the contract's optional `precondition` guard: `{ | ||
| * blockTextHash }`, echoed from a `blockTextHash` returned by a document | ||
| * read (`read_document`, `read_section`, `find_text`). Exported so | ||
| * `tools.ts` can attach the same shape to `suggest_changes` and | ||
| * `add_comment` without redeclaring it. | ||
| */ | ||
| const FOLIO_PRECONDITION_JSON_SCHEMA = { | ||
| type: "object", | ||
| description: "Optional guard: the operation is skipped (reason `preconditionFailed`) unless the target block's normalized text hash still matches.", | ||
| description: "Optional guard against editing a block that changed since you read it: echo the `blockTextHash` returned by read_document / read_section / find_text for this block. The operation is skipped (reason `preconditionFailed`) unless the target block's current normalized text hash still matches.", | ||
| properties: { blockTextHash: { | ||
| type: "string", | ||
| pattern: NORMALIZED_TEXT_HASH_PATTERN, | ||
| description: "Normalized hash of the target block's text." | ||
| description: "Normalized hash of the target block's text, from a prior document read." | ||
| } }, | ||
@@ -116,3 +123,3 @@ required: ["blockTextHash"], | ||
| }, | ||
| precondition: preconditionJsonSchema | ||
| precondition: FOLIO_PRECONDITION_JSON_SCHEMA | ||
| }; | ||
@@ -666,2 +673,2 @@ const blockIdProperty = { | ||
| //#endregion | ||
| export { FOLIO_DOCUMENT_OPERATION_BATCH_JSON_SCHEMA, FOLIO_DOCUMENT_OPERATION_JSON_SCHEMA, FOLIO_TEXT_RANGE_JSON_SCHEMA, folioDocumentOperationBatchSchema }; | ||
| export { FOLIO_DOCUMENT_OPERATION_BATCH_JSON_SCHEMA, FOLIO_DOCUMENT_OPERATION_JSON_SCHEMA, FOLIO_PRECONDITION_JSON_SCHEMA, FOLIO_TEXT_RANGE_JSON_SCHEMA, folioDocumentOperationBatchSchema }; |
+14
-1
@@ -14,2 +14,15 @@ import { FolioAIEditOperation } from "@stll/folio-core/server"; | ||
| declare const MAX_OPERATION_TEXT_LENGTH = 100000; | ||
| /** | ||
| * Aggregate cap on the SUM of every text-bearing field's length across one | ||
| * `suggest_changes` call. Each field alone is bounded by | ||
| * {@link MAX_OPERATION_TEXT_LENGTH} and each call by | ||
| * {@link MAX_OPERATIONS_PER_CALL} operations, but an operation can carry | ||
| * several capped fields (e.g. `comment` plus `text`), and a table-insertion | ||
| * operation can carry up to {@link MAX_TABLE_INSERTION_CELL_TEXTS} capped | ||
| * cell texts — so a maximally-shaped batch could still push roughly | ||
| * 50 * 256 * 100,000 ~= 1.28B characters into the tracked-changes engine in | ||
| * one call even though every per-field cap was respected. This budget bounds | ||
| * the running total instead. | ||
| */ | ||
| declare const MAX_TOTAL_OPERATION_TEXT_LENGTH = 2000000; | ||
| /** Plain-language error for a string argument over {@link MAX_OPERATION_TEXT_LENGTH}. */ | ||
@@ -46,2 +59,2 @@ declare const explainTextTooLong: (label: string, length: number) => string; | ||
| //#endregion | ||
| export { MAX_OPERATIONS_PER_CALL, MAX_OPERATION_TEXT_LENGTH, ParseAddCommentResult, ParseSuggestChangesResult, explainTextTooLong, parseAddCommentInput, parseSuggestChangesInput }; | ||
| export { MAX_OPERATIONS_PER_CALL, MAX_OPERATION_TEXT_LENGTH, MAX_TOTAL_OPERATION_TEXT_LENGTH, ParseAddCommentResult, ParseSuggestChangesResult, explainTextTooLong, parseAddCommentInput, parseSuggestChangesInput }; |
+81
-7
@@ -15,5 +15,46 @@ //#region src/parse.ts | ||
| const MAX_TABLE_INSERTION_CELL_TEXTS = 256; | ||
| /** | ||
| * Aggregate cap on the SUM of every text-bearing field's length across one | ||
| * `suggest_changes` call. Each field alone is bounded by | ||
| * {@link MAX_OPERATION_TEXT_LENGTH} and each call by | ||
| * {@link MAX_OPERATIONS_PER_CALL} operations, but an operation can carry | ||
| * several capped fields (e.g. `comment` plus `text`), and a table-insertion | ||
| * operation can carry up to {@link MAX_TABLE_INSERTION_CELL_TEXTS} capped | ||
| * cell texts — so a maximally-shaped batch could still push roughly | ||
| * 50 * 256 * 100,000 ~= 1.28B characters into the tracked-changes engine in | ||
| * one call even though every per-field cap was respected. This budget bounds | ||
| * the running total instead. | ||
| */ | ||
| const MAX_TOTAL_OPERATION_TEXT_LENGTH = 2e6; | ||
| /** Plain-language error for a string argument over {@link MAX_OPERATION_TEXT_LENGTH}. */ | ||
| const explainTextTooLong = (label, length) => `${label} is ${length.toLocaleString()} characters, over the ${MAX_OPERATION_TEXT_LENGTH.toLocaleString()}-character limit; shorten it or split it into multiple operations.`; | ||
| /** Plain-language error when the running total across all operations exceeds {@link MAX_TOTAL_OPERATION_TEXT_LENGTH}. */ | ||
| const explainAggregateTextTooLong = (index) => `operations[${index}] pushes suggest_changes' combined text over the ${MAX_TOTAL_OPERATION_TEXT_LENGTH.toLocaleString()}-character aggregate limit across all operations; split the edit across multiple suggest_changes calls.`; | ||
| /** Normalized text hashes (`hashFolioAIBlockText`) look like `h` followed by base-36 digits. */ | ||
| const NORMALIZED_TEXT_HASH_PATTERN = /^h[0-9a-z]+$/; | ||
| /** | ||
| * Parse an optional `precondition: { blockTextHash }` field, present on | ||
| * `add_comment` / `suggest_changes` operation arguments when the caller | ||
| * echoes a `blockTextHash` returned by an earlier `read_document` / | ||
| * `read_section` / `find_text` call. Guards the eventual apply against a | ||
| * document edit made between that read and this call — `execute.ts` | ||
| * otherwise has no way to distinguish "the model never read this block" from | ||
| * "the model read it and it has since changed". | ||
| * | ||
| * Returns `undefined` when omitted, the parsed precondition when valid, or a | ||
| * plain-language error string otherwise. | ||
| */ | ||
| const readOperationPrecondition = (value) => { | ||
| if (value === void 0) return; | ||
| if (!isPlainObject(value)) return "`precondition` must be an object when provided."; | ||
| const blockTextHash = value["blockTextHash"]; | ||
| if (!isNonEmptyString(blockTextHash) || !NORMALIZED_TEXT_HASH_PATTERN.test(blockTextHash)) return "`precondition.blockTextHash` must be a normalized block text hash, echoed from read_document / read_section / find_text."; | ||
| return { blockTextHash }; | ||
| }; | ||
| /** Decrement the shared budget by `length`; returns true once the aggregate cap is exceeded. */ | ||
| const consumeTextBudget = (budget, length) => { | ||
| budget.remaining -= length; | ||
| return budget.remaining < 0; | ||
| }; | ||
| /** | ||
| * Validate `add_comment`'s raw tool-call arguments and build the | ||
@@ -51,2 +92,7 @@ * `commentOnBlock` {@link FolioAIEditOperation} it applies. Pure: does not | ||
| }; | ||
| const precondition = readOperationPrecondition(args["precondition"]); | ||
| if (typeof precondition === "string") return { | ||
| ok: false, | ||
| error: `add_comment's ${precondition}` | ||
| }; | ||
| return { | ||
@@ -59,3 +105,4 @@ ok: true, | ||
| comment: { text }, | ||
| ...quote !== void 0 ? { quote } : {} | ||
| ...quote !== void 0 ? { quote } : {}, | ||
| ...precondition !== void 0 ? { precondition } : {} | ||
| } | ||
@@ -79,3 +126,3 @@ }; | ||
| if (typeof endOffset !== "number" || !Number.isInteger(endOffset) || endOffset <= startOffset) return `${path}.endOffset must be an integer greater than startOffset.`; | ||
| if (!isNonEmptyString(selectedTextHash) || !/^h[0-9a-z]+$/.test(selectedTextHash)) return `${path}.selectedTextHash must be a normalized text hash.`; | ||
| if (!isNonEmptyString(selectedTextHash) || !NORMALIZED_TEXT_HASH_PATTERN.test(selectedTextHash)) return `${path}.selectedTextHash must be a normalized text hash.`; | ||
| return { | ||
@@ -90,5 +137,22 @@ type, | ||
| }; | ||
| /** Validate + map one `suggest_changes` operation, or return a plain-language error string. */ | ||
| const buildSuggestedOperation = (raw, index) => { | ||
| /** | ||
| * Validate + map one `suggest_changes` operation, or return a plain-language | ||
| * error string. Parses the shared `precondition` field itself (every | ||
| * operation variant accepts it, so it does not belong to any one type's | ||
| * branch below) and merges it onto whatever {@link buildSuggestedOperationCore} | ||
| * builds. | ||
| */ | ||
| const buildSuggestedOperation = (raw, index, budget) => { | ||
| if (!isPlainObject(raw)) return `operations[${index}] must be an object.`; | ||
| const precondition = readOperationPrecondition(raw["precondition"]); | ||
| if (typeof precondition === "string") return `operations[${index}] ${precondition}`; | ||
| const built = buildSuggestedOperationCore(raw, index, budget); | ||
| if (typeof built === "string") return built; | ||
| return precondition === void 0 ? built : { | ||
| ...built, | ||
| precondition | ||
| }; | ||
| }; | ||
| /** Type-and-shape-specific half of {@link buildSuggestedOperation}. */ | ||
| const buildSuggestedOperationCore = (raw, index, budget) => { | ||
| const type = raw["type"]; | ||
@@ -101,3 +165,6 @@ const blockId = raw["blockId"]; | ||
| if (comment !== void 0 && typeof comment !== "string") return `operations[${index}].comment must be a string when provided.`; | ||
| if (typeof comment === "string" && comment.length > 1e5) return explainTextTooLong(`operations[${index}].comment`, comment.length); | ||
| if (typeof comment === "string") { | ||
| if (comment.length > 1e5) return explainTextTooLong(`operations[${index}].comment`, comment.length); | ||
| if (consumeTextBudget(budget, comment.length)) return explainAggregateTextTooLong(index); | ||
| } | ||
| const opId = isNonEmptyString(id) ? id : `op-${index + 1}`; | ||
@@ -142,2 +209,3 @@ const commentField = typeof comment === "string" ? { comment: { text: comment } } : {}; | ||
| if (replace.length > 1e5) return explainTextTooLong(`operations[${index}] (replaceRange) \`replace\``, replace.length); | ||
| if (consumeTextBudget(budget, replace.length)) return explainAggregateTextTooLong(index); | ||
| return { | ||
@@ -162,2 +230,3 @@ id: opId, | ||
| if (cellText.length > 1e5) return explainTextTooLong(`operations[${index}] (${type}) \`cellTexts[${cellIndex}]\``, cellText.length); | ||
| if (consumeTextBudget(budget, cellText.length)) return explainAggregateTextTooLong(index); | ||
| cellTexts.push(cellText); | ||
@@ -200,4 +269,6 @@ } | ||
| if (find.length > 1e5) return explainTextTooLong(`operations[${index}] (replaceInBlock) \`find\``, find.length); | ||
| if (consumeTextBudget(budget, find.length)) return explainAggregateTextTooLong(index); | ||
| if (typeof replace !== "string") return `operations[${index}] (replaceInBlock) requires a string \`replace\`.`; | ||
| if (replace.length > 1e5) return explainTextTooLong(`operations[${index}] (replaceInBlock) \`replace\``, replace.length); | ||
| if (consumeTextBudget(budget, replace.length)) return explainAggregateTextTooLong(index); | ||
| return { | ||
@@ -216,2 +287,3 @@ id: opId, | ||
| if (text.length > 1e5) return explainTextTooLong(`operations[${index}] (${type}) \`text\``, text.length); | ||
| if (consumeTextBudget(budget, text.length)) return explainAggregateTextTooLong(index); | ||
| return { | ||
@@ -229,2 +301,3 @@ id: opId, | ||
| if (text.length > 1e5) return explainTextTooLong(`operations[${index}] (replaceBlock) \`text\``, text.length); | ||
| if (consumeTextBudget(budget, text.length)) return explainAggregateTextTooLong(index); | ||
| return { | ||
@@ -265,4 +338,5 @@ id: opId, | ||
| const operations = []; | ||
| const budget = { remaining: MAX_TOTAL_OPERATION_TEXT_LENGTH }; | ||
| for (const [index, raw] of rawOperations.entries()) { | ||
| const built = buildSuggestedOperation(raw, index); | ||
| const built = buildSuggestedOperation(raw, index, budget); | ||
| if (typeof built === "string") return { | ||
@@ -280,2 +354,2 @@ ok: false, | ||
| //#endregion | ||
| export { MAX_OPERATIONS_PER_CALL, MAX_OPERATION_TEXT_LENGTH, explainTextTooLong, parseAddCommentInput, parseSuggestChangesInput }; | ||
| export { MAX_OPERATIONS_PER_CALL, MAX_OPERATION_TEXT_LENGTH, MAX_TOTAL_OPERATION_TEXT_LENGTH, explainTextTooLong, parseAddCommentInput, parseSuggestChangesInput }; |
+15
-11
| import { FOLIO_AGENT_TOOL_NAMES } from "./types.js"; | ||
| import { FOLIO_TEXT_RANGE_JSON_SCHEMA } from "./operation-schema.js"; | ||
| import { FOLIO_PRECONDITION_JSON_SCHEMA, FOLIO_TEXT_RANGE_JSON_SCHEMA } from "./operation-schema.js"; | ||
| import { FOLIO_DOCUMENT_OPERATION_TYPES } from "@stll/folio-core/server"; | ||
@@ -92,6 +92,8 @@ //#region src/tools.ts | ||
| * `{ text }` object; | ||
| * - the review metadata (`severity`, `area`), `precondition` guard, and the | ||
| * insert/replace formatting knobs (`inheritFormatting`, `pageBreakBefore`, | ||
| * `preserveFormatting`, `styleId`, `parties`, `quote`, | ||
| * except `formatting`) are not exposed to the model. | ||
| * - the review metadata (`severity`, `area`) and the insert/replace | ||
| * formatting knobs (`inheritFormatting`, `pageBreakBefore`, | ||
| * `preserveFormatting`, `styleId`, `parties`, `quote`, except `formatting`) | ||
| * are not exposed to the model. `precondition` IS exposed (see | ||
| * `FOLIO_PRECONDITION_JSON_SCHEMA`) so the model can guard an edit against | ||
| * staleness introduced between an earlier read and this call. | ||
| */ | ||
@@ -174,3 +176,4 @@ const SUGGEST_CHANGES_EXCLUDED_OPERATION_TYPES = /* @__PURE__ */ new Set(["commentOnBlock", "insertSignatureTable"]); | ||
| description: "Optional comment explaining this edit, attached to the affected text, up to 100,000 characters." | ||
| } | ||
| }, | ||
| precondition: FOLIO_PRECONDITION_JSON_SCHEMA | ||
| }, | ||
@@ -191,3 +194,3 @@ required: ["type"], | ||
| name: FOLIO_AGENT_TOOL_NAMES.readDocument, | ||
| description: "Read the full document body as a list of blocks (paragraphs, headings, list items). Call this first, or whenever you need fresh block ids after a mutation — block ids from a stale read may no longer resolve.", | ||
| description: "Read the full document body as a list of blocks (paragraphs, headings, list items). Call this first, or whenever you need fresh block ids after a mutation — block ids from a stale read may no longer resolve. Each block includes a `blockTextHash`; echo it as `precondition.blockTextHash` on a suggest_changes / add_comment operation to guard against the block changing before that call runs.", | ||
| inputSchema: { | ||
@@ -217,3 +220,3 @@ type: "object", | ||
| name: FOLIO_AGENT_TOOL_NAMES.readSection, | ||
| description: "Read one logical heading section using a handle from get_document_outline. Content is block-bounded and paginated with an afterBlockId cursor, avoiding a full-document read.", | ||
| description: "Read one logical heading section using a handle from get_document_outline. Content is block-bounded and paginated with an afterBlockId cursor, avoiding a full-document read. Each block includes a `blockTextHash`; echo it as `precondition.blockTextHash` on a suggest_changes / add_comment operation.", | ||
| inputSchema: { | ||
@@ -260,3 +263,3 @@ type: "object", | ||
| name: FOLIO_AGENT_TOOL_NAMES.findText, | ||
| description: "Search a document, section, rendered page, or story and return `{ matches, truncated, totalMatches }`. Main-story matches include a stable block and exact range; other stories return story-relative offsets. Every match includes surrounding context. `matches` is capped at 200 entries; `truncated` is true and `totalMatches` reports the real count when there were more — narrow the query or scope instead of assuming you saw every hit.", | ||
| description: "Search a document, section, rendered page, or story and return `{ matches, truncated, totalMatches }`. Main-story matches include a stable block, exact range, and `blockTextHash` (echo it as `precondition.blockTextHash` on a later suggest_changes / add_comment operation); other stories return story-relative offsets. Every match includes surrounding context. `matches` is capped at 200 entries; `truncated` is true and `totalMatches` reports the real count when there were more — narrow the query or scope instead of assuming you saw every hit.", | ||
| inputSchema: { | ||
@@ -349,3 +352,4 @@ type: "object", | ||
| description: "The comment body, up to 100,000 characters." | ||
| } | ||
| }, | ||
| precondition: FOLIO_PRECONDITION_JSON_SCHEMA | ||
| }, | ||
@@ -358,3 +362,3 @@ required: ["blockId", "text"], | ||
| name: FOLIO_AGENT_TOOL_NAMES.suggestChanges, | ||
| description: "Propose one or more edits as tracked changes for a human to accept or reject — nothing is applied directly to the visible text. Each operation needs a blockId from `read_document` or `find_text`; if the document changed since that read, re-read it and retry with fresh ids (a skip reason will say so). At most 50 operations per call — batch larger edits across multiple calls. Each `find` / `replace` / `text` / `comment` string is capped at 100,000 characters.", | ||
| description: "Propose one or more edits as tracked changes for a human to accept or reject — nothing is applied directly to the visible text. Each operation needs a blockId from `read_document` or `find_text`; if the document changed since that read, re-read it and retry with fresh ids (a skip reason will say so). Pass the `blockTextHash` from that read as `precondition.blockTextHash` to guard against the document changing between the read and this call. At most 50 operations per call — batch larger edits across multiple calls. Each `find` / `replace` / `text` / `comment` string is capped at 100,000 characters.", | ||
| inputSchema: { | ||
@@ -361,0 +365,0 @@ type: "object", |
+11
-1
@@ -61,2 +61,11 @@ import { FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FolioAITextRangeHandle, FolioDocumentOperationIssue, FolioDocumentOperationReceipt, FolioDocumentOutlineEntry, FolioDocumentSectionHandle, FolioDocumentStoryHandle, FolioReviewChange } from "@stll/folio-core/server"; | ||
| text: string; | ||
| /** | ||
| * Normalized-text hash of this block at read time. Echo it back as | ||
| * `precondition.blockTextHash` on a `suggest_changes` / `add_comment` | ||
| * operation targeting this block so the apply call is guarded against | ||
| * edits made to the document between this read and that apply — without | ||
| * it, only same-call staleness within one `suggest_changes` batch is | ||
| * caught. | ||
| */ | ||
| blockTextHash: string; | ||
| }; | ||
@@ -69,3 +78,4 @@ /** One main-story `find_text` match. Existing consumers can keep using its block and range directly. */ | ||
| }; | ||
| blockId: string; /** Stable handle that can be passed directly to `show_in_document` or a range operation. */ | ||
| blockId: string; /** Normalized-text hash of the whole containing block; see {@link FolioAgentBlock.blockTextHash}. */ | ||
| blockTextHash: string; /** Stable handle that can be passed directly to `show_in_document` or a range operation. */ | ||
| range: FolioAITextRangeHandle; /** 0-based index of this occurrence within its block. */ | ||
@@ -72,0 +82,0 @@ occurrenceInBlock: number; /** Real rendered page when a live paginated surface supplies it. */ |
+2
-2
| { | ||
| "name": "@stll/folio-agents", | ||
| "version": "0.7.0", | ||
| "version": "0.8.0", | ||
| "description": "Framework-neutral LLM tool layer over folio's ai-edits engine: function-calling tools so an agent can read and mutate .docx documents through @stll/folio-core.", | ||
@@ -57,3 +57,3 @@ "keywords": [ | ||
| "dependencies": { | ||
| "@stll/folio-core": "^0.6.0" | ||
| "@stll/folio-core": "^0.12.0" | ||
| }, | ||
@@ -60,0 +60,0 @@ "main": "./dist/index.js", |
Sorry, the diff of this file is too big to display
226705
5.14%4989
3.68%+ Added
+ Added
- Removed
Updated