@pikku/core 0.12.80 → 0.12.83
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 +345 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/function/function-runner.js +2 -5
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +11 -11
- package/dist/index.js +3 -3
- package/dist/pikku-state.js +4 -0
- package/dist/services/ai-agent-runner-service.d.ts +7 -0
- package/dist/services/ai-run-state-service.d.ts +10 -0
- package/dist/services/in-memory-ai-run-state-service.d.ts +5 -1
- package/dist/services/in-memory-ai-run-state-service.js +9 -0
- package/dist/services/index.d.ts +15 -16
- package/dist/services/index.js +5 -5
- package/dist/services/meta-service.d.ts +2 -1
- package/dist/services/scoped-credential-service.d.ts +21 -0
- package/dist/services/scoped-credential-service.js +53 -0
- package/dist/testing/service-tests/ai-storage-service-tests.js +76 -0
- package/dist/types/core.types.d.ts +2 -3
- package/dist/types/state.types.d.ts +19 -1
- package/dist/wirings/actor-flow/index.d.ts +1 -1
- package/dist/wirings/ai-agent/ai-agent-finalize.d.ts +58 -0
- package/dist/wirings/ai-agent/ai-agent-finalize.js +138 -0
- package/dist/wirings/ai-agent/ai-agent-interrupt.js +1 -0
- package/dist/wirings/ai-agent/ai-agent-memory.d.ts +2 -8
- package/dist/wirings/ai-agent/ai-agent-memory.js +34 -17
- package/dist/wirings/ai-agent/ai-agent-model-config.d.ts +7 -0
- package/dist/wirings/ai-agent/ai-agent-model-config.js +44 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +4 -0
- package/dist/wirings/ai-agent/ai-agent-runner.js +61 -40
- package/dist/wirings/ai-agent/ai-agent-stream.js +89 -36
- package/dist/wirings/ai-agent/ai-agent-turn.d.ts +1 -0
- package/dist/wirings/ai-agent/ai-agent-turn.js +1 -0
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +46 -1
- package/dist/wirings/ai-agent/index.d.ts +8 -7
- package/dist/wirings/ai-agent/index.js +5 -4
- package/dist/wirings/ai-scorer/ai-scorer-grade.d.ts +26 -0
- package/dist/wirings/ai-scorer/ai-scorer-grade.js +33 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.d.ts +17 -0
- package/dist/wirings/ai-scorer/ai-scorer-judge.js +92 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-live.js +38 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.d.ts +18 -0
- package/dist/wirings/ai-scorer/ai-scorer-registry.js +46 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.d.ts +8 -0
- package/dist/wirings/ai-scorer/ai-scorer-sampling.js +31 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.d.ts +10 -0
- package/dist/wirings/ai-scorer/ai-scorer-snapshots.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.d.ts +15 -0
- package/dist/wirings/ai-scorer/ai-scorer-worker.js +58 -0
- package/dist/wirings/ai-scorer/ai-scorer.d.ts +39 -0
- package/dist/wirings/ai-scorer/ai-scorer.js +40 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.d.ts +90 -0
- package/dist/wirings/ai-scorer/ai-scorer.types.js +4 -0
- package/dist/wirings/ai-scorer/index.d.ts +6 -0
- package/dist/wirings/ai-scorer/index.js +5 -0
- package/dist/wirings/channel/index.d.ts +5 -6
- package/dist/wirings/channel/index.js +3 -4
- package/dist/wirings/channel/local/local-channel-runner.js +8 -1
- package/dist/wirings/cli/channel/cli-raw-channel-runner.js +9 -1
- package/dist/wirings/cli/channel/index.d.ts +1 -2
- package/dist/wirings/cli/channel/index.js +0 -1
- package/dist/wirings/cli/cli-runner.js +13 -1
- package/dist/wirings/credential/index.d.ts +1 -1
- package/dist/wirings/gateway/index.d.ts +1 -1
- package/dist/wirings/http/http-runner.js +8 -2
- package/dist/wirings/http/index.d.ts +1 -2
- package/dist/wirings/mcp/index.d.ts +1 -1
- package/dist/wirings/mcp/mcp-runner.d.ts +15 -0
- package/dist/wirings/mcp/mcp-runner.js +18 -5
- package/dist/wirings/persona/index.d.ts +3 -4
- package/dist/wirings/persona/index.js +2 -3
- package/dist/wirings/queue/index.d.ts +1 -3
- package/dist/wirings/queue/index.js +1 -3
- package/dist/wirings/rpc/addon-runner.d.ts +8 -0
- package/dist/wirings/rpc/addon-runner.js +31 -3
- package/dist/wirings/rpc/rpc-runner.js +4 -0
- package/dist/wirings/rpc/rpc-types.d.ts +8 -0
- package/dist/wirings/rpc/wire-addon.d.ts +25 -0
- package/dist/wirings/rpc/wire-addon.js +8 -0
- package/dist/wirings/scheduler/index.d.ts +1 -1
- package/dist/wirings/trigger/index.d.ts +1 -1
- package/dist/wirings/virtual-user/index.d.ts +5 -6
- package/dist/wirings/virtual-user/index.js +2 -4
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +85 -15
- package/dist/wirings/workflow/feature.d.ts +2 -1
- package/dist/wirings/workflow/index.d.ts +5 -16
- package/dist/wirings/workflow/index.js +1 -9
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +17 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +48 -13
- package/dist/wirings/workflow/pikku-workflow-service.js +17 -3
- package/dist/wirings/workflow/scenario-step.types.d.ts +8 -0
- package/dist/wirings/workflow/scenario.types.d.ts +37 -0
- package/dist/wirings/workflow/workflow-approval-audit.d.ts +16 -0
- package/dist/wirings/workflow/workflow-approval-audit.js +40 -0
- package/dist/wirings/workflow/workflow-approval-policy.d.ts +20 -0
- package/dist/wirings/workflow/workflow-approval-policy.js +48 -0
- package/dist/wirings/workflow/workflow-approval.d.ts +29 -1
- package/dist/wirings/workflow/workflow-approval.js +65 -2
- package/dist/wirings/workflow/workflow-run-ownership.d.ts +2 -1
- package/dist/wirings/workflow/workflow-run-ownership.js +2 -1
- package/dist/wirings/workflow/workflow.types.d.ts +2 -37
- package/knowledge/decisions/internals/addon-pikku-meta-ships-at-the-package-root-or-under-dist.md +32 -0
- package/knowledge/decisions/internals/an-addon-scope-root-loses-to-a-root-the-host-already-declares.md +39 -0
- package/knowledge/decisions/internals/index.md +30 -3
- package/knowledge/decisions/internals/validate-runs-checks-by-precondition.md +115 -0
- package/knowledge/decisions/security/a-function-never-receives-the-secret-service.md +37 -0
- package/knowledge/decisions/security/a-workflow-run-is-read-and-approved-by-its-owner.md +30 -14
- package/knowledge/decisions/security/an-approval-answer-outlives-the-run-it-answered.md +59 -0
- package/knowledge/decisions/security/index.md +3 -1
- package/knowledge/questions/index.md +1 -1
- package/package.json +3 -2
- package/scripts/generate-api-report.mts +143 -18
- package/src/api-report.test.ts +2 -2
- package/src/errors/index.ts +1 -1
- package/src/function/function-runner.test.ts +52 -0
- package/src/function/function-runner.ts +5 -9
- package/src/function/index.ts +0 -2
- package/src/index.ts +0 -35
- package/src/pikku-state.ts +5 -0
- package/src/public-surface.json +81 -118
- package/src/services/ai-agent-runner-service.ts +12 -1
- package/src/services/ai-run-state-service.ts +11 -0
- package/src/services/in-memory-ai-run-state-service.ts +13 -0
- package/src/services/index.ts +7 -58
- package/src/services/meta-service.ts +2 -4
- package/src/services/scoped-credential-service.test.ts +86 -0
- package/src/services/scoped-credential-service.ts +63 -0
- package/src/testing/service-tests/ai-storage-service-tests.ts +93 -0
- package/src/types/core.types.ts +4 -7
- package/src/types/state.types.ts +21 -1
- package/src/wirings/actor-flow/index.ts +0 -3
- package/src/wirings/ai-agent/ai-agent-finalize.test.ts +186 -0
- package/src/wirings/ai-agent/ai-agent-finalize.ts +197 -0
- package/src/wirings/ai-agent/ai-agent-interrupt.ts +1 -0
- package/src/wirings/ai-agent/ai-agent-memory.ts +54 -38
- package/src/wirings/ai-agent/ai-agent-model-config.test.ts +72 -3
- package/src/wirings/ai-agent/ai-agent-model-config.ts +49 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +4 -0
- package/src/wirings/ai-agent/ai-agent-runner.ts +71 -40
- package/src/wirings/ai-agent/ai-agent-stream-output-hooks.test.ts +353 -0
- package/src/wirings/ai-agent/ai-agent-stream.ts +116 -54
- package/src/wirings/ai-agent/ai-agent-turn.test.ts +67 -0
- package/src/wirings/ai-agent/ai-agent-turn.ts +1 -0
- package/src/wirings/ai-agent/ai-agent.types.ts +64 -4
- package/src/wirings/ai-agent/index.ts +2 -16
- package/src/wirings/ai-scorer/ai-scorer-grade.test.ts +106 -0
- package/src/wirings/ai-scorer/ai-scorer-grade.ts +55 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.test.ts +143 -0
- package/src/wirings/ai-scorer/ai-scorer-judge.ts +120 -0
- package/src/wirings/ai-scorer/ai-scorer-live.test.ts +174 -0
- package/src/wirings/ai-scorer/ai-scorer-live.ts +56 -0
- package/src/wirings/ai-scorer/ai-scorer-registry.ts +63 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.test.ts +34 -0
- package/src/wirings/ai-scorer/ai-scorer-sampling.ts +36 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.test.ts +49 -0
- package/src/wirings/ai-scorer/ai-scorer-snapshots.ts +46 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.test.ts +122 -0
- package/src/wirings/ai-scorer/ai-scorer-worker.ts +69 -0
- package/src/wirings/ai-scorer/ai-scorer.ts +76 -0
- package/src/wirings/ai-scorer/ai-scorer.types.ts +107 -0
- package/src/wirings/ai-scorer/index.ts +24 -0
- package/src/wirings/channel/index.ts +1 -20
- package/src/wirings/channel/local/local-channel-runner.test.ts +68 -0
- package/src/wirings/channel/local/local-channel-runner.ts +8 -1
- package/src/wirings/cli/channel/cli-raw-channel-runner.test.ts +23 -0
- package/src/wirings/cli/channel/cli-raw-channel-runner.ts +12 -1
- package/src/wirings/cli/channel/index.ts +0 -7
- package/src/wirings/cli/cli-runner.test.ts +68 -0
- package/src/wirings/cli/cli-runner.ts +18 -1
- package/src/wirings/credential/index.ts +0 -1
- package/src/wirings/gateway/index.ts +0 -3
- package/src/wirings/http/http-runner.test.ts +66 -0
- package/src/wirings/http/http-runner.ts +10 -2
- package/src/wirings/http/index.ts +1 -1
- package/src/wirings/mcp/index.ts +0 -1
- package/src/wirings/mcp/mcp-runner.test.ts +181 -0
- package/src/wirings/mcp/mcp-runner.ts +35 -5
- package/src/wirings/persona/index.ts +0 -8
- package/src/wirings/queue/index.ts +0 -14
- package/src/wirings/rpc/addon-runner.ts +62 -3
- package/src/wirings/rpc/addon-secrets.test.ts +391 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +4 -0
- package/src/wirings/rpc/rpc-types.ts +8 -0
- package/src/wirings/rpc/wire-addon.ts +33 -0
- package/src/wirings/scheduler/index.ts +0 -1
- package/src/wirings/trigger/index.ts +0 -1
- package/src/wirings/virtual-user/index.ts +0 -16
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +96 -16
- package/src/wirings/workflow/feature.ts +2 -5
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -68
- package/src/wirings/workflow/pikku-scenario-service.ts +81 -16
- package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -12
- package/src/wirings/workflow/pikku-workflow-service.ts +28 -4
- package/src/wirings/workflow/scenario-expectations.test.ts +75 -0
- package/src/wirings/workflow/scenario-hooks.test.ts +3 -2
- package/src/wirings/workflow/scenario-step.types.ts +8 -0
- package/src/wirings/workflow/scenario.types.ts +63 -0
- package/src/wirings/workflow/workflow-approval-audit.ts +47 -0
- package/src/wirings/workflow/workflow-approval-policy.test.ts +524 -0
- package/src/wirings/workflow/workflow-approval-policy.ts +68 -0
- package/src/wirings/workflow/workflow-approval.ts +113 -9
- package/src/wirings/workflow/workflow-run-authority.test.ts +12 -15
- package/src/wirings/workflow/workflow-run-ownership.ts +2 -1
- package/src/wirings/workflow/workflow.types.ts +1 -63
- package/src/wirings-stay-decoupled.test.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/internal.d.ts +0 -3
- package/dist/internal.js +0 -2
- package/dist/middleware/timeout.d.ts +0 -9
- package/dist/middleware/timeout.js +0 -15
- package/dist/pikku-response.d.ts +0 -6
- package/dist/pikku-response.js +0 -6
- package/dist/services/gopass-secrets.d.ts +0 -15
- package/dist/services/gopass-secrets.js +0 -76
- package/dist/services/http-scenario-actors.d.ts +0 -75
- package/dist/services/http-scenario-actors.js +0 -195
- package/dist/services/http-user-flow-actors.d.ts +0 -67
- package/dist/services/http-user-flow-actors.js +0 -193
- package/dist/services/scenario-actors-service.d.ts +0 -127
- package/dist/services/scenario-actors-service.js +0 -40
- package/dist/services/user-flow-actors-service.d.ts +0 -39
- package/dist/wirings/credential/wire-credential.d.ts +0 -48
- package/dist/wirings/credential/wire-credential.js +0 -47
- package/dist/wirings/oauth2/oauth2-client.d.ts +0 -47
- package/dist/wirings/oauth2/oauth2-client.js +0 -263
- package/dist/wirings/oauth2/oauth2-routes.d.ts +0 -35
- package/dist/wirings/oauth2/oauth2-routes.js +0 -146
- package/dist/wirings/scope/wire-scope.d.ts +0 -33
- package/dist/wirings/scope/wire-scope.js +0 -32
- package/dist/wirings/workflow/dsl/index.d.ts +0 -5
- package/dist/wirings/workflow/dsl/index.js +0 -4
- package/dist/wirings/workflow/graph/index.d.ts +0 -5
- package/dist/wirings/workflow/graph/index.js +0 -4
- /package/dist/{services/user-flow-actors-service.js → wirings/workflow/scenario.types.js} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ScorerInput } from './ai-scorer.types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The last few finished runs, held so a scenario can grade one.
|
|
5
|
+
*
|
|
6
|
+
* A scenario cannot rebuild a `ScorerInput` from storage: the run record holds
|
|
7
|
+
* status and usage, and the prompt, answer and tool calls are spread across the
|
|
8
|
+
* thread's messages, where the boundary of a single run is not recoverable. So
|
|
9
|
+
* the runtime keeps the snapshot it already took at finalize — which is also
|
|
10
|
+
* what makes a scenario's grade comparable to a live one, since both grade the
|
|
11
|
+
* identical object, redactions included.
|
|
12
|
+
*
|
|
13
|
+
* Off unless something turns it on, and bounded when it is. A process that
|
|
14
|
+
* grades no scenarios holds nothing, and one that does cannot accumulate run
|
|
15
|
+
* content without limit.
|
|
16
|
+
*/
|
|
17
|
+
const snapshots = new Map<string, ScorerInput>()
|
|
18
|
+
|
|
19
|
+
let limit = 0
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Start retaining snapshots. Called where the scenario grading RPC is
|
|
23
|
+
* registered, so the buffer exists in exactly the processes that can read it —
|
|
24
|
+
* a development server — and never in a deployed bundle.
|
|
25
|
+
*/
|
|
26
|
+
export const enableScoreSnapshots = (maxRuns = 50): void => {
|
|
27
|
+
limit = maxRuns
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const recordScoreSnapshot = (run: ScorerInput): void => {
|
|
31
|
+
if (limit === 0) return
|
|
32
|
+
snapshots.set(run.runId, run)
|
|
33
|
+
while (snapshots.size > limit) {
|
|
34
|
+
const oldest = snapshots.keys().next().value
|
|
35
|
+
if (oldest === undefined) break
|
|
36
|
+
snapshots.delete(oldest)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const getScoreSnapshot = (runId: string): ScorerInput | undefined =>
|
|
41
|
+
snapshots.get(runId)
|
|
42
|
+
|
|
43
|
+
export const resetScoreSnapshots = (): void => {
|
|
44
|
+
snapshots.clear()
|
|
45
|
+
limit = 0
|
|
46
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { beforeEach, describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
4
|
+
import { pikkuAIScoreWorkerFunc } from './ai-scorer-worker.js'
|
|
5
|
+
import { pikkuAIJudge, pikkuAIScorer } from './ai-scorer.js'
|
|
6
|
+
import type { PikkuAIScorer, ScoreJob } from './ai-scorer.types.js'
|
|
7
|
+
|
|
8
|
+
const job = (overrides: Partial<ScoreJob> = {}): ScoreJob => ({
|
|
9
|
+
scorerName: 'brevity',
|
|
10
|
+
runId: 'run-1',
|
|
11
|
+
agentName: 'assistant',
|
|
12
|
+
input: 'what is the capital of France?',
|
|
13
|
+
output: 'Paris',
|
|
14
|
+
toolCalls: [],
|
|
15
|
+
usage: { inputTokens: 10, outputTokens: 5 },
|
|
16
|
+
...overrides,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const install = (scorer: PikkuAIScorer, aiAgentRunner?: unknown) => {
|
|
20
|
+
pikkuState(null, 'agent', 'scorers').set(scorer.name, scorer)
|
|
21
|
+
const saved: unknown[] = []
|
|
22
|
+
pikkuState(null, 'package', 'singletonServices', {
|
|
23
|
+
aiRunState: {
|
|
24
|
+
saveScore: async (score: unknown) => {
|
|
25
|
+
saved.push(score)
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
aiAgentRunner,
|
|
29
|
+
} as never)
|
|
30
|
+
return saved
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('pikkuAIScoreWorkerFunc', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
resetPikkuState()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('records what a heuristic scorer graded, against the run it graded', async () => {
|
|
39
|
+
const saved = install(
|
|
40
|
+
pikkuAIScorer({
|
|
41
|
+
name: 'brevity',
|
|
42
|
+
description: 'Shorter is better',
|
|
43
|
+
score: (input) => ({
|
|
44
|
+
score: input.output.length < 20 ? 1 : 0,
|
|
45
|
+
reason: 'Answered in one word.',
|
|
46
|
+
}),
|
|
47
|
+
})
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
await pikkuAIScoreWorkerFunc({}, job())
|
|
51
|
+
|
|
52
|
+
assert.deepEqual(saved, [
|
|
53
|
+
{
|
|
54
|
+
runId: 'run-1',
|
|
55
|
+
scorerName: 'brevity',
|
|
56
|
+
score: 1,
|
|
57
|
+
reason: 'Answered in one word.',
|
|
58
|
+
},
|
|
59
|
+
])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('hands a judge to the model rather than calling a score function it has not got', async () => {
|
|
63
|
+
const saved = install(
|
|
64
|
+
pikkuAIJudge({
|
|
65
|
+
name: 'helpfulness',
|
|
66
|
+
description: 'Is the answer useful',
|
|
67
|
+
model: 'claude-opus-5',
|
|
68
|
+
goal: 'Grade helpfulness.',
|
|
69
|
+
}),
|
|
70
|
+
{
|
|
71
|
+
run: async () => ({
|
|
72
|
+
object: { score: 0.5, reason: 'Terse.' },
|
|
73
|
+
usage: { inputTokens: 80, outputTokens: 20 },
|
|
74
|
+
}),
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
await pikkuAIScoreWorkerFunc({}, job({ scorerName: 'helpfulness' }))
|
|
79
|
+
|
|
80
|
+
assert.deepEqual(saved, [
|
|
81
|
+
{
|
|
82
|
+
runId: 'run-1',
|
|
83
|
+
scorerName: 'helpfulness',
|
|
84
|
+
score: 0.5,
|
|
85
|
+
reason: 'Terse.',
|
|
86
|
+
metadata: { judgeModel: 'claude-opus-5', judgeTokens: 100 },
|
|
87
|
+
},
|
|
88
|
+
])
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('fails the job when there is nowhere to record the grade, so it is retried rather than lost', async () => {
|
|
92
|
+
pikkuState(null, 'agent', 'scorers').set(
|
|
93
|
+
'brevity',
|
|
94
|
+
pikkuAIScorer({
|
|
95
|
+
name: 'brevity',
|
|
96
|
+
description: 'Shorter is better',
|
|
97
|
+
score: () => ({ score: 1 }),
|
|
98
|
+
})
|
|
99
|
+
)
|
|
100
|
+
pikkuState(null, 'package', 'singletonServices', {} as never)
|
|
101
|
+
|
|
102
|
+
await assert.rejects(
|
|
103
|
+
() => pikkuAIScoreWorkerFunc({}, job()),
|
|
104
|
+
/AI run state service not initialized/
|
|
105
|
+
)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('fails on a scorer name that resolves to nothing', async () => {
|
|
109
|
+
install(
|
|
110
|
+
pikkuAIScorer({
|
|
111
|
+
name: 'brevity',
|
|
112
|
+
description: 'Shorter is better',
|
|
113
|
+
score: () => ({ score: 1 }),
|
|
114
|
+
})
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
await assert.rejects(
|
|
118
|
+
() => pikkuAIScoreWorkerFunc({}, job({ scorerName: 'gone' })),
|
|
119
|
+
/AI scorer not found: gone/
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { pikkuState } from '../../pikku-state.js'
|
|
2
|
+
import { addFunction } from '../../function/function-runner.js'
|
|
3
|
+
import { wireQueueWorker } from '../queue/queue-runner.js'
|
|
4
|
+
import { gradeRun } from './ai-scorer-grade.js'
|
|
5
|
+
import { SCORER_LANE_QUEUES, type ScoreJob } from './ai-scorer.types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The one worker behind both lanes: it resolves the scorer by name and grades.
|
|
9
|
+
* The lane a job arrived on decides nothing except how long it may take.
|
|
10
|
+
*/
|
|
11
|
+
export async function pikkuAIScoreWorkerFunc(
|
|
12
|
+
_services: Record<string, unknown>,
|
|
13
|
+
job: ScoreJob
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const services = pikkuState(null, 'package', 'singletonServices')
|
|
16
|
+
if (!services) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Singleton services not initialized: cannot run the '${job.scorerName}' grade of run ${job.runId}`
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await gradeRun(job, services, { persist: true })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SCORE_WORKER_FUNC_ID = 'pikkuAIScoreWorker'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bind the two lane queues to the shared worker.
|
|
29
|
+
*
|
|
30
|
+
* Registered programmatically rather than emitted into the user's bootstrap:
|
|
31
|
+
* `wireQueueWorker` warns and returns for a name codegen produced no metadata
|
|
32
|
+
* for, so the metadata is synthesised here alongside the registration. This
|
|
33
|
+
* mirrors how workflows wire their own queues.
|
|
34
|
+
*/
|
|
35
|
+
export const wireAIScorerQueueWorkers = (): void => {
|
|
36
|
+
// No scorers means no lanes: a deployment that grades nothing should not be
|
|
37
|
+
// left holding two queues nothing ever writes to.
|
|
38
|
+
if (pikkuState(null, 'agent', 'scorers').size === 0) return
|
|
39
|
+
|
|
40
|
+
const functions = pikkuState(null, 'function', 'functions')
|
|
41
|
+
const functionsMeta = pikkuState(null, 'function', 'meta')
|
|
42
|
+
const queueMeta = pikkuState(null, 'queue', 'meta')
|
|
43
|
+
|
|
44
|
+
if (!functions.has(SCORE_WORKER_FUNC_ID)) {
|
|
45
|
+
addFunction(SCORE_WORKER_FUNC_ID, { func: pikkuAIScoreWorkerFunc } as never)
|
|
46
|
+
}
|
|
47
|
+
if (!functionsMeta[SCORE_WORKER_FUNC_ID]) {
|
|
48
|
+
functionsMeta[SCORE_WORKER_FUNC_ID] = {
|
|
49
|
+
pikkuFuncId: SCORE_WORKER_FUNC_ID,
|
|
50
|
+
sessionless: true,
|
|
51
|
+
functionType: 'helper',
|
|
52
|
+
inputSchemaName: null,
|
|
53
|
+
outputSchemaName: null,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
for (const queueName of Object.values(SCORER_LANE_QUEUES)) {
|
|
58
|
+
if (!queueMeta[queueName]) {
|
|
59
|
+
queueMeta[queueName] = {
|
|
60
|
+
pikkuFuncId: SCORE_WORKER_FUNC_ID,
|
|
61
|
+
name: queueName,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
wireQueueWorker({
|
|
65
|
+
name: queueName,
|
|
66
|
+
func: { func: pikkuAIScoreWorkerFunc },
|
|
67
|
+
} as never)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PikkuAIScorer,
|
|
3
|
+
ScorerInput,
|
|
4
|
+
ScorerOutput,
|
|
5
|
+
} from './ai-scorer.types.js'
|
|
6
|
+
|
|
7
|
+
const assertSampleRate = (name: string, sampleRate: number | undefined) => {
|
|
8
|
+
if (sampleRate === undefined) return 1
|
|
9
|
+
if (sampleRate < 0 || sampleRate > 1) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`Scorer '${name}' has a sampleRate of ${sampleRate} — it is a fraction of live runs to grade, so it must be between 0 and 1`
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
return sampleRate
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A heuristic scorer: pure code over the finished run, no model call, so it
|
|
19
|
+
* grades on the fast lane.
|
|
20
|
+
*/
|
|
21
|
+
export const pikkuAIScorer = <Services = any>(config: {
|
|
22
|
+
name: string
|
|
23
|
+
description: string
|
|
24
|
+
/** 0..1 fraction of live runs to grade. Defaults to all of them. */
|
|
25
|
+
sampleRate?: number
|
|
26
|
+
/**
|
|
27
|
+
* Grades against a known-correct answer. Such a scorer is test-only — live
|
|
28
|
+
* traffic has no answer key, so the runtime never samples it.
|
|
29
|
+
*/
|
|
30
|
+
requiresReference?: boolean
|
|
31
|
+
score: (
|
|
32
|
+
input: ScorerInput,
|
|
33
|
+
services: Services
|
|
34
|
+
) => ScorerOutput | Promise<ScorerOutput>
|
|
35
|
+
}): PikkuAIScorer<Services> => ({
|
|
36
|
+
name: config.name,
|
|
37
|
+
description: config.description,
|
|
38
|
+
lane: 'fast',
|
|
39
|
+
sampleRate: assertSampleRate(config.name, config.sampleRate),
|
|
40
|
+
requiresReference: config.requiresReference ?? false,
|
|
41
|
+
score: config.score,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* An LLM-judge scorer: the runtime makes the model call and forces a structured
|
|
46
|
+
* `{ score, reason }`, so a judge is a rubric rather than a prompt to parse.
|
|
47
|
+
*
|
|
48
|
+
* The rubric field is `goal`, matching `pikkuAIAgent`'s prompt vocabulary — a
|
|
49
|
+
* judge is a degenerate agent, and should use the same word for the same thing.
|
|
50
|
+
* `prompt` is the escape hatch for non-standard framing.
|
|
51
|
+
*/
|
|
52
|
+
export const pikkuAIJudge = <Services = any>(config: {
|
|
53
|
+
name: string
|
|
54
|
+
description: string
|
|
55
|
+
/** 0..1 fraction of live runs to grade. Defaults to all of them. */
|
|
56
|
+
sampleRate?: number
|
|
57
|
+
/**
|
|
58
|
+
* Grades against a known-correct answer. Such a judge is test-only — live
|
|
59
|
+
* traffic has no answer key, so the runtime never samples it.
|
|
60
|
+
*/
|
|
61
|
+
requiresReference?: boolean
|
|
62
|
+
model: string
|
|
63
|
+
goal: string
|
|
64
|
+
prompt?: (input: ScorerInput) => string
|
|
65
|
+
}): PikkuAIScorer<Services> => ({
|
|
66
|
+
name: config.name,
|
|
67
|
+
description: config.description,
|
|
68
|
+
lane: 'slow',
|
|
69
|
+
sampleRate: assertSampleRate(config.name, config.sampleRate),
|
|
70
|
+
requiresReference: config.requiresReference ?? false,
|
|
71
|
+
judge: {
|
|
72
|
+
model: config.model,
|
|
73
|
+
goal: config.goal,
|
|
74
|
+
...(config.prompt ? { prompt: config.prompt } : {}),
|
|
75
|
+
},
|
|
76
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a scorer is shown about a finished run.
|
|
3
|
+
*
|
|
4
|
+
* One snapshot, taken after the output middleware has resolved, so a scorer and
|
|
5
|
+
* the persisted run record see the same thing — including the redactions.
|
|
6
|
+
*/
|
|
7
|
+
export interface ScorerInput {
|
|
8
|
+
runId: string
|
|
9
|
+
agentName: string
|
|
10
|
+
threadId?: string
|
|
11
|
+
resourceId?: string
|
|
12
|
+
/** The user prompt the run answered. */
|
|
13
|
+
input: string
|
|
14
|
+
/** The agent's final text answer. */
|
|
15
|
+
output: string
|
|
16
|
+
/**
|
|
17
|
+
* The known-correct answer. Supplied by a scenario for a reference-based
|
|
18
|
+
* judge; never set on live traffic, which has no answer key.
|
|
19
|
+
*/
|
|
20
|
+
reference?: string
|
|
21
|
+
toolCalls: {
|
|
22
|
+
name: string
|
|
23
|
+
args: unknown
|
|
24
|
+
result?: unknown
|
|
25
|
+
error?: string
|
|
26
|
+
}[]
|
|
27
|
+
usage: {
|
|
28
|
+
inputTokens: number
|
|
29
|
+
outputTokens: number
|
|
30
|
+
model?: string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ScorerOutput {
|
|
35
|
+
/** 0..1, so grades are comparable across scorers. */
|
|
36
|
+
score: number
|
|
37
|
+
reason?: string
|
|
38
|
+
metadata?: Record<string, unknown>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The queue a scorer's jobs go to. Two lanes exist only so a flood of slow
|
|
43
|
+
* LLM-judge jobs cannot starve the cheap heuristic ones; within a lane it is
|
|
44
|
+
* plain FIFO.
|
|
45
|
+
*/
|
|
46
|
+
export type ScorerLane = 'fast' | 'slow'
|
|
47
|
+
|
|
48
|
+
export const SCORER_LANE_QUEUES: Record<ScorerLane, string> = {
|
|
49
|
+
fast: 'ai-score-fast',
|
|
50
|
+
slow: 'ai-score-slow',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A judge's model call. Present only on a scorer built with `pikkuAIJudge`;
|
|
55
|
+
* `score` is present only on one built with `pikkuAIScorer`. Exactly one of the
|
|
56
|
+
* two is set, which is what the two constructors exist to guarantee.
|
|
57
|
+
*/
|
|
58
|
+
export type ScorerJudgeConfig = {
|
|
59
|
+
model: string
|
|
60
|
+
goal: string
|
|
61
|
+
prompt?: (input: ScorerInput) => string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type PikkuAIScorer<Services = any> = {
|
|
65
|
+
name: string
|
|
66
|
+
description: string
|
|
67
|
+
lane: ScorerLane
|
|
68
|
+
/** 0..1 fraction of live runs to grade. */
|
|
69
|
+
sampleRate: number
|
|
70
|
+
/**
|
|
71
|
+
* Grades against a known-correct answer, so it is test-only: live traffic has
|
|
72
|
+
* no answer key and the runtime never samples it.
|
|
73
|
+
*/
|
|
74
|
+
requiresReference: boolean
|
|
75
|
+
score?: (
|
|
76
|
+
input: ScorerInput,
|
|
77
|
+
services: Services
|
|
78
|
+
) => ScorerOutput | Promise<ScorerOutput>
|
|
79
|
+
judge?: ScorerJudgeConfig
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type ScorerMeta = Record<
|
|
83
|
+
string,
|
|
84
|
+
{
|
|
85
|
+
name: string
|
|
86
|
+
description: string
|
|
87
|
+
lane: ScorerLane
|
|
88
|
+
sampleRate: number
|
|
89
|
+
requiresReference: boolean
|
|
90
|
+
sourceFile?: string
|
|
91
|
+
exportedName?: string
|
|
92
|
+
}
|
|
93
|
+
>
|
|
94
|
+
|
|
95
|
+
/** A single scorer's job on a lane queue. */
|
|
96
|
+
export type ScoreJob = ScorerInput & {
|
|
97
|
+
scorerName: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type AIRunScore = {
|
|
101
|
+
runId: string
|
|
102
|
+
scorerName: string
|
|
103
|
+
score: number
|
|
104
|
+
reason?: string
|
|
105
|
+
metadata?: Record<string, unknown>
|
|
106
|
+
createdAt: Date
|
|
107
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { pikkuAIScorer, pikkuAIJudge } from './ai-scorer.js'
|
|
2
|
+
export {
|
|
3
|
+
addAIScorer,
|
|
4
|
+
getAIScorers,
|
|
5
|
+
getAIScorersMeta,
|
|
6
|
+
} from './ai-scorer-registry.js'
|
|
7
|
+
export { gradeRun } from './ai-scorer-grade.js'
|
|
8
|
+
export {
|
|
9
|
+
enableScoreSnapshots,
|
|
10
|
+
getScoreSnapshot,
|
|
11
|
+
} from './ai-scorer-snapshots.js'
|
|
12
|
+
export {
|
|
13
|
+
wireAIScorerQueueWorkers,
|
|
14
|
+
} from './ai-scorer-worker.js'
|
|
15
|
+
export type {
|
|
16
|
+
AIRunScore,
|
|
17
|
+
PikkuAIScorer,
|
|
18
|
+
ScoreJob,
|
|
19
|
+
ScorerInput,
|
|
20
|
+
ScorerJudgeConfig,
|
|
21
|
+
ScorerLane,
|
|
22
|
+
ScorerMeta,
|
|
23
|
+
ScorerOutput,
|
|
24
|
+
} from './ai-scorer.types.js'
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
export { wireChannel
|
|
1
|
+
export { wireChannel } from './channel-runner.js'
|
|
2
2
|
export {
|
|
3
3
|
addChannelMiddleware,
|
|
4
4
|
combineChannelMiddleware,
|
|
5
|
-
wrapChannelWithMiddleware,
|
|
6
5
|
} from './channel-middleware-runner.js'
|
|
7
6
|
export { logChannels } from './log-channels.js'
|
|
8
7
|
export { PikkuAbstractChannelHandler } from './pikku-abstract-channel-handler.js'
|
|
@@ -18,25 +17,14 @@ export type {
|
|
|
18
17
|
CorePikkuChannelMiddlewareFactory,
|
|
19
18
|
ChannelMessageMeta,
|
|
20
19
|
ChannelMeta,
|
|
21
|
-
ChannelRemote,
|
|
22
20
|
PikkuChannel,
|
|
23
21
|
PikkuChannelHandlerFactory,
|
|
24
22
|
} from './channel.types.js'
|
|
25
23
|
export { defineChannelRoutes } from './define-channel-routes.js'
|
|
26
24
|
export {
|
|
27
|
-
CHANNEL_RPC_PENDING,
|
|
28
|
-
CHANNEL_RPC_REQUEST,
|
|
29
|
-
CHANNEL_RPC_RESPONSE,
|
|
30
25
|
ChannelDeploymentService,
|
|
31
26
|
ChannelRPCError,
|
|
32
27
|
ChannelRPCRegistry,
|
|
33
|
-
createChannelRPCResponder,
|
|
34
|
-
createChannelRPCInputValidator,
|
|
35
|
-
createChannelRPCResultValidator,
|
|
36
|
-
isChannelRPCPending,
|
|
37
|
-
isChannelRPCRequest,
|
|
38
|
-
isChannelRPCResponse,
|
|
39
|
-
resolveCapability,
|
|
40
28
|
unsupportedChannelRemote,
|
|
41
29
|
} from './channel-rpc.js'
|
|
42
30
|
export type {
|
|
@@ -44,16 +32,9 @@ export type {
|
|
|
44
32
|
ApprovalRequester,
|
|
45
33
|
Capabilities,
|
|
46
34
|
Capability,
|
|
47
|
-
CapabilityDef,
|
|
48
35
|
CapabilityHandler,
|
|
49
36
|
ChannelRPCPending,
|
|
50
37
|
ChannelRPCRequest,
|
|
51
38
|
ChannelRPCResponse,
|
|
52
39
|
ChannelRPCValidator,
|
|
53
40
|
} from './channel-rpc.js'
|
|
54
|
-
export {
|
|
55
|
-
channelRemote,
|
|
56
|
-
getChannelHostRPC,
|
|
57
|
-
handleChannelRPCResponse,
|
|
58
|
-
releaseChannelHostRPC,
|
|
59
|
-
} from './channel-host-rpc.js'
|
|
@@ -3,6 +3,7 @@ import * as assert from 'node:assert/strict'
|
|
|
3
3
|
import { runLocalChannel } from './local-channel-runner.js'
|
|
4
4
|
import { pikkuState, resetPikkuState } from '../../../pikku-state.js'
|
|
5
5
|
import { wireChannel } from '../channel-runner.js'
|
|
6
|
+
import { addFunction } from '../../../function/function-runner.js'
|
|
6
7
|
import { addHTTPMiddleware } from '../../http/http-runner.js'
|
|
7
8
|
import type {
|
|
8
9
|
HTTPMethod,
|
|
@@ -270,3 +271,70 @@ test('runChannel should run HTTP middleware on websocket upgrade and establish s
|
|
|
270
271
|
await result.message(JSON.stringify({ action: 'ping' }))
|
|
271
272
|
assert.deepEqual(sent, { action: 'ping' })
|
|
272
273
|
})
|
|
274
|
+
|
|
275
|
+
test('a message handler that returns nothing does not attempt to send', async () => {
|
|
276
|
+
// The connect path already guards this; the message path did not, so a handler
|
|
277
|
+
// with nothing to say produced `send requires a non-empty message` on every
|
|
278
|
+
// message. Gateway websockets hit it every time — their generated message
|
|
279
|
+
// handler returns undefined by design — which is why a chat gateway could
|
|
280
|
+
// accept a connection and never deliver anything to its handler.
|
|
281
|
+
resetPikkuState()
|
|
282
|
+
pikkuState(null, 'package', 'singletonServices', mockSingletonServices as any)
|
|
283
|
+
pikkuState(null, 'package', 'factories', {
|
|
284
|
+
createWireServices: mockCreateWireServices,
|
|
285
|
+
} as any)
|
|
286
|
+
|
|
287
|
+
const sent: unknown[] = []
|
|
288
|
+
let handled = 0
|
|
289
|
+
|
|
290
|
+
pikkuState(null, 'channel', 'meta', {
|
|
291
|
+
quiet: {
|
|
292
|
+
name: 'quiet',
|
|
293
|
+
route: '/quiet-channel',
|
|
294
|
+
message: { pikkuFuncId: 'quietMessage' },
|
|
295
|
+
messageWirings: {},
|
|
296
|
+
},
|
|
297
|
+
} as any)
|
|
298
|
+
pikkuState(null, 'function', 'meta')['quietMessage'] = {
|
|
299
|
+
pikkuFuncId: 'quietMessage',
|
|
300
|
+
inputSchemaName: null,
|
|
301
|
+
outputSchemaName: null,
|
|
302
|
+
sessionless: true,
|
|
303
|
+
} as any
|
|
304
|
+
const quietMessage = {
|
|
305
|
+
auth: false,
|
|
306
|
+
func: async () => {
|
|
307
|
+
handled++
|
|
308
|
+
return undefined
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
wireChannel({
|
|
312
|
+
name: 'quiet',
|
|
313
|
+
route: '/quiet-channel',
|
|
314
|
+
auth: false,
|
|
315
|
+
onMessage: quietMessage as any,
|
|
316
|
+
} as any)
|
|
317
|
+
|
|
318
|
+
httpRouter.initialize()
|
|
319
|
+
|
|
320
|
+
const handler = await runLocalChannel({
|
|
321
|
+
channelId: 'quiet-channel-id',
|
|
322
|
+
request: new PikkuMockRequest('/quiet-channel', 'get'),
|
|
323
|
+
response: new PikkuMockResponse(),
|
|
324
|
+
route: '/quiet-channel',
|
|
325
|
+
})
|
|
326
|
+
assert.ok(handler)
|
|
327
|
+
handler.registerOnSend(async (message: unknown) => {
|
|
328
|
+
sent.push(message)
|
|
329
|
+
})
|
|
330
|
+
handler.open()
|
|
331
|
+
|
|
332
|
+
await handler.message(JSON.stringify({ text: 'hello' }))
|
|
333
|
+
|
|
334
|
+
assert.equal(handled, 1, 'the handler should have run')
|
|
335
|
+
assert.deepEqual(
|
|
336
|
+
sent,
|
|
337
|
+
[],
|
|
338
|
+
'nothing should have been sent for an empty result'
|
|
339
|
+
)
|
|
340
|
+
})
|
|
@@ -213,7 +213,14 @@ export const runLocalChannel = async ({
|
|
|
213
213
|
channelHandler.registerOnMessage(async (data) => {
|
|
214
214
|
try {
|
|
215
215
|
const result = await onMessage(data)
|
|
216
|
-
|
|
216
|
+
// Guarded exactly as the connect path above is. A handler with nothing
|
|
217
|
+
// to say is normal — a gateway websocket's generated message handler
|
|
218
|
+
// always returns undefined — and sending that raised `send requires a
|
|
219
|
+
// non-empty message`, which surfaced as a channel that accepted a
|
|
220
|
+
// connection and then silently delivered nothing.
|
|
221
|
+
if (result !== undefined) {
|
|
222
|
+
await channel.send(result)
|
|
223
|
+
}
|
|
217
224
|
} catch (e: any) {
|
|
218
225
|
singletonServices.logger.error(e)
|
|
219
226
|
const errorResponse = getErrorResponse(e)
|
|
@@ -103,6 +103,29 @@ describe('handleRawCLI', () => {
|
|
|
103
103
|
assert.match(result.error!, /deploy plan is red/)
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
+
test('masks the raw error message to the client in production', async () => {
|
|
107
|
+
const prior = process.env.NODE_ENV
|
|
108
|
+
process.env.NODE_ENV = 'production'
|
|
109
|
+
try {
|
|
110
|
+
wireTestCLI(() => {
|
|
111
|
+
throw new Error('connection to internal-db:5432 failed: bad password')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const result = await handleRawCLI({
|
|
115
|
+
programName: 'fabric',
|
|
116
|
+
args: ['deploy'],
|
|
117
|
+
singletonServices,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
assert.equal(result.exitCode, 1)
|
|
121
|
+
assert.equal(result.error, 'Command failed')
|
|
122
|
+
assert.doesNotMatch(result.error!, /internal-db|password/)
|
|
123
|
+
} finally {
|
|
124
|
+
if (prior === undefined) delete process.env.NODE_ENV
|
|
125
|
+
else process.env.NODE_ENV = prior
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
106
129
|
test('exits 0 on help and 1 on an unknown command', async () => {
|
|
107
130
|
wireTestCLI(() => ({}))
|
|
108
131
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pikkuState } from '../../../pikku-state.js'
|
|
2
|
+
import { isProduction } from '../../../env.js'
|
|
2
3
|
import type { CLIMeta } from '../cli.types.js'
|
|
3
4
|
import { parseCLIArguments, generateCommandHelp } from '../command-parser.js'
|
|
4
5
|
import { runCLICommand } from '../cli-runner.js'
|
|
@@ -132,6 +133,16 @@ export async function handleRawCLI({
|
|
|
132
133
|
// A command can throw anything; `.message` off a string is undefined, and
|
|
133
134
|
// the run would then exit 1 saying nothing at all.
|
|
134
135
|
const message = e instanceof Error ? e.message : String(e)
|
|
135
|
-
|
|
136
|
+
// The raw message can carry internals (a stack, a DB error, a path). This
|
|
137
|
+
// runs over a channel that may be remote, so return a generic error in
|
|
138
|
+
// production and keep the detail server-side. Dev keeps the message inline.
|
|
139
|
+
singletonServices.logger.error?.(
|
|
140
|
+
`CLI channel command failed: ${message}`,
|
|
141
|
+
e
|
|
142
|
+
)
|
|
143
|
+
const clientError = isProduction()
|
|
144
|
+
? 'Command failed'
|
|
145
|
+
: message || 'Command failed'
|
|
146
|
+
return { error: clientError, exitCode: 1, commandId }
|
|
136
147
|
}
|
|
137
148
|
}
|
|
@@ -3,13 +3,6 @@ export { handleRawCLI } from './cli-raw-channel-runner.js'
|
|
|
3
3
|
export type { RawCLIFrame, RawCLIResult } from './cli-raw-channel-runner.js'
|
|
4
4
|
export { executeRawCLIViaChannel } from './cli-raw-client-runner.js'
|
|
5
5
|
export type {
|
|
6
|
-
ClientCLIRenderServices,
|
|
7
6
|
CorePikkuCLIClientRender,
|
|
8
7
|
} from './cli-raw-client-runner.js'
|
|
9
|
-
export {
|
|
10
|
-
APPROVAL_FLAGS,
|
|
11
|
-
approverForMode,
|
|
12
|
-
createTerminalApprover,
|
|
13
|
-
takeApprovalFlags,
|
|
14
|
-
} from './cli-approval.js'
|
|
15
8
|
export type { ApprovalMode } from './cli-approval.js'
|