@oomol-lab/open-flow 0.1.0-beta.75 → 0.1.0-beta.77

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 (146) hide show
  1. package/README.md +3 -1
  2. package/dist/browser/{ContentIcon-FCpKiwjp.js → ContentIcon-VZQO8VZU.js} +9 -6
  3. package/dist/browser/{FlowCanvas-dzRBnlUT.js → FlowCanvas-BNGDMaWr.js} +75 -63
  4. package/dist/browser/{createResourceDialog-BeYDdUl-.js → createResourceDialog-BmtakfVg.js} +63 -57
  5. package/dist/browser/{dialog-ZtpsFbl4.js → dialog-BcFBfRB1.js} +126 -32
  6. package/dist/browser/{exportSvg-DqXw1yf-.js → exportSvg-CG8hEUNk.js} +2 -2
  7. package/dist/browser/{flowChanges-b_5UobFj.js → flowChanges-BGo_4Ypo.js} +6063 -11285
  8. package/dist/browser/{flowWorkspace-ItojZbO4.js → flowWorkspace-BFHGev7s.js} +1065 -2570
  9. package/dist/browser/preview.css +626 -653
  10. package/dist/browser/preview.js +1907 -9663
  11. package/dist/browser/theme.css +117 -263
  12. package/dist/browser/{tooltip-wZJRB9oP.js → tooltip-BAWSUexx.js} +2 -2
  13. package/dist/browser/{typeScriptSession-CSRwWmZm.js → typeScriptSession-CRoezJPN.js} +1 -1
  14. package/dist/browser/ui.css +464 -463
  15. package/dist/browser/ui.js +4 -3
  16. package/dist/browser/workbench.css +9891 -9897
  17. package/dist/browser/workbench.d.ts +6 -2
  18. package/dist/browser/workbench.js +838 -499
  19. package/dist/browser/{workbenchSelect-M441Lz0d.js → workbenchSelect-CjK-xdm3.js} +2 -2
  20. package/dist/command/index.js +48 -50
  21. package/dist/common/authoring.d.ts +5 -5
  22. package/dist/common/authoringExamples.d.ts +3 -2
  23. package/dist/common/authoringSchema.d.ts +26 -4
  24. package/dist/common/connector-proxy.d.ts +1 -1
  25. package/dist/common/control-api-conformance.js +352 -144
  26. package/dist/common/control-api-errors.d.ts +12 -0
  27. package/dist/common/control-api.d.ts +38 -4
  28. package/dist/common/control-api.js +373 -162
  29. package/dist/common/control-requests.d.ts +22 -3
  30. package/dist/common/control-requests.js +268 -28
  31. package/dist/common/cron-trigger.d.ts +2 -1
  32. package/dist/common/decision.d.ts +4 -4
  33. package/dist/common/decision.js +5 -2
  34. package/dist/common/engine-contract.d.ts +6 -1
  35. package/dist/common/event-source-definition.d.ts +1 -1
  36. package/dist/common/event-source-view.d.ts +22 -0
  37. package/dist/common/event-source.d.ts +1 -1
  38. package/dist/common/event-source.js +24 -65
  39. package/dist/common/event-sources.d.ts +0 -21
  40. package/dist/common/flow-authoring.js +90 -20
  41. package/dist/common/flow-change-schema.d.ts +9 -1
  42. package/dist/common/flow-change.d.ts +8 -202
  43. package/dist/common/flow-change.js +96 -24
  44. package/dist/common/flow-encoding.js +89 -21
  45. package/dist/common/flow-input-value.d.ts +1 -1
  46. package/dist/common/flow-model.d.ts +121 -0
  47. package/dist/common/flow-notifications.d.ts +1 -1
  48. package/dist/common/flow-retry.d.ts +2 -4
  49. package/dist/common/flow-semantics.js +161 -80
  50. package/dist/common/flow-state.d.ts +123 -0
  51. package/dist/common/flow-state.js +5852 -0
  52. package/dist/common/flow-workflowReferences.d.ts +8 -0
  53. package/dist/common/flowInspection.d.ts +1 -1
  54. package/dist/common/http-hosts.d.ts +6 -0
  55. package/dist/common/http-request.d.ts +13 -5
  56. package/dist/common/http-request.js +23 -5
  57. package/dist/common/integration-trigger.d.ts +2 -1
  58. package/dist/common/mcp.d.ts +96 -3
  59. package/dist/common/mcp.js +355 -142
  60. package/dist/common/openapi.d.ts +4 -4
  61. package/dist/common/openapi.js +4 -1
  62. package/dist/common/poll-trigger.d.ts +2 -1
  63. package/dist/common/run-events.js +1 -0
  64. package/dist/common/runtime-contract.d.ts +37 -1
  65. package/dist/common/runtime-contract.js +717 -72
  66. package/dist/common/runtime-nodejs-compat.d.ts +44 -0
  67. package/dist/common/runtime-nodejs-compat.js +5870 -0
  68. package/dist/common/scheduler-checkpoint.d.ts +1 -1
  69. package/dist/common/scheduler-node-execution.d.ts +2 -1
  70. package/dist/common/scheduler.d.ts +2 -2
  71. package/dist/common/scheduler.js +76 -6
  72. package/dist/common/state-conformance.d.ts +16 -0
  73. package/dist/common/tags.d.ts +4 -0
  74. package/dist/common/trigger-catalog.d.ts +1 -7
  75. package/dist/common/trigger-config-options.d.ts +1 -1
  76. package/dist/common/trigger-config.d.ts +1 -1
  77. package/dist/common/trigger-contract.d.ts +2 -1
  78. package/dist/common/trigger-localization.d.ts +8 -2
  79. package/dist/common/trigger-model.d.ts +101 -0
  80. package/dist/common/webhook-response.d.ts +1 -1
  81. package/dist/common/webhook-trigger.d.ts +3 -1
  82. package/dist/common/workflow.d.ts +3 -12
  83. package/dist/common/workflow.js +25 -23
  84. package/dist/index.d.ts +52 -14
  85. package/package.json +9 -1
  86. package/src/base/common/json.ts +1 -1
  87. package/src/connector/common/proxy.ts +1 -1
  88. package/src/control/common/api.ts +90 -16
  89. package/src/control/common/authoring.ts +6 -1
  90. package/src/control/common/authoringExamples.ts +8 -0
  91. package/src/control/common/authoringSchema.ts +26 -13
  92. package/src/control/common/authoringTools.ts +1 -1
  93. package/src/control/common/conformance.ts +1 -1
  94. package/src/control/common/connectorDecoders.ts +1 -1
  95. package/src/control/common/errors.ts +6 -0
  96. package/src/control/common/eventSources.ts +2 -29
  97. package/src/control/common/flowDecoders.ts +1 -0
  98. package/src/control/common/mcp.ts +16 -0
  99. package/src/control/common/requests.ts +10 -2
  100. package/src/control/common/tags.ts +20 -0
  101. package/src/control/common/triggerCatalog.ts +1 -8
  102. package/src/decision/common/decision.ts +15 -7
  103. package/src/event-source/common/definition.ts +1 -1
  104. package/src/event-source/common/index.ts +1 -1
  105. package/src/event-source/common/view.ts +20 -0
  106. package/src/execution/common/checkpoint.ts +1 -1
  107. package/src/execution/common/events.ts +3 -0
  108. package/src/execution/common/node-execution.ts +4 -1
  109. package/src/execution/common/scheduler.ts +3 -1
  110. package/src/flow/common/change.ts +60 -193
  111. package/src/flow/common/changeSchema.ts +39 -4
  112. package/src/flow/common/connectionUsage.ts +1 -1
  113. package/src/flow/common/errorHandling.ts +1 -1
  114. package/src/flow/common/inputValue.ts +1 -1
  115. package/src/flow/common/model.ts +130 -0
  116. package/src/flow/common/modules.ts +1 -1
  117. package/src/flow/common/portChanges.ts +1 -1
  118. package/src/flow/common/retry.ts +2 -4
  119. package/src/flow/common/semantics.ts +31 -4
  120. package/src/flow/common/webhookResponse.ts +1 -1
  121. package/src/flow/common/workflowReferences.ts +31 -0
  122. package/src/http/common/authBindings.ts +6 -9
  123. package/src/http/common/hosts.ts +13 -0
  124. package/src/http/common/httpRequest.ts +30 -15
  125. package/src/openapi/common/openapi.ts +9 -5
  126. package/src/{execution → runtime}/common/engineContract.ts +23 -15
  127. package/src/runtime/common/nodejsCompat.ts +275 -0
  128. package/src/runtime/common/nodejsGuest.generated.json +47 -0
  129. package/src/runtime/common/runtime.ts +1254 -0
  130. package/src/state/common/conformance.ts +86 -0
  131. package/src/state/common/state.ts +169 -0
  132. package/src/trigger/common/config.ts +1 -1
  133. package/src/trigger/common/configOptions.ts +1 -1
  134. package/src/trigger/common/contract.ts +2 -1
  135. package/src/trigger/common/cron.ts +2 -1
  136. package/src/trigger/common/integration.ts +2 -1
  137. package/src/trigger/common/model.ts +87 -0
  138. package/src/trigger/common/poll.ts +2 -1
  139. package/src/trigger/common/webhook.ts +3 -1
  140. package/src/trigger/providers/fieldDescriptions.ts +1 -1
  141. package/src/trigger/providers/localization.ts +9 -2
  142. package/src/trigger/providers/remote.ts +1 -1
  143. package/src/types/index.ts +55 -15
  144. package/src/workflow/common/workflow.ts +3 -44
  145. package/src/execution/common/runtime.ts +0 -563
  146. /package/src/{control → connector}/common/providerIconSprite.ts +0 -0
@@ -1,6 +1,8 @@
1
1
  import type { CanvasCommand } from '../../canvas/common/commands.ts'
2
+ import type { FlowStateView, StateScope } from '../../state/common/state.ts'
2
3
 
3
- import { currentEngineContract } from '../../execution/common/engineContract.ts'
4
+ import { currentEngineContract } from '../../runtime/common/engineContract.ts'
5
+ import { flowStateViewSchema } from '../../state/common/state.ts'
4
6
  import { workflowContractSchema, workflowReferencesSchema } from '../../workflow/common/workflow.ts'
5
7
  export type { CanvasCommand, CanvasCommandSubscriber } from '../../canvas/common/commands.ts'
6
8
  import type { LiveDetails } from './liveDetails.ts'
@@ -9,12 +11,12 @@ import { decodeLiveDetails } from './liveDetails.ts'
9
11
  export { liveEntries } from './liveDetails.ts'
10
12
  export { flowInputDefinitions } from './authoringFields.ts'
11
13
  export type { LiveDetails, LiveEntry } from './liveDetails.ts'
14
+ import type { ProviderIconAppearance } from '../../connector/common/providerIconSprite.ts'
12
15
  import type { AuthoringRead, AuthoringSearch, AuthoringRequest, AuthoringType } from './authoringSchema.ts'
13
- import type { ProviderIconAppearance } from './providerIconSprite.ts'
14
16
 
15
17
  import { authoringReadResponse, authoringSearchResponse, authoringEditResponse, authoringCheckResponse } from './authoringSchema.ts'
16
- export { providerIconAppearance } from './providerIconSprite.ts'
17
- export type { ProviderIconAppearance, ProviderIconSprite, ProviderIconSpriteCatalog } from './providerIconSprite.ts'
18
+ export { providerIconAppearance } from '../../connector/common/providerIconSprite.ts'
19
+ export type { ProviderIconAppearance, ProviderIconSprite, ProviderIconSpriteCatalog } from '../../connector/common/providerIconSprite.ts'
18
20
  export { connectorAccess as decodeConnectorAccess, connectorAccessSnapshot as decodeConnectorAccessSnapshot } from './connectorDecoders.ts'
19
21
  import type { ProviderAccessIdentity, ProviderAccessReference } from './providerAccess.ts'
20
22
  export {
@@ -35,7 +37,8 @@ export { decodeEventSource, decodeEventSources, type CreateEventSource, type Upd
35
37
  import type { TriggerCatalogCache } from './triggerCatalog.ts'
36
38
 
37
39
  import { decodeTriggerCatalog } from './triggerCatalog.ts'
38
- export { decodeTriggerCatalog, searchTriggerKeys, type TriggerCatalog, type TriggerCatalogCache, type TriggerDisplay } from './triggerCatalog.ts'
40
+ export { decodeTriggerCatalog, searchTriggerKeys, type TriggerCatalog, type TriggerCatalogCache } from './triggerCatalog.ts'
41
+ export type { TriggerDisplay } from '../../trigger/providers/localization.ts'
39
42
  import type { TriggerConfigOption } from '../../trigger/common/configOptions.ts'
40
43
  export type { TriggerConfigOption } from '../../trigger/common/configOptions.ts'
41
44
  import type { ResultQuery } from './results.ts'
@@ -76,6 +79,8 @@ export type { FlowCatalogEvent, FlowChangeEvent } from './flowNotifications.ts'
76
79
 
77
80
  import { ApiError } from './errors.ts'
78
81
  import { randomId } from './random.ts'
82
+ import { decodeTag, decodeTagListItem, normalizeFlowQuery } from './tags.ts'
83
+ export { normalizeFlowQuery } from './tags.ts'
79
84
 
80
85
  export interface CanvasBroadcast {
81
86
  readonly version: 1
@@ -102,7 +107,29 @@ export interface FlowResources {
102
107
  }
103
108
  }
104
109
 
110
+ export interface FlowUpdate {
111
+ readonly name?: string
112
+ readonly tagIds?: readonly string[]
113
+ }
114
+
115
+ export interface FlowQuery {
116
+ readonly query?: string
117
+ readonly tagIds?: readonly string[]
118
+ readonly untagged?: boolean
119
+ }
120
+
121
+ export interface Tag {
122
+ readonly tagId: string
123
+ readonly name: string
124
+ readonly version: 1
125
+ }
126
+
127
+ export interface TagListItem extends Tag {
128
+ readonly referenceCount: number
129
+ }
130
+
105
131
  export interface Flow {
132
+ readonly tagIds?: readonly string[]
106
133
  readonly live?: { readonly enabled: boolean; readonly hasStart: boolean; readonly publicationId: string; readonly revisionId: string }
107
134
 
108
135
  readonly createdAt: string
@@ -676,6 +703,7 @@ export function decodeRunEvent(value: unknown) {
676
703
  nodeKind !== 'decision' &&
677
704
  nodeKind !== 'openapi' &&
678
705
  nodeKind !== 'http-request' &&
706
+ nodeKind !== 'dedupe' &&
679
707
  nodeKind !== 'llm' &&
680
708
  nodeKind !== 'agent' &&
681
709
  nodeKind !== 'value' &&
@@ -808,9 +836,13 @@ export class ControlClient {
808
836
  }
809
837
 
810
838
  async listFlows(
811
- options: { readonly cursor?: string; readonly includeTotal?: boolean; readonly limit?: number; readonly signal?: AbortSignal } = {},
839
+ options: FlowQuery & { readonly cursor?: string; readonly includeTotal?: boolean; readonly limit?: number; readonly signal?: AbortSignal } = {},
812
840
  ): Promise<FlowPage> {
813
841
  const parameters = new URLSearchParams()
842
+ const filter = normalizeFlowQuery(options)
843
+ if (filter.query) parameters.set('query', filter.query)
844
+ if (filter.tagIds.length) parameters.set('tagIds', filter.tagIds.join(','))
845
+ if (filter.untagged) parameters.set('untagged', 'true')
814
846
  if (options.cursor != null) parameters.set('cursor', options.cursor)
815
847
  if (options.limit != null) parameters.set('limit', String(options.limit))
816
848
  if (options.includeTotal != null) parameters.set('includeTotal', String(options.includeTotal))
@@ -896,20 +928,23 @@ export class ControlClient {
896
928
  }
897
929
  }
898
930
 
899
- async importFlow(file: WorkflowFile, options: { readonly name?: string; readonly teamId?: string; readonly idempotencyKey?: string } = {}): Promise<Flow> {
931
+ async importFlow(
932
+ file: WorkflowFile,
933
+ options: { readonly name?: string; readonly teamId?: string; readonly idempotencyKey?: string; readonly tagIds?: readonly string[] } = {},
934
+ ): Promise<Flow> {
900
935
  return flow(
901
936
  await this.request('/v1/flows/import', {
902
937
  method: 'POST',
903
938
  headers: { 'Idempotency-Key': options.idempotencyKey ?? `flow-${randomId()}` },
904
- body: JSON.stringify({ file, name: options.name, teamId: options.teamId, version: 1 }),
939
+ body: JSON.stringify({ file, name: options.name, teamId: options.teamId, tagIds: options.tagIds, version: 1 }),
905
940
  }),
906
941
  )
907
942
  }
908
943
 
909
- async createFlow(name: string, idempotencyKey = `flow-${randomId()}`, teamId?: string): Promise<Flow> {
944
+ async createFlow(name: string, idempotencyKey = `flow-${randomId()}`, teamId?: string, tagIds?: readonly string[]): Promise<Flow> {
910
945
  return flow(
911
946
  await this.request('/v1/flows', {
912
- body: JSON.stringify({ name, ...(teamId == null ? {} : { teamId }), version: 1 }),
947
+ body: JSON.stringify({ name, ...(teamId == null ? {} : { teamId }), ...(tagIds == null ? {} : { tagIds }), version: 1 }),
913
948
  headers: { 'idempotency-key': idempotencyKey },
914
949
  method: 'POST',
915
950
  }),
@@ -938,13 +973,52 @@ export class ControlClient {
938
973
  return source.listeners.map(errorListener)
939
974
  }
940
975
 
941
- async renameFlow(flowId: string, name: string): Promise<Flow> {
942
- return flow(
943
- await this.request(`/v1/flows/${segment(flowId)}`, {
944
- body: JSON.stringify({ name, version: 1 }),
945
- method: 'PATCH',
946
- }),
976
+ async listTags(signal?: AbortSignal): Promise<{ readonly tags: readonly TagListItem[]; readonly version: 1 }> {
977
+ const source = record(await this.request('/v1/tags', { signal }))
978
+ if (source.version != 1 || !Array.isArray(source.tags)) return invalidResponse()
979
+ return { tags: source.tags.map(decodeTagListItem), version: 1 }
980
+ }
981
+
982
+ async createTag(name: string): Promise<Tag> {
983
+ return decodeTag(await this.request('/v1/tags', { method: 'POST', body: JSON.stringify({ name, version: 1 }) }))
984
+ }
985
+
986
+ async renameTag(tagId: string, name: string): Promise<Tag> {
987
+ return decodeTag(await this.request(`/v1/tags/${segment(tagId)}`, { method: 'PATCH', body: JSON.stringify({ name, version: 1 }) }))
988
+ }
989
+
990
+ async deleteTag(tagId: string): Promise<void> {
991
+ const source = record(await this.request(`/v1/tags/${segment(tagId)}`, { method: 'DELETE' }))
992
+ if (source.version != 1) invalidResponse()
993
+ }
994
+
995
+ async setFlowTags(flowId: string, tagIds: readonly string[]): Promise<Flow> {
996
+ return flow(await this.request(`/v1/flows/${segment(flowId)}/tags`, { method: 'PUT', body: JSON.stringify({ tagIds, version: 1 }) }))
997
+ }
998
+
999
+ async updateFlow(flowId: string, changes: FlowUpdate): Promise<Flow> {
1000
+ return flow(await this.request(`/v1/flows/${segment(flowId)}`, { method: 'PATCH', body: JSON.stringify({ ...changes, version: 1 }) }))
1001
+ }
1002
+
1003
+ async getFlowState(flowId: string, scope: StateScope, values = false): Promise<FlowStateView> {
1004
+ const result = flowStateViewSchema.safeParse(await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}&values=${values}`))
1005
+ return result.success && result.data.scope === scope ? result.data : invalidResponse()
1006
+ }
1007
+
1008
+ async clearFlowState(flowId: string, scope: StateScope, key?: string): Promise<void> {
1009
+ const source = record(
1010
+ await this.request(`/v1/flows/${segment(flowId)}/state?scope=${scope}${key == null ? '' : `&key=${encodeURIComponent(key)}`}`, { method: 'DELETE' }),
947
1011
  )
1012
+ if (source.version !== 1 || source.cleared !== true) invalidResponse()
1013
+ }
1014
+
1015
+ async clearFlowDedupe(flowId: string, scope: StateScope, space: string): Promise<void> {
1016
+ const source = record(await this.request(`/v1/flows/${segment(flowId)}/dedupe?scope=${scope}&space=${encodeURIComponent(space)}`, { method: 'DELETE' }))
1017
+ if (source.version !== 1 || source.cleared !== true) invalidResponse()
1018
+ }
1019
+
1020
+ async renameFlow(flowId: string, name: string): Promise<Flow> {
1021
+ return this.updateFlow(flowId, { name })
948
1022
  }
949
1023
 
950
1024
  async deleteFlow(flowId: string): Promise<Flow> {
@@ -43,6 +43,7 @@ import {
43
43
  } from '../../flow/common/nodeChanges.ts'
44
44
  import { retryConfigurationSchema } from '../../flow/common/retry.ts'
45
45
  import { httpRequestTask, httpRequestConfigSchema, httpRequestInputs } from '../../http/common/httpRequest.ts'
46
+ import { dedupeConfigSchema, dedupeTask } from '../../state/common/state.ts'
46
47
  import { withWorkflowTask, workflowTask, workflowSelectionTask, workflowCapability, workflowAgentTool } from '../../workflow/common/workflow.ts'
47
48
  import { conditionConfigurationView, prepareConditionConfiguration } from './authoringCondition.ts'
48
49
  import { authoringDiagnosticField } from './authoringDiagnosticFields.ts'
@@ -146,7 +147,7 @@ export function authoringNode(content: RevisionContent, ref: string) {
146
147
  ),
147
148
  }
148
149
  else if (task.executor.kind == 'workflow') config = { flowId: task.executor.flowId }
149
- else if (task.executor.kind == 'http-request') {
150
+ else if (task.executor.kind == 'http-request' || task.executor.kind == 'dedupe') {
150
151
  const { kind: _kind, ...settings } = task.executor
151
152
  config = settings
152
153
  } else if (task.executor.kind == 'openapi') {
@@ -521,6 +522,7 @@ export async function compileAuthoring(base: RevisionContent, request: Authoring
521
522
  task = { ...before, name: String(update.name), executor: { ...executor, ...config } } as ManagedTaskDefinition
522
523
  } else task = await connectorTask(config, String(update.name))
523
524
  } else if (type == 'decision') task = decisionTask(config.questions as Parameters<typeof decisionTask>[0], String(update.name))
525
+ else if (type == 'dedupe') task = { ...before, name: String(update.name), executor: { kind: 'dedupe', ...dedupeConfigSchema.parse(config) } }
524
526
  else if (type == 'http-request') {
525
527
  task = httpRequestTask(httpRequestConfigSchema.parse(config), String(update.name), before)
526
528
  next = { ...next, inputs: httpRequestInputs(node.inputs, before, task) } as GraphNode
@@ -774,6 +776,9 @@ export async function compileAuthoring(base: RevisionContent, request: Authoring
774
776
  case 'connector':
775
777
  changes = createManagedTask(identity, await connectorTask(config, name))
776
778
  break
779
+ case 'dedupe':
780
+ changes = createManagedTask(identity, dedupeTask(dedupeConfigSchema.parse(config), name))
781
+ break
777
782
  case 'http-request':
778
783
  changes = createManagedTask(identity, httpRequestTask(httpRequestConfigSchema.parse(config), name))
779
784
  break
@@ -13,6 +13,7 @@ import {
13
13
  } from '../../flow/common/nodeChanges.ts'
14
14
  import { httpRequestTask } from '../../http/common/httpRequest.ts'
15
15
  import { openApiTask, selectOperation } from '../../openapi/common/openapi.ts'
16
+ import { dedupeTask } from '../../state/common/state.ts'
16
17
 
17
18
  const descriptions = {
18
19
  'start': 'A Start node with a required message parameter. Supply it through trigger.outputs.',
@@ -23,6 +24,7 @@ const descriptions = {
23
24
  'poll': 'Gmail polling by key; replace CONNECTION_ID with an active Gmail connection.',
24
25
  'integration': 'Telegram webhook integration by key; replace CONNECTION_ID with an active Telegram connection.',
25
26
  'decision': 'AI Decision evaluates named questions against target and returns a complete answer object per output. Configure target before running.',
27
+ 'dedupe': 'Filter items using persistent history. Records immediately, including when downstream work fails. Empty field uses each scalar item as identity.',
26
28
  'http-request': 'A single HTTP request. Bind url and request inputs; outputs are body, statusCode and headers.',
27
29
  'openapi': 'A fixed JSON API operation. Replace the example document and URL before running.',
28
30
  'connector':
@@ -57,6 +59,12 @@ export function authoringExample(name: string): { version: 1; operations: readon
57
59
  { name: 'needs_support', type: 'noul', instructions: 'Does the customer need support?' },
58
60
  ])
59
61
  break
62
+ case 'dedupe':
63
+ operations = [
64
+ ...createManagedTask({ nodeId: 'dedupe' }, dedupeTask()),
65
+ { kind: 'graph.node.input.set', nodeId: 'dedupe', handle: 'items', value: { kind: 'value', value: ['a', 'b', 'a'] } },
66
+ ]
67
+ break
60
68
  case 'http-request':
61
69
  operations = [
62
70
  ...createManagedTask({ nodeId: 'http' }, httpRequestTask()),
@@ -6,10 +6,13 @@ import {
6
6
  connectorAgentToolSchema,
7
7
  workflowAgentToolSchema,
8
8
  connectorCapabilitySchema,
9
+ httpCapabilitySchema,
9
10
  authoringDefinitionSchemas as definitions,
10
11
  } from '../../flow/common/changeSchema.ts'
11
12
  import { retryConfigurationSchema, retryNodeTypes } from '../../flow/common/retry.ts'
12
13
  import { httpRequestConfigSchema } from '../../http/common/httpRequest.ts'
14
+ import { currentEngineContract, findEngineContract } from '../../runtime/common/engineContract.ts'
15
+ import { dedupeConfigSchema } from '../../state/common/state.ts'
13
16
  import { authoringLlmConfiguration } from './authoringLlm.ts'
14
17
  import { authoringOpenApiSchema } from './authoringOpenApi.ts'
15
18
 
@@ -33,6 +36,7 @@ export const authoringTypes = [
33
36
  'decision',
34
37
  'openapi',
35
38
  'http-request',
39
+ 'dedupe',
36
40
  'condition',
37
41
  'value',
38
42
  'wait',
@@ -71,7 +75,9 @@ const agentTool = z.union([
71
75
  workflowAgentToolSchema.omit({ id: true, inputs: true, outputs: true, revisionId: true }).extend({ inputs: toolSources }).strict(),
72
76
  connectorAgentToolSchema.omit({ id: true, inputs: true }).extend({ inputs: toolSources }).strict(),
73
77
  ])
74
- const authoringCapabilities = z.array(z.union([connectorCapabilitySchema, z.strictObject({ kind: z.literal('workflow'), name: id, flowId: id })]))
78
+ const authoringCapabilities = z.array(
79
+ z.union([connectorCapabilitySchema, z.strictObject({ kind: z.literal('workflow'), name: id, flowId: id }), httpCapabilitySchema]),
80
+ )
75
81
  const notification = z.strictObject({
76
82
  ...definitions.connector.shape,
77
83
  messageHandle: id,
@@ -114,6 +120,7 @@ export const authoringConfigurations = {
114
120
  'connector': definitions.connector.omit({ outputMode: true }).strict(),
115
121
  'openapi': authoringOpenApiSchema,
116
122
  'http-request': httpRequestConfigSchema,
123
+ 'dedupe': dedupeConfigSchema,
117
124
  'agent': definitions.agent
118
125
  .omit({ prompt: true, tools: true })
119
126
  .extend({
@@ -259,6 +266,9 @@ const requestContracts = {
259
266
  },
260
267
  }
261
268
 
269
+ const codeBuiltins = [...findEngineContract(currentEngineContract)!.builtinModules!].filter((name) => !name.startsWith('node:'))
270
+ const codeGuidance = `Code is an ES module whose default export receives (inputs, context) and returns the business outputs. It may import the Node builtins ${codeBuiltins.join(', ')}, with or without the node: prefix; fs is a fresh in-memory filesystem containing /tmp per execution. require, dynamic import, packages and the http/net builtins are unavailable. To call HTTP APIs, declare their hosts in config.capabilities as {kind: "http", hosts: ["api.example.com", "*.example.org"]} and use context.fetch(url, {method, headers, body}); it returns status, ok, headers.get(), text(), json() and bytes(), does not follow redirects, adds no credentials, and limits bodies to 1 MiB. Prefer an HTTP Request node for a single call without transformation and Connector Actions for services with accounts. context provides inputs, signal, getPrevious(), fetch, actions, workflows, artifact, flowId, runId and blockId. Select workflows through config.capabilities: [{kind: "workflow", flowId, name}]. Call context.workflows.call(name, input) and await its business output. The platform derives input/output types. Refresh a selected workflow tool with node.refresh and tool: its name. Calls obey node and Run time limits.`
271
+
262
272
  /** Transport-independent discovery of the same requests the service validates. */
263
273
  export function authoringRequestContract(name: string) {
264
274
  if (!Object.hasOwn(requestContracts, name)) return undefined
@@ -276,7 +286,7 @@ export function authoringSchema(kind?: AuthoringType) {
276
286
  }
277
287
  : {
278
288
  type: kind,
279
- onError: retryNodeTypes.some((nodeType) => nodeType == kind) ? { enum: ['stop', 'error'] } : false,
289
+ onError: kind == 'dedupe' || retryNodeTypes.some((nodeType) => nodeType == kind) ? { enum: ['stop', 'error'] } : false,
280
290
  retry: retryNodeTypes.some((nodeType) => nodeType == kind) ? z.toJSONSchema(retryConfigurationSchema) : false,
281
291
  inputSources: ['poll', 'integration'].includes(kind)
282
292
  ? ['value', 'unset', 'default']
@@ -299,17 +309,19 @@ export function authoringSchema(kind?: AuthoringType) {
299
309
  ? 'Configure static parameters with node.add.inputs or input.set. Definitions come from the selected Trigger key; execution output and variable sources are unavailable.'
300
310
  : kind == 'llm'
301
311
  ? 'Configure model settings, message templates and history as defaults. Input bindings with the same names override these defaults; named business inputs can be bound directly. Template strings interpolate {{name}}.'
302
- : kind == 'http-request'
303
- ? 'Configure method, query/headers modes and names, bodyFormat, authentication, responseFormat and statusPolicy. Bind url, query.<name>/header.<name> or whole query/headers objects, body and auth.token (Basic: auth.username/auth.password). Credentials accept variables or upstream outputs only. Outputs: body, statusCode, headers. Authentication is replaced as a complete choice.'
304
- : kind == 'openapi'
305
- ? 'Select an operation; the tool resolves its ports and authentication. Read authenticationChoices from node details. authentication is replaced as a complete choice. Bind advertised credentials through inputs.'
306
- : kind == 'workflow'
307
- ? 'Select another workflow with config.flowId. A workflow cannot call itself, including in a disconnected node. Owned unpublished or unavailable targets may be saved as incomplete drafts. Use node.refresh to load the current published interface.'
308
- : kind == 'agent'
309
- ? 'Tools accept Connector Actions or {kind: "workflow", flowId, name, description, approval, inputs}. Workflow parameter sources use model, input or value. The platform derives the interface. Refresh a workflow tool with node.refresh and tool: its name.'
310
- : kind == 'code'
311
- ? 'Select workflows through config.capabilities: [{kind: "workflow", flowId, name}]. Call context.workflows.call(name, input) and await its business output. The platform derives input/output types. Refresh a selected workflow tool with node.refresh and tool: its name. Calls obey node and Run time limits.'
312
- : 'Supply business configuration and input data. Fixed definitions are assembled by the tool. Existing constraints and defaults are preserved on partial updates.',
312
+ : kind == 'dedupe'
313
+ ? 'Bind an items array. Configure field (dot-separated identity path, empty for scalar items), mode (new or increasing), and space (node or flow). Outputs are items and duplicates. History persists immediately; downstream failures do not roll it back. Increasing mode accepts finite numbers or RFC 3339 timestamps, never mixed types.'
314
+ : kind == 'http-request'
315
+ ? 'Configure method, query/headers modes and names, bodyFormat, authentication, responseFormat and statusPolicy. Bind url, query.<name>/header.<name> or whole query/headers objects, body and auth.token (Basic: auth.username/auth.password). Credentials accept variables or upstream outputs only. Outputs: body, statusCode, headers. Authentication is replaced as a complete choice.'
316
+ : kind == 'openapi'
317
+ ? 'Select an operation; the tool resolves its ports and authentication. Read authenticationChoices from node details. authentication is replaced as a complete choice. Bind advertised credentials through inputs.'
318
+ : kind == 'workflow'
319
+ ? 'Select another workflow with config.flowId. A workflow cannot call itself, including in a disconnected node. Owned unpublished or unavailable targets may be saved as incomplete drafts. Use node.refresh to load the current published interface.'
320
+ : kind == 'agent'
321
+ ? 'Tools accept Connector Actions or {kind: "workflow", flowId, name, description, approval, inputs}. Workflow parameter sources use model, input or value. The platform derives the interface. Refresh a workflow tool with node.refresh and tool: its name.'
322
+ : kind == 'code'
323
+ ? codeGuidance
324
+ : 'Supply business configuration and input data. Fixed definitions are assembled by the tool. Existing constraints and defaults are preserved on partial updates.',
313
325
  example: {
314
326
  op: 'node.add',
315
327
  as: 'step',
@@ -323,6 +335,7 @@ export function authoringSchema(kind?: AuthoringType) {
323
335
  'connector': { config: { action: '<Action ID>', connectionId: '<Connection ID>' } },
324
336
  'poll': { config: { key: '<Trigger key>', schedule: [{ type: 'every', unit: 'minute', value: 5 }] } },
325
337
  'integration': { config: { key: '<Trigger key>' } },
338
+ 'dedupe': { config: { field: 'id', mode: 'new', space: 'node' }, inputs: { items: { kind: 'value', value: [{ id: 'a' }, { id: 'b' }] } } },
326
339
  'http-request': { config: {}, inputs: { url: { kind: 'value', value: 'https://example.com' } } },
327
340
  'openapi': { config: { sourceUrl: 'https://example.com/openapi.json', path: '/items', method: 'get' } },
328
341
  'code': { code: 'export default ({value}) => ({result:value})', inputs: { value: { kind: 'value', value: 'Hello' } } },
@@ -17,7 +17,7 @@ export type AuthoringCapability = Exclude<TaskCapability, WorkflowCapability> |
17
17
  export type WorkflowLookup = (flowId: string) => Promise<WorkflowContract | WorkflowCandidate>
18
18
 
19
19
  interface ToolIdentity {
20
- readonly kind?: 'connector' | 'workflow'
20
+ readonly kind?: string
21
21
  readonly name?: string
22
22
  readonly flowId?: string
23
23
  }
@@ -4,8 +4,8 @@ import type { ConnectorConnection, CreateEventSource, ResultInfo, PollTriggerTes
4
4
  import type { ProviderAccessIdentity } from './providerAccess.ts'
5
5
 
6
6
  import { dequal } from 'dequal/lite'
7
- import { currentEngineContract as engineContract } from '../../execution/common/engineContract.ts'
8
7
  import { upgradeRevisionContent } from '../../flow/common/change.ts'
8
+ import { currentEngineContract as engineContract } from '../../runtime/common/engineContract.ts'
9
9
  import { ControlClient, decodeRunEvent } from './api.ts'
10
10
  import { allConnectorConnectionsQuery } from './connectorQueries.ts'
11
11
 
@@ -14,10 +14,10 @@ import type {
14
14
  ProviderAccessBindingCandidate,
15
15
  } from './api.ts'
16
16
 
17
+ import { providerIconAppearance } from '../../connector/common/providerIconSprite.ts'
17
18
  import { exact, requiredFields, integer, invalidResponse, jsonValue, record, string } from './decoding.ts'
18
19
  import { ApiError } from './errors.ts'
19
20
  import { parseProviderAccessSource } from './providerAccess.ts'
20
- import { providerIconAppearance } from './providerIconSprite.ts'
21
21
 
22
22
  export function leinaBots(value: unknown): readonly LeinaBot[] {
23
23
  const source = record(value)
@@ -42,6 +42,9 @@ export const controlErrorCode = {
42
42
  triggerKeyInvalid: 'trigger-key.invalid',
43
43
  triggerKeyNotFound: 'trigger-key.not-found',
44
44
  triggerNotFound: 'trigger.not-found',
45
+ tagInvalid: 'tag.invalid',
46
+ tagConflict: 'tag.conflict',
47
+ tagNotFound: 'tag.not-found',
45
48
  variableInvalid: 'variable.invalid',
46
49
  variableLimitReached: 'variable.limit-reached',
47
50
  variableNotFound: 'variable.not-found',
@@ -93,6 +96,9 @@ export const controlErrorMetadata = {
93
96
  [controlErrorCode.triggerKeyInvalid]: { status: 409 },
94
97
  [controlErrorCode.triggerKeyNotFound]: { status: 404 },
95
98
  [controlErrorCode.triggerNotFound]: { status: 404 },
99
+ [controlErrorCode.tagInvalid]: { status: 400 },
100
+ [controlErrorCode.tagConflict]: { status: 409 },
101
+ [controlErrorCode.tagNotFound]: { status: 404 },
96
102
  [controlErrorCode.variableInvalid]: { status: 400 },
97
103
  [controlErrorCode.variableLimitReached]: { status: 409 },
98
104
  [controlErrorCode.variableNotFound]: { status: 404 },
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod'
2
+ import { eventSourceViewSchema } from '../../event-source/common/view.ts'
2
3
 
3
4
  const id = z.string().min(1).max(256)
4
5
  const eventType = z.string().regex(/^[a-z][a-z0-9_.:]{0,127}$/)
@@ -10,45 +11,17 @@ const eventSourceSchema = z.object({
10
11
  revision: z.int().positive(),
11
12
  name,
12
13
  provider: z.enum(['feishu', 'feishu_app_bot', 'slackbot', 'intercom', 'sentry', 'zoom', 'fireflies', 'jira']),
13
- appId: id.nullable(),
14
- workspaceId: id.optional(),
15
- installationId: id.optional(),
16
- organizationId: id.optional(),
17
- projectId: id.optional(),
18
- projectKey: id.optional(),
19
- subscriptionStatus: z.enum(['idle', 'pending', 'ready', 'error']).optional(),
20
- lastError: z.string().optional(),
21
- signingSecretConfigured: z.boolean().optional(),
14
+ ...eventSourceViewSchema.shape,
22
15
  connectionId: id,
23
16
  teamId: id.nullable(),
24
17
  enabled: z.boolean(),
25
18
  eventTypes: z.array(eventType),
26
- manageSubscriptions: z.boolean().nullable(),
27
- verificationTokenConfigured: z.boolean().nullable(),
28
- encryptKeyConfigured: z.boolean().nullable(),
29
19
  endpointUrl: z.string().nullable(),
30
- verifiedAt: z.string().nullable(),
31
20
  lastReceivedAt: z.string().nullable(),
32
21
  updatedAt: z.string(),
33
22
  consumers: z.array(z.object({ flowId: id, flowName: z.string(), triggerNodeId: id })),
34
23
  })
35
24
 
36
- export const eventSourceViewSchema = eventSourceSchema.pick({
37
- appId: true,
38
- workspaceId: true,
39
- installationId: true,
40
- organizationId: true,
41
- projectId: true,
42
- projectKey: true,
43
- subscriptionStatus: true,
44
- lastError: true,
45
- signingSecretConfigured: true,
46
- manageSubscriptions: true,
47
- verificationTokenConfigured: true,
48
- encryptKeyConfigured: true,
49
- verifiedAt: true,
50
- })
51
-
52
25
  export interface CreateFeishuEventSource {
53
26
  readonly kind?: 'feishu'
54
27
  readonly version: 1
@@ -60,6 +60,7 @@ export function flow(value: unknown): Flow {
60
60
  revisionId: string(target.revisionId),
61
61
  },
62
62
  }),
63
+ tagIds: source.tagIds === undefined ? [] : strings(source.tagIds),
63
64
  createdAt: string(source.createdAt),
64
65
  ...(source.connectorTeamId == null ? {} : { connectorTeamId: string(source.connectorTeamId) }),
65
66
  draftRevisionId: string(source.draftRevisionId),
@@ -5,6 +5,7 @@ import { canvasCommandSchema } from '../../canvas/common/commands.ts'
5
5
  import { runStatuses } from '../../execution/common/runLifecycle.ts'
6
6
  import { waitCommentSchema } from '../../execution/common/wait.ts'
7
7
  import { resourceNameIssue } from '../../flow/common/change.ts'
8
+ import { stateQuerySchema, stateClearSchema, dedupeClearSchema } from '../../state/common/state.ts'
8
9
  import { authoringReadSchema, authoringSearchSchema, authoringRequestSchema, authoringSchemaQuery } from './authoringSchema.ts'
9
10
  import { resultQuerySchema } from './resultQuery.ts'
10
11
 
@@ -50,6 +51,21 @@ function tool<Args>(description: string, schema: z.ZodType<Args>, readOnly: bool
50
51
  }
51
52
 
52
53
  export const mcpTools = {
54
+ flow_state: tool(
55
+ 'Inspect Flow state usage, business keys and deduplication spaces. Values are omitted unless requested. Scope is required.',
56
+ stateQuerySchema.extend({ flowId: flow }),
57
+ true,
58
+ ),
59
+ flow_state_clear: tool(
60
+ 'Delete one business state key or clear all business values. Explicit draft/live scope is required; live changes production state immediately.',
61
+ stateClearSchema.extend({ flowId: flow }),
62
+ false,
63
+ ),
64
+ flow_dedupe_clear: tool(
65
+ 'Clear one deduplication space (flow or node:<id>). Explicit draft/live scope is required; live changes production history immediately.',
66
+ dedupeClearSchema.extend({ flowId: flow }),
67
+ false,
68
+ ),
53
69
  flow_canvas: tool(
54
70
  'Broadcast a transient canvas command to connected Workbenches viewing this Flow. Success only confirms Server broadcast, not delivery or execution. No replay, session selection or Presentation save. Pan uses screen pixels (positive right/down); zoom is an absolute ratio (0.1–3); fit includes all nodes. Repeating pan moves again.',
55
71
  z.strictObject({ flowId: flow, command: canvasCommandSchema }),
@@ -18,6 +18,8 @@ const json: z.ZodType<JsonValue> = z.json()
18
18
  const id = z.string().min(1)
19
19
  const version = z.literal(1)
20
20
  const flowName = id.refine((value) => value == value.trim() && resourceNameIssue(value) == null, 'Flow name is invalid.')
21
+ const tagIds = z.array(id).transform((ids) => [...new Set(ids)].toSorted())
22
+ const tagName = id.transform((name) => name.trim()).refine((name) => resourceNameIssue(name) == null, 'Tag name is invalid.')
21
23
  const inputs = z.record(z.string(), z.record(z.string(), json))
22
24
  const trigger = z.strictObject({ nodeId: id, outputs: z.record(z.string(), json) })
23
25
  const schemas = {
@@ -31,8 +33,11 @@ const schemas = {
31
33
  createEventSource: createEventSourceSchema,
32
34
  updateEventSource: updateEventSourceSchema,
33
35
  eventSourceRevision: eventSourceRevisionSchema,
34
- importFlow: z.strictObject({ file: json, name: flowName.optional(), teamId: id.optional(), version }),
35
- createFlow: z.strictObject({ name: flowName, teamId: id.optional(), version }),
36
+ importFlow: z.strictObject({ file: json, name: flowName.optional(), teamId: id.optional(), tagIds: tagIds.optional(), version }),
37
+ createFlow: z.strictObject({ name: flowName, teamId: id.optional(), tagIds: tagIds.optional(), version }),
38
+ saveTag: z.strictObject({ name: tagName, version }),
39
+ setFlowTags: z.strictObject({ tagIds, version }),
40
+ updateFlow: z.union([z.strictObject({ name: flowName, tagIds: tagIds.optional(), version }), z.strictObject({ name: flowName.optional(), tagIds, version })]),
36
41
  renameFlow: z.strictObject({ name: flowName, version }),
37
42
  changeDraft: z.strictObject({ expectedRevisionId: id, operations: z.array(json).min(1), version }),
38
43
  repairDraft: z.strictObject({ expectedRevisionId: id, version }),
@@ -79,6 +84,9 @@ export const controlRequests = {
79
84
  eventSourceRevision: decoder(schemas.eventSourceRevision),
80
85
  importFlow: decoder(schemas.importFlow),
81
86
  createFlow: decoder(schemas.createFlow),
87
+ saveTag: decoder(schemas.saveTag),
88
+ setFlowTags: decoder(schemas.setFlowTags),
89
+ updateFlow: decoder(schemas.updateFlow),
82
90
  renameFlow: decoder(schemas.renameFlow),
83
91
  changeDraft(value: unknown): { expectedRevisionId: string; operations: readonly DraftOperation[]; version: 1 } {
84
92
  const body = schemas.changeDraft.parse(value)
@@ -0,0 +1,20 @@
1
+ import type { FlowQuery, Tag, TagListItem } from './api.ts'
2
+
3
+ import { integer, invalidResponse, record, string } from './decoding.ts'
4
+
5
+ export function normalizeFlowQuery(query: FlowQuery = {}): Required<FlowQuery> {
6
+ return { query: query.query?.trim().toLowerCase() ?? '', tagIds: [...new Set(query.tagIds ?? [])].toSorted(), untagged: query.untagged ?? false }
7
+ }
8
+
9
+ export function decodeTag(value: unknown): Tag {
10
+ const source = record(value)
11
+ if (source.version != 1) return invalidResponse()
12
+ return { tagId: string(source.tagId), name: string(source.name), version: 1 }
13
+ }
14
+
15
+ export function decodeTagListItem(value: unknown): TagListItem {
16
+ const source = record(value)
17
+ const referenceCount = integer(source.referenceCount)
18
+ if (referenceCount < 0) return invalidResponse()
19
+ return { ...decodeTag(source), referenceCount }
20
+ }
@@ -1,19 +1,12 @@
1
1
  import type { TriggerKeySnapshot } from '../../flow/common/change.ts'
2
2
  import type { UiLanguage } from '../../localization/common/languages.ts'
3
+ import type { TriggerDisplay } from '../../trigger/providers/localization.ts'
3
4
  import type { TriggerKeySummary } from './api.ts'
4
5
 
5
6
  import { isUiLanguage } from '../../localization/common/languages.ts'
6
7
  import { exact, requiredFields, invalidResponse, record, string } from './decoding.ts'
7
8
  import { triggerKey } from './triggerDecoders.ts'
8
9
 
9
- export interface TriggerDisplay {
10
- readonly configInputs: Readonly<Record<string, string>>
11
- readonly configInputLabels: Readonly<Record<string, string>>
12
- readonly displayName: string
13
- readonly description: string
14
- readonly outputs: Readonly<Record<string, string>>
15
- }
16
-
17
10
  export interface TriggerCatalog {
18
11
  readonly version: 3
19
12
  readonly locale: UiLanguage
@@ -1,4 +1,4 @@
1
- import type { InputPort, JsonValue, ManagedTaskDefinition, Port } from '../../flow/common/change.ts'
1
+ import type { InputPort, JsonValue, ManagedTaskDefinitionOf, Port, TaskExecutor } from '../../flow/common/model.ts'
2
2
 
3
3
  import { dequal } from 'dequal/lite'
4
4
  import { matchesSchema } from '../../flow/common/schema.ts'
@@ -18,6 +18,10 @@ export interface DecisionIssue {
18
18
  readonly message: string
19
19
  }
20
20
 
21
+ function isDecisionExecutor(executor: TaskExecutor): executor is DecisionExecutor {
22
+ return executor.kind === 'decision'
23
+ }
24
+
21
25
  export const decisionModel = 'typesafe/jev'
22
26
  export const decisionLimits = { choice: 255, score: 10 } as const
23
27
 
@@ -34,8 +38,8 @@ export function limitDecisionQuestions(questions: readonly DecisionQuestion[]):
34
38
  }
35
39
 
36
40
  /** Loading an oversized task also updates its previously generated output schemas. */
37
- export function limitDecisionTask(task: ManagedTaskDefinition): ManagedTaskDefinition {
38
- if (task.executor.kind !== 'decision') return task
41
+ export function limitDecisionTask<Task extends ManagedTaskDefinitionOf<TaskExecutor>>(task: Task): Task {
42
+ if (!isDecisionExecutor(task.executor)) return task
39
43
  const original = task.executor.questions
40
44
  const questions = limitDecisionQuestions(original)
41
45
  if (questions === original) return task
@@ -45,7 +49,8 @@ export function limitDecisionTask(task: ManagedTaskDefinition): ManagedTaskDefin
45
49
  if (index < 0 || original[index] === questions[index] || !dequal(port.jsonSchema, answerSchema(original[index]!))) return port
46
50
  return { ...port, jsonSchema: decisionAnswerSchema(questions[index]!) }
47
51
  })
48
- return { ...task, executor: { ...task.executor, questions }, outputs }
52
+ // Only the decision executor and its derived outputs change, so the task keeps its own type.
53
+ return { ...task, executor: { ...task.executor, questions }, outputs } as Task
49
54
  }
50
55
  export const decisionStateSchema: JsonValue = { type: ['string', 'object', 'array'] }
51
56
  const probability: JsonValue = { type: 'number', minimum: 0, maximum: 1 }
@@ -102,7 +107,10 @@ function answerSchema(question: DecisionQuestion): JsonValue {
102
107
  return { type: 'object', properties, required: Object.keys(properties) }
103
108
  }
104
109
 
105
- export function decisionTask(questions: readonly DecisionQuestion[] = [defaultDecisionQuestion()], name = 'AI Decision'): ManagedTaskDefinition {
110
+ export function decisionTask(
111
+ questions: readonly DecisionQuestion[] = [defaultDecisionQuestion()],
112
+ name = 'AI Decision',
113
+ ): ManagedTaskDefinitionOf<DecisionExecutor> {
106
114
  questions = limitDecisionQuestions(questions)
107
115
  const inputs: InputPort[] = [
108
116
  { handle: 'target', description: 'Text, object, or array to evaluate across all questions.', jsonSchema: decisionStateSchema, nullable: false },
@@ -111,9 +119,9 @@ export function decisionTask(questions: readonly DecisionQuestion[] = [defaultDe
111
119
  return { name, executor: { kind: 'decision', questions }, inputs, outputs }
112
120
  }
113
121
 
114
- export function decisionTaskIssues(task: ManagedTaskDefinition): readonly DecisionIssue[] {
122
+ export function decisionTaskIssues(task: ManagedTaskDefinitionOf<TaskExecutor>): readonly DecisionIssue[] {
115
123
  task = limitDecisionTask(task)
116
- if (task.executor.kind !== 'decision') return []
124
+ if (!isDecisionExecutor(task.executor)) return []
117
125
  const expected = decisionTask(task.executor.questions, task.name)
118
126
  return [
119
127
  ...decisionQuestionIssues(task.executor.questions),