@pikku/core 0.12.150 → 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 +6 -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 +3 -0
  56. package/dist/wirings/agent/agent-prepare.js +24 -8
  57. package/dist/wirings/agent/agent-rpc.js +1 -0
  58. package/dist/wirings/agent/agent-runner.js +3 -0
  59. package/dist/wirings/agent/agent-stream.js +4 -0
  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,147 @@
1
+ import { ConflictError, ForbiddenError, NotFoundError, UnauthorizedError, UnprocessableContentError, } from '../../errors/errors.js';
2
+ import { AbandonedError } from '../../function/abort-scope.js';
3
+ import { RemoteAddonAuthError } from '../../wirings/addon/remote-addon-auth.js';
4
+ import { RemoteAddonFailedError, RemoteAddonTimeoutError, } from '../../wirings/rpc/rpc-runner.js';
5
+ import { QueueJobDiscardedError } from '../../wirings/queue/queue-runner.js';
6
+ import { InMemoryWorkflowService } from '../../wirings/workflow/in-memory-workflow-service.js';
7
+ import { PikkuWorkflowService } from '../../wirings/workflow/pikku-workflow-service.js';
8
+ /** Sleeps go through the scheduler, as in a deployment, so the wake-up is driven by hand. */
9
+ export class ScheduledSleeps extends InMemoryWorkflowService {
10
+ scheduleSleep(...args) {
11
+ return PikkuWorkflowService.prototype['scheduleSleep'].apply(this, args);
12
+ }
13
+ }
14
+ /** What a step's RPC throws for an outcome: the class a real call would raise. */
15
+ export const outcomeError = (outcome) => {
16
+ switch (outcome) {
17
+ case 'throw-pikku-error':
18
+ case 'step-compensated':
19
+ case 'fan-out-sibling-failure':
20
+ return new ConflictError('refused');
21
+ case 'throw-plain-error':
22
+ case 'job-exhausted':
23
+ return new Error('boom');
24
+ case 'schema-invalid':
25
+ case 'job-poison':
26
+ return new UnprocessableContentError('schema refused the data');
27
+ case 'auth-mesh-jwt':
28
+ return new UnauthorizedError();
29
+ case 'auth-remote-addon':
30
+ return new RemoteAddonAuthError('far', 'resolved token was empty');
31
+ case 'permission-denied':
32
+ return new ForbiddenError();
33
+ case 'remote-4xx':
34
+ return new NotFoundError();
35
+ case 'remote-5xx':
36
+ case 'remote-network':
37
+ return new RemoteAddonFailedError('far', 'fn');
38
+ case 'remote-timeout':
39
+ return new RemoteAddonTimeoutError('far', 'fn', 2);
40
+ case 'job-discarded':
41
+ return new QueueJobDiscardedError('job', 'not wanted');
42
+ case 'abandoned':
43
+ return new AbandonedError('the caller left');
44
+ default:
45
+ return undefined;
46
+ }
47
+ };
48
+ const isDsl = (workflow) => workflow.startsWith('dsl');
49
+ /**
50
+ * Registers a workflow named `flow-<id>` that reaches `step`, then has its last
51
+ * RPC meet `outcome`. Returns what brings the run to its end.
52
+ */
53
+ export const defineRun = (h, run, child = false) => {
54
+ const { id, workflow, step, outcome } = run;
55
+ const queued = workflow.endsWith('-queued');
56
+ const failure = () => outcomeError(outcome);
57
+ let released = false;
58
+ let calls = 0;
59
+ const last = {
60
+ queued,
61
+ forward: async () => {
62
+ const error = failure();
63
+ if (error)
64
+ throw error;
65
+ if (step === 'retry' && ++calls === 1)
66
+ throw new Error('flaky');
67
+ return {};
68
+ },
69
+ };
70
+ const undoable = step === 'compensation' || outcome === 'step-compensated'
71
+ ? { compensate: async () => { } }
72
+ : {};
73
+ h.register(`first-${id}`, {
74
+ queued,
75
+ ...undoable,
76
+ forward: async (_data, wire) => {
77
+ if (outcome === 'workflow-cancelled') {
78
+ await h.ws.cancelRun(wire.workflow?.runId ?? wire.workflowStep?.runId, h.rpc);
79
+ }
80
+ return {};
81
+ },
82
+ });
83
+ h.register(`last-${id}`, last);
84
+ h.register(`left-${id}`, { queued, forward: async () => ({}) });
85
+ const inner = child ? `inner-${id}` : `flow-${id}`;
86
+ if (isDsl(workflow)) {
87
+ h.defineDsl(inner, async (w) => {
88
+ await w.do('First', `first-${id}`, {});
89
+ if (step === 'sleep')
90
+ await w.sleep('Nap', 1);
91
+ if (step === 'suspend' && !released)
92
+ await w.suspend('Hold');
93
+ if (step === 'approval') {
94
+ const decision = await w.approval('Sign off', {
95
+ schema: {
96
+ '~standard': {
97
+ version: 1,
98
+ vendor: 'matrix',
99
+ validate: (value) => ({ value }),
100
+ },
101
+ },
102
+ });
103
+ if (decision.data?.ok === false) {
104
+ throw new ConflictError('the approval was rejected');
105
+ }
106
+ }
107
+ if (step === 'fan-out') {
108
+ await Promise.all([
109
+ w.do('Left', `left-${id}`, {}),
110
+ w.do('Last', `last-${id}`, {}),
111
+ ]);
112
+ }
113
+ else {
114
+ await w.do('Last', `last-${id}`, {}, step === 'retry' ? { retries: 2 } : undefined);
115
+ }
116
+ });
117
+ }
118
+ else {
119
+ h.defineGraph(inner, `first-${id}`, {
120
+ [`first-${id}`]: {
121
+ next: step === 'fan-out' ? [`left-${id}`, `last-${id}`] : `last-${id}`,
122
+ },
123
+ [`left-${id}`]: {},
124
+ [`last-${id}`]: step === 'retry' ? { retries: 2 } : {},
125
+ });
126
+ }
127
+ if (child) {
128
+ h.defineDsl(`flow-${id}`, async (w) => w.do('Child', inner, {}));
129
+ }
130
+ return {
131
+ /** Wakes whatever the run is waiting on, then lets it finish. */
132
+ settle: async (runId, wakeSleeps) => {
133
+ if (step === 'sleep')
134
+ await wakeSleeps();
135
+ if (step === 'suspend') {
136
+ released = true;
137
+ await h.ws.resumeWorkflow(runId);
138
+ }
139
+ if (step === 'approval') {
140
+ await h.ws.approveStep(runId, 'Sign off', {
141
+ ok: outcome !== 'approval-rejected',
142
+ });
143
+ }
144
+ await h.pump();
145
+ },
146
+ };
147
+ };
@@ -0,0 +1,66 @@
1
+ import type { Logger } from './services/logger.js';
2
+ /** One invocation's place in a W3C trace. */
3
+ export type TraceContext = {
4
+ /** 32 lowercase hex characters, shared by every hop of the trace. */
5
+ traceId: string;
6
+ /** 16 lowercase hex characters, identifying this invocation. */
7
+ spanId: string;
8
+ /** The caller's span, when the trace was continued from another hop. */
9
+ parentSpanId?: string;
10
+ sampled: boolean;
11
+ };
12
+ /** Whether `value` is a usable trace id: 32 lowercase hex, not all zero. */
13
+ export declare const isValidTraceId: (value: unknown) => value is string;
14
+ /** A new trace, or a new span in `traceId` when one is given. */
15
+ export declare const createTrace: (traceId?: string) => TraceContext;
16
+ /**
17
+ * Reads a W3C `traceparent` header. Returns `undefined` for anything malformed,
18
+ * an all-zero id, version `ff`, or a version `00` header with extra fields.
19
+ */
20
+ export declare const parseTraceparent: (header: unknown) => (Pick<TraceContext, 'traceId' | 'sampled'> & {
21
+ parentSpanId: string;
22
+ }) | undefined;
23
+ /** Formats `trace` as the `traceparent` of a call it makes: it is the parent. */
24
+ export declare const formatTraceparent: (trace: Pick<TraceContext, 'traceId' | 'spanId' | 'sampled'>) => string;
25
+ /**
26
+ * Resolves the trace for an inbound HTTP request: a valid `traceId` from the
27
+ * runtime, then the `traceparent` header if the deployment opted in, then a new
28
+ * trace.
29
+ *
30
+ * knowledge: decisions/security/inbound-traceparent-is-ignored-until-the-caller-is-authenticated.md
31
+ */
32
+ export declare const resolveHTTPTrace: (readHeader: () => unknown, logger: Logger, { traceId, trustInboundTraceparent, }: {
33
+ traceId?: string;
34
+ trustInboundTraceparent?: boolean;
35
+ }) => TraceContext;
36
+ /**
37
+ * Starts this hop's span in the trace a job, scheduled task or workflow run
38
+ * carried, or a new trace when it carried nothing usable. A carried value needs
39
+ * no signature: it names a trace, never an identity.
40
+ */
41
+ export declare const continueTrace: (traceparent: unknown) => TraceContext;
42
+ /**
43
+ * Continues the caller's trace on an already-running invocation. Only for a
44
+ * caller that has been authenticated, and in place because the HTTP runner
45
+ * holds `wire.trace` for its error response.
46
+ */
47
+ export declare const adoptTraceparent: (wire: {
48
+ traceId?: string;
49
+ trace?: TraceContext;
50
+ }, header: unknown) => void;
51
+ /** `logger` scoped to a trace, so what it writes can be found by that id. */
52
+ export declare const tracedLogger: (logger: Logger, traceId?: string) => Logger;
53
+ /** The wire fields for a hop that continues `traceparent`, or starts a new trace. */
54
+ export declare const traceWire: (traceparent?: unknown) => {
55
+ traceId: string;
56
+ trace: TraceContext;
57
+ };
58
+ /** {@link traceWire} for a child of `parent`, when there is a parent. */
59
+ export declare const childTraceWire: (parent: TraceContext | undefined) => {
60
+ traceId: string;
61
+ trace: TraceContext;
62
+ };
63
+ /** The `traceparent` for a call made from `wire`, if it is in a W3C trace. */
64
+ export declare const outgoingTraceparent: (wire: {
65
+ trace?: TraceContext;
66
+ }) => string | undefined;
package/dist/trace.js ADDED
@@ -0,0 +1,97 @@
1
+ const TRACE_ID = /^[0-9a-f]{32}$/;
2
+ const TRACEPARENT = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})(-.*)?$/;
3
+ const ALL_ZERO = /^0+$/;
4
+ const randomHex = (bytes) => {
5
+ const hex = Array.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, '0')).join('');
6
+ // All zeroes means "invalid" in W3C, so it can never be issued.
7
+ return ALL_ZERO.test(hex) ? randomHex(bytes) : hex;
8
+ };
9
+ /** Whether `value` is a usable trace id: 32 lowercase hex, not all zero. */
10
+ export const isValidTraceId = (value) => typeof value === 'string' && TRACE_ID.test(value) && !ALL_ZERO.test(value);
11
+ /** A new trace, or a new span in `traceId` when one is given. */
12
+ export const createTrace = (traceId) => ({
13
+ traceId: traceId ?? randomHex(16),
14
+ spanId: randomHex(8),
15
+ sampled: true,
16
+ });
17
+ /**
18
+ * Reads a W3C `traceparent` header. Returns `undefined` for anything malformed,
19
+ * an all-zero id, version `ff`, or a version `00` header with extra fields.
20
+ */
21
+ export const parseTraceparent = (header) => {
22
+ if (typeof header !== 'string')
23
+ return undefined;
24
+ const match = TRACEPARENT.exec(header);
25
+ if (!match)
26
+ return undefined;
27
+ const [, version, traceId, parentSpanId, flags, extra] = match;
28
+ if (version === 'ff' || (version === '00' && extra !== undefined)) {
29
+ return undefined;
30
+ }
31
+ if (ALL_ZERO.test(traceId) || ALL_ZERO.test(parentSpanId))
32
+ return undefined;
33
+ return {
34
+ traceId: traceId,
35
+ parentSpanId: parentSpanId,
36
+ sampled: (Number.parseInt(flags, 16) & 1) === 1,
37
+ };
38
+ };
39
+ /** Formats `trace` as the `traceparent` of a call it makes: it is the parent. */
40
+ export const formatTraceparent = (trace) => `00-${trace.traceId}-${trace.spanId}-${trace.sampled ? '01' : '00'}`;
41
+ /**
42
+ * Resolves the trace for an inbound HTTP request: a valid `traceId` from the
43
+ * runtime, then the `traceparent` header if the deployment opted in, then a new
44
+ * trace.
45
+ *
46
+ * knowledge: decisions/security/inbound-traceparent-is-ignored-until-the-caller-is-authenticated.md
47
+ */
48
+ export const resolveHTTPTrace = (readHeader, logger, { traceId, trustInboundTraceparent, }) => {
49
+ if (traceId !== undefined) {
50
+ if (isValidTraceId(traceId))
51
+ return createTrace(traceId);
52
+ logger.warn('Ignoring a traceId that is not 32 lowercase hex characters');
53
+ }
54
+ if (trustInboundTraceparent) {
55
+ let header;
56
+ try {
57
+ header = readHeader();
58
+ }
59
+ catch { }
60
+ const inbound = parseTraceparent(header);
61
+ if (inbound)
62
+ return { ...inbound, spanId: randomHex(8) };
63
+ }
64
+ return createTrace();
65
+ };
66
+ /**
67
+ * Starts this hop's span in the trace a job, scheduled task or workflow run
68
+ * carried, or a new trace when it carried nothing usable. A carried value needs
69
+ * no signature: it names a trace, never an identity.
70
+ */
71
+ export const continueTrace = (traceparent) => {
72
+ const carried = parseTraceparent(traceparent);
73
+ return carried ? { ...carried, spanId: randomHex(8) } : createTrace();
74
+ };
75
+ /**
76
+ * Continues the caller's trace on an already-running invocation. Only for a
77
+ * caller that has been authenticated, and in place because the HTTP runner
78
+ * holds `wire.trace` for its error response.
79
+ */
80
+ export const adoptTraceparent = (wire, header) => {
81
+ const inbound = parseTraceparent(header);
82
+ if (!inbound)
83
+ return;
84
+ wire.traceId = inbound.traceId;
85
+ wire.trace = Object.assign(wire.trace ?? createTrace(inbound.traceId), inbound);
86
+ };
87
+ /** `logger` scoped to a trace, so what it writes can be found by that id. */
88
+ export const tracedLogger = (logger, traceId) => (traceId && logger.scope?.(traceId)) || logger;
89
+ /** The wire fields for a hop that continues `traceparent`, or starts a new trace. */
90
+ export const traceWire = (traceparent) => {
91
+ const trace = continueTrace(traceparent);
92
+ return { traceId: trace.traceId, trace };
93
+ };
94
+ /** {@link traceWire} for a child of `parent`, when there is a parent. */
95
+ export const childTraceWire = (parent) => traceWire(parent && formatTraceparent(parent));
96
+ /** The `traceparent` for a call made from `wire`, if it is in a W3C trace. */
97
+ export const outgoingTraceparent = (wire) => wire.trace && formatTraceparent(wire.trace);
@@ -1,4 +1,6 @@
1
1
  import type { AbortScope } from '../function/abort-scope.js';
2
+ import type { TraceContext } from '../trace.js';
3
+ export type { TraceContext };
2
4
  import type { IncomingWebhookService } from '../services/incoming-webhook-service.js';
3
5
  import type { TriggerSourceStore } from '../services/trigger-source-store.js';
4
6
  import type { LeaseService } from '../services/lease-service.js';
@@ -88,7 +90,7 @@ export type CoreConfig<Config extends Record<string, unknown> = {}> = {
88
90
  /** Refuse a secret that declares no `allowedHosts` rather than treating it as unrestricted. */
89
91
  requireAllowedHosts?: boolean;
90
92
  };
91
- workflow?: WorkflowServiceConfig;
93
+ workflow?: Partial<WorkflowServiceConfig>;
92
94
  webhook?: WebhookServiceConfig;
93
95
  postgres?: PostgresConfig;
94
96
  } & Config;
@@ -255,8 +257,10 @@ export type PikkuWire<In = unknown, Out = unknown, HasInitialSession extends boo
255
257
  * sets it; services that log fall back to the singleton logger.
256
258
  */
257
259
  logger: Logger;
258
- /** Trace ID for distributed tracing — propagated across mesh RPC calls via the x-request-id header */
260
+ /** Trace ID for distributed tracing — propagated across mesh RPC calls via the traceparent header */
259
261
  traceId: string;
262
+ /** This invocation's place in the trace, when it has a W3C one. */
263
+ trace: TraceContext;
260
264
  functionId: string;
261
265
  addonNamespace: string;
262
266
  http: PikkuHTTP<In>;
@@ -5,6 +5,9 @@ export declare const addonCallerOf: (wire: PikkuRawWire) => string | undefined;
5
5
  export declare class AddonNotUsedError extends PikkuError {
6
6
  constructor(caller: string, target: string);
7
7
  }
8
+ export declare class AddonUsesShadowedError extends PikkuError {
9
+ constructor(name: string, target: string);
10
+ }
8
11
  /**
9
12
  * The name an addon's call resolves to on this host, through its `uses`.
10
13
  *
@@ -29,6 +29,15 @@ addError(AddonNotUsedError, {
29
29
  status: 403,
30
30
  message: 'Addon called another addon it does not use.',
31
31
  });
32
+ export class AddonUsesShadowedError extends PikkuError {
33
+ constructor(name, target) {
34
+ super(`Addon '${name}' maps its own name in 'uses' to '${target}', but a call to '${name}:<function>' always reaches the addon itself, so the mapping can never be used. Mount the addon under another name.`);
35
+ }
36
+ }
37
+ addError(AddonUsesShadowedError, {
38
+ status: 500,
39
+ message: 'Addon uses maps its own name.',
40
+ });
32
41
  /**
33
42
  * The name an addon's call resolves to on this host, through its `uses`.
34
43
  *
@@ -32,6 +32,12 @@ type StudioScreenBase = {
32
32
  * Lowercase letters, digits and hyphens, starting with a letter. Leave it out and the chat is unchanged.
33
33
  */
34
34
  agent?: string;
35
+ /**
36
+ * Whether Studio's general Chat tab shows on this screen, beside the
37
+ * screen's agent tab. Defaults to `true`. `false` hides it; with no `agent`
38
+ * the screen then has no chat at all.
39
+ */
40
+ defaultChat?: boolean;
35
41
  } & ({
36
42
  /** `'studio'` gives `/<path>`, `'team'` gives `/<team>/<path>`. */
37
43
  scope: 'studio' | 'team';
@@ -1,5 +1,6 @@
1
1
  import { pikkuState } from '../../pikku-state.js';
2
2
  import { getTagGroups } from '../../utils.js';
3
+ import { AddonUsesShadowedError } from './addon-reach.js';
3
4
  /**
4
5
  * Installs an addon into this project: its functions, wirings and scopes become
5
6
  * part of the app, under the namespace and options given here.
@@ -7,6 +8,10 @@ import { getTagGroups } from '../../utils.js';
7
8
  * @example snippet: addonWiring
8
9
  */
9
10
  export const wireAddon = (config) => {
11
+ const shadowed = config.uses?.[config.name];
12
+ if (shadowed !== undefined && shadowed !== config.name) {
13
+ throw new AddonUsesShadowedError(config.name, shadowed);
14
+ }
10
15
  pikkuState(null, 'addons', 'packages').set(config.name, {
11
16
  package: config.package,
12
17
  rpcEndpoint: config.rpcEndpoint,
@@ -51,6 +51,17 @@ export type WireRemoteAddonConfig = {
51
51
  * knowledge: decisions/security/wire-addon-expose-selects-the-rpc-surface.md
52
52
  */
53
53
  expose?: boolean | string[];
54
+ /**
55
+ * A package installed in this project that carries the screens and
56
+ * contributors shown under this name, when they live apart from the contract
57
+ * package. Read at build time, never at runtime. `ui` mounts its screens;
58
+ * `contributors` opts in to the ones it registers.
59
+ */
60
+ local?: {
61
+ package: string;
62
+ ui?: boolean;
63
+ contributors?: string[];
64
+ };
54
65
  /** Milliseconds a call may take before it is aborted. Defaults to {@link DEFAULT_REMOTE_ADDON_TIMEOUT_MS}. */
55
66
  timeoutMs?: number;
56
67
  /** Extra headers for every call to the host. Cannot replace the auth, trace or contract headers. */
@@ -12,6 +12,8 @@ export type RunUsage = {
12
12
  * a scorer is shown is exactly what was stored — including the redactions.
13
13
  */
14
14
  export type FinalizedRun = {
15
+ /** The trace the run's scoring jobs stay in. */
16
+ traceparent?: string;
15
17
  runId: string;
16
18
  agentName: string;
17
19
  threadId: string;
@@ -130,7 +130,7 @@ export const finalizeAgentRun = async (agentRunState, run) => {
130
130
  // Best-effort and last: the client already has its answer, so a grading
131
131
  // failure must not surface as a failed run.
132
132
  try {
133
- await scoreFinishedRun(snapshot, services);
133
+ await scoreFinishedRun(snapshot, services, run.traceparent);
134
134
  }
135
135
  catch (error) {
136
136
  services.logger?.error(`[pikku] Live scoring failed for run ${run.runId}`, error);
@@ -3,6 +3,7 @@ import type { CoreAgent, AgentInput, AgentToolDef, AgentMessage, AgentStreamChan
3
3
  import type { AgentRunnerParams, AgentRunnerService } from '../../services/agent-runner-service.js';
4
4
  import { PikkuError } from '../../errors/error-handler.js';
5
5
  import type { SessionService } from '../../services/user-session-service.js';
6
+ import { type TraceContext } from '../../trace.js';
6
7
  /**
7
8
  * Per-request context for running an agent: the caller's session, credential access and, for
8
9
  * a sessionless request, the minted anonymous owner id.
@@ -13,6 +14,8 @@ export type RunAgentParams = {
13
14
  getCredential?: GetCredential;
14
15
  /** Ephemeral owner identity minted for a sessionless request; never client-supplied, never reused across requests. */
15
16
  anonymousOwnerResourceId?: string;
17
+ /** The caller's trace. Each tool call is a span in it. */
18
+ trace?: TraceContext;
16
19
  };
17
20
  export declare function resolveSessionPrincipal(params: RunAgentParams, sessionScope: SessionScope | undefined): string | undefined;
18
21
  export declare function resolveOwnerResourceId(params: RunAgentParams, sessionScope: SessionScope | undefined, requestedResourceId: string): string;
@@ -11,6 +11,7 @@ import { resolveNamespace, ContextAwareRPCService, } from '../../wirings/rpc/rpc
11
11
  import { getOrCreatePackageSingletonServices } from '../../wirings/addon/addon-runner.js';
12
12
  import { resolveMemoryServices, loadContextMessages, trimMessages, } from './agent-memory.js';
13
13
  import { resolveModelConfig } from './agent-model-config.js';
14
+ import { childTraceWire, createTrace, outgoingTraceparent, tracedLogger, } from '../../trace.js';
14
15
  export function resolveSessionPrincipal(params, sessionScope) {
15
16
  const session = params.sessionService?.get();
16
17
  if ((sessionScope ?? 'user') === 'org') {
@@ -321,6 +322,8 @@ export function buildSubAgentRunInput(message, threadId, resourceId, parentConte
321
322
  export async function buildToolDefs(params, agentSessionMap, resourceId, agentName, packageName, streamContext, agentMiddlewares, agentMode, parentContext) {
322
323
  const singletonServices = getSingletonServices();
323
324
  const tools = [];
325
+ // Every tool of a run sits under one trace, whoever started the run.
326
+ const trace = params.trace ?? createTrace();
324
327
  const missingRpcs = [];
325
328
  const approvalPolicy = streamContext?.options?.requiresToolApproval ?? 'explicit';
326
329
  const meta = pikkuState(packageName, 'agent', 'agentsMeta')[agentName];
@@ -442,9 +445,14 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
442
445
  approvalDescriptionFn,
443
446
  readonly: fnMeta?.readonly || undefined,
444
447
  execute: async (toolInput, options) => {
445
- const wire = params.sessionService
446
- ? { ...createMiddlewareSessionWireProps(params.sessionService) }
447
- : {};
448
+ // The call below is the tool's span, under the run's.
449
+ const wire = {
450
+ traceId: trace.traceId,
451
+ trace,
452
+ ...(params.sessionService
453
+ ? createMiddlewareSessionWireProps(params.sessionService)
454
+ : {}),
455
+ };
448
456
  if (options?.abortScope)
449
457
  wire.abortScope = options.abortScope;
450
458
  const rpcService = new ContextAwareRPCService(singletonServices, wire, { sessionService: params.sessionService });
@@ -594,13 +602,21 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
594
602
  if (!workflowService) {
595
603
  throw new Error(`workflowService is not configured — cannot run workflow tool '${workflowName}'`);
596
604
  }
597
- const wire = params.sessionService
598
- ? { ...createMiddlewareSessionWireProps(params.sessionService) }
599
- : {};
605
+ const wire = {
606
+ ...childTraceWire(trace),
607
+ ...(params.sessionService
608
+ ? createMiddlewareSessionWireProps(params.sessionService)
609
+ : {}),
610
+ };
600
611
  if (options?.abortScope)
601
612
  wire.abortScope = options.abortScope;
602
613
  const rpcService = new ContextAwareRPCService(singletonServices, wire, { sessionService: params.sessionService });
603
- return workflowService.runToCompletion(workflowName, toolInput, rpcService, { wire: { type: 'internal' } });
614
+ return workflowService.runToCompletion(workflowName, toolInput, rpcService, {
615
+ wire: {
616
+ type: 'internal',
617
+ traceparent: outgoingTraceparent(wire),
618
+ },
619
+ });
604
620
  },
605
621
  });
606
622
  }
@@ -619,7 +635,7 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
619
635
  ...err.payload,
620
636
  };
621
637
  }
622
- singletonServices.logger.error(`AI agent tool '${tool.name}' threw during execute()`, err);
638
+ tracedLogger(singletonServices.logger, trace.traceId).error(`AI agent tool '${tool.name}' threw during execute()`, err);
623
639
  throw err;
624
640
  }
625
641
  };
@@ -14,6 +14,7 @@ export const createAgentRPC = (wire, options) => {
14
14
  const params = () => ({
15
15
  sessionService: options.sessionService,
16
16
  getCredential: wire.getCredential?.bind(wire),
17
+ trace: wire.trace,
17
18
  });
18
19
  const streamingChannel = () => {
19
20
  const channel = wire.channel;
@@ -1,3 +1,4 @@
1
+ import { outgoingTraceparent } from '../../trace.js';
1
2
  import { saveMessages, resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './agent-memory.js';
2
3
  import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolveOwnerResourceId, agentSessionScope, assertResourcePrincipalOwner, assertRunBelongsToAgent, assertAgentAuthorized, resolveAgentRunner, } from './agent-prepare.js';
3
4
  import { checkForApprovals, appendStepMessages } from './agent-stream.js';
@@ -224,6 +225,7 @@ export async function runAgent(agentName, input, params, agentSessionMap) {
224
225
  uiSpec: structuredOutput.uiSpec,
225
226
  });
226
227
  await finalizeAgentRun(agentRunState, {
228
+ traceparent: outgoingTraceparent({ trace: params.trace }),
227
229
  runId,
228
230
  agentName,
229
231
  threadId,
@@ -557,6 +559,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
557
559
  steps: accumulatedSteps,
558
560
  });
559
561
  await finalizeAgentRun(agentRunState, {
562
+ traceparent: outgoingTraceparent({ trace: params.trace }),
560
563
  runId: run.runId,
561
564
  agentName: resolvedName,
562
565
  threadId: run.threadId,
@@ -1,3 +1,4 @@
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';
@@ -185,6 +186,7 @@ const warnUnstreamedOutputHooks = (agentName, agentMiddlewares, logger) => {
185
186
  };
186
187
  async function postStreamCleanup(persistingChannel, agentRunState, runId, run) {
187
188
  await finalizeAgentRun(agentRunState, {
189
+ traceparent: run.traceparent,
188
190
  runId,
189
191
  agentName: run.agentName,
190
192
  threadId: run.threadId,
@@ -619,6 +621,7 @@ export async function streamAgent(agentName, input, channel, params, agentSessio
619
621
  return persistingChannel.fullText;
620
622
  }
621
623
  await postStreamCleanup(persistingChannel, agentRunState, runId, {
624
+ traceparent: outgoingTraceparent({ trace: params.trace }),
622
625
  agentName,
623
626
  threadId,
624
627
  resourceId: input.resourceId,
@@ -1019,6 +1022,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
1019
1022
  return;
1020
1023
  }
1021
1024
  await postStreamCleanup(persistingChannel, agentRunState, run.runId, {
1025
+ traceparent: outgoingTraceparent({ trace: params.trace }),
1022
1026
  ...run,
1023
1027
  input: lastUserMessageText(runnerParams.messages),
1024
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.