@pikku/inspector 0.12.34 → 0.12.36
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 +56 -0
- package/dist/add/add-functions.js +2 -2
- package/dist/add/add-http-route.js +4 -1
- package/dist/add/add-rpc-invocations.js +7 -0
- package/dist/add/add-workflow.d.ts +1 -1
- package/dist/add/add-workflow.js +39 -11
- package/dist/error-codes.d.ts +2 -1
- package/dist/error-codes.js +2 -1
- 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/patterns.d.ts +1 -1
- package/dist/utils/workflow/dsl/patterns.js +1 -1
- package/dist/utils/workflow/graph/convert-dsl-to-graph.js +3 -3
- package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
- package/package.json +2 -2
- package/run-tests.sh +4 -4
- package/src/add/add-functions.ts +11 -5
- 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 +53 -14
- package/src/add/expect-eventually-scenario-only.test.ts +108 -0
- package/src/error-codes.ts +2 -1
- 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/patterns.ts +1 -1
- package/src/utils/workflow/graph/convert-dsl-to-graph.ts +3 -3
- package/src/utils/workflow/graph/workflow-graph.types.ts +5 -5
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
+
## 0.12.36
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 61c9ce9: Add `actor.converse(...)` — actor agents for user journeys (#850)
|
|
6
|
+
|
|
7
|
+
An actor can now hold a dynamic, LLM-driven conversation with a target Pikku AI
|
|
8
|
+
agent in its own persona:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const verdict = await actors.pm.converse({
|
|
12
|
+
agent: 'todoBot',
|
|
13
|
+
task: 'Get a todo created for the launch',
|
|
14
|
+
evaluate: 'A todo about the launch now exists',
|
|
15
|
+
})
|
|
16
|
+
// verdict: { passed, reasoning, transcript }
|
|
17
|
+
// then assert deterministically as the same actor:
|
|
18
|
+
const todos = await actors.pm.invoke('listTodos', {})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The actor drives the target over the real transport (the agent's own
|
|
22
|
+
`agentRun` / `agentApprove` HTTP routes, signed in as the actor), plays the
|
|
23
|
+
persona from its `pikku.config.json` config, answers the agent's tool-approval
|
|
24
|
+
requests in-persona (`approvals: 'in-persona' | 'always' | 'never'`), and
|
|
25
|
+
returns its verdict on whether the task was met. Deterministic checks stay the
|
|
26
|
+
caller's job — they already hold the actor.
|
|
27
|
+
|
|
28
|
+
The conversation engine is transport-agnostic (persona LLM + injected target
|
|
29
|
+
driver); the persona's own turns run in-process via the configured
|
|
30
|
+
`aiAgentRunner` (`model` from the call or the actors-service default).
|
|
31
|
+
|
|
32
|
+
`agent` is typed against the generated agent-name union (`keyof AgentMap`), so
|
|
33
|
+
it's author-time checked and autocompleted in a typed project.
|
|
34
|
+
|
|
35
|
+
- 472a349: Rename the userflow concept to scenario (#862). `pikkuUserFlow` becomes `pikkuScenario`, `pikku userflow run/list` becomes `pikku scenario run/list`, the workflow meta flag `userFlow` becomes `scenario`, actor types are now `ScenarioActor`/`ScenarioActors`/`ScenarioActorConfig` (`createHttpScenarioActors`), pikku.config.json's `userFlows` key becomes `scenarios`, the generated actors file is `pikku-scenario-actors.gen.ts` (`createScenarioActors`), the actor sign-in secret env var is `SCENARIO_ACTOR_SECRET`, and the console's User Flows view is now Scenarios.
|
|
36
|
+
- Updated dependencies [61c9ce9]
|
|
37
|
+
- Updated dependencies [f1f39f8]
|
|
38
|
+
- Updated dependencies [c45e98d]
|
|
39
|
+
- Updated dependencies [472a349]
|
|
40
|
+
- @pikku/core@0.12.52
|
|
41
|
+
|
|
42
|
+
## 0.12.35
|
|
43
|
+
|
|
44
|
+
### Patch Changes
|
|
45
|
+
|
|
46
|
+
- 7ebea62: Tree-shake addon registrations in filtered inspector states (per-unit deploy codegen).
|
|
47
|
+
- `filterInspectorState` drops an addon's `wireAddonDeclarations`/`usedAddons` unless something kept actually references it (kept wiring targeting `namespace:*`, kept agent/MCP tool, or a body-level `rpc.invoke('namespace:*')` from a file that still contains a kept function). The generated per-unit bootstrap no longer imports unused addon package bootstraps — previously every deploy unit registered every addon's entire function surface, which pulled dev-only code (e.g. `@pikku/addon-console`'s static `node:fs` imports) into Cloudflare Worker bundles and failed upload with `No such module "node:fs"`.
|
|
48
|
+
- Body-level `rpc.invoke()` targets are now tracked per source file (`rpc.invokedFunctionsByFile`) so wiring-level `ref()` targets no longer pin an addon into every unit.
|
|
49
|
+
- `aggregateRequiredServices` computes addon parent services per used addon function (from the addon's shipped per-function `services` meta) instead of blanket-adding `addonRequiredParentServices` — and matches namespaced ids only, so bare project function names colliding with addon function names no longer force the blanket.
|
|
50
|
+
- Addon builds keep per-function `services` in the shipped `pikku-functions-meta.gen.json` so parent projects can do the above; addons built before this fall back to the blanket.
|
|
51
|
+
- HTTP route meta records `refTarget` for `ref('namespace:fn')`-wired routes, so per-unit filtering keeps the addon registration (and only that function's services) when the route deploys.
|
|
52
|
+
|
|
53
|
+
- Updated dependencies [7ebea62]
|
|
54
|
+
- Updated dependencies [e57dd65]
|
|
55
|
+
- @pikku/core@0.12.51
|
|
56
|
+
|
|
1
57
|
## 0.12.34
|
|
2
58
|
|
|
3
59
|
### Patch Changes
|
|
@@ -251,10 +251,10 @@ export const addFunctions = (logger, node, checker, state, options) => {
|
|
|
251
251
|
return;
|
|
252
252
|
}
|
|
253
253
|
// Match identifiers that contain both "pikku" and "func" (case insensitive),
|
|
254
|
-
// plus
|
|
254
|
+
// plus pikkuScenario (a workflow wrapper without "func" in its name)
|
|
255
255
|
const pikkuFuncPattern = /pikku.*func/i;
|
|
256
256
|
if (!pikkuFuncPattern.test(expression.text) &&
|
|
257
|
-
expression.text !== '
|
|
257
|
+
expression.text !== 'pikkuScenario') {
|
|
258
258
|
return;
|
|
259
259
|
}
|
|
260
260
|
// only handle calls like pikkuFunc(...)
|
|
@@ -220,8 +220,10 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
|
|
|
220
220
|
const middleware = resolveHTTPMiddlewareFromObject(state, fullRoute, obj, tags, checker);
|
|
221
221
|
// Resolve permissions
|
|
222
222
|
const permissions = resolveHTTPPermissionsFromObject(state, fullRoute, obj, tags, checker);
|
|
223
|
-
// Track used functions/middleware/permissions for service aggregation
|
|
224
223
|
state.serviceAggregation.usedFunctions.add(funcName);
|
|
224
|
+
if (refAddonTarget) {
|
|
225
|
+
state.serviceAggregation.usedFunctions.add(refAddonTarget);
|
|
226
|
+
}
|
|
225
227
|
extractWireNames(middleware).forEach((name) => state.serviceAggregation.usedMiddleware.add(name));
|
|
226
228
|
extractWireNames(permissions).forEach((name) => state.serviceAggregation.usedPermissions.add(name));
|
|
227
229
|
// Check for SSE
|
|
@@ -232,6 +234,7 @@ export function registerHTTPRoute({ obj, state, checker, logger, sourceFile, bas
|
|
|
232
234
|
state.http.files.add(sourceFile.fileName);
|
|
233
235
|
state.http.meta[method][fullRoute] = {
|
|
234
236
|
pikkuFuncId: funcName,
|
|
237
|
+
...(refAddonTarget && { refTarget: refAddonTarget }),
|
|
235
238
|
...(packageName && { packageName }),
|
|
236
239
|
route: fullRoute,
|
|
237
240
|
sourceFile: sourceFile.fileName,
|
|
@@ -85,10 +85,17 @@ export function addRPCInvocations(node, state, logger) {
|
|
|
85
85
|
const functionRef = firstArg.text;
|
|
86
86
|
logger.debug(`• Found RPC invocation: ${functionRef}`);
|
|
87
87
|
state.rpc.invokedFunctions.add(functionRef);
|
|
88
|
+
let byFile = state.rpc.invokedFunctionsByFile.get(sourceFileName);
|
|
89
|
+
if (!byFile) {
|
|
90
|
+
byFile = new Set();
|
|
91
|
+
state.rpc.invokedFunctionsByFile.set(sourceFileName, byFile);
|
|
92
|
+
}
|
|
93
|
+
byFile.add(functionRef);
|
|
88
94
|
const namespace = extractNamespace(functionRef);
|
|
89
95
|
if (namespace) {
|
|
90
96
|
logger.debug(` → Addon detected: ${namespace}`);
|
|
91
97
|
state.rpc.usedAddons.add(namespace);
|
|
98
|
+
state.serviceAggregation.usedFunctions.add(functionRef);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
94
101
|
// Handle template literals like `function-${name}`
|
|
@@ -6,6 +6,6 @@ import type { WorkflowStepMeta } from '@pikku/core/workflow';
|
|
|
6
6
|
export declare function collectInvokedRPCs(steps: WorkflowStepMeta[], rpcs: Set<string>): void;
|
|
7
7
|
/**
|
|
8
8
|
* Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
|
|
9
|
-
*
|
|
9
|
+
* pikkuScenario() calls. Detects workflow registration and extracts metadata.
|
|
10
10
|
*/
|
|
11
11
|
export declare const addWorkflow: AddWiring;
|
package/dist/add/add-workflow.js
CHANGED
|
@@ -19,6 +19,25 @@ function extractStepName(node, checker) {
|
|
|
19
19
|
return getSourceText(node);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Walk a function body for any `X.expectEventually(...)` call. Used to enforce
|
|
24
|
+
* that the durable-polling assertion is a scenario-only primitive — regular
|
|
25
|
+
* workflows must not carry test/assertion semantics.
|
|
26
|
+
*/
|
|
27
|
+
function containsExpectEventuallyCall(node) {
|
|
28
|
+
if (ts.isCallExpression(node) &&
|
|
29
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
30
|
+
node.expression.name.text === 'expectEventually') {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
let found = false;
|
|
34
|
+
ts.forEachChild(node, (child) => {
|
|
35
|
+
if (!found && !ts.isFunctionDeclaration(child)) {
|
|
36
|
+
found = containsExpectEventuallyCall(child);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return found;
|
|
40
|
+
}
|
|
22
41
|
/**
|
|
23
42
|
* Recursively check if any step has inline type (non-serializable)
|
|
24
43
|
*/
|
|
@@ -177,7 +196,7 @@ function getWorkflowInvocations(node, checker, state, workflowName, steps) {
|
|
|
177
196
|
}
|
|
178
197
|
/**
|
|
179
198
|
* Inspector for pikkuWorkflowFunc(), pikkuWorkflowComplexFunc() and
|
|
180
|
-
*
|
|
199
|
+
* pikkuScenario() calls. Detects workflow registration and extracts metadata.
|
|
181
200
|
*/
|
|
182
201
|
export const addWorkflow = (logger, node, checker, state) => {
|
|
183
202
|
if (!ts.isCallExpression(node)) {
|
|
@@ -196,10 +215,10 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
196
215
|
else if (expression.text === 'pikkuWorkflowComplexFunc') {
|
|
197
216
|
wrapperType = 'complex';
|
|
198
217
|
}
|
|
199
|
-
else if (expression.text === '
|
|
200
|
-
// A
|
|
218
|
+
else if (expression.text === 'pikkuScenario') {
|
|
219
|
+
// A scenario is a complex workflow whose steps run as actors over the
|
|
201
220
|
// real transport — same extraction rules as complex, distinct meta.
|
|
202
|
-
wrapperType = '
|
|
221
|
+
wrapperType = 'scenario';
|
|
203
222
|
}
|
|
204
223
|
else {
|
|
205
224
|
return;
|
|
@@ -244,6 +263,15 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
244
263
|
logger.critical(ErrorCode.MISSING_FUNC, `Could not resolve workflow function for '${workflowName}'.`);
|
|
245
264
|
return;
|
|
246
265
|
}
|
|
266
|
+
// expectEventually is a scenario-only assertion primitive — regular
|
|
267
|
+
// workflows must stay free of test/eval semantics.
|
|
268
|
+
if (wrapperType !== 'scenario' &&
|
|
269
|
+
containsExpectEventuallyCall(resolvedFunc)) {
|
|
270
|
+
logger.critical(ErrorCode.EXPECT_EVENTUALLY_SCENARIO_ONLY, `Workflow '${workflowName}' calls workflow.expectEventually(), which is only ` +
|
|
271
|
+
`available in scenarios (pikkuScenario). Move it into a scenario, or drive ` +
|
|
272
|
+
`the assertion outside the workflow.`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
247
275
|
// Track workflow file for wiring generation
|
|
248
276
|
if (exportedName) {
|
|
249
277
|
state.workflows.files.set(pikkuFuncId, {
|
|
@@ -308,8 +336,8 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
308
336
|
const sink = steps.length > 0 ? [] : steps;
|
|
309
337
|
getWorkflowInvocations(resolvedFunc, checker, state, workflowName, sink);
|
|
310
338
|
}
|
|
311
|
-
// Actor names the flow's steps run as (
|
|
312
|
-
// personas view and the
|
|
339
|
+
// Actor names the flow's steps run as (scenarios) — powers the console
|
|
340
|
+
// personas view and the Scenario graph badges.
|
|
313
341
|
const actorNames = [
|
|
314
342
|
...new Set(steps
|
|
315
343
|
.map((s) => ('actor' in s ? s.actor : undefined))
|
|
@@ -327,19 +355,19 @@ export const addWorkflow = (logger, node, checker, state) => {
|
|
|
327
355
|
errors,
|
|
328
356
|
tags,
|
|
329
357
|
expose,
|
|
330
|
-
|
|
358
|
+
scenario: wrapperType === 'scenario' ? true : undefined,
|
|
331
359
|
actors: actorNames.length > 0 ? actorNames : undefined,
|
|
332
360
|
};
|
|
333
|
-
//
|
|
361
|
+
// Scenarios are pure stories of remote RPCs (same rule as client-side CLI
|
|
334
362
|
// renderers): the func may only destructure logger/config — everything else
|
|
335
363
|
// must go through actor steps so the flow runs against the TARGET
|
|
336
364
|
// environment, never local services.
|
|
337
365
|
const funcMeta = state.functions.meta[pikkuFuncId];
|
|
338
|
-
if (wrapperType === '
|
|
366
|
+
if (wrapperType === 'scenario' && funcMeta?.services) {
|
|
339
367
|
const disallowed = funcMeta.services.services.filter((svc) => svc !== 'logger' && svc !== 'config');
|
|
340
368
|
if (disallowed.length > 0) {
|
|
341
|
-
logger.critical(ErrorCode.
|
|
342
|
-
`
|
|
369
|
+
logger.critical(ErrorCode.SCENARIO_HAS_SERVICES, `Scenario '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
|
|
370
|
+
`Scenarios may only use 'logger'/'config' — drive everything else through ` +
|
|
343
371
|
`actor steps (workflow.do(step, rpc, data, { actor: actors.x })) so the flow ` +
|
|
344
372
|
`runs against the target environment.`);
|
|
345
373
|
return;
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -19,7 +19,8 @@ export declare enum ErrorCode {
|
|
|
19
19
|
MISSING_QUEUE_NAME = "PKU384",
|
|
20
20
|
MISSING_CHANNEL_NAME = "PKU400",
|
|
21
21
|
CLI_CLIENTSIDE_RENDERER_HAS_SERVICES = "PKU672",
|
|
22
|
-
|
|
22
|
+
SCENARIO_HAS_SERVICES = "PKU673",
|
|
23
|
+
EXPECT_EVENTUALLY_SCENARIO_ONLY = "PKU675",
|
|
23
24
|
DYNAMIC_STEP_NAME = "PKU529",
|
|
24
25
|
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
|
|
25
26
|
INVALID_DSL_WORKFLOW = "PKU641",
|
package/dist/error-codes.js
CHANGED
|
@@ -21,7 +21,8 @@ export var ErrorCode;
|
|
|
21
21
|
ErrorCode["MISSING_QUEUE_NAME"] = "PKU384";
|
|
22
22
|
ErrorCode["MISSING_CHANNEL_NAME"] = "PKU400";
|
|
23
23
|
ErrorCode["CLI_CLIENTSIDE_RENDERER_HAS_SERVICES"] = "PKU672";
|
|
24
|
-
ErrorCode["
|
|
24
|
+
ErrorCode["SCENARIO_HAS_SERVICES"] = "PKU673";
|
|
25
|
+
ErrorCode["EXPECT_EVENTUALLY_SCENARIO_ONLY"] = "PKU675";
|
|
25
26
|
ErrorCode["DYNAMIC_STEP_NAME"] = "PKU529";
|
|
26
27
|
ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
|
|
27
28
|
ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
|
package/dist/inspector.js
CHANGED
|
@@ -93,6 +93,7 @@ export function getInitialInspectorState(rootDir) {
|
|
|
93
93
|
exposedMeta: {},
|
|
94
94
|
exposedFiles: new Map(),
|
|
95
95
|
invokedFunctions: new Set(),
|
|
96
|
+
invokedFunctionsByFile: new Map(),
|
|
96
97
|
usedAddons: new Set(),
|
|
97
98
|
wireAddonDeclarations: new Map(),
|
|
98
99
|
wireAddonFiles: new Set(),
|
package/dist/types.d.ts
CHANGED
|
@@ -366,6 +366,9 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
366
366
|
// Track used functions/middleware/permissions
|
|
367
367
|
if (routeMeta.pikkuFuncId) {
|
|
368
368
|
filteredState.serviceAggregation.usedFunctions.add(routeMeta.pikkuFuncId);
|
|
369
|
+
if (routeMeta.refTarget) {
|
|
370
|
+
filteredState.serviceAggregation.usedFunctions.add(routeMeta.refTarget);
|
|
371
|
+
}
|
|
369
372
|
// For workflow/agent routes, also add the base name
|
|
370
373
|
// so the workflow/agent definition survives pruning
|
|
371
374
|
const colonIdx = routeMeta.pikkuFuncId.indexOf(':');
|
|
@@ -796,6 +799,46 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
796
799
|
if (filteredState.serviceAggregation.requiredServices.has('workflowService')) {
|
|
797
800
|
filteredState.serviceAggregation.requiredServices.add('queueService');
|
|
798
801
|
}
|
|
802
|
+
if ((filteredState.rpc.wireAddonDeclarations?.size ?? 0) > 0) {
|
|
803
|
+
const referencedIds = new Set(filteredState.serviceAggregation.usedFunctions);
|
|
804
|
+
const keptFiles = new Set();
|
|
805
|
+
for (const funcId of filteredState.serviceAggregation.usedFunctions) {
|
|
806
|
+
const file = filteredState.functions.files.get(funcId);
|
|
807
|
+
if (file?.path)
|
|
808
|
+
keptFiles.add(file.path);
|
|
809
|
+
}
|
|
810
|
+
for (const [file, invoked] of state.rpc.invokedFunctionsByFile ??
|
|
811
|
+
new Map()) {
|
|
812
|
+
if (!keptFiles.has(file))
|
|
813
|
+
continue;
|
|
814
|
+
for (const id of invoked) {
|
|
815
|
+
referencedIds.add(id);
|
|
816
|
+
if (id.includes(':')) {
|
|
817
|
+
filteredState.serviceAggregation.usedFunctions.add(id);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const toolMeta of Object.values(filteredState.mcpEndpoints?.toolsMeta ?? {})) {
|
|
822
|
+
if (toolMeta.pikkuFuncId)
|
|
823
|
+
referencedIds.add(toolMeta.pikkuFuncId);
|
|
824
|
+
}
|
|
825
|
+
for (const agentMeta of Object.values(filteredState.agents?.agentsMeta ?? {})) {
|
|
826
|
+
for (const tool of agentMeta.tools ?? [])
|
|
827
|
+
referencedIds.add(tool);
|
|
828
|
+
}
|
|
829
|
+
const keptNamespaces = new Set();
|
|
830
|
+
for (const namespace of filteredState.rpc.wireAddonDeclarations.keys()) {
|
|
831
|
+
const prefix = `${namespace}:`;
|
|
832
|
+
for (const id of referencedIds) {
|
|
833
|
+
if (id.startsWith(prefix)) {
|
|
834
|
+
keptNamespaces.add(namespace);
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
filteredState.rpc.wireAddonDeclarations = new Map([...filteredState.rpc.wireAddonDeclarations].filter(([namespace]) => keptNamespaces.has(namespace)));
|
|
840
|
+
filteredState.rpc.usedAddons = new Set([...filteredState.rpc.usedAddons].filter((namespace) => keptNamespaces.has(namespace)));
|
|
841
|
+
}
|
|
799
842
|
// Recalculate requiredServices based on filtered functions/middleware/permissions
|
|
800
843
|
// Need to cast to InspectorState temporarily for aggregateRequiredServices
|
|
801
844
|
const stateForAggregation = filteredState;
|
|
@@ -135,6 +135,7 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
135
135
|
// No variables meta — that's fine
|
|
136
136
|
}
|
|
137
137
|
// Load addon serverlessIncompatible service names from pikku-addon-meta.gen.json
|
|
138
|
+
let loadedParentServices = false;
|
|
138
139
|
try {
|
|
139
140
|
const addonMetaPath = require.resolve(`${decl.package}/.pikku/console/pikku-addon-meta.gen.json`);
|
|
140
141
|
const addonMetaRaw = await readFile(addonMetaPath, 'utf-8');
|
|
@@ -144,24 +145,29 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
144
145
|
state.addonServerlessIncompatible.set(namespace, addonMeta.serverlessIncompatible);
|
|
145
146
|
logger.debug(`Addon '${namespace}' marks [${addonMeta.serverlessIncompatible.join(', ')}] as serverless-incompatible`);
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
// Load addon required parent services from pikku-services.gen
|
|
152
|
-
try {
|
|
153
|
-
const servicesGenPath = require.resolve(`${decl.package}/.pikku/pikku-services.gen.js`);
|
|
154
|
-
const servicesModule = await import(servicesGenPath);
|
|
155
|
-
if (servicesModule.requiredParentServices &&
|
|
156
|
-
Array.isArray(servicesModule.requiredParentServices)) {
|
|
157
|
-
for (const service of servicesModule.requiredParentServices) {
|
|
148
|
+
if (Array.isArray(addonMeta.requiredParentServices) &&
|
|
149
|
+
addonMeta.requiredParentServices.length > 0) {
|
|
150
|
+
for (const service of addonMeta.requiredParentServices) {
|
|
158
151
|
state.addonRequiredParentServices.push(service);
|
|
159
152
|
}
|
|
160
|
-
|
|
153
|
+
loadedParentServices = true;
|
|
154
|
+
logger.debug(`Loaded ${addonMeta.requiredParentServices.length} required parent services for '${namespace}' from addon meta`);
|
|
161
155
|
}
|
|
162
156
|
}
|
|
163
|
-
catch {
|
|
164
|
-
|
|
157
|
+
catch { }
|
|
158
|
+
if (!loadedParentServices) {
|
|
159
|
+
try {
|
|
160
|
+
const servicesGenPath = require.resolve(`${decl.package}/.pikku/pikku-services.gen.js`);
|
|
161
|
+
const servicesModule = await import(servicesGenPath);
|
|
162
|
+
if (servicesModule.requiredParentServices &&
|
|
163
|
+
Array.isArray(servicesModule.requiredParentServices)) {
|
|
164
|
+
for (const service of servicesModule.requiredParentServices) {
|
|
165
|
+
state.addonRequiredParentServices.push(service);
|
|
166
|
+
}
|
|
167
|
+
logger.debug(`Loaded ${servicesModule.requiredParentServices.length} required parent services for '${namespace}' from ${decl.package}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
165
171
|
}
|
|
166
172
|
try {
|
|
167
173
|
const httpContractsPath = require.resolve(`${decl.package}/.pikku/http/pikku-http-contracts-meta.gen.json`);
|
|
@@ -253,19 +253,44 @@ export function aggregateRequiredServices(state) {
|
|
|
253
253
|
requiredServices.add('eventHub');
|
|
254
254
|
}
|
|
255
255
|
// 7. Services that consumed addons need from the parent project.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
const addonFnServices = new Map();
|
|
257
|
+
for (const [namespace, fns] of Object.entries(state.addonFunctions ?? {})) {
|
|
258
|
+
for (const [id, meta] of Object.entries(fns)) {
|
|
259
|
+
addonFnServices.set(`${namespace}:${id}`, meta?.services?.services);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const parentDeclared = state.addonRequiredParentServices ?? [];
|
|
263
|
+
const parentDeclaredSet = new Set(parentDeclared);
|
|
264
|
+
const defaultServices = new Set([
|
|
265
|
+
'config',
|
|
266
|
+
'logger',
|
|
267
|
+
'variables',
|
|
268
|
+
'schema',
|
|
269
|
+
'secrets',
|
|
270
|
+
]);
|
|
271
|
+
let usesAddonFn = false;
|
|
272
|
+
let addonFactoryNeeded = false;
|
|
273
|
+
for (const funcId of usedFunctions) {
|
|
274
|
+
if (!addonFnServices.has(funcId))
|
|
275
|
+
continue;
|
|
276
|
+
usesAddonFn = true;
|
|
277
|
+
const services = addonFnServices.get(funcId);
|
|
278
|
+
if (!services) {
|
|
279
|
+
addonFactoryNeeded = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
for (const service of services) {
|
|
283
|
+
if (parentDeclaredSet.has(service)) {
|
|
284
|
+
requiredServices.add(service);
|
|
285
|
+
}
|
|
286
|
+
else if (!internalServices.has(service) &&
|
|
287
|
+
!defaultServices.has(service)) {
|
|
288
|
+
addonFactoryNeeded = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (usesAddonFn && addonFactoryNeeded) {
|
|
293
|
+
for (const service of parentDeclared) {
|
|
269
294
|
requiredServices.add(service);
|
|
270
295
|
}
|
|
271
296
|
}
|
|
@@ -78,6 +78,7 @@ export function serializeInspectorState(state) {
|
|
|
78
78
|
exposedMeta: state.rpc.exposedMeta,
|
|
79
79
|
exposedFiles: Array.from(state.rpc.exposedFiles.entries()),
|
|
80
80
|
invokedFunctions: Array.from(state.rpc.invokedFunctions),
|
|
81
|
+
invokedFunctionsByFile: Array.from((state.rpc.invokedFunctionsByFile ?? new Map()).entries()).map(([file, fns]) => [file, Array.from(fns)]),
|
|
81
82
|
usedAddons: Array.from(state.rpc.usedAddons),
|
|
82
83
|
wireAddonDeclarations: Array.from(state.rpc.wireAddonDeclarations.entries()),
|
|
83
84
|
wireAddonFiles: Array.from(state.rpc.wireAddonFiles),
|
|
@@ -235,6 +236,7 @@ export function deserializeInspectorState(data) {
|
|
|
235
236
|
exposedMeta: data.rpc.exposedMeta,
|
|
236
237
|
exposedFiles: new Map(data.rpc.exposedFiles),
|
|
237
238
|
invokedFunctions: new Set(data.rpc.invokedFunctions),
|
|
239
|
+
invokedFunctionsByFile: new Map((data.rpc.invokedFunctionsByFile || []).map(([file, fns]) => [file, new Set(fns)])),
|
|
238
240
|
usedAddons: new Set(data.rpc.usedAddons || []),
|
|
239
241
|
wireAddonDeclarations: new Map(data.rpc.wireAddonDeclarations || []),
|
|
240
242
|
wireAddonFiles: new Set(data.rpc.wireAddonFiles || []),
|
|
@@ -4,7 +4,7 @@ import * as ts from 'typescript';
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
|
-
* (both are RPC steps; expectEventually is the polling variant used by
|
|
7
|
+
* (both are RPC steps; expectEventually is the polling variant used by scenarios)
|
|
8
8
|
*/
|
|
9
9
|
export declare function isWorkflowDoCall(node: ts.CallExpression, checker: ts.TypeChecker): boolean;
|
|
10
10
|
/**
|
|
@@ -4,7 +4,7 @@ import * as ts from 'typescript';
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Check if a call expression is workflow.do() or workflow.expectEventually()
|
|
7
|
-
* (both are RPC steps; expectEventually is the polling variant used by
|
|
7
|
+
* (both are RPC steps; expectEventually is the polling variant used by scenarios)
|
|
8
8
|
*/
|
|
9
9
|
export function isWorkflowDoCall(node, checker) {
|
|
10
10
|
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
@@ -298,11 +298,11 @@ export function convertDslToGraph(workflowName, meta) {
|
|
|
298
298
|
}
|
|
299
299
|
const entryNodeIds = nodes.length > 0 ? [nodes[0].nodeId] : [];
|
|
300
300
|
// Determine source type:
|
|
301
|
-
// -
|
|
301
|
+
// - scenario: complex workflow whose steps run as actors (scenario)
|
|
302
302
|
// - dsl === true: pure DSL workflow, can be serialized
|
|
303
303
|
// - dsl === false: complex workflow with inline steps, not serializable
|
|
304
|
-
const source = meta.
|
|
305
|
-
? '
|
|
304
|
+
const source = meta.scenario
|
|
305
|
+
? 'scenario'
|
|
306
306
|
: meta.dsl === false
|
|
307
307
|
? 'complex'
|
|
308
308
|
: 'dsl';
|
|
@@ -96,9 +96,9 @@ export interface FunctionNode extends BaseNode {
|
|
|
96
96
|
outputVar?: string;
|
|
97
97
|
/** Hash of nodeId + RPC input/output schemas for version detection */
|
|
98
98
|
stepHash?: string;
|
|
99
|
-
/**
|
|
99
|
+
/** Scenario actor name this step runs as (workflow.do {actor: actors.x}) */
|
|
100
100
|
actor?: string;
|
|
101
|
-
/** True for workflow.expectEventually polling steps (
|
|
101
|
+
/** True for workflow.expectEventually polling steps (scenarios) */
|
|
102
102
|
expectEventually?: boolean;
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
@@ -127,9 +127,9 @@ export declare const isFlowNode: (node: SerializedGraphNode) => node is FlowNode
|
|
|
127
127
|
* - 'dsl': Pure DSL workflow (pikkuWorkflowFunc) - can be round-tripped to code
|
|
128
128
|
* - 'complex': Complex workflow (pikkuWorkflowComplexFunc) - contains inline steps, not serializable
|
|
129
129
|
* - 'graph': Graph-based workflow (pikkuWorkflowGraph)
|
|
130
|
-
* - '
|
|
130
|
+
* - 'scenario': Scenario (pikkuScenario) - complex workflow whose steps run as actors
|
|
131
131
|
*/
|
|
132
|
-
export type WorkflowSourceType = 'dsl' | 'complex' | 'graph' | '
|
|
132
|
+
export type WorkflowSourceType = 'dsl' | 'complex' | 'graph' | 'scenario';
|
|
133
133
|
/**
|
|
134
134
|
* Serialized workflow graph - the canonical JSON format
|
|
135
135
|
*/
|
|
@@ -146,7 +146,7 @@ export interface SerializedWorkflowGraph {
|
|
|
146
146
|
description?: string;
|
|
147
147
|
/** Tags for organization */
|
|
148
148
|
tags?: string[];
|
|
149
|
-
/** Actor names a
|
|
149
|
+
/** Actor names a scenario's steps run as */
|
|
150
150
|
actors?: string[];
|
|
151
151
|
/** If true, workflow always executes inline without queues */
|
|
152
152
|
inline?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.36",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
38
|
-
"@pikku/core": "^0.12.
|
|
38
|
+
"@pikku/core": "^0.12.52",
|
|
39
39
|
"openapi-types": "^12.1.3",
|
|
40
40
|
"path-to-regexp": "^8.3.0",
|
|
41
41
|
"ts-json-schema-generator": "2.9.1-next.16",
|
package/run-tests.sh
CHANGED
|
@@ -38,16 +38,16 @@ if [ ${#files[@]} -eq 0 ]; then
|
|
|
38
38
|
fi
|
|
39
39
|
|
|
40
40
|
# Construct the node command
|
|
41
|
-
node_cmd=
|
|
41
|
+
node_cmd=(node --import tsx --test)
|
|
42
42
|
|
|
43
43
|
# Append options based on flags
|
|
44
44
|
if [ "$watch_mode" = true ]; then
|
|
45
|
-
node_cmd
|
|
45
|
+
node_cmd+=(--watch)
|
|
46
46
|
fi
|
|
47
47
|
|
|
48
48
|
if [ "$coverage_mode" = true ]; then
|
|
49
|
-
node_cmd
|
|
49
|
+
node_cmd+=(--test-coverage-include="src/**/*.{ts,js}" --test-coverage-exclude="**/dist/**" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info)
|
|
50
50
|
fi
|
|
51
51
|
|
|
52
52
|
# Execute the node command with the expanded list of files
|
|
53
|
-
$node_cmd "${files[@]}"
|
|
53
|
+
"${node_cmd[@]}" "${files[@]}"
|
package/src/add/add-functions.ts
CHANGED
|
@@ -358,11 +358,11 @@ export const addFunctions: AddWiring = (
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
// Match identifiers that contain both "pikku" and "func" (case insensitive),
|
|
361
|
-
// plus
|
|
361
|
+
// plus pikkuScenario (a workflow wrapper without "func" in its name)
|
|
362
362
|
const pikkuFuncPattern = /pikku.*func/i
|
|
363
363
|
if (
|
|
364
364
|
!pikkuFuncPattern.test(expression.text) &&
|
|
365
|
-
expression.text !== '
|
|
365
|
+
expression.text !== 'pikkuScenario'
|
|
366
366
|
) {
|
|
367
367
|
return
|
|
368
368
|
}
|
|
@@ -494,9 +494,15 @@ export const addFunctions: AddWiring = (
|
|
|
494
494
|
approvalRequired = getPropertyValue(firstArg, 'approvalRequired') as
|
|
495
495
|
| boolean
|
|
496
496
|
| undefined
|
|
497
|
-
workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as
|
|
498
|
-
|
|
499
|
-
|
|
497
|
+
workflowQueued = getPropertyValue(firstArg, 'workflowQueued') as
|
|
498
|
+
| boolean
|
|
499
|
+
| undefined
|
|
500
|
+
workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as
|
|
501
|
+
| number
|
|
502
|
+
| undefined
|
|
503
|
+
workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as
|
|
504
|
+
| string
|
|
505
|
+
| undefined
|
|
500
506
|
|
|
501
507
|
// Extract approvalDescription identifier reference
|
|
502
508
|
for (const prop of firstArg.properties) {
|
|
@@ -389,8 +389,10 @@ export function registerHTTPRoute({
|
|
|
389
389
|
checker
|
|
390
390
|
)
|
|
391
391
|
|
|
392
|
-
// Track used functions/middleware/permissions for service aggregation
|
|
393
392
|
state.serviceAggregation.usedFunctions.add(funcName)
|
|
393
|
+
if (refAddonTarget) {
|
|
394
|
+
state.serviceAggregation.usedFunctions.add(refAddonTarget)
|
|
395
|
+
}
|
|
394
396
|
extractWireNames(middleware).forEach((name) =>
|
|
395
397
|
state.serviceAggregation.usedMiddleware.add(name)
|
|
396
398
|
)
|
|
@@ -415,6 +417,7 @@ export function registerHTTPRoute({
|
|
|
415
417
|
state.http.files.add(sourceFile.fileName)
|
|
416
418
|
state.http.meta[method][fullRoute] = {
|
|
417
419
|
pikkuFuncId: funcName,
|
|
420
|
+
...(refAddonTarget && { refTarget: refAddonTarget }),
|
|
418
421
|
...(packageName && { packageName }),
|
|
419
422
|
route: fullRoute,
|
|
420
423
|
sourceFile: sourceFile.fileName,
|