@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
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
validateRemoteAddonDependencies,
|
|
12
12
|
validateRemoteAddonAuth,
|
|
13
13
|
validateAgentToolReferences,
|
|
14
|
+
validateAgentModels,
|
|
14
15
|
} from './post-process.js'
|
|
15
16
|
import { ErrorCode } from '../error-codes.js'
|
|
16
17
|
import type { InspectorState, InspectorLogger } from '../types.js'
|
|
@@ -713,3 +714,73 @@ describe('validateAgentToolReferences (ref() resolved at build time)', () => {
|
|
|
713
714
|
assert.deepEqual(criticals, [])
|
|
714
715
|
})
|
|
715
716
|
})
|
|
717
|
+
|
|
718
|
+
const makeModelState = (
|
|
719
|
+
models: Record<string, string>
|
|
720
|
+
): Omit<InspectorState, 'typesLookup'> =>
|
|
721
|
+
({
|
|
722
|
+
agents: {
|
|
723
|
+
agentsMeta: Object.fromEntries(
|
|
724
|
+
Object.entries(models).map(([name, model]) => [
|
|
725
|
+
name,
|
|
726
|
+
{ name, model, sourceFile: `src/${name}.agent.ts` },
|
|
727
|
+
])
|
|
728
|
+
),
|
|
729
|
+
},
|
|
730
|
+
}) as unknown as Omit<InspectorState, 'typesLookup'>
|
|
731
|
+
|
|
732
|
+
describe('validateAgentModels (aliases resolve against pikku.config.json)', () => {
|
|
733
|
+
test('a provider-qualified model needs no alias', () => {
|
|
734
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
735
|
+
validateAgentModels(logger, makeModelState({ triage: 'openai/gpt-5-mini' }))
|
|
736
|
+
assert.deepEqual(criticals, [])
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
test('a missing model is still reported', () => {
|
|
740
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
741
|
+
validateAgentModels(logger, makeModelState({ triage: '' }))
|
|
742
|
+
assert.equal(criticals.length, 1)
|
|
743
|
+
assert.equal(criticals[0]!.code, ErrorCode.MISSING_MODEL)
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
test('a bare name that the models table defines is accepted', () => {
|
|
747
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
748
|
+
validateAgentModels(logger, makeModelState({ triage: 'cheap' }), {
|
|
749
|
+
cheap: 'openai/gpt-5-mini',
|
|
750
|
+
})
|
|
751
|
+
assert.deepEqual(criticals, [])
|
|
752
|
+
})
|
|
753
|
+
|
|
754
|
+
test('a bare name with no alias is an error naming the ones that exist', () => {
|
|
755
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
756
|
+
validateAgentModels(logger, makeModelState({ triage: 'exspensive' }), {
|
|
757
|
+
cheap: 'openai/gpt-5-mini',
|
|
758
|
+
expensive: 'openai/gpt-5',
|
|
759
|
+
})
|
|
760
|
+
assert.equal(criticals.length, 1)
|
|
761
|
+
assert.equal(criticals[0]!.code, ErrorCode.INVALID_MODEL)
|
|
762
|
+
assert.match(criticals[0]!.message, /cheap, expensive/)
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
test('with no models table a bare name still fails, as it always has', () => {
|
|
766
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
767
|
+
validateAgentModels(logger, makeModelState({ triage: 'gpt-5-mini' }))
|
|
768
|
+
assert.equal(criticals.length, 1)
|
|
769
|
+
assert.equal(criticals[0]!.code, ErrorCode.INVALID_MODEL)
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
for (const inherited of ['toString', 'constructor', '__proto__']) {
|
|
773
|
+
test(`a bare '${inherited}' is not an alias just because Object has one`, () => {
|
|
774
|
+
const { logger, criticals } = makeCriticalLogger()
|
|
775
|
+
validateAgentModels(logger, makeModelState({ triage: inherited }), {
|
|
776
|
+
cheap: 'openai/gpt-5-mini',
|
|
777
|
+
})
|
|
778
|
+
assert.equal(
|
|
779
|
+
criticals.length,
|
|
780
|
+
1,
|
|
781
|
+
'an inherited property name must not read as a configured alias'
|
|
782
|
+
)
|
|
783
|
+
assert.equal(criticals[0]!.code, ErrorCode.INVALID_MODEL)
|
|
784
|
+
})
|
|
785
|
+
}
|
|
786
|
+
})
|
|
@@ -15,7 +15,6 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
15
15
|
import { join } from 'node:path'
|
|
16
16
|
import { extractTypeKeys } from './type-utils.js'
|
|
17
17
|
import { ErrorCode } from '../error-codes.js'
|
|
18
|
-
import { isSecretBrokerFunction } from './secret-brokers.js'
|
|
19
18
|
import { findSecretAliasServices } from './secret-alias-services.js'
|
|
20
19
|
import { relative } from 'node:path'
|
|
21
20
|
import { AUTH_HANDLER_FUNC_ID } from '../add/add-auth.js'
|
|
@@ -746,9 +745,14 @@ export function validateSchemaReferences(
|
|
|
746
745
|
})
|
|
747
746
|
}
|
|
748
747
|
|
|
748
|
+
/**
|
|
749
|
+
* A model is either concrete (`provider/model`) or an alias, which must appear
|
|
750
|
+
* in the `models` table from pikku.config.json.
|
|
751
|
+
*/
|
|
749
752
|
export function validateAgentModels(
|
|
750
753
|
logger: InspectorLogger,
|
|
751
|
-
state: InspectorState | Omit<InspectorState, 'typesLookup'
|
|
754
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>,
|
|
755
|
+
modelAliases: Record<string, string> = {}
|
|
752
756
|
): void {
|
|
753
757
|
for (const [, meta] of Object.entries(state.agents.agentsMeta)) {
|
|
754
758
|
const model = meta.model
|
|
@@ -759,10 +763,17 @@ export function validateAgentModels(
|
|
|
759
763
|
)
|
|
760
764
|
continue
|
|
761
765
|
}
|
|
762
|
-
|
|
766
|
+
// `hasOwn`, not a truthy lookup: every object literal inherits `toString`,
|
|
767
|
+
// `constructor` and `__proto__`, so a bare model named after one of them
|
|
768
|
+
// would read as a configured alias and pass a check nothing else repeats.
|
|
769
|
+
if (!model.includes('/') && !Object.hasOwn(modelAliases, model)) {
|
|
770
|
+
const known = Object.keys(modelAliases).sort()
|
|
763
771
|
logger.critical(
|
|
764
772
|
ErrorCode.INVALID_MODEL,
|
|
765
|
-
`AI agent '${meta.name}' uses model '${model}', which
|
|
773
|
+
`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. ` +
|
|
774
|
+
(known.length
|
|
775
|
+
? `Known aliases: ${known.join(', ')}.`
|
|
776
|
+
: `No "models" aliases are configured.`)
|
|
766
777
|
)
|
|
767
778
|
}
|
|
768
779
|
}
|
|
@@ -982,15 +993,6 @@ export function validateSchemaWiringSeparation(
|
|
|
982
993
|
}
|
|
983
994
|
}
|
|
984
995
|
|
|
985
|
-
/** Marks the functions that keep the full `SecretService`. */
|
|
986
|
-
export function computeSecretBrokers(state: InspectorState): void {
|
|
987
|
-
for (const [id, meta] of Object.entries(state.functions.meta)) {
|
|
988
|
-
if (isSecretBrokerFunction(meta.pikkuFuncId ?? id)) {
|
|
989
|
-
meta.secretBroker = true
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
|
|
994
996
|
/**
|
|
995
997
|
* Refuses a singleton service that is a `SecretService` under another name.
|
|
996
998
|
*
|
|
@@ -1029,9 +1031,6 @@ export function validateNoSecretAliasServices(
|
|
|
1029
1031
|
}
|
|
1030
1032
|
|
|
1031
1033
|
for (const [id, meta] of Object.entries(state.functions.meta)) {
|
|
1032
|
-
if (meta.secretBroker) {
|
|
1033
|
-
continue
|
|
1034
|
-
}
|
|
1035
1034
|
report('Function', id, meta.services?.services ?? [])
|
|
1036
1035
|
}
|
|
1037
1036
|
for (const [id, def] of Object.entries(state.permissions.definitions)) {
|
|
@@ -1284,6 +1283,32 @@ export function validateAgentToolReferences(
|
|
|
1284
1283
|
}
|
|
1285
1284
|
}
|
|
1286
1285
|
|
|
1286
|
+
/**
|
|
1287
|
+
* Validates that every scorer an agent asks to be graded by exists.
|
|
1288
|
+
*
|
|
1289
|
+
* The runtime warns and skips an unresolvable name rather than failing a run
|
|
1290
|
+
* that has already answered the user — which means a typo would otherwise cost
|
|
1291
|
+
* nothing visible and simply grade nothing, forever.
|
|
1292
|
+
*/
|
|
1293
|
+
export function validateAgentScorerReferences(
|
|
1294
|
+
logger: InspectorLogger,
|
|
1295
|
+
state: InspectorState | Omit<InspectorState, 'typesLookup'>
|
|
1296
|
+
): void {
|
|
1297
|
+
const known = Object.keys(state.scorers.scorersMeta)
|
|
1298
|
+
|
|
1299
|
+
for (const [agentKey, agent] of Object.entries(state.agents.agentsMeta)) {
|
|
1300
|
+
const where = agent.sourceFile ? ` (${agent.sourceFile})` : ''
|
|
1301
|
+
for (const scorer of agent.scorers ?? []) {
|
|
1302
|
+
if (known.includes(scorer)) continue
|
|
1303
|
+
logger.critical(
|
|
1304
|
+
ErrorCode.AGENT_SCORER_NOT_FOUND,
|
|
1305
|
+
`AI agent '${agentKey}'${where} asks to be graded by scorer '${scorer}', which is not declared in this project. ` +
|
|
1306
|
+
`Declared scorers: ${known.join(', ') || 'none'}.`
|
|
1307
|
+
)
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1287
1312
|
/**
|
|
1288
1313
|
* Validates that every scope granted by a system role is declared via
|
|
1289
1314
|
* `defineScope`. Runs after all visitors, so declaration order does not matter.
|
|
@@ -170,6 +170,8 @@ export interface SerializableInspectorState {
|
|
|
170
170
|
secretOverrides?: Record<string, string>
|
|
171
171
|
variableOverrides?: Record<string, string>
|
|
172
172
|
credentialOverrides?: Record<string, string>
|
|
173
|
+
globalSecrets?: string
|
|
174
|
+
globalCredentials?: string
|
|
173
175
|
},
|
|
174
176
|
]
|
|
175
177
|
>
|
|
@@ -185,6 +187,10 @@ export interface SerializableInspectorState {
|
|
|
185
187
|
agentsMeta: InspectorState['agents']['agentsMeta']
|
|
186
188
|
files: [string, { path: string; exportedName: string }][]
|
|
187
189
|
}
|
|
190
|
+
scorers: {
|
|
191
|
+
scorersMeta: InspectorState['scorers']['scorersMeta']
|
|
192
|
+
files: [string, { path: string; exportedName: string }][]
|
|
193
|
+
}
|
|
188
194
|
cli: {
|
|
189
195
|
meta: InspectorState['cli']['meta']
|
|
190
196
|
files: string[]
|
|
@@ -409,6 +415,10 @@ export function serializeInspectorState(
|
|
|
409
415
|
agentsMeta: state.agents?.agentsMeta ?? {},
|
|
410
416
|
files: Array.from(state.agents?.files?.entries() ?? []),
|
|
411
417
|
},
|
|
418
|
+
scorers: {
|
|
419
|
+
scorersMeta: state.scorers?.scorersMeta ?? {},
|
|
420
|
+
files: Array.from(state.scorers?.files?.entries() ?? []),
|
|
421
|
+
},
|
|
412
422
|
cli: {
|
|
413
423
|
meta: state.cli.meta,
|
|
414
424
|
files: Array.from(state.cli.files),
|
|
@@ -613,6 +623,10 @@ export function deserializeInspectorState(
|
|
|
613
623
|
agentsMeta: data.agents?.agentsMeta || {},
|
|
614
624
|
files: new Map(data.agents?.files || []),
|
|
615
625
|
},
|
|
626
|
+
scorers: {
|
|
627
|
+
scorersMeta: data.scorers?.scorersMeta || {},
|
|
628
|
+
files: new Map(data.scorers?.files || []),
|
|
629
|
+
},
|
|
616
630
|
cli: {
|
|
617
631
|
meta: data.cli.meta,
|
|
618
632
|
files: new Set(data.cli.files),
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import * as ts from 'typescript'
|
|
4
|
+
import { extractDSLWorkflow } from './extract-dsl-workflow.js'
|
|
5
|
+
import type { ApprovalStepMeta } from '@pikku/core/workflow'
|
|
6
|
+
|
|
7
|
+
const checker = {
|
|
8
|
+
getSymbolAtLocation: () => undefined,
|
|
9
|
+
getAliasedSymbol: () => undefined,
|
|
10
|
+
getShorthandAssignmentValueSymbol: () => undefined,
|
|
11
|
+
getTypeAtLocation: () => undefined,
|
|
12
|
+
} as unknown as ts.TypeChecker
|
|
13
|
+
|
|
14
|
+
/** Extracts the single approval step from a workflow body. */
|
|
15
|
+
const approvalStepFor = (options: string): ApprovalStepMeta => {
|
|
16
|
+
const file = ts.createSourceFile(
|
|
17
|
+
'wf.ts',
|
|
18
|
+
`const wf = pikkuWorkflowFunc({
|
|
19
|
+
func: async (services, data, { workflow }) => {
|
|
20
|
+
await workflow.approval('Release funds', ${options})
|
|
21
|
+
},
|
|
22
|
+
})`,
|
|
23
|
+
ts.ScriptTarget.Latest,
|
|
24
|
+
true
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
let node: ts.Node | undefined
|
|
28
|
+
const visit = (n: ts.Node) => {
|
|
29
|
+
if (ts.isCallExpression(n) && n.expression.getText() === 'pikkuWorkflowFunc') {
|
|
30
|
+
node = n
|
|
31
|
+
}
|
|
32
|
+
ts.forEachChild(n, visit)
|
|
33
|
+
}
|
|
34
|
+
visit(file)
|
|
35
|
+
assert.ok(node, 'failed to find the workflow definition')
|
|
36
|
+
|
|
37
|
+
const result = extractDSLWorkflow(node, checker)
|
|
38
|
+
assert.equal(result.status, 'ok', JSON.stringify(result))
|
|
39
|
+
const step = result.steps?.find(
|
|
40
|
+
(s): s is ApprovalStepMeta => s.type === 'approval'
|
|
41
|
+
)
|
|
42
|
+
assert.ok(step, 'no approval step was extracted')
|
|
43
|
+
return step
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('an approval gate carries its policy into the graph', () => {
|
|
47
|
+
test('approvers and approverScope survive extraction', () => {
|
|
48
|
+
const step = approvalStepFor(
|
|
49
|
+
`{ schema: DecisionSchema, approvers: 'not-initiator', approverScope: 'payments:approve' }`
|
|
50
|
+
)
|
|
51
|
+
assert.equal(step.approvers, 'not-initiator')
|
|
52
|
+
assert.equal(step.approverScope, 'payments:approve')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('expiry still survives alongside them', () => {
|
|
56
|
+
const step = approvalStepFor(
|
|
57
|
+
`{ schema: DecisionSchema, expiry: '3d', approvers: 'owner' }`
|
|
58
|
+
)
|
|
59
|
+
assert.equal(step.expiry, '3d')
|
|
60
|
+
assert.equal(step.approvers, 'owner')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('a gate declaring no policy records none', () => {
|
|
64
|
+
const step = approvalStepFor(`{ schema: DecisionSchema }`)
|
|
65
|
+
assert.equal(step.approvers, undefined)
|
|
66
|
+
assert.equal(step.approverScope, undefined)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -1051,14 +1051,15 @@ function extractApprovalStep(
|
|
|
1051
1051
|
step.outputVar = outputVar
|
|
1052
1052
|
}
|
|
1053
1053
|
// The `schema` option is a runtime value validated inside the workflow body,
|
|
1054
|
-
// so it is deliberately not serialized here — only
|
|
1055
|
-
// and planned-step ladder need in order to describe the gate.
|
|
1054
|
+
// so it is deliberately not serialized here — only the literal options the
|
|
1055
|
+
// graph and planned-step ladder need in order to describe the gate.
|
|
1056
1056
|
const options = args[1]
|
|
1057
1057
|
if (options && ts.isObjectLiteralExpression(options)) {
|
|
1058
1058
|
for (const prop of options.properties) {
|
|
1059
|
+
if (!ts.isPropertyAssignment(prop)) continue
|
|
1060
|
+
const name = prop.name.getText()
|
|
1059
1061
|
if (
|
|
1060
|
-
|
|
1061
|
-
prop.name.getText() === 'expiry' &&
|
|
1062
|
+
name === 'expiry' &&
|
|
1062
1063
|
(ts.isStringLiteral(prop.initializer) ||
|
|
1063
1064
|
ts.isNumericLiteral(prop.initializer))
|
|
1064
1065
|
) {
|
|
@@ -1066,6 +1067,13 @@ function extractApprovalStep(
|
|
|
1066
1067
|
? Number(prop.initializer.text)
|
|
1067
1068
|
: prop.initializer.text
|
|
1068
1069
|
}
|
|
1070
|
+
if (name === 'approvers' && ts.isStringLiteral(prop.initializer)) {
|
|
1071
|
+
step.approvers = prop.initializer
|
|
1072
|
+
.text as ApprovalStepMeta['approvers']
|
|
1073
|
+
}
|
|
1074
|
+
if (name === 'approverScope' && ts.isStringLiteral(prop.initializer)) {
|
|
1075
|
+
step.approverScope = prop.initializer.text
|
|
1076
|
+
}
|
|
1069
1077
|
}
|
|
1070
1078
|
}
|
|
1071
1079
|
return step
|
|
@@ -77,20 +77,23 @@ export function getScenarioStepPhase(
|
|
|
77
77
|
/**
|
|
78
78
|
* The expectation helpers that assert without being a `then`.
|
|
79
79
|
*
|
|
80
|
-
* `expectService` and `
|
|
81
|
-
* steps, so they carry no phase and never reach the step meta — but
|
|
82
|
-
* a witness: a recorded service call,
|
|
83
|
-
* a scenario whose only assertion is one of these
|
|
80
|
+
* `expectService`, `expectError` and `expectScore` are inline steps rather than
|
|
81
|
+
* scenario steps, so they carry no phase and never reach the step meta — but
|
|
82
|
+
* each one is a witness: a recorded service call, a refusal, or a graded run.
|
|
83
|
+
* PKU680 has to see them, or a scenario whose only assertion is one of these
|
|
84
|
+
* reads as asserting nothing.
|
|
84
85
|
*/
|
|
85
86
|
const SCENARIO_EXPECTATION_CALLS = [
|
|
86
87
|
'expectService',
|
|
87
88
|
'expectError',
|
|
88
89
|
'expectEventually',
|
|
90
|
+
'expectScore',
|
|
89
91
|
] as const
|
|
90
92
|
|
|
91
93
|
/**
|
|
92
94
|
* Check if a call expression is one of the scenario expectation helpers —
|
|
93
|
-
* `scenario.expectService()`, `.expectError()
|
|
95
|
+
* `scenario.expectService()`, `.expectError()`, `.expectEventually()` or
|
|
96
|
+
* `.expectScore()`.
|
|
94
97
|
*/
|
|
95
98
|
export function isScenarioExpectationCall(node: ts.CallExpression): boolean {
|
|
96
99
|
return (
|
package/src/visit.ts
CHANGED
|
@@ -34,6 +34,7 @@ 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
36
|
import { addAIAgent } from './add/add-ai-agent.js'
|
|
37
|
+
import { addAIScorer } from './add/add-ai-scorer.js'
|
|
37
38
|
import { addApprovalDescription } from './add/add-approval-description.js'
|
|
38
39
|
|
|
39
40
|
export const visitSetup = (
|
|
@@ -172,6 +173,7 @@ export const visitRoutes = (
|
|
|
172
173
|
addWorkflowGraph(logger, node, checker, state, nextOptions)
|
|
173
174
|
addFeature(logger, node, checker, state, nextOptions)
|
|
174
175
|
addAIAgent(logger, node, checker, state, nextOptions)
|
|
176
|
+
addAIScorer(logger, node, checker, state, nextOptions)
|
|
175
177
|
|
|
176
178
|
ts.forEachChild(node, (child) =>
|
|
177
179
|
visitRoutes(logger, checker, child, state, nextOptions)
|