@hexabot-ai/api 3.2.2-alpha.10 → 3.2.2-alpha.11
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.
- package/dist/actions/actions.service.d.ts +1 -0
- package/dist/actions/actions.service.js +19 -1
- package/dist/actions/actions.service.js.map +1 -1
- package/dist/app.module.js +2 -0
- package/dist/app.module.js.map +1 -1
- package/dist/extensions/actions/ai/mcp.binding.js +2 -0
- package/dist/extensions/actions/ai/mcp.binding.js.map +1 -1
- package/dist/extensions/actions/ai/memory.binding.js +2 -0
- package/dist/extensions/actions/ai/memory.binding.js.map +1 -1
- package/dist/extensions/actions/ai/retrieve-content-rag.action.js +2 -0
- package/dist/extensions/actions/ai/retrieve-content-rag.action.js.map +1 -1
- package/dist/extensions/actions/messaging/list.action.d.ts +2 -2
- package/dist/extensions/actions/messaging/list.action.js +7 -0
- package/dist/extensions/actions/messaging/list.action.js.map +1 -1
- package/dist/extensions/actions/subscriber/update-labels.action.js +3 -0
- package/dist/extensions/actions/subscriber/update-labels.action.js.map +1 -1
- package/dist/extensions/actions/workflow/call-workflow.action.d.ts +19 -0
- package/dist/extensions/actions/workflow/call-workflow.action.js +83 -0
- package/dist/extensions/actions/workflow/call-workflow.action.js.map +1 -0
- package/dist/extensions/actions/workflow/i18n/en.translations.json +9 -0
- package/dist/extensions/actions/workflow/i18n/fr.translations.json +9 -0
- package/dist/extensions/index.d.ts +1 -0
- package/dist/extensions/index.js +1 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools/hexabot-mcp.tools.d.ts +58 -0
- package/dist/static/assets/{browser-ponyfill-DN-Mu1Dr.js → browser-ponyfill-C573gsYp.js} +1 -1
- package/dist/static/assets/{cssMode-BCWxI_Ux.js → cssMode-Dl3j1m5K.js} +1 -1
- package/dist/static/assets/{freemarker2-CFvevPey.js → freemarker2-BC7MYUau.js} +1 -1
- package/dist/static/assets/{handlebars-BCStA9Eg.js → handlebars-DF9DBgjd.js} +1 -1
- package/dist/static/assets/{html-DkIwCA7q.js → html-CbBSWv6-.js} +1 -1
- package/dist/static/assets/{htmlMode-DHeMkUp8.js → htmlMode-BUAbNnKS.js} +1 -1
- package/dist/static/assets/{index-C6VeVxYx.js → index-DwNnhpf5.js} +2103 -2103
- package/dist/static/assets/{javascript-DLPKJzLc.js → javascript-BG37perB.js} +1 -1
- package/dist/static/assets/{jsonMode-BUIDFK3O.js → jsonMode-CTS3-YZB.js} +1 -1
- package/dist/static/assets/{liquid-B0XmwdKh.js → liquid-LhmqJOdD.js} +1 -1
- package/dist/static/assets/{lspLanguageFeatures-Fcpw3Ev_.js → lspLanguageFeatures-BRCtbLBB.js} +1 -1
- package/dist/static/assets/{mdx-DmJKi-dU.js → mdx-DETvz3oa.js} +1 -1
- package/dist/static/assets/{python-B1WnWY4I.js → python-F6lZ-u5p.js} +1 -1
- package/dist/static/assets/{razor-CL2s-6qt.js → razor-PKvkfOLq.js} +1 -1
- package/dist/static/assets/{tsMode-DSFhDcf0.js → tsMode-BpTc9xsq.js} +1 -1
- package/dist/static/assets/{typescript-CmHv7Jwl.js → typescript-chpcHZSI.js} +1 -1
- package/dist/static/assets/{xml-Cyy6zXDM.js → xml-CZLk_iMt.js} +1 -1
- package/dist/static/assets/{yaml-D_D6TAHE.js → yaml-D6hLCygP.js} +1 -1
- package/dist/static/index.html +1 -1
- package/dist/static/locales/en/translation.json +7 -1
- package/dist/static/locales/fr/translation.json +8 -2
- package/dist/transfer/adapters/content-type-transfer.adapter.d.ts +14 -0
- package/dist/transfer/adapters/content-type-transfer.adapter.js +102 -0
- package/dist/transfer/adapters/content-type-transfer.adapter.js.map +1 -0
- package/dist/transfer/adapters/credential-transfer.adapter.d.ts +13 -0
- package/dist/transfer/adapters/credential-transfer.adapter.js +110 -0
- package/dist/transfer/adapters/credential-transfer.adapter.js.map +1 -0
- package/dist/transfer/adapters/label-transfer.adapter.d.ts +15 -0
- package/dist/transfer/adapters/label-transfer.adapter.js +189 -0
- package/dist/transfer/adapters/label-transfer.adapter.js.map +1 -0
- package/dist/transfer/adapters/mcp-server-transfer.adapter.d.ts +16 -0
- package/dist/transfer/adapters/mcp-server-transfer.adapter.js +169 -0
- package/dist/transfer/adapters/mcp-server-transfer.adapter.js.map +1 -0
- package/dist/transfer/adapters/memory-definition-transfer.adapter.d.ts +14 -0
- package/dist/transfer/adapters/memory-definition-transfer.adapter.js +111 -0
- package/dist/transfer/adapters/memory-definition-transfer.adapter.js.map +1 -0
- package/dist/transfer/index.d.ts +7 -0
- package/dist/transfer/index.js +24 -0
- package/dist/transfer/index.js.map +1 -0
- package/dist/transfer/workflow-transfer-adapter.registry.d.ts +27 -0
- package/dist/transfer/workflow-transfer-adapter.registry.js +164 -0
- package/dist/transfer/workflow-transfer-adapter.registry.js.map +1 -0
- package/dist/transfer/workflow-transfer-definition.service.d.ts +40 -0
- package/dist/transfer/workflow-transfer-definition.service.js +353 -0
- package/dist/transfer/workflow-transfer-definition.service.js.map +1 -0
- package/dist/transfer/workflow-transfer-resource-adapter.d.ts +33 -0
- package/dist/transfer/workflow-transfer-resource-adapter.js +81 -0
- package/dist/transfer/workflow-transfer-resource-adapter.js.map +1 -0
- package/dist/transfer/workflow-transfer.controller.d.ts +9 -0
- package/dist/transfer/workflow-transfer.controller.js +63 -0
- package/dist/transfer/workflow-transfer.controller.js.map +1 -0
- package/dist/transfer/workflow-transfer.module.d.ts +2 -0
- package/dist/transfer/workflow-transfer.module.js +47 -0
- package/dist/transfer/workflow-transfer.module.js.map +1 -0
- package/dist/transfer/workflow-transfer.service.d.ts +51 -0
- package/dist/transfer/workflow-transfer.service.js +509 -0
- package/dist/transfer/workflow-transfer.service.js.map +1 -0
- package/dist/transfer/workflow-transfer.types.d.ts +31 -0
- package/dist/transfer/workflow-transfer.types.js +35 -0
- package/dist/transfer/workflow-transfer.types.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/user/dto/user.dto.d.ts +1 -1
- package/dist/utils/test/fixtures/label-group.d.ts +1 -1
- package/dist/utils/test/fixtures/label.d.ts +1 -1
- package/dist/utils/test/fixtures/subscriber.d.ts +1 -1
- package/dist/utils/test/fixtures/workflow-run.js +4 -1
- package/dist/utils/test/fixtures/workflow-run.js.map +1 -1
- package/dist/workflow/contexts/workflow-runtime.context.d.ts +3 -1
- package/dist/workflow/contexts/workflow-runtime.context.js +5 -0
- package/dist/workflow/contexts/workflow-runtime.context.js.map +1 -1
- package/dist/workflow/controllers/workflow-run.controller.js +2 -0
- package/dist/workflow/controllers/workflow-run.controller.js.map +1 -1
- package/dist/workflow/dto/workflow-run.dto.d.ts +1 -0
- package/dist/workflow/dto/workflow-run.dto.js +11 -0
- package/dist/workflow/dto/workflow-run.dto.js.map +1 -1
- package/dist/workflow/entities/memory-record.entity.d.ts +2 -0
- package/dist/workflow/entities/workflow-run.entity.d.ts +62 -0
- package/dist/workflow/entities/workflow-run.entity.js +15 -1
- package/dist/workflow/entities/workflow-run.entity.js.map +1 -1
- package/dist/workflow/index.d.ts +7 -0
- package/dist/workflow/index.js +7 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/repositories/workflow-run.repository.js +7 -1
- package/dist/workflow/repositories/workflow-run.repository.js.map +1 -1
- package/dist/workflow/resource-refs.d.ts +17 -0
- package/dist/workflow/resource-refs.js +15 -0
- package/dist/workflow/resource-refs.js.map +1 -0
- package/dist/workflow/services/agentic.service.d.ts +7 -1
- package/dist/workflow/services/agentic.service.js +134 -16
- package/dist/workflow/services/agentic.service.js.map +1 -1
- package/dist/workflow/services/workflow-run.service.d.ts +3 -0
- package/dist/workflow/services/workflow-run.service.js +75 -3
- package/dist/workflow/services/workflow-run.service.js.map +1 -1
- package/dist/workflow/services/workflow-version.service.d.ts +2 -0
- package/dist/workflow/services/workflow-version.service.js +19 -0
- package/dist/workflow/services/workflow-version.service.js.map +1 -1
- package/dist/workflow/services/workflow.service.d.ts +3 -1
- package/dist/workflow/services/workflow.service.js +5 -0
- package/dist/workflow/services/workflow.service.js.map +1 -1
- package/dist/workflow/types.d.ts +31 -0
- package/dist/workflow/types.js +2 -1
- package/dist/workflow/types.js.map +1 -1
- package/dist/workflow/workflow.module.js +3 -0
- package/dist/workflow/workflow.module.js.map +1 -1
- package/package.json +4 -4
- package/src/actions/actions.service.ts +34 -4
- package/src/app.module.ts +2 -0
- package/src/extensions/actions/ai/mcp.binding.ts +2 -0
- package/src/extensions/actions/ai/memory.binding.ts +2 -0
- package/src/extensions/actions/ai/retrieve-content-rag.action.ts +2 -0
- package/src/extensions/actions/messaging/list.action.ts +7 -0
- package/src/extensions/actions/subscriber/update-labels.action.ts +3 -0
- package/src/extensions/actions/workflow/call-workflow.action.ts +112 -0
- package/src/extensions/actions/workflow/i18n/en.translations.json +9 -0
- package/src/extensions/actions/workflow/i18n/fr.translations.json +9 -0
- package/src/extensions/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/transfer/adapters/content-type-transfer.adapter.ts +154 -0
- package/src/transfer/adapters/credential-transfer.adapter.ts +158 -0
- package/src/transfer/adapters/label-transfer.adapter.ts +291 -0
- package/src/transfer/adapters/mcp-server-transfer.adapter.ts +255 -0
- package/src/transfer/adapters/memory-definition-transfer.adapter.ts +167 -0
- package/src/transfer/index.ts +19 -0
- package/src/transfer/workflow-transfer-adapter.registry.ts +250 -0
- package/src/transfer/workflow-transfer-definition.service.ts +587 -0
- package/src/transfer/workflow-transfer-resource-adapter.ts +132 -0
- package/src/transfer/workflow-transfer.controller.ts +86 -0
- package/src/transfer/workflow-transfer.module.ts +46 -0
- package/src/transfer/workflow-transfer.service.ts +858 -0
- package/src/transfer/workflow-transfer.types.ts +98 -0
- package/src/utils/test/fixtures/workflow-run.ts +4 -1
- package/src/workflow/contexts/workflow-runtime.context.ts +12 -1
- package/src/workflow/controllers/workflow-run.controller.ts +2 -0
- package/src/workflow/dto/workflow-run.dto.ts +10 -0
- package/src/workflow/entities/workflow-run.entity.ts +22 -1
- package/src/workflow/index.ts +14 -0
- package/src/workflow/repositories/workflow-run.repository.ts +7 -1
- package/src/workflow/resource-refs.ts +44 -0
- package/src/workflow/services/agentic.service.ts +256 -26
- package/src/workflow/services/workflow-run.service.ts +120 -7
- package/src/workflow/services/workflow-version.service.ts +33 -0
- package/src/workflow/services/workflow.service.ts +21 -1
- package/src/workflow/types.ts +52 -0
- package/src/workflow/workflow.module.ts +3 -0
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
StepExecutionRecord,
|
|
12
12
|
WorkflowRunner,
|
|
13
13
|
} from '@hexabot-ai/agentic';
|
|
14
|
-
import {
|
|
14
|
+
import { WorkflowFull, WorkflowRunFull } from '@hexabot-ai/types';
|
|
15
15
|
import { Injectable } from '@nestjs/common';
|
|
16
16
|
|
|
17
17
|
import { ActionService } from '@/actions/actions.service';
|
|
@@ -20,16 +20,30 @@ import { I18nService } from '@/i18n/services/i18n.service';
|
|
|
20
20
|
import { LoggerService } from '@/logger/logger.service';
|
|
21
21
|
|
|
22
22
|
import { WorkflowContextFactory } from '../contexts/workflow-context-factory';
|
|
23
|
-
import { WorkflowRuntimeContext } from '../contexts/workflow-runtime.context';
|
|
23
|
+
import type { WorkflowRuntimeContext } from '../contexts/workflow-runtime.context';
|
|
24
24
|
import { TriggerEventWrapper } from '../lib/trigger-event-wrapper';
|
|
25
25
|
import { parseWorkflowDefinition } from '../lib/workflow-definition';
|
|
26
|
-
import {
|
|
26
|
+
import type {
|
|
27
|
+
CallWorkflowOptions,
|
|
28
|
+
CallWorkflowResult,
|
|
29
|
+
RunStrategy,
|
|
30
|
+
RunWorkflowOptions,
|
|
31
|
+
WorkflowCallService,
|
|
32
|
+
WorkflowResult,
|
|
33
|
+
} from '../types';
|
|
27
34
|
|
|
28
35
|
import { WorkflowRunService } from './workflow-run.service';
|
|
29
36
|
import { WorkflowService } from './workflow.service';
|
|
30
37
|
|
|
38
|
+
const MAX_CALL_STACK_DEPTH = 10;
|
|
39
|
+
|
|
40
|
+
type RunWorkflowExecution = {
|
|
41
|
+
run: WorkflowRunFull;
|
|
42
|
+
result: WorkflowResult;
|
|
43
|
+
};
|
|
44
|
+
|
|
31
45
|
@Injectable()
|
|
32
|
-
export class AgenticService {
|
|
46
|
+
export class AgenticService implements WorkflowCallService {
|
|
33
47
|
constructor(
|
|
34
48
|
private readonly workflowService: WorkflowService,
|
|
35
49
|
private readonly workflowRunService: WorkflowRunService,
|
|
@@ -42,7 +56,7 @@ export class AgenticService {
|
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
58
|
* Process an event by resuming a suspended workflow run if it exists,
|
|
45
|
-
* otherwise start a new run using the latest configured workflow
|
|
59
|
+
* otherwise start a new run using the latest configured workflow.
|
|
46
60
|
*/
|
|
47
61
|
async handleEvent(
|
|
48
62
|
event: TriggerEventWrapper,
|
|
@@ -75,11 +89,13 @@ export class AgenticService {
|
|
|
75
89
|
suspensionReason: suspendedRun.suspensionReason,
|
|
76
90
|
});
|
|
77
91
|
|
|
78
|
-
|
|
92
|
+
const execution = await this.runWorkflow({
|
|
79
93
|
mode: 'resume',
|
|
80
94
|
run: suspendedRun,
|
|
81
95
|
event,
|
|
82
96
|
});
|
|
97
|
+
|
|
98
|
+
return execution.run;
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
const workflowToRun = requestedWorkflowId
|
|
@@ -104,11 +120,13 @@ export class AgenticService {
|
|
|
104
120
|
workflowId: workflowToRun.id,
|
|
105
121
|
});
|
|
106
122
|
|
|
107
|
-
|
|
123
|
+
const execution = await this.runWorkflow({
|
|
108
124
|
mode: 'start',
|
|
109
125
|
workflow: workflowToRun,
|
|
110
126
|
event,
|
|
111
127
|
});
|
|
128
|
+
|
|
129
|
+
return execution.run;
|
|
112
130
|
} catch (err) {
|
|
113
131
|
this.logger.error(
|
|
114
132
|
'Unable to process incoming event through agentic workflow',
|
|
@@ -120,20 +138,70 @@ export class AgenticService {
|
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
/**
|
|
123
|
-
*
|
|
141
|
+
* Call another workflow from a running parent workflow.
|
|
142
|
+
*
|
|
143
|
+
* This is intentionally an API-level operation: workflows are database
|
|
144
|
+
* records, so type enforcement, call-stack validation, child run creation,
|
|
145
|
+
* and durable parent/child linkage all live here instead of in the agentic
|
|
146
|
+
* DSL runner.
|
|
147
|
+
*/
|
|
148
|
+
async callWorkflow({
|
|
149
|
+
workflowId,
|
|
150
|
+
input,
|
|
151
|
+
parentContext,
|
|
152
|
+
}: CallWorkflowOptions): Promise<CallWorkflowResult> {
|
|
153
|
+
const parentRun = await this.workflowRunService.findOneAndPopulate(
|
|
154
|
+
parentContext.workflowRunId,
|
|
155
|
+
);
|
|
156
|
+
if (!parentRun) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Unable to load parent workflow run ${parentContext.workflowRunId}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const workflow = await this.workflowService.findOneAndPopulate(workflowId);
|
|
163
|
+
if (!workflow) {
|
|
164
|
+
throw new Error(`Workflow with ID ${workflowId} not found`);
|
|
165
|
+
}
|
|
166
|
+
if (!workflow.definition) {
|
|
167
|
+
throw new Error(`Workflow ${workflowId} is missing a definition`);
|
|
168
|
+
}
|
|
169
|
+
if (workflow.type !== parentRun.workflow.type) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Workflow ${workflowId} has type "${workflow.type}" and cannot be called from a "${parentRun.workflow.type}" workflow`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await this.assertCanCallWorkflow(parentRun, workflow.id);
|
|
176
|
+
|
|
177
|
+
const execution = await this.runWorkflow({
|
|
178
|
+
mode: 'start',
|
|
179
|
+
workflow,
|
|
180
|
+
event: parentContext.event,
|
|
181
|
+
parentRun,
|
|
182
|
+
input,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return this.toCallWorkflowResult(execution.run, execution.result);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Shared runner lifecycle for starts, external resumes, and internal parent
|
|
190
|
+
* resumes. When a resumed child reaches a terminal state, this method also
|
|
191
|
+
* unwinds the suspended parent with the child result payload.
|
|
124
192
|
*/
|
|
125
193
|
private async runWorkflow(
|
|
126
194
|
options: RunWorkflowOptions,
|
|
127
|
-
): Promise<
|
|
195
|
+
): Promise<RunWorkflowExecution> {
|
|
128
196
|
const { event, mode } = options;
|
|
129
197
|
const run =
|
|
130
198
|
mode === 'start'
|
|
131
|
-
? await this.createRun(options.workflow, event
|
|
199
|
+
? await this.createRun(options.workflow, event, {
|
|
200
|
+
parentRun: options.parentRun,
|
|
201
|
+
input: options.input,
|
|
202
|
+
})
|
|
132
203
|
: options.run;
|
|
133
|
-
const
|
|
134
|
-
run.workflow.id,
|
|
135
|
-
);
|
|
136
|
-
const definition = this.resolveRunDefinition(run, workflow);
|
|
204
|
+
const definition = this.resolveRunDefinition(run);
|
|
137
205
|
if (!definition) {
|
|
138
206
|
throw new Error('Workflow definition is required to run the workflow');
|
|
139
207
|
}
|
|
@@ -151,7 +219,7 @@ export class AgenticService {
|
|
|
151
219
|
this.logger.debug('Preparing workflow runner', {
|
|
152
220
|
mode,
|
|
153
221
|
runId: run.id,
|
|
154
|
-
workflowId: workflow?.id,
|
|
222
|
+
workflowId: run.workflow?.id,
|
|
155
223
|
triggeredById: run.triggeredBy?.id,
|
|
156
224
|
});
|
|
157
225
|
|
|
@@ -160,6 +228,7 @@ export class AgenticService {
|
|
|
160
228
|
run,
|
|
161
229
|
context,
|
|
162
230
|
workflowInstance,
|
|
231
|
+
mode === 'resume' ? options.resumeData : undefined,
|
|
163
232
|
);
|
|
164
233
|
|
|
165
234
|
this.logger.debug('Marking workflow run as running', {
|
|
@@ -189,6 +258,17 @@ export class AgenticService {
|
|
|
189
258
|
} catch (err) {
|
|
190
259
|
this.logger.error('Workflow runner threw during execution', err);
|
|
191
260
|
await this.markRunFailed(run, strategy.runner, context.state, err);
|
|
261
|
+
if (mode === 'resume') {
|
|
262
|
+
await this.resumeParentRunFromChild(
|
|
263
|
+
run,
|
|
264
|
+
{
|
|
265
|
+
status: 'failed',
|
|
266
|
+
error: err,
|
|
267
|
+
snapshot: strategy.runner.getSnapshot(),
|
|
268
|
+
},
|
|
269
|
+
event,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
192
272
|
|
|
193
273
|
throw err;
|
|
194
274
|
}
|
|
@@ -208,7 +288,15 @@ export class AgenticService {
|
|
|
208
288
|
context,
|
|
209
289
|
);
|
|
210
290
|
|
|
211
|
-
|
|
291
|
+
if (mode === 'resume') {
|
|
292
|
+
await this.resumeParentRunFromChild(run, result, event);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const persistedRun = await this.workflowRunService.findOneAndPopulate(
|
|
296
|
+
run.id,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return { run: persistedRun ?? run, result };
|
|
212
300
|
}
|
|
213
301
|
|
|
214
302
|
/**
|
|
@@ -231,12 +319,16 @@ export class AgenticService {
|
|
|
231
319
|
|
|
232
320
|
/**
|
|
233
321
|
* Build the execution strategy for starting a new workflow or resuming an existing one.
|
|
322
|
+
*
|
|
323
|
+
* `resumeData` is supplied only for internal child-to-parent unwinds; normal
|
|
324
|
+
* event resumes use the current event input.
|
|
234
325
|
*/
|
|
235
326
|
private async createRunStrategy(
|
|
236
327
|
mode: 'start' | 'resume',
|
|
237
328
|
run: WorkflowRunFull,
|
|
238
329
|
context: WorkflowRuntimeContext,
|
|
239
330
|
workflowInstance: AgenticWorkflow,
|
|
331
|
+
resumeData?: unknown,
|
|
240
332
|
): Promise<RunStrategy> {
|
|
241
333
|
if (mode === 'start') {
|
|
242
334
|
const runner = await workflowInstance.buildAsyncRunner({
|
|
@@ -256,7 +348,7 @@ export class AgenticService {
|
|
|
256
348
|
};
|
|
257
349
|
}
|
|
258
350
|
|
|
259
|
-
const latestInput = context.event.buildInput();
|
|
351
|
+
const latestInput = resumeData ?? context.event.buildInput();
|
|
260
352
|
const runner = await workflowInstance.buildRunnerFromState({
|
|
261
353
|
state: this.buildExecutionState(run),
|
|
262
354
|
context,
|
|
@@ -289,10 +381,18 @@ export class AgenticService {
|
|
|
289
381
|
|
|
290
382
|
/**
|
|
291
383
|
* Create a workflow run record and load it with relations.
|
|
384
|
+
*
|
|
385
|
+
* Child calls inherit the same event metadata, initiator, and thread as the
|
|
386
|
+
* parent, but store an explicit `parentRun` relation so future events can
|
|
387
|
+
* resume the deepest suspended child before returning to the parent.
|
|
292
388
|
*/
|
|
293
389
|
private async createRun(
|
|
294
390
|
workflow: WorkflowFull,
|
|
295
391
|
event: TriggerEventWrapper,
|
|
392
|
+
options: {
|
|
393
|
+
parentRun?: WorkflowRunFull;
|
|
394
|
+
input?: Record<string, unknown>;
|
|
395
|
+
} = {},
|
|
296
396
|
): Promise<WorkflowRunFull> {
|
|
297
397
|
const initiator = event.getInitiator();
|
|
298
398
|
if (!workflow.definition) {
|
|
@@ -307,7 +407,8 @@ export class AgenticService {
|
|
|
307
407
|
workflowVersion: workflow.currentVersion?.id ?? null,
|
|
308
408
|
triggeredBy: initiator.id,
|
|
309
409
|
thread: event.getThreadId() ?? null,
|
|
310
|
-
|
|
410
|
+
parentRun: options.parentRun?.id ?? null,
|
|
411
|
+
input: options.input ?? event.buildInput(),
|
|
311
412
|
context: Object.keys(initialContext).length > 0 ? initialContext : null,
|
|
312
413
|
metadata: event.getMetadata(),
|
|
313
414
|
});
|
|
@@ -321,18 +422,147 @@ export class AgenticService {
|
|
|
321
422
|
}
|
|
322
423
|
|
|
323
424
|
/**
|
|
324
|
-
*
|
|
425
|
+
* Validate that a workflow call does not create a recursive or overly deep stack.
|
|
426
|
+
*
|
|
427
|
+
* The walk follows active parent links, so only the current call stack is
|
|
428
|
+
* rejected; a workflow can still be called again after an earlier stack has
|
|
429
|
+
* completed.
|
|
430
|
+
*/
|
|
431
|
+
private async assertCanCallWorkflow(
|
|
432
|
+
parentRun: WorkflowRunFull,
|
|
433
|
+
targetWorkflowId: string,
|
|
434
|
+
): Promise<void> {
|
|
435
|
+
let current: WorkflowRunFull | null = parentRun;
|
|
436
|
+
let depth = 0;
|
|
437
|
+
|
|
438
|
+
while (current) {
|
|
439
|
+
depth += 1;
|
|
440
|
+
if (depth >= MAX_CALL_STACK_DEPTH) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`Workflow call stack depth cannot exceed ${MAX_CALL_STACK_DEPTH}`,
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (current.workflow.id === targetWorkflowId) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Workflow call cycle detected for workflow ${targetWorkflowId}`,
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const parentRunId = this.resolveRunId(current.parentRun);
|
|
453
|
+
current = parentRunId
|
|
454
|
+
? await this.workflowRunService.findOneAndPopulate(parentRunId)
|
|
455
|
+
: null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Convert a child run terminal/suspended result into the `call_workflow`
|
|
461
|
+
* contract. The same payload shape is used for immediate returns and for
|
|
462
|
+
* resuming a parent after the child completed in a later event.
|
|
325
463
|
*/
|
|
326
|
-
private
|
|
464
|
+
private toCallWorkflowResult(
|
|
327
465
|
run: WorkflowRunFull,
|
|
328
|
-
|
|
329
|
-
) {
|
|
466
|
+
result: WorkflowResult,
|
|
467
|
+
): CallWorkflowResult {
|
|
468
|
+
if (result.status === 'finished') {
|
|
469
|
+
return {
|
|
470
|
+
status: 'finished',
|
|
471
|
+
workflow_id: run.workflow.id,
|
|
472
|
+
workflow_run_id: run.id,
|
|
473
|
+
output: result.output,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (result.status === 'suspended') {
|
|
478
|
+
return {
|
|
479
|
+
status: 'suspended',
|
|
480
|
+
workflow_id: run.workflow.id,
|
|
481
|
+
workflow_run_id: run.id,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return {
|
|
486
|
+
status: 'failed',
|
|
487
|
+
workflow_id: run.workflow.id,
|
|
488
|
+
workflow_run_id: run.id,
|
|
489
|
+
error: this.stringifyError(result.error),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Resume a suspended parent run after its child run completes or fails.
|
|
495
|
+
*
|
|
496
|
+
* This may cascade through multiple ancestors because resuming the parent can
|
|
497
|
+
* itself finish a child call that belongs to another suspended parent.
|
|
498
|
+
*/
|
|
499
|
+
private async resumeParentRunFromChild(
|
|
500
|
+
childRun: WorkflowRunFull,
|
|
501
|
+
result: WorkflowResult,
|
|
502
|
+
event: TriggerEventWrapper,
|
|
503
|
+
): Promise<void> {
|
|
504
|
+
if (result.status === 'suspended') {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const parentRunId = this.resolveRunId(childRun.parentRun);
|
|
509
|
+
if (!parentRunId) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const parentRun =
|
|
514
|
+
await this.workflowRunService.findOneAndPopulate(parentRunId);
|
|
515
|
+
if (!parentRun) {
|
|
516
|
+
this.logger.warn('Unable to resume missing parent workflow run', {
|
|
517
|
+
childRunId: childRun.id,
|
|
518
|
+
parentRunId,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (parentRun.status !== 'suspended') {
|
|
525
|
+
this.logger.warn(
|
|
526
|
+
'Skipping parent workflow resume because it is not suspended',
|
|
527
|
+
{
|
|
528
|
+
childRunId: childRun.id,
|
|
529
|
+
parentRunId,
|
|
530
|
+
parentStatus: parentRun.status,
|
|
531
|
+
},
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
await this.runWorkflow({
|
|
538
|
+
mode: 'resume',
|
|
539
|
+
run: parentRun,
|
|
540
|
+
event,
|
|
541
|
+
resumeData: this.toCallWorkflowResult(childRun, result),
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Resolve a workflow run relation from either a populated object or id.
|
|
547
|
+
*/
|
|
548
|
+
private resolveRunId(
|
|
549
|
+
run: string | { id?: string | null } | null | undefined,
|
|
550
|
+
): string | null {
|
|
551
|
+
if (typeof run === 'string') {
|
|
552
|
+
return run;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return run?.id ?? null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Resolve the workflow definition snapshot used by the current run.
|
|
560
|
+
*/
|
|
561
|
+
private resolveRunDefinition(run: WorkflowRunFull) {
|
|
330
562
|
const definitionYml = run.workflowVersion?.definitionYml;
|
|
331
563
|
if (typeof definitionYml === 'string' && definitionYml.trim() !== '') {
|
|
332
564
|
return parseWorkflowDefinition(definitionYml);
|
|
333
565
|
}
|
|
334
|
-
|
|
335
|
-
return workflow?.definition;
|
|
336
566
|
}
|
|
337
567
|
|
|
338
568
|
/**
|
|
@@ -471,7 +701,7 @@ export class AgenticService {
|
|
|
471
701
|
private pickOutput(
|
|
472
702
|
result: WorkflowResult,
|
|
473
703
|
state: ExecutionState | undefined,
|
|
474
|
-
|
|
704
|
+
previousOutput?: Record<string, unknown> | null,
|
|
475
705
|
): Record<string, unknown> | null {
|
|
476
706
|
if (result.status === 'finished' && result.output) {
|
|
477
707
|
return result.output;
|
|
@@ -481,7 +711,7 @@ export class AgenticService {
|
|
|
481
711
|
return state.output;
|
|
482
712
|
}
|
|
483
713
|
|
|
484
|
-
return
|
|
714
|
+
return previousOutput ?? null;
|
|
485
715
|
}
|
|
486
716
|
|
|
487
717
|
/**
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Full terms: see LICENSE.md.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { WorkflowSnapshot } from '@hexabot-ai/agentic';
|
|
7
|
+
import { EWorkflowRunStatus, WorkflowSnapshot } from '@hexabot-ai/agentic';
|
|
8
8
|
import { WorkflowRun, WorkflowRunFull } from '@hexabot-ai/types';
|
|
9
9
|
import { Injectable } from '@nestjs/common';
|
|
10
|
+
import { In } from 'typeorm';
|
|
10
11
|
|
|
11
12
|
import { BaseOrmService } from '@/utils/generics/base-orm.service';
|
|
12
13
|
|
|
@@ -18,6 +19,12 @@ type StateUpdate = {
|
|
|
18
19
|
context?: Record<string, unknown> | null;
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
const ACTIVE_WORKFLOW_RUN_STATUSES = [
|
|
23
|
+
EWorkflowRunStatus.IDLE,
|
|
24
|
+
EWorkflowRunStatus.RUNNING,
|
|
25
|
+
EWorkflowRunStatus.SUSPENDED,
|
|
26
|
+
];
|
|
27
|
+
|
|
21
28
|
@Injectable()
|
|
22
29
|
export class WorkflowRunService extends BaseOrmService<WorkflowRunOrmEntity> {
|
|
23
30
|
/**
|
|
@@ -130,25 +137,131 @@ export class WorkflowRunService extends BaseOrmService<WorkflowRunOrmEntity> {
|
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
/**
|
|
133
|
-
* Find the
|
|
140
|
+
* Find the suspended run that should receive the next event.
|
|
141
|
+
*
|
|
142
|
+
* For call-and-return stacks, the parent can be suspended while the child is
|
|
143
|
+
* either running or suspended for the same initiator/thread. The next external
|
|
144
|
+
* event must resume the deepest suspended leaf first; once that child
|
|
145
|
+
* completes, AgenticService resumes its parent internally with the child
|
|
146
|
+
* result.
|
|
134
147
|
*
|
|
135
148
|
* @param triggeredById - Identifier of the subscriber whose suspended run should be fetched.
|
|
136
|
-
* @param
|
|
137
|
-
* @
|
|
149
|
+
* @param threadId - Optional thread id for conversational continuity.
|
|
150
|
+
* @param workflowId - Optional workflow id; a parent match still resolves to its child leaf.
|
|
151
|
+
* @returns The suspended leaf run populated with relations, or `null` when none exists.
|
|
138
152
|
*/
|
|
139
153
|
async findSuspendedRunByInitiator(
|
|
140
154
|
triggeredById: string,
|
|
141
155
|
threadId?: string,
|
|
142
156
|
workflowId?: string,
|
|
143
157
|
): Promise<WorkflowRunFull | null> {
|
|
144
|
-
|
|
158
|
+
const runs = await this.findAndPopulate({
|
|
145
159
|
where: {
|
|
146
160
|
triggeredBy: { id: triggeredById },
|
|
147
161
|
...(threadId ? { thread: { id: threadId } } : {}),
|
|
148
|
-
status:
|
|
149
|
-
...(workflowId ? { workflow: { id: workflowId } } : {}),
|
|
162
|
+
status: In(ACTIVE_WORKFLOW_RUN_STATUSES),
|
|
150
163
|
},
|
|
151
164
|
order: { suspendedAt: 'DESC', createdAt: 'DESC' },
|
|
152
165
|
});
|
|
166
|
+
|
|
167
|
+
if (runs.length === 0) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const runsById = new Map(runs.map((run) => [run.id, run]));
|
|
172
|
+
const parentRunIds = new Set(
|
|
173
|
+
runs
|
|
174
|
+
.map((run) => this.resolveRunId(run.parentRun))
|
|
175
|
+
.filter((value): value is string => typeof value === 'string'),
|
|
176
|
+
);
|
|
177
|
+
// Leaves are active runs that are not the parent of another unfinished
|
|
178
|
+
// candidate. Only suspended leaves can consume external events; running or
|
|
179
|
+
// idle leaves are still in flight and block their suspended ancestors.
|
|
180
|
+
const leaves = runs.filter((run) => !parentRunIds.has(run.id));
|
|
181
|
+
const candidates = (leaves.length > 0 ? leaves : runs)
|
|
182
|
+
.filter((run) => run.status === EWorkflowRunStatus.SUSPENDED)
|
|
183
|
+
.filter((run) =>
|
|
184
|
+
workflowId
|
|
185
|
+
? this.isRunInWorkflowStack(run, workflowId, runsById)
|
|
186
|
+
: true,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return this.pickDeepestSuspendedRun(candidates, runsById);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private resolveRunId(
|
|
193
|
+
run: string | { id?: string | null } | null | undefined,
|
|
194
|
+
): string | null {
|
|
195
|
+
if (typeof run === 'string') {
|
|
196
|
+
return run;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return run?.id ?? null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private isRunInWorkflowStack(
|
|
203
|
+
run: WorkflowRunFull,
|
|
204
|
+
workflowId: string,
|
|
205
|
+
runsById: Map<string, WorkflowRunFull>,
|
|
206
|
+
): boolean {
|
|
207
|
+
let current: WorkflowRunFull | undefined = run;
|
|
208
|
+
const visited = new Set<string>();
|
|
209
|
+
|
|
210
|
+
while (current && !visited.has(current.id)) {
|
|
211
|
+
visited.add(current.id);
|
|
212
|
+
if (current.workflow.id === workflowId) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
const parentRunId = this.resolveRunId(current.parentRun);
|
|
216
|
+
current = parentRunId ? runsById.get(parentRunId) : undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private pickDeepestSuspendedRun(
|
|
223
|
+
runs: WorkflowRunFull[],
|
|
224
|
+
runsById: Map<string, WorkflowRunFull>,
|
|
225
|
+
): WorkflowRunFull | null {
|
|
226
|
+
if (runs.length === 0) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const getDepth = (run: WorkflowRunFull): number => {
|
|
231
|
+
let depth = 0;
|
|
232
|
+
let current: WorkflowRunFull | undefined = run;
|
|
233
|
+
const visited = new Set<string>();
|
|
234
|
+
|
|
235
|
+
while (current && !visited.has(current.id)) {
|
|
236
|
+
visited.add(current.id);
|
|
237
|
+
depth += 1;
|
|
238
|
+
const parentRunId = this.resolveRunId(current.parentRun);
|
|
239
|
+
current = parentRunId ? runsById.get(parentRunId) : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return depth;
|
|
243
|
+
};
|
|
244
|
+
const getTime = (value?: Date | string | null): number => {
|
|
245
|
+
if (!value) {
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
250
|
+
const timestamp = date.getTime();
|
|
251
|
+
|
|
252
|
+
return Number.isNaN(timestamp) ? 0 : timestamp;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return [...runs].sort((a, b) => {
|
|
256
|
+
const depthDiff = getDepth(b) - getDepth(a);
|
|
257
|
+
if (depthDiff !== 0) {
|
|
258
|
+
return depthDiff;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
getTime(b.suspendedAt) - getTime(a.suspendedAt) ||
|
|
263
|
+
getTime(b.createdAt) - getTime(a.createdAt)
|
|
264
|
+
);
|
|
265
|
+
})[0];
|
|
153
266
|
}
|
|
154
267
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { WorkflowVersion } from '@hexabot-ai/types';
|
|
8
8
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
9
|
+
import { EntityManager } from 'typeorm';
|
|
9
10
|
|
|
10
11
|
import { BaseOrmService } from '@/utils/generics/base-orm.service';
|
|
11
12
|
|
|
@@ -63,6 +64,38 @@ export class WorkflowVersionService extends BaseOrmService<WorkflowVersionOrmEnt
|
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Persist a workflow version snapshot with the provided transaction manager.
|
|
69
|
+
*
|
|
70
|
+
* This method mirrors createSnapshot's version-number behavior without
|
|
71
|
+
* escaping the caller's transaction or emitting repository-level events.
|
|
72
|
+
*/
|
|
73
|
+
public async createSnapshotWithManager(
|
|
74
|
+
manager: EntityManager,
|
|
75
|
+
params: WorkflowNewVersionDto,
|
|
76
|
+
): Promise<WorkflowVersionOrmEntity> {
|
|
77
|
+
const latestVersion = await manager.findOne(WorkflowVersionOrmEntity, {
|
|
78
|
+
where: { workflow: { id: params.workflow } },
|
|
79
|
+
order: { version: 'DESC' },
|
|
80
|
+
});
|
|
81
|
+
const nextVersion = latestVersion ? latestVersion.version + 1 : 1;
|
|
82
|
+
|
|
83
|
+
return await manager.save(
|
|
84
|
+
WorkflowVersionOrmEntity,
|
|
85
|
+
manager.create(WorkflowVersionOrmEntity, {
|
|
86
|
+
workflow: { id: params.workflow },
|
|
87
|
+
version: nextVersion,
|
|
88
|
+
definitionYml: params.definitionYml,
|
|
89
|
+
action: params.action,
|
|
90
|
+
message: params.message,
|
|
91
|
+
parentVersion: params.parentVersion
|
|
92
|
+
? { id: params.parentVersion }
|
|
93
|
+
: null,
|
|
94
|
+
createdBy: params.createdBy ? { id: params.createdBy } : null,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
66
99
|
/**
|
|
67
100
|
* Restore a workflow to a prior version by creating a new snapshot.
|
|
68
101
|
*
|
|
@@ -12,11 +12,12 @@ import {
|
|
|
12
12
|
InternalServerErrorException,
|
|
13
13
|
} from '@nestjs/common';
|
|
14
14
|
import { HookEventKey, OnEvent } from '@nestjs/event-emitter';
|
|
15
|
-
import { FindOneOptions } from 'typeorm';
|
|
15
|
+
import { EntityManager, FindOneOptions } from 'typeorm';
|
|
16
16
|
import { z, ZodType } from 'zod';
|
|
17
17
|
|
|
18
18
|
import { UpdateOneOptions } from '@/utils/generics/base-orm.repository';
|
|
19
19
|
import { BaseOrmService } from '@/utils/generics/base-orm.service';
|
|
20
|
+
import { InferCreateDto } from '@/utils/types/dto.types';
|
|
20
21
|
import {
|
|
21
22
|
Room,
|
|
22
23
|
SocketGet,
|
|
@@ -68,6 +69,25 @@ export class WorkflowService extends BaseOrmService<WorkflowOrmEntity> {
|
|
|
68
69
|
return latest ?? null;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Persist a workflow using the provided transaction manager.
|
|
74
|
+
*
|
|
75
|
+
* This method intentionally does not emit BaseOrmRepository mutation events;
|
|
76
|
+
* callers that own a larger transaction must emit app-level events after
|
|
77
|
+
* commit.
|
|
78
|
+
*/
|
|
79
|
+
async createWithManager(
|
|
80
|
+
manager: EntityManager,
|
|
81
|
+
payload: InferCreateDto<WorkflowOrmEntity>,
|
|
82
|
+
): Promise<WorkflowOrmEntity> {
|
|
83
|
+
const entity = manager.create(
|
|
84
|
+
WorkflowOrmEntity,
|
|
85
|
+
this.repository.actionDtoToEntity(payload),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return await manager.save(WorkflowOrmEntity, entity);
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
/**
|
|
72
92
|
* Update a workflow while enforcing immutability of its trigger type.
|
|
73
93
|
*/
|