@formio/uag 1.13.2 → 2.0.0-rc.10

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 (115) hide show
  1. package/README.md +237 -535
  2. package/bin/uag-eval.js +99 -0
  3. package/bin/uag.js +116 -0
  4. package/lib/UAGFormInterface.d.ts +72 -2
  5. package/lib/UAGFormInterface.js +226 -57
  6. package/lib/UAGProjectInterface.d.ts +45 -5
  7. package/lib/UAGProjectInterface.js +187 -36
  8. package/lib/actionLogs.d.ts +24 -0
  9. package/lib/actionLogs.js +149 -0
  10. package/lib/actions/runTask.d.ts +43 -0
  11. package/lib/actions/runTask.js +324 -0
  12. package/lib/agent/anonymous.d.ts +34 -0
  13. package/lib/agent/anonymous.js +99 -0
  14. package/lib/agent/chat.d.ts +114 -0
  15. package/lib/agent/chat.js +475 -0
  16. package/lib/agent/engines.d.ts +17 -0
  17. package/lib/agent/engines.js +46 -0
  18. package/lib/agent/errors.d.ts +25 -0
  19. package/lib/agent/errors.js +132 -0
  20. package/lib/agent/index.d.ts +4 -0
  21. package/lib/agent/index.js +19 -0
  22. package/lib/agent/remote.d.ts +71 -0
  23. package/lib/agent/remote.js +252 -0
  24. package/lib/agent/requestId.d.ts +4 -0
  25. package/lib/agent/requestId.js +19 -0
  26. package/lib/agent/router.d.ts +61 -0
  27. package/lib/agent/router.js +307 -0
  28. package/lib/agent/stubs.d.ts +37 -0
  29. package/lib/agent/stubs.js +39 -0
  30. package/lib/agent/tools.d.ts +31 -0
  31. package/lib/agent/tools.js +76 -0
  32. package/lib/config.d.ts +97 -3
  33. package/lib/config.js +19 -0
  34. package/lib/database.d.ts +70 -0
  35. package/lib/database.js +212 -0
  36. package/lib/documents/convert.d.ts +38 -0
  37. package/lib/documents/convert.js +151 -0
  38. package/lib/documents/converters.d.ts +28 -0
  39. package/lib/documents/converters.js +149 -0
  40. package/lib/documents/detect.d.ts +31 -0
  41. package/lib/documents/detect.js +169 -0
  42. package/lib/documents/index.d.ts +9 -0
  43. package/lib/documents/index.js +43 -0
  44. package/lib/documents/mime.d.ts +39 -0
  45. package/lib/documents/mime.js +100 -0
  46. package/lib/documents/plugins.d.ts +18 -0
  47. package/lib/documents/plugins.js +65 -0
  48. package/lib/documents/readers.d.ts +19 -0
  49. package/lib/documents/readers.js +64 -0
  50. package/lib/documents/routes.d.ts +52 -0
  51. package/lib/documents/routes.js +99 -0
  52. package/lib/documents/types.d.ts +126 -0
  53. package/lib/documents/types.js +2 -0
  54. package/lib/documents/uagJs.d.ts +17 -0
  55. package/lib/documents/uagJs.js +157 -0
  56. package/lib/formflow.d.ts +47 -0
  57. package/lib/formflow.js +28 -0
  58. package/lib/identity.d.ts +53 -0
  59. package/lib/identity.js +107 -0
  60. package/lib/index.d.ts +98 -1
  61. package/lib/index.js +382 -8
  62. package/lib/markdown.d.ts +1 -0
  63. package/lib/markdown.js +166 -0
  64. package/lib/modules/api.d.ts +25 -0
  65. package/lib/modules/api.js +98 -0
  66. package/lib/modules/index.d.ts +4 -0
  67. package/lib/modules/index.js +24 -0
  68. package/lib/modules/routes.d.ts +14 -0
  69. package/lib/modules/routes.js +21 -0
  70. package/lib/modules/types.d.ts +68 -0
  71. package/lib/modules/types.js +5 -0
  72. package/lib/pluginFiles.d.ts +28 -0
  73. package/lib/pluginFiles.js +101 -0
  74. package/lib/plugins.d.ts +17 -0
  75. package/lib/plugins.js +93 -0
  76. package/lib/richText.d.ts +20 -0
  77. package/lib/richText.js +99 -0
  78. package/lib/router.d.ts +9 -0
  79. package/lib/router.js +34 -25
  80. package/lib/runner.d.ts +149 -0
  81. package/lib/runner.js +353 -0
  82. package/lib/runs.d.ts +27 -0
  83. package/lib/runs.js +13 -0
  84. package/lib/template.js +7 -9
  85. package/lib/templates/uagComponentNotFound.md +5 -5
  86. package/lib/testing.d.ts +74 -0
  87. package/lib/testing.js +180 -0
  88. package/lib/tools/SchemaBuilder.d.ts +3 -3
  89. package/lib/tools/SchemaBuilder.js +4 -4
  90. package/lib/tools/agentDecide.d.ts +8 -0
  91. package/lib/tools/agentDecide.js +57 -0
  92. package/lib/tools/agentProvide.d.ts +8 -0
  93. package/lib/tools/agentProvide.js +97 -0
  94. package/lib/tools/collectData.js +3 -2
  95. package/lib/tools/confirmSubmission.js +1 -1
  96. package/lib/tools/fetchExternalData.js +8 -17
  97. package/lib/tools/findSubmission.js +62 -56
  98. package/lib/tools/getFieldInfo.js +1 -1
  99. package/lib/tools/getFormFields.js +1 -1
  100. package/lib/tools/getForms.js +1 -1
  101. package/lib/tools/index.d.ts +4 -1
  102. package/lib/tools/index.js +21 -14
  103. package/lib/tools/readDocument.d.ts +17 -0
  104. package/lib/tools/readDocument.js +99 -0
  105. package/lib/tools/submissionUpdate.js +4 -3
  106. package/lib/tools/submitForm.js +1 -1
  107. package/lib/tools/utils.d.ts +40 -0
  108. package/lib/tools/utils.js +16 -1
  109. package/lib/version.d.ts +2 -0
  110. package/lib/version.js +5 -0
  111. package/package.json +49 -19
  112. package/testing.d.ts +1 -0
  113. package/testing.js +2 -0
  114. package/lib/tools/agentProvideData.d.ts +0 -3
  115. package/lib/tools/agentProvideData.js +0 -62
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INTERNAL_ERROR = void 0;
4
+ exports.isAgentApiRequest = isAgentApiRequest;
5
+ exports.errorCode = errorCode;
6
+ exports.agentError = agentError;
7
+ exports.requestIdOf = requestIdOf;
8
+ exports.errorBody = errorBody;
9
+ exports.agentErrorShape = agentErrorShape;
10
+ const crypto_1 = require("crypto");
11
+ const protocol_1 = require("@formio/uag-sdk/protocol");
12
+ const formflow_1 = require("../formflow");
13
+ /**
14
+ * The Agent API's one error shape. Every error a form's agent operation or
15
+ * `/agent/capabilities` answers is JSON
16
+ *
17
+ * { "requestId": "...", "error": "a message for people", "code": "a_stable_code", ... }
18
+ *
19
+ * with the same id in `X-Request-Id`, whatever refused it: the UAG, the
20
+ * project's authentication, the portal's team access, the body parser.
21
+ * Clients branch on `code`; `error` is for people. A 500 never carries details:
22
+ * those are in the UAG's log under the request id.
23
+ */
24
+ /** A form's agent operations, and the capabilities. */
25
+ const OPERATION = /\/agent\/(extract|decide|generate|chat)\/?$/i;
26
+ const CAPABILITIES = /^\/agent\/capabilities\/?$/i;
27
+ function isAgentApiRequest(req) {
28
+ return (req.method === 'POST' && OPERATION.test(req.path)) || (req.method === 'GET' && CAPABILITIES.test(req.path));
29
+ }
30
+ /** The code for a status, when the error names none. */
31
+ function errorCode(status) {
32
+ switch (status) {
33
+ case 400: return 'invalid_request';
34
+ case 401: return 'unauthorized';
35
+ case 403: return 'forbidden';
36
+ case 404: return 'not_found';
37
+ case 409: return 'conflict';
38
+ case 413: return 'payload_too_large';
39
+ case 429: return 'rate_limited';
40
+ case 502: return 'engines_failed';
41
+ case 503: return 'unavailable';
42
+ case 504: return 'timeout';
43
+ default: return status >= 500 ? 'internal_error' : 'invalid_request';
44
+ }
45
+ }
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
+ }
65
+ /** The request's id, assigned once: the caller's, when it is a short token, or a new one. */
66
+ function requestIdOf(req, res) {
67
+ const request = req;
68
+ if (!request.requestId) {
69
+ request.requestId = (0, protocol_1.safeRequestId)(req.get('x-request-id')) || (0, crypto_1.randomUUID)();
70
+ }
71
+ if (!res.headersSent)
72
+ res.set('X-Request-Id', request.requestId);
73
+ return request.requestId;
74
+ }
75
+ /** The error body for a status and whatever a handler sent: an object, JSON text, plain text or nothing. */
76
+ function errorBody(requestId, status, sent) {
77
+ let value = sent;
78
+ if (Buffer.isBuffer(value))
79
+ value = value.toString('utf8');
80
+ if (typeof value === 'string') {
81
+ try {
82
+ value = JSON.parse(value);
83
+ }
84
+ catch {
85
+ value = { error: value };
86
+ }
87
+ }
88
+ const given = value && typeof value === 'object' ? value : {};
89
+ const text = [given.error, given.detail, given.message].find((part) => typeof part === 'string' && part.trim());
90
+ // A 500's own message may carry internals: only the UAG's fixed sentence goes out.
91
+ const error = status >= 500 && status !== 502 && status !== 503 && status !== 504 ? exports.INTERNAL_ERROR : (text || defaultMessage(status));
92
+ return {
93
+ requestId,
94
+ error,
95
+ code: typeof given.code === 'string' && given.code ? given.code : errorCode(status),
96
+ ...(typeof given.edition === 'string' && { edition: given.edition })
97
+ };
98
+ }
99
+ function defaultMessage(status) {
100
+ switch (status) {
101
+ case 401: return 'Unauthorized.';
102
+ case 403: return 'Forbidden.';
103
+ case 404: return 'Not found.';
104
+ case 413: return 'The request body is too large.';
105
+ case 429: return 'Too many requests. Try again shortly.';
106
+ default: return 'The request could not be completed.';
107
+ }
108
+ }
109
+ /**
110
+ * Mounted ahead of every route: for an Agent API request, assigns the request
111
+ * id and rewrites any error response into the one shape, so refusals from
112
+ * middleware the UAG does not own (the project's authentication answers plain
113
+ * text, the portal's team access an empty 401) look like every other.
114
+ */
115
+ function agentErrorShape() {
116
+ return (req, res, next) => {
117
+ if (!isAgentApiRequest(req))
118
+ return next();
119
+ const requestId = requestIdOf(req, res);
120
+ const send = res.send.bind(res);
121
+ let shaped = false;
122
+ res.send = ((body) => {
123
+ // An event stream writes its own error events; only whole responses are shaped.
124
+ if (shaped || res.statusCode < 400)
125
+ return send(body);
126
+ shaped = true;
127
+ res.type('application/json');
128
+ return send(JSON.stringify(errorBody(requestId, res.statusCode, body)));
129
+ });
130
+ next();
131
+ };
132
+ }
@@ -0,0 +1,4 @@
1
+ export { StubAgentEngine, StubDecisionEngine, stubGenerateRequests } from './stubs';
2
+ export { BUILT_IN_ENGINES, UAG_ROOT, loadUAGEngines } from './engines';
3
+ export { AgentRouter, FormAgentRouter, agentCredentials, agentNotFound } from './router';
4
+ export { RemoteEngine, MIN_KEY_LENGTH } from './remote';
@@ -0,0 +1,19 @@
1
+ "use strict";
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.stubGenerateRequests = exports.StubDecisionEngine = exports.StubAgentEngine = void 0;
4
+ var stubs_1 = require("./stubs");
5
+ Object.defineProperty(exports, "StubAgentEngine", { enumerable: true, get: function () { return stubs_1.StubAgentEngine; } });
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; } });
8
+ var engines_1 = require("./engines");
9
+ Object.defineProperty(exports, "BUILT_IN_ENGINES", { enumerable: true, get: function () { return engines_1.BUILT_IN_ENGINES; } });
10
+ Object.defineProperty(exports, "UAG_ROOT", { enumerable: true, get: function () { return engines_1.UAG_ROOT; } });
11
+ Object.defineProperty(exports, "loadUAGEngines", { enumerable: true, get: function () { return engines_1.loadUAGEngines; } });
12
+ var router_1 = require("./router");
13
+ Object.defineProperty(exports, "AgentRouter", { enumerable: true, get: function () { return router_1.AgentRouter; } });
14
+ Object.defineProperty(exports, "FormAgentRouter", { enumerable: true, get: function () { return router_1.FormAgentRouter; } });
15
+ Object.defineProperty(exports, "agentCredentials", { enumerable: true, get: function () { return router_1.agentCredentials; } });
16
+ Object.defineProperty(exports, "agentNotFound", { enumerable: true, get: function () { return router_1.agentNotFound; } });
17
+ var remote_1 = require("./remote");
18
+ Object.defineProperty(exports, "RemoteEngine", { enumerable: true, get: function () { return remote_1.RemoteEngine; } });
19
+ Object.defineProperty(exports, "MIN_KEY_LENGTH", { enumerable: true, get: function () { return remote_1.MIN_KEY_LENGTH; } });
@@ -0,0 +1,71 @@
1
+ import type { RemoteEngineDescription } from '@formio/uag-sdk/protocol';
2
+ import type { AgentEngine, DecisionEngine } from '../formflow';
3
+ /**
4
+ * The `remote` engine type: an engine hosted by an engine server
5
+ * (`formio/uag-engines`), reached over the remote engine protocol. Any
6
+ * engine package can run there, so model credentials and model traffic stay in
7
+ * that container.
8
+ *
9
+ * UAG_ENGINES=jev,claude:remote
10
+ * UAG_ENGINE_CLAUDE_URL=http://engines:3300
11
+ * UAG_ENGINE_CLAUDE_KEY=<the engine server key, at least 32 characters>
12
+ *
13
+ * Every request carries the engine server key, and never a Form.io credential.
14
+ * Research extracts also carry a scoped token, which the engine server uses for
15
+ * its own MCP connection back to the UAG: it allows only the task's read-only
16
+ * tools, with the caller's identity. Each call also carries the Agent API
17
+ * request id, when it is a short token, for logs on either side.
18
+ */
19
+ /** The shortest engine server key accepted. */
20
+ export declare const MIN_KEY_LENGTH = 32;
21
+ type Fetch = typeof fetch;
22
+ type Connection = {
23
+ /** The instance name, for errors. */
24
+ name: string;
25
+ baseUrl: string;
26
+ key: string;
27
+ fetch: Fetch;
28
+ /** The most PDF bytes one generate request may send: documents.maxTotalBytes. */
29
+ maxDocumentBytes: number;
30
+ };
31
+ /**
32
+ * Describe the remote engine, retrying while the engine server is unreachable
33
+ * or failing, with the backoff used to wait for a Form.io project. A refused
34
+ * key, or a server that does not host the engine, stops startup at once.
35
+ */
36
+ export declare function describeRemote(connection: Connection, engine: string, { retries, retryDelay, timeout }?: {
37
+ retries?: number;
38
+ retryDelay?: number;
39
+ timeout?: number;
40
+ }): Promise<RemoteEngineDescription>;
41
+ /** An engine object for a described remote engine. */
42
+ export declare function remoteEngine(connection: Connection, remoteName: string, described: RemoteEngineDescription): DecisionEngine | AgentEngine;
43
+ export declare const RemoteEngine: Readonly<{
44
+ type: "engine";
45
+ name: string;
46
+ title: string;
47
+ kinds: ("agent" | "decision")[];
48
+ apiVersion: 1;
49
+ description: string;
50
+ settings: {
51
+ url: {
52
+ required: true;
53
+ description: string;
54
+ };
55
+ key: {
56
+ required: true;
57
+ secret: true;
58
+ description: string;
59
+ };
60
+ engine: {
61
+ description: string;
62
+ };
63
+ timeout: {
64
+ type: "number";
65
+ default: number;
66
+ description: string;
67
+ };
68
+ };
69
+ create({ name, settings, options, config }: import("@formio/uag-sdk").EngineCreateContext<any>): Promise<import("@formio/appserver-core/lib/formflow").AgentEngine | import("@formio/appserver-core/lib/formflow").DecisionEngine>;
70
+ }>;
71
+ export {};
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RemoteEngine = exports.MIN_KEY_LENGTH = void 0;
4
+ exports.describeRemote = describeRemote;
5
+ exports.remoteEngine = remoteEngine;
6
+ const uag_sdk_1 = require("@formio/uag-sdk");
7
+ const protocol_1 = require("@formio/uag-sdk/protocol");
8
+ const requestId_1 = require("./requestId");
9
+ const debug = require('debug')('formio:uag:remote');
10
+ /**
11
+ * The `remote` engine type: an engine hosted by an engine server
12
+ * (`formio/uag-engines`), reached over the remote engine protocol. Any
13
+ * engine package can run there, so model credentials and model traffic stay in
14
+ * that container.
15
+ *
16
+ * UAG_ENGINES=jev,claude:remote
17
+ * UAG_ENGINE_CLAUDE_URL=http://engines:3300
18
+ * UAG_ENGINE_CLAUDE_KEY=<the engine server key, at least 32 characters>
19
+ *
20
+ * Every request carries the engine server key, and never a Form.io credential.
21
+ * Research extracts also carry a scoped token, which the engine server uses for
22
+ * its own MCP connection back to the UAG: it allows only the task's read-only
23
+ * tools, with the caller's identity. Each call also carries the Agent API
24
+ * request id, when it is a short token, for logs on either side.
25
+ */
26
+ /** The shortest engine server key accepted. */
27
+ exports.MIN_KEY_LENGTH = 32;
28
+ const DEFAULT_RETRIES = 5;
29
+ const DEFAULT_RETRY_DELAY = 5000;
30
+ const MAX_RETRY_DELAY = 60000;
31
+ class StartupError extends uag_sdk_1.EngineConfigError {
32
+ /** Whether trying again may help: the server was unreachable or failing. */
33
+ constructor(message, retry) {
34
+ super(message);
35
+ this.retry = retry;
36
+ }
37
+ }
38
+ async function readJson(response) {
39
+ try {
40
+ return await response.json();
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ /**
47
+ * Describe the remote engine, retrying while the engine server is unreachable
48
+ * or failing, with the backoff used to wait for a Form.io project. A refused
49
+ * key, or a server that does not host the engine, stops startup at once.
50
+ */
51
+ async function describeRemote(connection, engine, { retries = DEFAULT_RETRIES, retryDelay = DEFAULT_RETRY_DELAY, timeout = 10000 } = {}) {
52
+ const where = `the engine server at ${connection.baseUrl}`;
53
+ const attempts = Math.max(1, retries);
54
+ for (let attempt = 1;; attempt++) {
55
+ try {
56
+ return await describeOnce(connection, engine, where, timeout);
57
+ }
58
+ catch (err) {
59
+ if (!(err instanceof StartupError && err.retry) || attempt >= attempts) {
60
+ throw new uag_sdk_1.EngineConfigError(`The "${connection.name}" engine could not start: ${err.message}`);
61
+ }
62
+ const delay = Math.min(retryDelay * Math.pow(2, attempt - 1), MAX_RETRY_DELAY);
63
+ debug(`${err.message} Retrying in ${delay}ms (attempt ${attempt} of ${attempts}).`);
64
+ await new Promise((resolve) => setTimeout(resolve, delay));
65
+ }
66
+ }
67
+ }
68
+ async function describeOnce(connection, engine, where, timeout) {
69
+ let response;
70
+ try {
71
+ response = await connection.fetch(`${connection.baseUrl}/v1/engines`, {
72
+ headers: { Accept: 'application/json', Authorization: `Bearer ${connection.key}` },
73
+ signal: AbortSignal.timeout(timeout),
74
+ });
75
+ }
76
+ catch (err) {
77
+ throw new StartupError(`${where} is unreachable (${err?.cause?.code || err?.name || 'no response'}).`, true);
78
+ }
79
+ if (response.status === 401 || response.status === 403) {
80
+ throw new StartupError(`${where} refused the engine server key (HTTP ${response.status}).`, false);
81
+ }
82
+ if (!response.ok) {
83
+ throw new StartupError(`${where} answered HTTP ${response.status}.`, response.status >= 500);
84
+ }
85
+ let engines;
86
+ try {
87
+ engines = (0, protocol_1.checkDescription)(await readJson(response));
88
+ }
89
+ catch (err) {
90
+ throw new StartupError(`${where} did not answer as an engine server: ${err.message}`, false);
91
+ }
92
+ const described = engines.find((candidate) => candidate.name === engine);
93
+ if (!described) {
94
+ throw new StartupError(`${where} does not host an engine named "${engine}"` +
95
+ (engines.length ? ` (it hosts ${engines.map((e) => `"${e.name}"`).join(', ')}).` : ' (it hosts none).'), false);
96
+ }
97
+ return described;
98
+ }
99
+ /** One protocol call. Failures are engine failures, safe to show unless the server says otherwise. */
100
+ async function call(connection, path, body, signal, scopedToken) {
101
+ signal?.throwIfAborted();
102
+ const requestId = (0, protocol_1.safeRequestId)((0, requestId_1.currentRequestId)());
103
+ const headers = {
104
+ 'Content-Type': 'application/json',
105
+ Accept: 'application/json',
106
+ Authorization: `Bearer ${connection.key}`,
107
+ ...(scopedToken && { [protocol_1.SCOPED_TOKEN_HEADER]: scopedToken }),
108
+ ...(requestId && { [protocol_1.REQUEST_ID_HEADER]: requestId }),
109
+ };
110
+ let response;
111
+ try {
112
+ response = await connection.fetch(`${connection.baseUrl}${path}`, {
113
+ method: 'POST',
114
+ headers,
115
+ body: JSON.stringify({ protocol: protocol_1.PROTOCOL_VERSION, ...body }),
116
+ signal,
117
+ });
118
+ }
119
+ catch (err) {
120
+ if (signal?.aborted)
121
+ throw signal.reason ?? err;
122
+ // Callers see the failure; only the UAG's log names where the engine server is.
123
+ debug(`The engine server at ${connection.baseUrl} could not be reached: ${err?.cause?.code || err?.name || 'no response'}`);
124
+ throw new uag_sdk_1.EngineError('The engine server could not be reached.');
125
+ }
126
+ const json = await readJson(response);
127
+ if (!response.ok) {
128
+ if (json?.expose === true && typeof json.error === 'string') {
129
+ throw new uag_sdk_1.EngineError(json.error);
130
+ }
131
+ debug(`The engine server answered HTTP ${response.status}: ${json?.error || '(no message)'}`);
132
+ throw new uag_sdk_1.EngineError(`The engine server answered HTTP ${response.status}.`);
133
+ }
134
+ return json;
135
+ }
136
+ /** Validate a response, reporting a malformed one as an engine failure. */
137
+ function valid(check, body) {
138
+ try {
139
+ return check(body);
140
+ }
141
+ catch (err) {
142
+ throw new uag_sdk_1.EngineError(`The engine server's answer did not follow the remote engine protocol: ${err.message}`);
143
+ }
144
+ }
145
+ /** An engine object for a described remote engine. */
146
+ function remoteEngine(connection, remoteName, described) {
147
+ const path = `/v1/engines/${encodeURIComponent(remoteName)}`;
148
+ if (described.kind === 'decision') {
149
+ return {
150
+ name: connection.name,
151
+ kind: 'decision',
152
+ apiVersion: uag_sdk_1.ENGINE_API_VERSION,
153
+ model: described.model,
154
+ maxOptions: described.maxOptions,
155
+ async decide({ state, questions, signal }) {
156
+ const response = valid(protocol_1.checkDecisionResponse, await call(connection, `${path}/decide`, { state, questions }, signal));
157
+ return { ...response, model: response.model || described.model };
158
+ },
159
+ };
160
+ }
161
+ const agent = {
162
+ name: connection.name,
163
+ kind: 'agent',
164
+ apiVersion: uag_sdk_1.ENGINE_API_VERSION,
165
+ model: described.model,
166
+ toolUse: described.toolUse === true,
167
+ accepts: described.accepts || ['text'],
168
+ async extract(request) {
169
+ const research = !!request.tools?.length;
170
+ // The engine server researches through its own MCP connection to the
171
+ // UAG, which needs a scoped token; without one (no JWT_SECRET), no call.
172
+ if (research && !request.scopedToken) {
173
+ throw new uag_sdk_1.EngineError('The remote engine cannot research without a scoped token. Set JWT_SECRET on the UAG.');
174
+ }
175
+ const body = {
176
+ instructions: request.instructions,
177
+ state: request.state,
178
+ fields: request.fields,
179
+ schema: request.schema,
180
+ ...(research && { tools: request.tools, maxIterations: request.maxIterations || 10 }),
181
+ };
182
+ const response = valid(protocol_1.checkExtractResponse, await call(connection, `${path}/extract`, body, request.signal, research ? request.scopedToken : undefined));
183
+ return { ...response, model: response.model || described.model };
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;
205
+ }
206
+ exports.RemoteEngine = (0, uag_sdk_1.defineEngine)({
207
+ type: 'engine',
208
+ name: 'remote',
209
+ title: 'Remote engine (engine server)',
210
+ kinds: ['decision', 'agent'],
211
+ apiVersion: uag_sdk_1.ENGINE_API_VERSION,
212
+ description: 'An engine hosted by an engine server, over the remote engine protocol.',
213
+ settings: {
214
+ url: { required: true, description: 'The engine server, such as http://engines:3300.' },
215
+ key: { required: true, secret: true, description: `The engine server key, at least ${exports.MIN_KEY_LENGTH} characters.` },
216
+ engine: { description: 'The engine\'s name on the engine server. Default: the instance name.' },
217
+ timeout: { type: 'number', default: 10000, description: 'Milliseconds for each attempt to describe the engine at startup.' },
218
+ },
219
+ async create({ name, settings, options, config }) {
220
+ let url;
221
+ try {
222
+ url = new URL(settings.url);
223
+ }
224
+ catch {
225
+ throw new uag_sdk_1.EngineConfigError(`The "${name}" engine's url setting is not a URL: ${JSON.stringify(settings.url)}.`);
226
+ }
227
+ if (!['http:', 'https:'].includes(url.protocol)) {
228
+ throw new uag_sdk_1.EngineConfigError(`The "${name}" engine's url setting must be an http or https URL.`);
229
+ }
230
+ if (url.username || url.password) {
231
+ throw new uag_sdk_1.EngineConfigError(`The "${name}" engine's url setting must not include a user name or password: its key setting authenticates the UAG to the engine server.`);
232
+ }
233
+ if (String(settings.key).length < exports.MIN_KEY_LENGTH) {
234
+ throw new uag_sdk_1.EngineConfigError(`The "${name}" engine's key setting must be at least ${exports.MIN_KEY_LENGTH} characters, the same key as the engine server's UAG_ENGINE_SERVER_KEY.`);
235
+ }
236
+ const connection = {
237
+ name,
238
+ baseUrl: url.href.replace(/\/+$/, ''),
239
+ key: settings.key,
240
+ fetch: options.fetch || fetch,
241
+ maxDocumentBytes: Math.min(Number(config?.documents?.maxTotalBytes) || protocol_1.MAX_DOCUMENT_BYTES, protocol_1.MAX_DOCUMENT_BYTES),
242
+ };
243
+ const remoteName = settings.engine || name;
244
+ const described = await describeRemote(connection, remoteName, {
245
+ retries: config.retries,
246
+ retryDelay: config.retryDelay,
247
+ timeout: settings.timeout,
248
+ });
249
+ debug(`"${name}" is the ${described.kind} engine "${remoteName}" (${described.model}) at ${connection.baseUrl}.`);
250
+ return remoteEngine(connection, remoteName, described);
251
+ },
252
+ });
@@ -0,0 +1,4 @@
1
+ /** Run `fn` as part of the request `requestId`. */
2
+ export declare function withRequestId<T>(requestId: string | undefined, fn: () => T): T;
3
+ /** The id of the request being served, if any. */
4
+ export declare function currentRequestId(): string | undefined;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.withRequestId = withRequestId;
4
+ exports.currentRequestId = currentRequestId;
5
+ const async_hooks_1 = require("async_hooks");
6
+ /**
7
+ * The Agent API request an operation runs for, so the `remote` engine type can
8
+ * send its id to the engine server: the engine contract carries no request id,
9
+ * and Formflow calls engines within the operation's async context.
10
+ */
11
+ const current = new async_hooks_1.AsyncLocalStorage();
12
+ /** Run `fn` as part of the request `requestId`. */
13
+ function withRequestId(requestId, fn) {
14
+ return current.run(requestId, fn);
15
+ }
16
+ /** The id of the request being served, if any. */
17
+ function currentRequestId() {
18
+ return current.getStore();
19
+ }
@@ -0,0 +1,61 @@
1
+ import { NextFunction, Request, RequestHandler, Response, Router } from "express";
2
+ import { SubmissionRequest } from "@formio/appserver";
3
+ import { FormflowService } from "../formflow";
4
+ import type { UAGProjectInterface } from "../UAGProjectInterface";
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. */
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
+ };
34
+ /**
35
+ * `GET /agent/capabilities`, behind `authenticate`. Only this path is claimed:
36
+ * any other `/agent...` path (a project form at `agent`, for one) falls through
37
+ * to the routes after this router.
38
+ */
39
+ export declare function AgentRouter(project: UAGProjectInterface, authenticate: RequestHandler): Router;
40
+ /**
41
+ * A form's agent operation for a path that names no form of the project (the
42
+ * old root operations, a deleted form): answered 404 here, so it is never
43
+ * passed through to Form.io. A form that exists goes on to its own routes,
44
+ * which answer 404 themselves when it is not tagged `uag`.
45
+ */
46
+ export declare function agentNotFound(project: UAGProjectInterface): RequestHandler;
47
+ /**
48
+ * Who may call a form's agent operations, on top of the project's own
49
+ * authentication (Form.io user tokens, portal remote tokens, server keys):
50
+ * scoped tokens never (they are for /mcp), server keys only from servers, and
51
+ * UAG bearer tokens, verified here. A caller with no credential gets through
52
+ * only where the form allows anonymous use (see anonymousGate).
53
+ */
54
+ export declare function agentCredentials(project: UAGProjectInterface): RequestHandler;
55
+ /**
56
+ * A form's agent operations, `POST /agent/extract|decide|generate|chat`, mounted on
57
+ * the form's own routes (see UAGFormInterface.router), so `req.form` is the
58
+ * form of the URL.
59
+ */
60
+ export declare function FormAgentRouter(project: UAGProjectInterface, credentials: RequestHandler): Router;
61
+ export {};