@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/dist/add/add-functions.js +2 -2
  3. package/dist/add/add-http-route.js +4 -1
  4. package/dist/add/add-rpc-invocations.js +7 -0
  5. package/dist/add/add-workflow.d.ts +1 -1
  6. package/dist/add/add-workflow.js +39 -11
  7. package/dist/error-codes.d.ts +2 -1
  8. package/dist/error-codes.js +2 -1
  9. package/dist/inspector.js +1 -0
  10. package/dist/types.d.ts +1 -0
  11. package/dist/utils/filter-inspector-state.js +43 -0
  12. package/dist/utils/load-addon-functions-meta.js +20 -14
  13. package/dist/utils/post-process.js +38 -13
  14. package/dist/utils/serialize-inspector-state.d.ts +1 -0
  15. package/dist/utils/serialize-inspector-state.js +2 -0
  16. package/dist/utils/workflow/dsl/patterns.d.ts +1 -1
  17. package/dist/utils/workflow/dsl/patterns.js +1 -1
  18. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +3 -3
  19. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -5
  20. package/package.json +2 -2
  21. package/run-tests.sh +4 -4
  22. package/src/add/add-functions.ts +11 -5
  23. package/src/add/add-http-route.ts +4 -1
  24. package/src/add/add-rpc-invocations.test.ts +113 -0
  25. package/src/add/add-rpc-invocations.ts +8 -0
  26. package/src/add/add-workflow.ts +53 -14
  27. package/src/add/expect-eventually-scenario-only.test.ts +108 -0
  28. package/src/error-codes.ts +2 -1
  29. package/src/inspector.ts +1 -0
  30. package/src/types.ts +1 -0
  31. package/src/utils/filter-inspector-state.test.ts +231 -0
  32. package/src/utils/filter-inspector-state.ts +56 -0
  33. package/src/utils/load-addon-functions-meta.ts +26 -16
  34. package/src/utils/post-process.test.ts +254 -0
  35. package/src/utils/post-process.ts +41 -14
  36. package/src/utils/serialize-inspector-state.ts +11 -0
  37. package/src/utils/workflow/dsl/patterns.ts +1 -1
  38. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +3 -3
  39. package/src/utils/workflow/graph/workflow-graph.types.ts +5 -5
  40. 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 pikkuUserFlow (a workflow wrapper without "func" in its name)
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 !== 'pikkuUserFlow') {
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
- * pikkuUserFlow() calls. Detects workflow registration and extracts metadata.
9
+ * pikkuScenario() calls. Detects workflow registration and extracts metadata.
10
10
  */
11
11
  export declare const addWorkflow: AddWiring;
@@ -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
- * pikkuUserFlow() calls. Detects workflow registration and extracts metadata.
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 === 'pikkuUserFlow') {
200
- // A user flow is a complex workflow whose steps run as actors over the
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 = 'user-flow';
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 (user flows) — powers the console
312
- // personas view and the User Flow graph badges.
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
- userFlow: wrapperType === 'user-flow' ? true : undefined,
358
+ scenario: wrapperType === 'scenario' ? true : undefined,
331
359
  actors: actorNames.length > 0 ? actorNames : undefined,
332
360
  };
333
- // User flows are pure stories of remote RPCs (same rule as client-side CLI
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 === 'user-flow' && funcMeta?.services) {
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.USER_FLOW_HAS_SERVICES, `User flow '${workflowName}' destructures services: ${disallowed.join(', ')}. ` +
342
- `User flows may only use 'logger'/'config' — drive everything else through ` +
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;
@@ -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
- USER_FLOW_HAS_SERVICES = "PKU673",
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",
@@ -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["USER_FLOW_HAS_SERVICES"] = "PKU673";
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
@@ -413,6 +413,7 @@ export interface InspectorState {
413
413
  exportedName: string;
414
414
  }>;
415
415
  invokedFunctions: Set<string>;
416
+ invokedFunctionsByFile: Map<string, Set<string>>;
416
417
  usedAddons: Set<string>;
417
418
  wireAddonDeclarations: Map<string, {
418
419
  package: string;
@@ -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
- catch {
149
- // No addon meta or no serverlessIncompatible declared — that's fine
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
- logger.debug(`Loaded ${servicesModule.requiredParentServices.length} required parent services for '${namespace}' from ${decl.package}`);
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
- // No services gen — addon may not have requiredParentServices
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
- // These are required ONLY by units that actually deploy an addon function;
257
- // a unit that merely calls the addon over RPC (or never touches it) must not
258
- // carry them, or every per-unit bundle would over-include the addon's
259
- // parent-service dependencies (e.g. aiAgentRunner, deploymentService) and
260
- // defeat per-unit tree-shaking.
261
- const addonFuncIds = new Set();
262
- for (const fns of Object.values(state.addonFunctions ?? {})) {
263
- for (const id of Object.keys(fns))
264
- addonFuncIds.add(id);
265
- }
266
- const unitDeploysAddonFn = [...usedFunctions].some((fn) => addonFuncIds.has(fn));
267
- if (unitDeploysAddonFn) {
268
- for (const service of state.addonRequiredParentServices ?? []) {
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
  }
@@ -178,6 +178,7 @@ export interface SerializableInspectorState {
178
178
  exportedName: string;
179
179
  }]>;
180
180
  invokedFunctions: string[];
181
+ invokedFunctionsByFile?: Array<[string, string[]]>;
181
182
  usedAddons: string[];
182
183
  wireAddonDeclarations: Array<[
183
184
  string,
@@ -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 user flows)
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 user flows)
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
- // - userFlow: complex workflow whose steps run as actors (user flow)
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.userFlow
305
- ? 'user-flow'
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
- /** User-flow actor name this step runs as (workflow.do {actor: actors.x}) */
99
+ /** Scenario actor name this step runs as (workflow.do {actor: actors.x}) */
100
100
  actor?: string;
101
- /** True for workflow.expectEventually polling steps (user flows) */
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
- * - 'user-flow': User flow (pikkuUserFlow) - complex workflow whose steps run as actors
130
+ * - 'scenario': Scenario (pikkuScenario) - complex workflow whose steps run as actors
131
131
  */
132
- export type WorkflowSourceType = 'dsl' | 'complex' | 'graph' | 'user-flow';
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 user flow's steps run as */
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.34",
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.50",
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="node --import tsx --test"
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="$node_cmd --watch"
45
+ node_cmd+=(--watch)
46
46
  fi
47
47
 
48
48
  if [ "$coverage_mode" = true ]; then
49
- node_cmd="$node_cmd --test-coverage-include=\"src/**/*.{ts,js}\" --test-coverage-exclude=\"**/dist/**\" --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
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[@]}"
@@ -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 pikkuUserFlow (a workflow wrapper without "func" in its name)
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 !== 'pikkuUserFlow'
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 boolean | undefined
498
- workflowRetries = getPropertyValue(firstArg, 'workflowRetries') as number | undefined
499
- workflowTimeout = getPropertyValue(firstArg, 'workflowTimeout') as string | undefined
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,