@pikku/core 0.12.80 → 0.12.82
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 +312 -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 -15
- package/dist/services/index.js +5 -5
- 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 +0 -2
- package/dist/types/state.types.d.ts +13 -0
- 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 +2 -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 +4 -0
- package/dist/wirings/rpc/addon-runner.js +19 -3
- package/dist/wirings/rpc/rpc-runner.js +2 -0
- package/dist/wirings/rpc/rpc-types.d.ts +4 -0
- package/dist/wirings/rpc/wire-addon.d.ts +13 -0
- package/dist/wirings/rpc/wire-addon.js +4 -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/index.d.ts +6 -6
- package/dist/wirings/workflow/index.js +2 -2
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +7 -7
- package/dist/wirings/workflow/pikku-scenario-service.js +39 -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/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 +1 -1
- 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 -1
- 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 +70 -94
- 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 +3 -43
- 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 +3 -6
- package/src/types/state.types.ts +16 -0
- 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 +2 -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 +34 -3
- package/src/wirings/rpc/addon-secrets.test.ts +261 -0
- package/src/wirings/rpc/rpc-runner.test.ts +2 -0
- package/src/wirings/rpc/rpc-runner.ts +2 -0
- package/src/wirings/rpc/rpc-types.ts +4 -0
- package/src/wirings/rpc/wire-addon.ts +17 -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/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/index.ts +2 -20
- package/src/wirings/workflow/pikku-scenario-service.ts +60 -15
- 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/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 +0 -9
- 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/services/user-flow-actors-service.js +0 -1
- 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
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
CoreAIAgent,
|
|
5
5
|
AIAgentMemoryConfig,
|
|
6
6
|
AIAgentInput,
|
|
7
|
+
AIAgentStep,
|
|
7
8
|
AIMessage,
|
|
8
9
|
} from './ai-agent.types.js'
|
|
9
10
|
import type { AIStorageService } from '../../services/ai-storage-service.js'
|
|
@@ -143,13 +144,7 @@ export async function saveMessages(
|
|
|
143
144
|
result: {
|
|
144
145
|
text: string
|
|
145
146
|
uiSpec?: unknown
|
|
146
|
-
steps:
|
|
147
|
-
toolCalls?: {
|
|
148
|
-
name: string
|
|
149
|
-
args: Record<string, unknown>
|
|
150
|
-
result: string
|
|
151
|
-
}[]
|
|
152
|
-
}[]
|
|
147
|
+
steps: Pick<AIAgentStep, 'toolCalls'>[]
|
|
153
148
|
}
|
|
154
149
|
): Promise<string> {
|
|
155
150
|
const responseText = memoryConfig?.workingMemory
|
|
@@ -179,6 +174,7 @@ export async function saveMessages(
|
|
|
179
174
|
id: toolCallIds[i],
|
|
180
175
|
name: tc.name,
|
|
181
176
|
result: tc.result,
|
|
177
|
+
...(tc.error ? { error: tc.error } : {}),
|
|
182
178
|
})),
|
|
183
179
|
createdAt: new Date(),
|
|
184
180
|
})
|
|
@@ -370,6 +366,39 @@ export function stripWorkingMemoryForStreaming(text: string): string {
|
|
|
370
366
|
return output
|
|
371
367
|
}
|
|
372
368
|
|
|
369
|
+
async function persistWorkingMemory(
|
|
370
|
+
options: {
|
|
371
|
+
storage?: AIStorageService
|
|
372
|
+
threadId: string
|
|
373
|
+
workingMemorySchemaName?: string | null
|
|
374
|
+
logger?: Logger
|
|
375
|
+
schemaService?: SchemaService
|
|
376
|
+
},
|
|
377
|
+
workingMemory: Record<string, unknown>
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
if (!options.storage) return
|
|
380
|
+
|
|
381
|
+
const existing =
|
|
382
|
+
(await options.storage.getWorkingMemory(options.threadId, 'thread')) ?? {}
|
|
383
|
+
const merged = deepMergeWorkingMemory(existing, workingMemory)
|
|
384
|
+
|
|
385
|
+
if (options.schemaService && options.workingMemorySchemaName) {
|
|
386
|
+
try {
|
|
387
|
+
await options.schemaService.validateSchema(
|
|
388
|
+
options.workingMemorySchemaName,
|
|
389
|
+
merged
|
|
390
|
+
)
|
|
391
|
+
} catch (err) {
|
|
392
|
+
options.logger?.warn(
|
|
393
|
+
`Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
394
|
+
)
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await options.storage.saveWorkingMemory(options.threadId, 'thread', merged)
|
|
400
|
+
}
|
|
401
|
+
|
|
373
402
|
export function createWorkingMemoryMiddleware(options: {
|
|
374
403
|
storage?: AIStorageService
|
|
375
404
|
threadId: string
|
|
@@ -382,6 +411,21 @@ export function createWorkingMemoryMiddleware(options: {
|
|
|
382
411
|
}> {
|
|
383
412
|
return {
|
|
384
413
|
modifyOutputStream: async (_services, { event, state }) => {
|
|
414
|
+
// A streamed step ends at its `usage` event, or at `done` for a model
|
|
415
|
+
// that reports no usage. That is the last moment the unstripped text is
|
|
416
|
+
// still reachable: it lives only in `state.rawText`, because the strip
|
|
417
|
+
// below runs before anything downstream — including the channel that
|
|
418
|
+
// accumulates what `modifyOutput` would later be handed.
|
|
419
|
+
if (event.type === 'usage' || event.type === 'done') {
|
|
420
|
+
const { workingMemory } = extractWorkingMemory(state.rawText ?? '')
|
|
421
|
+
state.rawText = ''
|
|
422
|
+
state.emittedVisibleText = ''
|
|
423
|
+
if (workingMemory) {
|
|
424
|
+
await persistWorkingMemory(options, workingMemory)
|
|
425
|
+
}
|
|
426
|
+
return event
|
|
427
|
+
}
|
|
428
|
+
|
|
385
429
|
if (event.type !== 'text-delta') return event
|
|
386
430
|
|
|
387
431
|
const rawText = `${state.rawText ?? ''}${event.text}`
|
|
@@ -400,38 +444,10 @@ export function createWorkingMemoryMiddleware(options: {
|
|
|
400
444
|
if (!delta) return null
|
|
401
445
|
return { ...event, text: delta }
|
|
402
446
|
},
|
|
403
|
-
modifyOutput: async (_services, { text, messages
|
|
447
|
+
modifyOutput: async (_services, { text, messages }) => {
|
|
404
448
|
const { workingMemory, cleanedText } = extractWorkingMemory(text)
|
|
405
|
-
if (workingMemory
|
|
406
|
-
|
|
407
|
-
(await options.storage.getWorkingMemory(
|
|
408
|
-
options.threadId,
|
|
409
|
-
'thread'
|
|
410
|
-
)) ?? {}
|
|
411
|
-
const merged = deepMergeWorkingMemory(existing, workingMemory)
|
|
412
|
-
|
|
413
|
-
let valid = true
|
|
414
|
-
if (options.schemaService && options.workingMemorySchemaName) {
|
|
415
|
-
try {
|
|
416
|
-
await options.schemaService.validateSchema(
|
|
417
|
-
options.workingMemorySchemaName,
|
|
418
|
-
merged
|
|
419
|
-
)
|
|
420
|
-
} catch (err) {
|
|
421
|
-
valid = false
|
|
422
|
-
options.logger?.warn(
|
|
423
|
-
`Working memory validation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
424
|
-
)
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (valid) {
|
|
429
|
-
await options.storage.saveWorkingMemory(
|
|
430
|
-
options.threadId,
|
|
431
|
-
'thread',
|
|
432
|
-
merged
|
|
433
|
-
)
|
|
434
|
-
}
|
|
449
|
+
if (workingMemory) {
|
|
450
|
+
await persistWorkingMemory(options, workingMemory)
|
|
435
451
|
}
|
|
436
452
|
|
|
437
453
|
return {
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { describe, test, beforeEach } from 'node:test'
|
|
1
|
+
import { describe, test, beforeEach, afterEach } from 'node:test'
|
|
2
2
|
import assert from 'node:assert'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
resolveModelAlias,
|
|
5
|
+
resolveModelConfig,
|
|
6
|
+
} from './ai-agent-model-config.js'
|
|
7
|
+
import { pikkuState, resetPikkuState } from '../../pikku-state.js'
|
|
8
|
+
|
|
9
|
+
const setAliases = (aliases: Record<string, string>) =>
|
|
10
|
+
pikkuState(null, 'agent', 'modelAliases', aliases)
|
|
5
11
|
|
|
6
12
|
beforeEach(() => {
|
|
7
13
|
resetPikkuState()
|
|
14
|
+
delete process.env.PIKKU_MODEL_ALIASES
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
delete process.env.PIKKU_MODEL_ALIASES
|
|
8
19
|
})
|
|
9
20
|
|
|
10
21
|
describe('resolveModelConfig', () => {
|
|
@@ -34,4 +45,62 @@ describe('resolveModelConfig', () => {
|
|
|
34
45
|
assert.strictEqual(result.temperature, undefined)
|
|
35
46
|
assert.strictEqual(result.maxSteps, undefined)
|
|
36
47
|
})
|
|
48
|
+
|
|
49
|
+
test('resolves an aliased agent model through the table', () => {
|
|
50
|
+
setAliases({ cheap: 'openai/gpt-5-mini' })
|
|
51
|
+
const result = resolveModelConfig('testAgent', {
|
|
52
|
+
model: 'cheap',
|
|
53
|
+
temperature: 0.2,
|
|
54
|
+
})
|
|
55
|
+
assert.strictEqual(result.model, 'openai/gpt-5-mini')
|
|
56
|
+
assert.strictEqual(result.temperature, 0.2)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('resolveModelAlias', () => {
|
|
61
|
+
test('a provider-qualified model is never treated as an alias', () => {
|
|
62
|
+
setAliases({ 'openai/gpt-4': 'anthropic/claude-3' })
|
|
63
|
+
assert.strictEqual(resolveModelAlias('openai/gpt-4'), 'openai/gpt-4')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('an alias resolves to its provider-qualified model', () => {
|
|
67
|
+
setAliases({
|
|
68
|
+
cheap: 'openai/gpt-5-mini',
|
|
69
|
+
tool: 'anthropic/claude-sonnet-5',
|
|
70
|
+
})
|
|
71
|
+
assert.strictEqual(resolveModelAlias('cheap'), 'openai/gpt-5-mini')
|
|
72
|
+
assert.strictEqual(resolveModelAlias('tool'), 'anthropic/claude-sonnet-5')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('PIKKU_MODEL_ALIASES beats the generated table', () => {
|
|
76
|
+
setAliases({ cheap: 'openai/gpt-5-mini' })
|
|
77
|
+
process.env.PIKKU_MODEL_ALIASES = 'cheap:anthropic/claude-haiku-4-5'
|
|
78
|
+
assert.strictEqual(resolveModelAlias('cheap'), 'anthropic/claude-haiku-4-5')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('an env override splits on the first colon only', () => {
|
|
82
|
+
// A model id may itself contain a colon — `ollama/qwen2.5:7b` is the shape
|
|
83
|
+
// the runner's own error message cites. It stays provider-qualified so the
|
|
84
|
+
// resolved value is one `VercelAIAgentRunner.parseModel` would accept; a
|
|
85
|
+
// bare `bedrock:nova-lite:1` would pass here and fail the moment anything
|
|
86
|
+
// tried to use it.
|
|
87
|
+
process.env.PIKKU_MODEL_ALIASES = 'cheap:ollama/qwen2.5:7b'
|
|
88
|
+
assert.strictEqual(resolveModelAlias('cheap'), 'ollama/qwen2.5:7b')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('an env override leaves aliases it does not name alone', () => {
|
|
92
|
+
setAliases({ cheap: 'openai/gpt-5-mini', tool: 'openai/gpt-5' })
|
|
93
|
+
process.env.PIKKU_MODEL_ALIASES = 'cheap:openai/gpt-5-nano'
|
|
94
|
+
assert.strictEqual(resolveModelAlias('tool'), 'openai/gpt-5')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('an unknown alias throws rather than reaching a provider', () => {
|
|
98
|
+
setAliases({ cheap: 'openai/gpt-5-mini' })
|
|
99
|
+
assert.throws(() => resolveModelAlias('expensive'), /expensive/)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('the error names the aliases that do exist', () => {
|
|
103
|
+
setAliases({ cheap: 'openai/gpt-5-mini', tool: 'openai/gpt-5' })
|
|
104
|
+
assert.throws(() => resolveModelAlias('exspensive'), /cheap, tool/)
|
|
105
|
+
})
|
|
37
106
|
})
|
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
// knowledge: decisions/internals/ai-agent-model-config-stays-a-single-resolution-seam.md
|
|
2
|
+
import { pikkuState } from '../../pikku-state.js'
|
|
3
|
+
|
|
4
|
+
const isProviderQualified = (model: string) => model.includes('/')
|
|
5
|
+
|
|
6
|
+
/** `PIKKU_MODEL_ALIASES=cheap:openai/gpt-5-mini,tool:anthropic/claude-sonnet-5` */
|
|
7
|
+
const envAliases = (): Record<string, string> => {
|
|
8
|
+
const raw = process.env.PIKKU_MODEL_ALIASES
|
|
9
|
+
if (!raw) return {}
|
|
10
|
+
const aliases: Record<string, string> = {}
|
|
11
|
+
for (const entry of raw.split(',')) {
|
|
12
|
+
// First colon only — a model id may contain its own.
|
|
13
|
+
const separator = entry.indexOf(':')
|
|
14
|
+
if (separator === -1) continue
|
|
15
|
+
const alias = entry.slice(0, separator).trim()
|
|
16
|
+
const model = entry.slice(separator + 1).trim()
|
|
17
|
+
if (alias && model) aliases[alias] = model
|
|
18
|
+
}
|
|
19
|
+
return aliases
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolves a model name to a concrete `provider/model`. Aliases come from the
|
|
24
|
+
* `models` table in pikku.config.json; a name containing `/` is already
|
|
25
|
+
* concrete. Read from the main package, not the calling addon's — which model
|
|
26
|
+
* a tier points at is the hosting app's decision.
|
|
27
|
+
*/
|
|
28
|
+
export const resolveModelAlias = (model: string): string => {
|
|
29
|
+
if (isProviderQualified(model)) return model
|
|
30
|
+
|
|
31
|
+
const generated = pikkuState(null, 'agent', 'modelAliases')
|
|
32
|
+
const override = envAliases()[model]
|
|
33
|
+
const resolved = override ?? generated[model]
|
|
34
|
+
|
|
35
|
+
if (!resolved) {
|
|
36
|
+
const known = Object.keys({ ...generated, ...envAliases() }).sort()
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Unknown model alias '${model}'. ` +
|
|
39
|
+
(known.length
|
|
40
|
+
? `Known aliases: ${known.join(', ')}. `
|
|
41
|
+
: `No aliases are configured. `) +
|
|
42
|
+
`Add it to the "models" table in pikku.config.json, or name a ` +
|
|
43
|
+
`provider-qualified model such as 'openai/gpt-5-mini'.`
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return resolved
|
|
48
|
+
}
|
|
49
|
+
|
|
2
50
|
export function resolveModelConfig(
|
|
3
51
|
_agentName: string,
|
|
4
52
|
agent: { model: string; temperature?: number; maxSteps?: number }
|
|
5
53
|
): { model: string; temperature?: number; maxSteps?: number } {
|
|
6
54
|
return {
|
|
7
|
-
model: agent.model,
|
|
55
|
+
model: resolveModelAlias(agent.model),
|
|
8
56
|
temperature: agent.temperature,
|
|
9
57
|
maxSteps: agent.maxSteps,
|
|
10
58
|
}
|
|
@@ -565,6 +565,8 @@ export async function buildToolDefs(
|
|
|
565
565
|
variableOverrides: capturedAddonConfig?.variableOverrides,
|
|
566
566
|
credentialOverrides:
|
|
567
567
|
capturedAddonConfig?.credentialOverrides,
|
|
568
|
+
globalSecrets: capturedAddonConfig?.globalSecrets,
|
|
569
|
+
globalCredentials: capturedAddonConfig?.globalCredentials,
|
|
568
570
|
}
|
|
569
571
|
: undefined
|
|
570
572
|
services = await getOrCreatePackageSingletonServices(
|
|
@@ -49,6 +49,11 @@ import {
|
|
|
49
49
|
notifyAfterStep,
|
|
50
50
|
toAccumulatedStep,
|
|
51
51
|
} from './ai-agent-turn.js'
|
|
52
|
+
import {
|
|
53
|
+
applyOutputMiddleware,
|
|
54
|
+
finalizeAgentRun,
|
|
55
|
+
lastUserMessageText,
|
|
56
|
+
} from './ai-agent-finalize.js'
|
|
52
57
|
import { resolveModelConfig } from './ai-agent-model-config.js'
|
|
53
58
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js'
|
|
54
59
|
import { randomUUID } from './ai-agent-utils.js'
|
|
@@ -303,20 +308,13 @@ export async function runAIAgent(
|
|
|
303
308
|
steps: accumulatedSteps,
|
|
304
309
|
}
|
|
305
310
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
messages: outputMessages,
|
|
314
|
-
usage: totalUsage,
|
|
315
|
-
})
|
|
316
|
-
outputText = modResult.text
|
|
317
|
-
outputMessages = modResult.messages
|
|
318
|
-
}
|
|
319
|
-
}
|
|
311
|
+
const { text: outputText, steps: outputSteps } =
|
|
312
|
+
await applyOutputMiddleware(aiMiddlewares, singletonServices, {
|
|
313
|
+
text: finalText,
|
|
314
|
+
messages: runnerParams.messages,
|
|
315
|
+
steps: result.steps,
|
|
316
|
+
usage: totalUsage,
|
|
317
|
+
})
|
|
320
318
|
|
|
321
319
|
await saveMessages(
|
|
322
320
|
storage,
|
|
@@ -325,14 +323,20 @@ export async function runAIAgent(
|
|
|
325
323
|
memoryConfig,
|
|
326
324
|
persistedUserMessage,
|
|
327
325
|
{
|
|
328
|
-
...result,
|
|
329
326
|
text: outputText,
|
|
327
|
+
steps: outputSteps,
|
|
330
328
|
uiSpec: structuredOutput.uiSpec,
|
|
331
329
|
}
|
|
332
330
|
)
|
|
333
331
|
|
|
334
|
-
await aiRunState
|
|
335
|
-
|
|
332
|
+
await finalizeAgentRun(aiRunState, {
|
|
333
|
+
runId,
|
|
334
|
+
agentName,
|
|
335
|
+
threadId,
|
|
336
|
+
resourceId: input.resourceId,
|
|
337
|
+
input: lastUserMessageText(runnerParams.messages),
|
|
338
|
+
text: outputText,
|
|
339
|
+
steps: outputSteps,
|
|
336
340
|
usage: { ...totalUsage, model: agent.model },
|
|
337
341
|
})
|
|
338
342
|
|
|
@@ -341,7 +345,7 @@ export async function runAIAgent(
|
|
|
341
345
|
text: outputText,
|
|
342
346
|
object: finalObject,
|
|
343
347
|
threadId,
|
|
344
|
-
steps:
|
|
348
|
+
steps: outputSteps,
|
|
345
349
|
usage: totalUsage,
|
|
346
350
|
}
|
|
347
351
|
} catch (error) {
|
|
@@ -462,6 +466,7 @@ export async function resumeAIAgentSync(
|
|
|
462
466
|
toolName: string
|
|
463
467
|
args: any
|
|
464
468
|
result: string
|
|
469
|
+
error?: string
|
|
465
470
|
}[] = []
|
|
466
471
|
|
|
467
472
|
for (const pending of savedPendingApprovals) {
|
|
@@ -471,6 +476,7 @@ export async function resumeAIAgentSync(
|
|
|
471
476
|
if (!claimedIds.has(toolCallId)) continue
|
|
472
477
|
|
|
473
478
|
let resultStr: string
|
|
479
|
+
let toolError: string | undefined
|
|
474
480
|
|
|
475
481
|
if (rejectedIds.has(toolCallId)) {
|
|
476
482
|
resultStr =
|
|
@@ -497,7 +503,8 @@ export async function resumeAIAgentSync(
|
|
|
497
503
|
if (err?.payload?.error === 'missing_credential') {
|
|
498
504
|
resultStr = JSON.stringify(err.payload)
|
|
499
505
|
} else {
|
|
500
|
-
|
|
506
|
+
toolError = err instanceof Error ? err.message : String(err)
|
|
507
|
+
resultStr = `Error: ${toolError}`
|
|
501
508
|
}
|
|
502
509
|
}
|
|
503
510
|
} else {
|
|
@@ -512,6 +519,7 @@ export async function resumeAIAgentSync(
|
|
|
512
519
|
? JSON.parse(pending.args)
|
|
513
520
|
: pending.args,
|
|
514
521
|
result: resultStr,
|
|
522
|
+
...(toolError ? { error: toolError } : {}),
|
|
515
523
|
})
|
|
516
524
|
}
|
|
517
525
|
|
|
@@ -541,7 +549,21 @@ export async function resumeAIAgentSync(
|
|
|
541
549
|
memoryConfig,
|
|
542
550
|
agentRunner,
|
|
543
551
|
params,
|
|
544
|
-
aiRunState
|
|
552
|
+
aiRunState,
|
|
553
|
+
// The approved tools were executed here, before the model was re-entered,
|
|
554
|
+
// so they belong to the run's step record — otherwise a tool that failed
|
|
555
|
+
// after approval leaves no trace on the run at all.
|
|
556
|
+
toolCallMessages.length > 0
|
|
557
|
+
? {
|
|
558
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
559
|
+
toolCalls: toolCallMessages.map((tc) => ({
|
|
560
|
+
name: tc.toolName,
|
|
561
|
+
args: (tc.args ?? {}) as Record<string, unknown>,
|
|
562
|
+
result: tc.result,
|
|
563
|
+
...(tc.error ? { error: tc.error } : {}),
|
|
564
|
+
})),
|
|
565
|
+
}
|
|
566
|
+
: undefined
|
|
545
567
|
)
|
|
546
568
|
}
|
|
547
569
|
|
|
@@ -554,7 +576,8 @@ async function continueAfterToolResultSync(
|
|
|
554
576
|
memoryConfig: AIAgentMemoryConfig | undefined,
|
|
555
577
|
agentRunner: AIAgentRunnerService,
|
|
556
578
|
params: RunAIAgentParams,
|
|
557
|
-
aiRunState: AIRunStateService
|
|
579
|
+
aiRunState: AIRunStateService,
|
|
580
|
+
resumedToolStep?: AIAgentStep
|
|
558
581
|
): Promise<AIAgentOutput> {
|
|
559
582
|
const singletonServices = getSingletonServices()
|
|
560
583
|
const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta')
|
|
@@ -639,6 +662,13 @@ async function continueAfterToolResultSync(
|
|
|
639
662
|
runnerParams.tools = trackToolExecution(runnerParams.tools, interruptHandle)
|
|
640
663
|
|
|
641
664
|
try {
|
|
665
|
+
// Kept out of `accumulatedSteps` deliberately: that array drives
|
|
666
|
+
// `saveMessages`, and the approved tool's messages were already written to
|
|
667
|
+
// the thread before the model was re-entered. It belongs to the run's step
|
|
668
|
+
// record, not to persistence.
|
|
669
|
+
const withResumedStep = (steps: AIAgentStep[]): AIAgentStep[] =>
|
|
670
|
+
resumedToolStep ? [resumedToolStep, ...steps] : steps
|
|
671
|
+
|
|
642
672
|
const accumulatedSteps: AIAgentStep[] = []
|
|
643
673
|
const totalUsage = { inputTokens: 0, outputTokens: 0 }
|
|
644
674
|
let lastStepResult: AIAgentStepResult | null = null
|
|
@@ -717,7 +747,7 @@ async function continueAfterToolResultSync(
|
|
|
717
747
|
runId: run.runId,
|
|
718
748
|
text: suspendedText,
|
|
719
749
|
threadId: run.threadId,
|
|
720
|
-
steps: accumulatedSteps,
|
|
750
|
+
steps: withResumedStep(accumulatedSteps),
|
|
721
751
|
usage: totalUsage,
|
|
722
752
|
status: 'suspended',
|
|
723
753
|
pendingApprovals: approvalsNeeded.map((a) => ({
|
|
@@ -741,20 +771,13 @@ async function continueAfterToolResultSync(
|
|
|
741
771
|
steps: accumulatedSteps,
|
|
742
772
|
}
|
|
743
773
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
messages: outputMessages,
|
|
752
|
-
usage: totalUsage,
|
|
753
|
-
})
|
|
754
|
-
outputText = modResult.text
|
|
755
|
-
outputMessages = modResult.messages
|
|
756
|
-
}
|
|
757
|
-
}
|
|
774
|
+
const { text: outputText, steps: outputSteps } =
|
|
775
|
+
await applyOutputMiddleware(aiMiddlewares, singletonServices, {
|
|
776
|
+
text: finalText,
|
|
777
|
+
messages: runnerParams.messages,
|
|
778
|
+
steps: withResumedStep(result.steps),
|
|
779
|
+
usage: totalUsage,
|
|
780
|
+
})
|
|
758
781
|
|
|
759
782
|
await saveMessages(
|
|
760
783
|
storage,
|
|
@@ -763,13 +786,21 @@ async function continueAfterToolResultSync(
|
|
|
763
786
|
memoryConfig,
|
|
764
787
|
null,
|
|
765
788
|
{
|
|
766
|
-
...result,
|
|
767
789
|
text: outputText,
|
|
790
|
+
// The approved tool's messages were written before the model was
|
|
791
|
+
// re-entered, so only the steps this leg generated are persisted here.
|
|
792
|
+
steps: accumulatedSteps,
|
|
768
793
|
}
|
|
769
794
|
)
|
|
770
795
|
|
|
771
|
-
await aiRunState
|
|
772
|
-
|
|
796
|
+
await finalizeAgentRun(aiRunState, {
|
|
797
|
+
runId: run.runId,
|
|
798
|
+
agentName: resolvedName,
|
|
799
|
+
threadId: run.threadId,
|
|
800
|
+
resourceId: run.resourceId,
|
|
801
|
+
input: lastUserMessageText(runnerParams.messages),
|
|
802
|
+
text: outputText,
|
|
803
|
+
steps: outputSteps,
|
|
773
804
|
usage: { ...totalUsage, model: agent.model },
|
|
774
805
|
})
|
|
775
806
|
|
|
@@ -778,7 +809,7 @@ async function continueAfterToolResultSync(
|
|
|
778
809
|
text: outputText,
|
|
779
810
|
object: finalObject,
|
|
780
811
|
threadId: run.threadId,
|
|
781
|
-
steps:
|
|
812
|
+
steps: outputSteps,
|
|
782
813
|
usage: totalUsage,
|
|
783
814
|
}
|
|
784
815
|
} catch (error) {
|