@delexec/ops 0.1.0

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 (55) hide show
  1. package/README.md +3 -0
  2. package/README.zh-CN.md +6 -0
  3. package/node_modules/@delexec/caller-controller/README.md +3 -0
  4. package/node_modules/@delexec/caller-controller/README.zh-CN.md +6 -0
  5. package/node_modules/@delexec/caller-controller/package.json +53 -0
  6. package/node_modules/@delexec/caller-controller/src/server.js +127 -0
  7. package/node_modules/@delexec/caller-controller-core/README.md +3 -0
  8. package/node_modules/@delexec/caller-controller-core/README.zh-CN.md +6 -0
  9. package/node_modules/@delexec/caller-controller-core/package.json +26 -0
  10. package/node_modules/@delexec/caller-controller-core/src/index.js +1612 -0
  11. package/node_modules/@delexec/caller-skill-adapter/package.json +12 -0
  12. package/node_modules/@delexec/caller-skill-adapter/src/server.js +1042 -0
  13. package/node_modules/@delexec/caller-skill-mcp-adapter/README.md +65 -0
  14. package/node_modules/@delexec/caller-skill-mcp-adapter/package.json +16 -0
  15. package/node_modules/@delexec/caller-skill-mcp-adapter/src/server.js +527 -0
  16. package/node_modules/@delexec/responder-controller/README.md +3 -0
  17. package/node_modules/@delexec/responder-controller/README.zh-CN.md +6 -0
  18. package/node_modules/@delexec/responder-controller/package.json +53 -0
  19. package/node_modules/@delexec/responder-controller/src/server.js +254 -0
  20. package/node_modules/@delexec/responder-runtime-core/README.md +3 -0
  21. package/node_modules/@delexec/responder-runtime-core/README.zh-CN.md +6 -0
  22. package/node_modules/@delexec/responder-runtime-core/package.json +26 -0
  23. package/node_modules/@delexec/responder-runtime-core/src/executors.js +326 -0
  24. package/node_modules/@delexec/responder-runtime-core/src/index.js +1202 -0
  25. package/node_modules/@delexec/runtime-utils/README.md +3 -0
  26. package/node_modules/@delexec/runtime-utils/README.zh-CN.md +6 -0
  27. package/node_modules/@delexec/runtime-utils/package.json +23 -0
  28. package/node_modules/@delexec/runtime-utils/src/index.js +338 -0
  29. package/node_modules/@delexec/sqlite-store/README.md +3 -0
  30. package/node_modules/@delexec/sqlite-store/README.zh-CN.md +6 -0
  31. package/node_modules/@delexec/sqlite-store/package.json +26 -0
  32. package/node_modules/@delexec/sqlite-store/src/index.js +68 -0
  33. package/node_modules/@delexec/transport-email/README.md +3 -0
  34. package/node_modules/@delexec/transport-email/README.zh-CN.md +6 -0
  35. package/node_modules/@delexec/transport-email/package.json +23 -0
  36. package/node_modules/@delexec/transport-email/src/index.js +185 -0
  37. package/node_modules/@delexec/transport-emailengine/README.md +3 -0
  38. package/node_modules/@delexec/transport-emailengine/README.zh-CN.md +6 -0
  39. package/node_modules/@delexec/transport-emailengine/package.json +26 -0
  40. package/node_modules/@delexec/transport-emailengine/src/index.js +210 -0
  41. package/node_modules/@delexec/transport-gmail/README.md +3 -0
  42. package/node_modules/@delexec/transport-gmail/README.zh-CN.md +6 -0
  43. package/node_modules/@delexec/transport-gmail/package.json +26 -0
  44. package/node_modules/@delexec/transport-gmail/src/index.js +295 -0
  45. package/node_modules/@delexec/transport-relay-http/README.md +3 -0
  46. package/node_modules/@delexec/transport-relay-http/README.zh-CN.md +6 -0
  47. package/node_modules/@delexec/transport-relay-http/package.json +23 -0
  48. package/node_modules/@delexec/transport-relay-http/src/index.js +124 -0
  49. package/package.json +64 -0
  50. package/src/cli.js +1571 -0
  51. package/src/config.js +1180 -0
  52. package/src/example-hotline-worker.js +65 -0
  53. package/src/example-hotline.js +196 -0
  54. package/src/logging.js +56 -0
  55. package/src/supervisor.js +3070 -0
@@ -0,0 +1,1202 @@
1
+ import crypto from "node:crypto";
2
+ import http from "node:http";
3
+
4
+ import { buildStructuredError, canonicalizeResultPackageForSignature } from "@delexec/contracts";
5
+ import {
6
+ createConfiguredHotlineExecutor,
7
+ createExampleFunctionExecutor,
8
+ createFunctionExecutor,
9
+ createSimulatorExecutor,
10
+ createHotlineRouterExecutor,
11
+ deferTask
12
+ } from "./executors.js";
13
+
14
+ function nowIso() {
15
+ return new Date().toISOString();
16
+ }
17
+
18
+ function parseJsonBody(req) {
19
+ return new Promise((resolve, reject) => {
20
+ const chunks = [];
21
+ req.on("data", (chunk) => chunks.push(chunk));
22
+ req.on("end", () => {
23
+ if (chunks.length === 0) {
24
+ resolve({});
25
+ return;
26
+ }
27
+ try {
28
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
29
+ } catch {
30
+ reject(new Error("invalid_json"));
31
+ }
32
+ });
33
+ req.on("error", reject);
34
+ });
35
+ }
36
+
37
+ function sendJson(res, statusCode, data) {
38
+ res.writeHead(statusCode, {
39
+ "content-type": "application/json; charset=utf-8",
40
+ "access-control-allow-origin": "*",
41
+ "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
42
+ "access-control-allow-headers": "Content-Type, Authorization, X-Platform-Api-Key"
43
+ });
44
+ res.end(JSON.stringify(data));
45
+ }
46
+
47
+ function sendError(res, statusCode, code, message, { retryable, ...extra } = {}) {
48
+ sendJson(res, statusCode, buildStructuredError(code, message, { retryable, ...extra }));
49
+ }
50
+
51
+ async function postJson(baseUrl, pathname, { method = "POST", headers = {}, body } = {}) {
52
+ const response = await fetch(new URL(pathname, baseUrl), {
53
+ method,
54
+ headers: {
55
+ "content-type": "application/json; charset=utf-8",
56
+ ...headers
57
+ },
58
+ body: body === undefined ? undefined : JSON.stringify(body)
59
+ });
60
+
61
+ const text = await response.text();
62
+ return {
63
+ status: response.status,
64
+ body: text ? JSON.parse(text) : null
65
+ };
66
+ }
67
+
68
+ async function postMetricEvent(platform, body) {
69
+ if (!platform?.baseUrl || !platform.apiKey) {
70
+ return { ok: false, skipped: true };
71
+ }
72
+
73
+ const response = await postJson(platform.baseUrl, "/v1/metrics/events", {
74
+ headers: {
75
+ Authorization: `Bearer ${platform.apiKey}`
76
+ },
77
+ body
78
+ });
79
+
80
+ return { ok: response.status >= 200 && response.status < 300, response };
81
+ }
82
+
83
+ async function registerResponderOnPlatform(platform, body) {
84
+ if (!platform?.baseUrl) {
85
+ throw new Error("responder_platform_base_url_required");
86
+ }
87
+
88
+ const response = await postJson(platform.baseUrl, "/v2/responders/register", {
89
+ headers: platform.apiKey
90
+ ? {
91
+ Authorization: `Bearer ${platform.apiKey}`
92
+ }
93
+ : {},
94
+ body
95
+ });
96
+
97
+ if (response.status !== 201) {
98
+ const error = new Error("RESPONDER_PLATFORM_REGISTER_FAILED");
99
+ error.response = response;
100
+ throw error;
101
+ }
102
+
103
+ return response.body;
104
+ }
105
+
106
+ async function persistResponderState(onStateChanged, state) {
107
+ if (typeof onStateChanged === "function") {
108
+ await onStateChanged(state);
109
+ }
110
+ }
111
+
112
+ function buildResultTiming(task) {
113
+ const acceptedAt = task.accepted_at || task.enqueued_at || nowIso();
114
+ const finishedAt = task.completed_at || nowIso();
115
+ const acceptedMs = Date.parse(acceptedAt);
116
+ const finishedMs = Date.parse(finishedAt);
117
+ const elapsedMs =
118
+ Number.isFinite(acceptedMs) && Number.isFinite(finishedMs) ? Math.max(0, finishedMs - acceptedMs) : task.delay_ms;
119
+
120
+ return {
121
+ accepted_at: acceptedAt,
122
+ finished_at: finishedAt,
123
+ elapsed_ms: elapsedMs
124
+ };
125
+ }
126
+
127
+ function buildBaseResultPayload(task) {
128
+ return {
129
+ message_type: "remote_hotline_result",
130
+ request_id: task.request_id,
131
+ result_version: "0.1.0",
132
+ responder_id: task.responder_id,
133
+ hotline_id: task.hotline_id,
134
+ verification: task.verification || null,
135
+ timing: buildResultTiming(task)
136
+ };
137
+ }
138
+
139
+ function buildErrorResultPayload(task, { code, message, retryable = false, schemaValid = true, usage } = {}) {
140
+ return {
141
+ ...buildBaseResultPayload(task),
142
+ status: "error",
143
+ error: {
144
+ code,
145
+ message,
146
+ retryable
147
+ },
148
+ schema_valid: schemaValid,
149
+ usage: usage || { tokens_in: 0, tokens_out: 0 }
150
+ };
151
+ }
152
+
153
+ function buildGuardrailError(code, message) {
154
+ return {
155
+ status: "error",
156
+ error: {
157
+ code,
158
+ message,
159
+ retryable: false
160
+ },
161
+ schema_valid: true,
162
+ usage: { tokens_in: 0, tokens_out: 0 }
163
+ };
164
+ }
165
+
166
+ function buildResultPayload(task, execution) {
167
+ if (!execution || typeof execution !== "object") {
168
+ return buildErrorResultPayload(task, {
169
+ code: "EXECUTOR_INVALID_RESULT",
170
+ message: "Responder executor returned an invalid result object"
171
+ });
172
+ }
173
+
174
+ if (execution.status === "error") {
175
+ return buildErrorResultPayload(task, {
176
+ code: execution.error?.code || "EXECUTOR_RUNTIME_ERROR",
177
+ message: execution.error?.message || "Responder executor reported an error",
178
+ retryable: execution.error?.retryable === true,
179
+ schemaValid: execution.schema_valid !== false,
180
+ usage: execution.usage
181
+ });
182
+ }
183
+
184
+ if (execution.status !== "ok") {
185
+ return buildErrorResultPayload(task, {
186
+ code: "EXECUTOR_INVALID_RESULT",
187
+ message: "Responder executor must return status 'ok' or 'error'"
188
+ });
189
+ }
190
+
191
+ return {
192
+ ...buildBaseResultPayload(task),
193
+ status: "ok",
194
+ output: "output" in execution ? execution.output : null,
195
+ artifacts: sanitizeArtifactsForResult(execution.artifacts),
196
+ schema_valid: execution.schema_valid !== false,
197
+ usage: execution.usage || { tokens_in: 0, tokens_out: 0 }
198
+ };
199
+ }
200
+
201
+ function normalizeArtifactContent(artifact) {
202
+ if (Buffer.isBuffer(artifact?.content)) {
203
+ return artifact.content;
204
+ }
205
+ if (artifact?.content_base64) {
206
+ return Buffer.from(artifact.content_base64, "base64");
207
+ }
208
+ if (typeof artifact?.content === "string") {
209
+ return Buffer.from(artifact.content, "utf8");
210
+ }
211
+ return Buffer.alloc(0);
212
+ }
213
+
214
+ function materializeArtifacts(executionArtifacts = []) {
215
+ return (Array.isArray(executionArtifacts) ? executionArtifacts : []).map((artifact, index) => {
216
+ const content = normalizeArtifactContent(artifact);
217
+ return {
218
+ artifact_id: artifact?.artifact_id || `art_${index + 1}`,
219
+ name: artifact?.name || `artifact-${index + 1}.bin`,
220
+ media_type: artifact?.media_type || "application/octet-stream",
221
+ byte_size: content.length,
222
+ sha256: crypto.createHash("sha256").update(content).digest("hex"),
223
+ delivery: {
224
+ kind: "email_attachment"
225
+ },
226
+ content_base64: content.toString("base64")
227
+ };
228
+ });
229
+ }
230
+
231
+ function applyExecutionArtifacts(task, execution = {}) {
232
+ if (!execution || typeof execution !== "object") {
233
+ return execution;
234
+ }
235
+ if (!Array.isArray(execution.artifacts) || execution.artifacts.length === 0) {
236
+ return execution;
237
+ }
238
+ return {
239
+ ...execution,
240
+ artifacts: materializeArtifacts(execution.artifacts)
241
+ };
242
+ }
243
+
244
+ function sanitizeArtifactsForResult(artifacts = []) {
245
+ return (Array.isArray(artifacts) ? artifacts : []).map(({ content_base64, ...artifact }) => artifact);
246
+ }
247
+
248
+ function enforceArtifactSizeLimit(task, execution = {}) {
249
+ const maxAttachmentBytes = Number(process.env.EMAIL_MAX_ATTACHMENT_BYTES || 5 * 1024 * 1024);
250
+ const artifacts = Array.isArray(execution.artifacts) ? execution.artifacts : [];
251
+ const totalBytes = artifacts.reduce((sum, artifact) => sum + Number(artifact.byte_size || 0), 0);
252
+ if (totalBytes <= maxAttachmentBytes) {
253
+ return execution;
254
+ }
255
+
256
+ return {
257
+ status: "error",
258
+ error: {
259
+ code: "RESULT_ARTIFACT_TOO_LARGE",
260
+ message: `artifact payload exceeds email limit ${maxAttachmentBytes} bytes`,
261
+ retryable: false
262
+ },
263
+ schema_valid: true,
264
+ usage: execution.usage || { tokens_in: 0, tokens_out: 0 }
265
+ };
266
+ }
267
+
268
+ function signResultPayload(payload, state) {
269
+ const signingBytes = Buffer.from(JSON.stringify(canonicalizeResultPackageForSignature(payload)), "utf8");
270
+ const signature = crypto.sign(null, signingBytes, state.signing.privateKey);
271
+ return {
272
+ ...payload,
273
+ signature_algorithm: "Ed25519",
274
+ signer_public_key_pem: state.signing.publicKeyPem,
275
+ signature_base64: signature.toString("base64")
276
+ };
277
+ }
278
+
279
+ async function sendResultEnvelope(task, state, transport) {
280
+ const target = task.result_delivery?.address || task.return_route || task.reply_to;
281
+ if (!transport || !target || !task.result_package) {
282
+ return;
283
+ }
284
+
285
+ await transport.send({
286
+ message_id: `msg_result_${crypto.randomUUID()}`,
287
+ thread_id: task.thread_id || `req:${task.request_id}`,
288
+ from: state.identity.responder_id,
289
+ to: target,
290
+ type: "task.result",
291
+ request_id: task.request_id,
292
+ responder_id: state.identity.responder_id,
293
+ hotline_id: task.hotline_id,
294
+ verification: task.verification || null,
295
+ body_text: JSON.stringify(task.result_package),
296
+ attachments: ((task.execution_artifacts || []) || []).map((artifact) => ({
297
+ name: artifact.name,
298
+ media_type: artifact.media_type,
299
+ content_base64: artifact.content_base64,
300
+ byte_size: artifact.byte_size
301
+ })),
302
+ result_package: task.result_package,
303
+ sent_at: nowIso()
304
+ });
305
+ }
306
+
307
+ async function ackPlatform(task, platform) {
308
+ if (!platform?.baseUrl || !platform.apiKey) {
309
+ return { ok: false, skipped: true };
310
+ }
311
+
312
+ const response = await postJson(platform.baseUrl, `/v1/requests/${task.request_id}/ack`, {
313
+ headers: {
314
+ Authorization: `Bearer ${platform.apiKey}`
315
+ },
316
+ body: {
317
+ responder_id: platform.responderId || task.responder_id,
318
+ hotline_id: task.hotline_id,
319
+ eta_hint_s: Math.max(1, Math.ceil(task.delay_ms / 1000))
320
+ }
321
+ });
322
+
323
+ return { ok: response.status >= 200 && response.status < 300, response };
324
+ }
325
+
326
+ async function postRequestLifecycleEvent(task, platform, eventType, detail = {}) {
327
+ if (!platform?.baseUrl || !platform.apiKey) {
328
+ return { ok: false, skipped: true };
329
+ }
330
+
331
+ const response = await postJson(platform.baseUrl, `/v1/requests/${task.request_id}/events`, {
332
+ headers: {
333
+ Authorization: `Bearer ${platform.apiKey}`
334
+ },
335
+ body: {
336
+ responder_id: platform.responderId || task.responder_id,
337
+ hotline_id: task.hotline_id,
338
+ event_type: eventType,
339
+ ...detail
340
+ }
341
+ });
342
+
343
+ return { ok: response.status >= 200 && response.status < 300, response };
344
+ }
345
+
346
+ async function heartbeatPlatform(state, platform, status = "healthy") {
347
+ if (!platform?.baseUrl || !platform.apiKey || !state?.identity?.responder_id) {
348
+ return { ok: false, skipped: true };
349
+ }
350
+
351
+ const response = await postJson(platform.baseUrl, `/v1/responders/${state.identity.responder_id}/heartbeat`, {
352
+ headers: {
353
+ Authorization: `Bearer ${platform.apiKey}`
354
+ },
355
+ body: {
356
+ status
357
+ }
358
+ });
359
+
360
+ return { ok: response.status >= 200 && response.status < 300, response };
361
+ }
362
+
363
+ async function introspectTaskToken(task, platform) {
364
+ if (!platform?.baseUrl || !platform.apiKey || !task.task_token) {
365
+ return { active: true, skipped: true };
366
+ }
367
+
368
+ if (String(task.task_token).startsWith("local_task_")) {
369
+ return {
370
+ active: true,
371
+ skipped: true,
372
+ local_issued: true
373
+ };
374
+ }
375
+
376
+ const response = await postJson(platform.baseUrl, "/v1/tokens/introspect", {
377
+ headers: {
378
+ Authorization: `Bearer ${platform.apiKey}`
379
+ },
380
+ body: {
381
+ task_token: task.task_token
382
+ }
383
+ });
384
+
385
+ return response.body || { active: false, error: { code: "AUTH_INTROSPECT_FAILED", message: "token introspection request failed", retryable: true } };
386
+ }
387
+
388
+ function createTaskRecord(input, state, overrides = {}) {
389
+ const requestId = input.request_id || `req_${crypto.randomUUID()}`;
390
+ const acceptedAt = nowIso();
391
+ const payload = input.payload ?? input.task_input ?? null;
392
+
393
+ return {
394
+ task_id: input.task_id || `task_${crypto.randomUUID()}`,
395
+ request_id: requestId,
396
+ hotline_id: input.hotline_id || state.identity.hotline_ids[0],
397
+ task_type: input.task_type || null,
398
+ task_input: input.task_input ?? input.payload ?? null,
399
+ payload,
400
+ constraints: input.constraints || null,
401
+ simulate: input.simulate || "success",
402
+ priority: Number(input.priority || 5),
403
+ delay_ms: Number(input.delay_ms || 80),
404
+ lease_ttl_s: Number(input.lease_ttl_s || 30),
405
+ status: "QUEUED",
406
+ acked: true,
407
+ accepted_at: acceptedAt,
408
+ enqueued_at: acceptedAt,
409
+ updated_at: acceptedAt,
410
+ result_package: null,
411
+ result_delivery: overrides.result_delivery ?? input.result_delivery ?? null,
412
+ verification: overrides.verification ?? input.verification ?? null,
413
+ return_route: overrides.return_route ?? input.return_route ?? null,
414
+ reply_to: overrides.reply_to ?? input.reply_to ?? null,
415
+ thread_id: overrides.thread_id ?? input.thread_id ?? `req:${requestId}`,
416
+ task_token: input.task_token || null,
417
+ responder_id: input.responder_id || state.identity.responder_id,
418
+ raw_envelope: input.raw_envelope || null
419
+ };
420
+ }
421
+
422
+ function createExecutorContext(task) {
423
+ return {
424
+ requestId: task.request_id,
425
+ responderId: task.responder_id,
426
+ hotlineId: task.hotline_id,
427
+ taskType: task.task_type,
428
+ taskInput: task.task_input,
429
+ payload: task.payload,
430
+ constraints: task.constraints,
431
+ rawEnvelope: task.raw_envelope,
432
+ task
433
+ };
434
+ }
435
+
436
+ async function reportResponderMetric(platform, task, eventType, detail = {}) {
437
+ const metricKey = `${eventType}:${detail.code || ""}`;
438
+ task.metric_flags ||= {};
439
+ if (task.metric_flags[metricKey]) {
440
+ return;
441
+ }
442
+
443
+ task.metric_flags[metricKey] = true;
444
+ await postMetricEvent(platform, {
445
+ source: "responder-controller",
446
+ event_type: eventType,
447
+ request_id: task.request_id,
448
+ responder_id: task.responder_id,
449
+ hotline_id: task.hotline_id,
450
+ ...detail
451
+ });
452
+ }
453
+
454
+ function validateTaskGuardrails(task, { executor, guardrails = {} } = {}) {
455
+ const hardTimeoutS = Number(task.constraints?.hard_timeout_s);
456
+ const softTimeoutS = Number(task.constraints?.soft_timeout_s);
457
+ const hasHardTimeout = Number.isFinite(hardTimeoutS);
458
+ const hasSoftTimeout = Number.isFinite(softTimeoutS);
459
+ const maxHardTimeoutS = Number.isFinite(Number(guardrails.maxHardTimeoutS))
460
+ ? Number(guardrails.maxHardTimeoutS)
461
+ : null;
462
+ const allowedTaskTypes = Array.isArray(guardrails.allowedTaskTypes)
463
+ ? guardrails.allowedTaskTypes
464
+ : Array.isArray(executor?.allowedTaskTypes)
465
+ ? executor.allowedTaskTypes
466
+ : typeof executor?.getAllowedTaskTypes === "function"
467
+ ? executor.getAllowedTaskTypes(task.hotline_id)
468
+ : null;
469
+
470
+ if (hasSoftTimeout && softTimeoutS <= 0) {
471
+ return buildGuardrailError("CONTRACT_INVALID_TIMEOUT", "soft_timeout_s must be greater than 0");
472
+ }
473
+
474
+ if (hasHardTimeout && hardTimeoutS <= 0) {
475
+ return buildGuardrailError("CONTRACT_INVALID_TIMEOUT", "hard_timeout_s must be greater than 0");
476
+ }
477
+
478
+ if (hasSoftTimeout && hasHardTimeout && softTimeoutS > hardTimeoutS) {
479
+ return buildGuardrailError("CONTRACT_INVALID_TIMEOUT", "soft_timeout_s cannot exceed hard_timeout_s");
480
+ }
481
+
482
+ if (hasHardTimeout && maxHardTimeoutS && hardTimeoutS > maxHardTimeoutS) {
483
+ return buildGuardrailError(
484
+ "CONTRACT_TIMEOUT_EXCEEDS_RESPONDER_LIMIT",
485
+ `hard_timeout_s exceeds responder limit ${maxHardTimeoutS}`
486
+ );
487
+ }
488
+
489
+ if (task.task_type && Array.isArray(allowedTaskTypes) && allowedTaskTypes.length > 0) {
490
+ if (!allowedTaskTypes.includes(task.task_type)) {
491
+ return buildGuardrailError(
492
+ "CONTRACT_TASK_TYPE_UNSUPPORTED",
493
+ `task_type '${task.task_type}' is not allowed by responder guardrail`
494
+ );
495
+ }
496
+ }
497
+
498
+ return null;
499
+ }
500
+
501
+ async function finalizeTask(task, state, transport, platform, execution) {
502
+ const executionWithArtifacts = enforceArtifactSizeLimit(task, applyExecutionArtifacts(task, execution));
503
+ task.status = "COMPLETED";
504
+ task.completed_at = nowIso();
505
+ task.updated_at = task.completed_at;
506
+ task.execution_artifacts = Array.isArray(executionWithArtifacts.artifacts) ? executionWithArtifacts.artifacts : [];
507
+ task.result_package = signResultPayload(buildResultPayload(task, executionWithArtifacts), state);
508
+ await sendResultEnvelope(task, state, transport);
509
+ const lifecycleEvent =
510
+ task.result_package.status === "ok"
511
+ ? { eventType: "COMPLETED", detail: { status: "ok", finished_at: task.completed_at } }
512
+ : {
513
+ eventType: "FAILED",
514
+ detail: {
515
+ status: "error",
516
+ error_code: task.result_package.error?.code || "EXEC_UNKNOWN",
517
+ finished_at: task.completed_at
518
+ }
519
+ };
520
+ try {
521
+ await postRequestLifecycleEvent(task, platform, lifecycleEvent.eventType, lifecycleEvent.detail);
522
+ } catch {
523
+ // Completion events are observational only and must not invalidate result delivery.
524
+ }
525
+ await reportResponderMetric(
526
+ platform,
527
+ task,
528
+ task.result_package.status === "ok" ? "responder.task.succeeded" : "responder.task.failed",
529
+ task.result_package.status === "error" ? { code: task.result_package.error?.code || "EXEC_UNKNOWN" } : {}
530
+ );
531
+ }
532
+
533
+ async function failTask(task, state, transport, platform, error) {
534
+ await finalizeTask(task, state, transport, platform, {
535
+ status: "error",
536
+ error: {
537
+ code: "EXECUTOR_RUNTIME_ERROR",
538
+ message: error instanceof Error ? error.message : "unknown_error",
539
+ retryable: false
540
+ },
541
+ schema_valid: true,
542
+ usage: { tokens_in: 0, tokens_out: 0 }
543
+ });
544
+ }
545
+
546
+ export function createResponderState(options = {}) {
547
+ const workerConcurrency = Math.max(1, Number(options.workerConcurrency || process.env.RESPONDER_WORKER_CONCURRENCY || 1));
548
+ const signing = options.signing
549
+ ? {
550
+ privateKey: crypto.createPrivateKey(options.signing.privateKeyPem),
551
+ publicKeyPem: options.signing.publicKeyPem
552
+ }
553
+ : (() => {
554
+ const generated = crypto.generateKeyPairSync("ed25519");
555
+ return {
556
+ privateKey: generated.privateKey,
557
+ publicKeyPem: generated.publicKey.export({ type: "spki", format: "pem" }).toString()
558
+ };
559
+ })();
560
+
561
+ return {
562
+ tasks: new Map(),
563
+ requestIndex: new Map(),
564
+ queue: [],
565
+ activeTaskIds: [],
566
+ workerConcurrency,
567
+ signing,
568
+ identity: {
569
+ responder_id: options.responderId || "responder_starlight",
570
+ hotline_ids: options.hotlineIds || ["starlight.creative.studio.v1"]
571
+ },
572
+ hotlines: Array.isArray(options.hotlines) ? options.hotlines : [],
573
+ heartbeat: {
574
+ status: "healthy",
575
+ last_sent_at: null
576
+ }
577
+ };
578
+ }
579
+
580
+ export function serializeResponderState(state) {
581
+ return {
582
+ tasks: Array.from(state.tasks.entries()),
583
+ requestIndex: Array.from(state.requestIndex.entries()),
584
+ queue: [...state.queue],
585
+ activeTaskIds: [...(state.activeTaskIds || [])],
586
+ workerConcurrency: state.workerConcurrency,
587
+ identity: state.identity,
588
+ hotlines: state.hotlines,
589
+ heartbeat: state.heartbeat
590
+ };
591
+ }
592
+
593
+ export function hydrateResponderState(state, snapshot) {
594
+ if (!snapshot) {
595
+ return state;
596
+ }
597
+
598
+ state.tasks.clear();
599
+ for (const [taskId, task] of snapshot.tasks || []) {
600
+ state.tasks.set(taskId, task);
601
+ }
602
+
603
+ state.requestIndex.clear();
604
+ for (const [requestId, taskId] of snapshot.requestIndex || []) {
605
+ state.requestIndex.set(requestId, taskId);
606
+ }
607
+
608
+ state.queue = Array.isArray(snapshot.queue) ? [...snapshot.queue] : [];
609
+ state.activeTaskIds = [];
610
+ state.workerConcurrency = Math.max(1, Number(snapshot.workerConcurrency || state.workerConcurrency || 1));
611
+ state.identity = snapshot.identity || state.identity;
612
+ state.hotlines = Array.isArray(snapshot.hotlines) ? snapshot.hotlines : state.hotlines;
613
+ state.heartbeat = snapshot.heartbeat || state.heartbeat;
614
+ return state;
615
+ }
616
+
617
+ function getTaskByRequestId(state, requestId) {
618
+ const taskId = state.requestIndex.get(requestId);
619
+ return taskId ? state.tasks.get(taskId) || null : null;
620
+ }
621
+
622
+ function rememberTask(state, task) {
623
+ state.tasks.set(task.task_id, task);
624
+ state.requestIndex.set(task.request_id, task.task_id);
625
+ }
626
+
627
+ function workerConcurrencyForState(state, override = null) {
628
+ return Math.max(1, Number(override || state.workerConcurrency || 1));
629
+ }
630
+
631
+ async function runQueuedTask(task, state, { executor, transport = null, platform = null, onStateChanged = null } = {}) {
632
+ await persistResponderState(onStateChanged, state);
633
+ await new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(task.delay_ms || 0))));
634
+
635
+ try {
636
+ const execution = await executor.execute(createExecutorContext(task));
637
+ if (execution?.deferred === true) {
638
+ task.status = "RUNNING";
639
+ task.updated_at = nowIso();
640
+ task.deferred_reason = execution.reason || "deferred";
641
+ await persistResponderState(onStateChanged, state);
642
+ } else {
643
+ await finalizeTask(task, state, transport, platform, execution);
644
+ await persistResponderState(onStateChanged, state);
645
+ }
646
+ } catch (error) {
647
+ await failTask(task, state, transport, platform, error);
648
+ await persistResponderState(onStateChanged, state);
649
+ } finally {
650
+ state.activeTaskIds = (state.activeTaskIds || []).filter((taskId) => taskId !== task.task_id);
651
+ await persistResponderState(onStateChanged, state);
652
+ scheduleProcessQueue(state, { executor, transport, platform, onStateChanged });
653
+ }
654
+ }
655
+
656
+ function scheduleProcessQueue(state, { executor, transport = null, platform = null, onStateChanged = null, workerConcurrency = null } = {}) {
657
+ const maxWorkers = workerConcurrencyForState(state, workerConcurrency);
658
+ while ((state.activeTaskIds || []).length < maxWorkers) {
659
+ const nextTaskId = state.queue.shift();
660
+ if (!nextTaskId) {
661
+ return;
662
+ }
663
+
664
+ const task = state.tasks.get(nextTaskId);
665
+ if (!task) {
666
+ continue;
667
+ }
668
+
669
+ task.status = "RUNNING";
670
+ task.started_at = nowIso();
671
+ task.updated_at = task.started_at;
672
+ task.lease_expires_at = new Date(Date.now() + task.lease_ttl_s * 1000).toISOString();
673
+ state.activeTaskIds = [...(state.activeTaskIds || []), task.task_id];
674
+
675
+ void runQueuedTask(task, state, {
676
+ executor,
677
+ transport,
678
+ platform,
679
+ onStateChanged
680
+ });
681
+ }
682
+ }
683
+
684
+ async function enqueueTask(
685
+ state,
686
+ task,
687
+ { executor, transport = null, platform = null, onStateChanged = null, workerConcurrency = null } = {}
688
+ ) {
689
+ rememberTask(state, task);
690
+ state.queue.push(task.task_id);
691
+
692
+ state.queue.sort((leftId, rightId) => {
693
+ const left = state.tasks.get(leftId);
694
+ const right = state.tasks.get(rightId);
695
+ if (!left || !right) {
696
+ return 0;
697
+ }
698
+ if (left.priority !== right.priority) {
699
+ return left.priority - right.priority;
700
+ }
701
+ return left.enqueued_at.localeCompare(right.enqueued_at);
702
+ });
703
+
704
+ await persistResponderState(onStateChanged, state);
705
+ scheduleProcessQueue(state, { executor, transport, platform, onStateChanged, workerConcurrency });
706
+ }
707
+
708
+ async function processResponderInbox(state, {
709
+ executor,
710
+ transport = null,
711
+ platform = null,
712
+ guardrails = {},
713
+ onStateChanged = null,
714
+ receiver = null,
715
+ limit = 10
716
+ } = {}) {
717
+ if (!transport) {
718
+ return { accepted: [] };
719
+ }
720
+
721
+ const polled = await transport.poll({
722
+ limit,
723
+ receiver: receiver || state.identity.responder_id
724
+ });
725
+ const accepted = [];
726
+
727
+ for (const envelope of polled.items) {
728
+ if (envelope.responder_id && envelope.responder_id !== state.identity.responder_id) {
729
+ continue;
730
+ }
731
+ if (envelope.hotline_id && !state.identity.hotline_ids.includes(envelope.hotline_id)) {
732
+ continue;
733
+ }
734
+
735
+ await postMetricEvent(platform, {
736
+ source: "responder-controller",
737
+ event_type: "responder.task.received",
738
+ request_id: envelope.request_id || null,
739
+ responder_id: state.identity.responder_id,
740
+ hotline_id: envelope.hotline_id || null
741
+ });
742
+
743
+ const existing = getTaskByRequestId(state, envelope.request_id);
744
+ if (existing) {
745
+ if (existing.result_package) {
746
+ await sendResultEnvelope(
747
+ {
748
+ ...existing,
749
+ return_route: envelope.from || existing.return_route,
750
+ reply_to: envelope.from || existing.reply_to,
751
+ thread_id: envelope.thread_id || existing.thread_id
752
+ },
753
+ state,
754
+ transport
755
+ );
756
+ }
757
+
758
+ await transport.ack(envelope.message_id);
759
+ accepted.push({
760
+ message_id: envelope.message_id,
761
+ task_id: existing.task_id,
762
+ deduped: true,
763
+ replayed: Boolean(existing.result_package)
764
+ });
765
+ continue;
766
+ }
767
+
768
+ const task = createTaskRecord(
769
+ {
770
+ ...envelope,
771
+ raw_envelope: envelope
772
+ },
773
+ state,
774
+ {
775
+ return_route: envelope.from || null,
776
+ reply_to: envelope.from || "caller-controller",
777
+ thread_id: envelope.thread_id || `req:${envelope.request_id}`,
778
+ result_delivery: envelope.result_delivery || null,
779
+ verification: envelope.verification || null
780
+ }
781
+ );
782
+
783
+ const introspection = await introspectTaskToken(task, platform);
784
+ if (introspection.active === false) {
785
+ task.status = "COMPLETED";
786
+ task.completed_at = nowIso();
787
+ task.updated_at = task.completed_at;
788
+ task.result_package = signResultPayload(
789
+ buildErrorResultPayload(task, {
790
+ code: introspection.error?.code || introspection.error || "AUTH_TOKEN_INVALID",
791
+ message: introspection.error?.message || "Task token rejected during responder validation"
792
+ }),
793
+ state
794
+ );
795
+ rememberTask(state, task);
796
+ await sendResultEnvelope(task, state, transport);
797
+ await reportResponderMetric(platform, task, "responder.task.rejected", {
798
+ code: introspection.error?.code || introspection.error || "AUTH_TOKEN_INVALID"
799
+ });
800
+ await persistResponderState(onStateChanged, state);
801
+ } else {
802
+ const guardrailError = validateTaskGuardrails(task, { executor, guardrails });
803
+ if (guardrailError) {
804
+ task.status = "COMPLETED";
805
+ task.completed_at = nowIso();
806
+ task.updated_at = task.completed_at;
807
+ task.result_package = signResultPayload(buildResultPayload(task, guardrailError), state);
808
+ rememberTask(state, task);
809
+ await sendResultEnvelope(task, state, transport);
810
+ await reportResponderMetric(platform, task, "responder.task.rejected", {
811
+ code: guardrailError.error.code
812
+ });
813
+ await persistResponderState(onStateChanged, state);
814
+ } else {
815
+ await enqueueTask(state, task, { executor, transport, platform, onStateChanged });
816
+ await reportResponderMetric(platform, task, "responder.task.accepted");
817
+ const acked = await ackPlatform(task, platform);
818
+ task.acked = acked.ok;
819
+ await persistResponderState(onStateChanged, state);
820
+ }
821
+ }
822
+
823
+ await transport.ack(envelope.message_id);
824
+ accepted.push({ message_id: envelope.message_id, task_id: task.task_id });
825
+ }
826
+
827
+ return { accepted };
828
+ }
829
+
830
+ export function startResponderHeartbeatLoop({
831
+ state,
832
+ platform = null,
833
+ intervalMs = 30000,
834
+ logger = console,
835
+ onStateChanged = null
836
+ } = {}) {
837
+ if (!platform?.baseUrl || !platform.apiKey || !state?.identity?.responder_id) {
838
+ return () => {};
839
+ }
840
+
841
+ let stopped = false;
842
+
843
+ async function sendHeartbeat() {
844
+ if (stopped) {
845
+ return;
846
+ }
847
+
848
+ try {
849
+ const result = await heartbeatPlatform(state, platform, state.heartbeat?.status || "healthy");
850
+ if (result.ok) {
851
+ state.heartbeat.last_sent_at = nowIso();
852
+ await persistResponderState(onStateChanged, state);
853
+ }
854
+ } catch (error) {
855
+ logger?.warn?.(
856
+ `[responder-heartbeat] failed for ${state.identity.responder_id}: ${
857
+ error instanceof Error ? error.message : "unknown_error"
858
+ }`
859
+ );
860
+ }
861
+ }
862
+
863
+ void sendHeartbeat();
864
+ const timer = setInterval(() => {
865
+ void sendHeartbeat();
866
+ }, intervalMs);
867
+
868
+ return () => {
869
+ stopped = true;
870
+ clearInterval(timer);
871
+ };
872
+ }
873
+
874
+ export function startResponderInboxLoop({
875
+ state,
876
+ executor = createSimulatorExecutor(),
877
+ transport = null,
878
+ platform = null,
879
+ guardrails = {},
880
+ onStateChanged = null,
881
+ intervalMs = 250,
882
+ receiver = null,
883
+ logger = console
884
+ } = {}) {
885
+ if (!transport) {
886
+ return () => {};
887
+ }
888
+
889
+ let stopped = false;
890
+ let running = false;
891
+
892
+ async function pullInbox() {
893
+ if (stopped || running) {
894
+ return;
895
+ }
896
+ running = true;
897
+ try {
898
+ await processResponderInbox(state, {
899
+ executor,
900
+ transport,
901
+ platform,
902
+ guardrails,
903
+ onStateChanged,
904
+ receiver
905
+ });
906
+ } catch (error) {
907
+ logger?.warn?.(`[responder-inbox] pull failed: ${error instanceof Error ? error.message : "unknown_error"}`);
908
+ } finally {
909
+ running = false;
910
+ }
911
+ }
912
+
913
+ void pullInbox();
914
+ const timer = setInterval(() => {
915
+ void pullInbox();
916
+ }, intervalMs);
917
+
918
+ return () => {
919
+ stopped = true;
920
+ clearInterval(timer);
921
+ };
922
+ }
923
+
924
+ export function createResponderControllerServer({
925
+ state = createResponderState(),
926
+ serviceName = "responder-controller",
927
+ transport = null,
928
+ platform = null,
929
+ executor = createSimulatorExecutor(),
930
+ guardrails = {},
931
+ background = {},
932
+ onStateChanged = null,
933
+ onPlatformConfigured = null
934
+ } = {}) {
935
+ const workerConcurrency = workerConcurrencyForState(state, background.workerConcurrency);
936
+ state.workerConcurrency = workerConcurrency;
937
+ const server = http.createServer(async (req, res) => {
938
+ const method = req.method || "GET";
939
+ const url = new URL(req.url || "/", "http://localhost");
940
+ const pathname = url.pathname;
941
+
942
+ try {
943
+ if (method === "OPTIONS") {
944
+ res.writeHead(204, {
945
+ "access-control-allow-origin": "*",
946
+ "access-control-allow-methods": "GET,POST,PUT,PATCH,DELETE,OPTIONS",
947
+ "access-control-allow-headers": "Content-Type, Authorization, X-Platform-Api-Key"
948
+ });
949
+ res.end();
950
+ return;
951
+ }
952
+
953
+ if (method === "GET" && pathname === "/healthz") {
954
+ sendJson(res, 200, { ok: true, service: serviceName });
955
+ return;
956
+ }
957
+
958
+ if (method === "GET" && pathname === "/readyz") {
959
+ sendJson(res, 200, { ready: true, service: serviceName });
960
+ return;
961
+ }
962
+
963
+ if (method === "GET" && pathname === "/") {
964
+ sendJson(res, 200, {
965
+ service: serviceName,
966
+ status: "running",
967
+ executor: executor.name || "unknown",
968
+ responder_id: state.identity.responder_id,
969
+ hotline_ids: state.identity.hotline_ids,
970
+ worker_concurrency: workerConcurrency,
971
+ configured_hotlines:
972
+ typeof executor?.listHotlines === "function"
973
+ ? executor.listHotlines()
974
+ : Array.isArray(state.hotlines)
975
+ ? state.hotlines
976
+ : [],
977
+ guardrails: {
978
+ max_hard_timeout_s: Number.isFinite(Number(guardrails.maxHardTimeoutS))
979
+ ? Number(guardrails.maxHardTimeoutS)
980
+ : null,
981
+ allowed_task_types: Array.isArray(guardrails.allowedTaskTypes) ? guardrails.allowedTaskTypes : null
982
+ }
983
+ });
984
+ return;
985
+ }
986
+
987
+ if (method === "GET" && pathname === "/controller/public-key") {
988
+ sendJson(res, 200, {
989
+ responder_id: state.identity.responder_id,
990
+ public_key_pem: state.signing.publicKeyPem
991
+ });
992
+ return;
993
+ }
994
+
995
+ if (method === "POST" && pathname === "/controller/register") {
996
+ try {
997
+ const body = await parseJsonBody(req);
998
+ const responderId = body.responder_id || state.identity.responder_id;
999
+ const hotlineId = body.hotline_id || state.identity.hotline_ids[0];
1000
+ const headerApiKey = req.headers["x-platform-api-key"];
1001
+ const registerPlatform = {
1002
+ ...platform,
1003
+ apiKey:
1004
+ (typeof headerApiKey === "string" && headerApiKey.trim()) ||
1005
+ body.platform_api_key ||
1006
+ platform?.apiKey ||
1007
+ null
1008
+ };
1009
+ const registered = await registerResponderOnPlatform(registerPlatform, {
1010
+ responder_id: responderId,
1011
+ hotline_id: hotlineId,
1012
+ display_name: body.display_name || `${responderId} ${hotlineId}`,
1013
+ template_ref: body.template_ref || `${hotlineId}@v1`,
1014
+ task_delivery_address: body.task_delivery_address || `local://relay/${responderId}/${hotlineId}`,
1015
+ responder_public_key_pem: state.signing.publicKeyPem,
1016
+ task_types: body.task_types || [],
1017
+ capabilities: body.capabilities || [],
1018
+ tags: body.tags || [],
1019
+ input_schema: body.input_schema || null,
1020
+ output_schema: body.output_schema || null,
1021
+ contact_email: body.contact_email || null,
1022
+ support_email: body.support_email || null
1023
+ });
1024
+
1025
+ state.identity.responder_id = registered.responder_id;
1026
+ state.identity.hotline_ids = Array.from(new Set([...(state.identity.hotline_ids || []), registered.hotline_id]));
1027
+ if (platform) {
1028
+ platform.apiKey = registered.api_key || platform.apiKey;
1029
+ platform.responderId = registered.responder_id;
1030
+ }
1031
+ await persistResponderState(onStateChanged, state);
1032
+ if (typeof onPlatformConfigured === "function") {
1033
+ await onPlatformConfigured({
1034
+ platform,
1035
+ state,
1036
+ registered
1037
+ });
1038
+ }
1039
+ sendJson(res, 201, registered);
1040
+ } catch (error) {
1041
+ if (error instanceof Error && error.message === "responder_platform_base_url_required") {
1042
+ sendError(res, 409, "PLATFORM_NOT_CONFIGURED", "platform base URL is not configured");
1043
+ return;
1044
+ }
1045
+ if (error?.response) {
1046
+ sendJson(res, error.response.status, error.response.body || { error: { code: "RESPONDER_PLATFORM_REGISTER_FAILED", message: "registration rejected by platform", retryable: false } });
1047
+ return;
1048
+ }
1049
+ sendError(res, 502, "RESPONDER_PLATFORM_REGISTER_FAILED", error instanceof Error ? error.message : "unknown_error", { retryable: true });
1050
+ }
1051
+ return;
1052
+ }
1053
+
1054
+ if (method === "POST" && pathname === "/controller/tasks") {
1055
+ const body = await parseJsonBody(req);
1056
+ const task = createTaskRecord(body, state);
1057
+
1058
+ const existing = getTaskByRequestId(state, task.request_id);
1059
+ if (existing) {
1060
+ sendJson(res, existing.result_package ? 200 : 202, {
1061
+ accepted: !existing.result_package,
1062
+ deduped: true,
1063
+ replayed: Boolean(existing.result_package),
1064
+ task_id: existing.task_id,
1065
+ request_id: existing.request_id,
1066
+ status: existing.status,
1067
+ result_package: existing.result_package || null
1068
+ });
1069
+ return;
1070
+ }
1071
+
1072
+ await enqueueTask(state, task, { executor, transport, platform, onStateChanged, workerConcurrency });
1073
+
1074
+ sendJson(res, 202, {
1075
+ accepted: true,
1076
+ task_id: task.task_id,
1077
+ request_id: task.request_id,
1078
+ status: task.status,
1079
+ queue_policy: {
1080
+ mode: "priority_fifo",
1081
+ lease_ttl_s: task.lease_ttl_s,
1082
+ worker_concurrency: workerConcurrency
1083
+ }
1084
+ });
1085
+ return;
1086
+ }
1087
+
1088
+ if (method === "POST" && pathname === "/controller/inbox/pull") {
1089
+ if (!transport) {
1090
+ sendError(res, 409, "TRANSPORT_NOT_CONFIGURED", "message transport is not configured");
1091
+ return;
1092
+ }
1093
+
1094
+ const body = await parseJsonBody(req);
1095
+ const result = await processResponderInbox(state, {
1096
+ executor,
1097
+ transport,
1098
+ platform,
1099
+ guardrails,
1100
+ onStateChanged,
1101
+ receiver: body.receiver || state.identity.responder_id,
1102
+ limit: Number(body.limit || 10)
1103
+ });
1104
+
1105
+ sendJson(res, 200, { accepted: result.accepted });
1106
+ return;
1107
+ }
1108
+
1109
+ if (method === "GET" && pathname === "/controller/queue") {
1110
+ const queued = state.queue.map((taskId) => state.tasks.get(taskId)).filter(Boolean);
1111
+ const runningIds = new Set(state.activeTaskIds || []);
1112
+ const running = Array.from(state.tasks.values()).filter(
1113
+ (task) => task.status === "RUNNING" || runningIds.has(task.task_id)
1114
+ );
1115
+ sendJson(res, 200, { queued, running });
1116
+ return;
1117
+ }
1118
+
1119
+ const taskMatch = pathname.match(/^\/controller\/tasks\/([^/]+)$/);
1120
+ if (method === "GET" && taskMatch) {
1121
+ const task = state.tasks.get(taskMatch[1]);
1122
+ if (!task) {
1123
+ sendError(res, 404, "TASK_NOT_FOUND", "task does not exist");
1124
+ return;
1125
+ }
1126
+
1127
+ sendJson(res, 200, task);
1128
+ return;
1129
+ }
1130
+
1131
+ const resultMatch = pathname.match(/^\/controller\/tasks\/([^/]+)\/result$/);
1132
+ if (method === "GET" && resultMatch) {
1133
+ const task = state.tasks.get(resultMatch[1]);
1134
+ if (!task) {
1135
+ sendError(res, 404, "TASK_NOT_FOUND", "task does not exist");
1136
+ return;
1137
+ }
1138
+
1139
+ if (!task.result_package) {
1140
+ sendJson(res, 202, { available: false, status: task.status });
1141
+ return;
1142
+ }
1143
+
1144
+ sendJson(res, 200, { available: true, status: task.status, result_package: task.result_package });
1145
+ return;
1146
+ }
1147
+
1148
+ const replayMatch = pathname.match(/^\/controller\/tasks\/([^/]+)\/replay$/);
1149
+ if (method === "POST" && replayMatch) {
1150
+ const task = state.tasks.get(replayMatch[1]);
1151
+ if (!task) {
1152
+ sendError(res, 404, "TASK_NOT_FOUND", "task does not exist");
1153
+ return;
1154
+ }
1155
+
1156
+ if (!task.result_package) {
1157
+ sendError(res, 409, "RESULT_NOT_READY", "task result is not yet available", { status: task.status });
1158
+ return;
1159
+ }
1160
+
1161
+ sendJson(res, 200, { replayed: true, result_package: task.result_package });
1162
+ return;
1163
+ }
1164
+
1165
+ sendError(res, 404, "not_found", "no matching route", { path: pathname });
1166
+ } catch (error) {
1167
+ if (error.message === "invalid_json") {
1168
+ sendError(res, 400, "CONTRACT_INVALID_JSON", "request body is not valid JSON");
1169
+ return;
1170
+ }
1171
+
1172
+ sendError(res, 500, "RESPONDER_RUNTIME_INTERNAL_ERROR", error instanceof Error ? error.message : "unknown_error", { retryable: true });
1173
+ }
1174
+ });
1175
+
1176
+ if (background.enabled === true) {
1177
+ const stopInboxLoop = startResponderInboxLoop({
1178
+ state,
1179
+ executor,
1180
+ transport,
1181
+ platform,
1182
+ guardrails,
1183
+ onStateChanged,
1184
+ intervalMs: Number(background.inboxPollIntervalMs || 250),
1185
+ receiver: background.receiver || state.identity.responder_id
1186
+ });
1187
+ server.on("close", () => {
1188
+ stopInboxLoop();
1189
+ });
1190
+ }
1191
+
1192
+ return server;
1193
+ }
1194
+
1195
+ export {
1196
+ createConfiguredHotlineExecutor,
1197
+ createExampleFunctionExecutor,
1198
+ createFunctionExecutor,
1199
+ createSimulatorExecutor,
1200
+ createHotlineRouterExecutor,
1201
+ deferTask
1202
+ };