@pikku/core 0.12.86 → 0.12.89

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 (49) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/dev/hot-reload.d.ts +1 -1
  3. package/dist/dev/hot-reload.js +1 -1
  4. package/dist/types/core.types.d.ts +1 -1
  5. package/dist/types/index.d.ts +1 -1
  6. package/dist/types/index.js +1 -1
  7. package/dist/wirings/agent/agent-memory.d.ts +11 -0
  8. package/dist/wirings/agent/agent-memory.js +17 -2
  9. package/dist/wirings/agent/agent-stream.js +115 -91
  10. package/dist/wirings/agent/agent.types.d.ts +9 -8
  11. package/dist/wirings/rpc/rpc-runner.js +1 -1
  12. package/dist/wirings/variable/validate-variable-definitions.js +2 -2
  13. package/dist/wirings/variable/variable.types.d.ts +13 -7
  14. package/dist/wirings/workflow/pikku-scenario-service.d.ts +2 -2
  15. package/dist/wirings/workflow/pikku-workflow-service.d.ts +20 -6
  16. package/dist/wirings/workflow/pikku-workflow-service.js +29 -27
  17. package/dist/wirings/workflow/workflow-constants.d.ts +17 -0
  18. package/dist/wirings/workflow/workflow-constants.js +17 -0
  19. package/dist/wirings/workflow/workflow-recovery.d.ts +18 -1
  20. package/dist/wirings/workflow/workflow-recovery.js +30 -2
  21. package/dist/wirings/workflow/workflow-step-claim.d.ts +16 -0
  22. package/dist/wirings/workflow/workflow-step-claim.js +27 -0
  23. package/knowledge/decisions/security/a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md +47 -0
  24. package/knowledge/decisions/security/index.md +2 -1
  25. package/knowledge/decisions/security/scaffold-features-are-authenticated-unless-opted-out.md +6 -0
  26. package/package.json +1 -1
  27. package/src/dev/hot-reload.ts +1 -1
  28. package/src/types/core.types.ts +1 -1
  29. package/src/types/index.ts +24 -1
  30. package/src/wirings/agent/agent-memory.test.ts +73 -0
  31. package/src/wirings/agent/agent-memory.ts +16 -2
  32. package/src/wirings/agent/agent-middleware.types.test.ts +41 -0
  33. package/src/wirings/agent/agent-stream-delegate.test.ts +381 -0
  34. package/src/wirings/agent/agent-stream.ts +158 -78
  35. package/src/wirings/agent/agent.types.ts +9 -8
  36. package/src/wirings/rpc/rpc-runner.test.ts +6 -1
  37. package/src/wirings/rpc/rpc-runner.ts +1 -1
  38. package/src/wirings/variable/validate-variable-definitions.test.ts +11 -10
  39. package/src/wirings/variable/validate-variable-definitions.ts +2 -2
  40. package/src/wirings/variable/variable.types.ts +13 -7
  41. package/src/wirings/workflow/pikku-scenario-service.ts +29 -6
  42. package/src/wirings/workflow/pikku-workflow-service.ts +34 -27
  43. package/src/wirings/workflow/workflow-constants.ts +19 -0
  44. package/src/wirings/workflow/workflow-recovery.ts +31 -1
  45. package/src/wirings/workflow/workflow-stalled-recovery.test.ts +46 -0
  46. package/src/wirings/workflow/workflow-step-claim.ts +46 -0
  47. package/src/wirings/workflow/workflow-terminal-run-guard.test.ts +105 -0
  48. package/tsconfig.tsbuildinfo +1 -1
  49. package/tsconfig.type-tests.json +2 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,118 @@
1
+ ## 0.12.89
2
+
3
+ ### Patch Changes
4
+
5
+ - 32616af: Carry the trace id across a remote RPC hop
6
+
7
+ `ContextAwareRPCService` sent the wire's trace id as `x-trace-id`, but the HTTP
8
+ runner on the receiving end reads `x-request-id` — the header every other sender
9
+ uses, including `buildRemoteHeaders`, which every deployment service goes
10
+ through. The receiving side therefore ignored the incoming id and generated a
11
+ fresh one, so a trace broke at each remote RPC boundary instead of spanning it.
12
+ Remote RPC now sends `x-request-id` too.
13
+
14
+ - 6848cd9: fix(workflow): back off the stalled-run sweep, and skip runs that cannot move
15
+
16
+ `sweepUndispatchedSteps` has always consulted a per-run backoff so a genuine
17
+ queue backlog is not amplified by a tick that keeps firing at the steps the
18
+ backlog is already delaying. `sweepStalledRuns` — its sibling, doing the same
19
+ re-drive through the same orchestrator queue — had none, and re-resumed every
20
+ stalled run on every tick. A resume does not clear whatever wedged a run, so
21
+ the same runs came back on the next tick and the next: in production seven
22
+ permanently stuck runs refilled a purged orchestrator queue at seven messages a
23
+ minute, and a backlog of six thousand could never drain because each pass added
24
+ work the previous pass had not finished. It now takes the same backoff, and
25
+ both sweeps share one instance — the record belongs to the re-drive, not to the
26
+ signal that asked for it, so a run the relay nudged a moment ago is not nudged
27
+ again by the sweep.
28
+
29
+ `runWorkflowJob` also now returns immediately for a run in a terminal state
30
+ instead of taking the run lock and replaying the workflow body. The orchestrator
31
+ queue is at-least-once and the relay re-dispatches on purpose, so a message for
32
+ a run that already settled is routine — and replaying one could park the body on
33
+ a wait that nothing would ever satisfy, holding the run lock, and the pooled
34
+ connection under it, until something external gave up. `suspended` is
35
+ deliberately not included: it ends a pass, not the run.
36
+
37
+ ## 0.12.88
38
+
39
+ ### Patch Changes
40
+
41
+ - 4712e73: fix: collect working memory from a delegate-mode parent agent
42
+
43
+ A delegating parent's `text-delta` events were dropped at the outermost output
44
+ channel, above the working-memory hook, so every `<working_memory>` block it
45
+ wrote from its first hand-off onward was discarded before anything could read
46
+ it. The parent's text is now routed through the working-memory hook and into a
47
+ sink instead of being dropped outright, so the blocks are collected while the
48
+ client, the thread history and user channel middleware still see nothing.
49
+
50
+ The resume path built no delegate filter at all and streamed a delegating
51
+ parent's text to the client after an approval; it now suppresses text the same
52
+ way the initial path does.
53
+
54
+ The AI SDK rejects a system message inside `messages` outright, so the working
55
+ memory prompt the framework injects as one failed every run that enabled
56
+ working memory at all. The runner now lifts system messages onto the `system`
57
+ option, after the agent's own instructions.
58
+
59
+ - 082403f: fix(agent): make working-memory array semantics explicit
60
+
61
+ `deepMergeWorkingMemory` replaced arrays wholesale as a side effect of its
62
+ object-recursion guard, so nothing in the code said whether that was the
63
+ contract or an accident. The merge now handles arrays in an explicit,
64
+ documented branch. Replace is kept over append: the full state is echoed back
65
+ every turn, so appending would duplicate every item whenever the model re-emitted
66
+ the array.
67
+
68
+ `buildWorkingMemoryPrompt` now states that contract to the model rather than
69
+ leaving "only include changed fields" to be read as permission to send a partial
70
+ array. This is defensive: measured against `gpt-4.1-mini` the wording changes
71
+ nothing, because that model already re-emits the whole list. It is there for
72
+ models that do not.
73
+
74
+ ## 0.12.87
75
+
76
+ ### Patch Changes
77
+
78
+ - 9687ad1: fix: hand agent middleware the singleton services its type promises
79
+
80
+ `PikkuAgentMiddlewareHooks` typed its `services` parameter as the project's full
81
+ wire `Services`, while every runtime call site has only ever passed the singleton
82
+ services. A middleware that destructured a wire service typechecked and silently
83
+ received `undefined`.
84
+
85
+ The hooks are now bounded by `CoreSingletonServices` in core, and the generated
86
+ `pikkuAgentMiddleware` defaults to `WiredSingletonServices` like the other
87
+ middleware definers. Nothing changes at runtime: agent middleware hooks a _run_,
88
+ and a run is not a request — it can start from a scheduler or a workflow with no
89
+ wire behind it. A tool the run calls is an ordinary function call and still gets
90
+ its own wire services through `runPikkuFunc`.
91
+
92
+ - 2d21628: fix(kysely): claim a workflow step atomically in every SQL dialect
93
+
94
+ The workflow engine's "atomic claim" was a read-then-write guarded by
95
+ `withStepLock`, and `@pikku/kysely` inherited a silent pass-through for that
96
+ lock — so on every dialect but Postgres and MySQL a redelivered queue job could
97
+ claim a step another dispatch was already running, executing a side-effecting
98
+ step twice.
99
+
100
+ `@pikku/kysely` now claims the step with a status-guarded `UPDATE` and reads the
101
+ affected-row count, which is atomic in every SQL dialect without an
102
+ advisory-lock primitive. Relay redispatch is enabled for all Kysely dialects as
103
+ a result, not just Postgres and MySQL.
104
+
105
+ - 985b87b: Follow through on the variable `required` → `optional` rename: regenerate the
106
+ core API report and update the inspector's `defineVariable` gating-flag test,
107
+ both of which #1369 left describing the deleted `required` flag.
108
+ - 3a83f85: Stop re-exporting package internals through entry points
109
+
110
+ 66 names reached consumers only because an `export *` in an entry point swept
111
+ them up. Each one is referenced solely inside its own package, so the star is
112
+ now an explicit named re-export listing what is genuinely public. The
113
+ declarations themselves are untouched — this narrows the entry point, not the
114
+ module.
115
+
1
116
  ## 0.12.86
2
117
 
3
118
  ### 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 &&
@@ -182,7 +182,7 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
182
182
  * sets it; services that log fall back to the singleton logger.
183
183
  */
184
184
  logger: Logger;
185
- /** Trace ID for distributed tracing — propagated across remote RPC calls via x-trace-id header */
185
+ /** Trace ID for distributed tracing — propagated across remote RPC calls via the x-request-id header */
186
186
  traceId: string;
187
187
  functionId: string;
188
188
  addonNamespace: string;
@@ -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[];
@@ -245,7 +245,7 @@ export class ContextAwareRPCService {
245
245
  headers.authorization = `Bearer ${token}`;
246
246
  }
247
247
  if (this.wire.traceId) {
248
- headers['x-trace-id'] = this.wire.traceId;
248
+ headers['x-request-id'] = this.wire.traceId;
249
249
  }
250
250
  const base = serverUrl.replace(/\/+$/, '');
251
251
  const res = await fetch(`${base}/remote/rpc/${encodeURIComponent(remoteFn)}`, {
@@ -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';