@pikku/core 0.12.136 → 0.12.138

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 (59) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/dev/hot-reload.js +5 -2
  3. package/dist/env.d.ts +18 -0
  4. package/dist/env.js +60 -1
  5. package/dist/function/abort-scope.d.ts +6 -5
  6. package/dist/function/abort-scope.js +6 -17
  7. package/dist/function/function-runner.js +2 -1
  8. package/dist/middleware/auth-apikey.js +1 -1
  9. package/dist/middleware/auth-bearer.js +1 -1
  10. package/dist/middleware/auth-cookie.js +1 -1
  11. package/dist/schema.js +3 -1
  12. package/dist/services/in-memory-queue-service.js +4 -2
  13. package/dist/services/in-memory-workflow-service.js +7 -6
  14. package/dist/services/index.d.ts +0 -1
  15. package/dist/services/index.js +0 -1
  16. package/dist/services/local-variables.d.ts +5 -0
  17. package/dist/services/local-variables.js +7 -2
  18. package/dist/services/meta-service.d.ts +11 -0
  19. package/dist/services/meta-service.js +10 -1
  20. package/dist/services/queue-webhook-service.js +1 -1
  21. package/dist/services/webhook-service.d.ts +2 -2
  22. package/dist/services/webhook-service.js +6 -4
  23. package/dist/types/core.types.d.ts +10 -1
  24. package/dist/utils/hmac.d.ts +25 -10
  25. package/dist/utils/hmac.js +202 -24
  26. package/dist/wirings/agent/agent-agui.js +65 -62
  27. package/dist/wirings/agent/agent-interrupt.d.ts +4 -1
  28. package/dist/wirings/agent/agent-interrupt.js +3 -3
  29. package/dist/wirings/agent/agent-memory.js +5 -6
  30. package/dist/wirings/agent/agent-model-config.js +2 -1
  31. package/dist/wirings/agent/agent-prepare.js +21 -13
  32. package/dist/wirings/agent/agent-stream.js +38 -34
  33. package/dist/wirings/agent/agent.types.d.ts +8 -1
  34. package/dist/wirings/channel/channel-handler.js +1 -1
  35. package/dist/wirings/channel/local/local-channel-handler.js +5 -1
  36. package/dist/wirings/channel/local/local-channel-runner.js +6 -4
  37. package/dist/wirings/channel/local/local-eventhub-service.d.ts +1 -0
  38. package/dist/wirings/channel/local/local-eventhub-service.js +4 -1
  39. package/dist/wirings/cli/channel/cli-channel-runner.js +7 -2
  40. package/dist/wirings/cli/channel/cli-raw-client-runner.js +8 -4
  41. package/dist/wirings/cli/cli-runner.js +1 -1
  42. package/dist/wirings/gateway/gateway-runner.js +3 -3
  43. package/dist/wirings/http/http-runner.js +6 -6
  44. package/dist/wirings/persona/persona-app-scopes.js +1 -0
  45. package/dist/wirings/scheduler/scheduler-runner.js +1 -1
  46. package/dist/wirings/scope/define-scope.d.ts +19 -0
  47. package/dist/wirings/scope/define-scope.js +19 -0
  48. package/dist/wirings/scope/index.d.ts +2 -2
  49. package/dist/wirings/scope/index.js +1 -1
  50. package/dist/wirings/scope/scope.types.d.ts +14 -0
  51. package/dist/wirings/scope/validate-scope-definitions.js +1 -0
  52. package/dist/wirings/trigger/trigger-runner.js +3 -1
  53. package/dist/wirings/trigger/webhook-source-runner.js +5 -2
  54. package/dist/wirings/virtual-user/virtual-user-scaffold.js +3 -2
  55. package/dist/wirings/workflow/scenario-coverage.d.ts +70 -0
  56. package/dist/wirings/workflow/scenario-coverage.js +159 -0
  57. package/dist/wirings/workflow/workflow-invocation-id.js +85 -8
  58. package/package.json +4 -1
  59. package/src/public-surface.json +10 -3
@@ -1,42 +1,220 @@
1
- import { createHmac, createVerify, timingSafeEqual } from 'node:crypto';
1
+ /**
2
+ * Web Crypto only: no `node:crypto` and no `Buffer`, so this loads on edge
3
+ * runtimes. Web Crypto is async, so everything that touches a key returns a
4
+ * Promise; `timingSafeStringEqual` is plain JS and stays synchronous.
5
+ */
6
+ const encoder = new TextEncoder();
7
+ const subtle = () => {
8
+ const s = globalThis.crypto?.subtle;
9
+ if (!s)
10
+ throw new Error('WebCrypto not available');
11
+ return s;
12
+ };
13
+ const bytesToHex = (bytes) => {
14
+ let out = '';
15
+ for (const b of bytes)
16
+ out += b.toString(16).padStart(2, '0');
17
+ return out;
18
+ };
19
+ const bytesToBase64 = (bytes) => {
20
+ let binary = '';
21
+ for (const b of bytes)
22
+ binary += String.fromCharCode(b);
23
+ return btoa(binary);
24
+ };
25
+ /** Lenient like Node's `Buffer.from(_, 'hex')`: stops at the first bad pair. */
26
+ const hexToBytes = (hex) => {
27
+ const out = [];
28
+ for (let i = 0; i + 1 < hex.length; i += 2) {
29
+ const pair = hex.slice(i, i + 2);
30
+ if (!/^[0-9a-fA-F]{2}$/.test(pair))
31
+ break;
32
+ out.push(parseInt(pair, 16));
33
+ }
34
+ return Uint8Array.from(out);
35
+ };
36
+ /** Lenient like Node's `Buffer.from(_, 'base64')`: accepts base64url, ignores junk. */
37
+ const base64ToBytes = (value) => {
38
+ let clean = value
39
+ .replace(/-/g, '+')
40
+ .replace(/_/g, '/')
41
+ .replace(/[^A-Za-z0-9+/]/g, '');
42
+ if (clean.length % 4 === 1)
43
+ clean = clean.slice(0, -1);
44
+ clean += '='.repeat((4 - (clean.length % 4)) % 4);
45
+ const binary = atob(clean);
46
+ const out = new Uint8Array(binary.length);
47
+ for (let i = 0; i < binary.length; i++)
48
+ out[i] = binary.charCodeAt(i);
49
+ return out;
50
+ };
51
+ /**
52
+ * Constant-time string comparison (pure JS; for opaque tokens, not HMACs, which
53
+ * `verifyHmacSignature` checks with Web Crypto). Returns false on a length mismatch
54
+ * (the lengths of the two values are not secret).
55
+ */
56
+ export function timingSafeStringEqual(a, b) {
57
+ const left = encoder.encode(a);
58
+ const right = encoder.encode(b);
59
+ if (left.length !== right.length)
60
+ return false;
61
+ let diff = 0;
62
+ for (let i = 0; i < left.length; i++)
63
+ diff |= left[i] ^ right[i];
64
+ return diff === 0;
65
+ }
66
+ const HMAC_HASH = {
67
+ sha1: 'SHA-1',
68
+ sha256: 'SHA-256',
69
+ sha512: 'SHA-512',
70
+ };
71
+ const rawSecretBytes = (secret, encoding) => encoding === 'hex'
72
+ ? hexToBytes(secret)
73
+ : encoding === 'base64'
74
+ ? base64ToBytes(secret)
75
+ : encoder.encode(secret);
76
+ const secretBytes = (secret, encoding) => {
77
+ const bytes = rawSecretBytes(secret, encoding);
78
+ // Web Crypto refuses a zero-length key. HMAC zero-pads the key to the block
79
+ // size, so a single zero byte is the same key as an empty one. Only signing
80
+ // reaches here: verification refuses an empty key outright.
81
+ return bytes.length ? bytes : new Uint8Array(1);
82
+ };
83
+ const hmacBytes = async (secret, algorithm, payload, secretEncoding = 'utf8') => {
84
+ const hash = HMAC_HASH[algorithm];
85
+ if (!hash)
86
+ throw new Error(`Unsupported HMAC algorithm '${algorithm}'`);
87
+ const key = await subtle().importKey('raw', secretBytes(secret, secretEncoding), { name: 'HMAC', hash }, false, ['sign']);
88
+ const data = typeof payload === 'string' ? encoder.encode(payload) : payload;
89
+ return new Uint8Array(await subtle().sign('HMAC', key, data));
90
+ };
2
91
  /**
3
92
  * HMAC-SHA256 of a payload, hex-encoded. Senders wrap this in their own scheme
4
93
  * prefix (`sha256=`, `v0=`, …).
5
94
  */
6
- export function hmacSha256Hex(secret, payload) {
7
- return createHmac('sha256', secret).update(payload).digest('hex');
95
+ export async function hmacSha256Hex(secret, payload) {
96
+ return bytesToHex(await hmacBytes(secret, 'sha256', payload));
97
+ }
98
+ /** The HMAC of `payload` under `secret`, for handshakes that answer with a digest. */
99
+ export async function hmacDigest(secret, algorithm, payload, encoding, secretEncoding = 'utf8') {
100
+ const digest = await hmacBytes(secret, algorithm, payload, secretEncoding);
101
+ return encoding === 'hex' ? bytesToHex(digest) : bytesToBase64(digest);
8
102
  }
9
103
  /**
10
- * Constant-time signature comparison. Returns false on a length mismatch,
11
- * where `timingSafeEqual` itself throws.
104
+ * Whether `signature` is the HMAC of `payload` under `secret`. Fails closed on
105
+ * an empty secret (or one that decodes to an empty key) and on a missing or
106
+ * malformed signature. The comparison is `crypto.subtle.verify('HMAC', ...)`,
107
+ * so it runs in the platform's constant-time implementation.
12
108
  */
13
- export function timingSafeStringEqual(a, b) {
109
+ export async function verifyHmacSignature(secret, signature, algorithm, payload, encoding, secretEncoding = 'utf8') {
110
+ if (!secret || !signature)
111
+ return false;
14
112
  try {
15
- return timingSafeEqual(Buffer.from(a), Buffer.from(b));
113
+ const hash = HMAC_HASH[algorithm];
114
+ if (!hash)
115
+ return false;
116
+ const keyBytes = rawSecretBytes(secret, secretEncoding);
117
+ if (!keyBytes.length)
118
+ return false;
119
+ if (encoding === 'hex' && !/^(?:[0-9a-fA-F]{2})+$/.test(signature)) {
120
+ return false;
121
+ }
122
+ const sig = encoding === 'hex' ? hexToBytes(signature) : base64ToBytes(signature);
123
+ if (!sig.length)
124
+ return false;
125
+ const key = await subtle().importKey('raw', keyBytes, { name: 'HMAC', hash }, false, ['verify']);
126
+ const data = typeof payload === 'string' ? encoder.encode(payload) : payload;
127
+ return await subtle().verify('HMAC', key, sig, data);
16
128
  }
17
129
  catch {
18
130
  return false;
19
131
  }
20
132
  }
21
- /** The HMAC of `payload` under `secret`, for handshakes that answer with a digest. */
22
- export function hmacDigest(secret, algorithm, payload, encoding, secretEncoding = 'utf8') {
23
- return createHmac(algorithm, Buffer.from(secret, secretEncoding))
24
- .update(payload)
25
- .digest(encoding);
26
- }
27
- /** Whether `signature` is the HMAC of `payload` under `secret`, compared in constant time. */
28
- export function verifyHmacSignature(secret, signature, algorithm, payload, encoding, secretEncoding = 'utf8') {
29
- return (!!signature &&
30
- timingSafeStringEqual(signature, hmacDigest(secret, algorithm, payload, encoding, secretEncoding)));
31
- }
32
- /** Whether `signature` (base64) is `payload` signed by the private half of `publicKey` (PEM). */
33
- export function verifyPublicKeySignature(publicKey, signature, payload, options = {}) {
34
- if (!signature)
133
+ // DER prefixes of the SPKI algorithm identifiers we can import.
134
+ const OID_RSA = '06092a864886f70d010101';
135
+ const OID_EC = '06072a8648ce3d0201';
136
+ const OID_ED25519 = '06032b6570';
137
+ const EC_CURVES = {
138
+ '06082a8648ce3d030107': { name: 'P-256', size: 32 },
139
+ '06052b81040022': { name: 'P-384', size: 48 },
140
+ '06052b81040023': { name: 'P-521', size: 66 },
141
+ };
142
+ const SIGNATURE_HASH = {
143
+ sha1: 'SHA-1',
144
+ sha256: 'SHA-256',
145
+ sha384: 'SHA-384',
146
+ sha512: 'SHA-512',
147
+ };
148
+ const pemToDer = (pem) => base64ToBytes(pem.replace(/-----[A-Z ]+-----/g, '').replace(/\s+/g, ''));
149
+ /** Reads an ASN.1 DER length at `offset`; returns [length, bytesUsed]. */
150
+ const readLength = (der, offset) => {
151
+ const first = der[offset];
152
+ if (first < 0x80)
153
+ return [first, 1];
154
+ const count = first & 0x7f;
155
+ let length = 0;
156
+ for (let i = 0; i < count; i++)
157
+ length = (length << 8) | der[offset + 1 + i];
158
+ return [length, 1 + count];
159
+ };
160
+ /** An ECDSA signature as DER `SEQUENCE { r, s }` to the fixed-width `r || s` Web Crypto expects. */
161
+ const derToP1363 = (der, size) => {
162
+ if (der[0] !== 0x30)
163
+ throw new Error('Invalid DER signature');
164
+ let offset = 1 + readLength(der, 1)[1];
165
+ const out = new Uint8Array(size * 2);
166
+ for (let part = 0; part < 2; part++) {
167
+ if (der[offset] !== 0x02)
168
+ throw new Error('Invalid DER signature');
169
+ const [length, used] = readLength(der, offset + 1);
170
+ let integer = der.slice(offset + 1 + used, offset + 1 + used + length);
171
+ while (integer.length > size && integer[0] === 0)
172
+ integer = integer.slice(1);
173
+ if (integer.length > size)
174
+ throw new Error('Invalid DER signature');
175
+ out.set(integer, part * size + (size - integer.length));
176
+ offset += 1 + used + length;
177
+ }
178
+ return out;
179
+ };
180
+ /**
181
+ * Whether `signature` (base64) is `payload` signed by the private half of
182
+ * `publicKey` (an RSA, EC or Ed25519 PEM `SPKI` key). RSA-PSS keys are not
183
+ * supported and verify as false. Ed25519 ignores `algorithm` and `dsaEncoding`.
184
+ */
185
+ export async function verifyPublicKeySignature(publicKey, signature, payload, options = {}) {
186
+ if (!publicKey || !signature)
35
187
  return false;
36
188
  try {
37
- return createVerify(options.algorithm ?? 'sha256')
38
- .update(payload)
39
- .verify({ key: publicKey, dsaEncoding: options.dsaEncoding }, signature, 'base64');
189
+ const der0 = pemToDer(publicKey);
190
+ if (bytesToHex(der0).includes(OID_ED25519)) {
191
+ const key = await subtle().importKey('spki', der0, { name: 'Ed25519' }, false, ['verify']);
192
+ return await subtle().verify({ name: 'Ed25519' }, key, base64ToBytes(signature), (typeof payload === 'string'
193
+ ? encoder.encode(payload)
194
+ : payload));
195
+ }
196
+ const hash = SIGNATURE_HASH[(options.algorithm ?? 'sha256')
197
+ .toLowerCase()
198
+ .replace(/^rsa-/, '')
199
+ .replace(/-/g, '')];
200
+ if (!hash)
201
+ return false;
202
+ const der = pemToDer(publicKey);
203
+ const hex = bytesToHex(der);
204
+ const data = typeof payload === 'string' ? encoder.encode(payload) : payload;
205
+ const sig = base64ToBytes(signature);
206
+ if (hex.includes(OID_RSA)) {
207
+ const key = await subtle().importKey('spki', der, { name: 'RSASSA-PKCS1-v1_5', hash }, false, ['verify']);
208
+ return await subtle().verify('RSASSA-PKCS1-v1_5', key, sig, data);
209
+ }
210
+ const curve = hex.includes(OID_EC)
211
+ ? Object.entries(EC_CURVES).find(([oid]) => hex.includes(oid))?.[1]
212
+ : undefined;
213
+ if (!curve)
214
+ return false;
215
+ const key = await subtle().importKey('spki', der, { name: 'ECDSA', namedCurve: curve.name }, false, ['verify']);
216
+ const raw = options.dsaEncoding === 'ieee-p1363' ? sig : derToP1363(sig, curve.size);
217
+ return await subtle().verify({ name: 'ECDSA', hash }, key, raw, data);
40
218
  }
41
219
  catch {
42
220
  return false;
@@ -27,59 +27,62 @@ export function wrapChannelWithAGUI(inner, options) {
27
27
  let sawUsage = false;
28
28
  let usageModel;
29
29
  const usageTotals = { input: 0, output: 0 };
30
- function send(event) {
30
+ async function send(event) {
31
31
  if (!runStartedSent) {
32
32
  runStartedSent = true;
33
- inner.send({
33
+ await inner.send({
34
34
  type: 'RUN_STARTED',
35
35
  threadId,
36
36
  runId: resolveRunId(),
37
37
  });
38
38
  }
39
- inner.send(event);
39
+ await inner.send(event);
40
40
  }
41
- function endTextMessage() {
41
+ async function endTextMessage() {
42
42
  if (textMessageId) {
43
- send({ type: 'TEXT_MESSAGE_END', messageId: textMessageId });
43
+ await send({ type: 'TEXT_MESSAGE_END', messageId: textMessageId });
44
44
  textMessageId = null;
45
45
  }
46
46
  }
47
- function endThinkingMessage() {
47
+ async function endThinkingMessage() {
48
48
  if (thinkingMessageId) {
49
- send({ type: 'THINKING_TEXT_MESSAGE_END', messageId: thinkingMessageId });
50
- send({ type: 'THINKING_END' });
49
+ await send({
50
+ type: 'THINKING_TEXT_MESSAGE_END',
51
+ messageId: thinkingMessageId,
52
+ });
53
+ await send({ type: 'THINKING_END' });
51
54
  thinkingMessageId = null;
52
55
  }
53
56
  }
54
- function endStep() {
57
+ async function endStep() {
55
58
  if (openStepName) {
56
- send({ type: 'STEP_FINISHED', stepName: openStepName });
59
+ await send({ type: 'STEP_FINISHED', stepName: openStepName });
57
60
  openStepName = null;
58
61
  }
59
62
  }
60
- function ensureTextMessage() {
63
+ async function ensureTextMessage() {
61
64
  if (!textMessageId) {
62
65
  textMessageId = randomUUID();
63
- send({ type: 'TEXT_MESSAGE_START', messageId: textMessageId });
66
+ await send({ type: 'TEXT_MESSAGE_START', messageId: textMessageId });
64
67
  }
65
68
  return textMessageId;
66
69
  }
67
- function ensureThinkingMessage() {
70
+ async function ensureThinkingMessage() {
68
71
  if (!thinkingMessageId) {
69
72
  thinkingMessageId = randomUUID();
70
- send({ type: 'THINKING_START' });
71
- send({
73
+ await send({ type: 'THINKING_START' });
74
+ await send({
72
75
  type: 'THINKING_TEXT_MESSAGE_START',
73
76
  messageId: thinkingMessageId,
74
77
  });
75
78
  }
76
79
  return thinkingMessageId;
77
80
  }
78
- function finishRun() {
79
- endTextMessage();
80
- endThinkingMessage();
81
- endStep();
82
- send({
81
+ async function finishRun() {
82
+ await endTextMessage();
83
+ await endThinkingMessage();
84
+ await endStep();
85
+ await send({
83
86
  type: 'RUN_FINISHED',
84
87
  threadId,
85
88
  runId: resolveRunId(),
@@ -110,14 +113,14 @@ export function wrapChannelWithAGUI(inner, options) {
110
113
  remote: (funcName, data) => inner.remote(funcName, data),
111
114
  sendBinary: (data) => inner.sendBinary(data),
112
115
  close: () => inner.close(),
113
- send: (event) => {
116
+ send: async (event) => {
114
117
  if (terminal)
115
118
  return;
116
119
  switch (event.type) {
117
120
  case 'text-delta': {
118
- endThinkingMessage();
119
- const id = ensureTextMessage();
120
- send({
121
+ await endThinkingMessage();
122
+ const id = await ensureTextMessage();
123
+ await send({
121
124
  type: 'TEXT_MESSAGE_CONTENT',
122
125
  messageId: id,
123
126
  delta: event.text,
@@ -125,9 +128,9 @@ export function wrapChannelWithAGUI(inner, options) {
125
128
  break;
126
129
  }
127
130
  case 'reasoning-delta': {
128
- endTextMessage();
129
- const id = ensureThinkingMessage();
130
- send({
131
+ await endTextMessage();
132
+ const id = await ensureThinkingMessage();
133
+ await send({
131
134
  type: 'THINKING_TEXT_MESSAGE_CONTENT',
132
135
  messageId: id,
133
136
  delta: event.text,
@@ -135,19 +138,19 @@ export function wrapChannelWithAGUI(inner, options) {
135
138
  break;
136
139
  }
137
140
  case 'tool-call': {
138
- endTextMessage();
139
- endThinkingMessage();
140
- send({
141
+ await endTextMessage();
142
+ await endThinkingMessage();
143
+ await send({
141
144
  type: 'TOOL_CALL_START',
142
145
  toolCallId: event.toolCallId,
143
146
  toolCallName: event.toolName,
144
147
  });
145
- send({
148
+ await send({
146
149
  type: 'TOOL_CALL_ARGS',
147
150
  toolCallId: event.toolCallId,
148
151
  delta: resultToString(event.args) || '{}',
149
152
  });
150
- send({
153
+ await send({
151
154
  type: 'TOOL_CALL_END',
152
155
  toolCallId: event.toolCallId,
153
156
  toolCallName: event.toolName,
@@ -155,7 +158,7 @@ export function wrapChannelWithAGUI(inner, options) {
155
158
  break;
156
159
  }
157
160
  case 'tool-result': {
158
- send({
161
+ await send({
159
162
  type: 'TOOL_CALL_RESULT',
160
163
  messageId: randomUUID(),
161
164
  toolCallId: event.toolCallId,
@@ -165,8 +168,8 @@ export function wrapChannelWithAGUI(inner, options) {
165
168
  break;
166
169
  }
167
170
  case 'usage': {
168
- endTextMessage();
169
- endThinkingMessage();
171
+ await endTextMessage();
172
+ await endThinkingMessage();
170
173
  sawUsage = true;
171
174
  usageTotals.input += event.tokens.input;
172
175
  usageTotals.output += event.tokens.output;
@@ -175,30 +178,30 @@ export function wrapChannelWithAGUI(inner, options) {
175
178
  break;
176
179
  }
177
180
  case 'error': {
178
- endTextMessage();
179
- endThinkingMessage();
180
- endStep();
181
- send({ type: 'RUN_ERROR', message: event.message });
181
+ await endTextMessage();
182
+ await endThinkingMessage();
183
+ await endStep();
184
+ await send({ type: 'RUN_ERROR', message: event.message });
182
185
  terminal = true;
183
186
  break;
184
187
  }
185
188
  case 'done': {
186
- finishRun();
189
+ await finishRun();
187
190
  break;
188
191
  }
189
192
  case 'step-start': {
190
- endTextMessage();
191
- endThinkingMessage();
192
- endStep();
193
+ await endTextMessage();
194
+ await endThinkingMessage();
195
+ await endStep();
193
196
  stepSeq += 1;
194
197
  openStepName = `${event.agent ?? 'step'}#${stepSeq}`;
195
- send({ type: 'STEP_STARTED', stepName: openStepName });
198
+ await send({ type: 'STEP_STARTED', stepName: openStepName });
196
199
  break;
197
200
  }
198
201
  case 'approval-request': {
199
- endTextMessage();
200
- endThinkingMessage();
201
- send({
202
+ await endTextMessage();
203
+ await endThinkingMessage();
204
+ await send({
202
205
  type: 'CUSTOM',
203
206
  name: 'pikku:approval-request',
204
207
  value: {
@@ -214,9 +217,9 @@ export function wrapChannelWithAGUI(inner, options) {
214
217
  break;
215
218
  }
216
219
  case 'credential-request': {
217
- endTextMessage();
218
- endThinkingMessage();
219
- send({
220
+ await endTextMessage();
221
+ await endThinkingMessage();
222
+ await send({
220
223
  type: 'CUSTOM',
221
224
  name: 'pikku:credential-request',
222
225
  value: {
@@ -234,7 +237,7 @@ export function wrapChannelWithAGUI(inner, options) {
234
237
  break;
235
238
  }
236
239
  case 'generative-ui': {
237
- send({
240
+ await send({
238
241
  type: 'CUSTOM',
239
242
  name: 'pikku:generative-ui',
240
243
  value: { spec: event.spec },
@@ -242,7 +245,7 @@ export function wrapChannelWithAGUI(inner, options) {
242
245
  break;
243
246
  }
244
247
  case 'data': {
245
- send({
248
+ await send({
246
249
  type: 'CUSTOM',
247
250
  name: 'pikku:data',
248
251
  value: { name: event.name, data: event.data },
@@ -250,7 +253,7 @@ export function wrapChannelWithAGUI(inner, options) {
250
253
  break;
251
254
  }
252
255
  case 'agent-call': {
253
- send({
256
+ await send({
254
257
  type: 'CUSTOM',
255
258
  name: 'pikku:agent-call',
256
259
  value: {
@@ -262,7 +265,7 @@ export function wrapChannelWithAGUI(inner, options) {
262
265
  break;
263
266
  }
264
267
  case 'agent-result': {
265
- send({
268
+ await send({
266
269
  type: 'CUSTOM',
267
270
  name: 'pikku:agent-result',
268
271
  value: {
@@ -274,7 +277,7 @@ export function wrapChannelWithAGUI(inner, options) {
274
277
  break;
275
278
  }
276
279
  case 'suspended': {
277
- send({
280
+ await send({
278
281
  type: 'CUSTOM',
279
282
  name: 'pikku:suspended',
280
283
  value: { reason: event.reason, missingRpcs: event.missingRpcs },
@@ -282,10 +285,10 @@ export function wrapChannelWithAGUI(inner, options) {
282
285
  break;
283
286
  }
284
287
  case 'interrupted': {
285
- endTextMessage();
286
- endThinkingMessage();
287
- endStep();
288
- send({
288
+ await endTextMessage();
289
+ await endThinkingMessage();
290
+ await endStep();
291
+ await send({
289
292
  type: 'CUSTOM',
290
293
  name: 'pikku:interrupted',
291
294
  value: {
@@ -298,7 +301,7 @@ export function wrapChannelWithAGUI(inner, options) {
298
301
  }
299
302
  // knowledge: decisions/internals/agent-speech-travels-as-a-custom-agui-event.md
300
303
  case 'audio-delta': {
301
- send({
304
+ await send({
302
305
  type: 'CUSTOM',
303
306
  name: 'pikku:audio-delta',
304
307
  value: {
@@ -310,11 +313,11 @@ export function wrapChannelWithAGUI(inner, options) {
310
313
  break;
311
314
  }
312
315
  case 'audio-done': {
313
- send({ type: 'CUSTOM', name: 'pikku:audio-done', value: {} });
316
+ await send({ type: 'CUSTOM', name: 'pikku:audio-done', value: {} });
314
317
  break;
315
318
  }
316
319
  case 'transcript': {
317
- send({
320
+ await send({
318
321
  type: 'CUSTOM',
319
322
  name: 'pikku:transcript',
320
323
  value: { text: event.text },
@@ -1,3 +1,4 @@
1
+ import { type AbortScope } from '../../function/abort-scope.js';
1
2
  import type { AgentStorageService } from '../../services/agent-storage-service.js';
2
3
  /**
3
4
  * Why an in-flight agent run was cut short.
@@ -126,7 +127,9 @@ export declare const getInFlightTools: (runId: string) => string[];
126
127
  export declare const trackToolExecution: <T extends {
127
128
  name: string;
128
129
  readonly?: boolean;
129
- execute: (input: any) => Promise<any>;
130
+ execute: (input: any, options?: {
131
+ abortScope?: AbortScope;
132
+ }) => Promise<any>;
130
133
  }>(tools: T[], handle: InterruptibleRunHandle) => T[];
131
134
  export declare const trackInterruptNote: (threadId: string, write: Promise<void>) => void;
132
135
  /** Await an interrupted tool result still being written to this thread. */
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from './agent-utils.js';
2
- import { AbandonedError, runInAbortScope, } from '../../function/abort-scope.js';
2
+ import { AbandonedError } from '../../function/abort-scope.js';
3
3
  /**
4
4
  * Thrown out of the step loop when a run is interrupted, so an interruption is
5
5
  * distinguishable from a genuine failure at the one `catch` that would
@@ -162,7 +162,7 @@ export const trackToolExecution = (tools, handle) => tools.map((tool) => {
162
162
  }
163
163
  return {
164
164
  ...tool,
165
- execute: (input) => {
165
+ execute: (input, options) => {
166
166
  // Assumed to have changed something unless it says otherwise — the tool
167
167
  // that never declares its checkpoint is the one that cannot be assumed
168
168
  // harmless. `beginChanges()` can only ever make this more precise.
@@ -178,7 +178,7 @@ export const trackToolExecution = (tools, handle) => tools.map((tool) => {
178
178
  call.mutating = true;
179
179
  },
180
180
  };
181
- return handle.trackTool(tool.name, randomUUID(), () => runInAbortScope(scope, () => tool.execute(input)), {
181
+ return handle.trackTool(tool.name, randomUUID(), () => tool.execute(input, { ...options, abortScope: scope }), {
182
182
  get collectResult() {
183
183
  return call.mutating;
184
184
  },
@@ -1,4 +1,3 @@
1
- import { randomUUID } from 'crypto';
2
1
  export function resolveMemoryServices(agent, singletonServices) {
3
2
  const memoryConfig = agent.memory;
4
3
  const storage = memoryConfig?.storage
@@ -83,7 +82,7 @@ export async function loadContextMessages(memoryConfig, storage, input, workingM
83
82
  const workingMem = await workingMemoryStorage.getWorkingMemory(input.threadId, 'thread');
84
83
  const prompt = buildWorkingMemoryPrompt(workingMem, workingMemoryJsonSchema);
85
84
  contextMessages.push({
86
- id: `wm-${randomUUID()}`,
85
+ id: `wm-${globalThis.crypto.randomUUID()}`,
87
86
  role: 'system',
88
87
  content: prompt,
89
88
  createdAt: new Date(),
@@ -99,9 +98,9 @@ export async function saveMessages(storage, threadId, resourceId, memoryConfig,
99
98
  const newMessages = userMessage ? [userMessage] : [];
100
99
  for (const step of result.steps) {
101
100
  if (step.toolCalls?.length) {
102
- const toolCallIds = step.toolCalls.map(() => randomUUID());
101
+ const toolCallIds = step.toolCalls.map(() => globalThis.crypto.randomUUID());
103
102
  newMessages.push({
104
- id: randomUUID(),
103
+ id: globalThis.crypto.randomUUID(),
105
104
  role: 'assistant',
106
105
  toolCalls: step.toolCalls.map((tc, i) => ({
107
106
  id: toolCallIds[i],
@@ -111,7 +110,7 @@ export async function saveMessages(storage, threadId, resourceId, memoryConfig,
111
110
  createdAt: new Date(),
112
111
  });
113
112
  newMessages.push({
114
- id: randomUUID(),
113
+ id: globalThis.crypto.randomUUID(),
115
114
  role: 'tool',
116
115
  toolResults: step.toolCalls.map((tc, i) => ({
117
116
  id: toolCallIds[i],
@@ -132,7 +131,7 @@ export async function saveMessages(storage, threadId, resourceId, memoryConfig,
132
131
  ]
133
132
  : responseText;
134
133
  newMessages.push({
135
- id: randomUUID(),
134
+ id: globalThis.crypto.randomUUID(),
136
135
  role: 'assistant',
137
136
  content: assistantContent || undefined,
138
137
  createdAt: new Date(),
@@ -1,9 +1,10 @@
1
1
  // knowledge: decisions/internals/agent-model-config-stays-a-single-resolution-seam.md
2
2
  import { pikkuState } from '../../pikku-state.js';
3
+ import { readEnvVariable } from '../../env.js';
3
4
  const isProviderQualified = (model) => model.includes('/');
4
5
  /** `PIKKU_MODEL_ALIASES=cheap:openai/gpt-5-mini,tool:anthropic/claude-sonnet-5` */
5
6
  const envAliases = () => {
6
- const raw = process.env.PIKKU_MODEL_ALIASES;
7
+ const raw = readEnvVariable('PIKKU_MODEL_ALIASES');
7
8
  if (!raw)
8
9
  return {};
9
10
  const aliases = {};