@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
@@ -1,12 +1,12 @@
1
+ import { outgoingTraceparent } from '../../trace.js';
1
2
  import { finalizeAgentRun, lastUserMessageText } from './agent-finalize.js';
2
3
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
3
4
  import { applyInputMiddleware, describeApprovals } from './agent-turn.js';
4
- import { AIProviderNotConfiguredError } from '../../errors/errors.js';
5
5
  import { deniedToolResult, randomUUID } from './agent-utils.js';
6
6
  import { SPOKEN_TRANSCRIPT } from './voice-input.js';
7
7
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../../channel-middleware-runner.js';
8
8
  import { resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './agent-memory.js';
9
- import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, assertResourcePrincipalOwner, assertAgentAuthorized, ToolApprovalRequired, ToolCredentialRequired, APPROVAL_REQUIRED, CREDENTIAL_REQUIRED, } from './agent-prepare.js';
9
+ import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, assertResourcePrincipalOwner, assertAgentAuthorized, resolveAgentRunner, ToolApprovalRequired, ToolCredentialRequired, APPROVAL_REQUIRED, CREDENTIAL_REQUIRED, } from './agent-prepare.js';
10
10
  import { resolveModelConfig } from './agent-model-config.js';
11
11
  import { getInFlightTools, isAbortError, persistOrphanedToolResults, registerInterruptibleRun, signalRunInterrupt, trackInterruptNote, trackToolExecution, } from './agent-interrupt.js';
12
12
  function createPersistingChannel(parent, storage, threadId, logger) {
@@ -186,6 +186,7 @@ const warnUnstreamedOutputHooks = (agentName, agentMiddlewares, logger) => {
186
186
  };
187
187
  async function postStreamCleanup(persistingChannel, agentRunState, runId, run) {
188
188
  await finalizeAgentRun(agentRunState, {
189
+ traceparent: run.traceparent,
189
190
  runId,
190
191
  agentName: run.agentName,
191
192
  threadId: run.threadId,
@@ -620,6 +621,7 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
620
621
  return persistingChannel.fullText;
621
622
  }
622
623
  await postStreamCleanup(persistingChannel, agentRunState, runId, {
624
+ traceparent: outgoingTraceparent({ trace: params.trace }),
623
625
  agentName,
624
626
  threadId,
625
627
  resourceId: input.resourceId,
@@ -752,6 +754,7 @@ export async function resumeAgent(input, channel, params, options) {
752
754
  }
753
755
  const { agent, packageName, resolvedName } = resolveAgent(run.agentName);
754
756
  await assertAgentAuthorized(agent, params, packageName);
757
+ const agentRunner = await resolveAgentRunner(singletonServices, params);
755
758
  // The read above is not a claim — concurrent resumes all see the same pending
756
759
  // approval. `resolveApproval` is the claim, and the loser must not go on to
757
760
  // run the tool a second time.
@@ -761,10 +764,6 @@ export async function resumeAgent(input, channel, params, options) {
761
764
  }
762
765
  const { storage } = resolveMemoryServices(agent, singletonServices);
763
766
  const memoryConfig = agent.memory;
764
- const agentRunner = singletonServices.agentRunner;
765
- if (!agentRunner) {
766
- throw new AIProviderNotConfiguredError();
767
- }
768
767
  if (!input.approved) {
769
768
  if (pending.type === 'agent-call') {
770
769
  await agentRunState.updateRun(pending.agentRunId, { status: 'failed' });
@@ -1023,6 +1022,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
1023
1022
  return;
1024
1023
  }
1025
1024
  await postStreamCleanup(persistingChannel, agentRunState, run.runId, {
1025
+ traceparent: outgoingTraceparent({ trace: params.trace }),
1026
1026
  ...run,
1027
1027
  input: lastUserMessageText(runnerParams.messages),
1028
1028
  });
@@ -75,7 +75,7 @@ export declare const voiceInput: (config?: {
75
75
  secrets?: {
76
76
  requireAllowedHosts?: boolean;
77
77
  };
78
- workflow?: import("../workflow/workflow.types.js").WorkflowServiceConfig;
78
+ workflow?: Partial<import("../workflow/workflow.types.js").WorkflowServiceConfig>;
79
79
  webhook?: import("../../services/webhook-service.js").WebhookServiceConfig;
80
80
  postgres?: import("../../types/core.types.js").PostgresConfig;
81
81
  }>>;
@@ -93,7 +93,7 @@ export declare const voiceOutput: (config?: {
93
93
  secrets?: {
94
94
  requireAllowedHosts?: boolean;
95
95
  };
96
- workflow?: import("../workflow/workflow.types.js").WorkflowServiceConfig;
96
+ workflow?: Partial<import("../workflow/workflow.types.js").WorkflowServiceConfig>;
97
97
  webhook?: import("../../services/webhook-service.js").WebhookServiceConfig;
98
98
  postgres?: import("../../types/core.types.js").PostgresConfig;
99
99
  }>>;
@@ -10,6 +10,8 @@ import { type ScorerInput } from './agent-scorer.types.js';
10
10
  */
11
11
  export declare const scoreFinishedRun: (run: ScorerInput, services: Pick<CoreSingletonServices, 'logger'> & {
12
12
  queueService?: {
13
- add: (queueName: string, data: unknown) => Promise<any>;
13
+ add: (queueName: string, data: unknown, options?: {
14
+ traceparent?: string;
15
+ }) => Promise<any>;
14
16
  };
15
- }) => Promise<void>;
17
+ }, traceparent?: string) => Promise<void>;
@@ -9,7 +9,7 @@ import { SCORER_LANE_QUEUES } from './agent-scorer.types.js';
9
9
  * also strictly best-effort: the client already has its answer, so nothing here
10
10
  * may fail the run.
11
11
  */
12
- export const scoreFinishedRun = async (run, services) => {
12
+ export const scoreFinishedRun = async (run, services, traceparent) => {
13
13
  const scorers = scorersForAgent(run.agentName, services.logger).filter(
14
14
  // A reference-based judge grades against an answer key, and live traffic
15
15
  // has none.
@@ -29,7 +29,7 @@ export const scoreFinishedRun = async (run, services) => {
29
29
  scorerName: scorer.name,
30
30
  // No reference: a live run has no answer key.
31
31
  reference: undefined,
32
- });
32
+ }, traceparent ? { traceparent } : undefined);
33
33
  }
34
34
  catch (error) {
35
35
  services.logger?.error(`[pikku] Failed to enqueue the '${scorer.name}' grade of run ${run.runId}`, { error });
@@ -2,7 +2,8 @@ import type { CoreSingletonServices } from '../../types/core.types.js';
2
2
  import type { MiddlewareMetadata } from '../../middleware/middleware.types.js';
3
3
  import type { CoreChannel, ChannelMessageMeta } from './channel.types.js';
4
4
  import { type PikkuSessionService } from '../../services/user-session-service.js';
5
- export declare const runChannelLifecycleWithMiddleware: ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, wireServices, }: {
5
+ import { type TraceContext } from '../../trace.js';
6
+ export declare const runChannelLifecycleWithMiddleware: ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, wireServices, trace, }: {
6
7
  channelConfig: CoreChannel<unknown, any>;
7
8
  meta: ChannelMessageMeta;
8
9
  lifecycleConfig: any;
@@ -11,6 +12,8 @@ export declare const runChannelLifecycleWithMiddleware: ({ channelConfig, meta,
11
12
  channel: any;
12
13
  data?: unknown;
13
14
  channelMiddlewareMeta?: MiddlewareMetadata[];
15
+ /** The connection's trace; the lifecycle hook is a span in it. */
16
+ trace?: TraceContext;
14
17
  /**
15
18
  * Established while the socket was upgrading, so `onConnect` can tell who
16
19
  * just connected.
@@ -2,7 +2,8 @@ import { combineMiddleware, runMiddleware } from '../../middleware-runner.js';
2
2
  import { runPikkuFunc } from '../../function/function-runner.js';
3
3
  import { createMiddlewareSessionWireProps, } from '../../services/user-session-service.js';
4
4
  import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../../channel-middleware-runner.js';
5
- export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, wireServices, }) => {
5
+ import { childTraceWire } from '../../trace.js';
6
+ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, lifecycleConfig, lifecycleType, services, channel, data, channelMiddlewareMeta, userSession, wireServices, trace, }) => {
6
7
  const lifecycleMiddleware = typeof lifecycleConfig === 'object' && 'middleware' in lifecycleConfig
7
8
  ? lifecycleConfig.middleware || []
8
9
  : [];
@@ -17,6 +18,7 @@ export const runChannelLifecycleWithMiddleware = async ({ channelConfig, meta, l
17
18
  });
18
19
  let wire = {
19
20
  channel,
21
+ ...childTraceWire(trace),
20
22
  ...(userSession ? createMiddlewareSessionWireProps(userSession) : {}),
21
23
  };
22
24
  if (allChannelMiddleware.length > 0) {
@@ -3,6 +3,7 @@ import { runPikkuFunc } from '../../function/function-runner.js';
3
3
  import { isChannelRPCPending, isChannelRPCResponse } from './channel-rpc.js';
4
4
  import { handleChannelRPCResponse } from './channel-host-rpc.js';
5
5
  import { createMiddlewareSessionWireProps } from '../../services/user-session-service.js';
6
+ import { childTraceWire } from '../../trace.js';
6
7
  const getChannelMeta = (channelName) => {
7
8
  return pikkuState(null, 'channel', 'meta')[channelName];
8
9
  };
@@ -88,6 +89,7 @@ export const processMessageHandlers = (services, channelConfig, channelHandler,
88
89
  sessionService: userSession,
89
90
  wire: {
90
91
  channel: channelHandler.getChannel(),
92
+ ...childTraceWire(channelHandler.trace),
91
93
  ...(userSession && createMiddlewareSessionWireProps(userSession)),
92
94
  },
93
95
  packageName,
@@ -15,7 +15,7 @@ export declare const getChannelHostRPC: (channel: PikkuChannel<unknown, any>, op
15
15
  * Kept out of the channel handler so a channel nobody calls back on pays for no
16
16
  * registry, and because a handler has no services to build a validator with.
17
17
  */
18
- export declare const channelRemote: (channel: PikkuChannel<unknown, any, any>, funcName: string, data?: unknown) => Promise<unknown>;
18
+ export declare const channelRemote: (channel: PikkuChannel<unknown, any, any>, funcName: string, data?: unknown, traceparent?: string) => Promise<unknown>;
19
19
  /** False for an unknown connection or id, so a stray frame is dropped. */
20
20
  export declare const handleChannelRPCResponse: (channelId: string, message: unknown) => boolean;
21
21
  /**
@@ -19,10 +19,10 @@ export const getChannelHostRPC = (channel, options = {}) => {
19
19
  * Kept out of the channel handler so a channel nobody calls back on pays for no
20
20
  * registry, and because a handler has no services to build a validator with.
21
21
  */
22
- export const channelRemote = async (channel, funcName, data) => getChannelHostRPC(channel, {
22
+ export const channelRemote = async (channel, funcName, data, traceparent) => getChannelHostRPC(channel, {
23
23
  validateInput: createChannelRPCInputValidator(getSingletonServices()),
24
24
  validateResult: createChannelRPCResultValidator(getSingletonServices()),
25
- }).invoke(funcName, data);
25
+ }).invoke(funcName, data, undefined, traceparent);
26
26
  /** False for an unknown connection or id, so a stray frame is dropped. */
27
27
  export const handleChannelRPCResponse = (channelId, message) => hostRPCByChannel.get(channelId)?.handleResponse(message) ?? false;
28
28
  /**
@@ -7,9 +7,15 @@ import { type ApprovalRequester, type Capabilities } from './channel-rpc.types.j
7
7
  * name is refused exactly like an unknown one, so the caller cannot probe for
8
8
  * what this end can do.
9
9
  */
10
- export declare const createChannelRPCResponder: ({ capabilities, send, approve, }: {
10
+ export declare const createChannelRPCResponder: ({ capabilities, send, approve, trustInboundTraceparent, }: {
11
11
  capabilities: Capabilities;
12
12
  send: (data: unknown) => Promise<void> | void;
13
13
  /** Omitted means there is nobody to ask, which refuses rather than runs. */
14
14
  approve?: ApprovalRequester;
15
+ /**
16
+ * Hands a capability the peer's `traceparent`. Off by default, like public
17
+ * HTTP: the caller may turn it on only for a channel whose peer it has
18
+ * authenticated.
19
+ */
20
+ trustInboundTraceparent?: boolean;
15
21
  }) => (message: unknown) => Promise<boolean>;
@@ -7,7 +7,7 @@ import { CHANNEL_RPC_PENDING, CHANNEL_RPC_RESPONSE, isCapabilityDef, isChannelRP
7
7
  * name is refused exactly like an unknown one, so the caller cannot probe for
8
8
  * what this end can do.
9
9
  */
10
- export const createChannelRPCResponder = ({ capabilities, send, approve, }) => {
10
+ export const createChannelRPCResponder = ({ capabilities, send, approve, trustInboundTraceparent = false, }) => {
11
11
  return async (message) => {
12
12
  if (!isChannelRPCRequest(message)) {
13
13
  return false;
@@ -58,7 +58,7 @@ export const createChannelRPCResponder = ({ capabilities, send, approve, }) => {
58
58
  action: CHANNEL_RPC_RESPONSE,
59
59
  id: message.id,
60
60
  ok: true,
61
- result: await execute(message.data),
61
+ result: await execute(message.data, trustInboundTraceparent ? { traceparent: message.traceparent } : {}),
62
62
  };
63
63
  }
64
64
  catch (e) {
@@ -30,5 +30,5 @@ export declare class ChannelDeploymentService implements DeploymentService {
30
30
  * session on the wire would carry the caller's credentials while authorising
31
31
  * nothing.
32
32
  */
33
- invoke(funcName: string, data: unknown, _session?: unknown, traceId?: string): Promise<unknown>;
33
+ invoke(funcName: string, data: unknown, _session?: unknown, traceparent?: string): Promise<unknown>;
34
34
  }
@@ -37,7 +37,7 @@ export class ChannelDeploymentService {
37
37
  * session on the wire would carry the caller's credentials while authorising
38
38
  * nothing.
39
39
  */
40
- async invoke(funcName, data, _session, traceId) {
40
+ async invoke(funcName, data, _session, traceparent) {
41
41
  // Not a security control — the peer must check what it is handed, and a
42
42
  // hostile caller would send arguments that pass. It catches version drift
43
43
  // and fails it here rather than inside the peer's process.
@@ -60,7 +60,7 @@ export class ChannelDeploymentService {
60
60
  id,
61
61
  funcName,
62
62
  data,
63
- traceId,
63
+ traceparent,
64
64
  };
65
65
  try {
66
66
  await this.send(request);
@@ -12,7 +12,8 @@ export interface ChannelRPCRequest {
12
12
  id: string;
13
13
  funcName: string;
14
14
  data: unknown;
15
- traceId?: string;
15
+ /** A claim by the peer. See `trustInboundTraceparent` on the responder. */
16
+ traceparent?: string;
16
17
  }
17
18
  /** The answer to a {@link ChannelRPCRequest} with the same `id`: a result when `ok`, an error name and message otherwise. */
18
19
  export interface ChannelRPCResponse {
@@ -63,7 +64,9 @@ export interface ApprovalPolicy {
63
64
  approvalDescriptionFn?: (input: unknown) => Promise<string> | string;
64
65
  }
65
66
  /** A function one end of a channel offers for the peer to call through `channel.remote`. */
66
- export type CapabilityHandler = (data: any) => Promise<unknown> | unknown;
67
+ export type CapabilityHandler = (data: any, context?: {
68
+ traceparent?: string;
69
+ }) => Promise<unknown> | unknown;
67
70
  /**
68
71
  * `needsApproval` is required here on purpose. On `AgentToolDef` the same
69
72
  * field is optional and absence means "do not ask"; here absence means the
@@ -18,7 +18,7 @@ export const isChannelRPCRequest = (message) => isRecord(message) &&
18
18
  message.id.length > 0 &&
19
19
  typeof message.funcName === 'string' &&
20
20
  message.funcName.length > 0 &&
21
- (message.traceId === undefined || typeof message.traceId === 'string');
21
+ (message.traceparent === undefined || typeof message.traceparent === 'string');
22
22
  export const isChannelRPCResponse = (message) => isRecord(message) &&
23
23
  message.action === CHANNEL_RPC_RESPONSE &&
24
24
  typeof message.id === 'string' &&
@@ -4,12 +4,14 @@ export type Channel<ChannelType = unknown, OpeningData = unknown> = {
4
4
  channelName: string;
5
5
  channelObject?: ChannelType;
6
6
  openingData?: OpeningData;
7
+ /** The connection's W3C trace, so every frame continues it. */
8
+ traceparent?: string;
7
9
  };
8
10
  /**
9
11
  * Persists open channels for serverless runtimes, where each connect, message and disconnect runs in a separate invocation.
10
12
  *
11
- * It keeps the channel's opening data, its state, and the `pikkuUserId` of the
12
- * session established on connect. The session itself is not stored: each
13
+ * It keeps the channel's opening data, its trace, its state, and the
14
+ * `pikkuUserId` of the session established on connect. The session itself is not stored: each
13
15
  * message reloads it from the `sessionStore` by that id.
14
16
  */
15
17
  export declare abstract class ChannelStore<ChannelType = unknown, OpeningData = unknown, TypedChannel = Channel<ChannelType, OpeningData>> {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Persists open channels for serverless runtimes, where each connect, message and disconnect runs in a separate invocation.
3
3
  *
4
- * It keeps the channel's opening data, its state, and the `pikkuUserId` of the
5
- * session established on connect. The session itself is not stored: each
4
+ * It keeps the channel's opening data, its trace, its state, and the
5
+ * `pikkuUserId` of the session established on connect. The session itself is not stored: each
6
6
  * message reloads it from the `sessionStore` by that id.
7
7
  */
8
8
  export class ChannelStore {
@@ -7,6 +7,7 @@ export type CorePikkuChannelMiddleware<Services = any, Event = unknown> = (servi
7
7
  /** Builds a {@link CorePikkuChannelMiddleware} from configuration, so the same middleware can be reused with different settings. */
8
8
  export type CorePikkuChannelMiddlewareFactory<In = any, Services = any, Event = unknown> = (input: In) => CorePikkuChannelMiddleware<Services, Event>;
9
9
  import type { CorePikkuFunction, CorePikkuFunctionConfig, CorePikkuFunctionSessionless, CorePikkuPermission } from '../../function/functions.types.js';
10
+ import type { TraceContext } from '../../trace.js';
10
11
  /** Options for running a channel connection. They mean the same as the matching {@link RunHTTPWiringOptions}. */
11
12
  export type RunChannelOptions = Partial<{
12
13
  skipUserSession: boolean;
@@ -134,6 +135,8 @@ export interface PikkuChannel<OpeningData, out Out, Remote extends (...args: any
134
135
  }
135
136
  /** The runtime side of one connection: how a channel's sends reach the actual socket. */
136
137
  export interface PikkuChannelHandler<OpeningData = unknown, Out = unknown> {
138
+ /** The connection's trace. Each message and reverse RPC is a span in it. */
139
+ trace?: TraceContext;
137
140
  send(message: Out, isBinary?: boolean): Promise<void> | void;
138
141
  sendBinary(data: BinaryData): Promise<void> | void;
139
142
  getChannel(): PikkuChannel<OpeningData, Out>;
@@ -10,6 +10,7 @@ import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../..
10
10
  import { combineMiddleware, runMiddleware } from '../../../middleware-runner.js';
11
11
  import { runChannelLifecycleWithMiddleware } from '../channel-common.js';
12
12
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../../pikku-state.js';
13
+ import { childTraceWire, createTrace, tracedLogger } from '../../../trace.js';
13
14
  const runUpgradeMiddleware = async (singletonServices, request, response, userSession) => {
14
15
  const http = createHTTPWire(request, response);
15
16
  const httpGroups = pikkuState(null, 'middleware', 'httpGroup');
@@ -73,9 +74,14 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
73
74
  const main = async () => {
74
75
  try {
75
76
  channelHandler = new PikkuLocalChannelHandler(channelId, channelConfig.name, openingData, singletonServices.logger);
77
+ // knowledge: decisions/internals/a-channel-connection-is-one-trace.md
78
+ channelHandler.trace = createTrace();
79
+ const { traceId } = channelHandler.trace;
80
+ const logger = tracedLogger(singletonServices.logger, traceId);
76
81
  const channel = channelHandler.getChannel();
77
82
  const wire = {
78
83
  channel,
84
+ ...childTraceWire(channelHandler.trace),
79
85
  ...createMiddlewareSessionWireProps(userSession),
80
86
  };
81
87
  if (createWireServices) {
@@ -103,17 +109,19 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
103
109
  channelMiddlewareMeta: meta.channelMiddleware,
104
110
  userSession,
105
111
  wireServices,
112
+ trace: channelHandler?.trace,
106
113
  });
107
114
  if (result !== undefined) {
108
115
  await channel.send(result);
109
116
  }
110
117
  }
111
118
  catch (e) {
112
- singletonServices.logger.error(`Error handling onConnect: ${e}`);
119
+ logger.error(`Error handling onConnect: ${e}`);
113
120
  const errorResponse = getErrorResponse(e);
114
121
  await channel.send({
115
122
  error: errorResponse?.message ??
116
123
  (isProduction() ? 'Internal server error' : e.message),
124
+ errorId: traceId,
117
125
  ...(!isProduction() && { errorName: e.constructor?.name }),
118
126
  });
119
127
  }
@@ -132,14 +140,16 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
132
140
  channelMiddlewareMeta: meta.channelMiddleware,
133
141
  userSession,
134
142
  wireServices,
143
+ trace: channelHandler?.trace,
135
144
  });
136
145
  }
137
146
  catch (e) {
138
- singletonServices.logger.error(`Error handling onDisconnect: ${e}`);
147
+ logger.error(`Error handling onDisconnect: ${e}`);
139
148
  const errorResponse = getErrorResponse(e);
140
149
  await channel.send({
141
150
  error: errorResponse?.message ??
142
151
  (isProduction() ? 'Internal server error' : e.message),
152
+ errorId: traceId,
143
153
  ...(!isProduction() && { errorName: e.constructor?.name }),
144
154
  });
145
155
  }
@@ -160,15 +170,16 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
160
170
  }
161
171
  }
162
172
  catch (e) {
163
- singletonServices.logger.error(e);
173
+ logger.error(e);
164
174
  const errorResponse = getErrorResponse(e);
165
175
  await channel.send({
166
176
  error: errorResponse?.message ??
167
177
  (isProduction() ? 'Internal server error' : e.message),
178
+ errorId: traceId,
168
179
  ...(!isProduction() && { errorName: e.constructor?.name }),
169
180
  });
170
181
  setTimeout(() => {
171
- Promise.resolve(channel.close()).catch((closeError) => singletonServices.logger.error(closeError));
182
+ Promise.resolve(channel.close()).catch((closeError) => logger.error(closeError));
172
183
  }, 200);
173
184
  }
174
185
  });
@@ -181,7 +192,7 @@ export const runLocalChannel = async ({ channelId, request, response, route, ski
181
192
  }
182
193
  }
183
194
  catch (e) {
184
- singletonServices.logger.error(`Error handling binary message: ${e.message}`);
195
+ logger.error(`Error handling binary message: ${e.message}`);
185
196
  }
186
197
  });
187
198
  }
@@ -1,4 +1,5 @@
1
1
  import type { BinaryData, PikkuChannel, PikkuChannelHandler } from './channel.types.js';
2
+ import { type TraceContext } from '../../trace.js';
2
3
  /**
3
4
  * Base class for a runtime's {@link PikkuChannelHandler}. A runtime implements `send` and `sendBinary`; this builds the {@link PikkuChannel} on top, with in-memory state and `remote`.
4
5
  *
@@ -10,6 +11,7 @@ export declare abstract class PikkuAbstractChannelHandler<OpeningData = unknown,
10
11
  protected openingData: OpeningData;
11
12
  protected channel?: PikkuChannel<OpeningData, Out>;
12
13
  constructor(channelId: string, channelName: string, openingData: OpeningData);
14
+ trace?: TraceContext;
13
15
  abstract send(message: Out, isBinary?: boolean): Promise<void> | void;
14
16
  abstract sendBinary(data: BinaryData): Promise<void> | void;
15
17
  getChannel(): PikkuChannel<OpeningData, Out>;
@@ -1,4 +1,5 @@
1
1
  import { channelRemote, releaseChannelHostRPC } from './channel-host-rpc.js';
2
+ import { outgoingTraceparent } from '../../trace.js';
2
3
  /**
3
4
  * Base class for a runtime's {@link PikkuChannelHandler}. A runtime implements `send` and `sendBinary`; this builds the {@link PikkuChannel} on top, with in-memory state and `remote`.
4
5
  *
@@ -14,6 +15,7 @@ export class PikkuAbstractChannelHandler {
14
15
  this.channelName = channelName;
15
16
  this.openingData = openingData;
16
17
  }
18
+ trace;
17
19
  getChannel() {
18
20
  if (!this.channel) {
19
21
  let channelState;
@@ -32,7 +34,7 @@ export class PikkuAbstractChannelHandler {
32
34
  clearState: () => {
33
35
  channelState = undefined;
34
36
  },
35
- remote: (funcName, data) => channelRemote(this.getChannel(), funcName, data),
37
+ remote: (funcName, data) => channelRemote(this.getChannel(), funcName, data, outgoingTraceparent(this)),
36
38
  };
37
39
  }
38
40
  return this.channel;
@@ -7,6 +7,7 @@ import { PikkuSessionService, createMiddlewareSessionWireProps, } from '../../..
7
7
  import { getSingletonServices, getCreateWireServices, pikkuState, } from '../../../pikku-state.js';
8
8
  import { PikkuFetchHTTPRequest } from '../../http/pikku-fetch-http-request.js';
9
9
  import { runChannelLifecycleWithMiddleware } from '../channel-common.js';
10
+ import { createTrace, formatTraceparent, parseTraceparent, tracedLogger, } from '../../../trace.js';
10
11
  const getVariablesForChannel = ({ channelId, channelName, channelHandlerFactory, openingData, }) => {
11
12
  const channels = pikkuState(null, 'channel', 'channels');
12
13
  const channelConfig = channels.get(channelName);
@@ -26,6 +27,21 @@ const getVariablesForChannel = ({ channelId, channelName, channelHandlerFactory,
26
27
  meta,
27
28
  };
28
29
  };
30
+ /**
31
+ * The connection's trace as connect stored it. A frame is a span in it, so this
32
+ * is the connection's own span and not a new one.
33
+ */
34
+ // knowledge: decisions/internals/a-channel-connection-is-one-trace.md
35
+ const storedTrace = (traceparent) => {
36
+ const stored = parseTraceparent(traceparent);
37
+ return stored
38
+ ? {
39
+ traceId: stored.traceId,
40
+ spanId: stored.parentSpanId,
41
+ sampled: stored.sampled,
42
+ }
43
+ : createTrace();
44
+ };
29
45
  /**
30
46
  * Handles a serverless connect event: opens the channel, stores it, and runs `onConnect`.
31
47
  *
@@ -56,18 +72,21 @@ export const runChannelConnect = async ({ channelId, channelObject, request, res
56
72
  coerceDataFromSchema,
57
73
  userSession,
58
74
  });
75
+ const trace = createTrace();
59
76
  try {
60
77
  await channelStore.addChannel({
61
78
  channelId,
62
79
  channelName: channelConfig.name,
63
80
  openingData,
64
81
  channelObject,
82
+ traceparent: formatTraceparent(trace),
65
83
  });
66
- const { channel } = getVariablesForChannel({
84
+ const { channel, channelHandler } = getVariablesForChannel({
67
85
  channelId,
68
86
  channelHandlerFactory,
69
87
  channelName: channelConfig.name,
70
88
  });
89
+ channelHandler.trace = trace;
71
90
  channel.setState = (state) => channelStore.setState(channelId, state);
72
91
  // knowledge: decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md
73
92
  channel.getState = () => channelStore.getState(channelId);
@@ -95,6 +114,7 @@ export const runChannelConnect = async ({ channelId, channelObject, request, res
95
114
  channelMiddlewareMeta: meta.channelMiddleware,
96
115
  userSession,
97
116
  wireServices,
117
+ trace: channelHandler.trace,
98
118
  });
99
119
  }
100
120
  const pikkuUserId = userSession.getPikkuUserId();
@@ -131,7 +151,7 @@ export const runChannelDisconnect = async ({ channelId, channelStore, channelHan
131
151
  singletonServices.logger.info(`Channel ${channelId} not found during disconnect - already cleaned up`);
132
152
  return;
133
153
  }
134
- const { openingData, channelName, pikkuUserId } = channelData;
154
+ const { openingData, channelName, pikkuUserId, traceparent } = channelData;
135
155
  const { channel, channelConfig, meta } = getVariablesForChannel({
136
156
  channelId,
137
157
  channelHandlerFactory,
@@ -173,6 +193,7 @@ export const runChannelDisconnect = async ({ channelId, channelStore, channelHan
173
193
  channelMiddlewareMeta: meta.channelMiddleware,
174
194
  userSession,
175
195
  wireServices,
196
+ trace: storedTrace(traceparent),
176
197
  });
177
198
  }
178
199
  catch (e) {
@@ -198,13 +219,14 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
198
219
  const singletonServices = getSingletonServices();
199
220
  const createWireServices = getCreateWireServices();
200
221
  let wireServices;
201
- const { openingData, channelName, pikkuUserId } = await channelStore.getChannel(channelId);
222
+ const { openingData, channelName, pikkuUserId, traceparent } = await channelStore.getChannel(channelId);
202
223
  const { channel, channelHandler, channelConfig } = getVariablesForChannel({
203
224
  channelId,
204
225
  channelHandlerFactory,
205
226
  openingData,
206
227
  channelName,
207
228
  });
229
+ channelHandler.trace = storedTrace(traceparent);
208
230
  channel.setState = (state) => channelStore.setState(channelId, state);
209
231
  // knowledge: decisions/internals/channel-state-accessors-are-unsound-generics-that-every-implementation-asserts.md
210
232
  channel.getState = () => channelStore.getState(channelId);
@@ -234,10 +256,11 @@ export const runChannelMessage = async ({ channelId, channelStore, channelHandle
234
256
  response = await onMessage(data);
235
257
  }
236
258
  catch (e) {
237
- singletonServices.logger.error(`Error processing message: ${e.message || e}`);
259
+ tracedLogger(singletonServices.logger, channelHandler.trace.traceId).error(`Error processing message: ${e.message || e}`);
238
260
  return {
239
261
  error: e.message || 'Unknown error',
240
262
  errorName: e.constructor?.name,
263
+ errorId: channelHandler.trace.traceId,
241
264
  };
242
265
  }
243
266
  finally {
@@ -66,6 +66,7 @@ export const resolveGatewayAdapter = (config, services) => {
66
66
  }
67
67
  return resolved;
68
68
  };
69
+ import { traceWire } from '../../trace.js';
69
70
  /**
70
71
  * Wires a messaging gateway (Slack, WhatsApp, a web chat, ...) so each inbound message runs `func`
71
72
  * with the adapter-normalised message.
@@ -318,7 +319,8 @@ export const createListenerMessageHandler = (name, config, singletonServices) =>
318
319
  const parsed = adapter.parse(rawData);
319
320
  if (!parsed)
320
321
  return;
321
- const wire = {};
322
+ // Here rather than left to the function runner: middleware runs first.
323
+ const wire = traceWire();
322
324
  const gateway = {
323
325
  gatewayName: name,
324
326
  senderId: parsed.senderId,
@@ -62,9 +62,9 @@ export declare const pikkuFetch: <In, Out>(request: Request | PikkuHTTPRequest,
62
62
  *
63
63
  * The lower-level form of {@link fetch} for adapters that bring their own
64
64
  * request and response objects (Express, Fastify, uWS). A web `Request` is
65
- * wrapped with the `maxBodySize` limit. The trace ID is taken from `traceId`,
66
- * then the `x-request-id` header, then `generateRequestId`, and is returned to
67
- * the client as `errorId` on failures.
65
+ * wrapped with the `maxBodySize` limit. The trace comes from a valid `traceId`,
66
+ * then `traceparent` when `trustInboundTraceparent` is set, and is otherwise new.
67
+ * Its id is returned to the client as `errorId` on failures.
68
68
  *
69
69
  * Errors are written to `response`: a 4xx keeps its message and payload, while a
70
70
  * 5xx or an unknown error hides its message unless `exposeErrors` is set outside
@@ -78,4 +78,4 @@ export declare const pikkuFetch: <In, Out>(request: Request | PikkuHTTPRequest,
78
78
  * @throws The original error when `bubbleErrors` is set, after the error response has been written.
79
79
  * @throws {Error} When `skipUserSession` is set on a route that requires auth.
80
80
  */
81
- export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, generateRequestId, traceId: externalTraceId, maxBodySize, }?: RunHTTPWiringOptions) => Promise<Out | void>;
81
+ export declare const fetchData: <In, Out>(request: Request | PikkuHTTPRequest, response: PikkuHTTPResponse, { skipUserSession, respondWith404, logWarningsForStatusCodes, coerceDataFromSchema, bubbleErrors, exposeErrors, traceId, trustInboundTraceparent, maxBodySize, }?: RunHTTPWiringOptions) => Promise<Out | void>;