@pikku/core 0.12.148 → 0.12.151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/function/function-runner.js +23 -0
  2. package/dist/function/trace-queue-service.d.ts +26 -0
  3. package/dist/function/trace-queue-service.js +48 -0
  4. package/dist/mesh.d.ts +2 -2
  5. package/dist/mesh.js +3 -3
  6. package/dist/middleware/index.d.ts +1 -1
  7. package/dist/middleware/index.js +1 -1
  8. package/dist/middleware/mesh-auth.d.ts +3 -2
  9. package/dist/middleware/mesh-auth.js +7 -2
  10. package/dist/middleware/remote-jobs-secret.d.ts +1 -1
  11. package/dist/middleware/require-origin.d.ts +19 -0
  12. package/dist/middleware/require-origin.js +36 -2
  13. package/dist/services/deployment-service.d.ts +1 -1
  14. package/dist/services/incoming-webhook-service.d.ts +4 -2
  15. package/dist/services/incoming-webhook-service.js +4 -3
  16. package/dist/services/queue-webhook-service.js +7 -1
  17. package/dist/services/remote-jobs.d.ts +6 -1
  18. package/dist/services/scheduler-service.d.ts +3 -1
  19. package/dist/services/webhook-service.d.ts +2 -0
  20. package/dist/services/workflow-service.d.ts +4 -1
  21. package/dist/testing/index.d.ts +4 -0
  22. package/dist/testing/index.js +2 -0
  23. package/dist/testing/queue-identity-transport-tests.d.ts +18 -0
  24. package/dist/testing/queue-identity-transport-tests.js +162 -0
  25. package/dist/testing/queue-trace-conformance.d.ts +26 -0
  26. package/dist/testing/queue-trace-conformance.js +259 -0
  27. package/dist/testing/service-tests/channel-store-tests.js +11 -0
  28. package/dist/testing/service-tests/queued-workflow-harness.d.ts +18 -1
  29. package/dist/testing/service-tests/queued-workflow-harness.js +19 -3
  30. package/dist/testing/service-tests/workflow-service-tests.js +12 -0
  31. package/dist/testing/trace-matrix/assertions.d.ts +3 -0
  32. package/dist/testing/trace-matrix/assertions.js +190 -0
  33. package/dist/testing/trace-matrix/chain-harness.d.ts +119 -0
  34. package/dist/testing/trace-matrix/chain-harness.js +777 -0
  35. package/dist/testing/trace-matrix/dimensions.d.ts +55 -0
  36. package/dist/testing/trace-matrix/dimensions.js +305 -0
  37. package/dist/testing/trace-matrix/pairwise.d.ts +20 -0
  38. package/dist/testing/trace-matrix/pairwise.js +75 -0
  39. package/dist/testing/trace-matrix/report.d.ts +1 -0
  40. package/dist/testing/trace-matrix/report.js +16 -0
  41. package/dist/testing/trace-matrix/transports.d.ts +19 -0
  42. package/dist/testing/trace-matrix/transports.js +27 -0
  43. package/dist/testing/trace-matrix/workflow-cases.d.ts +24 -0
  44. package/dist/testing/trace-matrix/workflow-cases.js +147 -0
  45. package/dist/trace.d.ts +66 -0
  46. package/dist/trace.js +97 -0
  47. package/dist/types/core.types.d.ts +6 -2
  48. package/dist/wirings/addon/addon-reach.d.ts +3 -0
  49. package/dist/wirings/addon/addon-reach.js +9 -0
  50. package/dist/wirings/addon/define-plugin.d.ts +16 -0
  51. package/dist/wirings/addon/wire-addon.js +5 -0
  52. package/dist/wirings/addon/wire-remote-addon.d.ts +11 -0
  53. package/dist/wirings/agent/agent-finalize.d.ts +2 -0
  54. package/dist/wirings/agent/agent-finalize.js +1 -1
  55. package/dist/wirings/agent/agent-prepare.d.ts +12 -2
  56. package/dist/wirings/agent/agent-prepare.js +38 -14
  57. package/dist/wirings/agent/agent-rpc.js +2 -1
  58. package/dist/wirings/agent/agent-runner.js +5 -6
  59. package/dist/wirings/agent/agent-stream.js +6 -6
  60. package/dist/wirings/agent/voice-input.d.ts +1 -1
  61. package/dist/wirings/agent/voice-output.d.ts +1 -1
  62. package/dist/wirings/agent-scorer/agent-scorer-live.d.ts +4 -2
  63. package/dist/wirings/agent-scorer/agent-scorer-live.js +2 -2
  64. package/dist/wirings/channel/channel-common.d.ts +4 -1
  65. package/dist/wirings/channel/channel-common.js +3 -1
  66. package/dist/wirings/channel/channel-handler.js +2 -0
  67. package/dist/wirings/channel/channel-host-rpc.d.ts +1 -1
  68. package/dist/wirings/channel/channel-host-rpc.js +2 -2
  69. package/dist/wirings/channel/channel-rpc-responder.d.ts +7 -1
  70. package/dist/wirings/channel/channel-rpc-responder.js +2 -2
  71. package/dist/wirings/channel/channel-rpc-service.d.ts +1 -1
  72. package/dist/wirings/channel/channel-rpc-service.js +2 -2
  73. package/dist/wirings/channel/channel-rpc.types.d.ts +5 -2
  74. package/dist/wirings/channel/channel-rpc.types.js +1 -1
  75. package/dist/wirings/channel/channel-store.d.ts +4 -2
  76. package/dist/wirings/channel/channel-store.js +2 -2
  77. package/dist/wirings/channel/channel.types.d.ts +3 -0
  78. package/dist/wirings/channel/local/local-channel-runner.js +16 -5
  79. package/dist/wirings/channel/pikku-abstract-channel-handler.d.ts +2 -0
  80. package/dist/wirings/channel/pikku-abstract-channel-handler.js +3 -1
  81. package/dist/wirings/channel/serverless/serverless-channel-runner.js +27 -4
  82. package/dist/wirings/gateway/gateway-runner.js +3 -1
  83. package/dist/wirings/http/http-runner.d.ts +4 -4
  84. package/dist/wirings/http/http-runner.js +14 -18
  85. package/dist/wirings/http/http.types.d.ts +3 -3
  86. package/dist/wirings/mcp/mcp-runner.js +15 -5
  87. package/dist/wirings/queue/in-memory-queue-service.d.ts +1 -1
  88. package/dist/wirings/queue/in-memory-queue-service.js +8 -2
  89. package/dist/wirings/queue/index.d.ts +1 -0
  90. package/dist/wirings/queue/index.js +1 -0
  91. package/dist/wirings/queue/queue-runner.d.ts +3 -4
  92. package/dist/wirings/queue/queue-runner.js +8 -7
  93. package/dist/wirings/queue/queue-trace.d.ts +18 -0
  94. package/dist/wirings/queue/queue-trace.js +42 -0
  95. package/dist/wirings/queue/queue.types.d.ts +7 -0
  96. package/dist/wirings/queue/remote-queue-job.d.ts +1 -1
  97. package/dist/wirings/queue/remote-queue-job.js +5 -3
  98. package/dist/wirings/rpc/rpc-runner.d.ts +7 -1
  99. package/dist/wirings/rpc/rpc-runner.js +43 -21
  100. package/dist/wirings/rpc/rpc-types.d.ts +2 -0
  101. package/dist/wirings/scheduler/remote-scheduled-job.d.ts +1 -1
  102. package/dist/wirings/scheduler/remote-scheduled-job.js +2 -2
  103. package/dist/wirings/scheduler/scheduler-runner.d.ts +4 -4
  104. package/dist/wirings/scheduler/scheduler-runner.js +7 -6
  105. package/dist/wirings/trigger/pikku-trigger-service.js +7 -4
  106. package/dist/wirings/trigger/webhook-source-runner.d.ts +7 -1
  107. package/dist/wirings/trigger/webhook-source-runner.js +11 -3
  108. package/dist/wirings/workflow/graph/graph-runner.d.ts +1 -1
  109. package/dist/wirings/workflow/graph/graph-runner.js +49 -30
  110. package/dist/wirings/workflow/index.d.ts +3 -1
  111. package/dist/wirings/workflow/index.js +1 -0
  112. package/dist/wirings/workflow/pikku-scenario-service.js +2 -0
  113. package/dist/wirings/workflow/pikku-workflow-service.d.ts +37 -40
  114. package/dist/wirings/workflow/pikku-workflow-service.js +122 -133
  115. package/dist/wirings/workflow/workflow-child-step.d.ts +2 -2
  116. package/dist/wirings/workflow/workflow-child-step.js +11 -4
  117. package/dist/wirings/workflow/workflow-compensation.d.ts +6 -0
  118. package/dist/wirings/workflow/workflow-compensation.js +15 -1
  119. package/dist/wirings/workflow/workflow-constants.d.ts +13 -0
  120. package/dist/wirings/workflow/workflow-constants.js +13 -0
  121. package/dist/wirings/workflow/workflow-dsl-pass.d.ts +2 -0
  122. package/dist/wirings/workflow/workflow-dsl-pass.js +27 -11
  123. package/dist/wirings/workflow/workflow-recovery.d.ts +131 -3
  124. package/dist/wirings/workflow/workflow-recovery.js +233 -7
  125. package/dist/wirings/workflow/workflow-run-engine.types.d.ts +1 -0
  126. package/dist/wirings/workflow/workflow-run-mirror.d.ts +4 -0
  127. package/dist/wirings/workflow/workflow-run-mirror.js +16 -0
  128. package/dist/wirings/workflow/workflow-run-status.d.ts +2 -0
  129. package/dist/wirings/workflow/workflow-run-status.js +13 -0
  130. package/dist/wirings/workflow/workflow-run-trace.d.ts +22 -0
  131. package/dist/wirings/workflow/workflow-run-trace.js +24 -0
  132. package/dist/wirings/workflow/workflow-step-claim.d.ts +18 -0
  133. package/dist/wirings/workflow/workflow-step-claim.js +47 -0
  134. package/dist/wirings/workflow/workflow-step-retry.d.ts +6 -2
  135. package/dist/wirings/workflow/workflow-step-retry.js +8 -3
  136. package/dist/wirings/workflow/workflow-wire.d.ts +12 -0
  137. package/dist/wirings/workflow/workflow-wire.js +33 -0
  138. package/dist/wirings/workflow/workflow.types.d.ts +16 -0
  139. package/package.json +2 -1
@@ -0,0 +1,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
  *
@@ -22,6 +22,22 @@ type StudioScreenBase = {
22
22
  layout?: 'bare';
23
23
  /** Hides the screen and its nav for viewers without access. The addon's functions must enforce the same scopes: hiding a screen is not access control. */
24
24
  scopes?: string[];
25
+ /**
26
+ * The agent that answers in Studio's chat panel while this screen is open.
27
+ * `'studio:<name>'` names an agent Studio ships (e.g. `'studio:knowledge'`);
28
+ * `pikku all` only checks its format. A bare name is an agent the addon ships:
29
+ * it resolves to `agents/<name>.md` in the addon package: the agent's
30
+ * instructions, which name the skills it follows. Its frontmatter has a
31
+ * `title` and a `description`, both message keys in the addon's catalog.
32
+ * Lowercase letters, digits and hyphens, starting with a letter. Leave it out and the chat is unchanged.
33
+ */
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;
25
41
  } & ({
26
42
  /** `'studio'` gives `/<path>`, `'team'` gives `/<team>/<path>`. */
27
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);
@@ -1,8 +1,9 @@
1
1
  import type { CoreUserSession, GetCredential } from '../../types/core.types.js';
2
2
  import type { CoreAgent, AgentInput, AgentToolDef, AgentMessage, AgentStreamChannel, PikkuAgentMiddlewareHooks, SessionScope } from './agent.types.js';
3
- import type { AgentRunnerParams } from '../../services/agent-runner-service.js';
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;
@@ -155,11 +158,18 @@ export declare function buildToolDefs(params: RunAgentParams, agentSessionMap: M
155
158
  tools: AgentToolDef[];
156
159
  missingRpcs: string[];
157
160
  }>;
161
+ /**
162
+ * The singleton `agentRunner`, keyed with the caller's `AI_API_KEY` credential when one is set.
163
+ * Every entry that calls the model (run, stream, approve, resume) goes through this.
164
+ */
165
+ export declare function resolveAgentRunner(singletonServices: {
166
+ agentRunner?: AgentRunnerService;
167
+ }, params: RunAgentParams): Promise<AgentRunnerService>;
158
168
  export declare function prepareAgentRun(agentName: string, input: AgentInput, params: RunAgentParams, agentSessionMap: Map<string, string>, streamContext?: StreamContext): Promise<{
159
169
  agent: CoreAgent;
160
170
  packageName: string | null;
161
171
  resolvedName: string;
162
- agentRunner: import("../../services/agent-runner-service.js").AgentRunnerService;
172
+ agentRunner: AgentRunnerService;
163
173
  storage: import("../../services/agent-storage-service.js").AgentStorageService | undefined;
164
174
  memoryConfig: import("./agent.types.js").AgentMemoryConfig | undefined;
165
175
  threadId: 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
  };
@@ -679,20 +695,28 @@ export async function buildToolDefs(params, agentSessionMap, resourceId, agentNa
679
695
  }
680
696
  return { tools, missingRpcs };
681
697
  }
682
- export async function prepareAgentRun(agentName, input, params, agentSessionMap, streamContext) {
683
- const singletonServices = getSingletonServices();
684
- const { agent, packageName, resolvedName } = resolveAgent(agentName);
685
- await assertAgentAuthorized(agent, params, packageName);
686
- let agentRunner = singletonServices.agentRunner;
698
+ /**
699
+ * The singleton `agentRunner`, keyed with the caller's `AI_API_KEY` credential when one is set.
700
+ * Every entry that calls the model (run, stream, approve, resume) goes through this.
701
+ */
702
+ export async function resolveAgentRunner(singletonServices, params) {
703
+ const agentRunner = singletonServices.agentRunner;
687
704
  if (!agentRunner) {
688
705
  throw new AIProviderNotConfiguredError();
689
706
  }
690
707
  if (params.getCredential && agentRunner.withApiKey) {
691
708
  const aiCredential = await params.getCredential('AI_API_KEY');
692
709
  if (aiCredential?.apiKey?.trim()) {
693
- agentRunner = agentRunner.withApiKey(aiCredential.apiKey);
710
+ return agentRunner.withApiKey(aiCredential.apiKey);
694
711
  }
695
712
  }
713
+ return agentRunner;
714
+ }
715
+ export async function prepareAgentRun(agentName, input, params, agentSessionMap, streamContext) {
716
+ const singletonServices = getSingletonServices();
717
+ const { agent, packageName, resolvedName } = resolveAgent(agentName);
718
+ await assertAgentAuthorized(agent, params, packageName);
719
+ const agentRunner = await resolveAgentRunner(singletonServices, params);
696
720
  const { storage } = resolveMemoryServices(agent, singletonServices);
697
721
  const memoryConfig = agent.memory;
698
722
  const threadId = input.threadId;
@@ -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;
@@ -50,7 +51,7 @@ export const createAgentRPC = (wire, options) => {
50
51
  await resumeAgent({ runId, ...input }, wrapChannelWithAGUI(streamingChannel(), { runId }), params(), streamOptions);
51
52
  },
52
53
  interrupt: async (runId, reason) => interruptAgent({ runId, ...(reason ? { reason } : {}) }, { sessionService: options.sessionService }),
53
- approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAgentSync(runId, approvals, { sessionService: options.sessionService }, expectedAgentName === undefined
54
+ approve: async (runId, approvals, expectedAgentName) => asRunResult(await resumeAgentSync(runId, approvals, params(), expectedAgentName === undefined
54
55
  ? undefined
55
56
  : mapAddonCall(wire.addonNamespace, expectedAgentName))),
56
57
  };
@@ -1,5 +1,6 @@
1
+ import { outgoingTraceparent } from '../../trace.js';
1
2
  import { saveMessages, resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './agent-memory.js';
2
- import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, resolveOwnerResourceId, agentSessionScope, assertResourcePrincipalOwner, assertRunBelongsToAgent, assertAgentAuthorized, } from './agent-prepare.js';
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';
4
5
  import { AgentInterruptedError, isAbortError, persistOrphanedToolResults, registerInterruptibleRun, trackInterruptNote, trackToolExecution, } from './agent-interrupt.js';
5
6
  import { pikkuState, getSingletonServices } from '../../pikku-state.js';
@@ -7,7 +8,6 @@ import { applyInputMiddleware, describeApprovals, notifyAfterStep, toAccumulated
7
8
  import { applyOutputMiddleware, finalizeAgentRun, lastUserMessageText, } from './agent-finalize.js';
8
9
  import { resolveModelConfig } from './agent-model-config.js';
9
10
  import { validateAgentOutput } from './agent-output.js';
10
- import { AIProviderNotConfiguredError } from '../../errors/errors.js';
11
11
  import { deniedToolResult, randomUUID } from './agent-utils.js';
12
12
  function stripNulls(obj) {
13
13
  if (obj === null)
@@ -225,6 +225,7 @@ export async function runAgent(agentName, input, params, agentSessionMap) {
225
225
  uiSpec: structuredOutput.uiSpec,
226
226
  });
227
227
  await finalizeAgentRun(agentRunState, {
228
+ traceparent: outgoingTraceparent({ trace: params.trace }),
228
229
  runId,
229
230
  agentName,
230
231
  threadId,
@@ -308,10 +309,7 @@ export async function resumeAgentSync(runId, approvals, params, expectedAgentNam
308
309
  await assertAgentAuthorized(agent, params, packageName);
309
310
  const { storage } = resolveMemoryServices(agent, singletonServices);
310
311
  const memoryConfig = agent.memory;
311
- const agentRunner = singletonServices.agentRunner;
312
- if (!agentRunner) {
313
- throw new AIProviderNotConfiguredError();
314
- }
312
+ const agentRunner = await resolveAgentRunner(singletonServices, params);
315
313
  const approvedIds = new Set(approvals.filter((a) => a.approved).map((a) => a.toolCallId));
316
314
  const rejectedIds = new Set(approvals.filter((a) => !a.approved).map((a) => a.toolCallId));
317
315
  const savedPendingApprovals = [...(run.pendingApprovals ?? [])];
@@ -561,6 +559,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
561
559
  steps: accumulatedSteps,
562
560
  });
563
561
  await finalizeAgentRun(agentRunState, {
562
+ traceparent: outgoingTraceparent({ trace: params.trace }),
564
563
  runId: run.runId,
565
564
  agentName: resolvedName,
566
565
  threadId: run.threadId,