@pikku/inspector 0.12.55 → 0.12.58

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 (44) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/dist/add/add-ai-agent.js +25 -0
  3. package/dist/add/add-ai-scorer.d.ts +2 -0
  4. package/dist/add/add-ai-scorer.js +61 -0
  5. package/dist/add/add-wire-addon.js +16 -0
  6. package/dist/add/scenario-instrumentation.d.ts +6 -1
  7. package/dist/add/scenario-instrumentation.js +6 -0
  8. package/dist/error-codes.d.ts +2 -0
  9. package/dist/error-codes.js +3 -0
  10. package/dist/inspector.js +13 -4
  11. package/dist/types.d.ts +20 -0
  12. package/dist/utils/filter-inspector-state.js +8 -0
  13. package/dist/utils/post-process.d.ts +28 -3
  14. package/dist/utils/post-process.js +85 -15
  15. package/dist/utils/serialize-inspector-state.d.ts +9 -0
  16. package/dist/utils/serialize-inspector-state.js +8 -0
  17. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +13 -4
  18. package/dist/utils/workflow/dsl/patterns.d.ts +2 -1
  19. package/dist/utils/workflow/dsl/patterns.js +8 -5
  20. package/dist/utils/workflow/graph/serialize-workflow-graph.js +2 -10
  21. package/dist/visit.js +2 -0
  22. package/package.json +3 -3
  23. package/src/add/add-ai-agent.ts +35 -0
  24. package/src/add/add-ai-scorer.test.ts +197 -0
  25. package/src/add/add-ai-scorer.ts +83 -0
  26. package/src/add/add-scenario-step.test.ts +19 -0
  27. package/src/add/add-wire-addon.test.ts +43 -0
  28. package/src/add/add-wire-addon.ts +14 -0
  29. package/src/add/scenario-instrumentation.ts +6 -0
  30. package/src/error-codes.ts +4 -0
  31. package/src/inspector.ts +15 -4
  32. package/src/types.ts +17 -0
  33. package/src/utils/filter-inspector-state.ts +8 -0
  34. package/src/utils/post-process.test.ts +71 -0
  35. package/src/utils/post-process.ts +98 -16
  36. package/src/utils/serialize-inspector-state.ts +14 -0
  37. package/src/utils/workflow/dsl/extract-approval-policy.test.ts +68 -0
  38. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +12 -4
  39. package/src/utils/workflow/dsl/patterns.ts +8 -5
  40. package/src/utils/workflow/graph/serialize-workflow-graph.ts +2 -15
  41. package/src/visit.ts +2 -0
  42. package/tsconfig.tsbuildinfo +1 -1
  43. package/src/utils/secret-brokers.test.ts +0 -53
  44. package/src/utils/secret-brokers.ts +0 -11
package/CHANGELOG.md CHANGED
@@ -1,3 +1,171 @@
1
+ ## 0.12.58
2
+
3
+ ### Patch Changes
4
+
5
+ - 3ad2131: Name models by what they are for, and switch them all in one place
6
+
7
+ A `models` table in pikku.config.json maps an alias to a provider-qualified
8
+ model, so a declaration can say `model: 'cheap'` and the project repoints every
9
+ use of that tier at once instead of editing each agent. A model containing `/`
10
+ is still concrete and used exactly as written, which is how an agent that needs
11
+ one specific model pins it — aliases are opt-in.
12
+
13
+ The table is baked into codegen rather than read at runtime, so it applies to
14
+ deployed units and not just local runs, and `pikku dev`/`pikku serve` take
15
+ `--model cheap:openai/gpt-5-nano` to repoint a tier for one run without editing
16
+ the config.
17
+
18
+ Because the inspector already holds every agent's model literal, a bare name
19
+ with no matching alias now fails the build (PKU146) naming the aliases that do
20
+ exist, rather than reaching a provider as an unknown model.
21
+
22
+ Aliases resolve for every modality, not just agents: image, speech,
23
+ transcription, embedding and reranking all reach a provider through the same
24
+ point in the Vercel runner.
25
+
26
+ - b930dca: Remove the `secretBroker` escape hatch and scope addon secrets and credentials
27
+
28
+ `secretBroker` let three named console functions receive the real `SecretService`,
29
+ against the rule that a function never sees one. It is gone: the inspector allowlist,
30
+ the `FunctionRuntimeMeta` flag, the runner branches, and the `WiredSecretBrokerServices`
31
+ type. Console secret administration moved into the console addon, where a
32
+ `SecretAdminService` holds the `SecretService` and the functions hold none.
33
+
34
+ Addons are now scoped rather than trusted. The CLI emits each package's declared secret
35
+ keys, and the host wraps the `SecretService` in a `ScopedSecretService` and the
36
+ `CredentialService` in a new `ScopedCredentialService` before the addon's service factory
37
+ runs — so an addon reads only what it declared, cannot write secrets, and cannot enumerate
38
+ the app's users. `wireAddon({ globalSecrets, globalCredentials })` waives this, taking the
39
+ reason as its value; only the consuming app can grant it, and the deploy manifest reports
40
+ every grant under `unscopedSecretAddons` / `unscopedCredentialAddons`.
41
+
42
+ - 8978fbd: feat(workflow): let an approval gate declare who may answer it
43
+
44
+ `workflow.approval()` gains `approvers` (`'any' | 'owner' | 'not-initiator'`)
45
+ and `approverScope`, so a gate can require four-eyes sign-off, restrict itself
46
+ to the run's initiator, or require the decider to hold a named scope.
47
+
48
+ Both are enforced when the workflow replays the gate — the same place, and for
49
+ the same reason, the decision payload is validated: the policy is a value on
50
+ the workflow, and a decision can be recorded before the run has ever reached
51
+ the gate. A decision that fails the policy is discarded and the gate stays
52
+ closed. Where the run has already published its policy, the check also runs at
53
+ submission time so the caller gets a 403 rather than silence.
54
+
55
+ An answer is now recorded where it can be answered for later. The settled
56
+ decision carries `decidedBy` and `decidedAt` in its `ApprovalOutcome`, so who
57
+ signed reaches `workflowStep.result` and `workflowStepHistory` rather than
58
+ living only in mutable run state. Every answer — accepted, refused at the door,
59
+ or cleared on replay — is also written to the audit sink as
60
+ `workflow.approval.decided`, which outlives the run: `deleteRun` cascades to
61
+ steps and history, and a refused attempt never reaches a step at all. Projects
62
+ with no audit service wired are unaffected.
63
+
64
+ **This loosens the default.** `approveStep` previously refused anyone but the
65
+ run's initiator, unconditionally. A gate that declares no `approvers` now
66
+ accepts a decision from anyone the approve entrypoint admits — restore the old
67
+ behaviour per-gate with `approvers: 'owner'`, or gate the approve route with
68
+ `auth`/`permissions`. Ownership still governs _reads_ of a run unchanged.
69
+
70
+ - Updated dependencies [063f43a]
71
+ - Updated dependencies [ce66bf8]
72
+ - Updated dependencies [d0307a8]
73
+ - Updated dependencies [ce66bf8]
74
+ - Updated dependencies [3ad2131]
75
+ - Updated dependencies [b930dca]
76
+ - Updated dependencies [b95e77d]
77
+ - Updated dependencies [fd9d834]
78
+ - Updated dependencies [8978fbd]
79
+ - @pikku/core@0.12.82
80
+
81
+ ## 0.12.57
82
+
83
+ ### Patch Changes
84
+
85
+ - e110c55: Wire `pikkuAIScorer` / `pikkuAIJudge` through the inspector and codegen, and let an agent name the scorers that grade it.
86
+
87
+ The inspector reads a scorer's lane off which constructor was called rather than
88
+ off a field, so the two lanes cannot disagree with the code that produced them,
89
+ and refuses a scorer with no name or description — the meta is the only thing
90
+ that names it at runtime. An agent naming a scorer that was never declared is a
91
+ build error (`PKU155`) rather than an agent that quietly grades nothing forever.
92
+
93
+ Codegen emits a `ScorerName` union, so `scorers` on an agent is checked against
94
+ the scorers the project actually declares, plus the scorer wirings and meta.
95
+ `pikku validate` now also flags a scorer declared outside a `*.scorer.ts` file,
96
+ for the same reason scenarios have to live in files named for them: a rubric
97
+ buried in an agent definition is one nobody reviews as a rubric.
98
+
99
+ - e110c55: Add `scenario.expectScore` — grade a finished agent run with a declared scorer and assert on it.
100
+
101
+ An agent's answer cannot be matched against a fixed string, so a scenario grades
102
+ it instead. `expectScore(step, runId, scorer, { atLeast, atMost, reference })`
103
+ runs one declared scorer against the run the scenario just triggered and fails
104
+ with the reason the judge gave. The default bound is `atLeast: 0.5`, so an
105
+ unqualified assertion still fails a run graded zero.
106
+
107
+ Grading goes over the new `pikkuScenarioGradeRun` instrumentation RPC, which the
108
+ dev server registers alongside the coverage and stub RPCs — so it exists only in
109
+ processes that should have it, and never in a deployed bundle. It grades from
110
+ the snapshot the runtime already took when the run finished, which is what makes
111
+ a scenario's grade the same measurement production's sampler makes rather than
112
+ an approximation of it: a run's prompt, answer and tool calls are spread across
113
+ a thread's messages, where the boundary of one run is not recoverable.
114
+
115
+ Two things differ deliberately from live scoring. The sample rate is ignored — a
116
+ scorer grading 1% of traffic still grades every scenario run — and the grade is
117
+ returned rather than recorded, so a test's score never lands among the
118
+ production figures. `reference` supplies the answer key a `requiresReference`
119
+ judge grades against, which is the only way such a judge is reachable at all.
120
+
121
+ - Updated dependencies [e110c55]
122
+ - Updated dependencies [e110c55]
123
+ - Updated dependencies [e110c55]
124
+ - Updated dependencies [acc8077]
125
+ - Updated dependencies [905f737]
126
+ - Updated dependencies [3cc6428]
127
+ - Updated dependencies [c524adf]
128
+ - Updated dependencies [e110c55]
129
+ - @pikku/core@0.12.81
130
+
131
+ ## 0.12.56
132
+
133
+ ### Patch Changes
134
+
135
+ - 7e60867: Delete exports nothing references
136
+
137
+ A sweep of the `@pikku/core` surface for exports with no consumer anywhere in the repo — no package, template, verifier or e2e project imports them: `ExtractFunctionOutput`, `CLICommandDefinition`, `RequestHeaders`, `HTTPFunctionsMeta`, `HTTPWiringMiddleware`, `JsonRpcError`, `TriggerSourceInfo`, `getMCPResources`, `getMCPPrompts`, `onGraphNodeComplete` and `InputRef`.
138
+
139
+ Every one was a compatibility promise with nothing on the other end of it. Removing them narrows what 0.13 has to keep stable.
140
+
141
+ `isRef` looked like the twelfth, and isn't. It is the type guard that reads what `createRef` writes — the `__isRef` brand marking a graph node input as "substitute another node's output here". Nothing imported it because neither it nor `RefValue` was reachable from any entry point, so the one consumer that needed it, the inspector's graph serializer, had reimplemented the same four conditions privately as `isRefValue` along with its own structural copy of `RefValue`. Deleting `isRef` would have made that duplicate permanent, with the brand's shape asserted in two places free to drift apart.
142
+
143
+ So `isRef` and `RefValue` are exported from `@pikku/core/workflow` instead, and the inspector imports them rather than keeping its own copy.
144
+
145
+ - a879ab3: **Codegen reports a schema it named but never generated (PKU463).** A named
146
+ contract type that is declared but not exported is imported by the virtual
147
+ source file the schema generator compiles, resolves to nothing, and yields no
148
+ schema — while the function meta still carries the name. `pikku all` exited 0
149
+ and the first call to that function failed in a deployment with
150
+ `MissingSchemaError`. The reference is now checked once addon schemas are
151
+ merged, since an addon supplies its own and checking earlier would report every
152
+ one of them as unresolved.
153
+ - Updated dependencies [41c1a95]
154
+ - Updated dependencies [ce96383]
155
+ - Updated dependencies [7e60867]
156
+ - Updated dependencies [f8f1244]
157
+ - Updated dependencies [dcf20cb]
158
+ - Updated dependencies [6512384]
159
+ - Updated dependencies [e3b4c14]
160
+ - Updated dependencies [efd0ed1]
161
+ - Updated dependencies [cba98fb]
162
+ - Updated dependencies [ce96383]
163
+ - Updated dependencies [f8f1244]
164
+ - Updated dependencies [f8f1244]
165
+ - Updated dependencies [6e93a35]
166
+ - Updated dependencies [6dada45]
167
+ - @pikku/core@0.12.80
168
+
1
169
  ## 0.12.55
2
170
 
3
171
  ### Patch Changes
@@ -217,6 +217,29 @@ function resolveIdentifierToAgentName(identifier, checker) {
217
217
  }
218
218
  return null;
219
219
  }
220
+ /**
221
+ * The scorers an agent asks to be graded by, which are plain names rather than
222
+ * references: a scorer is resolved out of the registry at grading time, not
223
+ * imported by the agent, so nothing here has a declaration to chase.
224
+ */
225
+ function resolveScorerNames(obj, agentName, logger) {
226
+ const property = obj.properties.find((p) => ts.isPropertyAssignment(p) &&
227
+ ts.isIdentifier(p.name) &&
228
+ p.name.text === 'scorers');
229
+ if (!property || !ts.isPropertyAssignment(property))
230
+ return null;
231
+ if (!ts.isArrayLiteralExpression(property.initializer))
232
+ return null;
233
+ const names = [];
234
+ for (const element of property.initializer.elements) {
235
+ if (!ts.isStringLiteral(element)) {
236
+ logger.critical(ErrorCode.INVALID_VALUE, `AI agent '${agentName}' lists a scorer that is not an inline string: \`${element.getText()}\`. A scorer is named, not imported, so the name has to be readable from source.`);
237
+ continue;
238
+ }
239
+ names.push(element.text);
240
+ }
241
+ return names.length > 0 ? names : null;
242
+ }
220
243
  export const addAIAgent = (logger, node, checker, state, options) => {
221
244
  if (!ts.isCallExpression(node)) {
222
245
  return;
@@ -244,6 +267,7 @@ export const addAIAgent = (logger, node, checker, state, options) => {
244
267
  const maxStepsValue = getPropertyValue(obj, 'maxSteps');
245
268
  const temperatureValue = getPropertyValue(obj, 'temperature');
246
269
  const toolChoiceValue = getPropertyValue(obj, 'toolChoice');
270
+ const scorersValue = resolveScorerNames(obj, nameValue || '', logger);
247
271
  const toolsValue = resolveToolReferences(obj, checker, nameValue || '', logger);
248
272
  if (toolsValue) {
249
273
  for (const toolName of toolsValue) {
@@ -389,6 +413,7 @@ export const addAIAgent = (logger, node, checker, state, options) => {
389
413
  ...(toolsValue !== null && { tools: toolsValue }),
390
414
  ...(agentsValue !== null && { agents: agentsValue }),
391
415
  ...(workflowsValue !== null && { workflows: workflowsValue }),
416
+ ...(scorersValue !== null && { scorers: scorersValue }),
392
417
  tags,
393
418
  inputSchema,
394
419
  outputSchema,
@@ -0,0 +1,2 @@
1
+ import type { AddWiring } from '../types.js';
2
+ export declare const addAIScorer: AddWiring;
@@ -0,0 +1,61 @@
1
+ import * as ts from 'typescript';
2
+ import { getPropertyValue } from '../utils/get-property-value.js';
3
+ import { extractFunctionName } from '../utils/extract-function-name.js';
4
+ import { ErrorCode } from '../error-codes.js';
5
+ /**
6
+ * `pikkuAIScorer` grades in code and `pikkuAIJudge` grades with a model, which
7
+ * is the whole difference between the two lanes — so the lane is read off which
8
+ * constructor was called rather than from a field an author could contradict.
9
+ */
10
+ const LANES = {
11
+ pikkuAIScorer: 'fast',
12
+ pikkuAIJudge: 'slow',
13
+ };
14
+ export const addAIScorer = (logger, node, checker, state) => {
15
+ if (!ts.isCallExpression(node))
16
+ return;
17
+ const expression = node.expression;
18
+ if (!ts.isIdentifier(expression))
19
+ return;
20
+ const lane = LANES[expression.text];
21
+ if (!lane)
22
+ return;
23
+ const firstArg = node.arguments[0];
24
+ if (!firstArg || !ts.isObjectLiteralExpression(firstArg))
25
+ return;
26
+ const obj = firstArg;
27
+ const { exportedName } = extractFunctionName(node, checker, state.rootDir);
28
+ const nameValue = getPropertyValue(obj, 'name');
29
+ if (!nameValue) {
30
+ logger.critical(ErrorCode.MISSING_NAME, "AI scorer is missing the required 'name' property.");
31
+ return;
32
+ }
33
+ const description = getPropertyValue(obj, 'description');
34
+ if (!description) {
35
+ logger.critical(ErrorCode.MISSING_DESCRIPTION, `AI scorer '${nameValue}' is missing the required 'description' property.`);
36
+ return;
37
+ }
38
+ const sampleRateValue = getPropertyValue(obj, 'sampleRate');
39
+ if (sampleRateValue !== null &&
40
+ (sampleRateValue < 0 || sampleRateValue > 1)) {
41
+ logger.critical(ErrorCode.INVALID_VALUE, `AI scorer '${nameValue}' has a sampleRate of ${sampleRateValue} — it is the fraction of live runs to grade, so it must be between 0 and 1.`);
42
+ return;
43
+ }
44
+ const requiresReference = getPropertyValue(obj, 'requiresReference') ?? false;
45
+ const scorerKey = exportedName || nameValue;
46
+ if (exportedName) {
47
+ state.scorers.files.set(scorerKey, {
48
+ path: node.getSourceFile().fileName,
49
+ exportedName,
50
+ });
51
+ }
52
+ state.scorers.scorersMeta[scorerKey] = {
53
+ name: nameValue,
54
+ description,
55
+ lane,
56
+ sampleRate: sampleRateValue ?? 1,
57
+ requiresReference,
58
+ sourceFile: node.getSourceFile().fileName,
59
+ exportedName: exportedName || undefined,
60
+ };
61
+ };
@@ -53,6 +53,8 @@ export function addWireAddon(node, state, logger) {
53
53
  let secretOverrides;
54
54
  let variableOverrides;
55
55
  let credentialOverrides;
56
+ let globalSecrets;
57
+ let globalCredentials;
56
58
  for (const prop of firstArg.properties) {
57
59
  if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name))
58
60
  continue;
@@ -94,6 +96,18 @@ export function addWireAddon(node, state, logger) {
94
96
  ts.isObjectLiteralExpression(prop.initializer)) {
95
97
  credentialOverrides = parseStringRecord(prop.initializer);
96
98
  }
99
+ else if (key === 'globalSecrets') {
100
+ // The grant is real at runtime whatever the reason evaluates to, so a
101
+ // non-literal is recorded as its source text rather than dropped.
102
+ globalSecrets = ts.isStringLiteral(prop.initializer)
103
+ ? prop.initializer.text
104
+ : prop.initializer.getText();
105
+ }
106
+ else if (key === 'globalCredentials') {
107
+ globalCredentials = ts.isStringLiteral(prop.initializer)
108
+ ? prop.initializer.text
109
+ : prop.initializer.getText();
110
+ }
97
111
  }
98
112
  if (!name || !pkg)
99
113
  return;
@@ -108,6 +122,8 @@ export function addWireAddon(node, state, logger) {
108
122
  secretOverrides,
109
123
  variableOverrides,
110
124
  credentialOverrides,
125
+ globalSecrets,
126
+ globalCredentials,
111
127
  });
112
128
  state.rpc.usedAddons.add(name);
113
129
  state.rpc.wireAddonFiles.add(node.getSourceFile().fileName);
@@ -13,8 +13,13 @@
13
13
  * `pikku dev` registers the implementations itself. Ignoring by name (rather
14
14
  * than deleting the scaffold and requiring every project to regenerate) is what
15
15
  * keeps an existing project's checked-in `scenarios.gen.ts` from deploying.
16
+ *
17
+ * `pikkuScenarioGradeRun` was never scaffolded — it arrived after the split —
18
+ * but it belongs here for the same reason: grading a run on demand, ignoring the
19
+ * sample rate that governs live traffic, is a thing you do to a development
20
+ * server, and the name is reserved so an application cannot claim it.
16
21
  */
17
- export declare const SCENARIO_INSTRUMENTATION_FUNCTIONS: readonly ["pikkuScenarioTakeLiveCoverage", "pikkuScenarioResetLiveCoverage", "pikkuScenarioResetStubs", "pikkuScenarioGetStubCalls"];
22
+ export declare const SCENARIO_INSTRUMENTATION_FUNCTIONS: readonly ["pikkuScenarioTakeLiveCoverage", "pikkuScenarioResetLiveCoverage", "pikkuScenarioResetStubs", "pikkuScenarioGetStubCalls", "pikkuScenarioGradeRun"];
18
23
  export type ScenarioInstrumentationFunction = (typeof SCENARIO_INSTRUMENTATION_FUNCTIONS)[number];
19
24
  /**
20
25
  * True for a scenario instrumentation function, by name. Versioned ids
@@ -13,12 +13,18 @@
13
13
  * `pikku dev` registers the implementations itself. Ignoring by name (rather
14
14
  * than deleting the scaffold and requiring every project to regenerate) is what
15
15
  * keeps an existing project's checked-in `scenarios.gen.ts` from deploying.
16
+ *
17
+ * `pikkuScenarioGradeRun` was never scaffolded — it arrived after the split —
18
+ * but it belongs here for the same reason: grading a run on demand, ignoring the
19
+ * sample rate that governs live traffic, is a thing you do to a development
20
+ * server, and the name is reserved so an application cannot claim it.
16
21
  */
17
22
  export const SCENARIO_INSTRUMENTATION_FUNCTIONS = [
18
23
  'pikkuScenarioTakeLiveCoverage',
19
24
  'pikkuScenarioResetLiveCoverage',
20
25
  'pikkuScenarioResetStubs',
21
26
  'pikkuScenarioGetStubCalls',
27
+ 'pikkuScenarioGradeRun',
22
28
  ];
23
29
  const names = new Set(SCENARIO_INSTRUMENTATION_FUNCTIONS);
24
30
  /**
@@ -37,6 +37,7 @@ export declare enum ErrorCode {
37
37
  DB_FORM_ON_NON_STRING = "PKU484",
38
38
  DB_DEFAULT_SCHEMA_NAME_COLLISION = "PKU485",
39
39
  SCHEMA_GENERATION_ERROR = "PKU456",
40
+ SCHEMA_REFERENCE_UNRESOLVED = "PKU463",
40
41
  INLINE_SCHEMA = "PKU489",
41
42
  FUNCTION_METADATA_NOT_FOUND = "PKU559",
42
43
  DUPLICATE_AUTH_DEFINITION = "PKU581",
@@ -61,6 +62,7 @@ export declare enum ErrorCode {
61
62
  AGENT_TOOL_UNKNOWN_NAMESPACE = "PKU152",
62
63
  AGENT_TOOL_NOT_FOUND = "PKU153",
63
64
  AGENT_TOOL_MISSING_DESCRIPTION = "PKU154",
65
+ AGENT_SCORER_NOT_FOUND = "PKU155",
64
66
  SCHEMA_AND_WIRING_COLOCATED = "PKU490",
65
67
  SERVICES_NOT_DESTRUCTURED = "PKU410",
66
68
  WIRES_NOT_DESTRUCTURED = "PKU411",
@@ -42,6 +42,7 @@ export var ErrorCode;
42
42
  ErrorCode["DB_DEFAULT_SCHEMA_NAME_COLLISION"] = "PKU485";
43
43
  // Configuration errors
44
44
  ErrorCode["SCHEMA_GENERATION_ERROR"] = "PKU456";
45
+ ErrorCode["SCHEMA_REFERENCE_UNRESOLVED"] = "PKU463";
45
46
  ErrorCode["INLINE_SCHEMA"] = "PKU489";
46
47
  // Function errors
47
48
  ErrorCode["FUNCTION_METADATA_NOT_FOUND"] = "PKU559";
@@ -78,6 +79,8 @@ export var ErrorCode;
78
79
  ErrorCode["AGENT_TOOL_UNKNOWN_NAMESPACE"] = "PKU152";
79
80
  ErrorCode["AGENT_TOOL_NOT_FOUND"] = "PKU153";
80
81
  ErrorCode["AGENT_TOOL_MISSING_DESCRIPTION"] = "PKU154";
82
+ // Agent scorer reference errors
83
+ ErrorCode["AGENT_SCORER_NOT_FOUND"] = "PKU155";
81
84
  // File structure errors
82
85
  ErrorCode["SCHEMA_AND_WIRING_COLOCATED"] = "PKU490";
83
86
  // Optimization diagnostics
package/dist/inspector.js CHANGED
@@ -6,12 +6,12 @@ import { TypesMap } from './types-map.js';
6
6
  import { getFilesAndMethods } from './utils/get-files-and-methods.js';
7
7
  import { findCommonAncestor } from './utils/find-root-dir.js';
8
8
  import { createNestedProjectFilter } from './utils/nested-project-filter.js';
9
- import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, computeSecretBrokers, validateNoSecretAliasServices, validateSecretUsage, computeDiagnostics, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
9
+ import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateAgentScorerReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, validateNoSecretAliasServices, validateSecretUsage, computeDiagnostics, validateSchemaReferences, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
10
10
  import { validateExposedFunctionsGated } from './utils/validate-exposed-functions-gated.js';
11
11
  import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js';
12
12
  import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js';
13
13
  import { generateOpenAPISpec } from './utils/serialize-openapi-json.js';
14
- import { pikkuState } from '@pikku/core/internal';
14
+ import { pikkuState } from '@pikku/core/ecosystem';
15
15
  import { resolveLatestVersions } from './utils/resolve-versions.js';
16
16
  import { finalizeWorkflows } from './utils/workflow/graph/finalize-workflows.js';
17
17
  import { finalizeWorkflowHelperTypes, finalizeWorkflowWires, } from './utils/workflow/graph/finalize-workflow-wires.js';
@@ -116,6 +116,10 @@ export function getInitialInspectorState(rootDir) {
116
116
  agentsMeta: {},
117
117
  files: new Map(),
118
118
  },
119
+ scorers: {
120
+ scorersMeta: {},
121
+ files: new Map(),
122
+ },
119
123
  cli: {
120
124
  meta: {
121
125
  programs: {},
@@ -336,6 +340,11 @@ export const inspect = async (logger, routeFiles, options = {}) => {
336
340
  }
337
341
  // Re-load addon schemas (generateAllSchemas replaces state.schemas)
338
342
  await loadAddonSchemas(logger, state);
343
+ // After the addon merge, not before: an addon supplies its own schemas here,
344
+ // and checking earlier would report every one of them as unresolved.
345
+ if (options.schemaConfig) {
346
+ validateSchemaReferences(logger, state);
347
+ }
339
348
  state.manifest.initial = options.manifest ?? null;
340
349
  const contracts = extractContractsFromMeta(state.functions.meta);
341
350
  const baseManifest = state.manifest.initial ?? createEmptyManifest();
@@ -361,7 +370,6 @@ export const inspect = async (logger, routeFiles, options = {}) => {
361
370
  computeResolvedIOTypes(state);
362
371
  computeMiddlewareGroupsMeta(state);
363
372
  computePermissionsGroupsMeta(state);
364
- computeSecretBrokers(state);
365
373
  validateNoSecretAliasServices(logger, checker, state);
366
374
  validateSecretUsage(logger, state);
367
375
  computeDiagnostics(state);
@@ -379,8 +387,9 @@ export const inspect = async (logger, routeFiles, options = {}) => {
379
387
  if (options.openAPI) {
380
388
  state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
381
389
  }
382
- validateAgentModels(logger, state);
390
+ validateAgentModels(logger, state, options.modelAliases);
383
391
  validateAgentToolReferences(logger, state, options);
392
+ validateAgentScorerReferences(logger, state);
384
393
  validateSecretOverrides(logger, state);
385
394
  validateVariableOverrides(logger, state);
386
395
  validateCredentialOverrides(logger, state);
package/dist/types.d.ts CHANGED
@@ -9,6 +9,7 @@ import type { QueueWorkersMeta } from '@pikku/core/queue';
9
9
  import type { WorkflowsMeta } from '@pikku/core/workflow';
10
10
  import type { MCPResourceMeta, MCPToolMeta, MCPPromptMeta } from '@pikku/core/mcp';
11
11
  import type { AIAgentMeta } from '@pikku/core/ai-agent';
12
+ import type { ScorerMeta } from '@pikku/core/ai-scorer';
12
13
  import type { CLIMeta } from '@pikku/core/cli';
13
14
  import type { CLICommandMeta } from '@pikku/core/cli';
14
15
  import type { NodesMeta } from '@pikku/core/node';
@@ -229,6 +230,8 @@ export type InspectorOptions = Partial<{
229
230
  * tool's own name, so this is a quality gate rather than a correctness one.
230
231
  */
231
232
  strictMeta: boolean;
233
+ /** The `models` table from pikku.config.json, alias -> `provider/model`. */
234
+ modelAliases: Record<string, string>;
232
235
  sourceFile: ts.SourceFile;
233
236
  /**
234
237
  * The project's tsconfig, read for its path mappings only. The inspector's
@@ -518,6 +521,16 @@ export interface InspectorState {
518
521
  secretOverrides?: Record<string, string>;
519
522
  variableOverrides?: Record<string, string>;
520
523
  credentialOverrides?: Record<string, string>;
524
+ /**
525
+ * The app's stated reason for handing this instance the whole
526
+ * `SecretService` instead of one scoped to its declared secrets.
527
+ */
528
+ globalSecrets?: string;
529
+ /**
530
+ * The app's stated reason for handing this instance the whole
531
+ * `CredentialService` instead of one scoped to its declared credentials.
532
+ */
533
+ globalCredentials?: string;
521
534
  }>;
522
535
  wireAddonFiles: Set<string>;
523
536
  };
@@ -534,6 +547,13 @@ export interface InspectorState {
534
547
  exportedName: string;
535
548
  }>;
536
549
  };
550
+ scorers: {
551
+ scorersMeta: ScorerMeta;
552
+ files: Map<string, {
553
+ path: string;
554
+ exportedName: string;
555
+ }>;
556
+ };
537
557
  cli: {
538
558
  meta: CLIMeta;
539
559
  files: Set<string>;
@@ -328,6 +328,14 @@ export function filterInspectorState(state, filters, logger) {
328
328
  agentsMeta: JSON.parse(JSON.stringify(state.agents?.agentsMeta ?? {})),
329
329
  files: new Map(),
330
330
  },
331
+ // Scorers carry through every filter. They are named by the agents that
332
+ // survive it, and a deployment that quietly stopped grading would look
333
+ // exactly like one whose scorers all returned nothing.
334
+ scorers: {
335
+ ...state.scorers,
336
+ scorersMeta: JSON.parse(JSON.stringify(state.scorers?.scorersMeta ?? {})),
337
+ files: new Map(state.scorers?.files ?? []),
338
+ },
331
339
  rpc: {
332
340
  ...state.rpc,
333
341
  internalMeta: { ...state.rpc.internalMeta }, // Clone to avoid mutating original
@@ -50,7 +50,26 @@ export declare function computeResolvedIOTypes(state: InspectorState): void;
50
50
  export declare function computeMiddlewareGroupsMeta(state: InspectorState): void;
51
51
  export declare function computePermissionsGroupsMeta(state: InspectorState): void;
52
52
  export declare function computeRequiredSchemas(state: InspectorState, options: InspectorOptions): void;
53
- export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
53
+ /**
54
+ * Every schema a function's contract names has to exist once codegen is done.
55
+ *
56
+ * The way it stops existing is quiet. `generateCustomTypes` hands the schema
57
+ * generator a virtual source file that *imports* each named contract type, so a
58
+ * type that is declared but never exported resolves to nothing and yields no
59
+ * schema — while the function's meta still carries the name. Nothing downstream
60
+ * reads that name until the first call, which fails with a runtime
61
+ * `MissingSchemaError` in a deployment rather than in the build that caused it.
62
+ *
63
+ * Reported rather than critical: the reference is only *guaranteed* to break
64
+ * the functions that are actually invoked, so this blocks CI through
65
+ * `--fail-on-error` without failing a dev server over a function nobody calls.
66
+ */
67
+ export declare function validateSchemaReferences(logger: InspectorLogger, state: InspectorState): void;
68
+ /**
69
+ * A model is either concrete (`provider/model`) or an alias, which must appear
70
+ * in the `models` table from pikku.config.json.
71
+ */
72
+ export declare function validateAgentModels(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, modelAliases?: Record<string, string>): void;
54
73
  /**
55
74
  * Scenarios are pure stories of remote RPCs (same rule as client-side CLI
56
75
  * renderers): the func may only destructure logger/config — everything else
@@ -96,8 +115,6 @@ export declare function validateWorkflowGraphAddons(logger: InspectorLogger, sta
96
115
  * because the pikku state metadata doesn't exist in the CLI context.
97
116
  */
98
117
  export declare function validateSchemaWiringSeparation(logger: InspectorLogger, state: InspectorState): void;
99
- /** Marks the functions that keep the full `SecretService`. */
100
- export declare function computeSecretBrokers(state: InspectorState): void;
101
118
  /**
102
119
  * Refuses a singleton service that is a `SecretService` under another name.
103
120
  *
@@ -141,6 +158,14 @@ export declare function validateScopeReferences(logger: InspectorLogger, state:
141
158
  * a title labels a tool in a UI, it does not tell a model when to reach for it.
142
159
  */
143
160
  export declare function validateAgentToolReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>, options?: InspectorOptions): void;
161
+ /**
162
+ * Validates that every scorer an agent asks to be graded by exists.
163
+ *
164
+ * The runtime warns and skips an unresolvable name rather than failing a run
165
+ * that has already answered the user — which means a typo would otherwise cost
166
+ * nothing visible and simply grade nothing, forever.
167
+ */
168
+ export declare function validateAgentScorerReferences(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
144
169
  /**
145
170
  * Validates that every scope granted by a system role is declared via
146
171
  * `defineScope`. Runs after all visitors, so declaration order does not matter.