@nhtio/adk 1.20260609.0 → 1.20260610.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.
- package/CHANGELOG.md +132 -9
- package/batteries/llm/ollama/helpers.cjs +9 -0
- package/batteries/llm/ollama/helpers.cjs.map +1 -1
- package/batteries/llm/ollama/helpers.mjs +9 -0
- package/batteries/llm/ollama/helpers.mjs.map +1 -1
- package/batteries/llm/openai_chat_completions/helpers.cjs +19 -0
- package/batteries/llm/openai_chat_completions/helpers.cjs.map +1 -1
- package/batteries/llm/openai_chat_completions/helpers.mjs +19 -0
- package/batteries/llm/openai_chat_completions/helpers.mjs.map +1 -1
- package/batteries/media/builder.d.ts +245 -0
- package/batteries/media/contracts.cjs +119 -0
- package/batteries/media/contracts.cjs.map +1 -0
- package/batteries/media/contracts.d.ts +321 -0
- package/batteries/media/contracts.mjs +110 -0
- package/batteries/media/contracts.mjs.map +1 -0
- package/batteries/media/engines/audio_decode.cjs +92 -0
- package/batteries/media/engines/audio_decode.cjs.map +1 -0
- package/batteries/media/engines/audio_decode.d.ts +46 -0
- package/batteries/media/engines/audio_decode.mjs +90 -0
- package/batteries/media/engines/audio_decode.mjs.map +1 -0
- package/batteries/media/engines/execa_executor.cjs +64 -0
- package/batteries/media/engines/execa_executor.cjs.map +1 -0
- package/batteries/media/engines/execa_executor.d.ts +54 -0
- package/batteries/media/engines/execa_executor.mjs +62 -0
- package/batteries/media/engines/execa_executor.mjs.map +1 -0
- package/batteries/media/engines/fs_workspace.cjs +84 -0
- package/batteries/media/engines/fs_workspace.cjs.map +1 -0
- package/batteries/media/engines/fs_workspace.d.ts +51 -0
- package/batteries/media/engines/fs_workspace.mjs +82 -0
- package/batteries/media/engines/fs_workspace.mjs.map +1 -0
- package/batteries/media/engines/jimp.cjs +116 -0
- package/batteries/media/engines/jimp.cjs.map +1 -0
- package/batteries/media/engines/jimp.d.ts +32 -0
- package/batteries/media/engines/jimp.mjs +114 -0
- package/batteries/media/engines/jimp.mjs.map +1 -0
- package/batteries/media/engines/sharp.cjs +120 -0
- package/batteries/media/engines/sharp.cjs.map +1 -0
- package/batteries/media/engines/sharp.d.ts +42 -0
- package/batteries/media/engines/sharp.mjs +117 -0
- package/batteries/media/engines/sharp.mjs.map +1 -0
- package/batteries/media/engines/soffice.cjs +246 -0
- package/batteries/media/engines/soffice.cjs.map +1 -0
- package/batteries/media/engines/soffice.d.ts +39 -0
- package/batteries/media/engines/soffice.mjs +244 -0
- package/batteries/media/engines/soffice.mjs.map +1 -0
- package/batteries/media/engines/tesseract_js.cjs +87 -0
- package/batteries/media/engines/tesseract_js.cjs.map +1 -0
- package/batteries/media/engines/tesseract_js.d.ts +41 -0
- package/batteries/media/engines/tesseract_js.mjs +85 -0
- package/batteries/media/engines/tesseract_js.mjs.map +1 -0
- package/batteries/media/engines/transformers_asr.cjs +111 -0
- package/batteries/media/engines/transformers_asr.cjs.map +1 -0
- package/batteries/media/engines/transformers_asr.d.ts +41 -0
- package/batteries/media/engines/transformers_asr.mjs +109 -0
- package/batteries/media/engines/transformers_asr.mjs.map +1 -0
- package/batteries/media/exceptions.d.ts +103 -0
- package/batteries/media/forge.cjs +403 -0
- package/batteries/media/forge.cjs.map +1 -0
- package/batteries/media/forge.d.ts +90 -0
- package/batteries/media/forge.mjs +399 -0
- package/batteries/media/forge.mjs.map +1 -0
- package/batteries/media/formats.d.ts +72 -0
- package/batteries/media/index.d.ts +136 -0
- package/batteries/media/lint.cjs +339 -0
- package/batteries/media/lint.cjs.map +1 -0
- package/batteries/media/lint.d.ts +117 -0
- package/batteries/media/lint.mjs +331 -0
- package/batteries/media/lint.mjs.map +1 -0
- package/batteries/media/pipe.d.ts +66 -0
- package/batteries/media/plan.d.ts +133 -0
- package/batteries/media/registry.d.ts +92 -0
- package/batteries/media/runtime.d.ts +105 -0
- package/batteries/media/steps/doc.d.ts +33 -0
- package/batteries/media/steps/image_audio.d.ts +24 -0
- package/batteries/media/steps/ingest.d.ts +25 -0
- package/batteries/media/steps/pages.d.ts +18 -0
- package/batteries/media/steps/sheet.d.ts +36 -0
- package/batteries/media/steps/slides.d.ts +35 -0
- package/batteries/media/steps/text.d.ts +43 -0
- package/batteries/media/validate.d.ts +49 -0
- package/batteries/media/verbs.d.ts +126 -0
- package/batteries/media.cjs +3049 -0
- package/batteries/media.cjs.map +1 -0
- package/batteries/media.mjs +3009 -0
- package/batteries/media.mjs.map +1 -0
- package/batteries/tools/_shared/index.d.ts +142 -0
- package/batteries/tools/_shared.cjs +173 -0
- package/batteries/tools/_shared.cjs.map +1 -0
- package/batteries/tools/_shared.mjs +164 -0
- package/batteries/tools/_shared.mjs.map +1 -0
- package/batteries/tools/index.d.ts +2 -0
- package/batteries/tools/scrapper/exceptions.d.ts +21 -0
- package/batteries/tools/scrapper/index.d.ts +172 -0
- package/batteries/tools/scrapper/shared.d.ts +146 -0
- package/batteries/tools/scrapper.cjs +8 -0
- package/batteries/tools/scrapper.mjs +2 -0
- package/batteries/tools/searxng/index.d.ts +54 -20
- package/batteries/tools/searxng.cjs +2 -1
- package/batteries/tools/searxng.mjs +2 -2
- package/batteries/tools/web_retrieval/index.d.ts +186 -0
- package/batteries/tools/web_retrieval.cjs +206 -0
- package/batteries/tools/web_retrieval.cjs.map +1 -0
- package/batteries/tools/web_retrieval.mjs +201 -0
- package/batteries/tools/web_retrieval.mjs.map +1 -0
- package/batteries/tools.cjs +13 -1
- package/batteries/tools.mjs +4 -2
- package/batteries.cjs +13 -1
- package/batteries.mjs +4 -2
- package/common.d.ts +1 -1
- package/eslint.cjs +1 -1
- package/eslint.mjs +1 -1
- package/exceptions-C7FSHEnV.mjs +87 -0
- package/exceptions-C7FSHEnV.mjs.map +1 -0
- package/exceptions-CQi_lNs1.js +152 -0
- package/exceptions-CQi_lNs1.js.map +1 -0
- package/index.cjs +2 -2
- package/index.mjs +2 -2
- package/mcp/adk-docs-corpus.json +1 -1
- package/package.json +301 -178
- package/scrapper-BOLWYGbD.js +463 -0
- package/scrapper-BOLWYGbD.js.map +1 -0
- package/scrapper-hDKlNuCT.mjs +433 -0
- package/scrapper-hDKlNuCT.mjs.map +1 -0
- package/{searxng-Bkrwhwhw.js → searxng-CJtEpa8p.js} +82 -85
- package/searxng-CJtEpa8p.js.map +1 -0
- package/{searxng-CyA-nEu5.mjs → searxng-riarj_0u.mjs} +76 -85
- package/searxng-riarj_0u.mjs.map +1 -0
- package/skills/adk-assembly/SKILL.md +2 -2
- package/validate-BFaUYHDN.js +1298 -0
- package/validate-BFaUYHDN.js.map +1 -0
- package/validate-DSZ3wicB.mjs +1215 -0
- package/validate-DSZ3wicB.mjs.map +1 -0
- package/searxng-Bkrwhwhw.js.map +0 -1
- package/searxng-CyA-nEu5.mjs.map +0 -1
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
require("../../chunk-Ble4zEEl.js");
|
|
3
|
+
const require_tool_registry = require("../../tool_registry-CKJPze3j.js");
|
|
4
|
+
const require_tool = require("../../tool-D5WGVIcI.js");
|
|
5
|
+
require("../../common-DZl3ADJs.js");
|
|
6
|
+
const require_tool_call = require("../../tool_call-DixVlW40.js");
|
|
7
|
+
const require_spooled_markdown_artifact = require("../../spooled_markdown_artifact-ClX72lek.js");
|
|
8
|
+
require("../../guards.cjs");
|
|
9
|
+
const require_exceptions = require("../../exceptions-CQi_lNs1.js");
|
|
10
|
+
const require_validate = require("../../validate-BFaUYHDN.js");
|
|
11
|
+
let _nhtio_validation = require("@nhtio/validation");
|
|
12
|
+
//#region src/batteries/media/forge.ts
|
|
13
|
+
/**
|
|
14
|
+
* Agent glue: forge ADK tools from a media pipeline, so a model can drive local media work.
|
|
15
|
+
*
|
|
16
|
+
* @module @nhtio/adk/batteries/media/forge
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* The pipeline core (`@nhtio/adk/batteries/media`) is agent-agnostic; this module is the layer
|
|
20
|
+
* that knows the ADK loop. {@link forgeMediaTools} mints `Tool` instances over a configured
|
|
21
|
+
* {@link @nhtio/adk/batteries/media!MediaPipeline} in one of two surfaces (the consumer picks
|
|
22
|
+
* per deployment):
|
|
23
|
+
*
|
|
24
|
+
* - `'composite'` — one `media_query` tool taking `{ media_id, q }` (a pipe expression — the
|
|
25
|
+
* headline LLM DSL) or `{ media_id, ops }` (structured form), plus `list_media`. The tool
|
|
26
|
+
* description embeds the engine-narrowed verb grammar so the model never sees a verb the
|
|
27
|
+
* deployment can't run. One round-trip for multi-step work.
|
|
28
|
+
* - `'granular'` — one narrow tool per available verb (`doc_select`, `sheet_update_cells`, …),
|
|
29
|
+
* each internally a one-verb plan. Friendlier to small models; bigger tool list.
|
|
30
|
+
*
|
|
31
|
+
* Media flows in by reference: the model passes `media_id` values it discovered via the
|
|
32
|
+
* `list_media` tool (or inline id markers, where the LLM battery renders them); the resolver
|
|
33
|
+
* scans `ctx.turnMessages[].attachments` and `ctx.turnToolCalls[].results` by default. Outputs
|
|
34
|
+
* are persisted through `ctx.storeMediaBytes` and returned as `Media.toolGenerated(...)`, so
|
|
35
|
+
* file results land on `ToolCall.results` as first-class media.
|
|
36
|
+
*
|
|
37
|
+
* Processing failures return readable strings (`Error (CODE): …`) the model can act on; the
|
|
38
|
+
* pipe DSL's own syntax/semantic errors render the same way, so a model can repair its
|
|
39
|
+
* statement and retry. An optional {@link ToolGateFn} runs before every execution — the seam
|
|
40
|
+
* for human-approval/RBAC flows built on `ctx.waitFor` (the ADK gates primitive).
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* The default media resolver: scan `ctx.turnMessages[].attachments`, then
|
|
44
|
+
* `ctx.turnToolCalls[].results`, for a Media with the given id.
|
|
45
|
+
*
|
|
46
|
+
* @param ctx - The dispatch context.
|
|
47
|
+
* @param mediaId - The id to find.
|
|
48
|
+
* @returns The Media, or `undefined` when nothing in the turn carries that id.
|
|
49
|
+
*/
|
|
50
|
+
var defaultResolveMedia = (ctx, mediaId) => {
|
|
51
|
+
for (const message of ctx.turnMessages) for (const attachment of message.attachments ?? []) if (attachment.id === mediaId) return attachment;
|
|
52
|
+
for (const toolCall of ctx.turnToolCalls) {
|
|
53
|
+
const results = toolCall.results;
|
|
54
|
+
if (require_tool_call.Media.isMedia(results) && results.id === mediaId) return results;
|
|
55
|
+
if (Array.isArray(results)) {
|
|
56
|
+
const hit = results.find((r) => require_tool_call.Media.isMedia(r) && r.id === mediaId);
|
|
57
|
+
if (hit) return hit;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
/** Enumerate every Media visible in the turn (for `list_media` and MEDIA_NOT_FOUND hints). */
|
|
62
|
+
var enumerateMedia = (ctx) => {
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const message of ctx.turnMessages) for (const attachment of message.attachments ?? []) out.push({
|
|
65
|
+
id: attachment.id,
|
|
66
|
+
filename: attachment.filename,
|
|
67
|
+
kind: attachment.kind,
|
|
68
|
+
mimeType: attachment.mimeType,
|
|
69
|
+
source: attachment.source,
|
|
70
|
+
origin: "attachment"
|
|
71
|
+
});
|
|
72
|
+
for (const toolCall of ctx.turnToolCalls) {
|
|
73
|
+
const results = toolCall.results;
|
|
74
|
+
const mediaList = require_tool_call.Media.isMedia(results) ? [results] : Array.isArray(results) ? results.filter((r) => require_tool_call.Media.isMedia(r)) : [];
|
|
75
|
+
for (const media of mediaList) out.push({
|
|
76
|
+
id: media.id,
|
|
77
|
+
filename: media.filename,
|
|
78
|
+
kind: media.kind,
|
|
79
|
+
mimeType: media.mimeType,
|
|
80
|
+
source: media.source,
|
|
81
|
+
origin: "tool-result"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
};
|
|
86
|
+
var failure = (code, message) => `Error (${code}): ${message}`;
|
|
87
|
+
var mediaKindOf = (mimeType) => {
|
|
88
|
+
const mime = mimeType.toLowerCase();
|
|
89
|
+
if (mime.startsWith("image/")) return "image";
|
|
90
|
+
if (mime.startsWith("audio/")) return "audio";
|
|
91
|
+
if (mime.startsWith("video/")) return "video";
|
|
92
|
+
return "document";
|
|
93
|
+
};
|
|
94
|
+
/** Load a resolved Media's bytes into a pipeline payload. */
|
|
95
|
+
var toPayload = async (media) => ({
|
|
96
|
+
bytes: await media.asBytes(),
|
|
97
|
+
mimeType: media.mimeType,
|
|
98
|
+
filename: media.filename
|
|
99
|
+
});
|
|
100
|
+
/** Persist a payload through the dispatch byte conduit and mint a first-party Media. */
|
|
101
|
+
var toMedia = async (ctx, payload, toolName) => {
|
|
102
|
+
const id = crypto.randomUUID();
|
|
103
|
+
const reader = await ctx.storeMediaBytes(id, payload.bytes);
|
|
104
|
+
return require_tool_call.Media.toolGenerated({
|
|
105
|
+
id,
|
|
106
|
+
kind: mediaKindOf(payload.mimeType),
|
|
107
|
+
mimeType: payload.mimeType,
|
|
108
|
+
filename: payload.filename,
|
|
109
|
+
reader,
|
|
110
|
+
source: `tool:${toolName}`
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
/** Render a plan result as a tool handler return value. */
|
|
114
|
+
var renderResult = async (ctx, result, toolName) => {
|
|
115
|
+
if (result.kind === "media") return toMedia(ctx, result.payload, toolName);
|
|
116
|
+
if (result.kind === "media-list") return Promise.all(result.payloads.map((p) => toMedia(ctx, p, toolName)));
|
|
117
|
+
return typeof result.data === "string" ? result.data : JSON.stringify(result.data, null, 2);
|
|
118
|
+
};
|
|
119
|
+
/** Resolve + acquire the input media or produce a model-actionable failure string. */
|
|
120
|
+
var acquire = async (ctx, resolveMedia, mediaId) => {
|
|
121
|
+
const media = await resolveMedia(ctx, mediaId);
|
|
122
|
+
if (!media) {
|
|
123
|
+
const visible = enumerateMedia(ctx);
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
failure: failure("MEDIA_NOT_FOUND", `no media with id "${mediaId}". ${visible.length > 0 ? `Visible media ids: ${visible.map((m) => `${m.id} (${m.filename})`).join(", ")}` : "No media is visible in this turn — ask the user to attach a file, or call list_media to check."}`)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
payload: await toPayload(media)
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
/** Build a per-run @id ref resolver bound to this dispatch's visible media. */
|
|
135
|
+
var refResolverFor = (ctx, resolveMedia) => async (id) => {
|
|
136
|
+
const media = await resolveMedia(ctx, id);
|
|
137
|
+
if (!media) {
|
|
138
|
+
const visible = enumerateMedia(ctx).map((m) => m.id).join(", ");
|
|
139
|
+
throw new Error(`referenced media @${id} was not found in this turn. Visible ids: ${visible || "(none)"}`);
|
|
140
|
+
}
|
|
141
|
+
return toPayload(media);
|
|
142
|
+
};
|
|
143
|
+
/** Run the optional gate; a throw becomes the tool's error (standard downstream wrapping). */
|
|
144
|
+
var runGate = async (gate, ctx, tool, args) => {
|
|
145
|
+
if (gate) await gate(ctx, {
|
|
146
|
+
tool,
|
|
147
|
+
args
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
/** Map a thrown pipeline error to a readable failure string the model can act on. */
|
|
151
|
+
var renderError = (err) => {
|
|
152
|
+
if (require_tool_registry.isError(err) && err.name.startsWith("E_MEDIA_")) return failure(err.name.replace(/^E_MEDIA_/, ""), err.message);
|
|
153
|
+
throw err;
|
|
154
|
+
};
|
|
155
|
+
var argHelp = (name, arg) => {
|
|
156
|
+
return `${name}${arg.required ? "" : "?"}=${arg.type === "enum" ? (arg.values ?? []).join("|") : arg.type === "json" ? "'<JSON>'" : arg.type}`;
|
|
157
|
+
};
|
|
158
|
+
var verbHelp = (spec) => {
|
|
159
|
+
const verb = spec.id.replace(/\./g, " ");
|
|
160
|
+
const args = Object.entries(spec.args).map(([n, a]) => argHelp(n, a)).join(" ");
|
|
161
|
+
return args.length > 0 ? `${verb} ${args}` : verb;
|
|
162
|
+
};
|
|
163
|
+
/**
|
|
164
|
+
* Generate the model-facing grammar text for the configured deployment: only available verbs,
|
|
165
|
+
* with arg signatures, plus few-shot examples rendered from real plans via `toPipe` so they
|
|
166
|
+
* can never drift from the parser.
|
|
167
|
+
*/
|
|
168
|
+
var grammarText = (pipeline) => {
|
|
169
|
+
const available = new Set(require_validate.availableVerbs(pipeline.capabilities));
|
|
170
|
+
const lines = [];
|
|
171
|
+
for (const spec of require_validate.VERB_INDEX.values()) {
|
|
172
|
+
if (!available.has(spec.id.replace(/[._]+/g, " "))) continue;
|
|
173
|
+
lines.push(`- ${verbHelp(spec)} — ${spec.description}`);
|
|
174
|
+
}
|
|
175
|
+
const examples = [require_validate.toPipe({ steps: [{
|
|
176
|
+
verb: "select",
|
|
177
|
+
args: { pages: [
|
|
178
|
+
2,
|
|
179
|
+
3,
|
|
180
|
+
4,
|
|
181
|
+
5
|
|
182
|
+
] }
|
|
183
|
+
}, {
|
|
184
|
+
verb: "extract.text",
|
|
185
|
+
args: {}
|
|
186
|
+
}] }), require_validate.toPipe({ steps: [{
|
|
187
|
+
verb: "extract.text",
|
|
188
|
+
args: {}
|
|
189
|
+
}, {
|
|
190
|
+
verb: "chunk",
|
|
191
|
+
args: {
|
|
192
|
+
by: "sentence",
|
|
193
|
+
size: 512
|
|
194
|
+
}
|
|
195
|
+
}] })];
|
|
196
|
+
if (available.has("redact")) examples.push(require_validate.toPipe({ steps: [{
|
|
197
|
+
verb: "redact",
|
|
198
|
+
args: {
|
|
199
|
+
match: [{
|
|
200
|
+
source: "\\d{3}-\\d{2}-\\d{4}",
|
|
201
|
+
flags: ""
|
|
202
|
+
}],
|
|
203
|
+
replace: "[SSN]"
|
|
204
|
+
}
|
|
205
|
+
}] }));
|
|
206
|
+
return [
|
|
207
|
+
"Statements are pipe expressions: verb name=value ... | verb name=value ...",
|
|
208
|
+
"Named args only. Indices are 1-based. Quote values containing spaces or dashes.",
|
|
209
|
+
"Structured payloads are quoted JSON: updates='[{\"address\":\"B2\",\"value\":3}]'.",
|
|
210
|
+
"Reference other media inline by id: merge with=@<media id> (ids come from list_media).",
|
|
211
|
+
"",
|
|
212
|
+
"Available verbs:",
|
|
213
|
+
...lines,
|
|
214
|
+
"",
|
|
215
|
+
"Examples (remember: inside JSON tool args, backslashes must be doubled — \\\\d):",
|
|
216
|
+
...examples.map((e) => ` ${e}`)
|
|
217
|
+
].join("\n");
|
|
218
|
+
};
|
|
219
|
+
var buildListMediaTool = (overrides) => {
|
|
220
|
+
return new require_tool.Tool({
|
|
221
|
+
name: overrides?.list_media?.name ?? "list_media",
|
|
222
|
+
description: overrides?.list_media?.description ?? "Lists every media file visible in this conversation turn (user attachments and files produced by prior tool calls), with the media ids other media tools require.",
|
|
223
|
+
inputSchema: _nhtio_validation.validator.object({}),
|
|
224
|
+
artifactConstructor: () => require_spooled_markdown_artifact.SpooledJsonArtifact,
|
|
225
|
+
handler: async (_args, ctx) => {
|
|
226
|
+
const media = enumerateMedia(ctx);
|
|
227
|
+
return JSON.stringify(media, null, 2);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
var buildCompositeTool = (pipeline, resolveMedia, gate, overrides) => {
|
|
232
|
+
const name = overrides?.media_query?.name ?? "media_query";
|
|
233
|
+
return new require_tool.Tool({
|
|
234
|
+
name,
|
|
235
|
+
description: overrides?.media_query?.description ?? `Runs a media-processing statement against a file from this conversation, locally (no external services). Provide media_id (from list_media) and either q (a pipe statement) or ops (structured steps).
|
|
236
|
+
|
|
237
|
+
${grammarText(pipeline)}`,
|
|
238
|
+
inputSchema: _nhtio_validation.validator.object({
|
|
239
|
+
media_id: _nhtio_validation.validator.string().required().description("The id of the media to process (call list_media to discover ids)."),
|
|
240
|
+
q: _nhtio_validation.validator.string().description("A pipe statement, e.g. \"select pages=2-5 | extract text\"."),
|
|
241
|
+
ops: _nhtio_validation.validator.array().items(_nhtio_validation.validator.object({
|
|
242
|
+
verb: _nhtio_validation.validator.string().required(),
|
|
243
|
+
args: _nhtio_validation.validator.object().unknown(true).required()
|
|
244
|
+
})).description("Structured alternative to q: [{ verb, args }] steps.")
|
|
245
|
+
}),
|
|
246
|
+
handler: async (args, ctx) => {
|
|
247
|
+
const { media_id: mediaId, q, ops } = args;
|
|
248
|
+
if (q === void 0 === (ops === void 0)) return failure("BAD_REQUEST", "provide exactly one of q (pipe statement) or ops (structured steps)");
|
|
249
|
+
const dispatch = ctx;
|
|
250
|
+
await runGate(gate, dispatch, name, args);
|
|
251
|
+
const acquired = await acquire(dispatch, resolveMedia, mediaId);
|
|
252
|
+
if (!acquired.ok) return acquired.failure;
|
|
253
|
+
try {
|
|
254
|
+
const runOptions = {
|
|
255
|
+
signal: dispatch.abortSignal,
|
|
256
|
+
resolveRef: refResolverFor(dispatch, resolveMedia)
|
|
257
|
+
};
|
|
258
|
+
return await renderResult(dispatch, q !== void 0 ? await pipeline.query(acquired.payload, q, runOptions) : await pipeline.ops(acquired.payload, ops, runOptions), name);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
return renderError(err);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
/** Convert a granular tool's flat args into the verb's op args (drops media_id). */
|
|
266
|
+
var opArgsFrom = (args) => {
|
|
267
|
+
const out = {};
|
|
268
|
+
for (const [k, v] of Object.entries(args)) {
|
|
269
|
+
if (k === "media_id" || v === void 0) continue;
|
|
270
|
+
out[k] = v;
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
};
|
|
274
|
+
var granularSchemaFor = (spec) => {
|
|
275
|
+
const shape = { media_id: _nhtio_validation.validator.string().required().description("The id of the media to process (call list_media to discover ids).") };
|
|
276
|
+
for (const [argName, arg] of Object.entries(spec.args)) {
|
|
277
|
+
let schema;
|
|
278
|
+
switch (arg.type) {
|
|
279
|
+
case "number":
|
|
280
|
+
schema = _nhtio_validation.validator.number();
|
|
281
|
+
if (arg.min !== void 0) schema = schema.min(arg.min);
|
|
282
|
+
if (arg.max !== void 0) schema = schema.max(arg.max);
|
|
283
|
+
break;
|
|
284
|
+
case "boolean":
|
|
285
|
+
schema = _nhtio_validation.validator.boolean();
|
|
286
|
+
break;
|
|
287
|
+
case "enum":
|
|
288
|
+
schema = _nhtio_validation.validator.string().valid(...arg.values ?? []);
|
|
289
|
+
break;
|
|
290
|
+
case "number-list":
|
|
291
|
+
schema = _nhtio_validation.validator.array().items(_nhtio_validation.validator.number().min(arg.min ?? 1));
|
|
292
|
+
break;
|
|
293
|
+
case "string-list":
|
|
294
|
+
schema = arg.values ? _nhtio_validation.validator.array().items(_nhtio_validation.validator.string().valid(...arg.values)) : _nhtio_validation.validator.array().items(_nhtio_validation.validator.string());
|
|
295
|
+
break;
|
|
296
|
+
case "media-ref":
|
|
297
|
+
schema = _nhtio_validation.validator.string().description("A media id (from list_media) for the other media in this operation.");
|
|
298
|
+
break;
|
|
299
|
+
case "media-ref-list":
|
|
300
|
+
schema = _nhtio_validation.validator.array().items(_nhtio_validation.validator.string());
|
|
301
|
+
break;
|
|
302
|
+
case "name-or-index":
|
|
303
|
+
schema = _nhtio_validation.validator.alternatives().try(_nhtio_validation.validator.number().integer().min(1), _nhtio_validation.validator.string());
|
|
304
|
+
break;
|
|
305
|
+
case "regex-or-string-list":
|
|
306
|
+
schema = _nhtio_validation.validator.alternatives().try(_nhtio_validation.validator.string(), _nhtio_validation.validator.array().items(_nhtio_validation.validator.string()));
|
|
307
|
+
break;
|
|
308
|
+
case "json":
|
|
309
|
+
schema = _nhtio_validation.validator.any().optional();
|
|
310
|
+
break;
|
|
311
|
+
default: schema = _nhtio_validation.validator.string();
|
|
312
|
+
}
|
|
313
|
+
schema = schema.description(arg.description);
|
|
314
|
+
if (arg.required) schema = schema.required();
|
|
315
|
+
shape[argName] = schema;
|
|
316
|
+
}
|
|
317
|
+
return _nhtio_validation.validator.object(shape);
|
|
318
|
+
};
|
|
319
|
+
/** Normalize a granular tool's media-ref string args to MediaRef IR values. */
|
|
320
|
+
var normalizeRefArgs = (spec, args) => {
|
|
321
|
+
for (const [argName, arg] of Object.entries(spec.args)) {
|
|
322
|
+
const value = args[argName];
|
|
323
|
+
if (value === void 0) continue;
|
|
324
|
+
if (arg.type === "media-ref" && typeof value === "string") args[argName] = {
|
|
325
|
+
kind: "id",
|
|
326
|
+
id: value
|
|
327
|
+
};
|
|
328
|
+
if (arg.type === "media-ref-list" && Array.isArray(value)) args[argName] = value.map((v) => typeof v === "string" ? {
|
|
329
|
+
kind: "id",
|
|
330
|
+
id: v
|
|
331
|
+
} : v);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
var buildGranularTool = (pipeline, spec, resolveMedia, gate, overrides) => {
|
|
335
|
+
const defaultName = spec.id.replace(/\./g, "_");
|
|
336
|
+
const name = overrides?.[defaultName]?.name ?? defaultName;
|
|
337
|
+
return new require_tool.Tool({
|
|
338
|
+
name,
|
|
339
|
+
description: overrides?.[defaultName]?.description ?? `${spec.description} Processes a media file from this conversation locally. Indices are 1-based.`,
|
|
340
|
+
inputSchema: granularSchemaFor(spec),
|
|
341
|
+
handler: async (args, ctx) => {
|
|
342
|
+
const dispatch = ctx;
|
|
343
|
+
const { media_id: mediaId } = args;
|
|
344
|
+
await runGate(gate, dispatch, name, args);
|
|
345
|
+
const acquired = await acquire(dispatch, resolveMedia, mediaId);
|
|
346
|
+
if (!acquired.ok) return acquired.failure;
|
|
347
|
+
const opArgs = opArgsFrom(args);
|
|
348
|
+
normalizeRefArgs(spec, opArgs);
|
|
349
|
+
try {
|
|
350
|
+
return await renderResult(dispatch, await pipeline.ops(acquired.payload, [{
|
|
351
|
+
verb: spec.id,
|
|
352
|
+
args: opArgs
|
|
353
|
+
}], {
|
|
354
|
+
signal: dispatch.abortSignal,
|
|
355
|
+
resolveRef: refResolverFor(dispatch, resolveMedia)
|
|
356
|
+
}), name);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
return renderError(err);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
/**
|
|
364
|
+
* Forge agent tools over a configured media pipeline.
|
|
365
|
+
*
|
|
366
|
+
* @remarks
|
|
367
|
+
* The minted set follows the pipeline's configured engines: in the composite surface the
|
|
368
|
+
* `media_query` grammar text only advertises available verbs; in the granular surface a tool
|
|
369
|
+
* is minted only for verbs whose engine (if any) is configured. Either way `list_media` is
|
|
370
|
+
* included — it is the model's entry point for discovering `media_id` values.
|
|
371
|
+
*
|
|
372
|
+
* The returned record is keyed by tool name so consumers can register selectively or pass
|
|
373
|
+
* `Object.values(tools)` to `TurnRunnerConfig.tools`.
|
|
374
|
+
*
|
|
375
|
+
* @param pipeline - A pipeline from `createMediaPipeline`.
|
|
376
|
+
* @param options - Surface, resolver, gate, and overrides.
|
|
377
|
+
* @returns The minted tools, keyed by name.
|
|
378
|
+
*/
|
|
379
|
+
var forgeMediaTools = (pipeline, options) => {
|
|
380
|
+
if (options?.surface !== "composite" && options?.surface !== "granular") throw new require_exceptions.E_INVALID_MEDIA_PIPELINE_CONFIG([`forgeMediaTools requires surface: 'composite' | 'granular'`]);
|
|
381
|
+
const resolveMedia = options.resolveMedia ?? defaultResolveMedia;
|
|
382
|
+
const tools = {};
|
|
383
|
+
const listTool = buildListMediaTool(options.overrides);
|
|
384
|
+
tools[listTool.name] = listTool;
|
|
385
|
+
if (options.surface === "composite") {
|
|
386
|
+
const composite = buildCompositeTool(pipeline, resolveMedia, options.gate, options.overrides);
|
|
387
|
+
tools[composite.name] = composite;
|
|
388
|
+
return tools;
|
|
389
|
+
}
|
|
390
|
+
const available = new Set(require_validate.availableVerbs(pipeline.capabilities));
|
|
391
|
+
for (const spec of require_validate.VERB_INDEX.values()) {
|
|
392
|
+
if (!available.has(spec.id.replace(/[._]+/g, " "))) continue;
|
|
393
|
+
const tool = buildGranularTool(pipeline, spec, resolveMedia, options.gate, options.overrides);
|
|
394
|
+
tools[tool.name] = tool;
|
|
395
|
+
}
|
|
396
|
+
return tools;
|
|
397
|
+
};
|
|
398
|
+
//#endregion
|
|
399
|
+
exports.defaultResolveMedia = defaultResolveMedia;
|
|
400
|
+
exports.foldVerb = require_validate.foldVerb;
|
|
401
|
+
exports.forgeMediaTools = forgeMediaTools;
|
|
402
|
+
|
|
403
|
+
//# sourceMappingURL=forge.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"forge.cjs","names":[],"sources":["../../../src/batteries/media/forge.ts"],"sourcesContent":["/**\n * Agent glue: forge ADK tools from a media pipeline, so a model can drive local media work.\n *\n * @module @nhtio/adk/batteries/media/forge\n *\n * @remarks\n * The pipeline core (`@nhtio/adk/batteries/media`) is agent-agnostic; this module is the layer\n * that knows the ADK loop. {@link forgeMediaTools} mints `Tool` instances over a configured\n * {@link @nhtio/adk/batteries/media!MediaPipeline} in one of two surfaces (the consumer picks\n * per deployment):\n *\n * - `'composite'` — one `media_query` tool taking `{ media_id, q }` (a pipe expression — the\n * headline LLM DSL) or `{ media_id, ops }` (structured form), plus `list_media`. The tool\n * description embeds the engine-narrowed verb grammar so the model never sees a verb the\n * deployment can't run. One round-trip for multi-step work.\n * - `'granular'` — one narrow tool per available verb (`doc_select`, `sheet_update_cells`, …),\n * each internally a one-verb plan. Friendlier to small models; bigger tool list.\n *\n * Media flows in by reference: the model passes `media_id` values it discovered via the\n * `list_media` tool (or inline id markers, where the LLM battery renders them); the resolver\n * scans `ctx.turnMessages[].attachments` and `ctx.turnToolCalls[].results` by default. Outputs\n * are persisted through `ctx.storeMediaBytes` and returned as `Media.toolGenerated(...)`, so\n * file results land on `ToolCall.results` as first-class media.\n *\n * Processing failures return readable strings (`Error (CODE): …`) the model can act on; the\n * pipe DSL's own syntax/semantic errors render the same way, so a model can repair its\n * statement and retry. An optional {@link ToolGateFn} runs before every execution — the seam\n * for human-approval/RBAC flows built on `ctx.waitFor` (the ADK gates primitive).\n */\n\nimport { toPipe } from './plan'\nimport { isError } from '@nhtio/adk/guards'\nimport { availableVerbs } from './validate'\nimport { validator } from '@nhtio/validation'\nimport { VERB_INDEX, foldVerb } from './verbs'\nimport { E_INVALID_MEDIA_PIPELINE_CONFIG } from './exceptions'\nimport { Tool, Media, SpooledJsonArtifact } from '@nhtio/adk/common'\nimport type { MediaPipeline } from './index'\nimport type { MediaOp, MediaArgValue } from './plan'\nimport type { VerbSpec, VerbArgSpec } from './verbs'\nimport type { DispatchContext } from '@nhtio/adk/types'\nimport type { StepPayload, PlanResult } from './runtime'\n\n/**\n * Resolves a `media_id` to a {@link @nhtio/adk!Media} visible in the current dispatch.\n * The default implementation scans turn messages' attachments and prior tool-call results.\n */\nexport type MediaResolverFn = (\n ctx: DispatchContext,\n mediaId: string\n) => Media | undefined | Promise<Media | undefined>\n\n/**\n * Optional per-call gate run before any pipeline execution. Throwing aborts the call and\n * surfaces through the standard tool-error path. The canonical implementation awaits\n * `ctx.waitFor({ reason: 'tool_approval', payload: call })` and throws on denial — WHO\n * approves and HOW is the consumer's contract; this is the seam.\n */\nexport type ToolGateFn = (\n ctx: DispatchContext,\n call: { tool: string; args: unknown }\n) => void | Promise<void>\n\n/** Options for {@link forgeMediaTools}. */\nexport interface ForgeMediaToolsOptions {\n /** Which tool surface to mint. */\n surface: 'composite' | 'granular'\n /** Media-id resolution. Default: scan turn attachments + tool-call Media results. */\n resolveMedia?: MediaResolverFn\n /** Optional pre-execution gate (see {@link ToolGateFn}). */\n gate?: ToolGateFn\n /** Per-tool name/description overrides, keyed by the minted tool's default name. */\n overrides?: Record<string, { name?: string; description?: string }>\n}\n\n/**\n * The default media resolver: scan `ctx.turnMessages[].attachments`, then\n * `ctx.turnToolCalls[].results`, for a Media with the given id.\n *\n * @param ctx - The dispatch context.\n * @param mediaId - The id to find.\n * @returns The Media, or `undefined` when nothing in the turn carries that id.\n */\nexport const defaultResolveMedia: MediaResolverFn = (ctx, mediaId) => {\n for (const message of ctx.turnMessages) {\n for (const attachment of message.attachments ?? []) {\n if (attachment.id === mediaId) return attachment\n }\n }\n for (const toolCall of ctx.turnToolCalls) {\n const results = toolCall.results\n if (Media.isMedia(results) && results.id === mediaId) return results\n if (Array.isArray(results)) {\n const hit = results.find((r) => Media.isMedia(r) && r.id === mediaId)\n if (hit) return hit as Media\n }\n }\n return undefined\n}\n\n/** Enumerate every Media visible in the turn (for `list_media` and MEDIA_NOT_FOUND hints). */\nconst enumerateMedia = (\n ctx: DispatchContext\n): Array<{\n id: string\n filename: string\n kind: string\n mimeType: string\n source?: string\n origin: string\n}> => {\n const out: Array<{\n id: string\n filename: string\n kind: string\n mimeType: string\n source?: string\n origin: string\n }> = []\n for (const message of ctx.turnMessages) {\n for (const attachment of message.attachments ?? []) {\n out.push({\n id: attachment.id,\n filename: attachment.filename,\n kind: attachment.kind,\n mimeType: attachment.mimeType,\n source: attachment.source,\n origin: 'attachment',\n })\n }\n }\n for (const toolCall of ctx.turnToolCalls) {\n const results = toolCall.results\n const mediaList = Media.isMedia(results)\n ? [results]\n : Array.isArray(results)\n ? results.filter((r): r is Media => Media.isMedia(r))\n : []\n for (const media of mediaList) {\n out.push({\n id: media.id,\n filename: media.filename,\n kind: media.kind,\n mimeType: media.mimeType,\n source: media.source,\n origin: 'tool-result',\n })\n }\n }\n return out\n}\n\nconst failure = (code: string, message: string): string => `Error (${code}): ${message}`\n\nconst mediaKindOf = (mimeType: string): 'image' | 'audio' | 'video' | 'document' => {\n const mime = mimeType.toLowerCase()\n if (mime.startsWith('image/')) return 'image'\n if (mime.startsWith('audio/')) return 'audio'\n if (mime.startsWith('video/')) return 'video'\n return 'document'\n}\n\n/** Load a resolved Media's bytes into a pipeline payload. */\nconst toPayload = async (media: Media): Promise<StepPayload> => ({\n bytes: await media.asBytes(),\n mimeType: media.mimeType,\n filename: media.filename,\n})\n\n/** Persist a payload through the dispatch byte conduit and mint a first-party Media. */\nconst toMedia = async (\n ctx: DispatchContext,\n payload: StepPayload,\n toolName: string\n): Promise<Media> => {\n const id = crypto.randomUUID()\n const reader = await ctx.storeMediaBytes(id, payload.bytes)\n return Media.toolGenerated({\n id,\n kind: mediaKindOf(payload.mimeType),\n mimeType: payload.mimeType,\n filename: payload.filename,\n reader,\n source: `tool:${toolName}`,\n })\n}\n\n/** Render a plan result as a tool handler return value. */\nconst renderResult = async (\n ctx: DispatchContext,\n result: PlanResult,\n toolName: string\n): Promise<string | Media | Media[]> => {\n if (result.kind === 'media') return toMedia(ctx, result.payload, toolName)\n if (result.kind === 'media-list') {\n return Promise.all(result.payloads.map((p) => toMedia(ctx, p, toolName)))\n }\n return typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2)\n}\n\n/** Resolve + acquire the input media or produce a model-actionable failure string. */\nconst acquire = async (\n ctx: DispatchContext,\n resolveMedia: MediaResolverFn,\n mediaId: string\n): Promise<{ ok: true; payload: StepPayload } | { ok: false; failure: string }> => {\n const media = await resolveMedia(ctx, mediaId)\n if (!media) {\n const visible = enumerateMedia(ctx)\n const hint =\n visible.length > 0\n ? `Visible media ids: ${visible.map((m) => `${m.id} (${m.filename})`).join(', ')}`\n : 'No media is visible in this turn — ask the user to attach a file, or call list_media to check.'\n return {\n ok: false,\n failure: failure('MEDIA_NOT_FOUND', `no media with id \"${mediaId}\". ${hint}`),\n }\n }\n return { ok: true, payload: await toPayload(media) }\n}\n\n/** Build a per-run @id ref resolver bound to this dispatch's visible media. */\nconst refResolverFor =\n (ctx: DispatchContext, resolveMedia: MediaResolverFn) =>\n async (id: string): Promise<StepPayload> => {\n const media = await resolveMedia(ctx, id)\n if (!media) {\n const visible = enumerateMedia(ctx)\n .map((m) => m.id)\n .join(', ')\n throw new Error(\n `referenced media @${id} was not found in this turn. Visible ids: ${visible || '(none)'}`\n )\n }\n return toPayload(media)\n }\n\n/** Run the optional gate; a throw becomes the tool's error (standard downstream wrapping). */\nconst runGate = async (\n gate: ToolGateFn | undefined,\n ctx: DispatchContext,\n tool: string,\n args: unknown\n): Promise<void> => {\n if (gate) await gate(ctx, { tool, args })\n}\n\n/** Map a thrown pipeline error to a readable failure string the model can act on. */\nconst renderError = (err: unknown): string => {\n if (isError(err) && err.name.startsWith('E_MEDIA_')) {\n return failure(err.name.replace(/^E_MEDIA_/, ''), err.message)\n }\n throw err\n}\n\n// ── grammar text generation (composite surface) ──────────────────────────────\n\nconst argHelp = (name: string, arg: VerbArgSpec): string => {\n const req = arg.required ? '' : '?'\n const type =\n arg.type === 'enum' ? (arg.values ?? []).join('|') : arg.type === 'json' ? \"'<JSON>'\" : arg.type\n return `${name}${req}=${type}`\n}\n\nconst verbHelp = (spec: VerbSpec): string => {\n const verb = spec.id.replace(/\\./g, ' ')\n const args = Object.entries(spec.args)\n .map(([n, a]) => argHelp(n, a))\n .join(' ')\n return args.length > 0 ? `${verb} ${args}` : verb\n}\n\n/**\n * Generate the model-facing grammar text for the configured deployment: only available verbs,\n * with arg signatures, plus few-shot examples rendered from real plans via `toPipe` so they\n * can never drift from the parser.\n */\nconst grammarText = (pipeline: MediaPipeline): string => {\n const available = new Set(availableVerbs(pipeline.capabilities))\n const lines: string[] = []\n for (const spec of VERB_INDEX.values()) {\n if (!available.has(spec.id.replace(/[._]+/g, ' '))) continue\n lines.push(`- ${verbHelp(spec)} — ${spec.description}`)\n }\n const examples: string[] = [\n toPipe({\n steps: [\n { verb: 'select', args: { pages: [2, 3, 4, 5] } },\n { verb: 'extract.text', args: {} },\n ],\n }),\n toPipe({\n steps: [\n { verb: 'extract.text', args: {} },\n { verb: 'chunk', args: { by: 'sentence', size: 512 } },\n ],\n }),\n ]\n if (available.has('redact')) {\n examples.push(\n toPipe({\n steps: [\n {\n verb: 'redact',\n args: { match: [{ source: '\\\\d{3}-\\\\d{2}-\\\\d{4}', flags: '' }], replace: '[SSN]' },\n },\n ],\n })\n )\n }\n return [\n 'Statements are pipe expressions: verb name=value ... | verb name=value ...',\n 'Named args only. Indices are 1-based. Quote values containing spaces or dashes.',\n 'Structured payloads are quoted JSON: updates=\\'[{\"address\":\"B2\",\"value\":3}]\\'.',\n 'Reference other media inline by id: merge with=@<media id> (ids come from list_media).',\n '',\n 'Available verbs:',\n ...lines,\n '',\n 'Examples (remember: inside JSON tool args, backslashes must be doubled — \\\\\\\\d):',\n ...examples.map((e) => ` ${e}`),\n ].join('\\n')\n}\n\n// ── the tools ────────────────────────────────────────────────────────────────\n\nconst buildListMediaTool = (overrides: ForgeMediaToolsOptions['overrides']): Tool => {\n const name = overrides?.list_media?.name ?? 'list_media'\n return new Tool({\n name,\n description:\n overrides?.list_media?.description ??\n 'Lists every media file visible in this conversation turn (user attachments and files produced by prior tool calls), with the media ids other media tools require.',\n inputSchema: validator.object({}),\n artifactConstructor: () => SpooledJsonArtifact,\n handler: async (_args, ctx) => {\n const media = enumerateMedia(ctx as DispatchContext)\n return JSON.stringify(media, null, 2)\n },\n })\n}\n\nconst buildCompositeTool = (\n pipeline: MediaPipeline,\n resolveMedia: MediaResolverFn,\n gate: ToolGateFn | undefined,\n overrides: ForgeMediaToolsOptions['overrides']\n): Tool => {\n const name = overrides?.media_query?.name ?? 'media_query'\n const description =\n overrides?.media_query?.description ??\n `Runs a media-processing statement against a file from this conversation, locally (no external services). Provide media_id (from list_media) and either q (a pipe statement) or ops (structured steps).\n\n${grammarText(pipeline)}`\n return new Tool({\n name,\n description,\n inputSchema: validator.object({\n media_id: validator\n .string()\n .required()\n .description('The id of the media to process (call list_media to discover ids).'),\n q: validator\n .string()\n .description('A pipe statement, e.g. \"select pages=2-5 | extract text\".'),\n ops: validator\n .array()\n .items(\n validator.object({\n verb: validator.string().required(),\n args: validator.object().unknown(true).required(),\n })\n )\n .description('Structured alternative to q: [{ verb, args }] steps.'),\n }),\n handler: async (args, ctx) => {\n const {\n media_id: mediaId,\n q,\n ops,\n } = args as { media_id: string; q?: string; ops?: MediaOp[] }\n if ((q === undefined) === (ops === undefined)) {\n return failure(\n 'BAD_REQUEST',\n 'provide exactly one of q (pipe statement) or ops (structured steps)'\n )\n }\n const dispatch = ctx as DispatchContext\n await runGate(gate, dispatch, name, args)\n const acquired = await acquire(dispatch, resolveMedia, mediaId)\n if (!acquired.ok) return acquired.failure\n try {\n const runOptions = {\n signal: dispatch.abortSignal,\n resolveRef: refResolverFor(dispatch, resolveMedia),\n }\n const result =\n q !== undefined\n ? await pipeline.query(acquired.payload, q, runOptions)\n : await pipeline.ops(acquired.payload, ops as MediaOp[], runOptions)\n return await renderResult(dispatch, result, name)\n } catch (err) {\n return renderError(err)\n }\n },\n })\n}\n\n/** Convert a granular tool's flat args into the verb's op args (drops media_id). */\nconst opArgsFrom = (args: Record<string, unknown>): Record<string, MediaArgValue> => {\n const out: Record<string, MediaArgValue> = {}\n for (const [k, v] of Object.entries(args)) {\n if (k === 'media_id' || v === undefined) continue\n out[k] = v as MediaArgValue\n }\n return out\n}\n\nconst granularSchemaFor = (spec: VerbSpec): ReturnType<typeof validator.object> => {\n const shape: Record<string, ReturnType<typeof validator.any>> = {\n media_id: validator\n .string()\n .required()\n .description('The id of the media to process (call list_media to discover ids).'),\n }\n for (const [argName, arg] of Object.entries(spec.args)) {\n let schema\n switch (arg.type) {\n case 'number':\n schema = validator.number()\n if (arg.min !== undefined) schema = schema.min(arg.min)\n if (arg.max !== undefined) schema = schema.max(arg.max)\n break\n case 'boolean':\n schema = validator.boolean()\n break\n case 'enum':\n schema = validator.string().valid(...(arg.values ?? []))\n break\n case 'number-list':\n schema = validator.array().items(validator.number().min(arg.min ?? 1))\n break\n case 'string-list':\n schema = arg.values\n ? validator.array().items(validator.string().valid(...arg.values))\n : validator.array().items(validator.string())\n break\n case 'media-ref':\n schema = validator\n .string()\n .description('A media id (from list_media) for the other media in this operation.')\n break\n case 'media-ref-list':\n schema = validator.array().items(validator.string())\n break\n case 'name-or-index':\n schema = validator\n .alternatives()\n .try(validator.number().integer().min(1), validator.string())\n break\n case 'regex-or-string-list':\n schema = validator\n .alternatives()\n .try(validator.string(), validator.array().items(validator.string()))\n break\n case 'json':\n schema = validator.any().optional()\n break\n default:\n schema = validator.string()\n }\n schema = schema.description(arg.description)\n if (arg.required) schema = schema.required()\n shape[argName] = schema\n }\n return validator.object(shape)\n}\n\n/** Normalize a granular tool's media-ref string args to MediaRef IR values. */\nconst normalizeRefArgs = (spec: VerbSpec, args: Record<string, MediaArgValue>): void => {\n for (const [argName, arg] of Object.entries(spec.args)) {\n const value = args[argName]\n if (value === undefined) continue\n if (arg.type === 'media-ref' && typeof value === 'string') {\n args[argName] = { kind: 'id', id: value }\n }\n if (arg.type === 'media-ref-list' && Array.isArray(value)) {\n args[argName] = (value as unknown[]).map((v) =>\n typeof v === 'string' ? { kind: 'id', id: v } : (v as MediaArgValue)\n ) as MediaArgValue\n }\n }\n}\n\nconst buildGranularTool = (\n pipeline: MediaPipeline,\n spec: VerbSpec,\n resolveMedia: MediaResolverFn,\n gate: ToolGateFn | undefined,\n overrides: ForgeMediaToolsOptions['overrides']\n): Tool => {\n const defaultName = spec.id.replace(/\\./g, '_')\n const name = overrides?.[defaultName]?.name ?? defaultName\n return new Tool({\n name,\n description:\n overrides?.[defaultName]?.description ??\n `${spec.description} Processes a media file from this conversation locally. Indices are 1-based.`,\n inputSchema: granularSchemaFor(spec),\n handler: async (args, ctx) => {\n const dispatch = ctx as DispatchContext\n const { media_id: mediaId } = args as { media_id: string }\n await runGate(gate, dispatch, name, args)\n const acquired = await acquire(dispatch, resolveMedia, mediaId)\n if (!acquired.ok) return acquired.failure\n const opArgs = opArgsFrom(args as Record<string, unknown>)\n normalizeRefArgs(spec, opArgs)\n try {\n const result = await pipeline.ops(acquired.payload, [{ verb: spec.id, args: opArgs }], {\n signal: dispatch.abortSignal,\n resolveRef: refResolverFor(dispatch, resolveMedia),\n })\n return await renderResult(dispatch, result, name)\n } catch (err) {\n return renderError(err)\n }\n },\n })\n}\n\n/**\n * Forge agent tools over a configured media pipeline.\n *\n * @remarks\n * The minted set follows the pipeline's configured engines: in the composite surface the\n * `media_query` grammar text only advertises available verbs; in the granular surface a tool\n * is minted only for verbs whose engine (if any) is configured. Either way `list_media` is\n * included — it is the model's entry point for discovering `media_id` values.\n *\n * The returned record is keyed by tool name so consumers can register selectively or pass\n * `Object.values(tools)` to `TurnRunnerConfig.tools`.\n *\n * @param pipeline - A pipeline from `createMediaPipeline`.\n * @param options - Surface, resolver, gate, and overrides.\n * @returns The minted tools, keyed by name.\n */\nexport const forgeMediaTools = (\n pipeline: MediaPipeline,\n options: ForgeMediaToolsOptions\n): Record<string, Tool> => {\n if (options?.surface !== 'composite' && options?.surface !== 'granular') {\n throw new E_INVALID_MEDIA_PIPELINE_CONFIG([\n `forgeMediaTools requires surface: 'composite' | 'granular'`,\n ])\n }\n const resolveMedia = options.resolveMedia ?? defaultResolveMedia\n const tools: Record<string, Tool> = {}\n const listTool = buildListMediaTool(options.overrides)\n tools[listTool.name] = listTool\n\n if (options.surface === 'composite') {\n const composite = buildCompositeTool(pipeline, resolveMedia, options.gate, options.overrides)\n tools[composite.name] = composite\n return tools\n }\n\n const available = new Set(availableVerbs(pipeline.capabilities))\n for (const spec of VERB_INDEX.values()) {\n if (!available.has(spec.id.replace(/[._]+/g, ' '))) continue\n const tool = buildGranularTool(pipeline, spec, resolveMedia, options.gate, options.overrides)\n tools[tool.name] = tool\n }\n return tools\n}\n\n/** Re-exported so consumers can fold verbs the same way the forge does. */\nexport { foldVerb }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent glue: forge ADK tools from a media pipeline, so a model can drive local media work.
|
|
3
|
+
*
|
|
4
|
+
* @module @nhtio/adk/batteries/media/forge
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* The pipeline core (`@nhtio/adk/batteries/media`) is agent-agnostic; this module is the layer
|
|
8
|
+
* that knows the ADK loop. {@link forgeMediaTools} mints `Tool` instances over a configured
|
|
9
|
+
* {@link @nhtio/adk/batteries/media!MediaPipeline} in one of two surfaces (the consumer picks
|
|
10
|
+
* per deployment):
|
|
11
|
+
*
|
|
12
|
+
* - `'composite'` — one `media_query` tool taking `{ media_id, q }` (a pipe expression — the
|
|
13
|
+
* headline LLM DSL) or `{ media_id, ops }` (structured form), plus `list_media`. The tool
|
|
14
|
+
* description embeds the engine-narrowed verb grammar so the model never sees a verb the
|
|
15
|
+
* deployment can't run. One round-trip for multi-step work.
|
|
16
|
+
* - `'granular'` — one narrow tool per available verb (`doc_select`, `sheet_update_cells`, …),
|
|
17
|
+
* each internally a one-verb plan. Friendlier to small models; bigger tool list.
|
|
18
|
+
*
|
|
19
|
+
* Media flows in by reference: the model passes `media_id` values it discovered via the
|
|
20
|
+
* `list_media` tool (or inline id markers, where the LLM battery renders them); the resolver
|
|
21
|
+
* scans `ctx.turnMessages[].attachments` and `ctx.turnToolCalls[].results` by default. Outputs
|
|
22
|
+
* are persisted through `ctx.storeMediaBytes` and returned as `Media.toolGenerated(...)`, so
|
|
23
|
+
* file results land on `ToolCall.results` as first-class media.
|
|
24
|
+
*
|
|
25
|
+
* Processing failures return readable strings (`Error (CODE): …`) the model can act on; the
|
|
26
|
+
* pipe DSL's own syntax/semantic errors render the same way, so a model can repair its
|
|
27
|
+
* statement and retry. An optional {@link ToolGateFn} runs before every execution — the seam
|
|
28
|
+
* for human-approval/RBAC flows built on `ctx.waitFor` (the ADK gates primitive).
|
|
29
|
+
*/
|
|
30
|
+
import { foldVerb } from "./verbs";
|
|
31
|
+
import { Tool, Media } from "../../common";
|
|
32
|
+
import type { MediaPipeline } from "./index";
|
|
33
|
+
import type { DispatchContext } from "../../types";
|
|
34
|
+
/**
|
|
35
|
+
* Resolves a `media_id` to a {@link @nhtio/adk!Media} visible in the current dispatch.
|
|
36
|
+
* The default implementation scans turn messages' attachments and prior tool-call results.
|
|
37
|
+
*/
|
|
38
|
+
export type MediaResolverFn = (ctx: DispatchContext, mediaId: string) => Media | undefined | Promise<Media | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Optional per-call gate run before any pipeline execution. Throwing aborts the call and
|
|
41
|
+
* surfaces through the standard tool-error path. The canonical implementation awaits
|
|
42
|
+
* `ctx.waitFor({ reason: 'tool_approval', payload: call })` and throws on denial — WHO
|
|
43
|
+
* approves and HOW is the consumer's contract; this is the seam.
|
|
44
|
+
*/
|
|
45
|
+
export type ToolGateFn = (ctx: DispatchContext, call: {
|
|
46
|
+
tool: string;
|
|
47
|
+
args: unknown;
|
|
48
|
+
}) => void | Promise<void>;
|
|
49
|
+
/** Options for {@link forgeMediaTools}. */
|
|
50
|
+
export interface ForgeMediaToolsOptions {
|
|
51
|
+
/** Which tool surface to mint. */
|
|
52
|
+
surface: 'composite' | 'granular';
|
|
53
|
+
/** Media-id resolution. Default: scan turn attachments + tool-call Media results. */
|
|
54
|
+
resolveMedia?: MediaResolverFn;
|
|
55
|
+
/** Optional pre-execution gate (see {@link ToolGateFn}). */
|
|
56
|
+
gate?: ToolGateFn;
|
|
57
|
+
/** Per-tool name/description overrides, keyed by the minted tool's default name. */
|
|
58
|
+
overrides?: Record<string, {
|
|
59
|
+
name?: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The default media resolver: scan `ctx.turnMessages[].attachments`, then
|
|
65
|
+
* `ctx.turnToolCalls[].results`, for a Media with the given id.
|
|
66
|
+
*
|
|
67
|
+
* @param ctx - The dispatch context.
|
|
68
|
+
* @param mediaId - The id to find.
|
|
69
|
+
* @returns The Media, or `undefined` when nothing in the turn carries that id.
|
|
70
|
+
*/
|
|
71
|
+
export declare const defaultResolveMedia: MediaResolverFn;
|
|
72
|
+
/**
|
|
73
|
+
* Forge agent tools over a configured media pipeline.
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* The minted set follows the pipeline's configured engines: in the composite surface the
|
|
77
|
+
* `media_query` grammar text only advertises available verbs; in the granular surface a tool
|
|
78
|
+
* is minted only for verbs whose engine (if any) is configured. Either way `list_media` is
|
|
79
|
+
* included — it is the model's entry point for discovering `media_id` values.
|
|
80
|
+
*
|
|
81
|
+
* The returned record is keyed by tool name so consumers can register selectively or pass
|
|
82
|
+
* `Object.values(tools)` to `TurnRunnerConfig.tools`.
|
|
83
|
+
*
|
|
84
|
+
* @param pipeline - A pipeline from `createMediaPipeline`.
|
|
85
|
+
* @param options - Surface, resolver, gate, and overrides.
|
|
86
|
+
* @returns The minted tools, keyed by name.
|
|
87
|
+
*/
|
|
88
|
+
export declare const forgeMediaTools: (pipeline: MediaPipeline, options: ForgeMediaToolsOptions) => Record<string, Tool>;
|
|
89
|
+
/** Re-exported so consumers can fold verbs the same way the forge does. */
|
|
90
|
+
export { foldVerb };
|