@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/dist/dev/hot-reload.d.ts +1 -1
  3. package/dist/dev/hot-reload.js +1 -1
  4. package/dist/types/index.d.ts +1 -1
  5. package/dist/types/index.js +1 -1
  6. package/dist/wirings/agent/agent-memory.d.ts +11 -0
  7. package/dist/wirings/agent/agent-memory.js +17 -2
  8. package/dist/wirings/agent/agent-stream.js +115 -91
  9. package/dist/wirings/agent/agent.types.d.ts +9 -8
  10. package/dist/wirings/variable/validate-variable-definitions.js +2 -2
  11. package/dist/wirings/variable/variable.types.d.ts +13 -7
  12. package/dist/wirings/workflow/pikku-scenario-service.d.ts +2 -2
  13. package/dist/wirings/workflow/pikku-workflow-service.d.ts +20 -6
  14. package/dist/wirings/workflow/pikku-workflow-service.js +25 -25
  15. package/dist/wirings/workflow/workflow-step-claim.d.ts +16 -0
  16. package/dist/wirings/workflow/workflow-step-claim.js +27 -0
  17. package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +47 -0
  18. package/knowledge/decisions/security/index.md +2 -1
  19. package/knowledge/decisions/security/scaffold-features-are-authenticated-unless-opted-out.md +6 -0
  20. package/package.json +1 -1
  21. package/src/dev/hot-reload.ts +1 -1
  22. package/src/types/index.ts +24 -1
  23. package/src/wirings/agent/agent-memory.test.ts +73 -0
  24. package/src/wirings/agent/agent-memory.ts +16 -2
  25. package/src/wirings/agent/agent-middleware.types.test.ts +41 -0
  26. package/src/wirings/agent/agent-stream-delegate.test.ts +381 -0
  27. package/src/wirings/agent/agent-stream.ts +158 -78
  28. package/src/wirings/agent/agent.types.ts +9 -8
  29. package/src/wirings/variable/validate-variable-definitions.test.ts +11 -10
  30. package/src/wirings/variable/validate-variable-definitions.ts +2 -2
  31. package/src/wirings/variable/variable.types.ts +13 -7
  32. package/src/wirings/workflow/pikku-scenario-service.ts +29 -6
  33. package/src/wirings/workflow/pikku-workflow-service.ts +31 -27
  34. package/src/wirings/workflow/workflow-step-claim.ts +46 -0
  35. package/tsconfig.tsbuildinfo +1 -1
  36. package/tsconfig.type-tests.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,82 @@
1
+ ## 0.12.88
2
+
3
+ ### Patch Changes
4
+
5
+ - 4712e73: fix: collect working memory from a delegate-mode parent agent
6
+
7
+ A delegating parent's `text-delta` events were dropped at the outermost output
8
+ channel, above the working-memory hook, so every `<working_memory>` block it
9
+ wrote from its first hand-off onward was discarded before anything could read
10
+ it. The parent's text is now routed through the working-memory hook and into a
11
+ sink instead of being dropped outright, so the blocks are collected while the
12
+ client, the thread history and user channel middleware still see nothing.
13
+
14
+ The resume path built no delegate filter at all and streamed a delegating
15
+ parent's text to the client after an approval; it now suppresses text the same
16
+ way the initial path does.
17
+
18
+ The AI SDK rejects a system message inside `messages` outright, so the working
19
+ memory prompt the framework injects as one failed every run that enabled
20
+ working memory at all. The runner now lifts system messages onto the `system`
21
+ option, after the agent's own instructions.
22
+
23
+ - 082403f: fix(agent): make working-memory array semantics explicit
24
+
25
+ `deepMergeWorkingMemory` replaced arrays wholesale as a side effect of its
26
+ object-recursion guard, so nothing in the code said whether that was the
27
+ contract or an accident. The merge now handles arrays in an explicit,
28
+ documented branch. Replace is kept over append: the full state is echoed back
29
+ every turn, so appending would duplicate every item whenever the model re-emitted
30
+ the array.
31
+
32
+ `buildWorkingMemoryPrompt` now states that contract to the model rather than
33
+ leaving "only include changed fields" to be read as permission to send a partial
34
+ array. This is defensive: measured against `gpt-4.1-mini` the wording changes
35
+ nothing, because that model already re-emits the whole list. It is there for
36
+ models that do not.
37
+
38
+ ## 0.12.87
39
+
40
+ ### Patch Changes
41
+
42
+ - 9687ad1: fix: hand agent middleware the singleton services its type promises
43
+
44
+ `PikkuAgentMiddlewareHooks` typed its `services` parameter as the project's full
45
+ wire `Services`, while every runtime call site has only ever passed the singleton
46
+ services. A middleware that destructured a wire service typechecked and silently
47
+ received `undefined`.
48
+
49
+ The hooks are now bounded by `CoreSingletonServices` in core, and the generated
50
+ `pikkuAgentMiddleware` defaults to `WiredSingletonServices` like the other
51
+ middleware definers. Nothing changes at runtime: agent middleware hooks a _run_,
52
+ and a run is not a request — it can start from a scheduler or a workflow with no
53
+ wire behind it. A tool the run calls is an ordinary function call and still gets
54
+ its own wire services through `runPikkuFunc`.
55
+
56
+ - 2d21628: fix(kysely): claim a workflow step atomically in every SQL dialect
57
+
58
+ The workflow engine's "atomic claim" was a read-then-write guarded by
59
+ `withStepLock`, and `@pikku/kysely` inherited a silent pass-through for that
60
+ lock — so on every dialect but Postgres and MySQL a redelivered queue job could
61
+ claim a step another dispatch was already running, executing a side-effecting
62
+ step twice.
63
+
64
+ `@pikku/kysely` now claims the step with a status-guarded `UPDATE` and reads the
65
+ affected-row count, which is atomic in every SQL dialect without an
66
+ advisory-lock primitive. Relay redispatch is enabled for all Kysely dialects as
67
+ a result, not just Postgres and MySQL.
68
+
69
+ - 985b87b: Follow through on the variable `required` → `optional` rename: regenerate the
70
+ core API report and update the inspector's `defineVariable` gating-flag test,
71
+ both of which #1369 left describing the deleted `required` flag.
72
+ - 3a83f85: Stop re-exporting package internals through entry points
73
+
74
+ 66 names reached consumers only because an `export *` in an entry point swept
75
+ them up. Each one is referenced solely inside its own package, so the star is
76
+ now an explicit named re-export listing what is genuinely public. The
77
+ declarations themselves are untouched — this narrows the entry point, not the
78
+ module.
79
+
1
80
  ## 0.12.86
2
81
 
3
82
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  import type { Logger } from '../services/logger.js';
2
- export * from './reload-meta.js';
2
+ export { reloadGeneratedMeta, reconcileAddonRegistry } from './reload-meta.js';
3
3
  interface PikkuDevReloaderOptions {
4
4
  srcDirectories: string[];
5
5
  logger: Logger;
@@ -7,7 +7,7 @@ import { clearPermissionsCache } from '../permissions.js';
7
7
  import { clearChannelMiddlewareCache } from '../wirings/channel/channel-middleware-runner.js';
8
8
  import { httpRouter } from '../wirings/http/routers/http-router.js';
9
9
  import { createModuleRunner } from './module-runner.js';
10
- export * from './reload-meta.js';
10
+ export { reloadGeneratedMeta, reconcileAddonRegistry } from './reload-meta.js';
11
11
  const isFunctionConfig = (value) => {
12
12
  return (typeof value === 'object' &&
13
13
  value !== null &&
@@ -1,2 +1,2 @@
1
- export * from './core.types.js';
1
+ export type { AuthInstance, CommonWireMeta, CoreConfig, CoreServices, CoreSingletonServices, CoreUserSession, CreateConfig, CreateSingletonServices, CreateWireServices, GetCredential, PikkuWire, PikkuWiringTypes, PostgresConfig, SecretlessServices, SecurityAuditIssue, SecurityAuditReport, SecurityAuditSummary, SecurityAuditUpdate, SecuritySeverity, SecurityUpdateLevel, ServerLifecycle, WireServices, } from './core.types.js';
2
2
  export type * from './state.types.js';
@@ -1 +1 @@
1
- export * from './core.types.js';
1
+ export {};
@@ -8,6 +8,17 @@ export declare function resolveMemoryServices(agent: CoreAgent, singletonService
8
8
  storage: AgentStorageService | undefined;
9
9
  };
10
10
  export declare function isWorkingMemoryEnabled(memoryConfig: AgentMemoryConfig | undefined, storage: AgentStorageService | undefined): boolean;
11
+ /**
12
+ * Merges a model-emitted working memory update into the stored state.
13
+ *
14
+ * Objects merge key by key, and `null` deletes a key. Arrays are replaced
15
+ * wholesale — deliberately, not as a side effect of the object guard below.
16
+ * The full state is echoed back to the model every turn by
17
+ * {@link buildWorkingMemoryPrompt}, so it can always re-emit an array in full,
18
+ * and appending instead would duplicate every item each time it did.
19
+ * `buildWorkingMemoryPrompt` states this contract to the model; the two must
20
+ * stay in agreement.
21
+ */
11
22
  export declare function deepMergeWorkingMemory(existing: Record<string, unknown>, updates: Record<string, unknown>): Record<string, unknown>;
12
23
  export declare function buildWorkingMemoryPrompt(currentState: Record<string, unknown> | null, jsonSchema?: Record<string, unknown>): string;
13
24
  export declare function loadContextMessages(memoryConfig: AgentMemoryConfig | undefined, storage: AgentStorageService | undefined, input: AgentInput, workingMemoryJsonSchema?: Record<string, unknown>): Promise<AgentMessage[]>;
@@ -11,6 +11,17 @@ export function resolveMemoryServices(agent, singletonServices) {
11
11
  export function isWorkingMemoryEnabled(memoryConfig, storage) {
12
12
  return !!memoryConfig?.workingMemory && !!storage;
13
13
  }
14
+ /**
15
+ * Merges a model-emitted working memory update into the stored state.
16
+ *
17
+ * Objects merge key by key, and `null` deletes a key. Arrays are replaced
18
+ * wholesale — deliberately, not as a side effect of the object guard below.
19
+ * The full state is echoed back to the model every turn by
20
+ * {@link buildWorkingMemoryPrompt}, so it can always re-emit an array in full,
21
+ * and appending instead would duplicate every item each time it did.
22
+ * `buildWorkingMemoryPrompt` states this contract to the model; the two must
23
+ * stay in agreement.
24
+ */
14
25
  export function deepMergeWorkingMemory(existing, updates) {
15
26
  const result = { ...existing };
16
27
  for (const key of Object.keys(updates)) {
@@ -21,8 +32,10 @@ export function deepMergeWorkingMemory(existing, updates) {
21
32
  if (value === null) {
22
33
  delete result[key];
23
34
  }
35
+ else if (Array.isArray(value)) {
36
+ result[key] = value;
37
+ }
24
38
  else if (typeof value === 'object' &&
25
- !Array.isArray(value) &&
26
39
  typeof result[key] === 'object' &&
27
40
  result[key] !== null &&
28
41
  !Array.isArray(result[key])) {
@@ -56,7 +69,9 @@ export function buildWorkingMemoryPrompt(currentState, jsonSchema) {
56
69
  parts.push('When you learn new information, output a partial JSON update in <working_memory> tags. ' +
57
70
  'Only include durable facts you have actually derived or the user has confirmed. ' +
58
71
  'Do not output templates, placeholders, or narration. ' +
59
- 'Only include changed fields. Leave unknown fields untouched. Set a field to null to delete it.');
72
+ 'Only include changed fields. Leave unknown fields untouched. Set a field to null to delete it. ' +
73
+ 'Arrays are replaced, not merged: to change one, repeat every item you want to keep alongside the new ones, ' +
74
+ 'because any item you leave out is deleted.');
60
75
  return parts.join('\n\n');
61
76
  }
62
77
  export async function loadContextMessages(memoryConfig, storage, input, workingMemoryJsonSchema) {
@@ -203,6 +203,51 @@ async function postStreamCleanup(persistingChannel, agentRunState, runId, run) {
203
203
  usage: persistingChannel.totalUsage,
204
204
  });
205
205
  }
206
+ /**
207
+ * Adapts `modifyOutputStream` hooks to channel middleware, one closure per
208
+ * hook so each keeps its own `state` and event log for the whole run.
209
+ */
210
+ const toChannelStreamMiddleware = (hooks, sharedNotes, signal) => hooks
211
+ .filter((mw) => mw.modifyOutputStream)
212
+ .map((mw) => {
213
+ const state = {};
214
+ const allEvents = [];
215
+ return async (services, event, next) => {
216
+ allEvents.push(event);
217
+ const result = await mw.modifyOutputStream(services, {
218
+ event,
219
+ allEvents,
220
+ state,
221
+ shared: sharedNotes,
222
+ // Sends downstream directly, so a hook can hand back the fast event
223
+ // now and push the slow one when it is ready.
224
+ emit: next,
225
+ signal,
226
+ });
227
+ if (result == null)
228
+ return;
229
+ if (Array.isArray(result)) {
230
+ for (const r of result)
231
+ await next(r);
232
+ }
233
+ else {
234
+ await next(result);
235
+ }
236
+ };
237
+ });
238
+ /**
239
+ * The branch a delegating parent's own spoken text is routed down once it has
240
+ * handed off: the same working-memory hook instances as the main chain, so the
241
+ * in-band `<working_memory>` blocks are still collected, ending in a sink so
242
+ * neither the client, the thread history, nor user channel middleware sees
243
+ * text the parent was supposed to keep to itself.
244
+ *
245
+ * Routing at ingress rather than dropping further down the chain keeps the
246
+ * decision synchronous with the send: the working-memory hook is async, so a
247
+ * hand-off landing mid-flight would otherwise retroactively suppress text the
248
+ * parent spoke before it.
249
+ */
250
+ const createWorkingMemoryOnlyChannel = (channel, services, workingMemoryMiddleware) => wrapChannelWithMiddleware({ channel: { ...channel, send: () => { } } }, services, workingMemoryMiddleware).channel;
206
251
  async function runStreamStepLoop(params) {
207
252
  const { agent, runnerParams, maxSteps, agentRunner, streamChannel, channel, agentMiddlewares, } = params;
208
253
  const singletonServices = getSingletonServices();
@@ -446,13 +491,14 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
446
491
  channel.send({ type: 'done' });
447
492
  return '';
448
493
  }
494
+ const workingMemoryMiddleware = getWorkingMemoryMiddleware(memoryConfig, storage, {
495
+ threadId,
496
+ workingMemorySchemaName,
497
+ logger: singletonServices.logger,
498
+ schemaService: singletonServices.schema,
499
+ });
449
500
  const agentMiddlewares = [
450
- ...getWorkingMemoryMiddleware(memoryConfig, storage, {
451
- threadId,
452
- workingMemorySchemaName,
453
- logger: singletonServices.logger,
454
- schemaService: singletonServices.schema,
455
- }),
501
+ ...workingMemoryMiddleware,
456
502
  ...(agent.agentMiddleware ?? []),
457
503
  ];
458
504
  // One bag per run, shared by every middleware — see PikkuAgentMiddlewareHooks.
@@ -491,43 +537,25 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
491
537
  await storage.saveMessages(threadId, [persistedUserMessage]);
492
538
  }
493
539
  warnUnstreamedOutputHooks(agentName, agentMiddlewares, singletonServices.logger);
494
- const streamMiddleware = agentMiddlewares
495
- .filter((mw) => mw.modifyOutputStream)
496
- .map((mw) => {
497
- const state = {};
498
- const allEvents = [];
499
- return async (services, event, next) => {
500
- allEvents.push(event);
501
- const result = await mw.modifyOutputStream(services, {
502
- event,
503
- allEvents,
504
- state,
505
- shared: sharedNotes,
506
- // Sends downstream directly, so a hook can hand back the fast event
507
- // now and push the slow one when it is ready.
508
- emit: next,
509
- signal: interruptHandle.signal,
510
- });
511
- if (result == null)
512
- return;
513
- if (Array.isArray(result)) {
514
- for (const r of result)
515
- await next(r);
516
- }
517
- else {
518
- await next(result);
519
- }
520
- };
521
- });
540
+ const workingMemoryStreamMiddleware = toChannelStreamMiddleware(workingMemoryMiddleware, sharedNotes, interruptHandle.signal);
541
+ const streamMiddleware = toChannelStreamMiddleware(agent.agentMiddleware ?? [], sharedNotes, interruptHandle.signal);
522
542
  const agentsMeta = pikkuState(packageName, 'agent', 'agentsMeta');
523
543
  const meta = agentsMeta[resolvedName];
524
- const allChannelMiddleware = combineChannelMiddleware('agent', `stream:${agentName}`, {
525
- wireInheritedChannelMiddleware: meta?.channelMiddleware,
526
- wireChannelMiddleware: [
527
- ...(agent.channelMiddleware ?? []),
528
- ...streamMiddleware,
529
- ],
530
- });
544
+ const isDelegateMode = agent.agentMode !== 'supervise' && meta?.agents?.length;
545
+ const delegateState = { delegated: false };
546
+ if (isDelegateMode) {
547
+ streamContext.delegateState = delegateState;
548
+ }
549
+ const allChannelMiddleware = [
550
+ ...workingMemoryStreamMiddleware,
551
+ ...combineChannelMiddleware('agent', `stream:${agentName}`, {
552
+ wireInheritedChannelMiddleware: meta?.channelMiddleware,
553
+ wireChannelMiddleware: [
554
+ ...(agent.channelMiddleware ?? []),
555
+ ...streamMiddleware,
556
+ ],
557
+ }),
558
+ ];
531
559
  const persistingChannel = createPersistingChannel(channel, storage, threadId, singletonServices.logger);
532
560
  const wrappedChannel = allChannelMiddleware.length > 0
533
561
  ? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
@@ -545,21 +573,19 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
545
573
  return wrappedChannel.send(event);
546
574
  },
547
575
  };
548
- const isDelegateMode = agent.agentMode !== 'supervise' && meta?.agents?.length;
549
- const delegateState = { delegated: false };
550
- if (isDelegateMode) {
551
- streamContext.delegateState = delegateState;
552
- }
576
+ const workingMemoryOnlyChannel = isDelegateMode && workingMemoryStreamMiddleware.length > 0
577
+ ? createWorkingMemoryOnlyChannel(channel, singletonServices, workingMemoryStreamMiddleware)
578
+ : undefined;
553
579
  const outputChannel = isDelegateMode
554
580
  ? {
555
581
  ...credentialFilteredChannel,
556
582
  send: (event) => {
557
583
  if (delegateState.delegated &&
558
- (event.type === 'text-delta' || event.type === 'reasoning-delta'))
559
- return;
584
+ (event.type === 'text-delta' || event.type === 'reasoning-delta')) {
585
+ return workingMemoryOnlyChannel?.send(event);
586
+ }
560
587
  return credentialFilteredChannel.send(event);
561
588
  },
562
- delegateState,
563
589
  }
564
590
  : credentialFilteredChannel;
565
591
  try {
@@ -880,13 +906,14 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
880
906
  const allMessages = [...contextMessages, ...messages];
881
907
  const trimmedMessages = trimMessages(allMessages);
882
908
  const instructions = await buildInstructions(resolvedName, packageName);
909
+ const workingMemoryMiddleware = getWorkingMemoryMiddleware(memoryConfig, storage, {
910
+ threadId: run.threadId,
911
+ workingMemorySchemaName,
912
+ logger: singletonServices.logger,
913
+ schemaService: singletonServices.schema,
914
+ });
883
915
  const agentMiddlewares = [
884
- ...getWorkingMemoryMiddleware(memoryConfig, storage, {
885
- threadId: run.threadId,
886
- workingMemorySchemaName,
887
- logger: singletonServices.logger,
888
- schemaService: singletonServices.schema,
889
- }),
916
+ ...workingMemoryMiddleware,
890
917
  ...(agent.agentMiddleware ?? []),
891
918
  ];
892
919
  // One bag per run, shared by every middleware — see PikkuAgentMiddlewareHooks.
@@ -895,46 +922,43 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
895
922
  // knowledge: decisions/internals/a-resumed-agent-turn-is-as-interruptible-as-the-first.md
896
923
  const interruptHandle = registerInterruptibleRun(run.runId);
897
924
  warnUnstreamedOutputHooks(run.agentName, agentMiddlewares, singletonServices.logger);
898
- const streamMiddleware = agentMiddlewares
899
- .filter((mw) => mw.modifyOutputStream)
900
- .map((mw) => {
901
- const state = {};
902
- const allEvents = [];
903
- return async (services, event, next) => {
904
- allEvents.push(event);
905
- const result = await mw.modifyOutputStream(services, {
906
- event,
907
- allEvents,
908
- state,
909
- shared: sharedNotes,
910
- // Sends downstream directly, so a hook can hand back the fast event
911
- // now and push the slow one when it is ready.
912
- emit: next,
913
- signal: interruptHandle.signal,
914
- });
915
- if (result == null)
916
- return;
917
- if (Array.isArray(result)) {
918
- for (const r of result)
919
- await next(r);
920
- }
921
- else {
922
- await next(result);
923
- }
924
- };
925
- });
926
- const allChannelMiddleware = combineChannelMiddleware('agent', `stream:${run.agentName}`, {
927
- wireInheritedChannelMiddleware: meta?.channelMiddleware,
928
- wireChannelMiddleware: [
929
- ...(agent.channelMiddleware ?? []),
930
- ...streamMiddleware,
931
- ],
932
- });
925
+ const workingMemoryStreamMiddleware = toChannelStreamMiddleware(workingMemoryMiddleware, sharedNotes, interruptHandle.signal);
926
+ const streamMiddleware = toChannelStreamMiddleware(agent.agentMiddleware ?? [], sharedNotes, interruptHandle.signal);
927
+ const allChannelMiddleware = [
928
+ ...workingMemoryStreamMiddleware,
929
+ ...combineChannelMiddleware('agent', `stream:${run.agentName}`, {
930
+ wireInheritedChannelMiddleware: meta?.channelMiddleware,
931
+ wireChannelMiddleware: [
932
+ ...(agent.channelMiddleware ?? []),
933
+ ...streamMiddleware,
934
+ ],
935
+ }),
936
+ ];
933
937
  const persistingChannel = createPersistingChannel(channel, storage, run.threadId, singletonServices.logger);
934
938
  const wrappedChannel = allChannelMiddleware.length > 0
935
939
  ? wrapChannelWithMiddleware({ channel: persistingChannel }, singletonServices, allChannelMiddleware).channel
936
940
  : persistingChannel;
941
+ const isDelegateMode = agent.agentMode !== 'supervise' && meta?.agents?.length;
942
+ const delegateState = { delegated: false };
937
943
  const streamContext = { channel, options };
944
+ if (isDelegateMode) {
945
+ streamContext.delegateState = delegateState;
946
+ }
947
+ const workingMemoryOnlyChannel = isDelegateMode && workingMemoryStreamMiddleware.length > 0
948
+ ? createWorkingMemoryOnlyChannel(channel, singletonServices, workingMemoryStreamMiddleware)
949
+ : undefined;
950
+ const outputChannel = isDelegateMode
951
+ ? {
952
+ ...wrappedChannel,
953
+ send: (event) => {
954
+ if (delegateState.delegated &&
955
+ (event.type === 'text-delta' || event.type === 'reasoning-delta')) {
956
+ return workingMemoryOnlyChannel?.send(event);
957
+ }
958
+ return wrappedChannel.send(event);
959
+ },
960
+ }
961
+ : wrappedChannel;
938
962
  const resumeTools = (await buildToolDefs(params, new Map(), run.resourceId, resolvedName, packageName, streamContext, agentMiddlewares)).tools;
939
963
  const resolved = resolveModelConfig(resolvedName, agent);
940
964
  const maxSteps = resolved.maxSteps ?? 10;
@@ -959,7 +983,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
959
983
  runnerParams,
960
984
  maxSteps,
961
985
  agentRunner,
962
- streamChannel: wrappedChannel,
986
+ streamChannel: outputChannel,
963
987
  persistingChannel,
964
988
  channel,
965
989
  agentMiddlewares,
@@ -1,5 +1,6 @@
1
1
  import type { CorePermissionGroup, CorePikkuPermission } from '../../function/functions.types.js';
2
2
  import type { CorePikkuMiddleware, MiddlewareMetadata } from '../../middleware/middleware.types.js';
3
+ import type { CoreSingletonServices } from '../../types/core.types.js';
3
4
  import type { PermissionMetadata } from '../../function/function-meta.types.js';
4
5
  import type { AIProviderOptions } from '../../services/agent-runner-service.js';
5
6
  import type { PikkuChannel } from '../channel/channel.types.js';
@@ -169,8 +170,8 @@ export interface AgentToolDef extends Partial<ApprovalPolicy> {
169
170
  */
170
171
  forwardsApproval?: boolean;
171
172
  }
172
- export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown> = Record<string, unknown>, Services = any> {
173
- modifyInput?: (services: Services, ctx: {
173
+ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown> = Record<string, unknown>, SingletonServices extends CoreSingletonServices = CoreSingletonServices> {
174
+ modifyInput?: (services: SingletonServices, ctx: {
174
175
  messages: AgentMessage[];
175
176
  instructions: string;
176
177
  /**
@@ -197,7 +198,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
197
198
  messages: AgentMessage[];
198
199
  instructions: string;
199
200
  };
200
- modifyOutputStream?: (services: Services, ctx: {
201
+ modifyOutputStream?: (services: SingletonServices, ctx: {
201
202
  event: AgentStreamEvent;
202
203
  allEvents: readonly AgentStreamEvent[];
203
204
  /** Private to this middleware, for this run. Cross-middleware facts go in `shared`. */
@@ -241,7 +242,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
241
242
  * and handed to anything that grades the run, and scrubbing the reply alone
242
243
  * leaves them untouched.
243
244
  */
244
- modifyOutput?: (services: Services, ctx: {
245
+ modifyOutput?: (services: SingletonServices, ctx: {
245
246
  text: string;
246
247
  messages: AgentMessage[];
247
248
  usage: {
@@ -258,7 +259,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
258
259
  messages: AgentMessage[];
259
260
  toolCalls?: NonNullable<AgentStep['toolCalls']>;
260
261
  };
261
- beforeToolCall?: (services: Services, ctx: {
262
+ beforeToolCall?: (services: SingletonServices, ctx: {
262
263
  toolName: string;
263
264
  toolCallId: string;
264
265
  args: Record<string, unknown>;
@@ -267,7 +268,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
267
268
  } | void> | {
268
269
  args: Record<string, unknown>;
269
270
  } | void;
270
- afterToolCall?: (services: Services, ctx: {
271
+ afterToolCall?: (services: SingletonServices, ctx: {
271
272
  toolName: string;
272
273
  toolCallId: string;
273
274
  args: Record<string, unknown>;
@@ -278,7 +279,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
278
279
  } | void> | {
279
280
  result: unknown;
280
281
  } | void;
281
- afterStep?: (services: Services, ctx: {
282
+ afterStep?: (services: SingletonServices, ctx: {
282
283
  stepNumber: number;
283
284
  text: string;
284
285
  toolCalls: {
@@ -299,7 +300,7 @@ export interface PikkuAgentMiddlewareHooks<State extends Record<string, unknown>
299
300
  };
300
301
  finishReason: string;
301
302
  }) => Promise<void> | void;
302
- onError?: (services: Services, ctx: {
303
+ onError?: (services: SingletonServices, ctx: {
303
304
  error: Error;
304
305
  stepNumber: number;
305
306
  messages: AgentMessage[];
@@ -24,7 +24,7 @@ export function validateAndBuildVariableDefinitionsMeta(definitions, schemaLooku
24
24
  description: def.description,
25
25
  variableId: def.variableId,
26
26
  schema: def.schema,
27
- required: def.required,
27
+ optional: def.optional,
28
28
  docsUrl: def.docsUrl,
29
29
  sourceFile: def.sourceFile,
30
30
  };
@@ -39,7 +39,7 @@ export function validateAndBuildVariableDefinitionsMeta(definitions, schemaLooku
39
39
  description: def.description,
40
40
  variableId: def.variableId,
41
41
  schema: def.schema,
42
- required: def.required,
42
+ optional: def.optional,
43
43
  docsUrl: def.docsUrl,
44
44
  sourceFile: def.sourceFile,
45
45
  };
@@ -5,13 +5,19 @@ export type CoreVariable<T = unknown> = {
5
5
  variableId: string;
6
6
  schema: T;
7
7
  /**
8
- * A variable is OPTIONAL by default because `variables.get` returns
9
- * `T | undefined` and never throws every caller already handles absence, so
10
- * a deploy gate that blocks on one contradicts the API. Mark a variable
11
- * `required` for the few whose absence genuinely breaks the app; only those
12
- * block a deploy.
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
- required?: boolean;
20
+ optional?: boolean;
15
21
  docsUrl?: string;
16
22
  };
17
23
  export type VariableDefinitionMeta = {
@@ -20,7 +26,7 @@ export type VariableDefinitionMeta = {
20
26
  description?: string;
21
27
  variableId: string;
22
28
  schema?: Record<string, unknown> | string;
23
- required?: boolean;
29
+ optional?: boolean;
24
30
  docsUrl?: string;
25
31
  sourceFile?: string;
26
32
  };
@@ -6,8 +6,8 @@ import type { ScenarioPersonas } from '../../services/personas-service.js';
6
6
  import type { ScenarioBrowserProvider, ScenarioEnvironment, ScenarioSurface } from './scenario-step.types.js';
7
7
  import type { PikkuWorkflowWire, WorkflowQueueOptions } from './workflow.types.js';
8
8
  export { addFeature, resolveFeatureScenarios } from './feature.js';
9
- export type * from './scenario.types.js';
10
- export type * from './scenario-run.types.js';
9
+ export type { CoreFeature, CoreFeatureScenario, FeatureMeta, FeaturesMeta, PikkuBrowserWire, PikkuScenarioWire, ScenarioBrowserFailure, ScenarioBrowserProvider, ScenarioEnvironment, ScenarioStepKind, ScenarioStepMeta, ScenarioStepOptions, ScenarioStepPhase, ScenarioSurface, TestIdSelector, } from './scenario.types.js';
10
+ export type { ScenarioArtifact, ScenarioFailureDetail, ScenarioResult, ScenarioRunRecord, ScenarioRunReport, ScenarioRunStatus, ScenarioRunStore, ScenarioRunSummary, ScenarioStepRow, } from './scenario-run.types.js';
11
11
  export { SCENARIO_SURFACES } from './scenario-step.types.js';
12
12
  export { resolveScenarioSurfaces } from './scenario-surface.js';
13
13
  export { pollUntil, type PollOptions } from './scenario-poll.js';
@@ -105,12 +105,12 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
105
105
  *
106
106
  * Returns nothing by default so a store that cannot express the query keeps
107
107
  * working unchanged — and, because it does not opt in, gains no re-dispatches
108
- * either. A store must have an atomic `withStepLock` before overriding this,
109
- * or no concurrency for one to exclude: the relay makes duplicate dispatch
110
- * routine, and the claim in `executeWorkflowStepInner` is what keeps a
111
- * duplicate from becoming a second execution. `kysely-postgres` and
112
- * `kysely-mysql` qualify on the lock, `in-memory` on being inline and
113
- * single-process; `mongodb` and `kysely-sqlite` qualify on neither.
108
+ * either. A store must have an atomic `claimStepForExecution` before
109
+ * overriding this, or no concurrency for one to exclude: the relay makes
110
+ * duplicate dispatch routine, and the claim is what keeps a duplicate from
111
+ * becoming a second execution. Every `@pikku/kysely` dialect qualifies on its
112
+ * status-guarded claim, `in-memory` on being inline and single-process;
113
+ * `mongodb` still qualifies on neither.
114
114
  */
115
115
  protected findUndispatchedSteps(_before: Date, _limit: number): Promise<Array<{
116
116
  runId: string;
@@ -176,6 +176,20 @@ export declare abstract class PikkuWorkflowService implements WorkflowService {
176
176
  protected onChildWorkflowFailed(childRun: WorkflowRun, error: Error): Promise<void>;
177
177
  private runVersionMismatchFallback;
178
178
  executeWorkflowStep(runId: string, stepName: string, rpcName: string, data: any, rpcService: PikkuRPC): Promise<void>;
179
+ /**
180
+ * Take sole ownership of a step before it runs, returning the state to run
181
+ * under — or `null` when another dispatch already owns it.
182
+ *
183
+ * Dispatch is at-least-once by design: the relay re-dispatches steps it
184
+ * believes were dropped, and a queue can redeliver a job it already handed
185
+ * out. This is the one place that keeps a duplicate dispatch from becoming a
186
+ * second execution of a side-effecting step, so it is only as strong as the
187
+ * exclusion it is built on — and `withStepLock` excludes nothing unless the
188
+ * store backs it with a real primitive. A store able to express the decision
189
+ * as one conditional write should override this rather than reach for a lock,
190
+ * which is what `@pikku/kysely` does with a status-guarded `UPDATE`.
191
+ */
192
+ protected claimStepForExecution(runId: string, stepName: string, rpcName: string): Promise<StepState | null>;
179
193
  private executeWorkflowStepInner;
180
194
  orchestrateWorkflow(runId: string, rpcService: PikkuRPC): Promise<void>;
181
195
  private verifyQueueService;