@uipath/maestro-builder-sdk 6.41.2 → 6.43.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.
Files changed (73) hide show
  1. package/dist/api-index.md +99 -99
  2. package/dist/api-members.md +307 -307
  3. package/dist/check.d.ts +0 -19
  4. package/dist/check.js +40 -1036
  5. package/dist/cli/commands/registry.js +15 -0
  6. package/dist/compile-cli.js +2 -1
  7. package/dist/core/actions.d.ts +1 -713
  8. package/dist/core/actions.js +0 -193
  9. package/dist/core/agent-sidecar.d.ts +186 -0
  10. package/dist/core/agent-sidecar.js +409 -0
  11. package/dist/core/agent-tool-resources.d.ts +62 -0
  12. package/dist/core/agent-tool-resources.js +81 -0
  13. package/dist/core/cli-spelling.d.ts +9 -0
  14. package/dist/core/cli-spelling.js +9 -0
  15. package/dist/{config.js → core/connector-configuration.js} +2 -2
  16. package/dist/core/library-cache.d.ts +0 -9
  17. package/dist/core/library-cache.js +0 -9
  18. package/dist/core/library.d.ts +47 -0
  19. package/dist/core/library.js +135 -34
  20. package/dist/core/list-envelope.d.ts +36 -1
  21. package/dist/core/list-envelope.js +24 -19
  22. package/dist/core/lookup-store.d.ts +4 -3
  23. package/dist/core/lookup-store.js +46 -38
  24. package/dist/core/node-id-placeholder.d.ts +8 -0
  25. package/dist/core/node-id-placeholder.js +10 -0
  26. package/dist/core/read-text.d.ts +26 -0
  27. package/dist/core/read-text.js +43 -0
  28. package/dist/core/ts-literal.d.ts +6 -0
  29. package/dist/core/ts-literal.js +15 -0
  30. package/dist/core/typed-raw-node.js +0 -5
  31. package/dist/decompile.js +34 -822
  32. package/dist/flow-node-registry.js +2 -1
  33. package/dist/flow-node-type.d.ts +130 -6
  34. package/dist/flow-sdk.d.ts +1 -0
  35. package/dist/flow-sdk.js +1 -0
  36. package/dist/nodes/agent/flow/check.d.ts +88 -0
  37. package/dist/nodes/agent/flow/check.js +873 -0
  38. package/dist/nodes/agent/flow/factories.d.ts +134 -0
  39. package/dist/nodes/agent/flow/factories.js +143 -0
  40. package/dist/nodes/agent/flow/family.d.ts +3 -0
  41. package/dist/nodes/agent/flow/family.js +8 -0
  42. package/dist/nodes/agent/flow/index.d.ts +3 -0
  43. package/dist/nodes/agent/flow/index.js +3 -0
  44. package/dist/nodes/agent/flow/inline-agent-lift.d.ts +44 -0
  45. package/dist/nodes/agent/flow/inline-agent-lift.js +572 -0
  46. package/dist/nodes/agent/flow/inline-agent.d.ts +15 -0
  47. package/dist/nodes/agent/flow/inline-agent.js +526 -0
  48. package/dist/nodes/agent/flow/node-type.d.ts +20 -0
  49. package/dist/nodes/agent/flow/node-type.js +422 -0
  50. package/dist/nodes/agent/flow/resources.d.ts +502 -0
  51. package/dist/nodes/agent/flow/resources.js +1409 -0
  52. package/dist/nodes/agent/flow/spec.d.ts +448 -0
  53. package/dist/nodes/agent/flow/spec.js +1 -0
  54. package/dist/nodes/process/flow/check.d.ts +40 -2
  55. package/dist/nodes/process/flow/check.js +156 -0
  56. package/dist/nodes/process/flow/factories.d.ts +58 -3
  57. package/dist/nodes/process/flow/factories.js +59 -2
  58. package/dist/nodes/process/flow/node-type.d.ts +4 -1
  59. package/dist/nodes/process/flow/node-type.js +181 -10
  60. package/dist/nodes/process/flow/spec.d.ts +112 -5
  61. package/dist/registry/describe-cli.d.ts +10 -0
  62. package/dist/registry/describe-cli.js +41 -0
  63. package/dist/registry/library-reader.d.ts +12 -2
  64. package/dist/registry/library-reader.js +17 -16
  65. package/dist/registry/lookup-specs.d.ts +17 -1
  66. package/dist/registry/lookup-specs.js +3 -7
  67. package/dist/registry/path-cli.js +2 -1
  68. package/dist/registry/search-cli.js +2 -1
  69. package/dist/serialize.d.ts +9 -54
  70. package/dist/serialize.js +47 -2763
  71. package/package.json +2 -2
  72. /package/dist/{config.d.ts → core/connector-configuration.d.ts} +0 -0
  73. /package/dist/{ixp-definition.json → core/ixp-definition.json} +0 -0
@@ -0,0 +1,873 @@
1
+ import { Expr } from '../../../core/expr.js';
2
+ import { derivedResourceNodeId } from '../../../core/agent-resource-identity.js';
3
+ import { GUID_RE } from '../../../core/binding-messages.js';
4
+ import { prepareCommand } from '../../../core/cli-spelling.js';
5
+ import { nearestName } from '../../../core/edit-distance.js';
6
+ import { FLAT_SEP, flatNameOf, flatNameSegmentsOf } from '../../../core/expr.js';
7
+ import { FORM_FIELD_TYPES, formFieldOptionsProblem, formFieldOptionsWhy, formFieldSettingProblems, formFieldTypeHint, formFieldTypeWhy, validFormFieldTypesText, } from '../../../core/form-field-type.js';
8
+ import { ownEntry } from '../../../core/own-entry.js';
9
+ export const AGENT_DIAGNOSTICS = {
10
+ AGENT_CONTEXT_NO_FOLDER_PATH: { severity: 'warning', summary: 'a context index with no folderPath' },
11
+ AGENT_CONTEXT_NO_ID: { severity: 'error', summary: 'a context index whose id is not a GUID' },
12
+ AGENT_CONTEXT_QUERY_NOT_EXPRESSION: { severity: 'error', summary: 'a context index query that is neither a prompt nor an expression' },
13
+ AGENT_MEMORY_NOT_AUTHORABLE: { severity: 'error', summary: 'an inline agent that attaches memory, which the platform does not serve' },
14
+ CALL_NO_NUMBER: { severity: 'error', summary: 'a createOutgoingCall step with no from or to number' },
15
+ CALL_NUMBER_NOT_E164: { severity: 'warning', summary: 'a createOutgoingCall number that is not E.164' },
16
+ CONV_AGENT_CUSTOM_NO_ID: { severity: 'error', summary: "a conversational agent in 'custom' mode with no conversationId" },
17
+ CONV_AGENT_NO_MODEL: { severity: 'error', summary: 'a conversational agent with no model' },
18
+ CONV_AGENT_NO_PROMPT: { severity: 'error', summary: 'a conversational agent with no systemPrompt' },
19
+ CONV_AGENT_NO_TURN: { severity: 'error', summary: 'a conversational agent whose settings bind nothing' },
20
+ CONV_AGENT_SETTINGS_MODE: { severity: 'error', summary: "a conversational agent whose settings mode is neither 'simple' nor 'custom'" },
21
+ GUARDRAIL_ACTION_UNKNOWN: { severity: 'error', summary: 'a guardrail whose action type is unknown' },
22
+ GUARDRAIL_DUP_ID: { severity: 'error', summary: 'two guardrails with one id' },
23
+ GUARDRAIL_INCOMPLETE: { severity: 'error', summary: 'a guardrail missing a required field' },
24
+ GUARDRAIL_NO_RULES: { severity: 'error', summary: 'a custom guardrail with no rules' },
25
+ GUARDRAIL_NO_SCOPE: { severity: 'error', summary: 'a guardrail whose selector names no scope' },
26
+ GUARDRAIL_NO_VALIDATOR: { severity: 'error', summary: 'a built-in validator guardrail with no validatorType' },
27
+ GUARDRAIL_TYPE_UNKNOWN: { severity: 'error', summary: 'a guardrail whose type is unknown' },
28
+ INLINE_AGENT_COMPOSITE_INPUT: { severity: 'error', summary: 'an inline agent input bound to a computed expression' },
29
+ INLINE_AGENT_EMPTY_RETURNS: { severity: 'warning', summary: 'an inline agent with returns: {}' },
30
+ INLINE_AGENT_ESCALATION_APP_KEY_NOT_GUID: { severity: 'error', summary: 'an app escalation whose app key is not a GUID' },
31
+ INLINE_AGENT_ESCALATION_APP_WITH_FIELDS: { severity: 'error', summary: 'an app escalation that also lists quick-form fields' },
32
+ INLINE_AGENT_ESCALATION_FIELD_OPTIONS: { severity: 'error', summary: 'a quick-form escalation field whose options are invalid' },
33
+ INLINE_AGENT_ESCALATION_FIELD_OPTIONS_IGNORED: { severity: 'warning', summary: 'a quick-form escalation field with options its type ignores' },
34
+ INLINE_AGENT_ESCALATION_FIELD_SETTING_INVALID: { severity: 'error', summary: 'a quick-form escalation field with an invalid setting' },
35
+ INLINE_AGENT_ESCALATION_FIELD_TYPE_UNKNOWN: { severity: 'error', summary: 'a quick-form escalation field of an unknown type' },
36
+ INLINE_AGENT_ESCALATION_NO_APP: { severity: 'error', summary: 'an app escalation with no app' },
37
+ INLINE_AGENT_ESCALATION_NO_FIELDS: { severity: 'error', summary: 'a quick-form escalation with no fields' },
38
+ INLINE_AGENT_ESCALATION_NO_NAME: { severity: 'error', summary: 'an escalation with no name' },
39
+ INLINE_AGENT_ESCALATION_QUICKFORM_WITH_APP: { severity: 'error', summary: 'a quick-form escalation that also names an app' },
40
+ INLINE_AGENT_ESCALATION_VARIANT: { severity: 'error', summary: 'an escalation whose variant is unknown' },
41
+ INLINE_AGENT_INPUT_NAME_AMBIGUOUS: { severity: 'warning', summary: 'two inline agent inputs that reach the platform under one name' },
42
+ INLINE_AGENT_MEMORY_ID_NOT_GUID: { severity: 'error', summary: 'an agent memory whose id is not a GUID' },
43
+ INLINE_AGENT_MEMORY_INCOMPLETE: { severity: 'error', summary: 'an agent memory missing its name or id' },
44
+ INLINE_AGENT_NO_MODEL: { severity: 'error', summary: 'an inline agent with no model' },
45
+ INLINE_AGENT_NO_SYSTEM_PROMPT: { severity: 'error', summary: 'an inline agent with no systemPrompt' },
46
+ INLINE_AGENT_NO_USER_PROMPT: { severity: 'error', summary: 'an inline agent with no userPrompt' },
47
+ INLINE_AGENT_PROMPT_JSON_TEXT: { severity: 'error', summary: 'an inline agent prompt that asks for JSON text' },
48
+ INLINE_AGENT_READ_NOT_OUTPUT: { severity: 'error', summary: 'an inline agent read outside output' },
49
+ INLINE_AGENT_READ_UNKNOWN_FIELD: { severity: 'error', summary: 'an inline agent read of a field returns does not declare' },
50
+ INLINE_AGENT_RESOURCE_NODE_ID: { severity: 'error', summary: "an agent resource nodeId that is blank or another node's" },
51
+ INLINE_AGENT_RESOURCE_SOURCE_DUP: { severity: 'error', summary: 'two agent resources, or two agents, with one source' },
52
+ INLINE_AGENT_RESOURCE_SOURCE_NOT_GUID: { severity: 'error', summary: 'an agent resource source that is not a uuid' },
53
+ INLINE_AGENT_SCAFFOLD_MODEL: { severity: 'warning', summary: "an inline agent on the platform scaffold's model" },
54
+ INLINE_AGENT_SOURCE_NOT_GUID: { severity: 'error', summary: 'an inline agent source that is not a uuid' },
55
+ INLINE_AGENT_TOOL_A2A_INCOMPLETE: { severity: 'error', summary: 'an a2a tool missing its name, slug or key' },
56
+ INLINE_AGENT_TOOL_CLIENTSIDE_NO_NAME: { severity: 'error', summary: 'a client-side tool with no name' },
57
+ INLINE_AGENT_TOOL_CONNECTOR_NO_CONNECTION: { severity: 'error', summary: 'a connector tool with no connection or folder' },
58
+ INLINE_AGENT_TOOL_HTTP_METHOD: { severity: 'error', summary: 'an HTTP-request tool with an unknown method' },
59
+ INLINE_AGENT_TOOL_IXP_NO_NAME: { severity: 'error', summary: 'an IxP tool with no name' },
60
+ INLINE_AGENT_TOOL_IXP_PROJECT_NOT_GUID: { severity: 'error', summary: 'an IxP tool whose projectId is not a uuid' },
61
+ INLINE_AGENT_TOOL_KEY_NOT_GUID: { severity: 'error', summary: 'a process tool whose key is not a GUID' },
62
+ INLINE_AGENT_TOOL_MCP_INCOMPLETE: { severity: 'error', summary: 'an MCP tool missing its name or key' },
63
+ INLINE_AGENT_TOOL_NO_OPERATION: { severity: 'error', summary: 'a connector tool with no connector or operation' },
64
+ INLINE_AGENT_TOOL_NO_RESOURCE_ID: { severity: 'error', summary: 'a process tool with no name or folderPath' },
65
+ INLINE_AGENT_TOOL_UNKNOWN_BUILTIN: { severity: 'error', summary: 'a builtin tool that is not one of the three' },
66
+ INLINE_AGENT_TOOL_UNKNOWN_KIND: { severity: 'error', summary: 'a tool of an unknown kind' },
67
+ INLINE_AGENT_UNDECLARED_PLACEHOLDER: { severity: 'error', summary: 'a prompt placeholder naming no declared input' },
68
+ INLINE_AGENT_UNREFERENCED_INPUT: { severity: 'warning', summary: 'an inline agent input no prompt references' },
69
+ VOICE_AGENT_ITERATIONS: { severity: 'error', summary: 'a voice agent whose maxIterations is outside 1 to 8' },
70
+ VOICE_AGENT_NO_PROMPT: { severity: 'error', summary: 'a voice agent with no systemPrompt' },
71
+ VOICE_CALL_CONTEXT_NOT_OBJECT: { severity: 'error', summary: 'a voice step whose callContext reads a field inside the context' },
72
+ VOICE_MODEL_UNKNOWN: { severity: 'error', summary: "a voice agent naming a voice model outside the platform's catalog" },
73
+ VOICE_NO_CALL_CONTEXT: { severity: 'error', summary: 'an endCall or voice agent step with no callContext' },
74
+ VOICE_PERSONA_UNKNOWN: { severity: 'error', summary: "a voice agent persona that is not one of its model's voices" },
75
+ };
76
+ /**
77
+ * A conversational AGENT step (Phase 5).
78
+ *
79
+ * The turn wiring is the whole risk: `'simple'` mode reads everything out of
80
+ * ONE conversation-context binding, so a flow that names neither the context
81
+ * nor the individual fields deploys an agent with no transcript to answer.
82
+ */
83
+ export function checkConversationalAgent(inputs, ctx) {
84
+ const step = ctx.step;
85
+ const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
86
+ const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
87
+ if (blank(inputs?.model)) {
88
+ at('CONV_AGENT_NO_MODEL', `Conversational agent step "${step}" has no \`model\`, which the node's definition requires. `
89
+ + `\`uip agent model list\` shows what the tenant offers.`, `model: 'gpt-5.4'`);
90
+ }
91
+ if (blank(inputs?.systemPrompt)) {
92
+ at('CONV_AGENT_NO_PROMPT', `Conversational agent step "${step}" has no \`systemPrompt\` — the agent has no instructions for how `
93
+ + `to answer the person.`, `systemPrompt: 'You are a support agent. Be brief.'`);
94
+ }
95
+ const st = inputs?.settings;
96
+ const mode = st?.mode ?? 'simple';
97
+ if (mode !== 'simple' && mode !== 'custom') {
98
+ at('CONV_AGENT_SETTINGS_MODE', `Conversational agent step "${step}"'s settings mode is ${JSON.stringify(mode)}. Valid: 'simple' (one `
99
+ + `conversation-context binding) or 'custom' (bind each field yourself).`);
100
+ return;
101
+ }
102
+ const named = ['context', 'conversationId', 'exchangeId', 'messages', 'userSettings']
103
+ .filter((k) => st !== undefined && !blank(st[k]));
104
+ if (named.length === 0) {
105
+ at('CONV_AGENT_NO_TURN', `Conversational agent step "${step}" binds nothing in \`settings\` — it would run with no transcript and `
106
+ + `no conversation to answer into. In 'simple' mode bind the whole context from a wait or context step.`, `settings: { context: out('<waitStep>', 'conversationContext') }`);
107
+ }
108
+ else if (mode === 'custom' && blank(st?.conversationId)) {
109
+ at('CONV_AGENT_CUSTOM_NO_ID', `Conversational agent step "${step}" binds its turn field-by-field ('custom' mode) but names no `
110
+ + `\`conversationId\` — the one field the platform's own editor marks required, because the reply has `
111
+ + `nowhere to go without it.`, `settings: { mode: 'custom', conversationId: out('start', 'conversationId'), … }`);
112
+ }
113
+ }
114
+ /**
115
+ * A VOICE step (Phase 5D): create-outgoing-call, end-call, or the voice agent.
116
+ *
117
+ * Two things are decidable offline and expensive later: a call needs both
118
+ * numbers in a form the telephony provider accepts, and every voice step needs
119
+ * the `callContext` OBJECT — binding a bare conversation id instead is the
120
+ * classic mistake, because the field is typed `object` and a string passes the
121
+ * file's schema while failing at dispatch.
122
+ */
123
+ export function checkVoice(kind, inputs, ctx) {
124
+ const step = ctx.step;
125
+ const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
126
+ const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
127
+ if (kind === 'createOutgoingCall') {
128
+ for (const field of ['from', 'to']) {
129
+ const raw = inputs?.[field];
130
+ if (blank(raw)) {
131
+ at('CALL_NO_NUMBER', `createOutgoingCall step "${step}" has no \`${field}\`, which the node's schema requires — `
132
+ + `${field === 'from' ? 'the provisioned number the call comes from' : 'the number to dial'}.`, `${field}: '+15551234567'`);
133
+ continue;
134
+ }
135
+ // A literal number is checkable; an expression is the caller's business.
136
+ if (typeof raw === 'string' && !/^\+[1-9]\d{6,14}$/.test(raw.trim())) {
137
+ at('CALL_NUMBER_NOT_E164', `createOutgoingCall step "${step}"'s \`${field}\` is ${JSON.stringify(raw)}, which is not `
138
+ + `E.164 (a leading '+' and 7–15 digits). Telephony providers reject anything else at dispatch, `
139
+ + `so a run fails after the flow is deployed.`, `${field}: '+15551234567'`);
140
+ }
141
+ }
142
+ return;
143
+ }
144
+ // end-call and the voice agent are both keyed by the live call.
145
+ if (blank(inputs?.callContext)) {
146
+ at('VOICE_NO_CALL_CONTEXT', `${kind === 'endCall' ? 'endCall' : 'voiceAgent'} step "${step}" has no \`callContext\`, which the node's `
147
+ + `schema requires — it identifies the live call. The incoming-call trigger publishes it as `
148
+ + `out('start', 'callContext'), and createOutgoingCall as out('<dialStep>', 'callContext').`, `callContext: out('start', 'callContext')`);
149
+ }
150
+ else {
151
+ const cc = inputs.callContext;
152
+ const js = cc instanceof Expr && !cc.literal ? cc.js : undefined;
153
+ // The field is the call-context OBJECT. A read that ends at a scalar inside
154
+ // it (`…callContext.conversationId`, or a bare conversation id) passes the
155
+ // file's schema and then fails at dispatch.
156
+ if (js !== undefined && /\.(conversationId|id)\s*$/.test(js)) {
157
+ at('VOICE_CALL_CONTEXT_NOT_OBJECT', `${kind === 'endCall' ? 'endCall' : 'voiceAgent'} step "${step}"'s \`callContext\` reads `
158
+ + `"${js.trim()}", which is a field INSIDE the call context rather than the context object itself. `
159
+ + `The node's schema types it \`object\`; a scalar passes the file's schema and fails at dispatch.`, `callContext: out('start', 'callContext')`);
160
+ }
161
+ }
162
+ if (kind === 'voiceAgent') {
163
+ if (blank(inputs?.systemPrompt)) {
164
+ at('VOICE_AGENT_NO_PROMPT', `voiceAgent step "${step}" has no \`systemPrompt\` — a caller is waiting, and the agent has no `
165
+ + `instructions for what to say.`, `systemPrompt: 'Greet the caller and find out why they called.'`);
166
+ }
167
+ const mi = inputs?.maxIterations;
168
+ if (mi !== undefined && !(mi instanceof Expr)) {
169
+ const n = Number(mi);
170
+ if (!Number.isInteger(n) || n < 1 || n > 8) {
171
+ at('VOICE_AGENT_ITERATIONS', `voiceAgent step "${step}"'s \`maxIterations\` is ${JSON.stringify(mi)}. The node's schema allows `
172
+ + `1 to 8 — a tighter ceiling than a text agent's, because the caller is waiting on the turn.`, `maxIterations: 4`);
173
+ }
174
+ }
175
+ const v = inputs?.voice;
176
+ if (v !== undefined) {
177
+ const PERSONAS = {
178
+ 'gemini-3.1-flash-live-preview': ['Aoede', 'Charon', 'Fenrir', 'Kore', 'Leda', 'Orus', 'Puck', 'Zephyr'],
179
+ 'gpt-realtime-2': ['alloy', 'ash', 'ballad', 'coral', 'echo', 'sage', 'shimmer', 'verse'],
180
+ };
181
+ const model = typeof v.model === 'string' ? v.model : undefined;
182
+ // An own-key read: `in` also matched 'toString', and `PERSONAS[model].includes` then threw.
183
+ const personas = model !== undefined ? ownEntry(PERSONAS, model) : undefined;
184
+ if (model !== undefined && personas === undefined) {
185
+ at('VOICE_MODEL_UNKNOWN', `voiceAgent step "${step}" names the voice model ${JSON.stringify(model)}. The platform's catalog `
186
+ + `today is ${Object.keys(PERSONAS).map((m) => `'${m}'`).join(' and ')} — a voice model, not one of `
187
+ + `the text models an inline agent takes.`);
188
+ }
189
+ else if (typeof v.persona === 'string' && personas !== undefined && !personas.includes(v.persona)) {
190
+ at('VOICE_PERSONA_UNKNOWN', `voiceAgent step "${step}"'s persona ${JSON.stringify(v.persona)} is not one of `
191
+ + `${JSON.stringify(model)}'s voices (${personas.join(', ')}). A persona belongs to its model.`);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ // ─── an inline agent's prompt asking for JSON text ───────────────────────────────
197
+ //
198
+ // The phrase table behind `INLINE_AGENT_PROMPT_JSON_TEXT`. It is kept narrow on
199
+ // purpose, because the rule is an error. Three shapes count, and each skips a
200
+ // negation anywhere earlier in the same clause ("Do not return JSON", "Do not wrap
201
+ // the answer in JSON", "Return the items, not as JSON"):
202
+ //
203
+ // - DIRECT: an output verb, then only words from a closed qualifier list, then
204
+ // `json` — "Return ONLY a JSON object", "Return only the typed JSON contract".
205
+ // - VIA: an output verb, up to two words, then `as` / `in` / `with`, qualifiers,
206
+ // `json` — "Format your answer as JSON", "Respond with valid JSON".
207
+ // - BRACE: a quoted-key brace whose key is one of the step's own `returns` keys —
208
+ // `Return ONLY {"category","priority"}` next to `returns: { category, priority }`.
209
+ //
210
+ // A word outside the qualifier list ends a DIRECT match, so JSON as DATA does not
211
+ // count in the DIRECT form: "Answer the question using the JSON data below", "Give
212
+ // a summary of the JSON ticket", "Return the invoice number from the JSON". The VIA
213
+ // form allows two free words, so it can still fire on JSON as a topic ("Answer in
214
+ // Spanish with JSON examples"); the tail skips the common data nouns after `json`
215
+ // (keys, data, record, examples, input) and the JSON-family FORMATS a string field
216
+ // legitimately holds (JSON Schema, JSON Patch, JSON-LD, JSON Lines, …). `write` is
217
+ // not a verb here: "You help users write JSON" is a topic, not an answer shape.
218
+ //
219
+ // The skills repo's preview-doc gate (tests/tasks/uipath-maestro-flow/_shared/
220
+ // test_preview_inline_agent_prompts.py) ports this table to Python and loads a
221
+ // byte-identical copy of tests/fixtures/inline-agent-prompt-vectors.json, so
222
+ // change the table, the fixture and the port together.
223
+ const JSON_TEXT_VERBS = 'return|respond|reply|output|answer|produce|emit|format|give|provide';
224
+ /** Words that are only ever qualifiers of the answer's shape, never the thing it is about. */
225
+ const JSON_TEXT_QUALIFIERS = [
226
+ 'only', 'just', 'exactly', 'strictly', 'solely', 'simply', 'a', 'an', 'one', 'single', 'the',
227
+ 'your', 'its', 'this', 'that', 'valid', 'raw', 'pure', 'plain', 'strict', 'compact', 'minified',
228
+ 'well-formed', 'properly', 'formatted', 'typed', 'declared', 'structured', 'following', 'same',
229
+ 'final', 'back', 'proper', 'correct', 'machine-readable', 'parseable', 'parsable',
230
+ ];
231
+ /** A negation anywhere earlier in the same clause, or anywhere in the match's gap. */
232
+ const JSON_TEXT_NEGATIONS = new Set(['not', 'never', 'no', 'dont', 'without', 'rather', 'instead', 'avoid']);
233
+ const isJsonTextNegation = (w) => JSON_TEXT_NEGATIONS.has(w) || w.endsWith("n't") || w.endsWith('n’t');
234
+ const JSON_TEXT_WORD = "[A-Za-z][\\w'’-]*";
235
+ /** Words are separated by spaces, tabs or a comma — never a newline or a full stop. */
236
+ const JSON_TEXT_SEP = '[ \\t]*,?[ \\t]+';
237
+ const JSON_TEXT_QUAL = `(?:${[...JSON_TEXT_QUALIFIERS].sort((a, b) => b.length - a.length).join('|')})\\b`;
238
+ // `(?<![-\w])` skips `--output json`; `(?!\.\w)` skips `JSON.parse` / `JSON.stringify`;
239
+ // `(?!-(?:ld|rpc)\b)` skips JSON-LD / JSON-RPC (`\b` matches before the hyphen);
240
+ // the last lookahead skips "the JSON below" / "the JSON provided" / "the JSON you
241
+ // get", data nouns ("the JSON keys", "JSON examples") and JSON-family formats
242
+ // ("JSON Schema", "JSON Patch", "the JSON path", "a JSON Web Token").
243
+ const JSON_TEXT_TAIL = `json\\b(?!\\.\\w)(?!-(?:ld|rpc)\\b)(?![ \\t]+(?:below|above|provided|given|you`
244
+ + `|keys?|data|records?|examples?|inputs?`
245
+ + `|schemas?|patch|path|pointer|lines|rpc|web[ \\t]+tokens?)\\b)`;
246
+ const JSON_TEXT_VERB = `(?<![-\\w])(?:${JSON_TEXT_VERBS})\\b`;
247
+ const JSON_TEXT_DIRECT = new RegExp(`${JSON_TEXT_VERB}((?:${JSON_TEXT_SEP}${JSON_TEXT_QUAL}){0,5})${JSON_TEXT_SEP}${JSON_TEXT_TAIL}`, 'gi');
248
+ const JSON_TEXT_VIA = new RegExp(`${JSON_TEXT_VERB}((?:${JSON_TEXT_SEP}${JSON_TEXT_WORD}){0,2}?)${JSON_TEXT_SEP}(?:as|in|with)`
249
+ + `((?:${JSON_TEXT_SEP}${JSON_TEXT_QUAL}){0,4})${JSON_TEXT_SEP}${JSON_TEXT_TAIL}`, 'gi');
250
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
251
+ /**
252
+ * The phrase in `text` that tells a model to answer in JSON TEXT, or `undefined`.
253
+ *
254
+ * `check` uses it for `INLINE_AGENT_PROMPT_JSON_TEXT`, passing the step's `returns`
255
+ * keys so the brace form only counts a brace that spells one of them. Without
256
+ * `returnKeys` (the example and doc sweeps) any quoted identifier key counts.
257
+ * `{{input.*}}` placeholders are removed first, so an input named `json` never
258
+ * matches.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * jsonTextDirective('Return ONLY a JSON object with keys "subject" and "body".'); // 'Return ONLY a JSON'
263
+ * jsonTextDirective('Return ONLY {"category","priority"}.', ['category']); // '{"category",'
264
+ * jsonTextDirective('Return a result conforming to the output schema.'); // undefined
265
+ * jsonTextDirective('Do not return JSON.'); // undefined
266
+ * jsonTextDirective('Answer the question using the JSON data below.'); // undefined
267
+ * ```
268
+ */
269
+ export function jsonTextDirective(text, returnKeys) {
270
+ if (typeof text !== 'string' || text === '')
271
+ return undefined;
272
+ // A placeholder becomes a hard break (`·` is neither a word nor a gap), and a
273
+ // quoted or back-ticked `JSON` is read as the bare word.
274
+ const t = text
275
+ .replace(/\{\{\s*input\.[^}]*\}\}/g, ' · ')
276
+ .replace(/[`"'“”](json)[`"'“”]/gi, ' $1 ');
277
+ const words = (s) => (s.match(new RegExp(JSON_TEXT_WORD, 'g')) ?? []).map((w) => w.toLowerCase());
278
+ const keys = returnKeys?.length ? returnKeys.map(escapeRegExp).join('|') : '[A-Za-z_]\\w*';
279
+ const brace = new RegExp(`\\{[ \\t]*["'](?:${keys})["'][ \\t]*[,:}]`, 'g');
280
+ for (const rule of [JSON_TEXT_DIRECT, JSON_TEXT_VIA, brace]) {
281
+ rule.lastIndex = 0;
282
+ for (const m of t.matchAll(rule)) {
283
+ if (m.slice(1).some((g) => words(g ?? '').some(isJsonTextNegation)))
284
+ continue;
285
+ // Any negation earlier in the same clause: "Do not wrap the answer in JSON",
286
+ // "No need to return JSON". A comma ends a clause, so "Do not guess, return
287
+ // ONLY a JSON object" still counts.
288
+ const clause = t.slice(0, m.index).split(/[.;:!?\n,]/).pop() ?? '';
289
+ if (words(clause).some(isJsonTextNegation))
290
+ continue;
291
+ return m[0].trim();
292
+ }
293
+ }
294
+ return undefined;
295
+ }
296
+ /**
297
+ * Check an `inlineAgent(...)` call.
298
+ *
299
+ * The interesting difference from every other node this SDK authors: **nothing here
300
+ * lives on a tenant.** An inline agent's whole contract is in the call, so these
301
+ * rules can be certain rather than best-effort — the definition's own `required`
302
+ * list is checkable, and so is whether the prompts and the declared variables agree.
303
+ *
304
+ * Three rules exist because the PLATFORM does something with what you wrote:
305
+ *
306
+ * - `INLINE_AGENT_SCAFFOLD_MODEL` — the model the platform's own scaffold ships is
307
+ * stale, and v1's smoke check for this node type fails a flow that keeps it. A
308
+ * warning, not an error: it is a real model id, it just should not be a default
309
+ * nobody chose.
310
+ * - `INLINE_AGENT_UNREFERENCED_INPUT` — flow-v1 derives the deployed input list by
311
+ * scanning the PROMPTS for `$vars.*` references and only falls back to the
312
+ * authored `agentInputVariables` when that pre-pass did not run. So a declared
313
+ * input the prompts never mention is pruned deployed while arriving locally: a
314
+ * disagreement between rungs and the platform, which is the one class that has to
315
+ * be said out loud rather than left to a run.
316
+ * - `INLINE_AGENT_PROMPT_JSON_TEXT` — the platform returns `returns` as a typed
317
+ * object: it becomes the `agent.json` `outputSchema`, and the agent runtime ends
318
+ * a run with a structured call that fills exactly those fields. A prompt that
319
+ * ALSO asks for JSON text makes the model pack its whole answer, serialized, into
320
+ * one string field. See {@link jsonTextDirective}.
321
+ */
322
+ export function checkInlineAgent(inputs, ctx) {
323
+ const step = ctx.step;
324
+ const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
325
+ const warn = (code, message) => ctx.report(code, message);
326
+ const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
327
+ // `memory` is authorable in this SDK and NOT authorable on the platform. The
328
+ // bundled `uipath.agent.autonomous@1.4` carries a `memory` handle added by
329
+ // #431; the tenant's own manifest for that node type declares only
330
+ // `escalation, context, tool, success, error`, and the registry index (4977
331
+ // entries, 2026-09-09) contains NO `uipath.agent.resource.memory.*` type at
332
+ // all — so both halves of what `memory` emits are unknown to the platform.
333
+ // `context` is not the alternative: those resources are knowledge INDEXES
334
+ // (`…context.index.<slug>.<guid>`), i.e. context grounding, not episodic memory.
335
+ //
336
+ // Compile succeeds and `uip maestro flow validate` then hard-refuses the edge:
337
+ //
338
+ // [edges[edge_0].sourcePort] Edge references source handle "memory" on node
339
+ // "triage", but the current manifest for "uipath.agent.autonomous" does not
340
+ // declare that handle … Retry: RetryWillNotFix
341
+ //
342
+ // An artifact that compiles and can never validate is the worst of the two
343
+ // failures, so this is said here — before the emit — rather than left to a rung.
344
+ // It is an ERROR because there is no spelling that works today, and `--no-check`
345
+ // remains the escape for a tenant whose manifest does declare the handle.
346
+ if (inputs?.memory !== undefined) {
347
+ at('AGENT_MEMORY_NOT_AUTHORABLE', `Inline agent step "${step}" attaches \`memory\`, which the platform cannot resolve: the tenant's `
348
+ + `\`uipath.agent.autonomous\` manifest declares no \`memory\` handle (only escalation, context, tool, `
349
+ + `success, error) and the registry serves no \`uipath.agent.resource.memory.*\` node type. The flow `
350
+ + `compiles and then fails \`uip maestro flow validate\` as RetryWillNotFix. Drop \`memory\`; for `
351
+ + `retrieval over a knowledge source use a context index instead (\`tools\`/context grounding). `
352
+ + `Pass --no-check only on a tenant whose manifest does declare the handle.`);
353
+ }
354
+ // A context `query` is what the RUNTIME searches with, and it is not an
355
+ // expression slot. A plain string is the model-invoked `dynamic` variant: the
356
+ // text becomes the description the model reads before it writes its own query.
357
+ // An expression renders as the `static` variant instead, whose `value` the
358
+ // retriever is handed VERBATIM — there is no `$vars` scope inside the agent and
359
+ // nothing templates it once `uip agent refresh` has stripped the node, so the
360
+ // search runs on the literal text `=js:$vars…`. With the default `threshold: 0`
361
+ // it still returns chunks, so it reads as working while retrieving on nonsense.
362
+ // Refused rather than emitted until the argument-bound form is implemented
363
+ // (`{ variant: 'argument' }`, which the runtime resolves from agent state).
364
+ const contexts = inputs?.context === undefined ? []
365
+ : Array.isArray(inputs.context) ? inputs.context : [inputs.context];
366
+ for (const cx of contexts) {
367
+ if (cx?.query !== undefined && typeof cx.query !== 'string') {
368
+ at('AGENT_CONTEXT_QUERY_NOT_EXPRESSION', `Inline agent step "${step}" gives its context index "${cx.name ?? ''}" an EXPRESSION \`query\`. `
369
+ + `The runtime searches with that value literally — it is not evaluated inside the agent — so `
370
+ + `retrieval runs on the text of the expression and quietly returns unrelated chunks. Use a plain `
371
+ + `string: it becomes the guidance the model reads before writing its own query, which is what a `
372
+ + `run-dependent search needs.`);
373
+ }
374
+ }
375
+ // The index id is half of the resource node's TYPE
376
+ // (`…context.index.<slug>.<id>`), and the platform resolves the index by it.
377
+ // A blank, padded or non-GUID id builds a type no tenant serves, and nothing
378
+ // downstream says so: the flow compiles, merges and validates, and the agent
379
+ // runs ungrounded. So it is an error, not a warning. The id is tested as
380
+ // written, not trimmed, because serialize emits it as written.
381
+ for (const cx of contexts) {
382
+ const id = typeof cx?.id === 'string' ? cx.id : '';
383
+ if (!GUID_RE.test(id)) {
384
+ at('AGENT_CONTEXT_NO_ID', `Inline agent step "${step}" grounds on context index "${cx?.name ?? ''}" with `
385
+ + (id.trim() === '' ? 'no `id`'
386
+ : GUID_RE.test(id.trim()) ? `\`id\` ${JSON.stringify(cx?.id)}, which has whitespace around the GUID`
387
+ : `\`id\` ${JSON.stringify(cx?.id)}, which is not a GUID`) + '. '
388
+ + `The id is the index's key: it is the tail of the resource node's type and how the runtime finds `
389
+ + `the index, so without a real one the agent runs ungrounded and \`validate\` does not notice. `
390
+ + `\`uip solution resources list --kind Index --source remote --search "<name>"\` returns it as \`Key\`.`);
391
+ }
392
+ }
393
+ // An index outside the deployment folder needs its folder named. The resource
394
+ // file carries no `indexId`, so `indexName` + `folderPath` is the whole locator,
395
+ // and `@uipath/tool-agent`'s `createIndexBinding` leaves the folder off the
396
+ // binding when `folderPath` is empty. A WARNING, not an error: an index in the
397
+ // deployment folder resolves without it.
398
+ for (const cx of contexts) {
399
+ if (cx != null && cx.folderPath === undefined) {
400
+ warn('AGENT_CONTEXT_NO_FOLDER_PATH', `Inline agent step "${step}" grounds on context index "${cx.name ?? ''}" without a \`folderPath\`. `
401
+ + `The emitted resource carries no index id, so the folder is half of how the runtime finds the `
402
+ + `index, and the binding is written without one. Give the index's folder — `
403
+ + `\`uip solution resources list --kind Index --source remote --search "<name>"\` returns it.`);
404
+ }
405
+ }
406
+ // The definition's own `required`: systemPrompt, userPrompt, model — each with
407
+ // `minLength: 1`, so a blank string is as invalid as a missing key.
408
+ if (blank(inputs?.model)) {
409
+ at('INLINE_AGENT_NO_MODEL', `Inline agent step "${step}" has no \`model\`. The node's definition requires one (\`minLength: 1\`), so `
410
+ + `a flow without it does not validate. \`uip agent model list\` shows what the tenant offers.`, `model: 'gpt-5.4'`);
411
+ }
412
+ if (blank(inputs?.systemPrompt)) {
413
+ at('INLINE_AGENT_NO_SYSTEM_PROMPT', `Inline agent step "${step}" has no \`systemPrompt\`. The node's definition requires one `
414
+ + `(\`minLength: 1\`). It is where the agent is told what it is and what shape to answer in.`);
415
+ }
416
+ if (blank(inputs?.userPrompt)) {
417
+ at('INLINE_AGENT_NO_USER_PROMPT', `Inline agent step "${step}" has no \`userPrompt\`. The node's definition requires one `
418
+ + `(\`minLength: 1\`). It is the per-run request.`);
419
+ }
420
+ const model = typeof inputs?.model === 'string' ? inputs.model.trim() : '';
421
+ if (model === 'gpt-4o-2024-11-20') {
422
+ warn('INLINE_AGENT_SCAFFOLD_MODEL', `Inline agent step "${step}" uses \`gpt-4o-2024-11-20\` — the model the platform's own scaffold ships, `
423
+ + `and the one v1's smoke check for this node type FAILS a flow for keeping (it is the definition's `
424
+ + `\`inputDefaults.model\`, not a choice). Pick a current model: \`uip agent model list\`.`);
425
+ }
426
+ // Where each resource node sits (`nodeId`, `source`): decompile writes them
427
+ // for a node the canvas placed, and compile writes them as given. A blank or
428
+ // repeated id would make compile refuse the flow or overwrite another node,
429
+ // and a `source` that is not a uuid names a directory the platform does not
430
+ // expect (the agent's own `source` has the same rule).
431
+ const asList = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
432
+ const placed = [
433
+ ...asList(inputs?.context).map((r) => ({ what: `context index "${r?.name ?? ''}"`, kind: 'Context', nodeId: r?.nodeId, source: r?.source })),
434
+ ...asList(inputs?.tools).map((r) => ({ what: `${r?.kind ?? ''} tool`, kind: 'Tool', nodeId: r?.nodeId, source: r?.source })),
435
+ ...asList(inputs?.escalation).map((r) => ({ what: `escalation "${r?.name ?? ''}"`, kind: 'Escalation', nodeId: r?.nodeId, source: r?.source })),
436
+ ];
437
+ // The ids compile derives for the resources with no `nodeId`: the n-th one of
438
+ // a kind gets `<step><Kind>`, then `<step><Kind>2`, ... An explicit id equal to
439
+ // one of them makes compile refuse the flow, or rename the derived node.
440
+ for (const kind of ['Context', 'Tool', 'Escalation']) {
441
+ const derived = placed.filter((r) => r.kind === kind && r.nodeId === undefined);
442
+ derived.forEach((r, n) => {
443
+ const id = derivedResourceNodeId(step, kind, n);
444
+ ctx.resources.derivedNodeId(id, `the id compile derives for the ${r.what} of inline agent step "${step}"`);
445
+ });
446
+ }
447
+ /** Explicit `source` -> the resource on THIS agent that has it: two would write one resource.json. */
448
+ const sources = new Map();
449
+ for (const r of placed) {
450
+ if (r.nodeId !== undefined) {
451
+ const id = typeof r.nodeId === 'string' ? r.nodeId : '';
452
+ let other;
453
+ if (id.trim() === '') {
454
+ at('INLINE_AGENT_RESOURCE_NODE_ID', `Inline agent step "${step}" gives its ${r.what} the \`nodeId\` ${JSON.stringify(r.nodeId)}, which is `
455
+ + `not a node id. Omit \`nodeId\` to derive one, or give the id the node has in the flow.`);
456
+ }
457
+ else if ((other = ctx.resources.claimNodeId(id, r.what)) !== undefined) {
458
+ at('INLINE_AGENT_RESOURCE_NODE_ID', `Inline agent step "${step}" gives its ${r.what} the \`nodeId\` "${id}", which the ${other.what} `
459
+ + `(on step "${other.step}") already has. Node ids are unique in a flow.`);
460
+ }
461
+ }
462
+ if (r.source === undefined)
463
+ continue;
464
+ // Not trimmed: compile writes `source` as given, into `inputs.source` and the directory name.
465
+ if (!GUID_RE.test(String(r.source))) {
466
+ at('INLINE_AGENT_RESOURCE_SOURCE_NOT_GUID', `Inline agent step "${step}" gives its ${r.what} the \`source\` ${JSON.stringify(r.source)}, which is not `
467
+ + `a uuid${/^\s|\s$/.test(String(r.source)) ? ' (it has spaces around it)' : ''}. \`source\` is the resource's `
468
+ + `directory under the agent's own (\`resources/<source>/resource.json\`) and the node's \`inputs.source\`. `
469
+ + 'Omit it and one is derived from the flow id, the step and the resource.');
470
+ continue;
471
+ }
472
+ const key = String(r.source).toLowerCase();
473
+ const first = sources.get(key);
474
+ if (first !== undefined) {
475
+ at('INLINE_AGENT_RESOURCE_SOURCE_DUP', `Inline agent step "${step}" gives its ${first} and its ${r.what} the same \`source\` "${String(r.source)}". `
476
+ + 'Both would write `resources/<source>/resource.json` under this agent, and the second file would replace '
477
+ + 'the first. Give each resource its own `source`, or omit `source` to derive one.');
478
+ }
479
+ else {
480
+ sources.set(key, r.what);
481
+ }
482
+ }
483
+ if (typeof inputs?.source === 'string' && GUID_RE.test(inputs.source.trim())) {
484
+ const other = ctx.resources.claimFolder(inputs.source.trim());
485
+ if (other !== undefined) {
486
+ at('INLINE_AGENT_RESOURCE_SOURCE_DUP', `Inline agent steps "${other}" and "${step}" have the same \`source\` "${inputs.source}". Both would write `
487
+ + '`<source>/agent.json`, and the second file would replace the first. Give each agent its own `source`, '
488
+ + 'or omit it to derive one.');
489
+ }
490
+ }
491
+ if (inputs?.source !== undefined && !GUID_RE.test(String(inputs.source).trim())) {
492
+ at('INLINE_AGENT_SOURCE_NOT_GUID', `Inline agent step "${step}"'s \`source\` is ${JSON.stringify(inputs.source)}, which is not a uuid. `
493
+ + `\`source\` is the agent's own DIRECTORY name, and the platform resolves its configuration at `
494
+ + `"<source>/agent.json" — a non-uuid name is not what the designer or the runtime expect. Omit it and `
495
+ + `one is derived from the flow id and step name (stable across recompiles).`);
496
+ }
497
+ if (inputs?.returns && Object.keys(inputs.returns).length === 0) {
498
+ warn('INLINE_AGENT_EMPTY_RETURNS', `Inline agent step "${step}" declares \`returns: {}\`. That is not "no output" — it falls back to the `
499
+ + `definition's default, \`{ content: 'string' }\`, which is the scaffold shape. Declare the fields the `
500
+ + `agent must return, or drop the key and read \`content\`.`);
501
+ }
502
+ // A prompt that asks for JSON TEXT while `returns` declares the fields. The
503
+ // platform already returns those fields as a typed object — `returns` is the
504
+ // agent.json `outputSchema`, and the runtime ends a run with a structured call
505
+ // that fills exactly those keys — so the extra directive does not shape the
506
+ // answer, it gets serialized INTO it: on 2026-09-23 a writer declaring
507
+ // `{ subject, body }` came back with `body` = '{"subject":…,"body":…}' in 3 of 3
508
+ // debug runs, and the flow mailed the JSON. `uip maestro flow validate` accepts
509
+ // the prompt (it never reads prompt meaning), so this is where it is said. An
510
+ // ERROR for the same reason `INLINE_AGENT_UNDECLARED_PLACEHOLDER` is one: the
511
+ // platform accepts the shape and the run still delivers the wrong value, and a
512
+ // warning is printed past while compile writes the .flow anyway. Only when
513
+ // `returns` is declared: with the `{ content }` default, JSON text in `content`
514
+ // may be exactly what the author reads back.
515
+ const returnKeys = Object.keys(inputs?.returns ?? {});
516
+ if (returnKeys.length) {
517
+ for (const field of ['systemPrompt', 'userPrompt']) {
518
+ const phrase = jsonTextDirective(String(inputs?.[field] ?? ''), returnKeys);
519
+ if (phrase === undefined)
520
+ continue;
521
+ const rewrite = `Return a result conforming to the output schema. `
522
+ + returnKeys.map((k) => `${k}: <what it holds>.`).join(' ');
523
+ at('INLINE_AGENT_PROMPT_JSON_TEXT', `Inline agent step "${step}"'s \`${field}\` tells the model to answer in JSON text ("${phrase}"), `
524
+ + `but \`returns\` already declares the answer: ${returnKeys.map((k) => `\`${k}\``).join(', ')}. `
525
+ + `The platform returns those fields as a typed object — \`returns\` becomes the agent.json `
526
+ + `\`outputSchema\`, and the agent runtime ends each run with a structured call that fills exactly `
527
+ + `those keys — so \`out('${step}', '${returnKeys[0]}')\` reads a value, not text to parse. Asking `
528
+ + `for JSON as well makes the model pack its whole answer, serialized, into one string field (for `
529
+ + `example a \`body\` that reads '{"subject":…,"body":…}'). Replace only that JSON instruction (keep the rest `
530
+ + `of the prompt) with one line per field, in the platform's own wording: "${rewrite}" Declare a field \`'object'\` or `
531
+ + `\`'array'\` if it should carry structure. \`uip maestro flow validate\` accepts this prompt, so `
532
+ + `nothing later catches it. If this is a false positive (the prompt only mentions JSON as data), `
533
+ + `compile with --no-check.`);
534
+ }
535
+ }
536
+ // The prompts-vs-variables agreement, in both directions. Both prompts are
537
+ // searched, because flow-v1's pre-pass scans both.
538
+ const prompts = `${String(inputs?.systemPrompt ?? '')}\n${String(inputs?.userPrompt ?? '')}`;
539
+ const placeholders = [...prompts.matchAll(/\{\{\s*input\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\}\}/g)]
540
+ .map((m) => m[1]);
541
+ const declaredInputs = Object.keys(inputs?.inputs ?? {});
542
+ // Direction 1: a declared input no prompt references. The platform may prune it,
543
+ // in which case the agent receives nothing and the authored list sits unused.
544
+ const unreferenced = declaredInputs.filter((k) => !placeholders.includes(k));
545
+ if (declaredInputs.length && unreferenced.length === declaredInputs.length) {
546
+ warn('INLINE_AGENT_UNREFERENCED_INPUT', `Inline agent step "${step}" declares input(s) ${unreferenced.map((u) => `\`${u}\``).join(', ')} that `
547
+ + `neither prompt references as \`{{input.<name>}}\` — so NONE of them is referenced. Deployed, the `
548
+ + `authored list is not necessarily what the platform sends: flow-v1's `
549
+ + `\`preDeriveAgentInputDefinitions\` pre-pass builds the input list by scanning the node's prompts for `
550
+ + `variable references, merges what it finds with the authored list, and DROPS an entry no reference `
551
+ + `points at when its caller asks for pruning. A referenced input survives every path; an unreferenced `
552
+ + `one survives only some, and the failure mode is an agent that arrives with no arguments at all. `
553
+ + `Write \`{{input.${declaredInputs[0]}}}\` in a prompt — which is also how the agent gets told what it `
554
+ + `is looking at.`);
555
+ }
556
+ else if (unreferenced.length) {
557
+ warn('INLINE_AGENT_UNREFERENCED_INPUT', `Inline agent step "${step}" declares input(s) ${unreferenced.map((u) => `\`${u}\``).join(', ')} that `
558
+ + `neither prompt references as \`{{input.<name>}}\`. The platform may prune an unreferenced input, so `
559
+ + `it is not reliably sent — and an input the prompts never mention is one the agent cannot use anyway.`);
560
+ }
561
+ // Direction 2: a placeholder naming an input that does not exist. The compiler
562
+ // leaves it alone rather than deleting it, so it reaches the model as the literal
563
+ // text `{{input.x}}` — visible, but not what anyone meant.
564
+ const undeclared = [...new Set(placeholders.filter((p) => !declaredInputs.includes(p)))];
565
+ if (undeclared.length) {
566
+ at('INLINE_AGENT_UNDECLARED_PLACEHOLDER', `Inline agent step "${step}" references ${undeclared.map((u) => `\`{{input.${u}}}\``).join(', ')} in a `
567
+ + `prompt, but \`inputs\` declares ${declaredInputs.length ? declaredInputs.map((d) => `\`${d}\``).join(', ') : 'nothing'}. `
568
+ + `An unmatched placeholder is not substituted — it reaches the model as that literal text, and the `
569
+ + `value it was meant to carry never arrives.`, declaredInputs.length
570
+ ? `inputs: { ${undeclared[0]}: input('${undeclared[0]}'), … }`
571
+ : `inputs: { ${undeclared[0]}: input('${undeclared[0]}') }`);
572
+ }
573
+ // Direction 3: an input bound to a COMPUTED EXPRESSION rather than to one
574
+ // reference. The platform names each of an agent's arguments after the
575
+ // reference path it is bound to (the shell node's `agentInputVariables`), so
576
+ // an expression spanning zero or several references has no single argument —
577
+ // and therefore no name a `{{input.<x>}}` token could address. The compiler
578
+ // declares no argument for it, so its value does not reach the agent, and the
579
+ // placeholder reaches the model as literal text. An error rather than a
580
+ // warning, and for the same reason `INLINE_AGENT_UNDECLARED_PLACEHOLDER` is
581
+ // one: the value never arrives.
582
+ for (const [name, val] of Object.entries(inputs?.inputs ?? {})) {
583
+ if (!(val instanceof Expr) || val.literal)
584
+ continue;
585
+ if (flatNameOf(val.js) !== undefined)
586
+ continue;
587
+ if (!placeholders.includes(name))
588
+ continue;
589
+ at('INLINE_AGENT_COMPOSITE_INPUT', `Inline agent step "${step}"'s input \`${name}\` is bound to a computed expression `
590
+ + `(\`${val.js.length > 60 ? `${val.js.slice(0, 60)}…` : val.js}\`), and a prompt references it as `
591
+ + `\`{{input.${name}}}\`. The platform does not send the agent the names you declared: it names each `
592
+ + `argument after the REFERENCE PATH the input is bound to (\`$vars.a.output.b\` → \`a__output__b\`). `
593
+ + `A computed expression is not one reference, so it gets no argument: its value does not reach the `
594
+ + `agent, and the placeholder reaches the model as that literal text.`, `compute it in a script step first, then bind the step's output: `
595
+ + `.step('${name}Value', script({ code: 'return …;' })) … inputs: { ${name}: out('${name}Value') }`);
596
+ }
597
+ // A step id containing `__` makes the flat name AMBIGUOUS on the platform: the
598
+ // encoding joins path segments with `__` and v1's own inverse (`decodeFlatName`)
599
+ // documents that it round-trips only while ids "disallow `__`". A warning, not
600
+ // an error — the argument is still SENT and our token still matches it, so the
601
+ // agent works; what breaks is anything that decodes the name back to a path
602
+ // (the CLI's `hydrateInlineAgentInputs` reconcile, and the designer).
603
+ for (const [name, val] of Object.entries(inputs?.inputs ?? {})) {
604
+ if (!(val instanceof Expr) || val.literal)
605
+ continue;
606
+ const segments = flatNameSegmentsOf(val.js);
607
+ const offender = segments?.find((s) => s.includes(FLAT_SEP));
608
+ if (!offender)
609
+ continue;
610
+ warn('INLINE_AGENT_INPUT_NAME_AMBIGUOUS', `Inline agent step "${step}"'s input \`${name}\` reads \`${val.js}\`, whose path segment `
611
+ + `\`${offender}\` contains \`${FLAT_SEP}\`. The platform names the argument by joining the path's `
612
+ + `segments with \`${FLAT_SEP}\` (\`${segments.join(FLAT_SEP)}\`), so that name no longer says where `
613
+ + `the value came from — v1's own inverse round-trips only while ids avoid \`${FLAT_SEP}\`. The `
614
+ + `argument is still sent and the prompt still reads it; what breaks is anything decoding the name `
615
+ + `back to a path. Rename the step or variable to drop the double underscore.`);
616
+ }
617
+ // GUARDRAILS (Phase 4F). The node's definition types the input only as
618
+ // `array`, so a malformed entry deploys silently and either never trips or
619
+ // blocks every run — both invisible until production. The shapes below are
620
+ // Agent Builder's own discriminators.
621
+ const rails = inputs?.guardrails;
622
+ if (rails !== undefined) {
623
+ const ids = new Set();
624
+ rails.forEach((g, i) => {
625
+ const label = g?.name ? `"${g.name}"` : `guardrails[${i}]`;
626
+ if (!g || (g.$guardrailType !== 'custom' && g.$guardrailType !== 'builtInValidator')) {
627
+ at('GUARDRAIL_TYPE_UNKNOWN', `Inline agent step "${step}"'s ${label} has the $guardrailType `
628
+ + `${JSON.stringify(g?.$guardrailType)}. Valid: 'custom' `
629
+ + `(rules you author) or 'builtInValidator' (a platform validator).`);
630
+ return;
631
+ }
632
+ if (blank(g.id) || blank(g.name)) {
633
+ at('GUARDRAIL_INCOMPLETE', `Inline agent step "${step}"'s ${label} is missing ${blank(g.id) ? 'an id' : 'a name'} — `
634
+ + `both are required by the agent schema (the id keys the rail, the name is what trips show).`);
635
+ return;
636
+ }
637
+ if (ids.has(g.id)) {
638
+ at('GUARDRAIL_DUP_ID', `Inline agent step "${step}" declares the guardrail id "${g.id}" twice — the second silently `
639
+ + `shadows the first in Agent Builder's keyed storage.`);
640
+ return;
641
+ }
642
+ ids.add(g.id);
643
+ if (!Array.isArray(g.selector?.scopes) || g.selector.scopes.length === 0) {
644
+ at('GUARDRAIL_NO_SCOPE', `Inline agent step "${step}"'s guardrail "${g.name}" selects no scopes — it can never apply.`, `selector: { scopes: ['Agent'] }`);
645
+ }
646
+ const act = g.action?.$actionType;
647
+ if (act !== 'block' && act !== 'filter' && act !== 'log') {
648
+ at('GUARDRAIL_ACTION_UNKNOWN', `Inline agent step "${step}"'s guardrail "${g.name}" has the action ${JSON.stringify(act)}. `
649
+ + `Valid: 'block', 'filter', 'log'.`);
650
+ }
651
+ if (g.$guardrailType === 'custom' && (!Array.isArray(g.rules) || g.rules.length === 0)) {
652
+ at('GUARDRAIL_NO_RULES', `Inline agent step "${step}"'s custom guardrail "${g.name}" has no rules — nothing can ever `
653
+ + `trip it, so the action never fires.`, `rules: [{ $ruleType: 'always', applyTo: 'inputAndOutput' }]`);
654
+ }
655
+ if (g.$guardrailType === 'builtInValidator' && blank(g.validatorType)) {
656
+ at('GUARDRAIL_NO_VALIDATOR', `Inline agent step "${step}"'s guardrail "${g.name}" names no validatorType — the platform `
657
+ + `cannot resolve which built-in validator to run.`);
658
+ }
659
+ });
660
+ }
661
+ // TOOLS and ESCALATION (card G16). Cheap identity checks only: a wrong KIND
662
+ // or a non-uuid KEY emits a node type the tenant has never heard of, which
663
+ // nothing downstream of the compiler can see — the resource is dropped from
664
+ // emitted graph, so no rung ever exercises it. Library resolution for connector
665
+ // tools happens at compile (where the library is), not here.
666
+ const toolRefs = inputs?.tools === undefined ? []
667
+ : Array.isArray(inputs.tools) ? inputs.tools : [inputs.tools];
668
+ const BUILTIN_TOOLS = ['analyzefiles', 'summarize', 'batchtransform'];
669
+ for (const t of toolRefs) {
670
+ const kind = t?.kind;
671
+ if (kind === 'builtin') {
672
+ if (!BUILTIN_TOOLS.includes(String(t.tool))) {
673
+ at('INLINE_AGENT_TOOL_UNKNOWN_BUILTIN', `Inline agent step "${step}" declares a builtin tool ${JSON.stringify(t.tool)}. `
674
+ + `The platform ships exactly three: ${BUILTIN_TOOLS.map((b) => `'${b}'`).join(', ')} — the node type is a `
675
+ + `fixed string, so any other name is a node the tenant has never heard of.`, `{ kind: 'builtin', tool: 'summarize' }`);
676
+ }
677
+ }
678
+ else if (kind === 'connector') {
679
+ const c = t;
680
+ if (blank(c.connector) || blank(c.operation)) {
681
+ at('INLINE_AGENT_TOOL_NO_OPERATION', `Inline agent step "${step}" declares a connector tool without ${blank(c.connector) ? 'a `connector`' : 'an `operation`'}. `
682
+ + `Both name the curated-library entry the tool is resolved from — the same pair \`connector(…)\` takes.`, `{ kind: 'connector', connector: 'uipath-atlassian-jira', operation: 'create-issue' }`);
683
+ }
684
+ // A connector tool calls Integration Service on a CONNECTION, exactly as a
685
+ // connector step does — and before #867 there was no way to give it one, so
686
+ // the tool compiled with none anywhere (no binding, no resource.json row)
687
+ // and could only fail at run time. Both halves, as `registry prepare` writes them.
688
+ const labels = t;
689
+ const missing = ['connection', 'folder'].filter((k) => blank(labels[k]));
690
+ if (missing.length > 0) {
691
+ const key = blank(c.connector) ? '<connector-key>' : String(c.connector);
692
+ const op = blank(c.operation) ? '<operation>' : String(c.operation);
693
+ at('INLINE_AGENT_TOOL_CONNECTOR_NO_CONNECTION', `Inline agent step "${step}"'s connector tool "${key}/${op}" has no ${missing.map((m) => `\`${m}\``).join(' or ')}. `
694
+ + `The tool calls Integration Service on a tenant connection: without one the agent's tool has no `
695
+ + `connection binding, its resource.json carries none, and the solution's Resources never list it. `
696
+ + `Name the bindings.json labels the same way \`connector()\` takes them — \`${prepareCommand(key, op)}\` `
697
+ + `finds the connection and writes both entries.`, `{ kind: 'connector', connector: '${key}', operation: '${op}', connection: '<connection label>', folder: '<folder label>' }`);
698
+ }
699
+ else {
700
+ ctx.bindingLabels(`Inline agent step "${step}"'s connector tool`, labels, undefined, blank(c.connector) ? undefined : { key: String(c.connector), ...(blank(c.operation) ? {} : { action: String(c.operation) }) });
701
+ }
702
+ }
703
+ else if (kind === 'ixp') {
704
+ const x = t;
705
+ if (blank(x.projectId) || !GUID_RE.test(String(x.projectId).trim())) {
706
+ at('INLINE_AGENT_TOOL_IXP_PROJECT_NOT_GUID', `Inline agent step "${step}"'s IxP tool has \`projectId\` ${JSON.stringify(x.projectId)}, which is not a uuid. `
707
+ + `It is the node type's tail (uipath.agent.resource.tool.ixp.<slug>.<projectId>), so a wrong one emits a `
708
+ + `node type the tenant cannot resolve. Find it: uip maestro flow registry pull --force && `
709
+ + `uip maestro flow registry search -f "type:startsWith=uipath.agent.resource.tool.ixp".`);
710
+ }
711
+ if (blank(x.name)) {
712
+ at('INLINE_AGENT_TOOL_IXP_NO_NAME', `Inline agent step "${step}"'s IxP tool needs a \`name\` — the project's display name, which the definition `
713
+ + `carries as both its label and \`inputDefaults.name\`.`);
714
+ }
715
+ }
716
+ else if (kind === 'process' || kind === 'agent' || kind === 'api' || kind === 'flow'
717
+ || kind === 'maestro' || kind === 'function') {
718
+ const p = t;
719
+ if (blank(p.key) || !GUID_RE.test(String(p.key).trim())) {
720
+ at('INLINE_AGENT_TOOL_KEY_NOT_GUID', `Inline agent step "${step}"'s '${String(kind)}' tool has \`key\` ${JSON.stringify(p.key)}, which is not a uuid. `
721
+ + `The key IS the node type's tail (uipath.agent.resource.tool.…<key>), so a wrong one emits a node type `
722
+ + `the tenant cannot resolve. Find it: uip maestro flow registry search "uipath.agent.resource.tool".`);
723
+ }
724
+ if (blank(p.name) || blank(p.folderPath)) {
725
+ at('INLINE_AGENT_TOOL_NO_RESOURCE_ID', `Inline agent step "${step}"'s '${String(kind)}' tool needs both \`name\` and \`folderPath\` — they form the `
726
+ + `binding's resourceKey ("<folderPath>.<name>"), which is how the deployed flow finds the resource.`);
727
+ }
728
+ }
729
+ else if (kind === 'mcp') {
730
+ const m = t;
731
+ if (blank(m.name) || blank(m.key) || !GUID_RE.test(String(m.key).trim())) {
732
+ at('INLINE_AGENT_TOOL_MCP_INCOMPLETE', `Inline agent step "${step}"'s MCP tool needs a \`name\` and a uuid \`key\` — the node type is minted `
733
+ + `from them (uipath.agent.resource.tool.mcp.<name>.<key>) and the key is the reference the runtime `
734
+ + `resolves.`);
735
+ }
736
+ }
737
+ else if (kind === 'a2a') {
738
+ const a = t;
739
+ if (blank(a.name) || blank(a.key) || !GUID_RE.test(String(a.key).trim()) || blank(a.slug)) {
740
+ at('INLINE_AGENT_TOOL_A2A_INCOMPLETE', `Inline agent step "${step}"'s remote-A2A tool needs \`name\`, a uuid \`key\`, and \`slug\` — the node `
741
+ + `type is minted from name+key, and the runtime resolves the remote agent by slug (the platform's own `
742
+ + `mapper refuses a blank one).`);
743
+ }
744
+ }
745
+ else if (kind === 'clientside') {
746
+ if (blank(t.name)) {
747
+ at('INLINE_AGENT_TOOL_CLIENTSIDE_NO_NAME', `Inline agent step "${step}"'s client-side tool has no \`name\` — it is what the model calls, and the `
748
+ + `calling application dispatches on it.`);
749
+ }
750
+ }
751
+ else if (kind === 'httpRequest') {
752
+ const h = t;
753
+ const m = h.method === undefined ? undefined : String(h.method).toUpperCase();
754
+ if (m !== undefined && !['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(m)) {
755
+ at('INLINE_AGENT_TOOL_HTTP_METHOD', `Inline agent step "${step}"'s HTTP-request tool fixes the method to ${JSON.stringify(h.method)}. The `
756
+ + `tool's contract allows GET, POST, PUT, PATCH, DELETE — anything else the runtime refuses at call time.`);
757
+ }
758
+ }
759
+ else {
760
+ at('INLINE_AGENT_TOOL_UNKNOWN_KIND', `Inline agent step "${step}" declares a tool with kind ${JSON.stringify(kind)}. Valid kinds: 'builtin', `
761
+ + `'connector', 'process', 'agent', 'api', 'flow', 'maestro', 'function', 'ixp', 'mcp', 'a2a', `
762
+ + `'clientside', 'httpRequest'.`);
763
+ }
764
+ }
765
+ const escalations = inputs?.escalation === undefined ? []
766
+ : Array.isArray(inputs.escalation) ? inputs.escalation : [inputs.escalation];
767
+ for (const e of escalations) {
768
+ if (blank(e?.name)) {
769
+ at('INLINE_AGENT_ESCALATION_NO_NAME', `Inline agent step "${step}" declares an escalation without a \`name\`. The node's definition requires one `
770
+ + `(\`minLength: 1\`).`);
771
+ }
772
+ if (e?.variant !== undefined && e.variant !== 'quick-form') {
773
+ at('INLINE_AGENT_ESCALATION_VARIANT', `Inline agent step "${step}"'s escalation names the variant ${JSON.stringify(e.variant)}. Valid: `
774
+ + `'quick-form' — or omit it for the app-backed default.`);
775
+ continue;
776
+ }
777
+ if (e?.variant === 'quick-form') {
778
+ // The form lives INLINE: an app would be emitted and ignored, and a form
779
+ // with nothing to fill in is a notification nobody can answer.
780
+ if (e.app !== undefined) {
781
+ at('INLINE_AGENT_ESCALATION_QUICKFORM_WITH_APP', `Inline agent step "${step}"'s quick-form escalation also names an app. The form lives in the flow — `
782
+ + `drop \`app\`, or drop the variant for the app-backed escalation.`);
783
+ }
784
+ if (!Array.isArray(e.fields) || e.fields.length === 0) {
785
+ at('INLINE_AGENT_ESCALATION_NO_FIELDS', `Inline agent step "${step}"'s quick-form escalation declares no \`fields\` — the reviewer gets an `
786
+ + `empty form.`);
787
+ }
788
+ for (const f of Array.isArray(e.fields) ? e.fields : []) {
789
+ if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
790
+ at('INLINE_AGENT_ESCALATION_FIELD_TYPE_UNKNOWN', `Inline agent step "${step}"'s quick-form escalation gives the field "${String(f?.id ?? '?')}" the type `
791
+ + `${JSON.stringify(f?.type)}, ${formFieldTypeWhy(f?.type, `which the form does not recognize — it would `
792
+ + `be published as a plain string`)}. Valid: ${validFormFieldTypesText()}.`, formFieldTypeHint(f?.type));
793
+ }
794
+ const optionsProblem = formFieldOptionsProblem(f);
795
+ if (optionsProblem !== undefined) {
796
+ const message = `Inline agent step "${step}"'s quick-form escalation field "${String(f?.id ?? '?')}" `
797
+ + `${formFieldOptionsWhy(optionsProblem, f?.type)}.`;
798
+ if (optionsProblem === 'ignored')
799
+ warn('INLINE_AGENT_ESCALATION_FIELD_OPTIONS_IGNORED', message);
800
+ else {
801
+ at('INLINE_AGENT_ESCALATION_FIELD_OPTIONS', message, `options: [{ id: 'opt-1', value: 'high', label: 'High' }, { id: 'opt-2', value: 'low', label: 'Low' }]`);
802
+ }
803
+ }
804
+ for (const problem of formFieldSettingProblems(f)) {
805
+ at('INLINE_AGENT_ESCALATION_FIELD_SETTING_INVALID', `Inline agent step "${step}"'s quick-form escalation gives the field "${String(f?.id ?? '?')}" `
806
+ + `${problem}. The platform refuses the form.`);
807
+ }
808
+ }
809
+ continue;
810
+ }
811
+ if (e?.fields !== undefined) {
812
+ at('INLINE_AGENT_ESCALATION_APP_WITH_FIELDS', `Inline agent step "${step}"'s escalation declares inline \`fields\`, but its form belongs to the deployed `
813
+ + `app. Use variant: 'quick-form' for an inline form.`);
814
+ }
815
+ if (blank(e?.app?.key) || blank(e?.app?.name)) {
816
+ at('INLINE_AGENT_ESCALATION_NO_APP', `Inline agent step "${step}"'s escalation needs \`app.key\` and \`app.name\` — the deployed Action Center app `
817
+ + `that carries the review task. An escalation with no app is a task nobody can open.`);
818
+ }
819
+ else if (!GUID_RE.test(String(e.app.key).trim())) {
820
+ at('INLINE_AGENT_ESCALATION_APP_KEY_NOT_GUID', `Inline agent step "${step}"'s escalation \`app.key\` is ${JSON.stringify(e.app.key)}, which is not a uuid. `
821
+ + `It is the app's own key on the tenant, not its name.`);
822
+ }
823
+ }
824
+ // MEMORY (Phase 5A): identity is required — the node type is minted from it.
825
+ const mem = inputs?.memory;
826
+ if (mem !== undefined) {
827
+ if (blank(mem.name) || blank(mem.id)) {
828
+ at('INLINE_AGENT_MEMORY_INCOMPLETE', `Inline agent step "${step}"'s memory needs both \`name\` and \`id\` — the node type is minted from them `
829
+ + `(uipath.agent.resource.memory.<name>.<id>) and the runtime resolves the memory by id.`);
830
+ }
831
+ else if (!GUID_RE.test(String(mem.id).trim())) {
832
+ at('INLINE_AGENT_MEMORY_ID_NOT_GUID', `Inline agent step "${step}"'s memory \`id\` is ${JSON.stringify(mem.id)}, which is not a uuid. It is the `
833
+ + `memory's own id on the tenant, not its name.`);
834
+ }
835
+ }
836
+ }
837
+ /**
838
+ * A read of an inline agent's output: `out('<step>', '<field>')`.
839
+ *
840
+ * Stricter than the published families' equivalent, and it can afford to be: the
841
+ * declared `returns` IS the contract (there is no tenant to disagree with), and it
842
+ * is what any runner has to coerce the model's completion to: build
843
+ * `{ <declared keys> }` and put the raw completion in the first one if the model
844
+ * ignored the shape. So a read outside the set is undefined by construction, not
845
+ * merely probably.
846
+ */
847
+ export function checkInlineAgentRead({ node: name, field, rest, reader: step }, spec, report) {
848
+ if (field === undefined)
849
+ return;
850
+ if (field === 'error')
851
+ return;
852
+ if (field !== 'output') {
853
+ report('INLINE_AGENT_READ_NOT_OUTPUT', `Inline agent "${name}" publishes its answer under "output", but "$vars.${name}.${field}" (read in `
854
+ + `"${step}") asks for "${field}", which is undefined at run time.`, `out('${name}', '<field>')`);
855
+ return;
856
+ }
857
+ // The default when `returns` is omitted is the definition's own.
858
+ const declared = spec.returns && Object.keys(spec.returns).length
859
+ ? Object.keys(spec.returns)
860
+ : ['content'];
861
+ const wanted = (rest ?? '').replace(/^\./, '').split('.')[0];
862
+ if (!wanted)
863
+ return;
864
+ if (declared.includes(wanted))
865
+ return;
866
+ const nearest = nearestName(wanted, declared);
867
+ report('INLINE_AGENT_READ_UNKNOWN_FIELD', `Inline agent "${name}" declares no output "${wanted}" (read as "$vars.${name}.output.${wanted}" in `
868
+ + `"${step}"). Its \`returns\` names: ${declared.join(', ')}`
869
+ + `${spec.returns && Object.keys(spec.returns).length ? '' : ' (the definition\'s default, because the step declares no `returns`)'}`
870
+ + `. Unlike a published agent's, this contract is yours — the node's \`agentOutputVariables\` are these `
871
+ + `names, and the engine coerces the model's completion to exactly them, so a read outside the set is `
872
+ + `undefined by construction.`, nearest ? `out('${name}', '${nearest}')` : undefined);
873
+ }