@uipath/maestro-builder-sdk 6.42.0 → 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 (52) 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/compile-cli.js +2 -1
  6. package/dist/core/actions.d.ts +1 -713
  7. package/dist/core/actions.js +0 -193
  8. package/dist/core/agent-sidecar.d.ts +186 -0
  9. package/dist/core/agent-sidecar.js +409 -0
  10. package/dist/core/agent-tool-resources.d.ts +62 -0
  11. package/dist/core/agent-tool-resources.js +81 -0
  12. package/dist/{config.js → core/connector-configuration.js} +2 -2
  13. package/dist/core/node-id-placeholder.d.ts +8 -0
  14. package/dist/core/node-id-placeholder.js +10 -0
  15. package/dist/core/ts-literal.d.ts +6 -0
  16. package/dist/core/ts-literal.js +15 -0
  17. package/dist/core/typed-raw-node.js +0 -5
  18. package/dist/decompile.js +34 -822
  19. package/dist/flow-node-registry.js +2 -1
  20. package/dist/flow-node-type.d.ts +130 -6
  21. package/dist/flow-sdk.d.ts +1 -0
  22. package/dist/flow-sdk.js +1 -0
  23. package/dist/nodes/agent/flow/check.d.ts +88 -0
  24. package/dist/nodes/agent/flow/check.js +873 -0
  25. package/dist/nodes/agent/flow/factories.d.ts +134 -0
  26. package/dist/nodes/agent/flow/factories.js +143 -0
  27. package/dist/nodes/agent/flow/family.d.ts +3 -0
  28. package/dist/nodes/agent/flow/family.js +8 -0
  29. package/dist/nodes/agent/flow/index.d.ts +3 -0
  30. package/dist/nodes/agent/flow/index.js +3 -0
  31. package/dist/nodes/agent/flow/inline-agent-lift.d.ts +44 -0
  32. package/dist/nodes/agent/flow/inline-agent-lift.js +572 -0
  33. package/dist/nodes/agent/flow/inline-agent.d.ts +15 -0
  34. package/dist/nodes/agent/flow/inline-agent.js +526 -0
  35. package/dist/nodes/agent/flow/node-type.d.ts +20 -0
  36. package/dist/nodes/agent/flow/node-type.js +422 -0
  37. package/dist/nodes/agent/flow/resources.d.ts +502 -0
  38. package/dist/nodes/agent/flow/resources.js +1409 -0
  39. package/dist/nodes/agent/flow/spec.d.ts +448 -0
  40. package/dist/nodes/agent/flow/spec.js +1 -0
  41. package/dist/nodes/process/flow/check.d.ts +40 -2
  42. package/dist/nodes/process/flow/check.js +156 -0
  43. package/dist/nodes/process/flow/factories.d.ts +58 -3
  44. package/dist/nodes/process/flow/factories.js +59 -2
  45. package/dist/nodes/process/flow/node-type.d.ts +4 -1
  46. package/dist/nodes/process/flow/node-type.js +181 -10
  47. package/dist/nodes/process/flow/spec.d.ts +112 -5
  48. package/dist/serialize.d.ts +9 -54
  49. package/dist/serialize.js +47 -2763
  50. package/package.json +2 -2
  51. /package/dist/{config.d.ts → core/connector-configuration.d.ts} +0 -0
  52. /package/dist/{ixp-definition.json → core/ixp-definition.json} +0 -0
package/dist/check.js CHANGED
@@ -5,19 +5,16 @@
5
5
  * duplicate step names, references to undeclared variables/steps, empty branch
6
6
  * arms, and missing terminals.
7
7
  */
8
- import { Expr, SCHEDULE_PRESETS, parseIxpProjectType, flatNameOf, flatNameSegmentsOf, FLAT_SEP } from './flow-sdk.js';
8
+ import { Expr, SCHEDULE_PRESETS, parseIxpProjectType } from './flow-sdk.js';
9
9
  import { FlowNode } from './core/node-classes.js';
10
10
  import { planContributionBindings } from './core/contribution-bindings.js';
11
11
  import { definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
12
- import { actionVersion, bundledActionNodeType, bundledContainerDefinition, bundledDefinition, perCallDefinition, resolveContributionDefinition } from './serialize.js';
13
- import { FORM_FIELD_TYPES, formFieldOptionsProblem, formFieldOptionsWhy, formFieldSettingProblems, formFieldTypeHint, formFieldTypeWhy, validFormFieldTypesText, } from './core/form-field-type.js';
12
+ import { bundledActionNodeType, bundledContainerDefinition, bundledDefinition, perCallDefinition, resolveContributionDefinition } from './serialize.js';
14
13
  import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
15
14
  import { checkCookedEscapes } from './flow-expr-check.js';
16
15
  import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
17
- import { prepareCommand } from './core/cli-spelling.js';
18
16
  import { commentText } from './core/comment-text.js';
19
17
  import { nearestName } from './core/edit-distance.js';
20
- import { renamedFieldMessage } from './core/process-resource.js';
21
18
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome, outcomeIdOf } from './core/hitl-routing.js';
22
19
  import { hasExits } from './core/step-ports.js';
23
20
  import { stopsAtEnd } from './flow-step-tree.js';
@@ -25,7 +22,6 @@ import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
25
22
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
26
23
  import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
27
24
  import { planOutputBacking } from './flow-output-backing.js';
28
- import { ownEntry } from './core/own-entry.js';
29
25
  import { terminateStepClashMessage, terminateTwiceMessage } from './flow-terminate-names.js';
30
26
  import { endContentKey } from './flow-end-content.js';
31
27
  import { FLOW_NODE_TYPES } from './flow-node-registry.js';
@@ -34,7 +30,6 @@ import { plainHttpOnErrorMessage } from './nodes/http/flow/family.js';
34
30
  import { resolveVersion, resolveVersionCarried } from './core/nodes/versioned.js';
35
31
  import { declaredErrorPortRefusal } from './core/error-port.js';
36
32
  import { bodyUsesBreak, loopParallelWarning, loopVersionRefusal } from './loop-version.js';
37
- import { derivedResourceNodeId } from './core/agent-resource-identity.js';
38
33
  const newResourceIdLedger = () => ({ explicit: new Map(), taken: new Map(), agentSources: new Map() });
39
34
  /** INLINE_AGENT_RESOURCE_NODE_ID for each explicit `nodeId` another node of the file also gets. */
40
35
  function resourceNodeIdClashes(ledger) {
@@ -453,7 +448,7 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
453
448
  prevPortSource = s;
454
449
  const registered = registeredKind(s.spec);
455
450
  if (registered) {
456
- const { placed, checked } = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId, carriedLookup([built, ...ancestors]));
451
+ const { placed, checked } = checkRegisteredStep(registered, s, exprs, errorSource, opts, diags, typeOf, triggerId, ledger, carriedLookup([built, ...ancestors]));
457
452
  checkedSteps.set(s.name, checked);
458
453
  if (placed)
459
454
  registeredSteps.set(s.name, checked);
@@ -514,22 +509,10 @@ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger())
514
509
  });
515
510
  }
516
511
  }
517
- if (s.spec.kind === 'createOutgoingCall' || s.spec.kind === 'endCall' || s.spec.kind === 'voiceAgent') {
518
- checkVoice(s.name, s.spec.kind, s.spec.inputs, diags);
519
- }
520
- if (s.spec.kind === 'conversationalAgent') {
521
- checkConversationalAgent(s.name, s.spec.inputs, diags);
522
- }
523
512
  // The process family checks the step itself; the flow-level fire-and-forget
524
513
  // read rule below needs to know which steps dispatch without waiting.
525
514
  if (s.spec.kind === 'agenticProcess')
526
515
  agenticProcesses.set(s.name, s.spec.inputs);
527
- if (s.spec.kind === 'agent') {
528
- checkAgent(s.name, s.spec.inputs, diags);
529
- }
530
- if (s.spec.kind === 'inlineAgent') {
531
- checkInlineAgent(s.name, s.spec.inputs, diags, opts, ledger);
532
- }
533
516
  {
534
517
  const rule = OUTPUT_READ_RULES[s.spec.kind];
535
518
  if (typeof rule === 'function') {
@@ -2097,7 +2080,6 @@ function codeOnly(src) {
2097
2080
  code(false);
2098
2081
  return out.join('');
2099
2082
  }
2100
- const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2101
2083
  /**
2102
2084
  * A read of an API workflow's output: `out('<step>', '<field>')`.
2103
2085
  *
@@ -2112,132 +2094,6 @@ const GUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
2112
2094
  * - reading something other than `output` (e.g. `$vars.<step>.result`), which the
2113
2095
  * job never publishes.
2114
2096
  */
2115
- /**
2116
- * A conversational AGENT step (Phase 5).
2117
- *
2118
- * The turn wiring is the whole risk: `'simple'` mode reads everything out of
2119
- * ONE conversation-context binding, so a flow that names neither the context
2120
- * nor the individual fields deploys an agent with no transcript to answer.
2121
- */
2122
- function checkConversationalAgent(step, inputs, diags) {
2123
- const at = (code, message, suggestion) => {
2124
- diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
2125
- };
2126
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
2127
- if (blank(inputs?.model)) {
2128
- at('CONV_AGENT_NO_MODEL', `Conversational agent step "${step}" has no \`model\`, which the node's definition requires. `
2129
- + `\`uip agent model list\` shows what the tenant offers.`, `model: 'gpt-5.4'`);
2130
- }
2131
- if (blank(inputs?.systemPrompt)) {
2132
- at('CONV_AGENT_NO_PROMPT', `Conversational agent step "${step}" has no \`systemPrompt\` — the agent has no instructions for how `
2133
- + `to answer the person.`, `systemPrompt: 'You are a support agent. Be brief.'`);
2134
- }
2135
- const st = inputs?.settings;
2136
- const mode = st?.mode ?? 'simple';
2137
- if (mode !== 'simple' && mode !== 'custom') {
2138
- at('CONV_AGENT_SETTINGS_MODE', `Conversational agent step "${step}"'s settings mode is ${JSON.stringify(mode)}. Valid: 'simple' (one `
2139
- + `conversation-context binding) or 'custom' (bind each field yourself).`);
2140
- return;
2141
- }
2142
- const named = ['context', 'conversationId', 'exchangeId', 'messages', 'userSettings']
2143
- .filter((k) => st !== undefined && !blank(st[k]));
2144
- if (named.length === 0) {
2145
- at('CONV_AGENT_NO_TURN', `Conversational agent step "${step}" binds nothing in \`settings\` — it would run with no transcript and `
2146
- + `no conversation to answer into. In 'simple' mode bind the whole context from a wait or context step.`, `settings: { context: out('<waitStep>', 'conversationContext') }`);
2147
- }
2148
- else if (mode === 'custom' && blank(st?.conversationId)) {
2149
- at('CONV_AGENT_CUSTOM_NO_ID', `Conversational agent step "${step}" binds its turn field-by-field ('custom' mode) but names no `
2150
- + `\`conversationId\` — the one field the platform's own editor marks required, because the reply has `
2151
- + `nowhere to go without it.`, `settings: { mode: 'custom', conversationId: out('start', 'conversationId'), … }`);
2152
- }
2153
- }
2154
- /**
2155
- * A VOICE step (Phase 5D): create-outgoing-call, end-call, or the voice agent.
2156
- *
2157
- * Two things are decidable offline and expensive later: a call needs both
2158
- * numbers in a form the telephony provider accepts, and every voice step needs
2159
- * the `callContext` OBJECT — binding a bare conversation id instead is the
2160
- * classic mistake, because the field is typed `object` and a string passes the
2161
- * file's schema while failing at dispatch.
2162
- */
2163
- function checkVoice(step, kind, inputs, diags) {
2164
- const at = (code, message, suggestion) => {
2165
- diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
2166
- };
2167
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
2168
- if (kind === 'createOutgoingCall') {
2169
- for (const field of ['from', 'to']) {
2170
- const raw = inputs?.[field];
2171
- if (blank(raw)) {
2172
- at('CALL_NO_NUMBER', `createOutgoingCall step "${step}" has no \`${field}\`, which the node's schema requires — `
2173
- + `${field === 'from' ? 'the provisioned number the call comes from' : 'the number to dial'}.`, `${field}: '+15551234567'`);
2174
- continue;
2175
- }
2176
- // A literal number is checkable; an expression is the caller's business.
2177
- if (typeof raw === 'string' && !/^\+[1-9]\d{6,14}$/.test(raw.trim())) {
2178
- diags.push({
2179
- level: 'warning', code: 'CALL_NUMBER_NOT_E164', step,
2180
- message: `createOutgoingCall step "${step}"'s \`${field}\` is ${JSON.stringify(raw)}, which is not `
2181
- + `E.164 (a leading '+' and 7–15 digits). Telephony providers reject anything else at dispatch, `
2182
- + `so a run fails after the flow is deployed.`,
2183
- suggestion: `${field}: '+15551234567'`,
2184
- });
2185
- }
2186
- }
2187
- return;
2188
- }
2189
- // end-call and the voice agent are both keyed by the live call.
2190
- if (blank(inputs?.callContext)) {
2191
- at('VOICE_NO_CALL_CONTEXT', `${kind === 'endCall' ? 'endCall' : 'voiceAgent'} step "${step}" has no \`callContext\`, which the node's `
2192
- + `schema requires — it identifies the live call. The incoming-call trigger publishes it as `
2193
- + `out('start', 'callContext'), and createOutgoingCall as out('<dialStep>', 'callContext').`, `callContext: out('start', 'callContext')`);
2194
- }
2195
- else {
2196
- const cc = inputs.callContext;
2197
- const js = cc instanceof Expr && !cc.literal ? cc.js : undefined;
2198
- // The field is the call-context OBJECT. A read that ends at a scalar inside
2199
- // it (`…callContext.conversationId`, or a bare conversation id) passes the
2200
- // file's schema and then fails at dispatch.
2201
- if (js !== undefined && /\.(conversationId|id)\s*$/.test(js)) {
2202
- at('VOICE_CALL_CONTEXT_NOT_OBJECT', `${kind === 'endCall' ? 'endCall' : 'voiceAgent'} step "${step}"'s \`callContext\` reads `
2203
- + `"${js.trim()}", which is a field INSIDE the call context rather than the context object itself. `
2204
- + `The node's schema types it \`object\`; a scalar passes the file's schema and fails at dispatch.`, `callContext: out('start', 'callContext')`);
2205
- }
2206
- }
2207
- if (kind === 'voiceAgent') {
2208
- if (blank(inputs?.systemPrompt)) {
2209
- at('VOICE_AGENT_NO_PROMPT', `voiceAgent step "${step}" has no \`systemPrompt\` — a caller is waiting, and the agent has no `
2210
- + `instructions for what to say.`, `systemPrompt: 'Greet the caller and find out why they called.'`);
2211
- }
2212
- const mi = inputs?.maxIterations;
2213
- if (mi !== undefined && !(mi instanceof Expr)) {
2214
- const n = Number(mi);
2215
- if (!Number.isInteger(n) || n < 1 || n > 8) {
2216
- at('VOICE_AGENT_ITERATIONS', `voiceAgent step "${step}"'s \`maxIterations\` is ${JSON.stringify(mi)}. The node's schema allows `
2217
- + `1 to 8 — a tighter ceiling than a text agent's, because the caller is waiting on the turn.`, `maxIterations: 4`);
2218
- }
2219
- }
2220
- const v = inputs?.voice;
2221
- if (v !== undefined) {
2222
- const PERSONAS = {
2223
- 'gemini-3.1-flash-live-preview': ['Aoede', 'Charon', 'Fenrir', 'Kore', 'Leda', 'Orus', 'Puck', 'Zephyr'],
2224
- 'gpt-realtime-2': ['alloy', 'ash', 'ballad', 'coral', 'echo', 'sage', 'shimmer', 'verse'],
2225
- };
2226
- const model = typeof v.model === 'string' ? v.model : undefined;
2227
- // An own-key read: `in` also matched 'toString', and `PERSONAS[model].includes` then threw.
2228
- const personas = model !== undefined ? ownEntry(PERSONAS, model) : undefined;
2229
- if (model !== undefined && personas === undefined) {
2230
- at('VOICE_MODEL_UNKNOWN', `voiceAgent step "${step}" names the voice model ${JSON.stringify(model)}. The platform's catalog `
2231
- + `today is ${Object.keys(PERSONAS).map((m) => `'${m}'`).join(' and ')} — a voice model, not one of `
2232
- + `the text models an inline agent takes.`);
2233
- }
2234
- else if (typeof v.persona === 'string' && personas !== undefined && !personas.includes(v.persona)) {
2235
- at('VOICE_PERSONA_UNKNOWN', `voiceAgent step "${step}"'s persona ${JSON.stringify(v.persona)} is not one of `
2236
- + `${JSON.stringify(model)}'s voices (${personas.join(', ')}). A persona belongs to its model.`);
2237
- }
2238
- }
2239
- }
2240
- }
2241
2097
  /**
2242
2098
  * The read rule the process-shaped families share: the result is published
2243
2099
  * under `output`, keyed by a known field list — the author's `returns`, or a
@@ -2298,8 +2154,8 @@ const OUTPUT_READ_RULES = {
2298
2154
  apiWorkflow: { exempt: 'Registered: its node type reads its own output (nodes/process/flow/, a `reader` contract).' },
2299
2155
  publishedFunction: { exempt: 'Registered: its node type reads its own output (nodes/process/flow/, a `reader` contract).' },
2300
2156
  agenticProcess: { exempt: 'Registered: its node type reads its own output (nodes/process/flow/, a `reader` contract).' },
2301
- agent: checkAgentRead,
2302
- inlineAgent: checkInlineAgentRead,
2157
+ agent: { exempt: 'Registered: its node type reads its own output (nodes/process/flow/, a `reader` contract).' },
2158
+ inlineAgent: { exempt: 'Registered: its node type reads its own output (nodes/agent/flow/, a `reader` contract).' },
2303
2159
  queueItem: { exempt: 'Registered: its node type states the fields a read may name (nodes/queue/flow/, a `fields` output contract).' },
2304
2160
  script: {
2305
2161
  exempt: 'Not yet: only the field-map form of a script\'s `returns` gives a list to check against (the '
@@ -2307,885 +2163,6 @@ const OUTPUT_READ_RULES = {
2307
2163
  + 'its own false-positive sweep — flow-builder-sdk#855.',
2308
2164
  },
2309
2165
  };
2310
- /**
2311
- * Check an `agent(...)` call — a published or in-solution agent (coded or
2312
- * low-code).
2313
- *
2314
- * WHAT THESE RULES CAN AND CANNOT SEE. Whether the agent exists, which kind it is,
2315
- * and what it returns all live on the tenant. So these cover what is decidable from
2316
- * the call — and two of them are the exact classes v1's own baseline on this family
2317
- * fails on, which is why they are errors rather than warnings:
2318
- *
2319
- * - **A reference to nothing.** 5 of 16 `skill-flow-coded-agent` runs in the
2320
- * 06-29 → 07-20 window emitted an agent node whose `resourceKey` was a LOCAL
2321
- * project id and whose `folderPath` was `""`, and faulted at job start with
2322
- * `[170007] … HTTP 404 "The job's associated process could not be found"`. A
2323
- * blank `name` or `folderPath` is that flow, caught before a tenant round trip.
2324
- * - **The empty-input floor.** `inputs` is required by the type, so `{}` is the only
2325
- * way to reach it, and it is worth saying what the platform then does — see
2326
- * `AGENT_EMPTY_INPUT_FLOOR`.
2327
- *
2328
- * What no rule here can see: whether the agent's ANSWER is right. That is CP-3's
2329
- * ruling — a green live rung means a model responded with the declared shape — and
2330
- * the gate that does cover a flow FAKING an answer is an offline `replays:` seed
2331
- * asserting the exact unfixtured value, not a check.
2332
- */
2333
- function checkAgent(step, inputs, diags) {
2334
- const at = (code, message, suggestion) => {
2335
- diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
2336
- };
2337
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
2338
- const inSolution = inputs?.location === 'in-solution';
2339
- if (blank(inputs?.key) && !blank(inputs?.agent)) {
2340
- at('AGENT_NO_KEY', renamedFieldMessage('Agent', step, 'agent'), `key: ${JSON.stringify(inputs.agent)}`);
2341
- }
2342
- else if (blank(inputs?.key)) {
2343
- at('AGENT_NO_KEY', `Agent step "${step}" has no \`key\`. It is a GUID and becomes part of the node's `
2344
- + `type — without it there is no agent to start. For a published agent, find the Orchestrator `
2345
- + `release with \`uip or processes list --all-folders --limit 200\` (take the row's \`Key\`) or `
2346
- + `\`uip maestro flow registry search uipath.core.agent\`. For an in-solution agent, read `
2347
- + `\`resource.key\` from \`resources/solution_folder/process/agent/<project>.json\`.`, `key: '6a47dc5b-5fd4-461f-b97d-6c1817abcaaa'`);
2348
- }
2349
- else if (!GUID_RE.test(String(inputs.key).trim())) {
2350
- at('AGENT_KEY_NOT_GUID', `Agent step "${step}"'s \`key\` is ${JSON.stringify(inputs.key)}, which is not a key. \`key\` is `
2351
- + `the GUID the tenant gives the published agent (it becomes the node type's suffix); the agent's NAME `
2352
- + `goes in \`name\` and its folder in \`folderPath\`. Passing a ProcessKey like `
2353
- + `"CountLetters.CodedAgent.agent.CountLetters" here is the common form of this — that string is the `
2354
- + `row's \`ProcessKey\`, not its \`Key\`.`, `key: '<guid>', name: ${JSON.stringify(inputs.key)}`);
2355
- }
2356
- if (blank(inputs?.name)) {
2357
- at('AGENT_NO_NAME', `Agent step "${step}" has no \`name\`. The runtime resolves the agent's release by name + folder, not `
2358
- + `by the key in the node type, so without it the job cannot start — deployed, that is `
2359
- + `\`[170007] … 404 The job's associated process could not be found\`.`, `name: 'CountLetters LowCode Agent'`);
2360
- }
2361
- if (!inSolution && blank(inputs?.folderPath)) {
2362
- at('AGENT_NO_FOLDER', `Agent step "${step}" has no \`folderPath\`. The runtime resolves the agent by listing ONE Orchestrator `
2363
- + `folder, so a blank folder resolves nothing — and an empty \`folderPath\` is the exact shape of v1's `
2364
- + `commonest live failure on this node type (5 of 16 runs, 06-29 → 07-20: `
2365
- + `\`[170007] … 404 The job's associated process could not be found\`). It is also what disambiguates `
2366
- + `two same-named agents: this tenant has \`CountLetters\` in both \`Shared\` and `
2367
- + `\`Shared/uipath-maestro-flow/CountLetters CodedAgent\`.`, `folderPath: 'Shared/uipath-maestro-flow/CountLetters LowCode'`);
2368
- }
2369
- if (inSolution && blank(inputs?.projectId)) {
2370
- at('AGENT_LOCAL_NO_PROJECT_ID', `In-solution agent step "${step}" has no \`projectId\`. Read \`resource.projectKey\` from `
2371
- + `\`resources/solution_folder/process/agent/<project>.json\` after registering the sibling `
2372
- + `with \`uip solution projects add\`.`, `projectId: '<resource.projectKey>'`);
2373
- }
2374
- else if (inSolution && !GUID_RE.test(String(inputs.projectId).trim())) {
2375
- at('AGENT_LOCAL_PROJECT_ID_NOT_GUID', `In-solution agent step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
2376
- + `the registered sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
2377
- }
2378
- const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
2379
- if (name.includes('/')) {
2380
- at('AGENT_NAME_IS_PATH', `Agent step "${step}"'s \`name\` is ${JSON.stringify(name)}, which looks like a folder path. \`name\` is `
2381
- + `the agent's own name in Orchestrator; the path it lives in goes in \`folderPath\`. Together they form `
2382
- + `the resourceKey "<folderPath>.<name>", so swapping them points the job at nothing.`);
2383
- }
2384
- // The platform's SECOND place for rewriting an agent node's inputs behind the
2385
- // author's back (the first is the reserved-key class — see AGENTIC_RESERVED_INPUT).
2386
- // A warning rather than an error: an argument-less agent is legal, and the floor
2387
- // makes it work. What is not acceptable is that it happen silently.
2388
- if (inputs?.inputs && Object.keys(inputs.inputs).length === 0) {
2389
- diags.push({
2390
- level: 'warning',
2391
- code: 'AGENT_EMPTY_INPUT_FLOOR',
2392
- message: `Agent step "${step}" binds no inputs. The platform does not send an empty body for an agent the way `
2393
- + `it does for the other job families — flow-v1's \`applyEmptyInputFloor\` seeds `
2394
- + `\`JobArguments: { input: "" }\` (or \`{}\` / \`[]\`, typed off the agent's declared \`input\` field), `
2395
- + `because the Python agent runner validates the body against a schema with a REQUIRED \`input\` field `
2396
- + `and rejects a bare \`{}\` with "Field required: input" — the pod then exits 0x33. So the deployed `
2397
- + `node carries an argument named \`input\` that you did not write, and which is not in the agent's own `
2398
- + `schema unless it happens to declare one. Locally the envelope passes straight through, so no rung `
2399
- + `sees the difference. Bind the agent's own arguments if it takes any.`,
2400
- step,
2401
- });
2402
- }
2403
- if (inputs?.returns && Object.keys(inputs.returns).length === 0) {
2404
- diags.push({
2405
- level: 'warning',
2406
- code: 'AGENT_EMPTY_RETURNS',
2407
- message: `Agent step "${step}" declares \`returns: {}\` — an empty contract, which emits no output schema at `
2408
- + `all and so reads exactly like omitting it. If the agent returns nothing, drop the key; if it `
2409
- + `returns something, name the fields (\`uip maestro flow registry get uipath.core.agent.`
2410
- + `${inputs.key}\` shows them under outputDefinition.output.schema.properties).`,
2411
- step,
2412
- });
2413
- }
2414
- }
2415
- /**
2416
- * A read of a published agent's output: `out('<step>', '<field>')`.
2417
- *
2418
- * The three mistakes are api-workflow's and agentic-process's, plus a fourth this
2419
- * family owns: a read whose CASE differs from the declared field. That is worth its
2420
- * own message because for a while it was a live-only defect rather than a mistake —
2421
- * a poller that read the job through `uip agent run status` got a PascalCased
2422
- * envelope, so `output.Count` was right live and wrong offline while
2423
- * `output.count` was the reverse (flow-builder-sdk #122). Now the agent's own field
2424
- * names arrive verbatim and the wrong case is simply undefined; naming it as a CASE
2425
- * problem rather than an unknown field is the difference between a two-second fix and
2426
- * a re-read of the registry.
2427
- */
2428
- function checkAgentRead(name, field, rest, spec, step, diags) {
2429
- if (field === undefined)
2430
- return; // `out('call')` — the whole output object.
2431
- if (field === 'error')
2432
- return; // the error envelope every agent node declares.
2433
- if (field !== 'output') {
2434
- diags.push({
2435
- level: 'error',
2436
- code: 'AGENT_READ_NOT_OUTPUT',
2437
- message: `Agent "${name}" publishes the job's output arguments under "output", but `
2438
- + `"$vars.${name}.${field}" (read in "${step}") asks for "${field}", which is undefined at run time.`,
2439
- step,
2440
- suggestion: `out('${name}', '<field>')`,
2441
- });
2442
- return;
2443
- }
2444
- const declared = Object.keys(spec.returns ?? {});
2445
- const wanted = (rest ?? '').replace(/^\./, '').split('.')[0];
2446
- if (!wanted)
2447
- return; // `$vars.<name>.output` — the whole object.
2448
- if (declared.length === 0) {
2449
- diags.push({
2450
- level: 'error',
2451
- code: 'AGENT_READ_WITHOUT_RETURNS',
2452
- message: `"$vars.${name}.output.${wanted}" (read in "${step}") reads a field of agent "${name}"'s result, but `
2453
- + `the step declares no \`returns\` — so the node carries no output schema and nothing can tell a real `
2454
- + `field from a typo. The agent's output fields live on the tenant; declare the ones you read. `
2455
- + `\`uip maestro flow registry get uipath.core.agent.${spec.key}\` shows them under `
2456
- + `outputDefinition.output.schema.properties.`,
2457
- step,
2458
- suggestion: `returns: { ${wanted}: 'string' }`,
2459
- });
2460
- return;
2461
- }
2462
- if (declared.includes(wanted))
2463
- return;
2464
- const caseMatch = declared.find((d) => d.toLowerCase() === wanted.toLowerCase());
2465
- if (caseMatch) {
2466
- diags.push({
2467
- level: 'error',
2468
- code: 'AGENT_READ_WRONG_CASE',
2469
- message: `Agent "${name}" declares "${caseMatch}", not "${wanted}" (read as "$vars.${name}.output.${wanted}" `
2470
- + `in "${step}") — the same field, the wrong case. An agent answers with the field names its own `
2471
- + `definition declares, and a read is case-sensitive on every rung, so this is undefined everywhere.`,
2472
- step,
2473
- suggestion: `out('${name}', '${caseMatch}')`,
2474
- });
2475
- return;
2476
- }
2477
- const nearest = nearestName(wanted, declared);
2478
- diags.push({
2479
- level: 'error',
2480
- code: 'AGENT_READ_UNKNOWN_FIELD',
2481
- message: `Agent "${name}" declares no output "${wanted}" (read as "$vars.${name}.output.${wanted}" in "${step}"). `
2482
- + `Its \`returns\` names: ${declared.join(', ')}. A read outside that set is undefined on every rung and `
2483
- + `on the platform.`,
2484
- step,
2485
- ...(nearest ? { suggestion: `out('${name}', '${nearest}')` } : {}),
2486
- });
2487
- }
2488
- // ─── an inline agent's prompt asking for JSON text ───────────────────────────────
2489
- //
2490
- // The phrase table behind `INLINE_AGENT_PROMPT_JSON_TEXT`. It is kept narrow on
2491
- // purpose, because the rule is an error. Three shapes count, and each skips a
2492
- // negation anywhere earlier in the same clause ("Do not return JSON", "Do not wrap
2493
- // the answer in JSON", "Return the items, not as JSON"):
2494
- //
2495
- // - DIRECT: an output verb, then only words from a closed qualifier list, then
2496
- // `json` — "Return ONLY a JSON object", "Return only the typed JSON contract".
2497
- // - VIA: an output verb, up to two words, then `as` / `in` / `with`, qualifiers,
2498
- // `json` — "Format your answer as JSON", "Respond with valid JSON".
2499
- // - BRACE: a quoted-key brace whose key is one of the step's own `returns` keys —
2500
- // `Return ONLY {"category","priority"}` next to `returns: { category, priority }`.
2501
- //
2502
- // A word outside the qualifier list ends a DIRECT match, so JSON as DATA does not
2503
- // count in the DIRECT form: "Answer the question using the JSON data below", "Give
2504
- // a summary of the JSON ticket", "Return the invoice number from the JSON". The VIA
2505
- // form allows two free words, so it can still fire on JSON as a topic ("Answer in
2506
- // Spanish with JSON examples"); the tail skips the common data nouns after `json`
2507
- // (keys, data, record, examples, input) and the JSON-family FORMATS a string field
2508
- // legitimately holds (JSON Schema, JSON Patch, JSON-LD, JSON Lines, …). `write` is
2509
- // not a verb here: "You help users write JSON" is a topic, not an answer shape.
2510
- //
2511
- // The skills repo's preview-doc gate (tests/tasks/uipath-maestro-flow/_shared/
2512
- // test_preview_inline_agent_prompts.py) ports this table to Python and loads a
2513
- // byte-identical copy of tests/fixtures/inline-agent-prompt-vectors.json, so
2514
- // change the table, the fixture and the port together.
2515
- const JSON_TEXT_VERBS = 'return|respond|reply|output|answer|produce|emit|format|give|provide';
2516
- /** Words that are only ever qualifiers of the answer's shape, never the thing it is about. */
2517
- const JSON_TEXT_QUALIFIERS = [
2518
- 'only', 'just', 'exactly', 'strictly', 'solely', 'simply', 'a', 'an', 'one', 'single', 'the',
2519
- 'your', 'its', 'this', 'that', 'valid', 'raw', 'pure', 'plain', 'strict', 'compact', 'minified',
2520
- 'well-formed', 'properly', 'formatted', 'typed', 'declared', 'structured', 'following', 'same',
2521
- 'final', 'back', 'proper', 'correct', 'machine-readable', 'parseable', 'parsable',
2522
- ];
2523
- /** A negation anywhere earlier in the same clause, or anywhere in the match's gap. */
2524
- const JSON_TEXT_NEGATIONS = new Set(['not', 'never', 'no', 'dont', 'without', 'rather', 'instead', 'avoid']);
2525
- const isJsonTextNegation = (w) => JSON_TEXT_NEGATIONS.has(w) || w.endsWith("n't") || w.endsWith('n’t');
2526
- const JSON_TEXT_WORD = "[A-Za-z][\\w'’-]*";
2527
- /** Words are separated by spaces, tabs or a comma — never a newline or a full stop. */
2528
- const JSON_TEXT_SEP = '[ \\t]*,?[ \\t]+';
2529
- const JSON_TEXT_QUAL = `(?:${[...JSON_TEXT_QUALIFIERS].sort((a, b) => b.length - a.length).join('|')})\\b`;
2530
- // `(?<![-\w])` skips `--output json`; `(?!\.\w)` skips `JSON.parse` / `JSON.stringify`;
2531
- // `(?!-(?:ld|rpc)\b)` skips JSON-LD / JSON-RPC (`\b` matches before the hyphen);
2532
- // the last lookahead skips "the JSON below" / "the JSON provided" / "the JSON you
2533
- // get", data nouns ("the JSON keys", "JSON examples") and JSON-family formats
2534
- // ("JSON Schema", "JSON Patch", "the JSON path", "a JSON Web Token").
2535
- const JSON_TEXT_TAIL = `json\\b(?!\\.\\w)(?!-(?:ld|rpc)\\b)(?![ \\t]+(?:below|above|provided|given|you`
2536
- + `|keys?|data|records?|examples?|inputs?`
2537
- + `|schemas?|patch|path|pointer|lines|rpc|web[ \\t]+tokens?)\\b)`;
2538
- const JSON_TEXT_VERB = `(?<![-\\w])(?:${JSON_TEXT_VERBS})\\b`;
2539
- const JSON_TEXT_DIRECT = new RegExp(`${JSON_TEXT_VERB}((?:${JSON_TEXT_SEP}${JSON_TEXT_QUAL}){0,5})${JSON_TEXT_SEP}${JSON_TEXT_TAIL}`, 'gi');
2540
- const JSON_TEXT_VIA = new RegExp(`${JSON_TEXT_VERB}((?:${JSON_TEXT_SEP}${JSON_TEXT_WORD}){0,2}?)${JSON_TEXT_SEP}(?:as|in|with)`
2541
- + `((?:${JSON_TEXT_SEP}${JSON_TEXT_QUAL}){0,4})${JSON_TEXT_SEP}${JSON_TEXT_TAIL}`, 'gi');
2542
- const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2543
- /**
2544
- * The phrase in `text` that tells a model to answer in JSON TEXT, or `undefined`.
2545
- *
2546
- * `check` uses it for `INLINE_AGENT_PROMPT_JSON_TEXT`, passing the step's `returns`
2547
- * keys so the brace form only counts a brace that spells one of them. Without
2548
- * `returnKeys` (the example and doc sweeps) any quoted identifier key counts.
2549
- * `{{input.*}}` placeholders are removed first, so an input named `json` never
2550
- * matches.
2551
- *
2552
- * @example
2553
- * ```ts
2554
- * jsonTextDirective('Return ONLY a JSON object with keys "subject" and "body".'); // 'Return ONLY a JSON'
2555
- * jsonTextDirective('Return ONLY {"category","priority"}.', ['category']); // '{"category",'
2556
- * jsonTextDirective('Return a result conforming to the output schema.'); // undefined
2557
- * jsonTextDirective('Do not return JSON.'); // undefined
2558
- * jsonTextDirective('Answer the question using the JSON data below.'); // undefined
2559
- * ```
2560
- */
2561
- export function jsonTextDirective(text, returnKeys) {
2562
- if (typeof text !== 'string' || text === '')
2563
- return undefined;
2564
- // A placeholder becomes a hard break (`·` is neither a word nor a gap), and a
2565
- // quoted or back-ticked `JSON` is read as the bare word.
2566
- const t = text
2567
- .replace(/\{\{\s*input\.[^}]*\}\}/g, ' · ')
2568
- .replace(/[`"'“”](json)[`"'“”]/gi, ' $1 ');
2569
- const words = (s) => (s.match(new RegExp(JSON_TEXT_WORD, 'g')) ?? []).map((w) => w.toLowerCase());
2570
- const keys = returnKeys?.length ? returnKeys.map(escapeRegExp).join('|') : '[A-Za-z_]\\w*';
2571
- const brace = new RegExp(`\\{[ \\t]*["'](?:${keys})["'][ \\t]*[,:}]`, 'g');
2572
- for (const rule of [JSON_TEXT_DIRECT, JSON_TEXT_VIA, brace]) {
2573
- rule.lastIndex = 0;
2574
- for (const m of t.matchAll(rule)) {
2575
- if (m.slice(1).some((g) => words(g ?? '').some(isJsonTextNegation)))
2576
- continue;
2577
- // Any negation earlier in the same clause: "Do not wrap the answer in JSON",
2578
- // "No need to return JSON". A comma ends a clause, so "Do not guess, return
2579
- // ONLY a JSON object" still counts.
2580
- const clause = t.slice(0, m.index).split(/[.;:!?\n,]/).pop() ?? '';
2581
- if (words(clause).some(isJsonTextNegation))
2582
- continue;
2583
- return m[0].trim();
2584
- }
2585
- }
2586
- return undefined;
2587
- }
2588
- /**
2589
- * Check an `inlineAgent(...)` call.
2590
- *
2591
- * The interesting difference from every other node this SDK authors: **nothing here
2592
- * lives on a tenant.** An inline agent's whole contract is in the call, so these
2593
- * rules can be certain rather than best-effort — the definition's own `required`
2594
- * list is checkable, and so is whether the prompts and the declared variables agree.
2595
- *
2596
- * Three rules exist because the PLATFORM does something with what you wrote:
2597
- *
2598
- * - `INLINE_AGENT_SCAFFOLD_MODEL` — the model the platform's own scaffold ships is
2599
- * stale, and v1's smoke check for this node type fails a flow that keeps it. A
2600
- * warning, not an error: it is a real model id, it just should not be a default
2601
- * nobody chose.
2602
- * - `INLINE_AGENT_UNREFERENCED_INPUT` — flow-v1 derives the deployed input list by
2603
- * scanning the PROMPTS for `$vars.*` references and only falls back to the
2604
- * authored `agentInputVariables` when that pre-pass did not run. So a declared
2605
- * input the prompts never mention is pruned deployed while arriving locally: a
2606
- * disagreement between rungs and the platform, which is the one class that has to
2607
- * be said out loud rather than left to a run.
2608
- * - `INLINE_AGENT_PROMPT_JSON_TEXT` — the platform returns `returns` as a typed
2609
- * object: it becomes the `agent.json` `outputSchema`, and the agent runtime ends
2610
- * a run with a structured call that fills exactly those fields. A prompt that
2611
- * ALSO asks for JSON text makes the model pack its whole answer, serialized, into
2612
- * one string field. See {@link jsonTextDirective}.
2613
- */
2614
- function checkInlineAgent(step, inputs, diags, opts = {}, ledger = newResourceIdLedger()) {
2615
- const at = (code, message, suggestion) => {
2616
- diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
2617
- };
2618
- const warn = (code, message) => {
2619
- diags.push({ level: 'warning', code, message, step });
2620
- };
2621
- const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
2622
- // `memory` is authorable in this SDK and NOT authorable on the platform. The
2623
- // bundled `uipath.agent.autonomous@1.4` carries a `memory` handle added by
2624
- // #431; the tenant's own manifest for that node type declares only
2625
- // `escalation, context, tool, success, error`, and the registry index (4977
2626
- // entries, 2026-09-09) contains NO `uipath.agent.resource.memory.*` type at
2627
- // all — so both halves of what `memory` emits are unknown to the platform.
2628
- // `context` is not the alternative: those resources are knowledge INDEXES
2629
- // (`…context.index.<slug>.<guid>`), i.e. context grounding, not episodic memory.
2630
- //
2631
- // Compile succeeds and `uip maestro flow validate` then hard-refuses the edge:
2632
- //
2633
- // [edges[edge_0].sourcePort] Edge references source handle "memory" on node
2634
- // "triage", but the current manifest for "uipath.agent.autonomous" does not
2635
- // declare that handle … Retry: RetryWillNotFix
2636
- //
2637
- // An artifact that compiles and can never validate is the worst of the two
2638
- // failures, so this is said here — before the emit — rather than left to a rung.
2639
- // It is an ERROR because there is no spelling that works today, and `--no-check`
2640
- // remains the escape for a tenant whose manifest does declare the handle.
2641
- if (inputs?.memory !== undefined) {
2642
- at('AGENT_MEMORY_NOT_AUTHORABLE', `Inline agent step "${step}" attaches \`memory\`, which the platform cannot resolve: the tenant's `
2643
- + `\`uipath.agent.autonomous\` manifest declares no \`memory\` handle (only escalation, context, tool, `
2644
- + `success, error) and the registry serves no \`uipath.agent.resource.memory.*\` node type. The flow `
2645
- + `compiles and then fails \`uip maestro flow validate\` as RetryWillNotFix. Drop \`memory\`; for `
2646
- + `retrieval over a knowledge source use a context index instead (\`tools\`/context grounding). `
2647
- + `Pass --no-check only on a tenant whose manifest does declare the handle.`);
2648
- }
2649
- // A context `query` is what the RUNTIME searches with, and it is not an
2650
- // expression slot. A plain string is the model-invoked `dynamic` variant: the
2651
- // text becomes the description the model reads before it writes its own query.
2652
- // An expression renders as the `static` variant instead, whose `value` the
2653
- // retriever is handed VERBATIM — there is no `$vars` scope inside the agent and
2654
- // nothing templates it once `uip agent refresh` has stripped the node, so the
2655
- // search runs on the literal text `=js:$vars…`. With the default `threshold: 0`
2656
- // it still returns chunks, so it reads as working while retrieving on nonsense.
2657
- // Refused rather than emitted until the argument-bound form is implemented
2658
- // (`{ variant: 'argument' }`, which the runtime resolves from agent state).
2659
- const contexts = inputs?.context === undefined ? []
2660
- : Array.isArray(inputs.context) ? inputs.context : [inputs.context];
2661
- for (const cx of contexts) {
2662
- if (cx?.query !== undefined && typeof cx.query !== 'string') {
2663
- at('AGENT_CONTEXT_QUERY_NOT_EXPRESSION', `Inline agent step "${step}" gives its context index "${cx.name ?? ''}" an EXPRESSION \`query\`. `
2664
- + `The runtime searches with that value literally — it is not evaluated inside the agent — so `
2665
- + `retrieval runs on the text of the expression and quietly returns unrelated chunks. Use a plain `
2666
- + `string: it becomes the guidance the model reads before writing its own query, which is what a `
2667
- + `run-dependent search needs.`);
2668
- }
2669
- }
2670
- // The index id is half of the resource node's TYPE
2671
- // (`…context.index.<slug>.<id>`), and the platform resolves the index by it.
2672
- // A blank, padded or non-GUID id builds a type no tenant serves, and nothing
2673
- // downstream says so: the flow compiles, merges and validates, and the agent
2674
- // runs ungrounded. So it is an error, not a warning. The id is tested as
2675
- // written, not trimmed, because serialize emits it as written.
2676
- for (const cx of contexts) {
2677
- const id = typeof cx?.id === 'string' ? cx.id : '';
2678
- if (!GUID_RE.test(id)) {
2679
- at('AGENT_CONTEXT_NO_ID', `Inline agent step "${step}" grounds on context index "${cx?.name ?? ''}" with `
2680
- + (id.trim() === '' ? 'no `id`'
2681
- : GUID_RE.test(id.trim()) ? `\`id\` ${JSON.stringify(cx?.id)}, which has whitespace around the GUID`
2682
- : `\`id\` ${JSON.stringify(cx?.id)}, which is not a GUID`) + '. '
2683
- + `The id is the index's key: it is the tail of the resource node's type and how the runtime finds `
2684
- + `the index, so without a real one the agent runs ungrounded and \`validate\` does not notice. `
2685
- + `\`uip solution resources list --kind Index --source remote --search "<name>"\` returns it as \`Key\`.`);
2686
- }
2687
- }
2688
- // An index outside the deployment folder needs its folder named. The resource
2689
- // file carries no `indexId`, so `indexName` + `folderPath` is the whole locator,
2690
- // and `@uipath/tool-agent`'s `createIndexBinding` leaves the folder off the
2691
- // binding when `folderPath` is empty. A WARNING, not an error: an index in the
2692
- // deployment folder resolves without it.
2693
- for (const cx of contexts) {
2694
- if (cx != null && cx.folderPath === undefined) {
2695
- warn('AGENT_CONTEXT_NO_FOLDER_PATH', `Inline agent step "${step}" grounds on context index "${cx.name ?? ''}" without a \`folderPath\`. `
2696
- + `The emitted resource carries no index id, so the folder is half of how the runtime finds the `
2697
- + `index, and the binding is written without one. Give the index's folder — `
2698
- + `\`uip solution resources list --kind Index --source remote --search "<name>"\` returns it.`);
2699
- }
2700
- }
2701
- // The definition's own `required`: systemPrompt, userPrompt, model — each with
2702
- // `minLength: 1`, so a blank string is as invalid as a missing key.
2703
- if (blank(inputs?.model)) {
2704
- at('INLINE_AGENT_NO_MODEL', `Inline agent step "${step}" has no \`model\`. The node's definition requires one (\`minLength: 1\`), so `
2705
- + `a flow without it does not validate. \`uip agent model list\` shows what the tenant offers.`, `model: 'gpt-5.4'`);
2706
- }
2707
- if (blank(inputs?.systemPrompt)) {
2708
- at('INLINE_AGENT_NO_SYSTEM_PROMPT', `Inline agent step "${step}" has no \`systemPrompt\`. The node's definition requires one `
2709
- + `(\`minLength: 1\`). It is where the agent is told what it is and what shape to answer in.`);
2710
- }
2711
- if (blank(inputs?.userPrompt)) {
2712
- at('INLINE_AGENT_NO_USER_PROMPT', `Inline agent step "${step}" has no \`userPrompt\`. The node's definition requires one `
2713
- + `(\`minLength: 1\`). It is the per-run request.`);
2714
- }
2715
- const model = typeof inputs?.model === 'string' ? inputs.model.trim() : '';
2716
- if (model === 'gpt-4o-2024-11-20') {
2717
- warn('INLINE_AGENT_SCAFFOLD_MODEL', `Inline agent step "${step}" uses \`gpt-4o-2024-11-20\` — the model the platform's own scaffold ships, `
2718
- + `and the one v1's smoke check for this node type FAILS a flow for keeping (it is the definition's `
2719
- + `\`inputDefaults.model\`, not a choice). Pick a current model: \`uip agent model list\`.`);
2720
- }
2721
- // Where each resource node sits (`nodeId`, `source`): decompile writes them
2722
- // for a node the canvas placed, and compile writes them as given. A blank or
2723
- // repeated id would make compile refuse the flow or overwrite another node,
2724
- // and a `source` that is not a uuid names a directory the platform does not
2725
- // expect (the agent's own `source` has the same rule).
2726
- const asList = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
2727
- const placed = [
2728
- ...asList(inputs?.context).map((r) => ({ what: `context index "${r?.name ?? ''}"`, kind: 'Context', nodeId: r?.nodeId, source: r?.source })),
2729
- ...asList(inputs?.tools).map((r) => ({ what: `${r?.kind ?? ''} tool`, kind: 'Tool', nodeId: r?.nodeId, source: r?.source })),
2730
- ...asList(inputs?.escalation).map((r) => ({ what: `escalation "${r?.name ?? ''}"`, kind: 'Escalation', nodeId: r?.nodeId, source: r?.source })),
2731
- ];
2732
- // The ids compile derives for the resources with no `nodeId`: the n-th one of
2733
- // a kind gets `<step><Kind>`, then `<step><Kind>2`, ... An explicit id equal to
2734
- // one of them makes compile refuse the flow, or rename the derived node.
2735
- for (const kind of ['Context', 'Tool', 'Escalation']) {
2736
- const derived = placed.filter((r) => r.kind === kind && r.nodeId === undefined);
2737
- derived.forEach((r, n) => {
2738
- const id = derivedResourceNodeId(step, kind, n);
2739
- if (!ledger.taken.has(id))
2740
- ledger.taken.set(id, `the id compile derives for the ${r.what} of inline agent step "${step}"`);
2741
- });
2742
- }
2743
- /** Explicit `source` -> the resource on THIS agent that has it: two would write one resource.json. */
2744
- const sources = new Map();
2745
- for (const r of placed) {
2746
- if (r.nodeId !== undefined) {
2747
- const id = typeof r.nodeId === 'string' ? r.nodeId : '';
2748
- const other = ledger.explicit.get(id);
2749
- if (id.trim() === '') {
2750
- at('INLINE_AGENT_RESOURCE_NODE_ID', `Inline agent step "${step}" gives its ${r.what} the \`nodeId\` ${JSON.stringify(r.nodeId)}, which is `
2751
- + `not a node id. Omit \`nodeId\` to derive one, or give the id the node has in the flow.`);
2752
- }
2753
- else if (other !== undefined) {
2754
- at('INLINE_AGENT_RESOURCE_NODE_ID', `Inline agent step "${step}" gives its ${r.what} the \`nodeId\` "${id}", which the ${other.what} `
2755
- + `(on step "${other.step}") already has. Node ids are unique in a flow.`);
2756
- }
2757
- else {
2758
- ledger.explicit.set(id, { step, what: r.what });
2759
- }
2760
- }
2761
- if (r.source === undefined)
2762
- continue;
2763
- // Not trimmed: compile writes `source` as given, into `inputs.source` and the directory name.
2764
- if (!GUID_RE.test(String(r.source))) {
2765
- at('INLINE_AGENT_RESOURCE_SOURCE_NOT_GUID', `Inline agent step "${step}" gives its ${r.what} the \`source\` ${JSON.stringify(r.source)}, which is not `
2766
- + `a uuid${/^\s|\s$/.test(String(r.source)) ? ' (it has spaces around it)' : ''}. \`source\` is the resource's `
2767
- + `directory under the agent's own (\`resources/<source>/resource.json\`) and the node's \`inputs.source\`. `
2768
- + 'Omit it and one is derived from the flow id, the step and the resource.');
2769
- continue;
2770
- }
2771
- const key = String(r.source).toLowerCase();
2772
- const first = sources.get(key);
2773
- if (first !== undefined) {
2774
- at('INLINE_AGENT_RESOURCE_SOURCE_DUP', `Inline agent step "${step}" gives its ${first} and its ${r.what} the same \`source\` "${String(r.source)}". `
2775
- + 'Both would write `resources/<source>/resource.json` under this agent, and the second file would replace '
2776
- + 'the first. Give each resource its own `source`, or omit `source` to derive one.');
2777
- }
2778
- else {
2779
- sources.set(key, r.what);
2780
- }
2781
- }
2782
- if (typeof inputs?.source === 'string' && GUID_RE.test(inputs.source.trim())) {
2783
- const key = inputs.source.trim().toLowerCase();
2784
- const other = ledger.agentSources.get(key);
2785
- if (other !== undefined && other !== step) {
2786
- at('INLINE_AGENT_RESOURCE_SOURCE_DUP', `Inline agent steps "${other}" and "${step}" have the same \`source\` "${inputs.source}". Both would write `
2787
- + '`<source>/agent.json`, and the second file would replace the first. Give each agent its own `source`, '
2788
- + 'or omit it to derive one.');
2789
- }
2790
- else {
2791
- ledger.agentSources.set(key, step);
2792
- }
2793
- }
2794
- if (inputs?.source !== undefined && !GUID_RE.test(String(inputs.source).trim())) {
2795
- at('INLINE_AGENT_SOURCE_NOT_GUID', `Inline agent step "${step}"'s \`source\` is ${JSON.stringify(inputs.source)}, which is not a uuid. `
2796
- + `\`source\` is the agent's own DIRECTORY name, and the platform resolves its configuration at `
2797
- + `"<source>/agent.json" — a non-uuid name is not what the designer or the runtime expect. Omit it and `
2798
- + `one is derived from the flow id and step name (stable across recompiles).`);
2799
- }
2800
- if (inputs?.returns && Object.keys(inputs.returns).length === 0) {
2801
- warn('INLINE_AGENT_EMPTY_RETURNS', `Inline agent step "${step}" declares \`returns: {}\`. That is not "no output" — it falls back to the `
2802
- + `definition's default, \`{ content: 'string' }\`, which is the scaffold shape. Declare the fields the `
2803
- + `agent must return, or drop the key and read \`content\`.`);
2804
- }
2805
- // A prompt that asks for JSON TEXT while `returns` declares the fields. The
2806
- // platform already returns those fields as a typed object — `returns` is the
2807
- // agent.json `outputSchema`, and the runtime ends a run with a structured call
2808
- // that fills exactly those keys — so the extra directive does not shape the
2809
- // answer, it gets serialized INTO it: on 2026-09-23 a writer declaring
2810
- // `{ subject, body }` came back with `body` = '{"subject":…,"body":…}' in 3 of 3
2811
- // debug runs, and the flow mailed the JSON. `uip maestro flow validate` accepts
2812
- // the prompt (it never reads prompt meaning), so this is where it is said. An
2813
- // ERROR for the same reason `INLINE_AGENT_UNDECLARED_PLACEHOLDER` is one: the
2814
- // platform accepts the shape and the run still delivers the wrong value, and a
2815
- // warning is printed past while compile writes the .flow anyway. Only when
2816
- // `returns` is declared: with the `{ content }` default, JSON text in `content`
2817
- // may be exactly what the author reads back.
2818
- const returnKeys = Object.keys(inputs?.returns ?? {});
2819
- if (returnKeys.length) {
2820
- for (const field of ['systemPrompt', 'userPrompt']) {
2821
- const phrase = jsonTextDirective(String(inputs?.[field] ?? ''), returnKeys);
2822
- if (phrase === undefined)
2823
- continue;
2824
- const rewrite = `Return a result conforming to the output schema. `
2825
- + returnKeys.map((k) => `${k}: <what it holds>.`).join(' ');
2826
- at('INLINE_AGENT_PROMPT_JSON_TEXT', `Inline agent step "${step}"'s \`${field}\` tells the model to answer in JSON text ("${phrase}"), `
2827
- + `but \`returns\` already declares the answer: ${returnKeys.map((k) => `\`${k}\``).join(', ')}. `
2828
- + `The platform returns those fields as a typed object — \`returns\` becomes the agent.json `
2829
- + `\`outputSchema\`, and the agent runtime ends each run with a structured call that fills exactly `
2830
- + `those keys — so \`out('${step}', '${returnKeys[0]}')\` reads a value, not text to parse. Asking `
2831
- + `for JSON as well makes the model pack its whole answer, serialized, into one string field (for `
2832
- + `example a \`body\` that reads '{"subject":…,"body":…}'). Replace only that JSON instruction (keep the rest `
2833
- + `of the prompt) with one line per field, in the platform's own wording: "${rewrite}" Declare a field \`'object'\` or `
2834
- + `\`'array'\` if it should carry structure. \`uip maestro flow validate\` accepts this prompt, so `
2835
- + `nothing later catches it. If this is a false positive (the prompt only mentions JSON as data), `
2836
- + `compile with --no-check.`);
2837
- }
2838
- }
2839
- // The prompts-vs-variables agreement, in both directions. Both prompts are
2840
- // searched, because flow-v1's pre-pass scans both.
2841
- const prompts = `${String(inputs?.systemPrompt ?? '')}\n${String(inputs?.userPrompt ?? '')}`;
2842
- const placeholders = [...prompts.matchAll(/\{\{\s*input\.([A-Za-z_$][A-Za-z0-9_$]*)\s*\}\}/g)]
2843
- .map((m) => m[1]);
2844
- const declaredInputs = Object.keys(inputs?.inputs ?? {});
2845
- // Direction 1: a declared input no prompt references. The platform may prune it,
2846
- // in which case the agent receives nothing and the authored list sits unused.
2847
- const unreferenced = declaredInputs.filter((k) => !placeholders.includes(k));
2848
- if (declaredInputs.length && unreferenced.length === declaredInputs.length) {
2849
- warn('INLINE_AGENT_UNREFERENCED_INPUT', `Inline agent step "${step}" declares input(s) ${unreferenced.map((u) => `\`${u}\``).join(', ')} that `
2850
- + `neither prompt references as \`{{input.<name>}}\` — so NONE of them is referenced. Deployed, the `
2851
- + `authored list is not necessarily what the platform sends: flow-v1's `
2852
- + `\`preDeriveAgentInputDefinitions\` pre-pass builds the input list by scanning the node's prompts for `
2853
- + `variable references, merges what it finds with the authored list, and DROPS an entry no reference `
2854
- + `points at when its caller asks for pruning. A referenced input survives every path; an unreferenced `
2855
- + `one survives only some, and the failure mode is an agent that arrives with no arguments at all. `
2856
- + `Write \`{{input.${declaredInputs[0]}}}\` in a prompt — which is also how the agent gets told what it `
2857
- + `is looking at.`);
2858
- }
2859
- else if (unreferenced.length) {
2860
- warn('INLINE_AGENT_UNREFERENCED_INPUT', `Inline agent step "${step}" declares input(s) ${unreferenced.map((u) => `\`${u}\``).join(', ')} that `
2861
- + `neither prompt references as \`{{input.<name>}}\`. The platform may prune an unreferenced input, so `
2862
- + `it is not reliably sent — and an input the prompts never mention is one the agent cannot use anyway.`);
2863
- }
2864
- // Direction 2: a placeholder naming an input that does not exist. The compiler
2865
- // leaves it alone rather than deleting it, so it reaches the model as the literal
2866
- // text `{{input.x}}` — visible, but not what anyone meant.
2867
- const undeclared = [...new Set(placeholders.filter((p) => !declaredInputs.includes(p)))];
2868
- if (undeclared.length) {
2869
- at('INLINE_AGENT_UNDECLARED_PLACEHOLDER', `Inline agent step "${step}" references ${undeclared.map((u) => `\`{{input.${u}}}\``).join(', ')} in a `
2870
- + `prompt, but \`inputs\` declares ${declaredInputs.length ? declaredInputs.map((d) => `\`${d}\``).join(', ') : 'nothing'}. `
2871
- + `An unmatched placeholder is not substituted — it reaches the model as that literal text, and the `
2872
- + `value it was meant to carry never arrives.`, declaredInputs.length
2873
- ? `inputs: { ${undeclared[0]}: input('${undeclared[0]}'), … }`
2874
- : `inputs: { ${undeclared[0]}: input('${undeclared[0]}') }`);
2875
- }
2876
- // Direction 3: an input bound to a COMPUTED EXPRESSION rather than to one
2877
- // reference. The platform names each of an agent's arguments after the
2878
- // reference path it is bound to (the shell node's `agentInputVariables`), so
2879
- // an expression spanning zero or several references has no single argument —
2880
- // and therefore no name a `{{input.<x>}}` token could address. The compiler
2881
- // declares no argument for it, so its value does not reach the agent, and the
2882
- // placeholder reaches the model as literal text. An error rather than a
2883
- // warning, and for the same reason `INLINE_AGENT_UNDECLARED_PLACEHOLDER` is
2884
- // one: the value never arrives.
2885
- for (const [name, val] of Object.entries(inputs?.inputs ?? {})) {
2886
- if (!(val instanceof Expr) || val.literal)
2887
- continue;
2888
- if (flatNameOf(val.js) !== undefined)
2889
- continue;
2890
- if (!placeholders.includes(name))
2891
- continue;
2892
- at('INLINE_AGENT_COMPOSITE_INPUT', `Inline agent step "${step}"'s input \`${name}\` is bound to a computed expression `
2893
- + `(\`${val.js.length > 60 ? `${val.js.slice(0, 60)}…` : val.js}\`), and a prompt references it as `
2894
- + `\`{{input.${name}}}\`. The platform does not send the agent the names you declared: it names each `
2895
- + `argument after the REFERENCE PATH the input is bound to (\`$vars.a.output.b\` → \`a__output__b\`). `
2896
- + `A computed expression is not one reference, so it gets no argument: its value does not reach the `
2897
- + `agent, and the placeholder reaches the model as that literal text.`, `compute it in a script step first, then bind the step's output: `
2898
- + `.step('${name}Value', script({ code: 'return …;' })) … inputs: { ${name}: out('${name}Value') }`);
2899
- }
2900
- // A step id containing `__` makes the flat name AMBIGUOUS on the platform: the
2901
- // encoding joins path segments with `__` and v1's own inverse (`decodeFlatName`)
2902
- // documents that it round-trips only while ids "disallow `__`". A warning, not
2903
- // an error — the argument is still SENT and our token still matches it, so the
2904
- // agent works; what breaks is anything that decodes the name back to a path
2905
- // (the CLI's `hydrateInlineAgentInputs` reconcile, and the designer).
2906
- for (const [name, val] of Object.entries(inputs?.inputs ?? {})) {
2907
- if (!(val instanceof Expr) || val.literal)
2908
- continue;
2909
- const segments = flatNameSegmentsOf(val.js);
2910
- const offender = segments?.find((s) => s.includes(FLAT_SEP));
2911
- if (!offender)
2912
- continue;
2913
- warn('INLINE_AGENT_INPUT_NAME_AMBIGUOUS', `Inline agent step "${step}"'s input \`${name}\` reads \`${val.js}\`, whose path segment `
2914
- + `\`${offender}\` contains \`${FLAT_SEP}\`. The platform names the argument by joining the path's `
2915
- + `segments with \`${FLAT_SEP}\` (\`${segments.join(FLAT_SEP)}\`), so that name no longer says where `
2916
- + `the value came from — v1's own inverse round-trips only while ids avoid \`${FLAT_SEP}\`. The `
2917
- + `argument is still sent and the prompt still reads it; what breaks is anything decoding the name `
2918
- + `back to a path. Rename the step or variable to drop the double underscore.`);
2919
- }
2920
- // GUARDRAILS (Phase 4F). The node's definition types the input only as
2921
- // `array`, so a malformed entry deploys silently and either never trips or
2922
- // blocks every run — both invisible until production. The shapes below are
2923
- // Agent Builder's own discriminators.
2924
- const rails = inputs?.guardrails;
2925
- if (rails !== undefined) {
2926
- const ids = new Set();
2927
- rails.forEach((g, i) => {
2928
- const label = g?.name ? `"${g.name}"` : `guardrails[${i}]`;
2929
- if (!g || (g.$guardrailType !== 'custom' && g.$guardrailType !== 'builtInValidator')) {
2930
- at('GUARDRAIL_TYPE_UNKNOWN', `Inline agent step "${step}"'s ${label} has the $guardrailType `
2931
- + `${JSON.stringify(g?.$guardrailType)}. Valid: 'custom' `
2932
- + `(rules you author) or 'builtInValidator' (a platform validator).`);
2933
- return;
2934
- }
2935
- if (blank(g.id) || blank(g.name)) {
2936
- at('GUARDRAIL_INCOMPLETE', `Inline agent step "${step}"'s ${label} is missing ${blank(g.id) ? 'an id' : 'a name'} — `
2937
- + `both are required by the agent schema (the id keys the rail, the name is what trips show).`);
2938
- return;
2939
- }
2940
- if (ids.has(g.id)) {
2941
- at('GUARDRAIL_DUP_ID', `Inline agent step "${step}" declares the guardrail id "${g.id}" twice — the second silently `
2942
- + `shadows the first in Agent Builder's keyed storage.`);
2943
- return;
2944
- }
2945
- ids.add(g.id);
2946
- if (!Array.isArray(g.selector?.scopes) || g.selector.scopes.length === 0) {
2947
- at('GUARDRAIL_NO_SCOPE', `Inline agent step "${step}"'s guardrail "${g.name}" selects no scopes — it can never apply.`, `selector: { scopes: ['Agent'] }`);
2948
- }
2949
- const act = g.action?.$actionType;
2950
- if (act !== 'block' && act !== 'filter' && act !== 'log') {
2951
- at('GUARDRAIL_ACTION_UNKNOWN', `Inline agent step "${step}"'s guardrail "${g.name}" has the action ${JSON.stringify(act)}. `
2952
- + `Valid: 'block', 'filter', 'log'.`);
2953
- }
2954
- if (g.$guardrailType === 'custom' && (!Array.isArray(g.rules) || g.rules.length === 0)) {
2955
- at('GUARDRAIL_NO_RULES', `Inline agent step "${step}"'s custom guardrail "${g.name}" has no rules — nothing can ever `
2956
- + `trip it, so the action never fires.`, `rules: [{ $ruleType: 'always', applyTo: 'inputAndOutput' }]`);
2957
- }
2958
- if (g.$guardrailType === 'builtInValidator' && blank(g.validatorType)) {
2959
- at('GUARDRAIL_NO_VALIDATOR', `Inline agent step "${step}"'s guardrail "${g.name}" names no validatorType — the platform `
2960
- + `cannot resolve which built-in validator to run.`);
2961
- }
2962
- });
2963
- }
2964
- // TOOLS and ESCALATION (card G16). Cheap identity checks only: a wrong KIND
2965
- // or a non-uuid KEY emits a node type the tenant has never heard of, which
2966
- // nothing downstream of the compiler can see — the resource is dropped from
2967
- // emitted graph, so no rung ever exercises it. Library resolution for connector
2968
- // tools happens at compile (where the library is), not here.
2969
- const toolRefs = inputs?.tools === undefined ? []
2970
- : Array.isArray(inputs.tools) ? inputs.tools : [inputs.tools];
2971
- const BUILTIN_TOOLS = ['analyzefiles', 'summarize', 'batchtransform'];
2972
- for (const t of toolRefs) {
2973
- const kind = t?.kind;
2974
- if (kind === 'builtin') {
2975
- if (!BUILTIN_TOOLS.includes(String(t.tool))) {
2976
- at('INLINE_AGENT_TOOL_UNKNOWN_BUILTIN', `Inline agent step "${step}" declares a builtin tool ${JSON.stringify(t.tool)}. `
2977
- + `The platform ships exactly three: ${BUILTIN_TOOLS.map((b) => `'${b}'`).join(', ')} — the node type is a `
2978
- + `fixed string, so any other name is a node the tenant has never heard of.`, `{ kind: 'builtin', tool: 'summarize' }`);
2979
- }
2980
- }
2981
- else if (kind === 'connector') {
2982
- const c = t;
2983
- if (blank(c.connector) || blank(c.operation)) {
2984
- at('INLINE_AGENT_TOOL_NO_OPERATION', `Inline agent step "${step}" declares a connector tool without ${blank(c.connector) ? 'a `connector`' : 'an `operation`'}. `
2985
- + `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' }`);
2986
- }
2987
- // A connector tool calls Integration Service on a CONNECTION, exactly as a
2988
- // connector step does — and before #867 there was no way to give it one, so
2989
- // the tool compiled with none anywhere (no binding, no resource.json row)
2990
- // and could only fail at run time. Both halves, as `registry prepare` writes them.
2991
- const labels = t;
2992
- const missing = ['connection', 'folder'].filter((k) => blank(labels[k]));
2993
- if (missing.length > 0) {
2994
- const key = blank(c.connector) ? '<connector-key>' : String(c.connector);
2995
- const op = blank(c.operation) ? '<operation>' : String(c.operation);
2996
- at('INLINE_AGENT_TOOL_CONNECTOR_NO_CONNECTION', `Inline agent step "${step}"'s connector tool "${key}/${op}" has no ${missing.map((m) => `\`${m}\``).join(' or ')}. `
2997
- + `The tool calls Integration Service on a tenant connection: without one the agent's tool has no `
2998
- + `connection binding, its resource.json carries none, and the solution's Resources never list it. `
2999
- + `Name the bindings.json labels the same way \`connector()\` takes them — \`${prepareCommand(key, op)}\` `
3000
- + `finds the connection and writes both entries.`, `{ kind: 'connector', connector: '${key}', operation: '${op}', connection: '<connection label>', folder: '<folder label>' }`);
3001
- }
3002
- else {
3003
- checkBindingLabels(`Inline agent step "${step}"'s connector tool`, labels, opts, (code, message) => at(code, message), blank(c.connector) ? undefined : { key: String(c.connector), ...(blank(c.operation) ? {} : { action: String(c.operation) }) });
3004
- }
3005
- }
3006
- else if (kind === 'ixp') {
3007
- const x = t;
3008
- if (blank(x.projectId) || !GUID_RE.test(String(x.projectId).trim())) {
3009
- 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. `
3010
- + `It is the node type's tail (uipath.agent.resource.tool.ixp.<slug>.<projectId>), so a wrong one emits a `
3011
- + `node type the tenant cannot resolve. Find it: uip maestro flow registry pull --force && `
3012
- + `uip maestro flow registry search -f "type:startsWith=uipath.agent.resource.tool.ixp".`);
3013
- }
3014
- if (blank(x.name)) {
3015
- 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 `
3016
- + `carries as both its label and \`inputDefaults.name\`.`);
3017
- }
3018
- }
3019
- else if (kind === 'process' || kind === 'agent' || kind === 'api' || kind === 'flow'
3020
- || kind === 'maestro' || kind === 'function') {
3021
- const p = t;
3022
- if (blank(p.key) || !GUID_RE.test(String(p.key).trim())) {
3023
- 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. `
3024
- + `The key IS the node type's tail (uipath.agent.resource.tool.…<key>), so a wrong one emits a node type `
3025
- + `the tenant cannot resolve. Find it: uip maestro flow registry search "uipath.agent.resource.tool".`);
3026
- }
3027
- if (blank(p.name) || blank(p.folderPath)) {
3028
- at('INLINE_AGENT_TOOL_NO_RESOURCE_ID', `Inline agent step "${step}"'s '${String(kind)}' tool needs both \`name\` and \`folderPath\` — they form the `
3029
- + `binding's resourceKey ("<folderPath>.<name>"), which is how the deployed flow finds the resource.`);
3030
- }
3031
- }
3032
- else if (kind === 'mcp') {
3033
- const m = t;
3034
- if (blank(m.name) || blank(m.key) || !GUID_RE.test(String(m.key).trim())) {
3035
- 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 `
3036
- + `from them (uipath.agent.resource.tool.mcp.<name>.<key>) and the key is the reference the runtime `
3037
- + `resolves.`);
3038
- }
3039
- }
3040
- else if (kind === 'a2a') {
3041
- const a = t;
3042
- if (blank(a.name) || blank(a.key) || !GUID_RE.test(String(a.key).trim()) || blank(a.slug)) {
3043
- at('INLINE_AGENT_TOOL_A2A_INCOMPLETE', `Inline agent step "${step}"'s remote-A2A tool needs \`name\`, a uuid \`key\`, and \`slug\` — the node `
3044
- + `type is minted from name+key, and the runtime resolves the remote agent by slug (the platform's own `
3045
- + `mapper refuses a blank one).`);
3046
- }
3047
- }
3048
- else if (kind === 'clientside') {
3049
- if (blank(t.name)) {
3050
- 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 `
3051
- + `calling application dispatches on it.`);
3052
- }
3053
- }
3054
- else if (kind === 'httpRequest') {
3055
- const h = t;
3056
- const m = h.method === undefined ? undefined : String(h.method).toUpperCase();
3057
- if (m !== undefined && !['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(m)) {
3058
- at('INLINE_AGENT_TOOL_HTTP_METHOD', `Inline agent step "${step}"'s HTTP-request tool fixes the method to ${JSON.stringify(h.method)}. The `
3059
- + `tool's contract allows GET, POST, PUT, PATCH, DELETE — anything else the runtime refuses at call time.`);
3060
- }
3061
- }
3062
- else {
3063
- at('INLINE_AGENT_TOOL_UNKNOWN_KIND', `Inline agent step "${step}" declares a tool with kind ${JSON.stringify(kind)}. Valid kinds: 'builtin', `
3064
- + `'connector', 'process', 'agent', 'api', 'flow', 'maestro', 'function', 'ixp', 'mcp', 'a2a', `
3065
- + `'clientside', 'httpRequest'.`);
3066
- }
3067
- }
3068
- const escalations = inputs?.escalation === undefined ? []
3069
- : Array.isArray(inputs.escalation) ? inputs.escalation : [inputs.escalation];
3070
- for (const e of escalations) {
3071
- if (blank(e?.name)) {
3072
- at('INLINE_AGENT_ESCALATION_NO_NAME', `Inline agent step "${step}" declares an escalation without a \`name\`. The node's definition requires one `
3073
- + `(\`minLength: 1\`).`);
3074
- }
3075
- if (e?.variant !== undefined && e.variant !== 'quick-form') {
3076
- at('INLINE_AGENT_ESCALATION_VARIANT', `Inline agent step "${step}"'s escalation names the variant ${JSON.stringify(e.variant)}. Valid: `
3077
- + `'quick-form' — or omit it for the app-backed default.`);
3078
- continue;
3079
- }
3080
- if (e?.variant === 'quick-form') {
3081
- // The form lives INLINE: an app would be emitted and ignored, and a form
3082
- // with nothing to fill in is a notification nobody can answer.
3083
- if (e.app !== undefined) {
3084
- 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 — `
3085
- + `drop \`app\`, or drop the variant for the app-backed escalation.`);
3086
- }
3087
- if (!Array.isArray(e.fields) || e.fields.length === 0) {
3088
- at('INLINE_AGENT_ESCALATION_NO_FIELDS', `Inline agent step "${step}"'s quick-form escalation declares no \`fields\` — the reviewer gets an `
3089
- + `empty form.`);
3090
- }
3091
- for (const f of Array.isArray(e.fields) ? e.fields : []) {
3092
- if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
3093
- at('INLINE_AGENT_ESCALATION_FIELD_TYPE_UNKNOWN', `Inline agent step "${step}"'s quick-form escalation gives the field "${String(f?.id ?? '?')}" the type `
3094
- + `${JSON.stringify(f?.type)}, ${formFieldTypeWhy(f?.type, `which the form does not recognize — it would `
3095
- + `be published as a plain string`)}. Valid: ${validFormFieldTypesText()}.`, formFieldTypeHint(f?.type));
3096
- }
3097
- const optionsProblem = formFieldOptionsProblem(f);
3098
- if (optionsProblem !== undefined) {
3099
- const message = `Inline agent step "${step}"'s quick-form escalation field "${String(f?.id ?? '?')}" `
3100
- + `${formFieldOptionsWhy(optionsProblem, f?.type)}.`;
3101
- if (optionsProblem === 'ignored')
3102
- warn('INLINE_AGENT_ESCALATION_FIELD_OPTIONS_IGNORED', message);
3103
- else {
3104
- at('INLINE_AGENT_ESCALATION_FIELD_OPTIONS', message, `options: [{ id: 'opt-1', value: 'high', label: 'High' }, { id: 'opt-2', value: 'low', label: 'Low' }]`);
3105
- }
3106
- }
3107
- for (const problem of formFieldSettingProblems(f)) {
3108
- at('INLINE_AGENT_ESCALATION_FIELD_SETTING_INVALID', `Inline agent step "${step}"'s quick-form escalation gives the field "${String(f?.id ?? '?')}" `
3109
- + `${problem}. The platform refuses the form.`);
3110
- }
3111
- }
3112
- continue;
3113
- }
3114
- if (e?.fields !== undefined) {
3115
- at('INLINE_AGENT_ESCALATION_APP_WITH_FIELDS', `Inline agent step "${step}"'s escalation declares inline \`fields\`, but its form belongs to the deployed `
3116
- + `app. Use variant: 'quick-form' for an inline form.`);
3117
- }
3118
- if (blank(e?.app?.key) || blank(e?.app?.name)) {
3119
- at('INLINE_AGENT_ESCALATION_NO_APP', `Inline agent step "${step}"'s escalation needs \`app.key\` and \`app.name\` — the deployed Action Center app `
3120
- + `that carries the review task. An escalation with no app is a task nobody can open.`);
3121
- }
3122
- else if (!GUID_RE.test(String(e.app.key).trim())) {
3123
- 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. `
3124
- + `It is the app's own key on the tenant, not its name.`);
3125
- }
3126
- }
3127
- // MEMORY (Phase 5A): identity is required — the node type is minted from it.
3128
- const mem = inputs?.memory;
3129
- if (mem !== undefined) {
3130
- if (blank(mem.name) || blank(mem.id)) {
3131
- at('INLINE_AGENT_MEMORY_INCOMPLETE', `Inline agent step "${step}"'s memory needs both \`name\` and \`id\` — the node type is minted from them `
3132
- + `(uipath.agent.resource.memory.<name>.<id>) and the runtime resolves the memory by id.`);
3133
- }
3134
- else if (!GUID_RE.test(String(mem.id).trim())) {
3135
- 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 `
3136
- + `memory's own id on the tenant, not its name.`);
3137
- }
3138
- }
3139
- }
3140
- /**
3141
- * A read of an inline agent's output: `out('<step>', '<field>')`.
3142
- *
3143
- * Stricter than the published families' equivalent, and it can afford to be: the
3144
- * declared `returns` IS the contract (there is no tenant to disagree with), and it
3145
- * is what any runner has to coerce the model's completion to: build
3146
- * `{ <declared keys> }` and put the raw completion in the first one if the model
3147
- * ignored the shape. So a read outside the set is undefined by construction, not
3148
- * merely probably.
3149
- */
3150
- function checkInlineAgentRead(name, field, rest, spec, step, diags) {
3151
- if (field === undefined)
3152
- return;
3153
- if (field === 'error')
3154
- return;
3155
- if (field !== 'output') {
3156
- diags.push({
3157
- level: 'error',
3158
- code: 'INLINE_AGENT_READ_NOT_OUTPUT',
3159
- message: `Inline agent "${name}" publishes its answer under "output", but "$vars.${name}.${field}" (read in `
3160
- + `"${step}") asks for "${field}", which is undefined at run time.`,
3161
- step,
3162
- suggestion: `out('${name}', '<field>')`,
3163
- });
3164
- return;
3165
- }
3166
- // The default when `returns` is omitted is the definition's own.
3167
- const declared = spec.returns && Object.keys(spec.returns).length
3168
- ? Object.keys(spec.returns)
3169
- : ['content'];
3170
- const wanted = (rest ?? '').replace(/^\./, '').split('.')[0];
3171
- if (!wanted)
3172
- return;
3173
- if (declared.includes(wanted))
3174
- return;
3175
- const nearest = nearestName(wanted, declared);
3176
- diags.push({
3177
- level: 'error',
3178
- code: 'INLINE_AGENT_READ_UNKNOWN_FIELD',
3179
- message: `Inline agent "${name}" declares no output "${wanted}" (read as "$vars.${name}.output.${wanted}" in `
3180
- + `"${step}"). Its \`returns\` names: ${declared.join(', ')}`
3181
- + `${spec.returns && Object.keys(spec.returns).length ? '' : ' (the definition\'s default, because the step declares no `returns`)'}`
3182
- + `. Unlike a published agent's, this contract is yours — the node's \`agentOutputVariables\` are these `
3183
- + `names, and the engine coerces the model's completion to exactly them, so a read outside the set is `
3184
- + `undefined by construction.`,
3185
- step,
3186
- ...(nearest ? { suggestion: `out('${name}', '${nearest}')` } : {}),
3187
- });
3188
- }
3189
2166
  /**
3190
2167
  * The five forbidden inputs on an IxP node. They belonged to a prior schema and
3191
2168
  * v1's own authoring reference calls them "the most common training-data-recall
@@ -3749,7 +2726,7 @@ function registeredKind(spec) {
3749
2726
  * which serialize enforces (#942) and the type's tests pin; an author cannot
3750
2727
  * cause the gap, so there is no author diagnostic to report.
3751
2728
  */
3752
- function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId, carried) {
2729
+ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf, triggerId, ledger, carried) {
3753
2730
  const spec = s.spec;
3754
2731
  /** The step's node-local rules and expressions, as `impl` defines them. */
3755
2732
  const runRules = (impl, version = impl.version, carriedDef) => {
@@ -3764,7 +2741,25 @@ function checkRegisteredStep(entry, s, exprs, errorSource, opts, diags, typeOf,
3764
2741
  return name ? { name, type: typeOf.get(name) } : undefined;
3765
2742
  },
3766
2743
  options: opts,
3767
- bindingLabels: (where, labels, use) => checkBindingLabels(where, labels, opts, (code, message) => diags.push({ level: 'error', code, step: s.name, message }), undefined, use),
2744
+ bindingLabels: (where, labels, use, target) => checkBindingLabels(where, labels, opts, (code, message) => diags.push({ level: 'error', code, step: s.name, message }), target, use),
2745
+ resources: {
2746
+ derivedNodeId: (id, what) => { if (!ledger.taken.has(id))
2747
+ ledger.taken.set(id, what); },
2748
+ claimNodeId: (id, what) => {
2749
+ const other = ledger.explicit.get(id);
2750
+ if (other === undefined)
2751
+ ledger.explicit.set(id, { step: s.name, what });
2752
+ return other;
2753
+ },
2754
+ claimFolder: (source) => {
2755
+ const key = source.toLowerCase();
2756
+ const other = ledger.agentSources.get(key);
2757
+ if (other !== undefined && other !== s.name)
2758
+ return other;
2759
+ ledger.agentSources.set(key, s.name);
2760
+ return undefined;
2761
+ },
2762
+ },
3768
2763
  };
3769
2764
  impl.check?.(spec, ctx);
3770
2765
  for (const site of impl.expressions(spec)) {
@@ -3901,10 +2896,11 @@ function collectActionExprs(s, out, errorSource) {
3901
2896
  * - a bundled family by its bundled definition (`bundledActionNodeType` +
3902
2897
  * `bundledDefinition`, #1011: `documentClassify` was missing from the old
3903
2898
  * list although `uipath.document.classify@1.0` sets the flag);
3904
- * - a per-tenant family (`rpaWorkflow`, `apiWorkflow`, `agenticProcess`,
3905
- * `publishedFunction`, `agent`, `ixpExtract`) by the definition serialize
3906
- * synthesizes for it (`perCallDefinition`, #1040: `publishedFunction` was
3907
- * refused here although `publishedFunctionDef` sets the flag).
2899
+ * - a per-tenant family by the definition serialize synthesizes for it: a
2900
+ * registered one (the process family, `agent` among it) through its node
2901
+ * type's `errorPort`, which asks that definition, and `ixpExtract` through
2902
+ * `perCallDefinition` (#1040: `publishedFunction` was refused here although
2903
+ * `publishedFunctionDef` sets the flag).
3908
2904
  *
3909
2905
  * `publishedFunction`'s flag is measured, not assumed: on 2026-10-08 four
3910
2906
  * `flow debug` jobs on alpha/codereval called `acme-echo` with a non-string
@@ -4000,7 +2996,15 @@ placed, carried) {
4000
2996
  if (!placed)
4001
2997
  return;
4002
2998
  const { impl } = placed;
4003
- if (impl.definition === 'per-tenant') {
2999
+ if (impl.definition === 'per-tenant' && placed.carried !== undefined) {
3000
+ // At a carried version the carried definition answers, as serialize asks
3001
+ // it: the type's declaration covers only the versions it registers.
3002
+ if (!definitionHasErrorPort(placed.carried)) {
3003
+ diags.push({ level: 'error', code: 'ONERROR_NO_ERROR_PORT', message: typedNoErrorPortMessage(step.name, impl.kind, placed.carried), step: step.name });
3004
+ return;
3005
+ }
3006
+ }
3007
+ else if (impl.definition === 'per-tenant') {
4004
3008
  const port = typeof impl.errorPort === 'function' ? impl.errorPort(spec) : impl.errorPort === true;
4005
3009
  if (!port) {
4006
3010
  diags.push({
@@ -4034,7 +3038,7 @@ placed, carried) {
4034
3038
  // a bundled one does: it is the definition serialize will emit.
4035
3039
  const pinned = step.options?.version;
4036
3040
  const bundledDef = bundledType
4037
- ? bundledDefinition(bundledType, actionVersion(spec, pinned)) ?? (pinned !== undefined ? carried?.(bundledType, pinned) : undefined)
3041
+ ? bundledDefinition(bundledType, pinned) ?? (pinned !== undefined ? carried?.(bundledType, pinned) : undefined)
4038
3042
  : undefined;
4039
3043
  // A per-tenant family: ask the definition serialize synthesizes for it (#1040).
4040
3044
  let perCallDef;