@pikku/core 0.12.148 → 0.12.151

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 (139) hide show
  1. package/dist/function/function-runner.js +23 -0
  2. package/dist/function/trace-queue-service.d.ts +26 -0
  3. package/dist/function/trace-queue-service.js +48 -0
  4. package/dist/mesh.d.ts +2 -2
  5. package/dist/mesh.js +3 -3
  6. package/dist/middleware/index.d.ts +1 -1
  7. package/dist/middleware/index.js +1 -1
  8. package/dist/middleware/mesh-auth.d.ts +3 -2
  9. package/dist/middleware/mesh-auth.js +7 -2
  10. package/dist/middleware/remote-jobs-secret.d.ts +1 -1
  11. package/dist/middleware/require-origin.d.ts +19 -0
  12. package/dist/middleware/require-origin.js +36 -2
  13. package/dist/services/deployment-service.d.ts +1 -1
  14. package/dist/services/incoming-webhook-service.d.ts +4 -2
  15. package/dist/services/incoming-webhook-service.js +4 -3
  16. package/dist/services/queue-webhook-service.js +7 -1
  17. package/dist/services/remote-jobs.d.ts +6 -1
  18. package/dist/services/scheduler-service.d.ts +3 -1
  19. package/dist/services/webhook-service.d.ts +2 -0
  20. package/dist/services/workflow-service.d.ts +4 -1
  21. package/dist/testing/index.d.ts +4 -0
  22. package/dist/testing/index.js +2 -0
  23. package/dist/testing/queue-identity-transport-tests.d.ts +18 -0
  24. package/dist/testing/queue-identity-transport-tests.js +162 -0
  25. package/dist/testing/queue-trace-conformance.d.ts +26 -0
  26. package/dist/testing/queue-trace-conformance.js +259 -0
  27. package/dist/testing/service-tests/channel-store-tests.js +11 -0
  28. package/dist/testing/service-tests/queued-workflow-harness.d.ts +18 -1
  29. package/dist/testing/service-tests/queued-workflow-harness.js +19 -3
  30. package/dist/testing/service-tests/workflow-service-tests.js +12 -0
  31. package/dist/testing/trace-matrix/assertions.d.ts +3 -0
  32. package/dist/testing/trace-matrix/assertions.js +190 -0
  33. package/dist/testing/trace-matrix/chain-harness.d.ts +119 -0
  34. package/dist/testing/trace-matrix/chain-harness.js +777 -0
  35. package/dist/testing/trace-matrix/dimensions.d.ts +55 -0
  36. package/dist/testing/trace-matrix/dimensions.js +305 -0
  37. package/dist/testing/trace-matrix/pairwise.d.ts +20 -0
  38. package/dist/testing/trace-matrix/pairwise.js +75 -0
  39. package/dist/testing/trace-matrix/report.d.ts +1 -0
  40. package/dist/testing/trace-matrix/report.js +16 -0
  41. package/dist/testing/trace-matrix/transports.d.ts +19 -0
  42. package/dist/testing/trace-matrix/transports.js +27 -0
  43. package/dist/testing/trace-matrix/workflow-cases.d.ts +24 -0
  44. package/dist/testing/trace-matrix/workflow-cases.js +147 -0
  45. package/dist/trace.d.ts +66 -0
  46. package/dist/trace.js +97 -0
  47. package/dist/types/core.types.d.ts +6 -2
  48. package/dist/wirings/addon/addon-reach.d.ts +3 -0
  49. package/dist/wirings/addon/addon-reach.js +9 -0
  50. package/dist/wirings/addon/define-plugin.d.ts +16 -0
  51. package/dist/wirings/addon/wire-addon.js +5 -0
  52. package/dist/wirings/addon/wire-remote-addon.d.ts +11 -0
  53. package/dist/wirings/agent/agent-finalize.d.ts +2 -0
  54. package/dist/wirings/agent/agent-finalize.js +1 -1
  55. package/dist/wirings/agent/agent-prepare.d.ts +12 -2
  56. package/dist/wirings/agent/agent-prepare.js +38 -14
  57. package/dist/wirings/agent/agent-rpc.js +2 -1
  58. package/dist/wirings/agent/agent-runner.js +5 -6
  59. package/dist/wirings/agent/agent-stream.js +6 -6
  60. package/dist/wirings/agent/voice-input.d.ts +1 -1
  61. package/dist/wirings/agent/voice-output.d.ts +1 -1
  62. package/dist/wirings/agent-scorer/agent-scorer-live.d.ts +4 -2
  63. package/dist/wirings/agent-scorer/agent-scorer-live.js +2 -2
  64. package/dist/wirings/channel/channel-common.d.ts +4 -1
  65. package/dist/wirings/channel/channel-common.js +3 -1
  66. package/dist/wirings/channel/channel-handler.js +2 -0
  67. package/dist/wirings/channel/channel-host-rpc.d.ts +1 -1
  68. package/dist/wirings/channel/channel-host-rpc.js +2 -2
  69. package/dist/wirings/channel/channel-rpc-responder.d.ts +7 -1
  70. package/dist/wirings/channel/channel-rpc-responder.js +2 -2
  71. package/dist/wirings/channel/channel-rpc-service.d.ts +1 -1
  72. package/dist/wirings/channel/channel-rpc-service.js +2 -2
  73. package/dist/wirings/channel/channel-rpc.types.d.ts +5 -2
  74. package/dist/wirings/channel/channel-rpc.types.js +1 -1
  75. package/dist/wirings/channel/channel-store.d.ts +4 -2
  76. package/dist/wirings/channel/channel-store.js +2 -2
  77. package/dist/wirings/channel/channel.types.d.ts +3 -0
  78. package/dist/wirings/channel/local/local-channel-runner.js +16 -5
  79. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +3 -1
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +27 -4
  82. package/dist/wirings/gateway/gateway-runner.js +3 -1
  83. package/dist/wirings/http/http-runner.d.ts +4 -4
  84. package/dist/wirings/http/http-runner.js +14 -18
  85. package/dist/wirings/http/http.types.d.ts +3 -3
  86. package/dist/wirings/mcp/mcp-runner.js +15 -5
  87. package/dist/wirings/queue/in-memory-queue-service.d.ts +1 -1
  88. package/dist/wirings/queue/in-memory-queue-service.js +8 -2
  89. package/dist/wirings/queue/index.d.ts +1 -0
  90. package/dist/wirings/queue/index.js +1 -0
  91. package/dist/wirings/queue/queue-runner.d.ts +3 -4
  92. package/dist/wirings/queue/queue-runner.js +8 -7
  93. package/dist/wirings/queue/queue-trace.d.ts +18 -0
  94. package/dist/wirings/queue/queue-trace.js +42 -0
  95. package/dist/wirings/queue/queue.types.d.ts +7 -0
  96. package/dist/wirings/queue/remote-queue-job.d.ts +1 -1
  97. package/dist/wirings/queue/remote-queue-job.js +5 -3
  98. package/dist/wirings/rpc/rpc-runner.d.ts +7 -1
  99. package/dist/wirings/rpc/rpc-runner.js +43 -21
  100. package/dist/wirings/rpc/rpc-types.d.ts +2 -0
  101. package/dist/wirings/scheduler/remote-scheduled-job.d.ts +1 -1
  102. package/dist/wirings/scheduler/remote-scheduled-job.js +2 -2
  103. package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -4
  104. package/dist/wirings/scheduler/scheduler-runner.js +7 -6
  105. package/dist/wirings/trigger/pikku-trigger-service.js +7 -4
  106. package/dist/wirings/trigger/webhook-source-runner.d.ts +7 -1
  107. package/dist/wirings/trigger/webhook-source-runner.js +11 -3
  108. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  109. package/dist/wirings/workflow/graph/graph-runner.js +49 -30
  110. package/dist/wirings/workflow/index.d.ts +3 -1
  111. package/dist/wirings/workflow/index.js +1 -0
  112. package/dist/wirings/workflow/pikku-scenario-service.js +2 -0
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +37 -40
  114. package/dist/wirings/workflow/pikku-workflow-service.js +122 -133
  115. package/dist/wirings/workflow/workflow-child-step.d.ts +2 -2
  116. package/dist/wirings/workflow/workflow-child-step.js +11 -4
  117. package/dist/wirings/workflow/workflow-compensation.d.ts +6 -0
  118. package/dist/wirings/workflow/workflow-compensation.js +15 -1
  119. package/dist/wirings/workflow/workflow-constants.d.ts +13 -0
  120. package/dist/wirings/workflow/workflow-constants.js +13 -0
  121. package/dist/wirings/workflow/workflow-dsl-pass.d.ts +2 -0
  122. package/dist/wirings/workflow/workflow-dsl-pass.js +27 -11
  123. package/dist/wirings/workflow/workflow-recovery.d.ts +131 -3
  124. package/dist/wirings/workflow/workflow-recovery.js +233 -7
  125. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +1 -0
  126. package/dist/wirings/workflow/workflow-run-mirror.d.ts +4 -0
  127. package/dist/wirings/workflow/workflow-run-mirror.js +16 -0
  128. package/dist/wirings/workflow/workflow-run-status.d.ts +2 -0
  129. package/dist/wirings/workflow/workflow-run-status.js +13 -0
  130. package/dist/wirings/workflow/workflow-run-trace.d.ts +22 -0
  131. package/dist/wirings/workflow/workflow-run-trace.js +24 -0
  132. package/dist/wirings/workflow/workflow-step-claim.d.ts +18 -0
  133. package/dist/wirings/workflow/workflow-step-claim.js +47 -0
  134. package/dist/wirings/workflow/workflow-step-retry.d.ts +6 -2
  135. package/dist/wirings/workflow/workflow-step-retry.js +8 -3
  136. package/dist/wirings/workflow/workflow-wire.d.ts +12 -0
  137. package/dist/wirings/workflow/workflow-wire.js +33 -0
  138. package/dist/wirings/workflow/workflow.types.d.ts +16 -0
  139. package/package.json +2 -1
@@ -0,0 +1,777 @@
1
+ import { addFunction } from '../../function/function-runner.js';
2
+ import { beginChanges } from '../../function/abort-scope.js';
3
+ import { ConflictError, UnprocessableContentError, } from '../../errors/errors.js';
4
+ import { pikkuMeshAuthMiddleware } from '../../middleware/mesh-auth.js';
5
+ import { telemetryOuter } from '../../middleware/telemetry.js';
6
+ import { pikkuState } from '../../pikku-state.js';
7
+ import { httpRouter } from '../../wirings/http/routers/http-router.js';
8
+ import { fetch as pikkuFetch, wireHTTP, } from '../../wirings/http/http-runner.js';
9
+ import { wireChannel } from '../../wirings/channel/channel-runner.js';
10
+ import { runLocalChannel } from '../../wirings/channel/local/local-channel-runner.js';
11
+ import { runMCPTool } from '../../wirings/mcp/mcp-runner.js';
12
+ import { runCLICommand } from '../../wirings/cli/cli-runner.js';
13
+ import { runQueueJob } from '../../wirings/queue/queue-runner.js';
14
+ import { runScheduledTask } from '../../wirings/scheduler/scheduler-runner.js';
15
+ import { createListenerMessageHandler, wireGateway, } from '../../wirings/gateway/gateway-runner.js';
16
+ import { rpcService } from '../../wirings/rpc/rpc-runner.js';
17
+ import { PikkuTriggerService } from '../../wirings/trigger/pikku-trigger-service.js';
18
+ import { buildToolDefs } from '../../wirings/agent/agent-prepare.js';
19
+ import { QueueJobDiscardedError, } from '../../wirings/queue/index.js';
20
+ import { continueTrace, traceWire, tracedLogger, } from '../../trace.js';
21
+ import { QueuedWorkflowHarness } from '../service-tests/queued-workflow-harness.js';
22
+ import { ScheduledSleeps, defineRun } from './workflow-cases.js';
23
+ import { TRANSPORTS } from './transports.js';
24
+ const VALID = (index) => {
25
+ const id = (index + 1).toString(16);
26
+ return `00-${id.padStart(32, 'a')}-${id.padStart(16, 'b')}-01`;
27
+ };
28
+ /**
29
+ * Well formed and wrong: all-zero ids and an unknown version. A trace name a
30
+ * caller can type but no real tracer would send.
31
+ */
32
+ const FORGED = [
33
+ `00-${'0'.repeat(32)}-${'b'.repeat(16)}-01`,
34
+ `ff-${'a'.repeat(32)}-${'b'.repeat(16)}-01`,
35
+ ];
36
+ const MESH_SECRET = 'matrix-mesh-secret-of-a-sufficient-length-0123456789';
37
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
38
+ /** Starts whose function runs before a failure can be told from the wire alone. */
39
+ export const inboundContinues = (start, inbound) => inbound === 'valid' &&
40
+ ['http', 'queue-job', 'schedule', 'agent-tool'].includes(start);
41
+ const sessionless = (name, extra = {}) => ({
42
+ name,
43
+ pikkuFuncId: name,
44
+ sessionless: true,
45
+ inputSchemaName: null,
46
+ outputSchemaName: null,
47
+ ...extra,
48
+ });
49
+ /**
50
+ * Runs a chain — a start, one hop, an outcome — through the real runners, with
51
+ * brokers and the network replaced by in-process stand-ins that keep their wire
52
+ * format. Nothing here decides a trace: every id comes from the code under test.
53
+ */
54
+ export class TraceChain {
55
+ combo;
56
+ logs = [];
57
+ h = new QueuedWorkflowHarness(new ScheduledSleeps());
58
+ instances;
59
+ pending = [];
60
+ scheduled = [];
61
+ /** Sleep wake-ups armed through the scheduler. */
62
+ sleeps = [];
63
+ settlers = new Map();
64
+ transport;
65
+ fetchBefore = globalThis.fetch;
66
+ jobIds = 0;
67
+ timing;
68
+ inbound;
69
+ outcome;
70
+ constructor(combo) {
71
+ this.combo = combo;
72
+ this.timing = combo.timing ?? 'immediate';
73
+ this.inbound = combo.inbound ?? 'none';
74
+ this.outcome = combo.outcome;
75
+ this.transport = TRANSPORTS[combo.adapter ?? 'in-memory'];
76
+ const overlaps = this.timing === 'slow-overlap' || this.timing === 'concurrent-starts';
77
+ const length = combo.instances ?? (overlaps ? 2 : 1);
78
+ this.instances = Array.from({ length }, (_, index) => ({
79
+ index,
80
+ sent: this.inboundFor(index),
81
+ relays: [],
82
+ entries: [],
83
+ targets: [],
84
+ outgoing: [],
85
+ targetStartedAt: [],
86
+ }));
87
+ this.installServices();
88
+ if (combo.start === 'agent-tool' && this.inbound === 'valid') {
89
+ for (const instance of this.instances) {
90
+ instance.caller = continueTrace(instance.sent);
91
+ }
92
+ }
93
+ for (const instance of this.instances)
94
+ this.wire(instance);
95
+ httpRouter.initialize();
96
+ if (this.usesFetch)
97
+ this.installFetch();
98
+ }
99
+ inboundFor(index) {
100
+ switch (this.inbound) {
101
+ case 'none':
102
+ return undefined;
103
+ case 'valid':
104
+ return VALID(index);
105
+ case 'invalid':
106
+ return `not-a-traceparent-${index}`;
107
+ case 'forged':
108
+ return FORGED[index % FORGED.length];
109
+ }
110
+ }
111
+ get usesFetch() {
112
+ return this.combo.hop === 'remote-addon';
113
+ }
114
+ logger(scope) {
115
+ const write = (level) => (message) => {
116
+ this.logs.push({ level, scope, message });
117
+ };
118
+ return {
119
+ info: write('info'),
120
+ warn: write('warn'),
121
+ error: write('error'),
122
+ debug: write('debug'),
123
+ scope: (id) => this.logger(id),
124
+ };
125
+ }
126
+ installServices() {
127
+ const self = this;
128
+ const workflowQueue = this.singletons.queueService;
129
+ const queueService = {
130
+ supportsResults: false,
131
+ add: async (queueName, data, options) => {
132
+ if (!/^q-\d+$/.test(queueName)) {
133
+ return workflowQueue.add(queueName, data, options);
134
+ }
135
+ const index = Number(queueName.split('-').at(-1));
136
+ const id = `job-${++self.jobIds}`;
137
+ self.pending.push({
138
+ index,
139
+ queueName,
140
+ encoded: self.transport.encode(data, options),
141
+ options,
142
+ id,
143
+ });
144
+ return id;
145
+ },
146
+ getJob: async () => null,
147
+ };
148
+ const schedulerService = {
149
+ scheduleRPC: async (_delay, rpc, _data, _session, traceparent) => {
150
+ if (rpc === this.h.ws['getConfig']().sleeperRPCName) {
151
+ this.sleeps.push({ data: _data, traceparent });
152
+ return 'sleep';
153
+ }
154
+ this.scheduled.push({
155
+ index: Number(rpc.split('-').at(-1)),
156
+ rpc,
157
+ traceparent,
158
+ });
159
+ return 'task';
160
+ },
161
+ };
162
+ const deploymentService = {
163
+ invoke: (name, data, _s, tp) => this.callMesh(name, data, tp),
164
+ };
165
+ pikkuState(null, 'package', 'singletonServices', {
166
+ logger: this.logger(),
167
+ config: {},
168
+ queueService,
169
+ schedulerService,
170
+ deploymentService,
171
+ workflowService: this.h.ws,
172
+ secrets: {
173
+ getSecret: async () => ({ reveal: () => MESH_SECRET }),
174
+ },
175
+ jwt: {
176
+ decode: async (token) => {
177
+ if (token !== 'good')
178
+ throw new Error('bad signature');
179
+ return { aud: 'pikku-mesh' };
180
+ },
181
+ },
182
+ schema: {
183
+ compileSchema: async () => { },
184
+ getSchemaNames: () => new Set(),
185
+ validateSchema: async (key) => {
186
+ if (key.includes('Refused')) {
187
+ throw new UnprocessableContentError('schema refused the data');
188
+ }
189
+ },
190
+ },
191
+ });
192
+ }
193
+ async callMesh(name, data, traceparent) {
194
+ const index = Number(name.split('-').at(-1));
195
+ const instance = this.instances[index];
196
+ if (traceparent)
197
+ instance.outgoing.push(traceparent);
198
+ const response = await pikkuFetch(new Request(`http://mesh/mesh/rpc/${name}`, {
199
+ method: 'POST',
200
+ headers: {
201
+ 'content-type': 'application/json',
202
+ authorization: this.outcome === 'auth-mesh-jwt' ? 'Bearer forged' : 'Bearer good',
203
+ ...(traceparent ? { traceparent } : {}),
204
+ },
205
+ body: JSON.stringify(data ?? {}),
206
+ }));
207
+ const body = await response.json().catch(() => undefined);
208
+ if (!response.ok) {
209
+ instance.calleeErrorId = body?.errorId;
210
+ throw new ConflictError(`mesh answered ${response.status}`);
211
+ }
212
+ return body;
213
+ }
214
+ installFetch() {
215
+ const answer = async (url, init) => {
216
+ const index = Number(/far-(\d)/.exec(url)?.[1]);
217
+ const instance = this.instances[index];
218
+ const traceparent = init.headers.traceparent;
219
+ if (traceparent)
220
+ instance.outgoing.push(traceparent);
221
+ switch (this.outcome) {
222
+ case 'remote-4xx':
223
+ return new Response('{"name":"NotARegisteredError"}', { status: 404 });
224
+ case 'remote-5xx':
225
+ return new Response('nope', { status: 503 });
226
+ case 'remote-network':
227
+ throw new TypeError('connection refused');
228
+ case 'remote-timeout':
229
+ return new Promise((_, reject) => {
230
+ init.signal.addEventListener('abort', () => reject(init.signal.reason));
231
+ });
232
+ default:
233
+ return Response.json({ ok: true });
234
+ }
235
+ };
236
+ globalThis.fetch = ((url, init) => answer(String(url), init));
237
+ }
238
+ /** Restores what the constructor replaced. Call in a `finally`. */
239
+ dispose() {
240
+ globalThis.fetch = this.fetchBefore;
241
+ }
242
+ wire(instance) {
243
+ const { index } = instance;
244
+ const target = `target-${index}`;
245
+ const entry = `entry-${index}`;
246
+ const entryFunc = async (services, data, wire) => {
247
+ instance.entries.push(wire.trace);
248
+ await this.hop(instance, services, wire);
249
+ if (this.entryFails)
250
+ await this.failTarget();
251
+ if (this.timing === 'enqueue-before-fail') {
252
+ throw new ConflictError('failed just after enqueuing');
253
+ }
254
+ this.finishEntry(instance);
255
+ return { ok: true };
256
+ };
257
+ this.register(entry, entryFunc, {
258
+ schema: this.entryFails && this.isSchemaOutcome,
259
+ telemetry: true,
260
+ });
261
+ this.register(target, async (_s, _d, wire) => {
262
+ instance.targets.push(wire.trace);
263
+ instance.targetStartedAt.push(performance.now());
264
+ if (this.timing === 'slow-overlap')
265
+ await delay(2 - index);
266
+ if (this.timing === 'retry-backoff' &&
267
+ this.outcome === 'success' &&
268
+ instance.targets.length === 1) {
269
+ throw new Error('flaky');
270
+ }
271
+ if (!this.entryFails)
272
+ await this.failTarget();
273
+ return { ok: true };
274
+ }, {
275
+ permissions: this.outcome === 'permission-denied',
276
+ schema: !this.entryFails && this.isSchemaOutcome,
277
+ });
278
+ pikkuState(null, 'rpc', 'meta')[target] = target;
279
+ this.wireStart(instance, entry, entryFunc);
280
+ this.wireHop(instance, target);
281
+ }
282
+ /**
283
+ * Where the outcome lands: on the entry when the next hop runs nothing of
284
+ * ours, or when it is about the job the chain started as. Otherwise on the
285
+ * next function.
286
+ */
287
+ get entryFails() {
288
+ const { hop, start } = this.combo;
289
+ return (hop === 'remote-addon' ||
290
+ (start === 'queue-job' &&
291
+ hop !== 'enqueue' &&
292
+ this.outcome.startsWith('job-')));
293
+ }
294
+ /** An HTTP route refusing a forged mesh token, before any function runs. */
295
+ get meshGuardsEntry() {
296
+ return this.outcome === 'auth-mesh-jwt' && this.combo.hop !== 'mesh';
297
+ }
298
+ get isSchemaOutcome() {
299
+ return this.outcome === 'schema-invalid' || this.outcome === 'job-poison';
300
+ }
301
+ finishEntry(instance) {
302
+ instance.entryFinishedAt = performance.now();
303
+ }
304
+ register(name, func, { permissions, schema, telemetry, } = {}) {
305
+ addFunction(name, {
306
+ func,
307
+ auth: false,
308
+ ...(permissions
309
+ ? {
310
+ permissions: {
311
+ refuse: async () => {
312
+ return false;
313
+ },
314
+ },
315
+ }
316
+ : {}),
317
+ ...(telemetry ? { middleware: [telemetryOuter()] } : {}),
318
+ });
319
+ const inputSchemaName = schema ? 'Refused' : 'Accepted';
320
+ pikkuState(null, 'function', 'meta')[name] = sessionless(name, {
321
+ inputSchemaName,
322
+ });
323
+ pikkuState(null, 'misc', 'schemas').set(inputSchemaName, {});
324
+ }
325
+ async failTarget() {
326
+ switch (this.outcome) {
327
+ case 'throw-pikku-error':
328
+ throw new ConflictError('refused');
329
+ case 'throw-plain-error':
330
+ case 'job-exhausted':
331
+ throw new Error('boom');
332
+ case 'job-discarded':
333
+ throw new QueueJobDiscardedError('job', 'not wanted');
334
+ case 'abandoned':
335
+ await beginChanges({ abandoned: true, reason: 'the caller left' });
336
+ }
337
+ }
338
+ wireStart(instance, entry, func) {
339
+ const { index } = instance;
340
+ switch (this.combo.start) {
341
+ case 'http': {
342
+ const route = `/entry-${index}`;
343
+ const http = pikkuState(null, 'http', 'meta');
344
+ http.post[route] = {
345
+ pikkuFuncId: entry,
346
+ route,
347
+ method: 'post',
348
+ };
349
+ wireHTTP({
350
+ route,
351
+ method: 'post',
352
+ auth: false,
353
+ ...(this.meshGuardsEntry
354
+ ? { middleware: [pikkuMeshAuthMiddleware] }
355
+ : {}),
356
+ func: { func, auth: false },
357
+ });
358
+ return;
359
+ }
360
+ case 'channel':
361
+ pikkuState(null, 'channel', 'meta')[entry] = {
362
+ name: entry,
363
+ route: `/channel-${index}`,
364
+ messageWirings: { action: { go: { pikkuFuncId: entry } } },
365
+ };
366
+ wireChannel({
367
+ name: entry,
368
+ route: `/channel-${index}`,
369
+ onMessageWiring: { action: { go: { func, auth: false } } },
370
+ });
371
+ return;
372
+ case 'mcp':
373
+ pikkuState(null, 'mcp', 'toolsMeta')[entry] = {
374
+ pikkuFuncId: entry,
375
+ };
376
+ return;
377
+ case 'cli':
378
+ pikkuState(null, 'cli', 'meta', {
379
+ programs: {
380
+ ...pikkuState(null, 'cli', 'meta')?.programs,
381
+ [entry]: {
382
+ program: entry,
383
+ commands: {
384
+ go: {
385
+ command: 'go',
386
+ pikkuFuncId: entry,
387
+ positionals: [],
388
+ options: {},
389
+ },
390
+ },
391
+ options: {},
392
+ },
393
+ },
394
+ renderers: {},
395
+ });
396
+ pikkuState(null, 'cli', 'programs', {
397
+ ...pikkuState(null, 'cli', 'programs'),
398
+ [entry]: { middleware: [], renderers: {} },
399
+ });
400
+ return;
401
+ case 'gateway':
402
+ wireGateway(this.gatewayConfig(entry, func));
403
+ return;
404
+ case 'queue-job':
405
+ pikkuState(null, 'queue', 'meta')[`job-${index}`] = {
406
+ pikkuFuncId: entry,
407
+ };
408
+ pikkuState(null, 'queue', 'registrations').set(`job-${index}`, {
409
+ name: `job-${index}`,
410
+ func: {},
411
+ });
412
+ return;
413
+ case 'schedule':
414
+ pikkuState(null, 'scheduler', 'meta')[entry] = {
415
+ name: entry,
416
+ pikkuFuncId: entry,
417
+ schedule: '* * * * *',
418
+ };
419
+ pikkuState(null, 'scheduler', 'tasks').set(entry, {
420
+ name: entry,
421
+ schedule: '* * * * *',
422
+ func: { func },
423
+ });
424
+ return;
425
+ case 'agent-tool':
426
+ pikkuState(null, 'agent', 'agentsMeta')[entry] = {
427
+ name: entry,
428
+ tools: [entry],
429
+ };
430
+ pikkuState(null, 'agent', 'agents').set(entry, { name: entry });
431
+ pikkuState(null, 'rpc', 'meta')[entry] = entry;
432
+ return;
433
+ case 'trigger':
434
+ pikkuState(null, 'rpc', 'meta')[entry] = entry;
435
+ return;
436
+ }
437
+ }
438
+ gatewayConfig(entry, func) {
439
+ return {
440
+ name: entry,
441
+ type: 'listener',
442
+ adapter: {
443
+ name: 'matrix',
444
+ parse: (raw) => ({ senderId: 'sender', text: 'hi', raw }),
445
+ send: async () => { },
446
+ },
447
+ func: { func, auth: false },
448
+ };
449
+ }
450
+ wireHop(instance, target) {
451
+ const { index } = instance;
452
+ switch (this.combo.hop) {
453
+ case 'mesh': {
454
+ const route = `/mesh/rpc/${target}`;
455
+ const http = pikkuState(null, 'http', 'meta');
456
+ http.post[route] = {
457
+ pikkuFuncId: target,
458
+ route,
459
+ method: 'post',
460
+ };
461
+ wireHTTP({
462
+ route,
463
+ method: 'post',
464
+ auth: false,
465
+ middleware: [pikkuMeshAuthMiddleware],
466
+ func: {
467
+ func: pikkuState(null, 'function', 'functions').get(target).func,
468
+ auth: false,
469
+ },
470
+ });
471
+ return;
472
+ }
473
+ case 'remote-addon':
474
+ pikkuState(null, 'addons', 'packages').set(`far-${index}`, {
475
+ name: `far-${index}`,
476
+ package: `@far/pkg-${index}`,
477
+ remote: true,
478
+ serverUrl: `https://far-${index}.test`,
479
+ remoteTimeoutMs: 2,
480
+ ...(this.outcome === 'auth-remote-addon'
481
+ ? { remoteAuth: { resolve: () => '' } }
482
+ : {}),
483
+ });
484
+ return;
485
+ case 'enqueue':
486
+ pikkuState(null, 'queue', 'meta')[`q-${index}`] = {
487
+ pikkuFuncId: target,
488
+ };
489
+ pikkuState(null, 'queue', 'registrations').set(`q-${index}`, {
490
+ name: `q-${index}`,
491
+ func: {},
492
+ });
493
+ return;
494
+ case 'start-workflow':
495
+ case 'child-workflow':
496
+ this.settlers.set(index, defineRun(this.h, {
497
+ id: String(index),
498
+ workflow: (this.combo.workflow ?? 'dsl-queued'),
499
+ step: (this.combo.step ?? 'rpc'),
500
+ outcome: this.outcome,
501
+ }, this.combo.hop === 'child-workflow'));
502
+ return;
503
+ }
504
+ }
505
+ async hop(instance, services, wire) {
506
+ const { index } = instance;
507
+ const target = `target-${index}`;
508
+ switch (this.combo.hop) {
509
+ case 'rpc':
510
+ return wire.rpc.invoke(target, {});
511
+ case 'mesh':
512
+ return wire.rpc.mesh(target, {});
513
+ case 'remote-addon':
514
+ return wire.rpc.invoke(`far-${index}:fn`, {});
515
+ case 'enqueue':
516
+ return services.queueService.add(`q-${index}`, { index }, this.jobOptions);
517
+ case 'schedule':
518
+ return services.schedulerService.scheduleRPC(5, target, {});
519
+ case 'start-workflow':
520
+ case 'child-workflow': {
521
+ const { runId } = await wire.rpc.startWorkflow(`flow-${index}`, {}, {
522
+ inline: this.combo.workflow?.endsWith('-inline'),
523
+ });
524
+ instance.runId = runId;
525
+ }
526
+ }
527
+ }
528
+ get jobOptions() {
529
+ return {
530
+ ...(this.timing === 'delayed-job' ? { delay: 1 } : {}),
531
+ ...(this.outcome === 'job-exhausted' || this.timing === 'retry-backoff'
532
+ ? { attempts: 3, backoff: { type: 'fixed', delay: 1 } }
533
+ : {}),
534
+ };
535
+ }
536
+ /** Runs the chain from its start, then every job and timer it left behind. */
537
+ async run() {
538
+ await Promise.all(this.instances.map((i) => this.startInstance(i)));
539
+ await this.deliver();
540
+ await Promise.all(this.instances.map((i) => this.settle(i)));
541
+ await this.deliver();
542
+ return this.instances;
543
+ }
544
+ async settle(instance) {
545
+ const settler = this.settlers.get(instance.index);
546
+ if (!instance.runId || !settler)
547
+ return;
548
+ await settler.settle(instance.runId, async () => {
549
+ for (const { data } of this.sleeps.splice(0)) {
550
+ await this.h.ws.executeWorkflowSleepCompleted(data.runId, data.stepId);
551
+ }
552
+ });
553
+ }
554
+ async startInstance(instance) {
555
+ try {
556
+ await this.drive(instance);
557
+ }
558
+ catch (e) {
559
+ instance.failure = e;
560
+ instance.errorId ??= e?.error?.data?.errorId;
561
+ instance.callerRefused = true;
562
+ }
563
+ instance.entryFinishedAt ??= performance.now();
564
+ }
565
+ async drive(instance) {
566
+ const { index } = instance;
567
+ const entry = `entry-${index}`;
568
+ switch (this.combo.start) {
569
+ case 'http': {
570
+ const response = await pikkuFetch(new Request(`http://matrix/entry-${index}`, {
571
+ method: 'POST',
572
+ headers: {
573
+ 'content-type': 'application/json',
574
+ ...(instance.sent ? { traceparent: instance.sent } : {}),
575
+ ...(this.meshGuardsEntry
576
+ ? { authorization: 'Bearer forged' }
577
+ : {}),
578
+ },
579
+ body: '{}',
580
+ }), {
581
+ trustInboundTraceparent: this.inbound !== 'forged' && !this.meshGuardsEntry,
582
+ });
583
+ const body = await response.json().catch(() => undefined);
584
+ instance.errorId = body?.errorId;
585
+ instance.callerRefused = !response.ok;
586
+ return;
587
+ }
588
+ case 'channel': {
589
+ const channel = await runLocalChannel({
590
+ channelId: `channel-${index}`,
591
+ route: `/channel-${index}`,
592
+ request: {
593
+ path: () => `/channel-${index}`,
594
+ method: () => 'get',
595
+ header: (name) => name === 'traceparent' ? (instance.sent ?? null) : null,
596
+ cookie: () => null,
597
+ params: () => ({}),
598
+ setParams: () => { },
599
+ query: () => ({}),
600
+ data: async () => ({}),
601
+ json: async () => ({}),
602
+ },
603
+ response: {
604
+ status() {
605
+ return this;
606
+ },
607
+ header() {
608
+ return this;
609
+ },
610
+ json() { },
611
+ cookie() {
612
+ return this;
613
+ },
614
+ },
615
+ });
616
+ if (!channel)
617
+ throw new Error('the channel did not open');
618
+ const sent = [];
619
+ channel.registerOnSend((message) => {
620
+ sent.push(message);
621
+ });
622
+ await channel.open();
623
+ await channel.message(JSON.stringify({ action: 'go' }));
624
+ const failed = sent.find((message) => message?.error);
625
+ if (failed) {
626
+ instance.failure = failed;
627
+ instance.errorId = failed.errorId;
628
+ instance.callerRefused = true;
629
+ }
630
+ return;
631
+ }
632
+ case 'mcp':
633
+ await runMCPTool({ jsonrpc: '2.0', id: 1, params: {} }, {
634
+ mcp: {
635
+ sendResourceUpdated: () => { },
636
+ enableResources: async () => true,
637
+ enablePrompts: async () => true,
638
+ enableTools: async () => true,
639
+ },
640
+ ...(instance.sent
641
+ ? {
642
+ http: {
643
+ request: {
644
+ header: (name) => name === 'traceparent' ? instance.sent : null,
645
+ },
646
+ },
647
+ }
648
+ : {}),
649
+ }, entry);
650
+ return;
651
+ case 'cli':
652
+ await runCLICommand({
653
+ program: entry,
654
+ commandPath: ['go'],
655
+ data: {},
656
+ singletonServices: this.singletons,
657
+ onOutput: () => { },
658
+ });
659
+ return;
660
+ case 'gateway': {
661
+ const config = this.gatewayConfig(entry, pikkuState(null, 'function', 'functions').get(entry).func);
662
+ await createListenerMessageHandler(entry, config, this.singletons)({});
663
+ return;
664
+ }
665
+ case 'queue-job':
666
+ this.pending.push({
667
+ index,
668
+ queueName: `job-${index}`,
669
+ encoded: this.transport.encode({}, { traceparent: instance.sent, ...this.startJobOptions }),
670
+ options: this.startJobOptions,
671
+ id: `start-${index}`,
672
+ });
673
+ return;
674
+ case 'schedule':
675
+ await runScheduledTask({ name: entry, traceparent: instance.sent });
676
+ return;
677
+ case 'agent-tool': {
678
+ const { tools } = await buildToolDefs(instance.caller ? { trace: instance.caller } : {}, new Map(), 'resource-1', entry, null, undefined, []);
679
+ await tools[0].execute({});
680
+ return;
681
+ }
682
+ case 'trigger':
683
+ await new MatrixTriggers().fire(entry);
684
+ return;
685
+ }
686
+ }
687
+ get startJobOptions() {
688
+ return this.jobOptions;
689
+ }
690
+ get singletons() {
691
+ return pikkuState(null, 'package', 'singletonServices');
692
+ }
693
+ /** Delivers jobs and scheduled calls the way the adapter would. */
694
+ async deliver() {
695
+ for (const { index, rpc, traceparent } of this.scheduled.splice(0)) {
696
+ await this.fire(index, rpc, traceparent);
697
+ }
698
+ const batches = this.pending.splice(0);
699
+ await Promise.all(batches.map((job) => this.deliverJob(job)));
700
+ await this.h.pump();
701
+ if (this.pending.length > 0)
702
+ await this.deliver();
703
+ }
704
+ async deliverJob(job) {
705
+ const instance = this.instances[job.index];
706
+ const { data, traceparent } = this.transport.decode(job.encoded);
707
+ instance.job = job.encoded;
708
+ if (traceparent && job.queueName.startsWith('q-')) {
709
+ instance.outgoing.push(traceparent);
710
+ }
711
+ const attempt = () => runQueueJob({
712
+ job: {
713
+ id: job.id,
714
+ queueName: job.queueName,
715
+ data,
716
+ status: () => 'active',
717
+ metadata: () => ({
718
+ attemptsMade: 0,
719
+ maxAttempts: 1,
720
+ createdAt: new Date(),
721
+ }),
722
+ traceparent,
723
+ },
724
+ });
725
+ const attempts = job.options?.attempts ?? 1;
726
+ if (job.options?.delay)
727
+ await delay(job.options.delay);
728
+ for (let n = 0; n < attempts; n++) {
729
+ try {
730
+ await this.deliveredOnce(attempt);
731
+ return;
732
+ }
733
+ catch (e) {
734
+ instance.failure ??= e;
735
+ if (e instanceof QueueJobDiscardedError)
736
+ return;
737
+ }
738
+ }
739
+ }
740
+ async deliveredOnce(attempt) {
741
+ if (this.timing === 'duplicate-delivery') {
742
+ await attempt().catch(() => { });
743
+ await attempt();
744
+ }
745
+ else if (this.timing === 'lease-expiry-redelivery') {
746
+ // The lease ran out while the first delivery was still working.
747
+ const [first] = await Promise.allSettled([attempt(), attempt()]);
748
+ if (first.status === 'rejected')
749
+ throw first.reason;
750
+ }
751
+ else {
752
+ await attempt();
753
+ }
754
+ }
755
+ /** What a scheduler's worker does with a one-off `scheduleRPC`. */
756
+ async fire(index, rpc, traceparent) {
757
+ const instance = this.instances[index];
758
+ if (traceparent)
759
+ instance.outgoing.push(traceparent);
760
+ const services = this.singletons;
761
+ const wire = traceWire(traceparent);
762
+ instance.relays.push(wire.trace);
763
+ await rpcService
764
+ .getContextRPCService(services, wire, false)
765
+ .invoke(rpc, {})
766
+ .catch((e) => {
767
+ tracedLogger(services.logger, wire.traceId).error(e);
768
+ instance.failure ??= e;
769
+ });
770
+ }
771
+ }
772
+ class MatrixTriggers extends PikkuTriggerService {
773
+ async start() { }
774
+ fire(name) {
775
+ return this.onTriggerFire(name, [{ targetType: 'rpc', targetName: name }], {});
776
+ }
777
+ }