@pikku/inspector 0.12.33 → 0.12.35
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/CHANGELOG.md +31 -0
- package/dist/add/add-http-route.js +4 -1
- package/dist/add/add-rpc-invocations.js +7 -0
- package/dist/add/add-workflow.js +35 -4
- package/dist/inspector.js +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/utils/filter-inspector-state.js +43 -0
- package/dist/utils/load-addon-functions-meta.js +20 -14
- package/dist/utils/post-process.js +38 -13
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +9 -4
- package/dist/utils/workflow/dsl/patterns.d.ts +12 -1
- package/dist/utils/workflow/dsl/patterns.js +37 -2
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +8 -0
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +8 -0
- package/package.json +2 -2
- package/run-tests.sh +4 -4
- package/src/add/add-http-route.ts +4 -1
- package/src/add/add-rpc-invocations.test.ts +113 -0
- package/src/add/add-rpc-invocations.ts +8 -0
- package/src/add/add-workflow.ts +42 -4
- package/src/inspector.ts +1 -0
- package/src/types.ts +1 -0
- package/src/utils/filter-inspector-state.test.ts +231 -0
- package/src/utils/filter-inspector-state.ts +56 -0
- package/src/utils/load-addon-functions-meta.ts +26 -16
- package/src/utils/post-process.test.ts +254 -0
- package/src/utils/post-process.ts +41 -14
- package/src/utils/serialize-inspector-state.ts +11 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +12 -3
- package/src/utils/workflow/dsl/patterns.ts +48 -2
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +8 -0
- package/src/utils/workflow/graph/workflow-graph.types.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { strict as assert } from 'assert'
|
|
2
|
+
import { describe, test } from 'node:test'
|
|
3
|
+
import { aggregateRequiredServices } from './post-process.js'
|
|
4
|
+
import type { InspectorState } from '../types.js'
|
|
5
|
+
|
|
6
|
+
function makeState(
|
|
7
|
+
overrides: {
|
|
8
|
+
usedFunctions?: string[]
|
|
9
|
+
functionsMeta?: Record<string, any>
|
|
10
|
+
addonFunctions?: Record<string, Record<string, any>>
|
|
11
|
+
addonRequiredParentServices?: string[]
|
|
12
|
+
} = {}
|
|
13
|
+
): Omit<InspectorState, 'typesLookup'> {
|
|
14
|
+
return {
|
|
15
|
+
serviceAggregation: {
|
|
16
|
+
requiredServices: new Set<string>(),
|
|
17
|
+
usedFunctions: new Set(overrides.usedFunctions ?? []),
|
|
18
|
+
usedMiddleware: new Set<string>(),
|
|
19
|
+
usedPermissions: new Set<string>(),
|
|
20
|
+
allSingletonServices: [],
|
|
21
|
+
allWireServices: [],
|
|
22
|
+
},
|
|
23
|
+
functions: { meta: overrides.functionsMeta ?? {} },
|
|
24
|
+
middleware: { definitions: {}, tagMiddleware: new Map() },
|
|
25
|
+
permissions: { definitions: {}, tagPermissions: new Map() },
|
|
26
|
+
http: {
|
|
27
|
+
meta: {
|
|
28
|
+
get: {},
|
|
29
|
+
post: {},
|
|
30
|
+
put: {},
|
|
31
|
+
patch: {},
|
|
32
|
+
delete: {},
|
|
33
|
+
head: {},
|
|
34
|
+
options: {},
|
|
35
|
+
},
|
|
36
|
+
routeMiddleware: new Map(),
|
|
37
|
+
routePermissions: new Map(),
|
|
38
|
+
},
|
|
39
|
+
channels: { meta: {} },
|
|
40
|
+
queueWorkers: { meta: {} },
|
|
41
|
+
scheduledTasks: { meta: {} },
|
|
42
|
+
mcpEndpoints: { toolsMeta: {}, promptsMeta: {}, resourcesMeta: {} },
|
|
43
|
+
agents: { agentsMeta: {} },
|
|
44
|
+
workflows: { meta: {}, graphMeta: {} },
|
|
45
|
+
wireServicesMeta: new Map(),
|
|
46
|
+
rpc: { internalMeta: {}, exposedMeta: {} },
|
|
47
|
+
addonFunctions: overrides.addonFunctions ?? {},
|
|
48
|
+
addonRequiredParentServices: overrides.addonRequiredParentServices ?? [],
|
|
49
|
+
} as unknown as Omit<InspectorState, 'typesLookup'>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CONSOLE_PARENT_SERVICES = [
|
|
53
|
+
'metaService',
|
|
54
|
+
'aiAgentRunner',
|
|
55
|
+
'deploymentService',
|
|
56
|
+
'credentialService',
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
describe('aggregateRequiredServices — per-function addon services', () => {
|
|
60
|
+
test('a used addon function adds only its own parent services', () => {
|
|
61
|
+
const state = makeState({
|
|
62
|
+
usedFunctions: ['console:getSchema'],
|
|
63
|
+
addonFunctions: {
|
|
64
|
+
console: {
|
|
65
|
+
getSchema: {
|
|
66
|
+
services: { optimized: true, services: ['metaService'] },
|
|
67
|
+
},
|
|
68
|
+
runAgent: {
|
|
69
|
+
services: {
|
|
70
|
+
optimized: true,
|
|
71
|
+
services: ['aiAgentRunner', 'deploymentService'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
77
|
+
})
|
|
78
|
+
aggregateRequiredServices(state)
|
|
79
|
+
const required = state.serviceAggregation.requiredServices
|
|
80
|
+
assert.ok(required.has('metaService'))
|
|
81
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
82
|
+
assert.ok(!required.has('deploymentService'))
|
|
83
|
+
assert.ok(!required.has('credentialService'))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('an addon-created service falls back to the full parent blanket', () => {
|
|
87
|
+
const state = makeState({
|
|
88
|
+
usedFunctions: ['console:editCode'],
|
|
89
|
+
addonFunctions: {
|
|
90
|
+
console: {
|
|
91
|
+
editCode: {
|
|
92
|
+
services: {
|
|
93
|
+
optimized: true,
|
|
94
|
+
services: ['codeEditService', 'metaService'],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
100
|
+
})
|
|
101
|
+
aggregateRequiredServices(state)
|
|
102
|
+
const required = state.serviceAggregation.requiredServices
|
|
103
|
+
for (const service of CONSOLE_PARENT_SERVICES) {
|
|
104
|
+
assert.ok(required.has(service), `expected blanket to add ${service}`)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('missing services meta (old addon build) falls back to the blanket', () => {
|
|
109
|
+
const state = makeState({
|
|
110
|
+
usedFunctions: ['console:getSchema'],
|
|
111
|
+
addonFunctions: { console: { getSchema: {} } },
|
|
112
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
113
|
+
})
|
|
114
|
+
aggregateRequiredServices(state)
|
|
115
|
+
const required = state.serviceAggregation.requiredServices
|
|
116
|
+
for (const service of CONSOLE_PARENT_SERVICES) {
|
|
117
|
+
assert.ok(required.has(service), `expected blanket to add ${service}`)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('a bare project function colliding with an addon function name adds nothing', () => {
|
|
122
|
+
const state = makeState({
|
|
123
|
+
usedFunctions: ['getAgentThreads'],
|
|
124
|
+
functionsMeta: {
|
|
125
|
+
getAgentThreads: {
|
|
126
|
+
services: { optimized: true, services: ['kysely'] },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
addonFunctions: {
|
|
130
|
+
console: {
|
|
131
|
+
getAgentThreads: {
|
|
132
|
+
services: { optimized: true, services: ['metaService'] },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
137
|
+
})
|
|
138
|
+
aggregateRequiredServices(state)
|
|
139
|
+
const required = state.serviceAggregation.requiredServices
|
|
140
|
+
assert.ok(required.has('kysely'))
|
|
141
|
+
assert.ok(!required.has('metaService'))
|
|
142
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('no used addon functions adds no parent services', () => {
|
|
146
|
+
const state = makeState({
|
|
147
|
+
usedFunctions: ['listTasks'],
|
|
148
|
+
functionsMeta: {
|
|
149
|
+
listTasks: { services: { optimized: true, services: ['kysely'] } },
|
|
150
|
+
},
|
|
151
|
+
addonFunctions: {
|
|
152
|
+
console: {
|
|
153
|
+
getSchema: {
|
|
154
|
+
services: { optimized: true, services: ['metaService'] },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
159
|
+
})
|
|
160
|
+
aggregateRequiredServices(state)
|
|
161
|
+
const required = state.serviceAggregation.requiredServices
|
|
162
|
+
assert.ok(!required.has('metaService'))
|
|
163
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('internal services in addon function meta never force the blanket', () => {
|
|
167
|
+
const state = makeState({
|
|
168
|
+
usedFunctions: ['console:getSchema'],
|
|
169
|
+
addonFunctions: {
|
|
170
|
+
console: {
|
|
171
|
+
getSchema: {
|
|
172
|
+
services: {
|
|
173
|
+
optimized: true,
|
|
174
|
+
services: ['metaService', 'rpc', 'channel'],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
180
|
+
})
|
|
181
|
+
aggregateRequiredServices(state)
|
|
182
|
+
const required = state.serviceAggregation.requiredServices
|
|
183
|
+
assert.ok(required.has('metaService'))
|
|
184
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
185
|
+
assert.ok(!required.has('rpc'))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('default framework services in addon function meta never force the blanket', () => {
|
|
189
|
+
const state = makeState({
|
|
190
|
+
usedFunctions: ['ext:goodbye'],
|
|
191
|
+
addonFunctions: {
|
|
192
|
+
ext: {
|
|
193
|
+
goodbye: {
|
|
194
|
+
services: { optimized: true, services: ['logger', 'config'] },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
addonRequiredParentServices: ['greetingStore', 'auditSink'],
|
|
199
|
+
})
|
|
200
|
+
aggregateRequiredServices(state)
|
|
201
|
+
const required = state.serviceAggregation.requiredServices
|
|
202
|
+
assert.ok(!required.has('greetingStore'))
|
|
203
|
+
assert.ok(!required.has('auditSink'))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('a ref()-wired route (inline id + namespaced target) aggregates the target services', () => {
|
|
207
|
+
const state = makeState({
|
|
208
|
+
usedFunctions: [
|
|
209
|
+
'http:get:/function-tests/stream',
|
|
210
|
+
'console:streamFunctionTests',
|
|
211
|
+
],
|
|
212
|
+
functionsMeta: {
|
|
213
|
+
'http:get:/function-tests/stream': {
|
|
214
|
+
services: { optimized: false, services: [] },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
addonFunctions: {
|
|
218
|
+
console: {
|
|
219
|
+
streamFunctionTests: {
|
|
220
|
+
services: { optimized: true, services: ['metaService'] },
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
225
|
+
})
|
|
226
|
+
aggregateRequiredServices(state)
|
|
227
|
+
const required = state.serviceAggregation.requiredServices
|
|
228
|
+
assert.ok(required.has('metaService'))
|
|
229
|
+
assert.ok(!required.has('aiAgentRunner'))
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('multiple used addon functions union their parent services', () => {
|
|
233
|
+
const state = makeState({
|
|
234
|
+
usedFunctions: ['console:getSchema', 'console:runAgent'],
|
|
235
|
+
addonFunctions: {
|
|
236
|
+
console: {
|
|
237
|
+
getSchema: {
|
|
238
|
+
services: { optimized: true, services: ['metaService'] },
|
|
239
|
+
},
|
|
240
|
+
runAgent: {
|
|
241
|
+
services: { optimized: true, services: ['aiAgentRunner'] },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
addonRequiredParentServices: CONSOLE_PARENT_SERVICES,
|
|
246
|
+
})
|
|
247
|
+
aggregateRequiredServices(state)
|
|
248
|
+
const required = state.serviceAggregation.requiredServices
|
|
249
|
+
assert.ok(required.has('metaService'))
|
|
250
|
+
assert.ok(required.has('aiAgentRunner'))
|
|
251
|
+
assert.ok(!required.has('deploymentService'))
|
|
252
|
+
assert.ok(!required.has('credentialService'))
|
|
253
|
+
})
|
|
254
|
+
})
|
|
@@ -325,20 +325,47 @@ export function aggregateRequiredServices(
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
// 7. Services that consumed addons need from the parent project.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
328
|
+
const addonFnServices = new Map<string, string[] | undefined>()
|
|
329
|
+
for (const [namespace, fns] of Object.entries(state.addonFunctions ?? {})) {
|
|
330
|
+
for (const [id, meta] of Object.entries(fns)) {
|
|
331
|
+
addonFnServices.set(
|
|
332
|
+
`${namespace}:${id}`,
|
|
333
|
+
(meta as { services?: FunctionServicesMeta })?.services?.services
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const parentDeclared = state.addonRequiredParentServices ?? []
|
|
338
|
+
const parentDeclaredSet = new Set(parentDeclared)
|
|
339
|
+
const defaultServices = new Set([
|
|
340
|
+
'config',
|
|
341
|
+
'logger',
|
|
342
|
+
'variables',
|
|
343
|
+
'schema',
|
|
344
|
+
'secrets',
|
|
345
|
+
])
|
|
346
|
+
let usesAddonFn = false
|
|
347
|
+
let addonFactoryNeeded = false
|
|
348
|
+
for (const funcId of usedFunctions) {
|
|
349
|
+
if (!addonFnServices.has(funcId)) continue
|
|
350
|
+
usesAddonFn = true
|
|
351
|
+
const services = addonFnServices.get(funcId)
|
|
352
|
+
if (!services) {
|
|
353
|
+
addonFactoryNeeded = true
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
for (const service of services) {
|
|
357
|
+
if (parentDeclaredSet.has(service)) {
|
|
358
|
+
requiredServices.add(service)
|
|
359
|
+
} else if (
|
|
360
|
+
!internalServices.has(service) &&
|
|
361
|
+
!defaultServices.has(service)
|
|
362
|
+
) {
|
|
363
|
+
addonFactoryNeeded = true
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (usesAddonFn && addonFactoryNeeded) {
|
|
368
|
+
for (const service of parentDeclared) {
|
|
342
369
|
requiredServices.add(service)
|
|
343
370
|
}
|
|
344
371
|
}
|
|
@@ -155,6 +155,7 @@ export interface SerializableInspectorState {
|
|
|
155
155
|
exposedMeta: InspectorState['rpc']['exposedMeta']
|
|
156
156
|
exposedFiles: Array<[string, { path: string; exportedName: string }]>
|
|
157
157
|
invokedFunctions: string[]
|
|
158
|
+
invokedFunctionsByFile?: Array<[string, string[]]>
|
|
158
159
|
usedAddons: string[]
|
|
159
160
|
wireAddonDeclarations: Array<
|
|
160
161
|
[
|
|
@@ -379,6 +380,11 @@ export function serializeInspectorState(
|
|
|
379
380
|
exposedMeta: state.rpc.exposedMeta,
|
|
380
381
|
exposedFiles: Array.from(state.rpc.exposedFiles.entries()),
|
|
381
382
|
invokedFunctions: Array.from(state.rpc.invokedFunctions),
|
|
383
|
+
invokedFunctionsByFile: Array.from(
|
|
384
|
+
(
|
|
385
|
+
state.rpc.invokedFunctionsByFile ?? new Map<string, Set<string>>()
|
|
386
|
+
).entries()
|
|
387
|
+
).map(([file, fns]): [string, string[]] => [file, Array.from(fns)]),
|
|
382
388
|
usedAddons: Array.from(state.rpc.usedAddons),
|
|
383
389
|
wireAddonDeclarations: Array.from(
|
|
384
390
|
state.rpc.wireAddonDeclarations.entries()
|
|
@@ -565,6 +571,11 @@ export function deserializeInspectorState(
|
|
|
565
571
|
exposedMeta: data.rpc.exposedMeta,
|
|
566
572
|
exposedFiles: new Map(data.rpc.exposedFiles),
|
|
567
573
|
invokedFunctions: new Set(data.rpc.invokedFunctions),
|
|
574
|
+
invokedFunctionsByFile: new Map(
|
|
575
|
+
(data.rpc.invokedFunctionsByFile || []).map(
|
|
576
|
+
([file, fns]): [string, Set<string>] => [file, new Set(fns)]
|
|
577
|
+
)
|
|
578
|
+
),
|
|
568
579
|
usedAddons: new Set(data.rpc.usedAddons || []),
|
|
569
580
|
wireAddonDeclarations: new Map(data.rpc.wireAddonDeclarations || []),
|
|
570
581
|
wireAddonFiles: new Set(data.rpc.wireAddonFiles || []),
|
|
@@ -21,6 +21,8 @@ import type {
|
|
|
21
21
|
} from '@pikku/core/workflow'
|
|
22
22
|
import {
|
|
23
23
|
isWorkflowDoCall,
|
|
24
|
+
isWorkflowExpectEventuallyCall,
|
|
25
|
+
extractActorFromOptions,
|
|
24
26
|
isWorkflowSleepCall,
|
|
25
27
|
isWorkflowSuspendCall,
|
|
26
28
|
isThrowCancelException,
|
|
@@ -553,10 +555,15 @@ function extractRpcStep(
|
|
|
553
555
|
const inputs =
|
|
554
556
|
args.length >= 3 ? extractInputSources(args[2], context) : undefined
|
|
555
557
|
|
|
556
|
-
//
|
|
558
|
+
// do(step, rpc, data, options?) vs expectEventually(step, rpc, data, predicate, options?)
|
|
559
|
+
const expectEventually = isWorkflowExpectEventuallyCall(call)
|
|
560
|
+
const optionsIndex = expectEventually ? 4 : 3
|
|
561
|
+
const optionsArg =
|
|
562
|
+
args.length > optionsIndex ? args[optionsIndex] : undefined
|
|
563
|
+
|
|
557
564
|
const options =
|
|
558
|
-
|
|
559
|
-
? extractStepOptions(
|
|
565
|
+
optionsArg && ts.isObjectLiteralExpression(optionsArg)
|
|
566
|
+
? extractStepOptions(optionsArg, context)
|
|
560
567
|
: undefined
|
|
561
568
|
|
|
562
569
|
return {
|
|
@@ -566,6 +573,8 @@ function extractRpcStep(
|
|
|
566
573
|
outputVar,
|
|
567
574
|
inputs,
|
|
568
575
|
options,
|
|
576
|
+
actor: extractActorFromOptions(optionsArg),
|
|
577
|
+
expectEventually: expectEventually || undefined,
|
|
569
578
|
}
|
|
570
579
|
} catch (error) {
|
|
571
580
|
context.errors.push({
|
|
@@ -5,7 +5,8 @@ import * as ts from 'typescript'
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Check if a call expression is workflow.do()
|
|
8
|
+
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
9
|
+
* (both are RPC steps; expectEventually is the polling variant used by user flows)
|
|
9
10
|
*/
|
|
10
11
|
export function isWorkflowDoCall(
|
|
11
12
|
node: ts.CallExpression,
|
|
@@ -17,12 +18,57 @@ export function isWorkflowDoCall(
|
|
|
17
18
|
|
|
18
19
|
const propAccess = node.expression
|
|
19
20
|
return (
|
|
20
|
-
propAccess.name.text === 'do'
|
|
21
|
+
(propAccess.name.text === 'do' ||
|
|
22
|
+
propAccess.name.text === 'expectEventually') &&
|
|
21
23
|
ts.isIdentifier(propAccess.expression) &&
|
|
22
24
|
propAccess.expression.text === 'workflow'
|
|
23
25
|
)
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Check if a call expression is workflow.expectEventually()
|
|
30
|
+
*/
|
|
31
|
+
export function isWorkflowExpectEventuallyCall(
|
|
32
|
+
node: ts.CallExpression
|
|
33
|
+
): boolean {
|
|
34
|
+
return (
|
|
35
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
36
|
+
node.expression.name.text === 'expectEventually' &&
|
|
37
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
38
|
+
node.expression.expression.text === 'workflow'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract the actor NAME from a step options object literal:
|
|
44
|
+
* `{ actor: actors.x }` → 'x' (also `actors['x']`). Undefined when absent
|
|
45
|
+
* or not statically resolvable.
|
|
46
|
+
*/
|
|
47
|
+
export function extractActorFromOptions(
|
|
48
|
+
optionsArg: ts.Expression | undefined
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg)) return
|
|
51
|
+
for (const prop of optionsArg.properties) {
|
|
52
|
+
if (
|
|
53
|
+
ts.isPropertyAssignment(prop) &&
|
|
54
|
+
ts.isIdentifier(prop.name) &&
|
|
55
|
+
prop.name.text === 'actor'
|
|
56
|
+
) {
|
|
57
|
+
const init = prop.initializer
|
|
58
|
+
if (ts.isPropertyAccessExpression(init)) {
|
|
59
|
+
return init.name.text
|
|
60
|
+
}
|
|
61
|
+
if (
|
|
62
|
+
ts.isElementAccessExpression(init) &&
|
|
63
|
+
ts.isStringLiteral(init.argumentExpression)
|
|
64
|
+
) {
|
|
65
|
+
return init.argumentExpression.text
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
26
72
|
/**
|
|
27
73
|
* Check if a call expression is workflow.sleep()
|
|
28
74
|
*/
|
|
@@ -93,6 +93,12 @@ function convertStepToNode(
|
|
|
93
93
|
rpcName: step.rpcName,
|
|
94
94
|
next: nextNodeId,
|
|
95
95
|
}
|
|
96
|
+
if (step.actor) {
|
|
97
|
+
node.actor = step.actor
|
|
98
|
+
}
|
|
99
|
+
if (step.expectEventually) {
|
|
100
|
+
node.expectEventually = true
|
|
101
|
+
}
|
|
96
102
|
if (step.inputs) {
|
|
97
103
|
if (step.inputs === 'passthrough') {
|
|
98
104
|
// Entire data is passed through - store as reference to trigger
|
|
@@ -403,8 +409,10 @@ export function convertDslToGraph(
|
|
|
403
409
|
name: workflowName,
|
|
404
410
|
pikkuFuncId: meta.pikkuFuncId,
|
|
405
411
|
source,
|
|
412
|
+
title: meta.title,
|
|
406
413
|
description: meta.description,
|
|
407
414
|
tags: meta.tags,
|
|
415
|
+
actors: meta.actors,
|
|
408
416
|
context: meta.context,
|
|
409
417
|
nodes: nodesRecord,
|
|
410
418
|
entryNodeIds,
|
|
@@ -142,6 +142,10 @@ export interface FunctionNode extends BaseNode {
|
|
|
142
142
|
outputVar?: string
|
|
143
143
|
/** Hash of nodeId + RPC input/output schemas for version detection */
|
|
144
144
|
stepHash?: string
|
|
145
|
+
/** User-flow actor name this step runs as (workflow.do {actor: actors.x}) */
|
|
146
|
+
actor?: string
|
|
147
|
+
/** True for workflow.expectEventually polling steps (user flows) */
|
|
148
|
+
expectEventually?: boolean
|
|
145
149
|
}
|
|
146
150
|
|
|
147
151
|
/**
|
|
@@ -191,10 +195,14 @@ export interface SerializedWorkflowGraph {
|
|
|
191
195
|
pikkuFuncId: string
|
|
192
196
|
/** Source type: 'dsl' for pikkuWorkflowFunc, 'graph' for pikkuWorkflowGraph */
|
|
193
197
|
source: WorkflowSourceType
|
|
198
|
+
/** Optional short display name */
|
|
199
|
+
title?: string
|
|
194
200
|
/** Optional description */
|
|
195
201
|
description?: string
|
|
196
202
|
/** Tags for organization */
|
|
197
203
|
tags?: string[]
|
|
204
|
+
/** Actor names a user flow's steps run as */
|
|
205
|
+
actors?: string[]
|
|
198
206
|
/** If true, workflow always executes inline without queues */
|
|
199
207
|
inline?: boolean
|
|
200
208
|
/** Workflow context/state variables (from Zod schema) */
|