@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
@@ -9,13 +9,14 @@ import { deriveInvocationId } from './workflow-invocation-id.js';
9
9
  import { approvalDeciderFrom } from './workflow-approval-policy.js';
10
10
  import { buildRunTimeline, reconstructStateAt, } from './run-timeline.js';
11
11
  import { DEFAULT_STEP_RETRIES, WORKFLOW_CHILD_POLL_MAX_MS, WORKFLOW_END_STATES, WORKFLOW_POLL_FACTOR, WORKFLOW_POLL_MIN_MS, WORKFLOW_TERMINAL_STATES, } from './workflow-constants.js';
12
- import { WorkflowAsyncException, WorkflowCancelledException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotFoundError, WorkflowStepFunctionMismatchError, WorkflowStepNameNotString, WorkflowSuspendedException, } from './workflow-errors.js';
12
+ import { WorkflowAsyncException, WorkflowCancelledException, WorkflowDispatchException, WorkflowNotFoundError, WorkflowRunCancelledError, WorkflowRunFailedError, WorkflowRunNotFoundError, WorkflowStepNameNotString, WorkflowSuspendedException, } from './workflow-errors.js';
13
13
  import { resolveWorkflowMeta } from './workflow-meta-resolver.js';
14
14
  import { jobGroupFor, orchestratorQueueName, resolveWorkflowConfig, stepJobOptions, stepWorkerQueueName, } from './workflow-queue-routing.js';
15
15
  import { wireWorkflowQueueWorkers } from './workflow-queue-wiring.js';
16
16
  import { approvalStepNameFor, evaluateApprovalStep, recordApprovalDecision, } from './workflow-approval.js';
17
17
  import { auditApprovalDecision } from './workflow-approval-audit.js';
18
18
  import { recordSuspension, suspendStepNameFor } from './workflow-suspend.js';
19
+ import { claimStepByReadThenWrite } from './workflow-step-claim.js';
19
20
  import { RedispatchBackoff, sweepStalledRuns, sweepUndispatchedSteps, } from './workflow-recovery.js';
20
21
  export class PikkuWorkflowService {
21
22
  runExtension;
@@ -261,12 +262,12 @@ export class PikkuWorkflowService {
261
262
  *
262
263
  * Returns nothing by default so a store that cannot express the query keeps
263
264
  * working unchanged — and, because it does not opt in, gains no re-dispatches
264
- * either. A store must have an atomic `withStepLock` before overriding this,
265
- * or no concurrency for one to exclude: the relay makes duplicate dispatch
266
- * routine, and the claim in `executeWorkflowStepInner` is what keeps a
267
- * duplicate from becoming a second execution. `kysely-postgres` and
268
- * `kysely-mysql` qualify on the lock, `in-memory` on being inline and
269
- * single-process; `mongodb` and `kysely-sqlite` qualify on neither.
265
+ * either. A store must have an atomic `claimStepForExecution` before
266
+ * overriding this, or no concurrency for one to exclude: the relay makes
267
+ * duplicate dispatch routine, and the claim is what keeps a duplicate from
268
+ * becoming a second execution. Every `@pikku/kysely` dialect qualifies on its
269
+ * status-guarded claim, `in-memory` on being inline and single-process;
270
+ * `mongodb` still qualifies on neither.
270
271
  */
271
272
  async findUndispatchedSteps(_before, _limit) {
272
273
  return [];
@@ -697,25 +698,24 @@ export class PikkuWorkflowService {
697
698
  this.exitExecution(runId);
698
699
  }
699
700
  }
701
+ /**
702
+ * Take sole ownership of a step before it runs, returning the state to run
703
+ * under — or `null` when another dispatch already owns it.
704
+ *
705
+ * Dispatch is at-least-once by design: the relay re-dispatches steps it
706
+ * believes were dropped, and a queue can redeliver a job it already handed
707
+ * out. This is the one place that keeps a duplicate dispatch from becoming a
708
+ * second execution of a side-effecting step, so it is only as strong as the
709
+ * exclusion it is built on — and `withStepLock` excludes nothing unless the
710
+ * store backs it with a real primitive. A store able to express the decision
711
+ * as one conditional write should override this rather than reach for a lock,
712
+ * which is what `@pikku/kysely` does with a status-guarded `UPDATE`.
713
+ */
714
+ async claimStepForExecution(runId, stepName, rpcName) {
715
+ return this.withStepLock(runId, stepName, () => claimStepByReadThenWrite(this, runId, stepName, rpcName));
716
+ }
700
717
  async executeWorkflowStepInner(runId, stepName, rpcName, data, rpcService) {
701
- const claimed = await this.withStepLock(runId, stepName, async () => {
702
- const stepState = await this.getStepState(runId, stepName);
703
- // knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
704
- if (stepState.rpcName !== undefined &&
705
- stepState.rpcName !== (rpcName ?? null)) {
706
- throw new WorkflowStepFunctionMismatchError(runId, stepName);
707
- }
708
- if (stepState.status === 'succeeded' || stepState.status === 'running') {
709
- return null;
710
- }
711
- if (stepState.status === 'failed') {
712
- return this.createRetryAttempt(stepState.stepId, 'running');
713
- }
714
- if (stepState.status === 'pending' || stepState.status === 'scheduled') {
715
- await this.setStepRunning(stepState.stepId);
716
- }
717
- return stepState;
718
- });
718
+ const claimed = await this.claimStepForExecution(runId, stepName, rpcName);
719
719
  if (!claimed) {
720
720
  return;
721
721
  }
@@ -0,0 +1,16 @@
1
+ import type { StepState } from './workflow.types.js';
2
+ /** What claiming a step needs from the workflow service. */
3
+ export type StepClaimStore = {
4
+ getStepState(runId: string, stepName: string): Promise<StepState>;
5
+ setStepRunning(stepId: string): Promise<void>;
6
+ createRetryAttempt(failedStepId: string, status: 'pending' | 'running'): Promise<StepState>;
7
+ };
8
+ /**
9
+ * Decide whether this dispatch owns the step, by reading its state and then
10
+ * writing it — which only excludes a concurrent dispatch when the caller holds
11
+ * a lock that genuinely excludes one.
12
+ *
13
+ * A store that can express the whole decision as a single conditional write
14
+ * should do that instead of calling this.
15
+ */
16
+ export declare const claimStepByReadThenWrite: (store: StepClaimStore, runId: string, stepName: string, rpcName: string) => Promise<StepState | null>;
@@ -0,0 +1,27 @@
1
+ import { WorkflowStepFunctionMismatchError } from './workflow-errors.js';
2
+ /**
3
+ * Decide whether this dispatch owns the step, by reading its state and then
4
+ * writing it — which only excludes a concurrent dispatch when the caller holds
5
+ * a lock that genuinely excludes one.
6
+ *
7
+ * A store that can express the whole decision as a single conditional write
8
+ * should do that instead of calling this.
9
+ */
10
+ export const claimStepByReadThenWrite = async (store, runId, stepName, rpcName) => {
11
+ const stepState = await store.getStepState(runId, stepName);
12
+ // knowledge: decisions/security/a-step-runs-the-function-the-workflow-dispatched-it-with.md
13
+ if (stepState.rpcName !== undefined &&
14
+ stepState.rpcName !== (rpcName ?? null)) {
15
+ throw new WorkflowStepFunctionMismatchError(runId, stepName);
16
+ }
17
+ if (stepState.status === 'succeeded' || stepState.status === 'running') {
18
+ return null;
19
+ }
20
+ if (stepState.status === 'failed') {
21
+ return store.createRetryAttempt(stepState.stepId, 'running');
22
+ }
23
+ if (stepState.status === 'pending' || stepState.status === 'scheduled') {
24
+ await store.setStepRunning(stepState.stepId);
25
+ }
26
+ return stepState;
27
+ };
@@ -0,0 +1,47 @@
1
+ ---
2
+ type: decision
3
+ title: A scaffold flag says a surface exists, not who may call it
4
+ description: scaffold.<feature> is boolean | { path } — the auth field is gone, because the function runner already enforces each generated function's own auth, wiring, scopes and addon
5
+ tags: config, authorization, codegen, scaffold
6
+ ---
7
+
8
+ # A scaffold flag says a surface exists, not who may call it
9
+
10
+ `scaffold.<feature>` carried an `auth` field, defaulting closed — see
11
+ [[scaffold-features-are-authenticated-unless-opted-out]], which this supersedes.
12
+ The field is gone. `PikkuScaffoldFeature` is now `boolean | { path?: string }`:
13
+ whether the surface is generated, and where the file goes.
14
+
15
+ The field was a second gate in front of one that already runs. `resolveSession`
16
+ in `packages/core/src/function/function-runner.ts` throws `ForbiddenError` for a
17
+ `pikkuFunc` with no session, unconditionally; a `pikkuSessionlessFunc` requires
18
+ one when its own `auth: true` says so, when its wiring says so, or when
19
+ `resolveAddonAuth` does. That is where authentication is decided, per function,
20
+ by whoever wrote it. A config flag three directories away could only be coarser
21
+ than that, and being coarser was the whole problem the earlier decision was
22
+ trying to work around by defaulting it closed.
23
+
24
+ So the generated dispatchers — public RPC, public agent, workflow routes and the
25
+ events channel — emit a fixed `auth: false` on every function and wiring they
26
+ write. That is not the scaffold declaring the surface public. It is the
27
+ dispatcher declining to gate, because it does not know what it is forwarding to:
28
+ `rpcCaller` hands whatever name it was given to `rpc.exposed`, and the function
29
+ that resolves there is the one that answers for itself. Omitting the field would
30
+ not have been neutral — a wiring with no `auth` requires a session — so the
31
+ wrapper would reject the call before the gate that decides ever ran, which is a
32
+ harder gate than the config field it replaced, applied by the layer least
33
+ qualified to apply it.
34
+
35
+ The scoped admin surfaces do not emit it. `userAdmin` and `virtualUser` generate
36
+ `pikkuFunc` with `scopes: ['admin:users:...']` — session-required by
37
+ construction, and the function that decides rather than a wrapper in front of
38
+ one. `auth: false` there is refused by `runPikkuFunc` with a warning telling you
39
+ to use `pikkuSessionlessFunc` instead, and `auth: true` would only restate the
40
+ type.
41
+
42
+ `pikku enable <feature>` lost its `--noAuth` flag with the field it wrote.
43
+
44
+ **What this rules out:** reading a scaffold flag as an authorization decision;
45
+ gating a surface in the config instead of on the function that answers it; and
46
+ letting a generated dispatcher inherit the default `auth: true`, which reads as
47
+ "undecided" and behaves as "denied".
@@ -50,7 +50,8 @@ A rule about who may do what, and which way it fails when it is unsure.
50
50
  - [Queue job identities are signed at enqueue](queue-job-identities-are-signed-at-enqueue.md) — A job's pikkuUserId is an HMAC claim bound to the queue and payload; an unverifiable claim is dropped, never trusted
51
51
  - [Queue jobs carry the producer's pikku user id](queue-jobs-carry-the-producers-pikku-user-id.md) — A job's pikkuUserId is trusted as identity by the worker, so enqueue rights are effectively act-as-user rights
52
52
  - [Remote addon tokens are client credentials, not mesh trust](remote-addon-tokens-are-client-credentials-not-mesh-trust.md) — wireRemoteAddon authenticates as a client to a hosted library and fails closed on an empty token; it never uses PIKKU_REMOTE_SECRET
53
- - [A scaffolded surface is authenticated unless the config opts out in writing](scaffold-features-are-authenticated-unless-opted-out.md) — scaffold.<feature> became boolean | { auth, path }, where true means authenticated going public requires typing { auth: false }, and the legacy 'auth' | 'no-auth' strings are refused rather than coerced
53
+ - [A scaffold flag says a surface exists, not who may call it](a-scaffold-flag-says-a-surface-exists-not-who-may-call-it.md) — scaffold.<feature> is boolean | { path }; the auth field is gone because the function runner already enforces each generated function's own auth, wiring, scopes and addon
54
+ - [A scaffolded surface is authenticated unless the config opts out in writing](scaffold-features-are-authenticated-unless-opted-out.md) — SUPERSEDED; retains why the legacy 'auth' | 'no-auth' strings are refused rather than coerced
54
55
  - [Scenario-step functions are never externally invocable over RPC](scenario-step-functions-are-never-externally-invocable.md) — rpcExposed requires expose and rejects scenarioStep, so test steps stay reachable only from inside a scenario run
55
56
  - [Scope resolution happens at the session boundary and scope sync never deletes](scope-resolution-happens-at-the-session-boundary-and-sync-never-deletes.md) — ScopeService is called when a session is built, never by the function runner, and syncScopes only ever adds — revoking is an explicit operation
56
57
  - [Self-authentication is declared, not detected](self-authentication-is-declared-not-detected.md) — A function that authorizes callers in its own body says so with selfAuthenticated; codegen never tries to infer it
@@ -7,6 +7,12 @@ tags: config, authorization, codegen, scaffold
7
7
 
8
8
  # A scaffolded surface is authenticated unless the config opts out in writing
9
9
 
10
+ **Superseded by
11
+ [[a-scaffold-flag-says-a-surface-exists-not-who-may-call-it]]**: the `auth`
12
+ field is gone entirely, because the function runner already enforces each
13
+ generated function's own auth. The reasoning below still explains why the
14
+ legacy `'auth' | 'no-auth'` strings are refused rather than coerced.
15
+
10
16
  `scaffold.<feature>` was `'auth' | 'no-auth' | false`. Two things were wrong
11
17
  with it, and they compounded.
12
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/core",
3
- "version": "0.12.86",
3
+ "version": "0.12.88",
4
4
  "description": "The Pikku runtime — functions, wirings, services, middleware and types",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -11,7 +11,7 @@ import type { Logger } from '../services/logger.js'
11
11
  import type { CorePikkuFunctionConfig } from '../function/functions.types.js'
12
12
  import { createModuleRunner } from './module-runner.js'
13
13
 
14
- export * from './reload-meta.js'
14
+ export { reloadGeneratedMeta, reconcileAddonRegistry } from './reload-meta.js'
15
15
 
16
16
  interface PikkuDevReloaderOptions {
17
17
  srcDirectories: string[]
@@ -1,2 +1,25 @@
1
- export * from './core.types.js'
1
+ export type {
2
+ AuthInstance,
3
+ CommonWireMeta,
4
+ CoreConfig,
5
+ CoreServices,
6
+ CoreSingletonServices,
7
+ CoreUserSession,
8
+ CreateConfig,
9
+ CreateSingletonServices,
10
+ CreateWireServices,
11
+ GetCredential,
12
+ PikkuWire,
13
+ PikkuWiringTypes,
14
+ PostgresConfig,
15
+ SecretlessServices,
16
+ SecurityAuditIssue,
17
+ SecurityAuditReport,
18
+ SecurityAuditSummary,
19
+ SecurityAuditUpdate,
20
+ SecuritySeverity,
21
+ SecurityUpdateLevel,
22
+ ServerLifecycle,
23
+ WireServices,
24
+ } from './core.types.js'
2
25
  export type * from './state.types.js'
@@ -74,6 +74,72 @@ describe('agent-memory', () => {
74
74
  })
75
75
  })
76
76
 
77
+ test('deepMergeWorkingMemory replaces an array re-emitted in full', () => {
78
+ const result = deepMergeWorkingMemory(
79
+ { steps: [{ id: 1 }, { id: 2 }], other: 'kept' },
80
+ { steps: [{ id: 1 }, { id: 2 }, { id: 3 }] }
81
+ )
82
+
83
+ assert.deepEqual(result, {
84
+ steps: [{ id: 1 }, { id: 2 }, { id: 3 }],
85
+ other: 'kept',
86
+ })
87
+ })
88
+
89
+ test('deepMergeWorkingMemory replaces rather than appends a partial array', () => {
90
+ const result = deepMergeWorkingMemory(
91
+ { steps: [{ id: 1 }, { id: 2 }] },
92
+ { steps: [{ id: 3 }] }
93
+ )
94
+
95
+ assert.deepEqual(result, { steps: [{ id: 3 }] })
96
+ })
97
+
98
+ test('deepMergeWorkingMemory clears an array field set to null', () => {
99
+ const result = deepMergeWorkingMemory(
100
+ { steps: [1, 2, 3], city: 'Berlin' },
101
+ {
102
+ steps: null,
103
+ }
104
+ )
105
+
106
+ assert.deepEqual(result, { city: 'Berlin' })
107
+ })
108
+
109
+ test('deepMergeWorkingMemory replaces arrays nested inside merged objects', () => {
110
+ const result = deepMergeWorkingMemory(
111
+ {
112
+ profile: {
113
+ name: 'Yasser',
114
+ tags: ['a', 'b'],
115
+ },
116
+ },
117
+ {
118
+ profile: {
119
+ tags: ['c'],
120
+ },
121
+ }
122
+ )
123
+
124
+ assert.deepEqual(result, {
125
+ profile: {
126
+ name: 'Yasser',
127
+ tags: ['c'],
128
+ },
129
+ })
130
+ })
131
+
132
+ test('deepMergeWorkingMemory replaces across array and object shape changes', () => {
133
+ assert.deepEqual(
134
+ deepMergeWorkingMemory({ value: { a: 1 } }, { value: [1, 2] }),
135
+ { value: [1, 2] }
136
+ )
137
+ assert.deepEqual(
138
+ deepMergeWorkingMemory({ value: [1, 2] }, { value: { a: 1 } }),
139
+ { value: { a: 1 } }
140
+ )
141
+ })
142
+
77
143
  test('deepMergeWorkingMemory ignores prototype-polluting keys', () => {
78
144
  const before = ({} as any).isAdmin
79
145
  deepMergeWorkingMemory(
@@ -107,6 +173,13 @@ describe('agent-memory', () => {
107
173
  assert.match(prompt, /<working_memory>/)
108
174
  })
109
175
 
176
+ test('buildWorkingMemoryPrompt tells the model arrays are replaced whole', () => {
177
+ const prompt = buildWorkingMemoryPrompt({ steps: [1] })
178
+
179
+ assert.match(prompt, /Arrays are replaced, not merged/)
180
+ assert.match(prompt, /repeat every item you want to keep/)
181
+ })
182
+
110
183
  test('buildWorkingMemoryPrompt marks empty memory explicitly', () => {
111
184
  const prompt = buildWorkingMemoryPrompt(null)
112
185
  assert.match(prompt, /Current working memory: \(empty\)/)
@@ -39,6 +39,17 @@ export function isWorkingMemoryEnabled(
39
39
  return !!memoryConfig?.workingMemory && !!storage
40
40
  }
41
41
 
42
+ /**
43
+ * Merges a model-emitted working memory update into the stored state.
44
+ *
45
+ * Objects merge key by key, and `null` deletes a key. Arrays are replaced
46
+ * wholesale — deliberately, not as a side effect of the object guard below.
47
+ * The full state is echoed back to the model every turn by
48
+ * {@link buildWorkingMemoryPrompt}, so it can always re-emit an array in full,
49
+ * and appending instead would duplicate every item each time it did.
50
+ * `buildWorkingMemoryPrompt` states this contract to the model; the two must
51
+ * stay in agreement.
52
+ */
42
53
  export function deepMergeWorkingMemory(
43
54
  existing: Record<string, unknown>,
44
55
  updates: Record<string, unknown>
@@ -51,9 +62,10 @@ export function deepMergeWorkingMemory(
51
62
  const value = updates[key]
52
63
  if (value === null) {
53
64
  delete result[key]
65
+ } else if (Array.isArray(value)) {
66
+ result[key] = value
54
67
  } else if (
55
68
  typeof value === 'object' &&
56
- !Array.isArray(value) &&
57
69
  typeof result[key] === 'object' &&
58
70
  result[key] !== null &&
59
71
  !Array.isArray(result[key])
@@ -100,7 +112,9 @@ export function buildWorkingMemoryPrompt(
100
112
  'When you learn new information, output a partial JSON update in <working_memory> tags. ' +
101
113
  'Only include durable facts you have actually derived or the user has confirmed. ' +
102
114
  'Do not output templates, placeholders, or narration. ' +
103
- 'Only include changed fields. Leave unknown fields untouched. Set a field to null to delete it.'
115
+ 'Only include changed fields. Leave unknown fields untouched. Set a field to null to delete it. ' +
116
+ 'Arrays are replaced, not merged: to change one, repeat every item you want to keep alongside the new ones, ' +
117
+ 'because any item you leave out is deleted.'
104
118
  )
105
119
 
106
120
  return parts.join('\n\n')
@@ -0,0 +1,41 @@
1
+ // Type-checked via tsconfig.type-tests.json; no runtime assertions.
2
+
3
+ import { pikkuAgentMiddleware } from '../../middleware/middleware-factories.js'
4
+ import type { PikkuAgentMiddlewareHooks } from './agent.types.js'
5
+
6
+ const _singletonServicesCompile = pikkuAgentMiddleware<{ count: number }>({
7
+ modifyInput: async ({ logger }, { messages, instructions }) => {
8
+ logger.info(`agent input: ${messages.length} messages`)
9
+ return { messages, instructions }
10
+ },
11
+ modifyOutputStream: async ({ logger }, { event, state }) => {
12
+ state.count = (state.count ?? 0) + 1
13
+ logger.info(`event: ${event.type}`)
14
+ return event
15
+ },
16
+ afterStep: async ({ variables }, { stepNumber }) => {
17
+ variables.get(`STEP_${stepNumber}`)
18
+ },
19
+ onError: async ({ logger }, { error }) => {
20
+ logger.error(error.message)
21
+ },
22
+ })
23
+ void _singletonServicesCompile
24
+
25
+ const _wireServiceIsNotAvailable: PikkuAgentMiddlewareHooks = {
26
+ // @ts-expect-error — `http` is a wire service, and an agent run has no wire
27
+ modifyInput: async ({ http }, { messages, instructions }) => {
28
+ void http
29
+ return { messages, instructions }
30
+ },
31
+ }
32
+ void _wireServiceIsNotAvailable
33
+
34
+ const _wireServiceIsNotAvailableToTheFactory = pikkuAgentMiddleware({
35
+ // @ts-expect-error — `channel` is a wire service, and an agent run has no wire
36
+ afterToolCall: async ({ channel }, { result }) => {
37
+ void channel
38
+ return { result }
39
+ },
40
+ })
41
+ void _wireServiceIsNotAvailableToTheFactory