@pikku/inspector 0.12.59 → 0.12.61

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 (132) hide show
  1. package/CHANGELOG.md +373 -0
  2. package/OPTIMIZATION-PLAN.md +19 -19
  3. package/dist/add/add-agent-scorer.d.ts +2 -0
  4. package/dist/add/{add-ai-scorer.js → add-agent-scorer.js} +4 -4
  5. package/dist/add/{add-ai-agent.d.ts → add-agent.d.ts} +1 -1
  6. package/dist/add/{add-ai-agent.js → add-agent.js} +11 -11
  7. package/dist/add/add-auth.js +1 -1
  8. package/dist/add/add-cli.js +5 -0
  9. package/dist/add/add-functions.js +10 -1
  10. package/dist/add/add-http-route.js +18 -18
  11. package/dist/add/add-keyed-wiring.d.ts +2 -0
  12. package/dist/add/add-keyed-wiring.js +3 -1
  13. package/dist/add/add-middleware.js +5 -5
  14. package/dist/add/add-personas.js +3 -0
  15. package/dist/add/add-secret.js +1 -0
  16. package/dist/add/add-variable.js +1 -0
  17. package/dist/add/add-wire-remote-addon.js +2 -1
  18. package/dist/add/add-workflow-graph.js +63 -6
  19. package/dist/error-codes.d.ts +3 -1
  20. package/dist/error-codes.js +3 -1
  21. package/dist/index.d.ts +3 -1
  22. package/dist/index.js +1 -0
  23. package/dist/inspector.js +13 -3
  24. package/dist/types.d.ts +10 -5
  25. package/dist/utils/annotate-http-route-auth.js +2 -1
  26. package/dist/utils/check-secret-sinks.js +2 -1
  27. package/dist/utils/compute-required-schemas.d.ts +1 -1
  28. package/dist/utils/contract-hashes.d.ts +2 -1
  29. package/dist/utils/contract-hashes.js +1 -1
  30. package/dist/utils/extract-function-name.js +1 -1
  31. package/dist/utils/extract-services.d.ts +1 -1
  32. package/dist/utils/extract-surface-usage.d.ts +29 -0
  33. package/dist/utils/extract-surface-usage.js +97 -0
  34. package/dist/utils/filter-inspector-state.js +53 -16
  35. package/dist/utils/filter-utils.d.ts +1 -1
  36. package/dist/utils/get-property-value.js +42 -3
  37. package/dist/utils/load-addon-functions-meta.js +31 -0
  38. package/dist/utils/middleware.d.ts +2 -2
  39. package/dist/utils/middleware.js +5 -5
  40. package/dist/utils/permissions.d.ts +1 -1
  41. package/dist/utils/post-process.d.ts +28 -8
  42. package/dist/utils/post-process.js +83 -18
  43. package/dist/utils/resolve-deploy-target.d.ts +1 -1
  44. package/dist/utils/resolve-function-meta.d.ts +1 -1
  45. package/dist/utils/resolve-function-types.d.ts +1 -1
  46. package/dist/utils/resolve-versions.js +1 -1
  47. package/dist/utils/schema-generator.d.ts +1 -1
  48. package/dist/utils/serialize-inspector-state.d.ts +4 -3
  49. package/dist/utils/serialize-inspector-state.js +6 -4
  50. package/dist/utils/serialize-openapi-json.d.ts +1 -1
  51. package/dist/utils/validate-auth-sessionless.js +2 -1
  52. package/dist/utils/workflow/graph/finalize-workflows.js +1 -1
  53. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +6 -1
  54. package/dist/utils/workflow/graph/serialize-workflow-graph.js +26 -2
  55. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +9 -0
  56. package/dist/visit.js +4 -4
  57. package/package.json +10 -10
  58. package/run-tests.sh +0 -0
  59. package/src/add/{add-ai-scorer.test.ts → add-agent-scorer.test.ts} +11 -11
  60. package/src/add/{add-ai-scorer.ts → add-agent-scorer.ts} +4 -4
  61. package/src/add/{add-ai-agent.test.ts → add-agent.test.ts} +1 -1
  62. package/src/add/{add-ai-agent.ts → add-agent.ts} +13 -21
  63. package/src/add/add-auth.ts +2 -2
  64. package/src/add/add-cli.ts +6 -0
  65. package/src/add/add-credential.ts +3 -6
  66. package/src/add/add-file-with-factory.ts +2 -6
  67. package/src/add/add-functions.ts +23 -25
  68. package/src/add/add-http-route-ref.test.ts +149 -0
  69. package/src/add/add-http-route.ts +25 -28
  70. package/src/add/add-keyed-wiring.ts +8 -7
  71. package/src/add/add-middleware.ts +7 -5
  72. package/src/add/add-permission.ts +1 -1
  73. package/src/add/add-personas.test.ts +24 -0
  74. package/src/add/add-personas.ts +3 -1
  75. package/src/add/add-scenario-step.test.ts +35 -0
  76. package/src/add/add-scope.test.ts +4 -4
  77. package/src/add/add-scope.ts +4 -8
  78. package/src/add/add-secret.ts +1 -0
  79. package/src/add/add-system-role.test.ts +8 -8
  80. package/src/add/add-system-role.ts +2 -4
  81. package/src/add/add-variable.ts +1 -0
  82. package/src/add/add-wire-remote-addon.ts +4 -1
  83. package/src/add/add-workflow-graph-foreach.test.ts +174 -0
  84. package/src/add/add-workflow-graph.ts +78 -6
  85. package/src/add/add-workflow-sleep-expression.test.ts +3 -4
  86. package/src/add/addon-contracts.test.ts +1 -1
  87. package/src/add/config-value-metadata.test.ts +94 -0
  88. package/src/add/secret-sink-check.test.ts +4 -1
  89. package/src/error-codes.ts +3 -1
  90. package/src/index.ts +3 -4
  91. package/src/inspector.ts +20 -3
  92. package/src/types.ts +10 -7
  93. package/src/utils/annotate-http-route-auth.test.ts +5 -7
  94. package/src/utils/annotate-http-route-auth.ts +2 -1
  95. package/src/utils/check-secret-sinks.ts +4 -1
  96. package/src/utils/compute-required-schemas.ts +1 -1
  97. package/src/utils/contract-hashes.test.ts +1 -1
  98. package/src/utils/contract-hashes.ts +3 -2
  99. package/src/utils/extract-function-name.ts +1 -1
  100. package/src/utils/extract-services.ts +4 -2
  101. package/src/utils/extract-surface-usage.test.ts +147 -0
  102. package/src/utils/extract-surface-usage.ts +121 -0
  103. package/src/utils/filter-inspector-state.test.ts +138 -12
  104. package/src/utils/filter-inspector-state.ts +82 -21
  105. package/src/utils/filter-utils.ts +1 -1
  106. package/src/utils/get-property-value.ts +43 -5
  107. package/src/utils/load-addon-functions-meta.ts +40 -0
  108. package/src/utils/middleware.test.ts +23 -23
  109. package/src/utils/middleware.ts +6 -6
  110. package/src/utils/permissions.ts +1 -1
  111. package/src/utils/post-process.test.ts +68 -11
  112. package/src/utils/post-process.ts +91 -28
  113. package/src/utils/resolve-deploy-target.ts +1 -1
  114. package/src/utils/resolve-function-meta.ts +1 -1
  115. package/src/utils/resolve-function-types.ts +1 -1
  116. package/src/utils/resolve-versions.ts +1 -1
  117. package/src/utils/schema-generator.ts +2 -1
  118. package/src/utils/serialize-inspector-state.ts +10 -7
  119. package/src/utils/serialize-mcp-json.ts +1 -1
  120. package/src/utils/serialize-openapi-json.ts +1 -1
  121. package/src/utils/validate-auth-sessionless.ts +2 -1
  122. package/src/utils/validate-tags-resolve-to-middleware.test.ts +4 -1
  123. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +4 -1
  124. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +1 -1
  125. package/src/utils/workflow/graph/finalize-workflows.ts +6 -2
  126. package/src/utils/workflow/graph/serialize-workflow-graph.ts +34 -3
  127. package/src/utils/workflow/graph/workflow-graph.types.ts +10 -0
  128. package/src/visit.ts +4 -4
  129. package/tsconfig.tsbuildinfo +1 -1
  130. package/dist/add/add-ai-scorer.d.ts +0 -2
  131. package/dist/utils/secret-brokers.d.ts +0 -2
  132. package/dist/utils/secret-brokers.js +0 -9
@@ -7,11 +7,12 @@ import type {
7
7
  } from '../types.js'
8
8
  import type {
9
9
  FunctionServicesMeta,
10
- MiddlewareMetadata,
11
10
  PermissionMetadata,
12
- } from '@pikku/core'
11
+ } from '@pikku/core/function'
12
+ import type { MiddlewareMetadata } from '@pikku/core/middleware'
13
13
  import type ts from 'typescript'
14
14
  import { existsSync, readFileSync } from 'node:fs'
15
+ import { createRequire } from 'node:module'
15
16
  import { join } from 'node:path'
16
17
  import { extractTypeKeys } from './type-utils.js'
17
18
  import { ErrorCode } from '../error-codes.js'
@@ -308,12 +309,12 @@ export function aggregateRequiredServices(
308
309
  }
309
310
  }
310
311
 
311
- // AI agents need aiStorage + aiRunState + agentRunService + aiAgentRunner
312
+ // AI agents need agentStorage + agentRunState + agentRunService + agentRunner
312
313
  if (Object.keys(state.agents.agentsMeta).length > 0) {
313
- requiredServices.add('aiStorage')
314
- requiredServices.add('aiRunState')
314
+ requiredServices.add('agentStorage')
315
+ requiredServices.add('agentRunState')
315
316
  requiredServices.add('agentRunService')
316
- requiredServices.add('aiAgentRunner')
317
+ requiredServices.add('agentRunner')
317
318
  }
318
319
 
319
320
  // Channels need eventHub for pub/sub
@@ -480,10 +481,56 @@ export function validateVariableOverrides(
480
481
  }
481
482
 
482
483
  /**
484
+ * Can `pkg` be resolved from `rootDir`, by any means the loader would use?
485
+ *
486
+ * Resolution is attempted against `<pkg>/package.json` first and the bare
487
+ * specifier second, because neither alone is conclusive. A package with a
488
+ * restrictive `exports` map refuses the first with
489
+ * `ERR_PACKAGE_PATH_NOT_EXPORTED` — which is not a failure at all: reaching the
490
+ * exports map means the package was found. Only a module-not-found from both
491
+ * attempts means it is genuinely absent.
492
+ */
493
+ const isPackageResolvable = (pkg: string, rootDir: string): boolean => {
494
+ const NOT_FOUND = new Set(['MODULE_NOT_FOUND', 'ERR_MODULE_NOT_FOUND'])
495
+ let req: ReturnType<typeof createRequire>
496
+ try {
497
+ req = createRequire(join(rootDir, 'package.json'))
498
+ } catch {
499
+ return true // cannot ask; do not accuse
500
+ }
501
+ for (const specifier of [`${pkg}/package.json`, pkg]) {
502
+ try {
503
+ req.resolve(specifier)
504
+ return true
505
+ } catch (e: any) {
506
+ if (!NOT_FOUND.has(e?.code)) return true
507
+ }
508
+ }
509
+ return false
510
+ }
511
+
512
+ /**
513
+ * Every wired addon must be installed, in the bucket matching how it is used.
514
+ *
483
515
  * A `wireRemoteAddon` package ships types only — its handlers run on the host —
484
516
  * so it MUST be a devDependency, not a production dependency (a prod dep would
485
- * drag in the runtime deps remote consumption exists to avoid). This is the
486
- * mirror image of `wireAddon`, which requires a production dependency.
517
+ * drag in the runtime deps remote consumption exists to avoid).
518
+ *
519
+ * A plain `wireAddon` only has to be *resolvable*. The failure it allows is
520
+ * quiet in a way the remote one is not — an absent package makes the metadata
521
+ * load fail, which is caught and downgraded to a warning, so every
522
+ * `ref('<namespace>:…')` resolves against nothing and the surface is dead at
523
+ * runtime. The generated console is the common case: `scaffold.console` emits
524
+ * `wireAddon({ package: '@pikku/addon-console' })` and refs into it, and a
525
+ * project that never installed the package gets a console that 404s with
526
+ * nothing in the build output saying why.
527
+ *
528
+ * Deliberately a resolution check and not a `package.json` one. An addon is
529
+ * discovered because it is *wired*, not because it is a declared dependency —
530
+ * addons arrive by workspace link, by a local `addons/` directory recorded in
531
+ * `pikku-addons.json`, and by plain install, and only the last of those shows
532
+ * up in `dependencies`. Asking the resolver is the only question that matches
533
+ * what the loader will actually do.
487
534
  */
488
535
  export function validateRemoteAddonDependencies(
489
536
  logger: InspectorLogger,
@@ -492,10 +539,19 @@ export function validateRemoteAddonDependencies(
492
539
  const { wireAddonDeclarations } = state.rpc
493
540
  if (!wireAddonDeclarations || wireAddonDeclarations.size === 0) return
494
541
 
495
- const hasRemote = Array.from(wireAddonDeclarations.values()).some(
496
- (d) => d.remote
497
- )
498
- if (!hasRemote) return
542
+ // Local addons first, and independently of the manifest below: their check
543
+ // asks the resolver, not `package.json`, so a project without a manifest (as
544
+ // in some tests) must not skip it.
545
+ for (const [namespace, decl] of wireAddonDeclarations.entries()) {
546
+ if (decl.remote) continue
547
+ if (isPackageResolvable(decl.package, state.rootDir)) continue
548
+ logger.critical(
549
+ ErrorCode.ADDON_NOT_INSTALLED,
550
+ `Addon '${namespace}' ('${decl.package}') is wired with wireAddon but cannot be resolved — every ref('${namespace}:…') will resolve to nothing and the surface will be dead at runtime. Install it, or remove the wireAddon call.`
551
+ )
552
+ }
553
+
554
+ if (!Array.from(wireAddonDeclarations.values()).some((d) => d.remote)) return
499
555
 
500
556
  const pkgJsonPath = join(state.rootDir, 'package.json')
501
557
  if (!existsSync(pkgJsonPath)) return // no manifest to check (e.g. some tests)
@@ -724,10 +780,7 @@ export function computeRequiredSchemas(
724
780
  * the functions that are actually invoked, so this blocks CI through
725
781
  * `--fail-on-error` without failing a dev server over a function nobody calls.
726
782
  */
727
- export function validateSchemaReferences(
728
- logger: InspectorLogger,
729
- state: InspectorState
730
- ): void {
783
+ export function unresolvedSchemaReferences(state: InspectorState): string[] {
731
784
  const { functions, schemas } = state
732
785
  const available = new Set(Object.keys(schemas ?? {}))
733
786
  const unresolved: string[] = []
@@ -752,6 +805,15 @@ export function validateSchemaReferences(
752
805
  }
753
806
  }
754
807
 
808
+ return unresolved
809
+ }
810
+
811
+ export function validateSchemaReferences(
812
+ logger: InspectorLogger,
813
+ state: InspectorState
814
+ ): void {
815
+ const unresolved = unresolvedSchemaReferences(state)
816
+
755
817
  if (unresolved.length === 0) return
756
818
 
757
819
  logger.diagnostic({
@@ -842,10 +904,11 @@ export function validateScenarioServices(
842
904
  *
843
905
  * - the step target must be a static string literal (PKU678) — otherwise it
844
906
  * can't be bundled, typed or drawn;
845
- * - a step declaring a `browser` binding must be given an actor (PKU677), since
846
- * the browser context signs in as that persona. It is checked statically even
847
- * though the binding only runs on a browser run: a step that can never be
848
- * driven by a human is a latent failure, not a passing default run.
907
+ * - a step that runs as a persona must be given one (PKU677) — declared by
908
+ * `actor: true`, or implied by a `browser` binding, since the browser context
909
+ * signs in as that persona. It is checked statically even though a browser
910
+ * binding only runs on a browser run: a step that can never be driven by a
911
+ * human is a latent failure, not a passing default run.
849
912
  * - a scenario written as a step ladder must assert something (PKU680).
850
913
  *
851
914
  * It also marks the step's function as used, so `scenario run` boots the
@@ -872,14 +935,14 @@ export function validateScenarioSteps(
872
935
  }
873
936
  state.serviceAggregation.usedFunctions.add(step.stepFunc)
874
937
  const stepMeta = state.functions.meta[step.stepFunc]
875
- if (
876
- stepMeta?.scenarioStepSurfaces?.includes('browser') &&
877
- !step.actor
878
- ) {
938
+ if (stepMeta?.scenarioStepRequiresActor && !step.actor) {
939
+ const why = stepMeta.scenarioStepSurfaces?.includes('browser')
940
+ ? 'declares a browser binding'
941
+ : 'is driven by a persona'
879
942
  logger.critical(
880
- ErrorCode.SCENARIO_BROWSER_STEP_NEEDS_ACTOR,
881
- `Scenario '${workflowName}' calls step '${step.stepFunc}', which declares a browser binding, without an actor. ` +
882
- `Pass { actor: actors.<name> } so the browser context signs in as that persona.`
943
+ ErrorCode.SCENARIO_STEP_NEEDS_ACTOR,
944
+ `Scenario '${workflowName}' calls step '${step.stepFunc}', which ${why}, without an actor. ` +
945
+ `Pass { actor: actors.<name> } so it runs as that persona.`
883
946
  )
884
947
  }
885
948
  continue
@@ -1230,7 +1293,7 @@ export function validateScopeReferences(
1230
1293
  *
1231
1294
  * The description check is separate and opt-in (`--strict-meta`), because the
1232
1295
  * description is what the model is told the tool does: without one, the tool is
1233
- * offered to it under its own name (`ai-agent-prepare.ts`), which is a silent
1296
+ * offered to it under its own name (`agent-prepare.ts`), which is a silent
1234
1297
  * quality loss rather than a failure. A title deliberately does not satisfy it —
1235
1298
  * a title labels a tool in a UI, it does not tell a model when to reach for it.
1236
1299
  */
@@ -1,4 +1,4 @@
1
- import type { FunctionMeta } from '@pikku/core'
1
+ import type { FunctionMeta } from '@pikku/core/services'
2
2
 
3
3
  /**
4
4
  * Thrown when a function's explicit `deploy: 'serverless'` conflicts
@@ -1,4 +1,4 @@
1
- import type { FunctionMeta, FunctionsMeta } from '@pikku/core'
1
+ import type { FunctionMeta, FunctionsMeta } from '@pikku/core/services'
2
2
 
3
3
  /**
4
4
  * Look up function metadata by pikkuFuncId, checking both local functions
@@ -1,5 +1,5 @@
1
1
  import type { TypesMap } from '../types-map.js'
2
- import type { FunctionsMeta } from '@pikku/core'
2
+ import type { FunctionsMeta } from '@pikku/core/services'
3
3
 
4
4
  export function resolveFunctionIOTypes(
5
5
  pikkuFuncId: string,
@@ -1,4 +1,4 @@
1
- import { parseVersionedId, formatVersionedId } from '@pikku/core'
1
+ import { parseVersionedId, formatVersionedId } from '@pikku/core/utils'
2
2
  import type { CLICommandMeta } from '@pikku/core/cli'
3
3
  import type { InspectorState, InspectorLogger } from '../types.js'
4
4
  import { ErrorCode } from '../error-codes.js'
@@ -12,7 +12,8 @@ import { createGenerator, RootlessError } from 'ts-json-schema-generator'
12
12
  import { register } from 'tsx/esm/api'
13
13
  import * as z from 'zod'
14
14
  import { zodToTs, createAuxiliaryTypeStore } from 'zod-to-ts'
15
- import type { FunctionsMeta, JSONValue } from '@pikku/core'
15
+ import type { FunctionsMeta } from '@pikku/core/services'
16
+ import type { JSONValue } from '@pikku/core/utils'
16
17
  import type { HTTPWiringsMeta } from '@pikku/core/http'
17
18
  import type { TypesMap } from '../types-map.js'
18
19
  import { ErrorCode } from '../error-codes.js'
@@ -1,5 +1,5 @@
1
1
  import type { SecretUsage } from './extract-secret-usage.js'
2
- import type { JSONValue } from '@pikku/core'
2
+ import type { JSONValue } from '@pikku/core/utils'
3
3
  import type {
4
4
  InspectorDiagnostic,
5
5
  InspectorFeature,
@@ -212,6 +212,7 @@ export interface SerializableInspectorState {
212
212
  files: string[]
213
213
  usage: Record<string, SecretUsage>
214
214
  }
215
+ surfaceUsage: InspectorState['surfaceUsage']
215
216
  credentials: {
216
217
  definitions: InspectorState['credentials']['definitions']
217
218
  files: string[]
@@ -263,8 +264,8 @@ export interface SerializableInspectorState {
263
264
  ]
264
265
  >
265
266
  }
266
- aiMiddleware: {
267
- definitions: InspectorState['aiMiddleware']['definitions']
267
+ agentMiddleware: {
268
+ definitions: InspectorState['agentMiddleware']['definitions']
268
269
  }
269
270
  permissions: {
270
271
  definitions: InspectorState['permissions']['definitions']
@@ -440,6 +441,7 @@ export function serializeInspectorState(
440
441
  files: Array.from(state.secrets.files),
441
442
  usage: Object.fromEntries(state.secrets.usage),
442
443
  },
444
+ surfaceUsage: state.surfaceUsage,
443
445
  credentials: {
444
446
  definitions: state.credentials.definitions,
445
447
  files: Array.from(state.credentials.files),
@@ -473,8 +475,8 @@ export function serializeInspectorState(
473
475
  state.channelMiddleware.tagMiddleware.entries()
474
476
  ),
475
477
  },
476
- aiMiddleware: {
477
- definitions: state.aiMiddleware.definitions,
478
+ agentMiddleware: {
479
+ definitions: state.agentMiddleware.definitions,
478
480
  },
479
481
  permissions: {
480
482
  definitions: state.permissions.definitions,
@@ -648,6 +650,7 @@ export function deserializeInspectorState(
648
650
  files: new Set(data.secrets?.files || []),
649
651
  usage: new Map(Object.entries(data.secrets?.usage || {})),
650
652
  },
653
+ surfaceUsage: data.surfaceUsage || {},
651
654
  credentials: {
652
655
  definitions: data.credentials?.definitions || [],
653
656
  files: new Set(data.credentials?.files || []),
@@ -679,8 +682,8 @@ export function deserializeInspectorState(
679
682
  instances: data.channelMiddleware?.instances || {},
680
683
  tagMiddleware: new Map(data.channelMiddleware?.tagMiddleware || []),
681
684
  },
682
- aiMiddleware: {
683
- definitions: data.aiMiddleware?.definitions || {},
685
+ agentMiddleware: {
686
+ definitions: data.agentMiddleware?.definitions || {},
684
687
  },
685
688
  permissions: {
686
689
  definitions: data.permissions.definitions,
@@ -1,5 +1,5 @@
1
1
  import type { InspectorLogger, InspectorState } from '../types.js'
2
- import type { JSONValue } from '@pikku/core'
2
+ import type { JSONValue } from '@pikku/core/utils'
3
3
  import { resolveFunctionMeta } from './resolve-function-meta.js'
4
4
 
5
5
  interface MCPEndpoint {
@@ -1,4 +1,4 @@
1
- import type { FunctionsMeta } from '@pikku/core'
1
+ import type { FunctionsMeta } from '@pikku/core/services'
2
2
  import type { HTTPWiringsMeta } from '@pikku/core/http'
3
3
  import type { InspectorLogger } from '../types.js'
4
4
  import _convertSchema from '@openapi-contrib/json-schema-to-openapi-schema'
@@ -2,6 +2,7 @@ import type * as ts from 'typescript'
2
2
  import { getPropertyValue } from './get-property-value.js'
3
3
  import { ErrorCode } from '../error-codes.js'
4
4
  import type { InspectorLogger, InspectorState } from '../types.js'
5
+ import { resolveFunctionMeta } from './resolve-function-meta.js'
5
6
 
6
7
  export function validateAuthSessionless(
7
8
  logger: InspectorLogger,
@@ -11,7 +12,7 @@ export function validateAuthSessionless(
11
12
  wireDescription: string,
12
13
  inheritedAuth?: boolean
13
14
  ): boolean {
14
- const fnMeta = state.functions.meta[funcName]
15
+ const fnMeta = resolveFunctionMeta(state, funcName)
15
16
  if (!fnMeta) return true
16
17
 
17
18
  const routeAuth = getPropertyValue(obj, 'auth')
@@ -59,7 +59,10 @@ describe('validateTagsResolveToMiddleware', () => {
59
59
  })
60
60
 
61
61
  test('stays quiet when http middleware is registered for the tag', () => {
62
- assert.deepEqual(run(stateWith({ console: { tags: ['admin'] } }, ['admin'])), [])
62
+ assert.deepEqual(
63
+ run(stateWith({ console: { tags: ['admin'] } }, ['admin'])),
64
+ []
65
+ )
63
66
  })
64
67
 
65
68
  test('stays quiet when only channel middleware is registered for the tag', () => {
@@ -26,7 +26,10 @@ const approvalStepFor = (options: string): ApprovalStepMeta => {
26
26
 
27
27
  let node: ts.Node | undefined
28
28
  const visit = (n: ts.Node) => {
29
- if (ts.isCallExpression(n) && n.expression.getText() === 'pikkuWorkflowFunc') {
29
+ if (
30
+ ts.isCallExpression(n) &&
31
+ n.expression.getText() === 'pikkuWorkflowFunc'
32
+ ) {
30
33
  node = n
31
34
  }
32
35
  ts.forEachChild(n, visit)
@@ -1,7 +1,7 @@
1
1
  import { test } from 'node:test'
2
2
  import assert from 'node:assert'
3
3
  import { convertDslToGraph } from './convert-dsl-to-graph.js'
4
- import type { WorkflowsMeta } from '@pikku/core'
4
+ import type { WorkflowsMeta } from '@pikku/core/workflow'
5
5
 
6
6
  const scenarioMeta = (
7
7
  overrides: Partial<WorkflowsMeta[string]> = {}
@@ -1,5 +1,9 @@
1
- import type { FunctionsMeta } from '@pikku/core'
2
- import { isVersionedId, formatVersionedId, parseVersionedId } from '@pikku/core'
1
+ import type { FunctionsMeta } from '@pikku/core/services'
2
+ import {
3
+ isVersionedId,
4
+ formatVersionedId,
5
+ parseVersionedId,
6
+ } from '@pikku/core/utils'
3
7
  import type { SerializedWorkflowGraph } from './workflow-graph.types.js'
4
8
  import { canonicalJSON, hashString } from '../../hash.js'
5
9
  import { convertDslToGraph } from './convert-dsl-to-graph.js'
@@ -1,4 +1,4 @@
1
- import { isRef } from '@pikku/core/workflow'
1
+ import { isRef, template } from '@pikku/core/workflow'
2
2
  import type {
3
3
  SerializedWorkflowGraph,
4
4
  SerializedGraphNode,
@@ -36,6 +36,26 @@ function serializeInputMapping(
36
36
  return result
37
37
  }
38
38
 
39
+ /**
40
+ * Convert a `forEach` source — a bare node id, a ref, or a `(ref) => ref(...)`
41
+ * callback — to the serialized DataRef form
42
+ */
43
+ function serializeForEach(
44
+ forEach: unknown,
45
+ createRef: (nodeId: string, path?: string) => { nodeId: string; path?: string }
46
+ ): DataRef | undefined {
47
+ if (forEach === undefined) return undefined
48
+ if (typeof forEach === 'string') return { $ref: forEach }
49
+ if (typeof forEach === 'function') {
50
+ return serializeForEach(
51
+ (forEach as (ref: unknown) => unknown)(createRef),
52
+ createRef
53
+ )
54
+ }
55
+ if (isRef(forEach)) return convertRef(forEach)
56
+ return undefined
57
+ }
58
+
39
59
  /**
40
60
  * Convert next config from runtime format to serialized format
41
61
  * Runtime uses Record<string, string | string[]> for branching with graph.branch()
@@ -73,7 +93,9 @@ export function serializeWorkflowGraph(
73
93
  string,
74
94
  {
75
95
  func: { name?: string }
76
- input?: (ref: any) => Record<string, unknown>
96
+ input?: (ref: any, template?: any, $item?: any) => Record<string, unknown>
97
+ forEach?: string | { nodeId: string; path?: string } | ((ref: any) => unknown)
98
+ mode?: 'parallel' | 'sequential'
77
99
  next?: string | string[] | Record<string, string | string[]>
78
100
  onError?: string | string[]
79
101
  notes?: string
@@ -96,6 +118,8 @@ export function serializeWorkflowGraph(
96
118
  path,
97
119
  })
98
120
 
121
+ const createItemRef = (path?: string) => createRef('$item', path)
122
+
99
123
  // Track which nodes have incoming edges
100
124
  const hasIncomingEdge = new Set<string>()
101
125
 
@@ -124,7 +148,7 @@ export function serializeWorkflowGraph(
124
148
  // Evaluate input callback to get the mapping
125
149
  let input: Record<string, unknown | DataRef> = {}
126
150
  if (node.input) {
127
- const rawInput = node.input(createRef)
151
+ const rawInput = node.input(createRef, template, createItemRef)
128
152
  input = serializeInputMapping(rawInput)
129
153
  }
130
154
 
@@ -138,6 +162,13 @@ export function serializeWorkflowGraph(
138
162
  next: serializeNext(node.next),
139
163
  onError: node.onError,
140
164
  }
165
+ const forEach = serializeForEach(node.forEach, createRef)
166
+ if (forEach) {
167
+ funcNode.forEach = forEach
168
+ if (node.mode !== undefined) {
169
+ funcNode.mode = node.mode
170
+ }
171
+ }
141
172
  if (node.notes !== undefined) {
142
173
  funcNode.notes = node.notes
143
174
  }
@@ -141,11 +141,21 @@ interface BaseNode {
141
141
  /**
142
142
  * Function node - calls an RPC
143
143
  */
144
+ /** How the per-item instances of a `forEach` node run */
145
+ export type ForEachMode = 'parallel' | 'sequential'
146
+
144
147
  export interface FunctionNode extends BaseNode {
145
148
  /** RPC function name */
146
149
  rpcName: string
147
150
  /** Input mapping - values can be literals or DataRefs */
148
151
  input?: Record<string, unknown | DataRef>
152
+ /**
153
+ * Run this node once per element of the referenced array. The node's own
154
+ * result becomes the ordered array of per-item results.
155
+ */
156
+ forEach?: DataRef
157
+ /** How the per-item instances run. Defaults to 'parallel'. */
158
+ mode?: ForEachMode
149
159
  /** Output variable name for storing result */
150
160
  outputVar?: string
151
161
  /** Hash of nodeId + RPC input/output schemas for version detection */
package/src/visit.ts CHANGED
@@ -33,8 +33,8 @@ import { addPersonas } from './add/add-personas.js'
33
33
  import { addVariable } from './add/add-variable.js'
34
34
  import { addWorkflowGraph } from './add/add-workflow-graph.js'
35
35
  import { addFeature } from './add/add-feature.js'
36
- import { addAIAgent } from './add/add-ai-agent.js'
37
- import { addAIScorer } from './add/add-ai-scorer.js'
36
+ import { addAgent } from './add/add-agent.js'
37
+ import { addAgentScorer } from './add/add-agent-scorer.js'
38
38
  import { addApprovalDescription } from './add/add-approval-description.js'
39
39
 
40
40
  export const visitSetup = (
@@ -172,8 +172,8 @@ export const visitRoutes = (
172
172
  addMCPPrompt(logger, node, checker, state, nextOptions)
173
173
  addWorkflowGraph(logger, node, checker, state, nextOptions)
174
174
  addFeature(logger, node, checker, state, nextOptions)
175
- addAIAgent(logger, node, checker, state, nextOptions)
176
- addAIScorer(logger, node, checker, state, nextOptions)
175
+ addAgent(logger, node, checker, state, nextOptions)
176
+ addAgentScorer(logger, node, checker, state, nextOptions)
177
177
 
178
178
  ts.forEachChild(node, (child) =>
179
179
  visitRoutes(logger, checker, child, state, nextOptions)