@pikku/core 0.12.86 → 0.12.88
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 +79 -0
- package/dist/dev/hot-reload.d.ts +1 -1
- package/dist/dev/hot-reload.js +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +1 -1
- package/dist/wirings/agent/agent-memory.d.ts +11 -0
- package/dist/wirings/agent/agent-memory.js +17 -2
- package/dist/wirings/agent/agent-stream.js +115 -91
- package/dist/wirings/agent/agent.types.d.ts +9 -8
- package/dist/wirings/variable/validate-variable-definitions.js +2 -2
- package/dist/wirings/variable/variable.types.d.ts +13 -7
- package/dist/wirings/workflow/pikku-scenario-service.d.ts +2 -2
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +20 -6
- package/dist/wirings/workflow/pikku-workflow-service.js +25 -25
- package/dist/wirings/workflow/workflow-step-claim.d.ts +16 -0
- package/dist/wirings/workflow/workflow-step-claim.js +27 -0
- package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +47 -0
- package/knowledge/decisions/security/index.md +2 -1
- package/knowledge/decisions/security/scaffold-features-are-authenticated-unless-opted-out.md +6 -0
- package/package.json +1 -1
- package/src/dev/hot-reload.ts +1 -1
- package/src/types/index.ts +24 -1
- package/src/wirings/agent/agent-memory.test.ts +73 -0
- package/src/wirings/agent/agent-memory.ts +16 -2
- package/src/wirings/agent/agent-middleware.types.test.ts +41 -0
- package/src/wirings/agent/agent-stream-delegate.test.ts +381 -0
- package/src/wirings/agent/agent-stream.ts +158 -78
- package/src/wirings/agent/agent.types.ts +9 -8
- package/src/wirings/variable/validate-variable-definitions.test.ts +11 -10
- package/src/wirings/variable/validate-variable-definitions.ts +2 -2
- package/src/wirings/variable/variable.types.ts +13 -7
- package/src/wirings/workflow/pikku-scenario-service.ts +29 -6
- package/src/wirings/workflow/pikku-workflow-service.ts +31 -27
- package/src/wirings/workflow/workflow-step-claim.ts +46 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.type-tests.json +2 -1
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
CorePikkuMiddleware,
|
|
7
7
|
MiddlewareMetadata,
|
|
8
8
|
} from '../../middleware/middleware.types.js'
|
|
9
|
+
import type { CoreSingletonServices } from '../../types/core.types.js'
|
|
9
10
|
import type { PermissionMetadata } from '../../function/function-meta.types.js'
|
|
10
11
|
import type { AIProviderOptions } from '../../services/agent-runner-service.js'
|
|
11
12
|
import type { PikkuChannel } from '../channel/channel.types.js'
|
|
@@ -180,10 +181,10 @@ export interface AgentToolDef extends Partial<ApprovalPolicy> {
|
|
|
180
181
|
|
|
181
182
|
export interface PikkuAgentMiddlewareHooks<
|
|
182
183
|
State extends Record<string, unknown> = Record<string, unknown>,
|
|
183
|
-
|
|
184
|
+
SingletonServices extends CoreSingletonServices = CoreSingletonServices,
|
|
184
185
|
> {
|
|
185
186
|
modifyInput?: (
|
|
186
|
-
services:
|
|
187
|
+
services: SingletonServices,
|
|
187
188
|
ctx: {
|
|
188
189
|
messages: AgentMessage[]
|
|
189
190
|
instructions: string
|
|
@@ -210,7 +211,7 @@ export interface PikkuAgentMiddlewareHooks<
|
|
|
210
211
|
| { messages: AgentMessage[]; instructions: string }
|
|
211
212
|
|
|
212
213
|
modifyOutputStream?: (
|
|
213
|
-
services:
|
|
214
|
+
services: SingletonServices,
|
|
214
215
|
ctx: {
|
|
215
216
|
event: AgentStreamEvent
|
|
216
217
|
allEvents: readonly AgentStreamEvent[]
|
|
@@ -262,7 +263,7 @@ export interface PikkuAgentMiddlewareHooks<
|
|
|
262
263
|
* leaves them untouched.
|
|
263
264
|
*/
|
|
264
265
|
modifyOutput?: (
|
|
265
|
-
services:
|
|
266
|
+
services: SingletonServices,
|
|
266
267
|
ctx: {
|
|
267
268
|
text: string
|
|
268
269
|
messages: AgentMessage[]
|
|
@@ -282,7 +283,7 @@ export interface PikkuAgentMiddlewareHooks<
|
|
|
282
283
|
}
|
|
283
284
|
|
|
284
285
|
beforeToolCall?: (
|
|
285
|
-
services:
|
|
286
|
+
services: SingletonServices,
|
|
286
287
|
ctx: {
|
|
287
288
|
toolName: string
|
|
288
289
|
toolCallId: string
|
|
@@ -294,7 +295,7 @@ export interface PikkuAgentMiddlewareHooks<
|
|
|
294
295
|
| void
|
|
295
296
|
|
|
296
297
|
afterToolCall?: (
|
|
297
|
-
services:
|
|
298
|
+
services: SingletonServices,
|
|
298
299
|
ctx: {
|
|
299
300
|
toolName: string
|
|
300
301
|
toolCallId: string
|
|
@@ -305,7 +306,7 @@ export interface PikkuAgentMiddlewareHooks<
|
|
|
305
306
|
) => Promise<{ result: unknown } | void> | { result: unknown } | void
|
|
306
307
|
|
|
307
308
|
afterStep?: (
|
|
308
|
-
services:
|
|
309
|
+
services: SingletonServices,
|
|
309
310
|
ctx: {
|
|
310
311
|
stepNumber: number
|
|
311
312
|
text: string
|
|
@@ -323,7 +324,7 @@ export interface PikkuAgentMiddlewareHooks<
|
|
|
323
324
|
) => Promise<void> | void
|
|
324
325
|
|
|
325
326
|
onError?: (
|
|
326
|
-
services:
|
|
327
|
+
services: SingletonServices,
|
|
327
328
|
ctx: {
|
|
328
329
|
error: Error
|
|
329
330
|
stepNumber: number
|
|
@@ -89,26 +89,27 @@ describe('validateAndBuildVariableDefinitionsMeta', () => {
|
|
|
89
89
|
assert.deepStrictEqual(result, {})
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
test('carries
|
|
92
|
+
test('carries optional into the meta so only the opted-out ones skip the gate', () => {
|
|
93
93
|
const result = validateAndBuildVariableDefinitionsMeta(
|
|
94
94
|
[
|
|
95
|
-
{
|
|
96
|
-
name: 'consoleUrl',
|
|
97
|
-
displayName: 'Console URL',
|
|
98
|
-
variableId: 'CONSOLE_URL',
|
|
99
|
-
required: true,
|
|
100
|
-
sourceFile: 'a.ts',
|
|
101
|
-
},
|
|
102
95
|
{
|
|
103
96
|
name: 'corsOrigins',
|
|
104
97
|
displayName: 'Allowed Browser Origins',
|
|
105
98
|
variableId: 'CORS_ORIGINS',
|
|
99
|
+
optional: true,
|
|
100
|
+
sourceFile: 'a.ts',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'consoleUrl',
|
|
104
|
+
displayName: 'Console URL',
|
|
105
|
+
variableId: 'CONSOLE_URL',
|
|
106
106
|
sourceFile: 'a.ts',
|
|
107
107
|
},
|
|
108
108
|
] as any,
|
|
109
109
|
new Map()
|
|
110
110
|
)
|
|
111
|
-
assert.strictEqual(result['
|
|
112
|
-
|
|
111
|
+
assert.strictEqual(result['corsOrigins']!.optional, true)
|
|
112
|
+
// Undeclared means required, which is what makes the gate ask by default.
|
|
113
|
+
assert.strictEqual(result['consoleUrl']!.optional, undefined)
|
|
113
114
|
})
|
|
114
115
|
})
|
|
@@ -43,7 +43,7 @@ export function validateAndBuildVariableDefinitionsMeta(
|
|
|
43
43
|
description: def.description,
|
|
44
44
|
variableId: def.variableId,
|
|
45
45
|
schema: def.schema,
|
|
46
|
-
|
|
46
|
+
optional: def.optional,
|
|
47
47
|
docsUrl: def.docsUrl,
|
|
48
48
|
sourceFile: def.sourceFile,
|
|
49
49
|
}
|
|
@@ -60,7 +60,7 @@ export function validateAndBuildVariableDefinitionsMeta(
|
|
|
60
60
|
description: def.description,
|
|
61
61
|
variableId: def.variableId,
|
|
62
62
|
schema: def.schema,
|
|
63
|
-
|
|
63
|
+
optional: def.optional,
|
|
64
64
|
docsUrl: def.docsUrl,
|
|
65
65
|
sourceFile: def.sourceFile,
|
|
66
66
|
}
|
|
@@ -5,13 +5,19 @@ export type CoreVariable<T = unknown> = {
|
|
|
5
5
|
variableId: string
|
|
6
6
|
schema: T
|
|
7
7
|
/**
|
|
8
|
-
* A variable is
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* A variable is REQUIRED by default, and marking it `optional` is how a
|
|
9
|
+
* declaration says its absence is a supported state. Same flag, same
|
|
10
|
+
* polarity and same meaning as `CoreSecret.optional` — one word to learn
|
|
11
|
+
* rather than two with opposite senses.
|
|
12
|
+
*
|
|
13
|
+
* Defaulting to required rather than following `variables.get`'s
|
|
14
|
+
* `T | undefined` return is deliberate. That signature describes what a
|
|
15
|
+
* caller must HANDLE, not whether a deployment is correct without the value:
|
|
16
|
+
* an undefined feature flag is fine, an undefined API base URL is an outage
|
|
17
|
+
* that the type system cannot tell apart. Declaring the difference is the
|
|
18
|
+
* point of the flag, and the safe default for an undeclared one is to ask.
|
|
13
19
|
*/
|
|
14
|
-
|
|
20
|
+
optional?: boolean
|
|
15
21
|
docsUrl?: string
|
|
16
22
|
}
|
|
17
23
|
|
|
@@ -21,7 +27,7 @@ export type VariableDefinitionMeta = {
|
|
|
21
27
|
description?: string
|
|
22
28
|
variableId: string
|
|
23
29
|
schema?: Record<string, unknown> | string
|
|
24
|
-
|
|
30
|
+
optional?: boolean
|
|
25
31
|
docsUrl?: string
|
|
26
32
|
sourceFile?: string
|
|
27
33
|
}
|
|
@@ -59,8 +59,34 @@ const assertionStep = <T extends WorkflowStepOptions>(
|
|
|
59
59
|
({ ...options, retries: options?.retries ?? 0 }) as T & { retries: number }
|
|
60
60
|
|
|
61
61
|
export { addFeature, resolveFeatureScenarios } from './feature.js'
|
|
62
|
-
export type
|
|
63
|
-
|
|
62
|
+
export type {
|
|
63
|
+
CoreFeature,
|
|
64
|
+
CoreFeatureScenario,
|
|
65
|
+
FeatureMeta,
|
|
66
|
+
FeaturesMeta,
|
|
67
|
+
PikkuBrowserWire,
|
|
68
|
+
PikkuScenarioWire,
|
|
69
|
+
ScenarioBrowserFailure,
|
|
70
|
+
ScenarioBrowserProvider,
|
|
71
|
+
ScenarioEnvironment,
|
|
72
|
+
ScenarioStepKind,
|
|
73
|
+
ScenarioStepMeta,
|
|
74
|
+
ScenarioStepOptions,
|
|
75
|
+
ScenarioStepPhase,
|
|
76
|
+
ScenarioSurface,
|
|
77
|
+
TestIdSelector,
|
|
78
|
+
} from './scenario.types.js'
|
|
79
|
+
export type {
|
|
80
|
+
ScenarioArtifact,
|
|
81
|
+
ScenarioFailureDetail,
|
|
82
|
+
ScenarioResult,
|
|
83
|
+
ScenarioRunRecord,
|
|
84
|
+
ScenarioRunReport,
|
|
85
|
+
ScenarioRunStatus,
|
|
86
|
+
ScenarioRunStore,
|
|
87
|
+
ScenarioRunSummary,
|
|
88
|
+
ScenarioStepRow,
|
|
89
|
+
} from './scenario-run.types.js'
|
|
64
90
|
export { SCENARIO_SURFACES } from './scenario-step.types.js'
|
|
65
91
|
|
|
66
92
|
// Which of a step's bindings run: one for an action, every witness for a `then`
|
|
@@ -979,10 +1005,7 @@ export class PikkuScenarioService implements WorkflowRunExtension {
|
|
|
979
1005
|
* Whether a step is driven by a persona, and so must be given one. Stamped by
|
|
980
1006
|
* the definer from a `browser` binding or an explicit `actor: true`.
|
|
981
1007
|
*/
|
|
982
|
-
private requiresActor(
|
|
983
|
-
packageName: string | null,
|
|
984
|
-
stepFunc: string
|
|
985
|
-
): boolean {
|
|
1008
|
+
private requiresActor(packageName: string | null, stepFunc: string): boolean {
|
|
986
1009
|
return (
|
|
987
1010
|
this.scenarioStepConfig(packageName, stepFunc)?.requiresActor === true
|
|
988
1011
|
)
|
|
@@ -68,7 +68,6 @@ import {
|
|
|
68
68
|
WorkflowRunCancelledError,
|
|
69
69
|
WorkflowRunFailedError,
|
|
70
70
|
WorkflowRunNotFoundError,
|
|
71
|
-
WorkflowStepFunctionMismatchError,
|
|
72
71
|
WorkflowStepNameNotString,
|
|
73
72
|
WorkflowSuspendedException,
|
|
74
73
|
} from './workflow-errors.js'
|
|
@@ -95,6 +94,7 @@ import {
|
|
|
95
94
|
} from './workflow-approval.js'
|
|
96
95
|
import { auditApprovalDecision } from './workflow-approval-audit.js'
|
|
97
96
|
import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js'
|
|
97
|
+
import { claimStepByReadThenWrite } from './workflow-step-claim.js'
|
|
98
98
|
import {
|
|
99
99
|
RedispatchBackoff,
|
|
100
100
|
sweepStalledRuns,
|
|
@@ -639,12 +639,12 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
639
639
|
*
|
|
640
640
|
* Returns nothing by default so a store that cannot express the query keeps
|
|
641
641
|
* working unchanged — and, because it does not opt in, gains no re-dispatches
|
|
642
|
-
* either. A store must have an atomic `
|
|
643
|
-
* or no concurrency for one to exclude: the relay makes
|
|
644
|
-
* routine, and the claim
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
642
|
+
* either. A store must have an atomic `claimStepForExecution` before
|
|
643
|
+
* overriding this, or no concurrency for one to exclude: the relay makes
|
|
644
|
+
* duplicate dispatch routine, and the claim is what keeps a duplicate from
|
|
645
|
+
* becoming a second execution. Every `@pikku/kysely` dialect qualifies on its
|
|
646
|
+
* status-guarded claim, `in-memory` on being inline and single-process;
|
|
647
|
+
* `mongodb` still qualifies on neither.
|
|
648
648
|
*/
|
|
649
649
|
protected async findUndispatchedSteps(
|
|
650
650
|
_before: Date,
|
|
@@ -1300,6 +1300,29 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1300
1300
|
}
|
|
1301
1301
|
}
|
|
1302
1302
|
|
|
1303
|
+
/**
|
|
1304
|
+
* Take sole ownership of a step before it runs, returning the state to run
|
|
1305
|
+
* under — or `null` when another dispatch already owns it.
|
|
1306
|
+
*
|
|
1307
|
+
* Dispatch is at-least-once by design: the relay re-dispatches steps it
|
|
1308
|
+
* believes were dropped, and a queue can redeliver a job it already handed
|
|
1309
|
+
* out. This is the one place that keeps a duplicate dispatch from becoming a
|
|
1310
|
+
* second execution of a side-effecting step, so it is only as strong as the
|
|
1311
|
+
* exclusion it is built on — and `withStepLock` excludes nothing unless the
|
|
1312
|
+
* store backs it with a real primitive. A store able to express the decision
|
|
1313
|
+
* as one conditional write should override this rather than reach for a lock,
|
|
1314
|
+
* which is what `@pikku/kysely` does with a status-guarded `UPDATE`.
|
|
1315
|
+
*/
|
|
1316
|
+
protected async claimStepForExecution(
|
|
1317
|
+
runId: string,
|
|
1318
|
+
stepName: string,
|
|
1319
|
+
rpcName: string
|
|
1320
|
+
): Promise<StepState | null> {
|
|
1321
|
+
return this.withStepLock(runId, stepName, () =>
|
|
1322
|
+
claimStepByReadThenWrite(this, runId, stepName, rpcName)
|
|
1323
|
+
)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1303
1326
|
private async executeWorkflowStepInner(
|
|
1304
1327
|
runId: string,
|
|
1305
1328
|
stepName: string,
|
|
@@ -1307,26 +1330,7 @@ export abstract class PikkuWorkflowService implements WorkflowService {
|
|
|
1307
1330
|
data: any,
|
|
1308
1331
|
rpcService: PikkuRPC
|
|
1309
1332
|
): Promise<void> {
|
|
1310
|
-
const claimed = await this.
|
|
1311
|
-
const stepState = await this.getStepState(runId, stepName)
|
|
1312
|
-
// knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
|
|
1313
|
-
if (
|
|
1314
|
-
stepState.rpcName !== undefined &&
|
|
1315
|
-
stepState.rpcName !== (rpcName ?? null)
|
|
1316
|
-
) {
|
|
1317
|
-
throw new WorkflowStepFunctionMismatchError(runId, stepName)
|
|
1318
|
-
}
|
|
1319
|
-
if (stepState.status === 'succeeded' || stepState.status === 'running') {
|
|
1320
|
-
return null
|
|
1321
|
-
}
|
|
1322
|
-
if (stepState.status === 'failed') {
|
|
1323
|
-
return this.createRetryAttempt(stepState.stepId, 'running')
|
|
1324
|
-
}
|
|
1325
|
-
if (stepState.status === 'pending' || stepState.status === 'scheduled') {
|
|
1326
|
-
await this.setStepRunning(stepState.stepId)
|
|
1327
|
-
}
|
|
1328
|
-
return stepState
|
|
1329
|
-
})
|
|
1333
|
+
const claimed = await this.claimStepForExecution(runId, stepName, rpcName)
|
|
1330
1334
|
|
|
1331
1335
|
if (!claimed) {
|
|
1332
1336
|
return
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { WorkflowStepFunctionMismatchError } from './workflow-errors.js'
|
|
2
|
+
import type { StepState } from './workflow.types.js'
|
|
3
|
+
|
|
4
|
+
/** What claiming a step needs from the workflow service. */
|
|
5
|
+
export type StepClaimStore = {
|
|
6
|
+
getStepState(runId: string, stepName: string): Promise<StepState>
|
|
7
|
+
setStepRunning(stepId: string): Promise<void>
|
|
8
|
+
createRetryAttempt(
|
|
9
|
+
failedStepId: string,
|
|
10
|
+
status: 'pending' | 'running'
|
|
11
|
+
): Promise<StepState>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Decide whether this dispatch owns the step, by reading its state and then
|
|
16
|
+
* writing it — which only excludes a concurrent dispatch when the caller holds
|
|
17
|
+
* a lock that genuinely excludes one.
|
|
18
|
+
*
|
|
19
|
+
* A store that can express the whole decision as a single conditional write
|
|
20
|
+
* should do that instead of calling this.
|
|
21
|
+
*/
|
|
22
|
+
export const claimStepByReadThenWrite = async (
|
|
23
|
+
store: StepClaimStore,
|
|
24
|
+
runId: string,
|
|
25
|
+
stepName: string,
|
|
26
|
+
rpcName: string
|
|
27
|
+
): Promise<StepState | null> => {
|
|
28
|
+
const stepState = await store.getStepState(runId, stepName)
|
|
29
|
+
// knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
|
|
30
|
+
if (
|
|
31
|
+
stepState.rpcName !== undefined &&
|
|
32
|
+
stepState.rpcName !== (rpcName ?? null)
|
|
33
|
+
) {
|
|
34
|
+
throw new WorkflowStepFunctionMismatchError(runId, stepName)
|
|
35
|
+
}
|
|
36
|
+
if (stepState.status === 'succeeded' || stepState.status === 'running') {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
if (stepState.status === 'failed') {
|
|
40
|
+
return store.createRetryAttempt(stepState.stepId, 'running')
|
|
41
|
+
}
|
|
42
|
+
if (stepState.status === 'pending' || stepState.status === 'scheduled') {
|
|
43
|
+
await store.setStepRunning(stepState.stepId)
|
|
44
|
+
}
|
|
45
|
+
return stepState
|
|
46
|
+
}
|