@formio/uag 2.0.0-rc.5 → 2.0.0-rc.7

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.
Files changed (75) hide show
  1. package/bin/uag.js +6 -2
  2. package/lib/UAGFormInterface.d.ts +12 -0
  3. package/lib/UAGFormInterface.js +30 -3
  4. package/lib/UAGProjectInterface.d.ts +9 -1
  5. package/lib/UAGProjectInterface.js +14 -3
  6. package/lib/agent/anonymous.js +1 -1
  7. package/lib/agent/engines.d.ts +10 -0
  8. package/lib/agent/engines.js +15 -1
  9. package/lib/agent/errors.d.ts +9 -0
  10. package/lib/agent/errors.js +20 -0
  11. package/lib/agent/index.d.ts +1 -1
  12. package/lib/agent/index.js +2 -1
  13. package/lib/agent/remote.d.ts +5 -1
  14. package/lib/agent/remote.js +25 -2
  15. package/lib/agent/router.d.ts +32 -2
  16. package/lib/agent/router.js +17 -11
  17. package/lib/agent/stubs.d.ts +13 -2
  18. package/lib/agent/stubs.js +18 -5
  19. package/lib/agent/tools.d.ts +24 -3
  20. package/lib/agent/tools.js +47 -4
  21. package/lib/config.d.ts +31 -2
  22. package/lib/documents/convert.d.ts +38 -0
  23. package/lib/documents/convert.js +151 -0
  24. package/lib/documents/converters.d.ts +26 -0
  25. package/lib/documents/converters.js +139 -0
  26. package/lib/documents/detect.d.ts +31 -0
  27. package/lib/documents/detect.js +169 -0
  28. package/lib/documents/index.d.ts +9 -0
  29. package/lib/documents/index.js +43 -0
  30. package/lib/documents/mime.d.ts +39 -0
  31. package/lib/documents/mime.js +100 -0
  32. package/lib/documents/plugins.d.ts +18 -0
  33. package/lib/documents/plugins.js +65 -0
  34. package/lib/documents/readers.d.ts +19 -0
  35. package/lib/documents/readers.js +64 -0
  36. package/lib/documents/routes.d.ts +52 -0
  37. package/lib/documents/routes.js +99 -0
  38. package/lib/documents/types.d.ts +126 -0
  39. package/lib/documents/types.js +2 -0
  40. package/lib/documents/uagJs.d.ts +17 -0
  41. package/lib/documents/uagJs.js +157 -0
  42. package/lib/formflow.d.ts +4 -1
  43. package/lib/formflow.js +2 -2
  44. package/lib/index.d.ts +50 -6
  45. package/lib/index.js +150 -15
  46. package/lib/markdown.d.ts +1 -0
  47. package/lib/markdown.js +166 -0
  48. package/lib/modules/api.d.ts +22 -0
  49. package/lib/modules/api.js +100 -0
  50. package/lib/modules/index.d.ts +4 -0
  51. package/lib/modules/index.js +24 -0
  52. package/lib/modules/routes.d.ts +14 -0
  53. package/lib/modules/routes.js +21 -0
  54. package/lib/modules/types.d.ts +68 -0
  55. package/lib/modules/types.js +5 -0
  56. package/lib/pluginFiles.d.ts +16 -0
  57. package/lib/pluginFiles.js +62 -0
  58. package/lib/plugins.d.ts +17 -0
  59. package/lib/plugins.js +93 -0
  60. package/lib/richText.d.ts +20 -0
  61. package/lib/richText.js +99 -0
  62. package/lib/router.d.ts +9 -0
  63. package/lib/router.js +19 -0
  64. package/lib/testing.d.ts +74 -0
  65. package/lib/testing.js +165 -0
  66. package/lib/tools/index.d.ts +2 -1
  67. package/lib/tools/index.js +5 -2
  68. package/lib/tools/readDocument.d.ts +17 -0
  69. package/lib/tools/readDocument.js +99 -0
  70. package/lib/tools/submissionUpdate.js +3 -2
  71. package/lib/tools/utils.d.ts +40 -0
  72. package/lib/tools/utils.js +16 -1
  73. package/package.json +10 -5
  74. package/testing.d.ts +1 -0
  75. package/testing.js +2 -0
package/bin/uag.js CHANGED
@@ -37,9 +37,13 @@ function restartWithoutSnapshot() {
37
37
  /** Start the UAG with a module, listening on PORT (default 3200). */
38
38
  async function start(uagModule) {
39
39
  const Express = require('express');
40
- const { UAGServer, trustProxySetting } = require('../lib');
40
+ const { UAGServer, trustProxySetting, usePluginFiles } = require('../lib');
41
41
  const server = new UAGServer();
42
- if (uagModule) server.use(uagModule);
42
+ // A module may be exported as a promise (an async module).
43
+ const resolved = await uagModule;
44
+ if (resolved) server.use(resolved);
45
+ // Then the plugins file: UAG_PLUGINS, or the image's /app/plugins/index.mjs.
46
+ await usePluginFiles(server);
43
47
  const app = Express();
44
48
  // Behind a reverse proxy, its forwarded headers give the public URL (TRUST_PROXY).
45
49
  const trust = trustProxySetting(process.env.TRUST_PROXY);
@@ -191,5 +191,17 @@ export declare class UAGFormInterface extends FormInterface implements FormflowF
191
191
  path: string;
192
192
  label: string;
193
193
  }[];
194
+ /**
195
+ * A value an agent engine generated, before it is checked and written: HTML
196
+ * for a rich text field is reduced to the markup its editor keeps.
197
+ */
198
+ formflowNormalizeValue(component: Component, value: any): any;
199
+ /**
200
+ * A value an MCP client writes at a data path (collect_field_data,
201
+ * confirm_form_submission, submit_completed_form, submission_update), as
202
+ * an engine's is: HTML for a rich text field is reduced to the markup its
203
+ * editor keeps. Row indices are ignored to find the component.
204
+ */
205
+ normalizeWrittenValue(path: string, value: any): any;
194
206
  htmlToMarkdown(html: string): string;
195
207
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UAGFormInterface = void 0;
4
4
  const appserver_1 = require("@formio/appserver");
5
5
  const formflow_1 = require("./formflow");
6
+ const richText_1 = require("./richText");
6
7
  const core_1 = require("@formio/core");
7
8
  const lodash_1 = require("lodash");
8
9
  const node_html_markdown_1 = require("node-html-markdown");
@@ -235,8 +236,12 @@ class UAGFormInterface extends appserver_1.FormInterface {
235
236
  case 'signature':
236
237
  rule += 'Have the user draw their signature. The value will be a base64-encoded PNG image string of that signature.';
237
238
  break;
238
- case 'textfield':
239
239
  case 'textarea':
240
+ rule += (0, richText_1.isRichText)(component)
241
+ ? richText_1.RICH_TEXT_RULE
242
+ : 'The value can be any alphanumeric string, containing letters, numbers, white space characters, and common symbols.';
243
+ break;
244
+ case 'textfield':
240
245
  case 'hidden':
241
246
  rule += 'The value can be any alphanumeric string, containing letters, numbers, white space characters, and common symbols.';
242
247
  break;
@@ -596,7 +601,7 @@ class UAGFormInterface extends appserver_1.FormInterface {
596
601
  else if (value && this.isMultiple(comp) && typeof value === 'string') {
597
602
  value = value.split(',').map((v) => v.trim());
598
603
  }
599
- (0, lodash_1.set)(submission.data, path, value);
604
+ (0, lodash_1.set)(submission.data, path, this.normalizeWrittenValue(path, value));
600
605
  }
601
606
  return submission;
602
607
  }
@@ -621,7 +626,12 @@ class UAGFormInterface extends appserver_1.FormInterface {
621
626
  }
622
627
  else if (this.inputComponent(component) && (0, core_1.componentHasValue)(component, value)) {
623
628
  const modelType = core_1.Utils.getModelType(component);
624
- if ((0, lodash_1.isObjectLike)(value)) {
629
+ if ((0, formflow_1.isBinaryComponent)(component)) {
630
+ // Files and signatures are described, never inlined: a base64 upload would fill the context.
631
+ const described = (0, formflow_1.describeBinary)(component, value);
632
+ value = (0, lodash_1.isObjectLike)(described) ? JSON.stringify(described) : described;
633
+ }
634
+ else if ((0, lodash_1.isObjectLike)(value)) {
625
635
  value = JSON.stringify(value);
626
636
  }
627
637
  else if (modelType === 'string' && (0, lodash_1.isString)(value)) {
@@ -670,6 +680,23 @@ class UAGFormInterface extends appserver_1.FormInterface {
670
680
  formflowLabeledData(data = {}) {
671
681
  return this.formatData(data).map(({ path, label }) => ({ path, label }));
672
682
  }
683
+ /**
684
+ * A value an agent engine generated, before it is checked and written: HTML
685
+ * for a rich text field is reduced to the markup its editor keeps.
686
+ */
687
+ formflowNormalizeValue(component, value) {
688
+ return (0, richText_1.isRichText)(component) && typeof value === 'string' ? (0, richText_1.sanitizeRichText)(value) : value;
689
+ }
690
+ /**
691
+ * A value an MCP client writes at a data path (collect_field_data,
692
+ * confirm_form_submission, submit_completed_form, submission_update), as
693
+ * an engine's is: HTML for a rich text field is reduced to the markup its
694
+ * editor keeps. Row indices are ignored to find the component.
695
+ */
696
+ normalizeWrittenValue(path, value) {
697
+ const component = this.getComponent(String(path).replace(/\[\d+\]/g, ''));
698
+ return component ? this.formflowNormalizeValue(component, value) : value;
699
+ }
673
700
  htmlToMarkdown(html) {
674
701
  return node_html_markdown_1.NodeHtmlMarkdown.translate(html, {
675
702
  bulletMarker: '-',
@@ -1,11 +1,12 @@
1
1
  import { ProjectInterface, SubmissionRequest } from "@formio/appserver";
2
- import { EngineRegistry, FormflowConfig, FormflowForm, FormflowHost, FormflowTool, ScopedTokenClaims } from "./formflow";
2
+ import { EngineRegistry, FormflowConfig, FormflowFileReader, FormflowForm, FormflowHost, FormflowTool, ScopedTokenClaims } from "./formflow";
3
3
  import { UAGConfig } from "./config";
4
4
  import { Form } from "@formio/core";
5
5
  import { NextFunction, Response, Router } from "express";
6
6
  import { UAGFormInterface } from "./UAGFormInterface";
7
7
  import { ResponseTemplate, UAGTemplate } from "./template";
8
8
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import type { PluginRegistry } from "@formio/appserver";
9
10
  export declare class UAGProjectInterface extends ProjectInterface implements FormflowHost {
10
11
  user: any;
11
12
  /** The names of the UAG forms the project currently has. */
@@ -38,6 +39,13 @@ export declare class UAGProjectInterface extends ProjectInterface implements For
38
39
  formflowConfig(): FormflowConfig;
39
40
  /** A form tagged "uag", or null. */
40
41
  formflowForm(name: string): Promise<FormflowForm | null>;
42
+ /** The server's plugin registry, once its plugins are listed. */
43
+ plugins?: PluginRegistry;
44
+ /**
45
+ * Readers for uploaded documents: one built from the installed converters,
46
+ * which reads every type with a route to text.
47
+ */
48
+ formflowFileReaders(): FormflowFileReader[];
41
49
  /** Every MCP tool, as Agentic Formflow tools. */
42
50
  formflowTools(): Promise<FormflowTool[]>;
43
51
  authorizeRequest(req: SubmissionRequest, res: Response, next: NextFunction): Promise<void | Response<any, Record<string, any>>>;
@@ -15,6 +15,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
15
15
  const version_1 = require("./version");
16
16
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
17
17
  const router_2 = require("./agent/router");
18
+ const readers_1 = require("./documents/readers");
18
19
  const tools_1 = require("./agent/tools");
19
20
  const debug = require('debug')('formio:uag:UAGProjectInterface');
20
21
  class UAGProjectInterface extends appserver_1.ProjectInterface {
@@ -52,12 +53,15 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
52
53
  server.registerTool(tool.name, {
53
54
  title: tool.title,
54
55
  description: tool.description,
55
- inputSchema: tool.inputSchema,
56
+ // Always a schema, even an empty one: the MCP SDK calls a tool
57
+ // registered without one as `execute(extra)`, and every tool is
58
+ // `execute(args, extra)`, as Agent API research calls it.
59
+ inputSchema: tool.inputSchema || {},
56
60
  // Carries readOnly/destructive/idempotent/openWorld through to the
57
61
  // client. Tools from config.tools may omit them, so this stays
58
62
  // conditional rather than sending `annotations: undefined`.
59
63
  ...(tool.annotations && { annotations: tool.annotations })
60
- }, tool.execute);
64
+ }, (args, extra) => (0, tools_1.runTool)(tool, this, args, extra));
61
65
  }
62
66
  // A scoped token that allows no tools must still answer tools/list, with
63
67
  // an empty list: clients list tools first,
@@ -121,9 +125,16 @@ class UAGProjectInterface extends appserver_1.ProjectInterface {
121
125
  const form = await this.getForm(name);
122
126
  return form?.uag ? form : null;
123
127
  }
128
+ /**
129
+ * Readers for uploaded documents: one built from the installed converters,
130
+ * which reads every type with a route to text.
131
+ */
132
+ formflowFileReaders() {
133
+ return this.plugins ? [(0, readers_1.converterFileReader)(this.plugins)] : [];
134
+ }
124
135
  /** Every MCP tool, as Agentic Formflow tools. */
125
136
  async formflowTools() {
126
- return (await (0, tools_1.allTools)(this)).map(tools_1.formflowTool);
137
+ return (await (0, tools_1.allTools)(this)).map((tool) => (0, tools_1.formflowTool)(tool, this));
127
138
  }
128
139
  async authorizeRequest(req, res, next) {
129
140
  try {
@@ -76,7 +76,7 @@ function anonymousGate(project) {
76
76
  const form = request.form;
77
77
  const limits = anonymousLimits(project);
78
78
  if (limits.perAddress === 0 || limits.total === 0 || !(0, formflow_1.flagOn)(form?.form?.properties?.uagAnonymous)) {
79
- return res.status(401).json({ error: 'Sign in to use this form\'s agent operations.', code: 'sign_in_required' });
79
+ return res.status(401).json({ error: 'Sign in to use this form\'s agent operations and routes.', code: 'sign_in_required' });
80
80
  }
81
81
  const taken = limiterFor(project).take(String(req.ip || req.socket?.remoteAddress || ''), limits);
82
82
  if (!taken.allowed) {
@@ -4,6 +4,16 @@ import { EngineRegistry } from '../formflow';
4
4
  export declare const UAG_ROOT: string;
5
5
  /** Engine types every UAG has, whatever is installed. */
6
6
  export declare const BUILT_IN_ENGINES: Record<string, EngineTypeDefinition>;
7
+ /** What is known of each loaded engine type, by type name: for listing it as a plugin. */
8
+ export type LoadedEngineType = {
9
+ name: string;
10
+ title: string;
11
+ version?: string;
12
+ kinds: ('decision' | 'agent')[];
13
+ instances: string[];
14
+ };
15
+ /** The engine types a registry's instances were created from, in order of first use. */
16
+ export declare function engineTypesOf(registry: EngineRegistry | undefined): LoadedEngineType[];
7
17
  /**
8
18
  * Enable the engines the configuration names (`engines`), or else
9
19
  * `UAG_ENGINES`. Each type is resolved from the configuration's `engineTypes`,
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.BUILT_IN_ENGINES = exports.UAG_ROOT = void 0;
7
+ exports.engineTypesOf = engineTypesOf;
7
8
  exports.loadUAGEngines = loadUAGEngines;
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const uag_sdk_1 = require("@formio/uag-sdk");
@@ -19,6 +20,11 @@ exports.BUILT_IN_ENGINES = {
19
20
  'stub-decision': stubs_1.StubDecisionEngine,
20
21
  'stub-agent': stubs_1.StubAgentEngine,
21
22
  };
23
+ const loadedTypes = new WeakMap();
24
+ /** The engine types a registry's instances were created from, in order of first use. */
25
+ function engineTypesOf(registry) {
26
+ return (registry && loadedTypes.get(registry)) || [];
27
+ }
22
28
  /**
23
29
  * Enable the engines the configuration names (`engines`), or else
24
30
  * `UAG_ENGINES`. Each type is resolved from the configuration's `engineTypes`,
@@ -33,5 +39,13 @@ async function loadUAGEngines(config = {}, env = process.env) {
33
39
  config,
34
40
  apiVersion: appserver_1.Formflow.ENGINE_API_VERSION,
35
41
  });
36
- return appserver_1.Formflow.engineRegistry(loaded);
42
+ const registry = appserver_1.Formflow.engineRegistry(loaded);
43
+ const types = new Map();
44
+ for (const entry of loaded) {
45
+ const type = types.get(entry.type) || { name: entry.type, title: entry.title || entry.type, version: entry.typeVersion || entry.version, kinds: entry.kinds || [entry.engine.kind], instances: [] };
46
+ type.instances.push(entry.engine.name);
47
+ types.set(entry.type, type);
48
+ }
49
+ loadedTypes.set(registry, [...types.values()]);
50
+ return registry;
37
51
  }
@@ -3,6 +3,15 @@ export declare function isAgentApiRequest(req: Request): boolean;
3
3
  /** The code for a status, when the error names none. */
4
4
  export declare function errorCode(status: number): string;
5
5
  export declare const INTERNAL_ERROR = "The request failed. See the UAG logs for this request id.";
6
+ /**
7
+ * An error as the Agent API answers it: its status, and the body. Errors with
8
+ * a status (FormflowError) keep their message and code; anything else is a 500
9
+ * with no detail. Agent operations and module routes share it.
10
+ */
11
+ export declare function agentError(requestId: string, err: any): {
12
+ status: number;
13
+ body: Record<string, any>;
14
+ };
6
15
  /** The request's id, assigned once: the caller's, when it is a short token, or a new one. */
7
16
  export declare function requestIdOf(req: Request, res: Response): string;
8
17
  /** The error body for a status and whatever a handler sent: an object, JSON text, plain text or nothing. */
@@ -3,11 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.INTERNAL_ERROR = void 0;
4
4
  exports.isAgentApiRequest = isAgentApiRequest;
5
5
  exports.errorCode = errorCode;
6
+ exports.agentError = agentError;
6
7
  exports.requestIdOf = requestIdOf;
7
8
  exports.errorBody = errorBody;
8
9
  exports.agentErrorShape = agentErrorShape;
9
10
  const crypto_1 = require("crypto");
10
11
  const protocol_1 = require("@formio/uag-sdk/protocol");
12
+ const formflow_1 = require("../formflow");
11
13
  /**
12
14
  * The Agent API's one error shape. Every error a form's agent operation or
13
15
  * `/agent/capabilities` answers is JSON
@@ -42,6 +44,24 @@ function errorCode(status) {
42
44
  }
43
45
  }
44
46
  exports.INTERNAL_ERROR = 'The request failed. See the UAG logs for this request id.';
47
+ /**
48
+ * An error as the Agent API answers it: its status, and the body. Errors with
49
+ * a status (FormflowError) keep their message and code; anything else is a 500
50
+ * with no detail. Agent operations and module routes share it.
51
+ */
52
+ function agentError(requestId, err) {
53
+ const known = err instanceof formflow_1.FormflowError;
54
+ const status = known ? err.status : 500;
55
+ return {
56
+ status,
57
+ body: {
58
+ requestId,
59
+ error: status === 500 ? exports.INTERNAL_ERROR : String(err?.message || exports.INTERNAL_ERROR),
60
+ code: (known && err.code) || errorCode(status),
61
+ ...(err?.edition && { edition: err.edition })
62
+ }
63
+ };
64
+ }
45
65
  /** The request's id, assigned once: the caller's, when it is a short token, or a new one. */
46
66
  function requestIdOf(req, res) {
47
67
  const request = req;
@@ -1,4 +1,4 @@
1
- export { StubAgentEngine, StubDecisionEngine } from './stubs';
1
+ export { StubAgentEngine, StubDecisionEngine, stubGenerateRequests } from './stubs';
2
2
  export { BUILT_IN_ENGINES, UAG_ROOT, loadUAGEngines } from './engines';
3
3
  export { AgentRouter, FormAgentRouter, agentCredentials, agentNotFound } from './router';
4
4
  export { RemoteEngine, MIN_KEY_LENGTH } from './remote';
@@ -1,9 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MIN_KEY_LENGTH = exports.RemoteEngine = exports.agentNotFound = exports.agentCredentials = exports.FormAgentRouter = exports.AgentRouter = exports.loadUAGEngines = exports.UAG_ROOT = exports.BUILT_IN_ENGINES = exports.StubDecisionEngine = exports.StubAgentEngine = void 0;
3
+ exports.MIN_KEY_LENGTH = exports.RemoteEngine = exports.agentNotFound = exports.agentCredentials = exports.FormAgentRouter = exports.AgentRouter = exports.loadUAGEngines = exports.UAG_ROOT = exports.BUILT_IN_ENGINES = exports.stubGenerateRequests = exports.StubDecisionEngine = exports.StubAgentEngine = void 0;
4
4
  var stubs_1 = require("./stubs");
5
5
  Object.defineProperty(exports, "StubAgentEngine", { enumerable: true, get: function () { return stubs_1.StubAgentEngine; } });
6
6
  Object.defineProperty(exports, "StubDecisionEngine", { enumerable: true, get: function () { return stubs_1.StubDecisionEngine; } });
7
+ Object.defineProperty(exports, "stubGenerateRequests", { enumerable: true, get: function () { return stubs_1.stubGenerateRequests; } });
7
8
  var engines_1 = require("./engines");
8
9
  Object.defineProperty(exports, "BUILT_IN_ENGINES", { enumerable: true, get: function () { return engines_1.BUILT_IN_ENGINES; } });
9
10
  Object.defineProperty(exports, "UAG_ROOT", { enumerable: true, get: function () { return engines_1.UAG_ROOT; } });
@@ -25,6 +25,8 @@ type Connection = {
25
25
  baseUrl: string;
26
26
  key: string;
27
27
  fetch: Fetch;
28
+ /** The most PDF bytes one generate request may send: documents.maxTotalBytes. */
29
+ maxDocumentBytes: number;
28
30
  };
29
31
  /**
30
32
  * Describe the remote engine, retrying while the engine server is unreachable
@@ -39,7 +41,9 @@ export declare function describeRemote(connection: Connection, engine: string, {
39
41
  /** An engine object for a described remote engine. */
40
42
  export declare function remoteEngine(connection: Connection, remoteName: string, described: RemoteEngineDescription): DecisionEngine | AgentEngine;
41
43
  export declare const RemoteEngine: Readonly<{
42
- type: string;
44
+ type: "engine";
45
+ name: string;
46
+ title: string;
43
47
  kinds: ("agent" | "decision")[];
44
48
  apiVersion: 1;
45
49
  description: string;
@@ -158,12 +158,13 @@ function remoteEngine(connection, remoteName, described) {
158
158
  },
159
159
  };
160
160
  }
161
- return {
161
+ const agent = {
162
162
  name: connection.name,
163
163
  kind: 'agent',
164
164
  apiVersion: uag_sdk_1.ENGINE_API_VERSION,
165
165
  model: described.model,
166
166
  toolUse: described.toolUse === true,
167
+ accepts: described.accepts || ['text'],
167
168
  async extract(request) {
168
169
  const research = !!request.tools?.length;
169
170
  // The engine server researches through its own MCP connection to the
@@ -182,9 +183,30 @@ function remoteEngine(connection, remoteName, described) {
182
183
  return { ...response, model: response.model || described.model };
183
184
  },
184
185
  };
186
+ if (described.generate) {
187
+ agent.generate = async (request) => {
188
+ // Refused before anything is sent; the documents are never logged.
189
+ if ((0, protocol_1.pdfByteLength)(request.attachments) > connection.maxDocumentBytes) {
190
+ throw new uag_sdk_1.EngineError(`The attached PDFs exceed ${connection.maxDocumentBytes} bytes, the most one request to the engine server may carry.`);
191
+ }
192
+ const body = {
193
+ instructions: request.instructions,
194
+ ...(request.system !== undefined && { system: request.system }),
195
+ attachments: (0, protocol_1.encodeAttachments)(request.attachments),
196
+ format: request.format,
197
+ ...(request.maxTokens !== undefined && { maxTokens: request.maxTokens }),
198
+ };
199
+ const response = valid(protocol_1.checkGenerateResponse, await call(connection, `${path}/generate`, body, request.signal));
200
+ request.onProgress?.(response.text.length);
201
+ return { ...response, model: response.model || described.model };
202
+ };
203
+ }
204
+ return agent;
185
205
  }
186
206
  exports.RemoteEngine = (0, uag_sdk_1.defineEngine)({
187
- type: 'remote',
207
+ type: 'engine',
208
+ name: 'remote',
209
+ title: 'Remote engine (engine server)',
188
210
  kinds: ['decision', 'agent'],
189
211
  apiVersion: uag_sdk_1.ENGINE_API_VERSION,
190
212
  description: 'An engine hosted by an engine server, over the remote engine protocol.',
@@ -216,6 +238,7 @@ exports.RemoteEngine = (0, uag_sdk_1.defineEngine)({
216
238
  baseUrl: url.href.replace(/\/+$/, ''),
217
239
  key: settings.key,
218
240
  fetch: options.fetch || fetch,
241
+ maxDocumentBytes: Math.min(Number(config?.documents?.maxTotalBytes) || protocol_1.MAX_DOCUMENT_BYTES, protocol_1.MAX_DOCUMENT_BYTES),
219
242
  };
220
243
  const remoteName = settings.engine || name;
221
244
  const described = await describeRemote(connection, remoteName, {
@@ -1,7 +1,36 @@
1
- import { RequestHandler, Router } from "express";
1
+ import { NextFunction, Request, RequestHandler, Response, Router } from "express";
2
+ import { SubmissionRequest } from "@formio/appserver";
3
+ import { FormflowService } from "../formflow";
2
4
  import type { UAGProjectInterface } from "../UAGProjectInterface";
3
- /** The largest request body an agent operation takes, in bytes. */
5
+ /**
6
+ * The Agent API: HTTP adapters onto Agentic Formflow in @formio/appserver-core.
7
+ * Each endpoint reads its request, calls the service, and writes JSON (or, for
8
+ * extract, a Server-Sent Events stream). Errors answer with the status the
9
+ * service gives.
10
+ *
11
+ * The operations belong to a form, `{form}/agent/{operation}`, as its
12
+ * submissions do: the form comes from the URL and the caller from the
13
+ * project's own authentication. Capabilities describe the deployment, at
14
+ * `/agent/capabilities`.
15
+ */
16
+ type AgentRequest = SubmissionRequest & {
17
+ requestId: string;
18
+ };
19
+ /** The largest request body an agent operation takes by default, in bytes. */
4
20
  export declare const AGENT_BODY_LIMIT: number;
21
+ /**
22
+ * The largest request body the project's agent operations take: `agentApi.bodyLimit`,
23
+ * then AGENT_BODY_LIMIT, then 5 MB. A document sent inline (a base64 file a task reads)
24
+ * may need more; the server's own MAX_BODY_SIZE (16 MB) still applies first.
25
+ */
26
+ export declare function agentBodyLimit(project: UAGProjectInterface): number;
27
+ type Handler = (req: AgentRequest, res: Response, service: FormflowService) => Promise<void>;
28
+ /** The parts every Agent API endpoint shares: the request id, and errors answered as JSON (or as an event mid-stream). */
29
+ export declare function agentEndpoints(project: UAGProjectInterface): {
30
+ json: (req: Request, res: Response, next: NextFunction) => void | Response<any, Record<string, any>>;
31
+ requestId: (req: any, res: Response, next: NextFunction) => void;
32
+ handle: (operation: string, fn: Handler) => (req: Request, res: Response) => Promise<void>;
33
+ };
5
34
  /**
6
35
  * `GET /agent/capabilities`, behind `authenticate`. Only this path is claimed:
7
36
  * any other `/agent...` path (a project form at `agent`, for one) falls through
@@ -29,3 +58,4 @@ export declare function agentCredentials(project: UAGProjectInterface): RequestH
29
58
  * form of the URL.
30
59
  */
31
60
  export declare function FormAgentRouter(project: UAGProjectInterface, credentials: RequestHandler): Router;
61
+ export {};
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AGENT_BODY_LIMIT = void 0;
37
+ exports.agentBodyLimit = agentBodyLimit;
38
+ exports.agentEndpoints = agentEndpoints;
37
39
  exports.AgentRouter = AgentRouter;
38
40
  exports.agentNotFound = agentNotFound;
39
41
  exports.agentCredentials = agentCredentials;
@@ -107,19 +109,29 @@ async function respondWithStream(req, res, run) {
107
109
  res.json(done);
108
110
  }
109
111
  }
110
- /** The largest request body an agent operation takes, in bytes. */
112
+ /** The largest request body an agent operation takes by default, in bytes. */
111
113
  exports.AGENT_BODY_LIMIT = 5 * 1024 * 1024;
114
+ /**
115
+ * The largest request body the project's agent operations take: `agentApi.bodyLimit`,
116
+ * then AGENT_BODY_LIMIT, then 5 MB. A document sent inline (a base64 file a task reads)
117
+ * may need more; the server's own MAX_BODY_SIZE (16 MB) still applies first.
118
+ */
119
+ function agentBodyLimit(project) {
120
+ const configured = Number(project.config?.agentApi?.bodyLimit ?? process.env.AGENT_BODY_LIMIT);
121
+ return Number.isFinite(configured) && configured > 0 ? configured : exports.AGENT_BODY_LIMIT;
122
+ }
112
123
  /** The parts every Agent API endpoint shares: the request id, and errors answered as JSON (or as an event mid-stream). */
113
124
  function agentEndpoints(project) {
114
- const parse = express_1.default.json({ limit: exports.AGENT_BODY_LIMIT });
125
+ const limit = agentBodyLimit(project);
126
+ const parse = express_1.default.json({ limit });
115
127
  // The server parses JSON bodies first, with its own larger limit, so the
116
128
  // agent operations' limit is checked here on a body already read.
117
129
  const json = (req, res, next) => {
118
130
  if (req.body === undefined)
119
131
  return parse(req, res, next);
120
132
  const length = Number(req.get('content-length')) || Buffer.byteLength(JSON.stringify(req.body ?? null));
121
- if (length > exports.AGENT_BODY_LIMIT) {
122
- return res.status(413).json({ error: `The request body is larger than the agent operations' limit of ${exports.AGENT_BODY_LIMIT / (1024 * 1024)} MB.`, code: 'payload_too_large' });
133
+ if (length > limit) {
134
+ return res.status(413).json({ error: `The request body is larger than the agent operations' limit of ${Math.round(limit / (1024 * 1024) * 10) / 10} MB.`, code: 'payload_too_large' });
123
135
  }
124
136
  next();
125
137
  };
@@ -139,19 +151,13 @@ function agentEndpoints(project) {
139
151
  await (0, requestId_1.withRequestId)(agentReq.requestId, () => fn(agentReq, res, service));
140
152
  }
141
153
  catch (err) {
142
- const status = err instanceof formflow_1.FormflowError ? err.status : 500;
154
+ const { status, body } = (0, errors_1.agentError)(agentReq.requestId, err);
143
155
  if (status === 500) {
144
156
  debug(`[${agentReq.requestId}] ${operation} failed:`, err);
145
157
  }
146
158
  if (err instanceof formflow_1.RateLimitError) {
147
159
  res.set('Retry-After', String(Math.ceil(err.retryAfterMs / 1000)));
148
160
  }
149
- const body = {
150
- requestId: agentReq.requestId,
151
- error: status === 500 ? errors_1.INTERNAL_ERROR : err.message,
152
- code: (err instanceof formflow_1.FormflowError && err.code) || (0, errors_1.errorCode)(status),
153
- ...(err?.edition && { edition: err.edition })
154
- };
155
161
  if (res.headersSent) {
156
162
  // Mid-stream: report the failure as an event and close.
157
163
  res.write(`event: error\ndata: ${JSON.stringify(body)}\n\n`);
@@ -1,3 +1,4 @@
1
+ import type { GenerateRequest } from '@formio/uag-sdk';
1
2
  /**
2
3
  * Stand-in engines for tests and local development. They answer instantly with
3
4
  * predictable values, so the Agent API can be exercised end to end without
@@ -6,7 +7,9 @@
6
7
  * any instance name.
7
8
  */
8
9
  export declare const StubDecisionEngine: Readonly<{
9
- type: string;
10
+ type: "engine";
11
+ name: string;
12
+ title: string;
10
13
  kinds: "decision"[];
11
14
  apiVersion: 1;
12
15
  description: string;
@@ -14,8 +17,16 @@ export declare const StubDecisionEngine: Readonly<{
14
17
  calls: number;
15
18
  };
16
19
  }>;
20
+ /** How many generate requests the stub agent engines keep: a UAG running them for a demo never grows without bound. */
21
+ export declare const STUB_GENERATE_REQUESTS_KEPT = 20;
22
+ /** The last generate requests the stub agent engines received (STUB_GENERATE_REQUESTS_KEPT), oldest first, for tests. */
23
+ export declare const stubGenerateRequests: Array<Omit<GenerateRequest, 'signal' | 'onProgress'> & {
24
+ engine: string;
25
+ }>;
17
26
  export declare const StubAgentEngine: Readonly<{
18
- type: string;
27
+ type: "engine";
28
+ name: string;
29
+ title: string;
19
30
  kinds: "agent"[];
20
31
  apiVersion: 1;
21
32
  description: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StubAgentEngine = exports.StubDecisionEngine = void 0;
3
+ exports.StubAgentEngine = exports.stubGenerateRequests = exports.STUB_GENERATE_REQUESTS_KEPT = exports.StubDecisionEngine = void 0;
4
4
  const uag_sdk_1 = require("@formio/uag-sdk");
5
5
  const formflow_1 = require("../formflow");
6
6
  /**
@@ -11,16 +11,29 @@ const formflow_1 = require("../formflow");
11
11
  * any instance name.
12
12
  */
13
13
  exports.StubDecisionEngine = (0, uag_sdk_1.defineEngine)({
14
- type: 'stub-decision',
14
+ type: 'engine',
15
+ name: 'stub-decision',
16
+ title: 'Stub decision engine',
15
17
  kinds: ['decision'],
16
18
  apiVersion: uag_sdk_1.ENGINE_API_VERSION,
17
19
  description: 'Answers every decision with a fixed, predictable value. For tests and local development.',
18
20
  create: ({ name }) => (0, formflow_1.stubDecisionEngine)({ name }),
19
21
  });
22
+ /** How many generate requests the stub agent engines keep: a UAG running them for a demo never grows without bound. */
23
+ exports.STUB_GENERATE_REQUESTS_KEPT = 20;
24
+ /** The last generate requests the stub agent engines received (STUB_GENERATE_REQUESTS_KEPT), oldest first, for tests. */
25
+ exports.stubGenerateRequests = [];
20
26
  exports.StubAgentEngine = (0, uag_sdk_1.defineEngine)({
21
- type: 'stub-agent',
27
+ type: 'engine',
28
+ name: 'stub-agent',
29
+ title: 'Stub agent engine',
22
30
  kinds: ['agent'],
23
31
  apiVersion: uag_sdk_1.ENGINE_API_VERSION,
24
- description: 'Extracts nothing. For tests and local development.',
25
- create: ({ name }) => (0, formflow_1.stubAgentEngine)({ name }),
32
+ description: 'Extracts nothing; generates a predictable answer from the text it is given. For tests and local development.',
33
+ // Text only; its generate answer shows what reached it. Observing its requests keeps it a built-in stub.
34
+ create: ({ name }) => (0, formflow_1.stubAgentEngine)({ name, onGenerate: (request) => {
35
+ exports.stubGenerateRequests.push({ ...request, engine: name });
36
+ if (exports.stubGenerateRequests.length > exports.STUB_GENERATE_REQUESTS_KEPT)
37
+ exports.stubGenerateRequests.splice(0, exports.stubGenerateRequests.length - exports.STUB_GENERATE_REQUESTS_KEPT);
38
+ } }),
26
39
  });
@@ -1,10 +1,31 @@
1
1
  import { FormflowTool } from "../formflow";
2
+ import type { AuthRequest } from "@formio/appserver";
2
3
  import type { UAGProjectInterface } from "../UAGProjectInterface";
3
- import { ToolInfo } from "../tools/utils";
4
- /** Every tool the UAG serves, including custom tools from configuration. */
4
+ import { ToolEntry, ToolInfo } from "../tools/utils";
5
+ /** Record the module a tool came from: its `execute` gets the module's API as `extra.module`. */
6
+ export declare function setToolModule(entry: object, api: any): void;
7
+ /**
8
+ * A `config.tools` entry as a tool for the project: a bound tool's static
9
+ * fields, overlaid with what its `bind(project)` returns. A tool without
10
+ * `bind` (the 1.x shape) is used as it is.
11
+ */
12
+ export declare function resolveTool(project: UAGProjectInterface, entry: ToolEntry): Promise<ToolInfo | undefined>;
13
+ /**
14
+ * Every tool the UAG serves, including custom tools from configuration, each
15
+ * entry of which is a tool, optionally bound to the project.
16
+ */
5
17
  export declare function allTools(project: UAGProjectInterface): Promise<ToolInfo[]>;
18
+ /**
19
+ * Run a tool with its arguments and the `ToolExtra` for the project: the same
20
+ * call on `/mcp` and for Agent API research. `context` is whatever else the
21
+ * caller has (on `/mcp`, the MCP SDK's request context, with `authInfo`).
22
+ */
23
+ export declare function runTool(tool: ToolInfo, project: UAGProjectInterface, args: any, context: {
24
+ authInfo: AuthRequest;
25
+ [key: string]: any;
26
+ }): any;
6
27
  /**
7
28
  * An MCP tool as an Agentic Formflow tool: its schema as JSON, its arguments
8
29
  * validated with its own schema, and read-only only when annotated so.
9
30
  */
10
- export declare function formflowTool(tool: ToolInfo): FormflowTool;
31
+ export declare function formflowTool(tool: ToolInfo, project: UAGProjectInterface): FormflowTool;