@stll/folio-agents 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ ---
2
+ name: folio-agents
3
+ description: >-
4
+ Integrate and use @stll/folio-agents, the framework-neutral LLM tool layer
5
+ over @stll/folio-core: function-calling tools that read and mutate a .docx
6
+ document, with every mutation landing as a tracked change or comment
7
+ pending human review. Load this skill when wiring folio's tools into an
8
+ agent's tool-use loop (TanStack AI, Vercel AI SDK, raw Anthropic/OpenAI
9
+ SDKs, or a custom loop), choosing between the headless reviewer bridge and
10
+ the live-editor-ref bridge, or summarizing what changed between two
11
+ document versions.
12
+ metadata:
13
+ type: core
14
+ library: "@stll/folio-agents"
15
+ library_version: "0.0.0"
16
+ ---
17
+
18
+ # @stll/folio-agents
19
+
20
+ Framework-neutral LLM tool layer over `@stll/folio-core`'s ai-edits engine.
21
+ Gives a model function-calling tools to read and mutate a `.docx` document;
22
+ every mutation (`add_comment`, `suggest_changes`) lands as a tracked change or
23
+ comment pending human review — nothing is silently finalized.
24
+
25
+ ## Integration recipe
26
+
27
+ 1. `getFolioToolDefinitions()` returns `FolioAgentToolDefinition[]` — plain
28
+ `{ name, description, inputSchema }` objects with a conservative JSON
29
+ Schema subset (`type: "object"`, `properties`, `required`, `enum`,
30
+ `additionalProperties: false`, plain arrays — safe for providers that
31
+ reject uncommon keywords, e.g. Gemini's OpenAPI-3.0 subset).
32
+ 2. Register them with your framework:
33
+ - TanStack AI: pass `inputSchema` directly as `toolDefinition({ inputSchema })` — no wrapper needed.
34
+ - Raw Anthropic/OpenAI SDKs: `toAnthropicTools(defs)` / `toOpenAITools(defs)`.
35
+ - Vercel AI SDK: wrap each `inputSchema` in `jsonSchema()` from `ai`.
36
+ 3. In your tool-use loop, for every `tool_use` / `tool_call` the model emits:
37
+ call `executeFolioToolCall(name, args, bridge)` → returns
38
+ `{ ok: true, result } | { ok: false, error }`.
39
+ 4. Feed that result straight back to the model as the tool result message —
40
+ both branches are meant to reach the model, not just the `ok: true` one:
41
+ error strings are plain language ("blockId not found; re-read...") that
42
+ the model can act on directly.
43
+
44
+ ## Bridge selection
45
+
46
+ A `FolioAgentBridge` is the structural seam `executeFolioToolCall` drives.
47
+ Two are shipped:
48
+
49
+ - `createReviewerBridge(FolioDocxReviewer.fromBuffer(...))` — server/headless,
50
+ full capability. Use for a document that isn't open in a live editor (a
51
+ backend job, a batch review pass). Defaults to `mode: "tracked-changes"`
52
+ (pass `{ mode: "direct" }` to edit in place instead — rarely what you want
53
+ for an unreviewed agent).
54
+ - `createEditorRefBridge({ ref, author, getComments, setComments })` — drives
55
+ a live `DocxEditorRef` from `@stll/folio-react` (or anything structurally
56
+ matching `FolioAgentEditorRefLike`). Comments live in host React state, not
57
+ on the ref, hence the `getComments` / `setComments` pair (the same ones the
58
+ host already passes to `DocxEditor`).
59
+
60
+ The editor-ref bridge's capability now depends on what the underlying
61
+ `DocxEditorRef` implements. Against a ref with the read surface
62
+ (`getTrackedChanges`, `getCommentAnchors`, `getSelectionText`, `getPageText`),
63
+ it has full parity with the headless one: `read_changes` returns real tracked
64
+ changes, comment entries carry a resolved `blockId` / `quote`, and `read_page`
65
+ / `read_selection` work against the live view. Against an older ref that
66
+ predates those methods, each falls back independently: `read_changes` returns
67
+ `[]`, comment entries keep `blockId: null` / `quote: ""`, and `read_page` /
68
+ `read_selection` report an unsupported-capability error (see
69
+ `src/bridges/editor-ref.ts` for the exact per-method fallback). A bridge
70
+ omits an optional capability member entirely rather than implementing it as a
71
+ no-op, and `executeFolioToolCall` reports the corresponding tool call as
72
+ unsupported rather than throwing — this is how the headless reviewer bridge
73
+ signals `read_page` / `read_selection` / `scroll_to_block` as unsupported too,
74
+ since a headless document has no live page/selection/scroll surface at all.
75
+
76
+ ## Host-managed review queue
77
+
78
+ A host that already has its own review-queue UX — its own place to store a
79
+ model's proposed edits pending approval, separate from folio's
80
+ tracked-changes redlines — can skip `executeFolioToolCall` for
81
+ `suggest_changes` / `add_comment` and instead validate the model's tool-call
82
+ arguments directly with `parseSuggestChangesInput` / `parseAddCommentInput`.
83
+ These are the exact same validation rules `executeFolioToolCall` runs
84
+ (argument shape, the 50-operation cap, the 100,000-character text caps),
85
+ factored out so both the host's path and folio's own executor agree on what a
86
+ valid `suggest_changes` / `add_comment` call looks like. On success they
87
+ return the parsed `FolioAIEditOperation`(s) to route into the host's own
88
+ queue; on failure they return the same plain-language `error` string meant to
89
+ go straight back to the model.
90
+
91
+ ## Ground rules for the model
92
+
93
+ - Block ids (`blockId`) and comment ids (`commentId`) only ever come from a
94
+ prior tool call in the same conversation — `read_document`, `find_text`, or
95
+ `read_comments`. Never invent or reuse one from outside the conversation;
96
+ ids are not guessable and change whenever the document's structure changes.
97
+ - `suggest_changes` operations that get skipped (`skipped: [{ id, reason }]`)
98
+ return a plain-language reason, not a machine code (e.g. "the block changed
99
+ since your snapshot; re-read the document and retry with fresh ids"). Treat
100
+ a skip as a retry signal: re-read (`read_document` or `find_text`) and
101
+ reissue with fresh ids, not a dead end.
102
+ - `suggest_changes` defaults to tracked-changes mode on both shipped bridges
103
+ — it proposes redlines for a human to accept or reject, never edits the
104
+ visible text directly.
105
+ - The tool surface here is fixed: five edit operation kinds
106
+ (`replaceInBlock`, `insertAfterBlock`, `insertBeforeBlock`, `replaceBlock`,
107
+ `deleteBlock`) plus comment/reply/resolve. There is no
108
+ `insertSignatureTable`-style structural op, and none should be invented —
109
+ compose the five primitives, or extend `@stll/folio-core`'s ai-edits engine
110
+ itself if a document needs a structural operation this package doesn't
111
+ expose.
112
+ - **Untrusted documents:** `read_document`, `read_comments`, `read_changes`,
113
+ and `find_text` return document content verbatim, so a `.docx` from an
114
+ untrusted party can inject prompt instructions straight into the model's
115
+ context via its text — hosts should treat any document-derived tool result
116
+ as untrusted model input. Mutations stay safe regardless: `suggest_changes`
117
+ and `add_comment` only ever produce tracked changes or comments pending
118
+ human review, so an injected instruction can propose an edit, never apply
119
+ one.
120
+
121
+ ## Summarizing what changed
122
+
123
+ Two different questions, two different tools:
124
+
125
+ - **Pending redlines in one document** — what a reviewer would see as tracked
126
+ changes right now: use the `read_changes` tool (headless bridge only; see
127
+ the capability gap above) or call `reviewer.getChanges()` directly.
128
+ - **Between two saved `.docx` versions** — `compareDocxVersions(previousBuffer,
129
+ currentBuffer)` + `formatVersionDiffForLLM(diff)`. Both are plain async
130
+ functions, not tool definitions: a model can't attach two buffers to one
131
+ tool call (they aren't JSON-serializable arguments). Wrap them in a
132
+ host-side tool keyed by version identifiers instead — the model calls it
133
+ with two version ids, your backend resolves the buffers and returns the
134
+ formatted diff text.
135
+
136
+ `compareDocxVersions` compares the AS-ACCEPTED view of each buffer: any
137
+ tracked changes already pending in either one count as already applied before
138
+ the diff runs, so diffing two versions that each carry their own uncommitted
139
+ redlines still produces a clean diff instead of raw markup noise.
140
+
141
+ ## Reference
142
+
143
+ Tool names, full JSON Schemas, and the headless/live-editor quickstarts are
144
+ in this package's README.md. Read `src/tools.ts` for the exact input schema
145
+ of every tool and `src/execute.ts` for the skip-reason vocabulary.