@pikku/inspector 0.12.56 → 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.
- package/CHANGELOG.md +130 -0
- package/dist/add/add-ai-agent.js +25 -0
- package/dist/add/add-ai-scorer.d.ts +2 -0
- package/dist/add/add-ai-scorer.js +61 -0
- package/dist/add/add-wire-addon.js +16 -0
- package/dist/add/scenario-instrumentation.d.ts +6 -1
- package/dist/add/scenario-instrumentation.js +6 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +2 -0
- package/dist/inspector.js +7 -3
- package/dist/types.d.ts +20 -0
- package/dist/utils/filter-inspector-state.js +8 -0
- package/dist/utils/post-process.d.ts +13 -3
- package/dist/utils/post-process.js +33 -15
- package/dist/utils/serialize-inspector-state.d.ts +9 -0
- package/dist/utils/serialize-inspector-state.js +8 -0
- package/dist/utils/workflow/dsl/extract-dsl-workflow.js +13 -4
- package/dist/utils/workflow/dsl/patterns.d.ts +2 -1
- package/dist/utils/workflow/dsl/patterns.js +8 -5
- package/dist/visit.js +2 -0
- package/package.json +3 -3
- package/src/add/add-ai-agent.ts +35 -0
- package/src/add/add-ai-scorer.test.ts +197 -0
- package/src/add/add-ai-scorer.ts +83 -0
- package/src/add/add-scenario-step.test.ts +19 -0
- package/src/add/add-wire-addon.test.ts +43 -0
- package/src/add/add-wire-addon.ts +14 -0
- package/src/add/scenario-instrumentation.ts +6 -0
- package/src/error-codes.ts +3 -0
- package/src/inspector.ts +7 -3
- package/src/types.ts +17 -0
- package/src/utils/filter-inspector-state.ts +8 -0
- package/src/utils/post-process.test.ts +71 -0
- package/src/utils/post-process.ts +41 -16
- package/src/utils/serialize-inspector-state.ts +14 -0
- package/src/utils/workflow/dsl/extract-approval-policy.test.ts +68 -0
- package/src/utils/workflow/dsl/extract-dsl-workflow.ts +12 -4
- package/src/utils/workflow/dsl/patterns.ts +8 -5
- package/src/visit.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/src/utils/secret-brokers.test.ts +0 -53
- package/src/utils/secret-brokers.ts +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,133 @@
|
|
|
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
|
+
|
|
1
131
|
## 0.12.56
|
|
2
132
|
|
|
3
133
|
### Patch Changes
|
package/dist/add/add-ai-agent.js
CHANGED
|
@@ -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,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
|
/**
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export declare enum ErrorCode {
|
|
|
62
62
|
AGENT_TOOL_UNKNOWN_NAMESPACE = "PKU152",
|
|
63
63
|
AGENT_TOOL_NOT_FOUND = "PKU153",
|
|
64
64
|
AGENT_TOOL_MISSING_DESCRIPTION = "PKU154",
|
|
65
|
+
AGENT_SCORER_NOT_FOUND = "PKU155",
|
|
65
66
|
SCHEMA_AND_WIRING_COLOCATED = "PKU490",
|
|
66
67
|
SERVICES_NOT_DESTRUCTURED = "PKU410",
|
|
67
68
|
WIRES_NOT_DESTRUCTURED = "PKU411",
|
package/dist/error-codes.js
CHANGED
|
@@ -79,6 +79,8 @@ export var ErrorCode;
|
|
|
79
79
|
ErrorCode["AGENT_TOOL_UNKNOWN_NAMESPACE"] = "PKU152";
|
|
80
80
|
ErrorCode["AGENT_TOOL_NOT_FOUND"] = "PKU153";
|
|
81
81
|
ErrorCode["AGENT_TOOL_MISSING_DESCRIPTION"] = "PKU154";
|
|
82
|
+
// Agent scorer reference errors
|
|
83
|
+
ErrorCode["AGENT_SCORER_NOT_FOUND"] = "PKU155";
|
|
82
84
|
// File structure errors
|
|
83
85
|
ErrorCode["SCHEMA_AND_WIRING_COLOCATED"] = "PKU490";
|
|
84
86
|
// Optimization diagnostics
|
package/dist/inspector.js
CHANGED
|
@@ -6,7 +6,7 @@ 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,
|
|
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';
|
|
@@ -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: {},
|
|
@@ -366,7 +370,6 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
366
370
|
computeResolvedIOTypes(state);
|
|
367
371
|
computeMiddlewareGroupsMeta(state);
|
|
368
372
|
computePermissionsGroupsMeta(state);
|
|
369
|
-
computeSecretBrokers(state);
|
|
370
373
|
validateNoSecretAliasServices(logger, checker, state);
|
|
371
374
|
validateSecretUsage(logger, state);
|
|
372
375
|
computeDiagnostics(state);
|
|
@@ -384,8 +387,9 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
384
387
|
if (options.openAPI) {
|
|
385
388
|
state.openAPISpec = await generateOpenAPISpec(logger, state.functions.meta, state.http.meta, state.schemas, options.openAPI.additionalInfo, pikkuState(null, 'misc', 'errors'));
|
|
386
389
|
}
|
|
387
|
-
validateAgentModels(logger, state);
|
|
390
|
+
validateAgentModels(logger, state, options.modelAliases);
|
|
388
391
|
validateAgentToolReferences(logger, state, options);
|
|
392
|
+
validateAgentScorerReferences(logger, state);
|
|
389
393
|
validateSecretOverrides(logger, state);
|
|
390
394
|
validateVariableOverrides(logger, state);
|
|
391
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
|
|
@@ -65,7 +65,11 @@ export declare function computeRequiredSchemas(state: InspectorState, options: I
|
|
|
65
65
|
* `--fail-on-error` without failing a dev server over a function nobody calls.
|
|
66
66
|
*/
|
|
67
67
|
export declare function validateSchemaReferences(logger: InspectorLogger, state: InspectorState): void;
|
|
68
|
-
|
|
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;
|
|
69
73
|
/**
|
|
70
74
|
* Scenarios are pure stories of remote RPCs (same rule as client-side CLI
|
|
71
75
|
* renderers): the func may only destructure logger/config — everything else
|
|
@@ -111,8 +115,6 @@ export declare function validateWorkflowGraphAddons(logger: InspectorLogger, sta
|
|
|
111
115
|
* because the pikku state metadata doesn't exist in the CLI context.
|
|
112
116
|
*/
|
|
113
117
|
export declare function validateSchemaWiringSeparation(logger: InspectorLogger, state: InspectorState): void;
|
|
114
|
-
/** Marks the functions that keep the full `SecretService`. */
|
|
115
|
-
export declare function computeSecretBrokers(state: InspectorState): void;
|
|
116
118
|
/**
|
|
117
119
|
* Refuses a singleton service that is a `SecretService` under another name.
|
|
118
120
|
*
|
|
@@ -156,6 +158,14 @@ export declare function validateScopeReferences(logger: InspectorLogger, state:
|
|
|
156
158
|
* a title labels a tool in a UI, it does not tell a model when to reach for it.
|
|
157
159
|
*/
|
|
158
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;
|
|
159
169
|
/**
|
|
160
170
|
* Validates that every scope granted by a system role is declared via
|
|
161
171
|
* `defineScope`. Runs after all visitors, so declaration order does not matter.
|
|
@@ -2,7 +2,6 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { extractTypeKeys } from './type-utils.js';
|
|
4
4
|
import { ErrorCode } from '../error-codes.js';
|
|
5
|
-
import { isSecretBrokerFunction } from './secret-brokers.js';
|
|
6
5
|
import { findSecretAliasServices } from './secret-alias-services.js';
|
|
7
6
|
import { relative } from 'node:path';
|
|
8
7
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js';
|
|
@@ -606,15 +605,26 @@ export function validateSchemaReferences(logger, state) {
|
|
|
606
605
|
`is declared but not exported — export it, or inline the type at the call site.`,
|
|
607
606
|
});
|
|
608
607
|
}
|
|
609
|
-
|
|
608
|
+
/**
|
|
609
|
+
* A model is either concrete (`provider/model`) or an alias, which must appear
|
|
610
|
+
* in the `models` table from pikku.config.json.
|
|
611
|
+
*/
|
|
612
|
+
export function validateAgentModels(logger, state, modelAliases = {}) {
|
|
610
613
|
for (const [, meta] of Object.entries(state.agents.agentsMeta)) {
|
|
611
614
|
const model = meta.model;
|
|
612
615
|
if (!model) {
|
|
613
616
|
logger.critical(ErrorCode.MISSING_MODEL, `AI agent '${meta.name}' is missing the 'model' property.`);
|
|
614
617
|
continue;
|
|
615
618
|
}
|
|
616
|
-
|
|
617
|
-
|
|
619
|
+
// `hasOwn`, not a truthy lookup: every object literal inherits `toString`,
|
|
620
|
+
// `constructor` and `__proto__`, so a bare model named after one of them
|
|
621
|
+
// would read as a configured alias and pass a check nothing else repeats.
|
|
622
|
+
if (!model.includes('/') && !Object.hasOwn(modelAliases, model)) {
|
|
623
|
+
const known = Object.keys(modelAliases).sort();
|
|
624
|
+
logger.critical(ErrorCode.INVALID_MODEL, `AI agent '${meta.name}' uses model '${model}', which is neither provider-qualified as '<provider>/<model>' (e.g. 'openai/gpt-4') nor an alias in the "models" table of pikku.config.json. ` +
|
|
625
|
+
(known.length
|
|
626
|
+
? `Known aliases: ${known.join(', ')}.`
|
|
627
|
+
: `No "models" aliases are configured.`));
|
|
618
628
|
}
|
|
619
629
|
}
|
|
620
630
|
}
|
|
@@ -793,14 +803,6 @@ export function validateSchemaWiringSeparation(logger, state) {
|
|
|
793
803
|
}
|
|
794
804
|
}
|
|
795
805
|
}
|
|
796
|
-
/** Marks the functions that keep the full `SecretService`. */
|
|
797
|
-
export function computeSecretBrokers(state) {
|
|
798
|
-
for (const [id, meta] of Object.entries(state.functions.meta)) {
|
|
799
|
-
if (isSecretBrokerFunction(meta.pikkuFuncId ?? id)) {
|
|
800
|
-
meta.secretBroker = true;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
806
|
/**
|
|
805
807
|
* Refuses a singleton service that is a `SecretService` under another name.
|
|
806
808
|
*
|
|
@@ -830,9 +832,6 @@ export function validateNoSecretAliasServices(logger, checker, state) {
|
|
|
830
832
|
`SecretService is confined to pikkuServices, pikkuWireServices, addon service factories and middleware — give a service the secret value when you construct it and expose only what the function needs.`);
|
|
831
833
|
};
|
|
832
834
|
for (const [id, meta] of Object.entries(state.functions.meta)) {
|
|
833
|
-
if (meta.secretBroker) {
|
|
834
|
-
continue;
|
|
835
|
-
}
|
|
836
835
|
report('Function', id, meta.services?.services ?? []);
|
|
837
836
|
}
|
|
838
837
|
for (const [id, def] of Object.entries(state.permissions.definitions)) {
|
|
@@ -1038,6 +1037,25 @@ export function validateAgentToolReferences(logger, state, options = {}) {
|
|
|
1038
1037
|
}
|
|
1039
1038
|
}
|
|
1040
1039
|
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Validates that every scorer an agent asks to be graded by exists.
|
|
1042
|
+
*
|
|
1043
|
+
* The runtime warns and skips an unresolvable name rather than failing a run
|
|
1044
|
+
* that has already answered the user — which means a typo would otherwise cost
|
|
1045
|
+
* nothing visible and simply grade nothing, forever.
|
|
1046
|
+
*/
|
|
1047
|
+
export function validateAgentScorerReferences(logger, state) {
|
|
1048
|
+
const known = Object.keys(state.scorers.scorersMeta);
|
|
1049
|
+
for (const [agentKey, agent] of Object.entries(state.agents.agentsMeta)) {
|
|
1050
|
+
const where = agent.sourceFile ? ` (${agent.sourceFile})` : '';
|
|
1051
|
+
for (const scorer of agent.scorers ?? []) {
|
|
1052
|
+
if (known.includes(scorer))
|
|
1053
|
+
continue;
|
|
1054
|
+
logger.critical(ErrorCode.AGENT_SCORER_NOT_FOUND, `AI agent '${agentKey}'${where} asks to be graded by scorer '${scorer}', which is not declared in this project. ` +
|
|
1055
|
+
`Declared scorers: ${known.join(', ') || 'none'}.`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1041
1059
|
/**
|
|
1042
1060
|
* Validates that every scope granted by a system role is declared via
|
|
1043
1061
|
* `defineScope`. Runs after all visitors, so declaration order does not matter.
|
|
@@ -195,6 +195,8 @@ export interface SerializableInspectorState {
|
|
|
195
195
|
secretOverrides?: Record<string, string>;
|
|
196
196
|
variableOverrides?: Record<string, string>;
|
|
197
197
|
credentialOverrides?: Record<string, string>;
|
|
198
|
+
globalSecrets?: string;
|
|
199
|
+
globalCredentials?: string;
|
|
198
200
|
}
|
|
199
201
|
]>;
|
|
200
202
|
wireAddonFiles: string[];
|
|
@@ -212,6 +214,13 @@ export interface SerializableInspectorState {
|
|
|
212
214
|
exportedName: string;
|
|
213
215
|
}][];
|
|
214
216
|
};
|
|
217
|
+
scorers: {
|
|
218
|
+
scorersMeta: InspectorState['scorers']['scorersMeta'];
|
|
219
|
+
files: [string, {
|
|
220
|
+
path: string;
|
|
221
|
+
exportedName: string;
|
|
222
|
+
}][];
|
|
223
|
+
};
|
|
215
224
|
cli: {
|
|
216
225
|
meta: InspectorState['cli']['meta'];
|
|
217
226
|
files: string[];
|
|
@@ -95,6 +95,10 @@ export function serializeInspectorState(state) {
|
|
|
95
95
|
agentsMeta: state.agents?.agentsMeta ?? {},
|
|
96
96
|
files: Array.from(state.agents?.files?.entries() ?? []),
|
|
97
97
|
},
|
|
98
|
+
scorers: {
|
|
99
|
+
scorersMeta: state.scorers?.scorersMeta ?? {},
|
|
100
|
+
files: Array.from(state.scorers?.files?.entries() ?? []),
|
|
101
|
+
},
|
|
98
102
|
cli: {
|
|
99
103
|
meta: state.cli.meta,
|
|
100
104
|
files: Array.from(state.cli.files),
|
|
@@ -268,6 +272,10 @@ export function deserializeInspectorState(data) {
|
|
|
268
272
|
agentsMeta: data.agents?.agentsMeta || {},
|
|
269
273
|
files: new Map(data.agents?.files || []),
|
|
270
274
|
},
|
|
275
|
+
scorers: {
|
|
276
|
+
scorersMeta: data.scorers?.scorersMeta || {},
|
|
277
|
+
files: new Map(data.scorers?.files || []),
|
|
278
|
+
},
|
|
271
279
|
cli: {
|
|
272
280
|
meta: data.cli.meta,
|
|
273
281
|
files: new Set(data.cli.files),
|
|
@@ -803,19 +803,28 @@ function extractApprovalStep(call, context, outputVar) {
|
|
|
803
803
|
step.outputVar = outputVar;
|
|
804
804
|
}
|
|
805
805
|
// The `schema` option is a runtime value validated inside the workflow body,
|
|
806
|
-
// so it is deliberately not serialized here — only
|
|
807
|
-
// and planned-step ladder need in order to describe the gate.
|
|
806
|
+
// so it is deliberately not serialized here — only the literal options the
|
|
807
|
+
// graph and planned-step ladder need in order to describe the gate.
|
|
808
808
|
const options = args[1];
|
|
809
809
|
if (options && ts.isObjectLiteralExpression(options)) {
|
|
810
810
|
for (const prop of options.properties) {
|
|
811
|
-
if (ts.isPropertyAssignment(prop)
|
|
812
|
-
|
|
811
|
+
if (!ts.isPropertyAssignment(prop))
|
|
812
|
+
continue;
|
|
813
|
+
const name = prop.name.getText();
|
|
814
|
+
if (name === 'expiry' &&
|
|
813
815
|
(ts.isStringLiteral(prop.initializer) ||
|
|
814
816
|
ts.isNumericLiteral(prop.initializer))) {
|
|
815
817
|
step.expiry = ts.isNumericLiteral(prop.initializer)
|
|
816
818
|
? Number(prop.initializer.text)
|
|
817
819
|
: prop.initializer.text;
|
|
818
820
|
}
|
|
821
|
+
if (name === 'approvers' && ts.isStringLiteral(prop.initializer)) {
|
|
822
|
+
step.approvers = prop.initializer
|
|
823
|
+
.text;
|
|
824
|
+
}
|
|
825
|
+
if (name === 'approverScope' && ts.isStringLiteral(prop.initializer)) {
|
|
826
|
+
step.approverScope = prop.initializer.text;
|
|
827
|
+
}
|
|
819
828
|
}
|
|
820
829
|
}
|
|
821
830
|
return step;
|
|
@@ -35,7 +35,8 @@ export declare function isScenarioStepCall(node: ts.CallExpression): boolean;
|
|
|
35
35
|
export declare function getScenarioStepPhase(node: ts.CallExpression): ScenarioStepPhaseName | undefined;
|
|
36
36
|
/**
|
|
37
37
|
* Check if a call expression is one of the scenario expectation helpers —
|
|
38
|
-
* `scenario.expectService()`, `.expectError()
|
|
38
|
+
* `scenario.expectService()`, `.expectError()`, `.expectEventually()` or
|
|
39
|
+
* `.expectScore()`.
|
|
39
40
|
*/
|
|
40
41
|
export declare function isScenarioExpectationCall(node: ts.CallExpression): boolean;
|
|
41
42
|
/**
|