@shipfox/workflow-document 3.1.0 → 3.3.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.
@@ -12,6 +12,11 @@ export const WORKFLOW_LITERAL_NAME_PATTERN = /^(?:[^$]|\$\$\{\{|\$(?!\{\{))*$/;
12
12
  // The inverse of a literal name: a literal prefix followed by an unescaped
13
13
  // `${{`. An enum field that also accepts a template matches one or the other.
14
14
  export const WORKFLOW_INTERPOLATED_VALUE_PATTERN = /^(?:[^$]|\$\$\{\{|\$(?!\{\{))*\$\{\{/;
15
+ export const WORKFLOW_SESSION_KEY_MAX_LENGTH = 128;
16
+ export const WORKFLOW_SESSION_KEY_PATTERN_SOURCE = '^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$';
17
+ export const WORKFLOW_SESSION_KEY_PATTERN = new RegExp(WORKFLOW_SESSION_KEY_PATTERN_SOURCE);
18
+ const workflowSessionKeyLiteralPartPattern = /^[A-Za-z0-9._-]*$/;
19
+ const workflowSessionKeyLiteralPartStartPattern = /^[A-Za-z0-9]/;
15
20
  // Reasoning effort is an enum so editors can complete it, and a template so a
16
21
  // workflow can choose the effort from run context. The resolved value is
17
22
  // checked against the harness levels when the step is dispatched.
@@ -52,6 +57,8 @@ export const WORKFLOW_DOCUMENT_JOB_OUTPUTS_MAX_ENTRIES = WORKFLOW_DOCUMENT_ENV_M
52
57
  export const WORKFLOW_DOCUMENT_STEP_OUTPUTS_MAX_ENTRIES = WORKFLOW_DOCUMENT_ENV_MAX_ENTRIES;
53
58
  export const WORKFLOW_DOCUMENT_STEP_OUTPUT_SCHEMA_MAX_SERIALIZED_BYTES = WORKFLOW_DOCUMENT_ENV_MAX_SERIALIZED_BYTES;
54
59
  export const WORKFLOW_DOCUMENT_STEP_OUTPUT_SCHEMA_MAX_DEPTH = 64;
60
+ export const WORKFLOW_DOCUMENT_TOOL_WITH_MAX_SERIALIZED_BYTES = 32 * 1024;
61
+ export const WORKFLOW_DOCUMENT_TOOL_WITH_MAX_DEPTH = 16;
55
62
  const utf8Encoder = new TextEncoder();
56
63
  export const workflowDocumentEnvSchema = z.record(envNameSchema, z.union([
57
64
  envStringValueSchema,
@@ -76,10 +83,186 @@ export const workflowDocumentEnvSchema = z.record(envNameSchema, z.union([
76
83
  description: `Environment variables as string, number, or boolean values. Each map allows up to ${WORKFLOW_DOCUMENT_ENV_MAX_ENTRIES} entries and ${WORKFLOW_DOCUMENT_ENV_MAX_SERIALIZED_BYTES} serialized bytes.`
77
84
  });
78
85
  const workflowDocumentStepOutputKeyPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
86
+ export const workflowDocumentToolStepWithSchema = z// Validate nested values in an iterative refinement. A recursive Zod schema
87
+ // would traverse hostile depth before the tool-input limits can reject it.
88
+ .record(z.string().min(1), z.unknown()).superRefine((withValue, ctx)=>{
89
+ // The server injects `method` for `family.method` tools, so the author can
90
+ // never set it.
91
+ if ('method' in withValue) {
92
+ ctx.addIssue({
93
+ code: 'custom',
94
+ path: [
95
+ 'method'
96
+ ],
97
+ message: '`method` is not a valid tool input; the server injects it for `family.method` tools.'
98
+ });
99
+ }
100
+ validateWorkflowDocumentToolWith(withValue, ctx);
101
+ }).transform((withValue)=>withValue).meta({
102
+ description: 'Tool inputs as a JSON tree. The map allows up to ' + WORKFLOW_DOCUMENT_TOOL_WITH_MAX_SERIALIZED_BYTES + ' serialized bytes and ' + WORKFLOW_DOCUMENT_TOOL_WITH_MAX_DEPTH + ' nesting levels. Tool steps are not available yet.'
103
+ });
104
+ function validateWorkflowDocumentToolWith(withValue, ctx) {
105
+ const activeObjects = new Set();
106
+ const tasks = [
107
+ {
108
+ kind: 'value',
109
+ value: withValue,
110
+ depth: 1,
111
+ path: []
112
+ }
113
+ ];
114
+ let serializedBytes = 0;
115
+ const addSerializedBytes = (byteLength)=>{
116
+ serializedBytes += byteLength;
117
+ if (serializedBytes <= WORKFLOW_DOCUMENT_TOOL_WITH_MAX_SERIALIZED_BYTES) return true;
118
+ ctx.addIssue({
119
+ code: 'custom',
120
+ message: `Tool \`with\` cannot serialize to more than ${WORKFLOW_DOCUMENT_TOOL_WITH_MAX_SERIALIZED_BYTES} bytes.`
121
+ });
122
+ return false;
123
+ };
124
+ while(tasks.length > 0){
125
+ const task = tasks.pop();
126
+ if (task === undefined) continue;
127
+ if (task.kind === 'bytes') {
128
+ if (!addSerializedBytes(task.byteLength)) return;
129
+ continue;
130
+ }
131
+ if (task.kind === 'end') {
132
+ activeObjects.delete(task.value);
133
+ if (!addSerializedBytes(task.byteLength)) return;
134
+ continue;
135
+ }
136
+ const { value, depth, path } = task;
137
+ if (value === null) {
138
+ if (!addSerializedBytes(4)) return;
139
+ continue;
140
+ }
141
+ if (typeof value === 'string' || typeof value === 'boolean') {
142
+ if (!addSerializedBytes(jsonPrimitiveByteLength(value))) return;
143
+ continue;
144
+ }
145
+ if (typeof value === 'number') {
146
+ if (!Number.isFinite(value)) {
147
+ ctx.addIssue({
148
+ code: 'custom',
149
+ path,
150
+ message: 'Tool `with` values must be JSON-compatible.'
151
+ });
152
+ return;
153
+ }
154
+ if (!addSerializedBytes(jsonPrimitiveByteLength(value))) return;
155
+ continue;
156
+ }
157
+ if (typeof value !== 'object' || value === null) {
158
+ ctx.addIssue({
159
+ code: 'custom',
160
+ path,
161
+ message: 'Tool `with` values must be JSON-compatible.'
162
+ });
163
+ return;
164
+ }
165
+ if (depth > WORKFLOW_DOCUMENT_TOOL_WITH_MAX_DEPTH) {
166
+ ctx.addIssue({
167
+ code: 'custom',
168
+ message: `Tool \`with\` cannot be nested deeper than ${WORKFLOW_DOCUMENT_TOOL_WITH_MAX_DEPTH} levels.`
169
+ });
170
+ return;
171
+ }
172
+ if (activeObjects.has(value)) {
173
+ ctx.addIssue({
174
+ code: 'custom',
175
+ path,
176
+ message: 'Tool `with` values must be a JSON tree.'
177
+ });
178
+ return;
179
+ }
180
+ activeObjects.add(value);
181
+ if (Array.isArray(value)) {
182
+ if (!addSerializedBytes(1)) return;
183
+ tasks.push({
184
+ kind: 'end',
185
+ value,
186
+ byteLength: 1
187
+ });
188
+ for(let index = value.length - 1; index >= 0; index -= 1){
189
+ tasks.push({
190
+ kind: 'value',
191
+ value: value[index],
192
+ depth: depth + 1,
193
+ path: [
194
+ ...path,
195
+ index
196
+ ]
197
+ });
198
+ if (index > 0) tasks.push({
199
+ kind: 'bytes',
200
+ byteLength: 1
201
+ });
202
+ }
203
+ continue;
204
+ }
205
+ if (!isJsonRecord(value)) {
206
+ ctx.addIssue({
207
+ code: 'custom',
208
+ path,
209
+ message: 'Tool `with` values must be a JSON tree.'
210
+ });
211
+ return;
212
+ }
213
+ if (!addSerializedBytes(1)) return;
214
+ tasks.push({
215
+ kind: 'end',
216
+ value,
217
+ byteLength: 1
218
+ });
219
+ const entries = Object.entries(value);
220
+ for(let index = entries.length - 1; index >= 0; index -= 1){
221
+ const entry = entries[index];
222
+ if (entry === undefined) continue;
223
+ const [key, child] = entry;
224
+ tasks.push({
225
+ kind: 'value',
226
+ value: child,
227
+ depth: depth + 1,
228
+ path: [
229
+ ...path,
230
+ key
231
+ ]
232
+ });
233
+ tasks.push({
234
+ kind: 'bytes',
235
+ byteLength: jsonPrimitiveByteLength(key) + 1
236
+ });
237
+ if (index > 0) tasks.push({
238
+ kind: 'bytes',
239
+ byteLength: 1
240
+ });
241
+ }
242
+ }
243
+ }
244
+ function jsonPrimitiveByteLength(value) {
245
+ return utf8Encoder.encode(JSON.stringify(value)).byteLength;
246
+ }
247
+ function jsonSerializedByteLength(value) {
248
+ try {
249
+ const serialized = JSON.stringify(value);
250
+ return serialized === undefined ? undefined : utf8Encoder.encode(serialized).byteLength;
251
+ } catch {
252
+ return undefined;
253
+ }
254
+ }
255
+ function isJsonRecord(value) {
256
+ const prototype = Object.getPrototypeOf(value);
257
+ return prototype === Object.prototype || prototype === null;
258
+ }
79
259
  const workflowDocumentStepOutputTypeSchema = z.enum(workflowDocumentStepOutputTypes).meta({
80
260
  description: 'Declared output type. Use `json` when the output has a JSON Schema.'
81
261
  });
82
- const workflowDocumentStepOutputDeclarationSchema = z.union([
262
+ const workflowDocumentToolStepOutputMappingValueSchema = z.string().min(1).refine((value)=>value.includes('$' + '{{'), {
263
+ message: 'Tool-step output mappings must use a $' + '{{ }} expression.'
264
+ });
265
+ export const workflowDocumentStepOutputDeclarationSchema = z.union([
83
266
  workflowDocumentStepOutputTypeSchema.transform((type)=>({
84
267
  type
85
268
  })),
@@ -112,7 +295,17 @@ const workflowDocumentStepOutputDeclarationSchema = z.union([
112
295
  });
113
296
  return;
114
297
  }
115
- const serializedBytes = utf8Encoder.encode(JSON.stringify(schema)).byteLength;
298
+ const serializedBytes = jsonSerializedByteLength(schema);
299
+ if (serializedBytes === undefined) {
300
+ ctx.addIssue({
301
+ code: 'custom',
302
+ path: [
303
+ 'schema'
304
+ ],
305
+ message: 'Schema must be a serializable JSON Schema document.'
306
+ });
307
+ return;
308
+ }
116
309
  if (serializedBytes > WORKFLOW_DOCUMENT_STEP_OUTPUT_SCHEMA_MAX_SERIALIZED_BYTES) {
117
310
  ctx.addIssue({
118
311
  code: 'custom',
@@ -133,7 +326,12 @@ const workflowDocumentStepOutputDeclarationSchema = z.union([
133
326
  });
134
327
  }
135
328
  });
136
- export const workflowDocumentStepOutputsSchema = z.record(z.string(), workflowDocumentStepOutputDeclarationSchema).superRefine((outputs, ctx)=>{
329
+ function stepOutputsAreMappingForm(outputs) {
330
+ const interpolationOpen = '$' + '{{';
331
+ const values = Object.values(outputs);
332
+ return values.some((value)=>typeof value === 'string' && value.includes(interpolationOpen));
333
+ }
334
+ function stepOutputsRecordChecks(outputs, ctx) {
137
335
  const entries = Object.keys(outputs).length;
138
336
  if (entries > WORKFLOW_DOCUMENT_STEP_OUTPUTS_MAX_ENTRIES) {
139
337
  ctx.addIssue({
@@ -151,21 +349,38 @@ export const workflowDocumentStepOutputsSchema = z.record(z.string(), workflowDo
151
349
  message: 'Output keys must be CEL identifiers.'
152
350
  });
153
351
  }
154
- }).meta({
352
+ }
353
+ export const workflowDocumentStepOutputsSchema = z.record(z.string(), workflowDocumentStepOutputDeclarationSchema).superRefine((outputs, ctx)=>stepOutputsRecordChecks(outputs, ctx)).meta({
155
354
  description: `Named step outputs. Keys must be CEL identifiers and each step allows up to ${WORKFLOW_DOCUMENT_STEP_OUTPUTS_MAX_ENTRIES} declarations.`
156
355
  });
356
+ // The tool-step `outputs` form maps output keys to a single `${{ }}` expression
357
+ // over `result`. The expression layer validates the interpolation; the mapping
358
+ // form is rejected with the reserved tool step fields until tool steps exist.
359
+ export const workflowDocumentToolStepOutputsSchema = z.record(z.string(), workflowDocumentToolStepOutputMappingValueSchema).superRefine((outputs, ctx)=>stepOutputsRecordChecks(outputs, ctx)).meta({
360
+ description: 'Tool-step output mappings over `result`. Each value is exactly one $' + '{{ }} expression. Tool steps are not available yet.'
361
+ });
362
+ // `outputs` carries the declaration form on run, agent, and checkout steps and
363
+ // the expression mapping form on tool steps. One value union accepts both so a
364
+ // reserved tool step parses and is rejected by the step `superRefine`; zod
365
+ // reports the declaration branch's own issues for malformed declarations, and
366
+ // the step `superRefine` rejects the mapping form on every other step kind.
367
+ const workflowDocumentStepOutputValueSchema = z.union([
368
+ workflowDocumentStepOutputDeclarationSchema,
369
+ workflowDocumentToolStepOutputMappingValueSchema
370
+ ]);
371
+ const workflowDocumentStepOutputsFieldSchema = z.record(z.string(), workflowDocumentStepOutputValueSchema).superRefine((outputs, ctx)=>stepOutputsRecordChecks(outputs, ctx));
157
372
  const workflowDocumentTriggerBaseSchema = {
158
373
  source: z.string().min(1).meta({
159
- description: 'Integration connection slug or built-in trigger source. See [Trigger sources](/reference/trigger-sources).'
374
+ description: 'Integration connection slug or built-in trigger source. See [Integrations](/integrations) for provider sources.'
160
375
  }),
161
376
  with: z.record(z.string(), z.unknown()).optional().meta({
162
- description: 'Provider-specific values used to match or configure the trigger. See [Trigger sources](/reference/trigger-sources).'
377
+ description: 'Provider-specific values used to match or configure the trigger. See the provider event catalog in [Integrations](/integrations).'
163
378
  }),
164
379
  filter: z.string().min(1).optional().meta({
165
380
  description: 'CEL condition that filters matching events. It is not supported for `manual` or `cron` triggers. See [Expressions](/reference/expressions) and [Contexts](/reference/contexts#context-availability).'
166
381
  }),
167
382
  config: z.record(z.string(), z.unknown()).optional().meta({
168
- description: 'Source-specific configuration. It is supported only for top-level triggers with a known built-in source. See [cron triggers](/reference/trigger-sources#cron).'
383
+ description: 'Source-specific configuration. It is supported only for top-level triggers with a known built-in source. See [Schedule workflows](/how-to/author-workflows/schedule-workflows).'
169
384
  })
170
385
  };
171
386
  export const triggerSourceConfigSchemas = {
@@ -353,6 +568,115 @@ export const workflowDocumentStepIntegrationSchema = z.strictObject({
353
568
  description: 'Allows write-capable integration tools. Omit or set false for read-only access.'
354
569
  })
355
570
  });
571
+ // A session names an agent conversation that continues across steps of one run.
572
+ // The shorthand string form names the session and resumes it; the long form
573
+ // adds an explicit mode. The key is a field template, evaluated at step
574
+ // dispatch with the same context roots the prompt sees.
575
+ const workflowSessionKeyLiteralSchema = z.string().min(1).max(WORKFLOW_SESSION_KEY_MAX_LENGTH).regex(WORKFLOW_SESSION_KEY_PATTERN, {
576
+ message: 'Literal session keys must start with a letter or number and contain only letters, numbers, dots, underscores, or hyphens.'
577
+ });
578
+ const workflowSessionKeyTemplateSchema = z.string().min(1).regex(WORKFLOW_INTERPOLATED_VALUE_PATTERN, {
579
+ message: 'Session keys with interpolation must use a $' + '{{ }} template.'
580
+ }).refine(isValidWorkflowSessionKeyTemplateLiteralParts, {
581
+ message: 'Literal parts of interpolated session keys may contain only letters, numbers, dots, underscores, or hyphens, and may not exceed 128 characters in total.'
582
+ });
583
+ const workflowSessionKeySchema = z.union([
584
+ workflowSessionKeyLiteralSchema,
585
+ workflowSessionKeyTemplateSchema
586
+ ]);
587
+ export const workflowDocumentSessionSchema = z.union([
588
+ workflowSessionKeyLiteralSchema,
589
+ workflowSessionKeyTemplateSchema,
590
+ z.strictObject({
591
+ key: workflowSessionKeySchema.meta({
592
+ description: 'Session key, or a $' + '{{ }} interpolation that resolves to one.'
593
+ }),
594
+ mode: z.enum([
595
+ 'resume',
596
+ 'fork'
597
+ ]).optional().meta({
598
+ description: 'Session mode. `resume` continues the session and writes back; `fork` reads a snapshot and never writes. Defaults to `resume`.'
599
+ })
600
+ })
601
+ ]).meta({
602
+ description: 'Named agent session continued across steps of one workflow run. A string names the session and resumes it; an object adds the mode.'
603
+ });
604
+ export function isValidWorkflowSessionKeyTemplateLiteralParts(source) {
605
+ let cursor = 0;
606
+ let expressionSeen = false;
607
+ let literalLength = 0;
608
+ while(true){
609
+ const opener = source.indexOf('${{', cursor);
610
+ const literalEnd = opener === -1 ? source.length : opener;
611
+ const literal = source.slice(cursor, literalEnd);
612
+ literalLength += literal.length;
613
+ if (literalLength > WORKFLOW_SESSION_KEY_MAX_LENGTH || !workflowSessionKeyLiteralPartPattern.test(literal) || !expressionSeen && literal.length > 0 && !workflowSessionKeyLiteralPartStartPattern.test(literal)) {
614
+ return false;
615
+ }
616
+ if (opener === -1) return expressionSeen;
617
+ const close = findWorkflowSessionKeyTemplateClose(source, opener);
618
+ if (close === -1) return false;
619
+ expressionSeen = true;
620
+ cursor = close + 2;
621
+ }
622
+ }
623
+ function findWorkflowSessionKeyTemplateClose(source, openerIndex) {
624
+ let index = openerIndex + 3;
625
+ let depth = 0;
626
+ while(index < source.length){
627
+ const stringEnd = scanWorkflowSessionKeyStringLiteral(source, index);
628
+ if (stringEnd !== null) {
629
+ index = stringEnd;
630
+ continue;
631
+ }
632
+ if (source.startsWith('//', index)) {
633
+ const newline = source.indexOf('\n', index + 2);
634
+ index = newline === -1 ? source.length : newline;
635
+ continue;
636
+ }
637
+ if (depth === 0 && source.startsWith('}}', index)) return index;
638
+ const char = source[index];
639
+ if (char === '(' || char === '[' || char === '{') {
640
+ depth += 1;
641
+ } else if ((char === ')' || char === ']' || char === '}') && depth > 0) {
642
+ depth -= 1;
643
+ }
644
+ index += 1;
645
+ }
646
+ return -1;
647
+ }
648
+ function scanWorkflowSessionKeyStringLiteral(source, index) {
649
+ for (const prefix of [
650
+ 'r',
651
+ 'R',
652
+ 'b',
653
+ 'B',
654
+ ''
655
+ ]){
656
+ if (!source.startsWith(prefix, index)) continue;
657
+ const quoteIndex = index + prefix.length;
658
+ const quote = source[quoteIndex];
659
+ if (quote !== '"' && quote !== "'") continue;
660
+ const tripleQuote = quote.repeat(3);
661
+ if (source.startsWith(tripleQuote, quoteIndex)) {
662
+ return scanWorkflowSessionKeyQuotedString(source, quoteIndex + 3, tripleQuote, prefix === 'r' || prefix === 'R');
663
+ }
664
+ return scanWorkflowSessionKeyQuotedString(source, quoteIndex + 1, quote, prefix === 'r' || prefix === 'R');
665
+ }
666
+ return null;
667
+ }
668
+ function scanWorkflowSessionKeyQuotedString(source, startIndex, delimiter, raw) {
669
+ let index = startIndex;
670
+ while(index < source.length){
671
+ if (!raw && source[index] === '\\') {
672
+ index += 2;
673
+ continue;
674
+ }
675
+ if (source.startsWith(delimiter, index)) return index + delimiter.length;
676
+ index += 1;
677
+ }
678
+ return source.length;
679
+ }
356
680
  export const workflowDocumentAgentStepFields = [
357
681
  'model',
358
682
  'prompt',
@@ -360,7 +684,8 @@ export const workflowDocumentAgentStepFields = [
360
684
  'thinking',
361
685
  'provider',
362
686
  'tools',
363
- 'integrations'
687
+ 'integrations',
688
+ 'session'
364
689
  ];
365
690
  // A step is a run step (`run`), an inline agent step (`prompt`), or a checkout
366
691
  // step (`checkout`), never two kinds at once. They share one strict object so
@@ -368,8 +693,11 @@ export const workflowDocumentAgentStepFields = [
368
693
  // payload keys are present and emits one targeted issue per failure mode (a
369
694
  // plain union would surface every branch's errors at once). The `agent`
370
695
  // keyword is declared only so the reserved-keyword case produces a clear
371
- // message instead of a generic "unrecognized key".
372
- export const workflowDocumentStepSchema = z.strictObject({
696
+ // message instead of a generic "unrecognized key". The `tool`, `connection`,
697
+ // `with`, and tool-step `outputs` mapping form are declared the same way: they
698
+ // parse so their shape is checked, then any step carrying a reserved tool field
699
+ // is rejected until the tool step kind exists.
700
+ const workflowDocumentStepBaseSchema = z.strictObject({
373
701
  key: z.string().min(1).optional().meta({
374
702
  description: 'Stable step key for dependencies and outputs.'
375
703
  }),
@@ -398,6 +726,7 @@ export const workflowDocumentStepSchema = z.strictObject({
398
726
  description: 'Agent harness. When omitted, Shipfox uses the workspace default harness, or `pi` when none is configured.'
399
727
  }),
400
728
  thinking: agentThinkingFieldSchema.optional(),
729
+ session: workflowDocumentSessionSchema.optional(),
401
730
  provider: z.string().min(1).optional().meta({
402
731
  description: 'Model provider ID for an agent step. It requires `prompt` and is not valid on a run step.'
403
732
  }),
@@ -410,16 +739,24 @@ export const workflowDocumentStepSchema = z.strictObject({
410
739
  agent: z.unknown().optional().meta({
411
740
  description: 'Reserved keyword. It is rejected; use `prompt` to define an agent step.'
412
741
  }),
742
+ tool: z.string().min(1).optional().meta({
743
+ description: 'Literal integration tool id for a tool step. It is rejected; tool steps are not available yet.'
744
+ }),
745
+ connection: z.string().min(1).optional().meta({
746
+ description: 'Literal integration connection slug for a tool step. It is rejected; tool steps are not available yet.'
747
+ }),
748
+ with: workflowDocumentToolStepWithSchema.optional(),
413
749
  gate: workflowDocumentStepGateSchema.optional().meta({
414
750
  description: 'Success gate and optional restart behavior after the step runs.'
415
751
  }),
416
752
  env: workflowDocumentEnvSchema.optional().meta({
417
753
  description: 'Environment variables for a run step. They are not valid on an agent step.'
418
754
  }),
419
- outputs: workflowDocumentStepOutputsSchema.optional().meta({
420
- description: 'Named output declarations produced by this step.'
755
+ outputs: workflowDocumentStepOutputsFieldSchema.optional().meta({
756
+ description: 'Named output declarations produced by this step, or on a tool step a mapping of output keys to exactly one $' + '{{ }} expression over `result`. Tool steps are not available yet.'
421
757
  })
422
- }).superRefine((step, ctx)=>{
758
+ });
759
+ export const workflowDocumentStepSchema = workflowDocumentStepBaseSchema.superRefine((step, ctx)=>{
423
760
  if (step.agent !== undefined) {
424
761
  ctx.addIssue({
425
762
  code: 'custom',
@@ -430,6 +767,26 @@ export const workflowDocumentStepSchema = z.strictObject({
430
767
  });
431
768
  return;
432
769
  }
770
+ const reservedToolField = step.tool !== undefined ? 'tool' : step.connection !== undefined ? 'connection' : undefined;
771
+ if (reservedToolField !== undefined || step.with !== undefined) {
772
+ ctx.addIssue({
773
+ code: 'custom',
774
+ path: [
775
+ reservedToolField ?? 'with'
776
+ ],
777
+ message: 'Tool steps are not available yet.'
778
+ });
779
+ return;
780
+ }
781
+ if (step.outputs !== undefined && stepOutputsAreMappingForm(step.outputs)) {
782
+ ctx.addIssue({
783
+ code: 'custom',
784
+ path: [
785
+ 'outputs'
786
+ ],
787
+ message: 'The `outputs` mapping form is reserved for tool steps.'
788
+ });
789
+ }
433
790
  if (step.checkout !== undefined) {
434
791
  if (step.run !== undefined) {
435
792
  ctx.addIssue({
@@ -502,7 +859,10 @@ export const workflowDocumentStepSchema = z.strictObject({
502
859
  message: 'An agent step requires "prompt".'
503
860
  });
504
861
  }
505
- });
862
+ }).transform(({ outputs, ...step })=>outputs === undefined ? step : {
863
+ ...step,
864
+ outputs: outputs
865
+ });
506
866
  const workflowDocumentJobOutputsSchema = nonEmptyRecordSchema(z.string().min(1)).superRefine((outputs, ctx)=>{
507
867
  const entries = Object.keys(outputs).length;
508
868
  if (entries > WORKFLOW_DOCUMENT_JOB_OUTPUTS_MAX_ENTRIES) {
@@ -565,14 +925,41 @@ export const workflowDocumentSchema = z.strictObject({
565
925
  })
566
926
  });
567
927
  function maxJsonDepth(value) {
568
- if (value === null || typeof value !== 'object') return 0;
569
- if (Array.isArray(value)) {
570
- if (value.length === 0) return 1;
571
- return 1 + Math.max(...value.map(maxJsonDepth));
928
+ let maximumDepth = 0;
929
+ const activeObjects = new Set();
930
+ const pending = [
931
+ {
932
+ kind: 'enter',
933
+ value,
934
+ depth: 0
935
+ }
936
+ ];
937
+ while(pending.length > 0){
938
+ const current = pending.pop();
939
+ if (current === undefined) continue;
940
+ if (current.kind === 'leave') {
941
+ activeObjects.delete(current.value);
942
+ continue;
943
+ }
944
+ if (current.value === null || typeof current.value !== 'object') continue;
945
+ if (activeObjects.has(current.value)) continue;
946
+ activeObjects.add(current.value);
947
+ const depth = current.depth + 1;
948
+ maximumDepth = Math.max(maximumDepth, depth);
949
+ pending.push({
950
+ kind: 'leave',
951
+ value: current.value
952
+ });
953
+ const children = Object.values(current.value);
954
+ for(let index = children.length - 1; index >= 0; index -= 1){
955
+ pending.push({
956
+ kind: 'enter',
957
+ value: children[index],
958
+ depth
959
+ });
960
+ }
572
961
  }
573
- const entries = Object.values(value);
574
- if (entries.length === 0) return 1;
575
- return 1 + Math.max(...entries.map(maxJsonDepth));
962
+ return maximumDepth;
576
963
  }
577
964
  function isJsonSchemaDocument(value) {
578
965
  return typeof value === 'boolean' || typeof value === 'object' && value !== null && !Array.isArray(value);