@pikku/core 0.12.74 → 0.12.77
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.
- package/CHANGELOG.md +378 -0
- package/dist/column-form.d.ts +32 -0
- package/dist/column-form.js +42 -0
- package/dist/crypto-utils.d.ts +15 -4
- package/dist/crypto-utils.js +18 -2
- package/dist/data-classification.d.ts +44 -0
- package/dist/function/functions.types.d.ts +23 -10
- package/dist/function/index.d.ts +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +3 -1
- package/dist/middleware/auth-bearer.js +2 -1
- package/dist/middleware/remote-auth.js +1 -1
- package/dist/remote.js +1 -1
- package/dist/secret-value.d.ts +56 -0
- package/dist/secret-value.js +46 -0
- package/dist/services/audit-service.d.ts +74 -4
- package/dist/services/audit-service.js +7 -5
- package/dist/services/credential-wire-service.d.ts +5 -0
- package/dist/services/credential-wire-service.js +9 -1
- package/dist/services/email-service.d.ts +2 -1
- package/dist/services/index.d.ts +3 -3
- package/dist/services/index.js +1 -1
- package/dist/services/local-content-request-handler.d.ts +29 -0
- package/dist/services/local-content-request-handler.js +176 -0
- package/dist/services/local-secrets.d.ts +4 -3
- package/dist/services/local-secrets.js +7 -3
- package/dist/services/logger.d.ts +22 -5
- package/dist/services/queue-webhook-service.js +1 -1
- package/dist/services/scoped-secret-service.d.ts +4 -3
- package/dist/services/secret-service.d.ts +8 -3
- package/dist/services/typed-secret-service.d.ts +5 -4
- package/dist/services/webhook-service.d.ts +2 -1
- package/dist/testing/service-tests.js +6 -6
- package/dist/types/core.types.d.ts +25 -4
- package/dist/wirings/ai-agent/ai-agent-agui.js +13 -1
- package/dist/wirings/ai-agent/ai-agent-prepare.js +7 -1
- package/dist/wirings/ai-agent/ai-agent-runner.js +14 -2
- package/dist/wirings/ai-agent/ai-agent-stream.js +27 -1
- package/dist/wirings/ai-agent/ai-agent.types.d.ts +40 -0
- package/dist/wirings/ai-agent/index.d.ts +1 -1
- package/dist/wirings/ai-agent/index.js +1 -1
- package/dist/wirings/ai-agent/voice-input.d.ts +20 -0
- package/dist/wirings/ai-agent/voice-input.js +44 -9
- package/dist/wirings/ai-agent/voice-output.d.ts +15 -0
- package/dist/wirings/ai-agent/voice-output.js +10 -1
- package/dist/wirings/cli/channel/cli-raw-client-runner.d.ts +21 -3
- package/dist/wirings/cli/channel/cli-raw-client-runner.js +13 -5
- package/dist/wirings/cli/channel/index.d.ts +1 -0
- package/dist/wirings/persona/define-personas.d.ts +4 -0
- package/dist/wirings/persona/define-personas.js +4 -0
- package/dist/wirings/persona/persona.types.d.ts +11 -0
- package/dist/wirings/queue/queue-identity.js +2 -1
- package/dist/wirings/queue/queue.types.d.ts +2 -1
- package/dist/wirings/queue/signed-queue-service.d.ts +2 -1
- package/dist/wirings/rpc/remote-addon-auth.d.ts +2 -1
- package/dist/wirings/rpc/remote-addon-auth.js +6 -2
- package/dist/wirings/virtual-user/index.d.ts +3 -0
- package/dist/wirings/virtual-user/index.js +2 -0
- package/dist/wirings/virtual-user/prepare-virtual-user-run.d.ts +54 -0
- package/dist/wirings/virtual-user/prepare-virtual-user-run.js +49 -0
- package/dist/wirings/virtual-user/virtual-user-run-store.d.ts +90 -0
- package/dist/wirings/virtual-user/virtual-user-run-store.js +1 -0
- package/dist/wirings/workflow/dsl/workflow-dsl.types.d.ts +14 -10
- package/dist/wirings/workflow/pikku-scenario-service.js +1 -2
- package/dist/wirings/workflow/scenario-prose.js +1 -1
- package/dist/wirings/workflow/scenario-step.types.d.ts +13 -7
- package/dist/wirings/workflow/workflow.types.d.ts +7 -0
- package/knowledge/decisions/internals/a-virtual-user-run-is-not-a-workflow-and-not-a-queued-job.md +48 -0
- package/knowledge/decisions/internals/core-column-form-is-an-axis-of-its-own.md +84 -0
- package/knowledge/decisions/internals/core-data-classification-brand-is-an-optional-property.md +9 -2
- package/knowledge/decisions/internals/index.md +4 -0
- package/knowledge/decisions/internals/one-project-shape-check-two-validators.md +53 -0
- package/knowledge/decisions/internals/scenarios-live-in-files-named-for-them.md +48 -0
- package/knowledge/decisions/internals/validate-checks-personas-through-a-shared-module.md +43 -0
- package/package.json +3 -2
- package/src/column-form.test.ts +97 -0
- package/src/column-form.ts +58 -0
- package/src/crypto-utils.ts +25 -6
- package/src/data-classification.ts +44 -0
- package/src/function/functions.types.ts +47 -10
- package/src/function/index.ts +1 -0
- package/src/index.ts +24 -2
- package/src/middleware/auth-bearer.test.ts +3 -2
- package/src/middleware/auth-bearer.ts +2 -1
- package/src/middleware/remote-auth.test.ts +2 -1
- package/src/middleware/remote-auth.ts +1 -1
- package/src/remote.test.ts +2 -1
- package/src/remote.ts +1 -1
- package/src/secret-value.test.ts +204 -0
- package/src/secret-value.ts +111 -0
- package/src/services/audit-service.ts +87 -9
- package/src/services/credential-wire-service.ts +9 -1
- package/src/services/email-service.ts +3 -1
- package/src/services/index.ts +3 -3
- package/src/services/local-content-request-handler.test.ts +202 -0
- package/src/services/local-content-request-handler.ts +267 -0
- package/src/services/local-secrets.test.ts +20 -5
- package/src/services/local-secrets.ts +15 -7
- package/src/services/logger.ts +27 -7
- package/src/services/queue-webhook-service.test.ts +2 -1
- package/src/services/queue-webhook-service.ts +1 -1
- package/src/services/scoped-secret-service.ts +4 -3
- package/src/services/secret-service.ts +8 -3
- package/src/services/typed-secret-service.ts +11 -7
- package/src/services/webhook-service.ts +4 -1
- package/src/testing/service-tests.ts +6 -6
- package/src/types/core.types.ts +25 -4
- package/src/wirings/ai-agent/ai-agent-agui.test.ts +16 -0
- package/src/wirings/ai-agent/ai-agent-agui.ts +14 -1
- package/src/wirings/ai-agent/ai-agent-prepare.ts +7 -1
- package/src/wirings/ai-agent/ai-agent-runner.ts +18 -2
- package/src/wirings/ai-agent/ai-agent-stream.ts +32 -1
- package/src/wirings/ai-agent/ai-agent.types.ts +45 -1
- package/src/wirings/ai-agent/index.ts +2 -0
- package/src/wirings/ai-agent/voice-input.test.ts +65 -0
- package/src/wirings/ai-agent/voice-input.ts +48 -9
- package/src/wirings/ai-agent/voice-output.test.ts +91 -1
- package/src/wirings/ai-agent/voice-output.ts +28 -1
- package/src/wirings/cli/channel/cli-raw-client-runner.ts +39 -9
- package/src/wirings/cli/channel/index.ts +4 -0
- package/src/wirings/persona/define-personas.ts +4 -0
- package/src/wirings/persona/persona.types.ts +11 -0
- package/src/wirings/queue/queue-identity.test.ts +2 -1
- package/src/wirings/queue/queue-identity.ts +4 -1
- package/src/wirings/queue/queue.types.ts +6 -1
- package/src/wirings/queue/signed-queue-service.ts +2 -1
- package/src/wirings/rpc/remote-addon-auth.ts +8 -3
- package/src/wirings/rpc/rpc-runner.test.ts +6 -4
- package/src/wirings/virtual-user/index.ts +12 -0
- package/src/wirings/virtual-user/prepare-virtual-user-run.test.ts +115 -0
- package/src/wirings/virtual-user/prepare-virtual-user-run.ts +95 -0
- package/src/wirings/virtual-user/virtual-user-run-store.ts +98 -0
- package/src/wirings/workflow/dsl/workflow-dsl.types.ts +14 -16
- package/src/wirings/workflow/pikku-scenario-service.ts +1 -10
- package/src/wirings/workflow/scenario-prose.test.ts +5 -7
- package/src/wirings/workflow/scenario-prose.ts +1 -1
- package/src/wirings/workflow/scenario-service.test.ts +0 -1
- package/src/wirings/workflow/scenario-step.test.ts +4 -5
- package/src/wirings/workflow/scenario-step.types.ts +13 -7
- package/src/wirings/workflow/scenario-surface.test.ts +6 -5
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.type-tests.json +12 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSecretValue, isSecretValue, } from '../secret-value.js';
|
|
1
2
|
import { LocalVariablesService } from './local-variables.js';
|
|
2
3
|
export class LocalSecretService {
|
|
3
4
|
variables;
|
|
@@ -16,16 +17,19 @@ export class LocalSecretService {
|
|
|
16
17
|
async getSecret(key) {
|
|
17
18
|
const localValue = this.localSecrets.get(key);
|
|
18
19
|
if (localValue) {
|
|
19
|
-
return this.parseSecret(localValue);
|
|
20
|
+
return createSecretValue(this.parseSecret(localValue));
|
|
20
21
|
}
|
|
21
22
|
const value = await this.variables.get(key);
|
|
22
23
|
if (value) {
|
|
23
|
-
return this.parseSecret(value);
|
|
24
|
+
return createSecretValue(this.parseSecret(value));
|
|
24
25
|
}
|
|
25
26
|
throw new Error('Requested secret not found');
|
|
26
27
|
}
|
|
27
28
|
async setSecret(key, value) {
|
|
28
|
-
|
|
29
|
+
// Storing the wrapper would serialize it to '[secret]', so unwrap first —
|
|
30
|
+
// writing a secret back to the vault is exactly what this method is for.
|
|
31
|
+
const raw = isSecretValue(value) ? value.reveal() : value;
|
|
32
|
+
this.localSecrets.set(key, typeof raw === 'string' ? raw : JSON.stringify(raw));
|
|
29
33
|
}
|
|
30
34
|
async hasSecret(key) {
|
|
31
35
|
if (this.localSecrets.has(key)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Safe } from '../secret-value.js';
|
|
1
2
|
export declare enum LogLevel {
|
|
2
3
|
'trace' = 0,
|
|
3
4
|
'debug' = 1,
|
|
@@ -6,12 +7,28 @@ export declare enum LogLevel {
|
|
|
6
7
|
'error' = 4,
|
|
7
8
|
'critical' = 5
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* A log line is the easiest place to leak a vault secret, so every parameter is
|
|
12
|
+
* `Safe<>`-guarded: a `SecretValue` anywhere in the message or the metadata,
|
|
13
|
+
* however deeply nested, collapses to `never` and fails the build. Reveal it
|
|
14
|
+
* first if you genuinely mean to log it.
|
|
15
|
+
*/
|
|
9
16
|
export interface Logger {
|
|
10
|
-
info
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
info<M extends string | Record<string, any>, A extends unknown[]>(messageOrObj: Safe<M>, ...meta: {
|
|
18
|
+
[K in keyof A]: Safe<A[K]>;
|
|
19
|
+
}): void;
|
|
20
|
+
warn<M extends string | Record<string, any>, A extends unknown[]>(messageOrObj: Safe<M>, ...meta: {
|
|
21
|
+
[K in keyof A]: Safe<A[K]>;
|
|
22
|
+
}): void;
|
|
23
|
+
error<M extends string | Record<string, any> | Error, A extends unknown[]>(messageOrObj: Safe<M>, ...meta: {
|
|
24
|
+
[K in keyof A]: Safe<A[K]>;
|
|
25
|
+
}): void;
|
|
26
|
+
debug<A extends unknown[]>(message: string, ...meta: {
|
|
27
|
+
[K in keyof A]: Safe<A[K]>;
|
|
28
|
+
}): void;
|
|
29
|
+
trace?<A extends unknown[]>(message: string, ...meta: {
|
|
30
|
+
[K in keyof A]: Safe<A[K]>;
|
|
31
|
+
}): void;
|
|
15
32
|
setLevel(level: LogLevel): void;
|
|
16
33
|
/** A logger carrying a traceId on every entry, taken per-request to correlate calls. */
|
|
17
34
|
scope?(traceId: string): Logger;
|
|
@@ -32,7 +32,7 @@ export class QueueWebhookService extends WebhookService {
|
|
|
32
32
|
let secret = input.secret;
|
|
33
33
|
if (secret === undefined && webhookConfig?.secret) {
|
|
34
34
|
// Naive read: caching is the secret service's concern, not ours.
|
|
35
|
-
secret = await services.secrets.getSecret(webhookConfig.secret);
|
|
35
|
+
secret = (await services.secrets.getSecret(webhookConfig.secret)).reveal();
|
|
36
36
|
if (!secret) {
|
|
37
37
|
services.logger.error(`Webhook signing secret '${webhookConfig.secret}' (config.webhook.secret) resolved to nothing — outgoing webhooks will be sent UNSIGNED.`);
|
|
38
38
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SecretValue } from '../secret-value.js';
|
|
2
|
+
import type { SecretService, SecretValues } from './secret-service.js';
|
|
2
3
|
export declare class ScopedSecretService implements SecretService {
|
|
3
4
|
private secrets;
|
|
4
5
|
private allowedKeys;
|
|
5
6
|
constructor(secrets: SecretService, allowedKeys: Set<string>);
|
|
6
7
|
private assertAllowed;
|
|
7
|
-
getSecret<T = string>(key: string): Promise<T
|
|
8
|
+
getSecret<T = string>(key: string): Promise<SecretValue<T>>;
|
|
8
9
|
hasSecret(key: string): Promise<boolean>;
|
|
9
10
|
setSecret(_key: string, _value: unknown): Promise<void>;
|
|
10
11
|
deleteSecret(_key: string): Promise<void>;
|
|
11
|
-
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<Partial<T
|
|
12
|
+
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<Partial<SecretValues<T>>>;
|
|
12
13
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import type { SecretValue } from '../secret-value.js';
|
|
2
|
+
/** A record of secrets, each still wrapped. */
|
|
3
|
+
export type SecretValues<T> = {
|
|
4
|
+
[K in keyof T]: SecretValue<T[K]>;
|
|
5
|
+
};
|
|
1
6
|
export interface SecretService {
|
|
2
|
-
/** Throws if the secret is not found. */
|
|
3
|
-
getSecret<T = string>(key: string): Promise<T
|
|
7
|
+
/** Throws if the secret is not found. Unwrap the result with `.reveal()`. */
|
|
8
|
+
getSecret<T = string>(key: string): Promise<SecretValue<T>>;
|
|
4
9
|
/** Answers for any key, including a disallowed one — it must not throw. */
|
|
5
10
|
hasSecret(key: string): Promise<boolean>;
|
|
6
11
|
setSecret(key: string, value: unknown): Promise<void>;
|
|
@@ -10,5 +15,5 @@ export interface SecretService {
|
|
|
10
15
|
* must handle keys absent at runtime. Pass a shape as `T` to avoid casting,
|
|
11
16
|
* e.g. `getSecrets<{ FOO: string; BAR: { id: string } }>(['FOO', 'BAR'])`.
|
|
12
17
|
*/
|
|
13
|
-
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<Partial<T
|
|
18
|
+
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<Partial<SecretValues<T>>>;
|
|
14
19
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SecretValue } from '../secret-value.js';
|
|
2
|
+
import type { SecretService, SecretValues } from './secret-service.js';
|
|
2
3
|
export interface CredentialStatus {
|
|
3
4
|
secretId: string;
|
|
4
5
|
name: string;
|
|
@@ -20,12 +21,12 @@ export declare class TypedSecretService<TMap = Record<string, unknown>> implemen
|
|
|
20
21
|
private credentialsMeta;
|
|
21
22
|
private cache;
|
|
22
23
|
constructor(secrets: SecretService, credentialsMeta: Record<string, CredentialMeta>);
|
|
23
|
-
getSecret<K extends keyof TMap & string>(key: K): Promise<TMap[K]
|
|
24
|
-
getSecret<T = string>(key: string): Promise<T
|
|
24
|
+
getSecret<K extends keyof TMap & string>(key: K): Promise<SecretValue<TMap[K]>>;
|
|
25
|
+
getSecret<T = string>(key: string): Promise<SecretValue<T>>;
|
|
25
26
|
hasSecret(key: string): Promise<boolean>;
|
|
26
27
|
setSecret<K extends string>(key: K, value: K extends keyof TMap ? TMap[K] : unknown): Promise<void>;
|
|
27
28
|
deleteSecret(key: string): Promise<void>;
|
|
28
|
-
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<Partial<T
|
|
29
|
+
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<Partial<SecretValues<T>>>;
|
|
29
30
|
getAllStatus(): Promise<CredentialStatus[]>;
|
|
30
31
|
getMissing(): Promise<CredentialStatus[]>;
|
|
31
32
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Safe } from '../secret-value.js';
|
|
1
2
|
export interface SendWebhookInput {
|
|
2
3
|
url: string;
|
|
3
4
|
/** Sent as the `X-Pikku-Event` header. */
|
|
@@ -76,7 +77,7 @@ export declare const PIKKU_OUTGOING_WEBHOOK_QUEUE_NAME = "pikku-outgoing-webhook
|
|
|
76
77
|
export declare const DEFAULT_WEBHOOK_SIGNATURE_HEADER = "X-Pikku-Signature";
|
|
77
78
|
export declare const DEFAULT_WEBHOOK_RETRIES = 3;
|
|
78
79
|
export declare abstract class WebhookService {
|
|
79
|
-
abstract send(input:
|
|
80
|
+
abstract send<T extends SendWebhookInput>(input: Safe<T>): Promise<SendWebhookResult>;
|
|
80
81
|
/** Produces the header value, `sha256=<hex>`, not the bare digest. */
|
|
81
82
|
protected sign(secret: string, body: string): string;
|
|
82
83
|
/** Public because receivers verify with it; they share the signing scheme. */
|
|
@@ -542,16 +542,16 @@ export function defineServiceTests(config) {
|
|
|
542
542
|
endpoint: 'https://api.example.com',
|
|
543
543
|
});
|
|
544
544
|
const result = await service.getSecret('api-key');
|
|
545
|
-
assert.deepEqual(result, {
|
|
545
|
+
assert.deepEqual(result.reveal(), {
|
|
546
546
|
token: 'sk-123',
|
|
547
547
|
endpoint: 'https://api.example.com',
|
|
548
548
|
});
|
|
549
549
|
});
|
|
550
|
-
test('getSecret
|
|
550
|
+
test('getSecret wraps the raw string rather than returning it', async () => {
|
|
551
551
|
const service = await factory({ key: kek });
|
|
552
552
|
await service.setSecret('string-secret', 'plain-value');
|
|
553
553
|
const result = await service.getSecret('string-secret');
|
|
554
|
-
assert.strictEqual(result, 'plain-value');
|
|
554
|
+
assert.strictEqual(result.reveal(), 'plain-value');
|
|
555
555
|
});
|
|
556
556
|
test('hasSecret returns true/false', async () => {
|
|
557
557
|
const service = await factory({ key: kek });
|
|
@@ -569,7 +569,7 @@ export function defineServiceTests(config) {
|
|
|
569
569
|
await service.setSecret('upsert-key', { v: 1 });
|
|
570
570
|
await service.setSecret('upsert-key', { v: 2 });
|
|
571
571
|
const result = await service.getSecret('upsert-key');
|
|
572
|
-
assert.deepEqual(result, { v: 2 });
|
|
572
|
+
assert.deepEqual(result.reveal(), { v: 2 });
|
|
573
573
|
});
|
|
574
574
|
test('deleteSecret removes the key', async () => {
|
|
575
575
|
const service = await factory({ key: kek });
|
|
@@ -588,7 +588,7 @@ export function defineServiceTests(config) {
|
|
|
588
588
|
previousKey: kek,
|
|
589
589
|
});
|
|
590
590
|
const before = await rotatedService.getSecret('rotate-test');
|
|
591
|
-
assert.deepEqual(before, { important: 'data' });
|
|
591
|
+
assert.deepEqual(before.reveal(), { important: 'data' });
|
|
592
592
|
assert.ok(rotatedService.rotateKEK);
|
|
593
593
|
const count = await rotatedService.rotateKEK();
|
|
594
594
|
assert.ok(count > 0);
|
|
@@ -597,7 +597,7 @@ export function defineServiceTests(config) {
|
|
|
597
597
|
keyVersion: 2,
|
|
598
598
|
});
|
|
599
599
|
const after = await newOnlyService.getSecret('rotate-test');
|
|
600
|
-
assert.deepEqual(after, { important: 'data' });
|
|
600
|
+
assert.deepEqual(after.reveal(), { important: 'data' });
|
|
601
601
|
});
|
|
602
602
|
test('rotateKEK throws without previousKey', async () => {
|
|
603
603
|
const service = await factory({ key: kek });
|
|
@@ -25,6 +25,7 @@ import type { AIAgentRunnerService } from '../services/ai-agent-runner-service.j
|
|
|
25
25
|
import type { AIEmbeddingService } from '../services/ai-embedding-service.js';
|
|
26
26
|
import type { AIRunStateService } from '../services/ai-run-state-service.js';
|
|
27
27
|
import type { AgentRunService } from '../wirings/ai-agent/ai-agent.types.js';
|
|
28
|
+
import type { VirtualUserRunStore } from '../wirings/virtual-user/virtual-user-run-store.js';
|
|
28
29
|
import type { PikkuAIMiddlewareHooks } from '../wirings/ai-agent/ai-agent.types.js';
|
|
29
30
|
import type { WorkflowRunService } from '../wirings/workflow/workflow.types.js';
|
|
30
31
|
import type { CredentialService } from '../services/credential-service.js';
|
|
@@ -79,11 +80,12 @@ export type FunctionRuntimeMeta = {
|
|
|
79
80
|
*/
|
|
80
81
|
auth?: boolean;
|
|
81
82
|
/**
|
|
82
|
-
* The author's declaration that this function
|
|
83
|
-
* body. Carries no runtime effect — it records a gate codegen cannot see,
|
|
84
|
-
* an audit is not left to guess whether a sessionless function is open.
|
|
83
|
+
* The author's declaration that this function's permission check lives in its
|
|
84
|
+
* own body. Carries no runtime effect — it records a gate codegen cannot see,
|
|
85
|
+
* so an audit is not left to guess whether a sessionless function is open.
|
|
86
|
+
* Refused unless `allow.permissionsInBody` is set in `pikku.config.json`.
|
|
85
87
|
*/
|
|
86
|
-
|
|
88
|
+
permissionsInBody?: boolean;
|
|
87
89
|
remote?: boolean;
|
|
88
90
|
/**
|
|
89
91
|
* A step RPC: invoked by name only from a scenario run and refused
|
|
@@ -127,6 +129,19 @@ export type FunctionRuntimeMeta = {
|
|
|
127
129
|
scenarioStepAddon?: string;
|
|
128
130
|
/** Scenario steps only: the prose a reporter renders, with `{placeholders}` filled from the step's recorded input. */
|
|
129
131
|
scenarioStepTemplate?: string;
|
|
132
|
+
/**
|
|
133
|
+
* The function's `audit` config, resolved — `audit: true` reads as
|
|
134
|
+
* `{ durability: 'best-effort' }`. Absent means the function records nothing:
|
|
135
|
+
* `auditLog.write()` from an unmarked function is dropped with a warning, so
|
|
136
|
+
* this is the only place a reader can see which functions have a trail at all
|
|
137
|
+
* without running them.
|
|
138
|
+
*
|
|
139
|
+
* Informational. The runner resolves audit from the live function config, not
|
|
140
|
+
* from here, so meta and runtime cannot disagree about whether audit is on.
|
|
141
|
+
*/
|
|
142
|
+
audit?: {
|
|
143
|
+
durability: AuditDurability;
|
|
144
|
+
};
|
|
130
145
|
/** Keeps the full `SecretService`. Set by the inspector, read by the runner. */
|
|
131
146
|
secretBroker?: boolean;
|
|
132
147
|
version?: number;
|
|
@@ -255,6 +270,12 @@ export interface CoreSingletonServices<Config extends CoreConfig = CoreConfig> {
|
|
|
255
270
|
*/
|
|
256
271
|
webhookService?: WebhookService;
|
|
257
272
|
metaService?: MetaService;
|
|
273
|
+
/**
|
|
274
|
+
* Where virtual-user runs are recorded. A run is dispatched and answered for
|
|
275
|
+
* later, so this store is the only trace it leaves — see
|
|
276
|
+
* {@link VirtualUserRunStore}.
|
|
277
|
+
*/
|
|
278
|
+
virtualUserRunStore?: VirtualUserRunStore;
|
|
258
279
|
/** V8 precise-coverage collector (`pikku dev --coverage` only) */
|
|
259
280
|
coverageService?: CoverageService;
|
|
260
281
|
audit?: AuditService;
|
|
@@ -305,7 +305,11 @@ export function wrapChannelWithAGUI(inner, options) {
|
|
|
305
305
|
send({
|
|
306
306
|
type: 'CUSTOM',
|
|
307
307
|
name: 'pikku:audio-delta',
|
|
308
|
-
value: {
|
|
308
|
+
value: {
|
|
309
|
+
data: event.data,
|
|
310
|
+
format: event.format,
|
|
311
|
+
...(event.text === undefined ? {} : { text: event.text }),
|
|
312
|
+
},
|
|
309
313
|
});
|
|
310
314
|
break;
|
|
311
315
|
}
|
|
@@ -313,6 +317,14 @@ export function wrapChannelWithAGUI(inner, options) {
|
|
|
313
317
|
send({ type: 'CUSTOM', name: 'pikku:audio-done', value: {} });
|
|
314
318
|
break;
|
|
315
319
|
}
|
|
320
|
+
case 'transcript': {
|
|
321
|
+
send({
|
|
322
|
+
type: 'CUSTOM',
|
|
323
|
+
name: 'pikku:transcript',
|
|
324
|
+
value: { text: event.text },
|
|
325
|
+
});
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
316
328
|
}
|
|
317
329
|
},
|
|
318
330
|
};
|
|
@@ -660,7 +660,13 @@ export async function prepareAgentRun(agentName, input, params, agentSessionMap,
|
|
|
660
660
|
const contextMessages = await loadContextMessages(memoryConfig, storage, input, workingMemoryJsonSchema);
|
|
661
661
|
const userContent = input.attachments?.length
|
|
662
662
|
? [
|
|
663
|
-
|
|
663
|
+
// Omitted when there is nothing to say. An attachment on its own is a
|
|
664
|
+
// real turn — a spoken one carries audio and no text at all — and an
|
|
665
|
+
// empty text part alongside it is a part providers are entitled to
|
|
666
|
+
// reject, for a caller who never wrote one.
|
|
667
|
+
...(input.message
|
|
668
|
+
? [{ type: 'text', text: input.message }]
|
|
669
|
+
: []),
|
|
664
670
|
...input.attachments.map((a) => ({
|
|
665
671
|
type: a.type,
|
|
666
672
|
data: a.data,
|
|
@@ -73,6 +73,8 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
73
73
|
}),
|
|
74
74
|
...(agent.aiMiddleware ?? []),
|
|
75
75
|
];
|
|
76
|
+
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
77
|
+
const sharedNotes = {};
|
|
76
78
|
let modifiedMessages = runnerParams.messages;
|
|
77
79
|
let modifiedInstructions = runnerParams.instructions;
|
|
78
80
|
for (const mw of aiMiddlewares) {
|
|
@@ -80,6 +82,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
80
82
|
const result = await mw.modifyInput(singletonServices, {
|
|
81
83
|
messages: modifiedMessages,
|
|
82
84
|
instructions: modifiedInstructions,
|
|
85
|
+
shared: sharedNotes,
|
|
83
86
|
});
|
|
84
87
|
modifiedMessages = result.messages;
|
|
85
88
|
modifiedInstructions = result.instructions;
|
|
@@ -87,6 +90,12 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
87
90
|
}
|
|
88
91
|
runnerParams.messages = modifiedMessages;
|
|
89
92
|
runnerParams.instructions = modifiedInstructions;
|
|
93
|
+
// History records what the model was asked, which for a spoken turn is the
|
|
94
|
+
// transcript rather than the base64 audio that arrived — see the same note on
|
|
95
|
+
// the streaming path. Identity-checked, because a middleware may legitimately
|
|
96
|
+
// replace the message list with something unrelated to this turn.
|
|
97
|
+
const lastModified = modifiedMessages[modifiedMessages.length - 1];
|
|
98
|
+
const persistedUserMessage = lastModified?.id === userMessage.id ? lastModified : userMessage;
|
|
90
99
|
const runId = await aiRunState.createRun({
|
|
91
100
|
agentName,
|
|
92
101
|
threadId,
|
|
@@ -181,7 +190,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
181
190
|
args: a.args,
|
|
182
191
|
});
|
|
183
192
|
const completedStepsForSave = accumulatedSteps.slice(0, -1);
|
|
184
|
-
await saveMessages(storage, threadId, input.resourceId, memoryConfig,
|
|
193
|
+
await saveMessages(storage, threadId, input.resourceId, memoryConfig, persistedUserMessage, { text: '', steps: completedStepsForSave });
|
|
185
194
|
if (storage) {
|
|
186
195
|
await storage.saveMessages(threadId, [
|
|
187
196
|
{
|
|
@@ -242,7 +251,7 @@ export async function runAIAgent(agentName, input, params, agentSessionMap) {
|
|
|
242
251
|
outputMessages = modResult.messages;
|
|
243
252
|
}
|
|
244
253
|
}
|
|
245
|
-
await saveMessages(storage, threadId, input.resourceId, memoryConfig,
|
|
254
|
+
await saveMessages(storage, threadId, input.resourceId, memoryConfig, persistedUserMessage, {
|
|
246
255
|
...result,
|
|
247
256
|
text: outputText,
|
|
248
257
|
uiSpec: structuredOutput.uiSpec,
|
|
@@ -416,6 +425,8 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
|
|
|
416
425
|
}),
|
|
417
426
|
...(agent.aiMiddleware ?? []),
|
|
418
427
|
];
|
|
428
|
+
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
429
|
+
const sharedNotes = {};
|
|
419
430
|
let modifiedMessages = trimmedMessages;
|
|
420
431
|
let modifiedInstructions = instructions;
|
|
421
432
|
for (const mw of aiMiddlewares) {
|
|
@@ -423,6 +434,7 @@ async function continueAfterToolResultSync(run, agent, packageName, resolvedName
|
|
|
423
434
|
const result = await mw.modifyInput(singletonServices, {
|
|
424
435
|
messages: modifiedMessages,
|
|
425
436
|
instructions: modifiedInstructions,
|
|
437
|
+
shared: sharedNotes,
|
|
426
438
|
});
|
|
427
439
|
modifiedMessages = result.messages;
|
|
428
440
|
modifiedInstructions = result.instructions;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { pikkuState, getSingletonServices } from '../../pikku-state.js';
|
|
2
2
|
import { AIProviderNotConfiguredError } from '../../errors/errors.js';
|
|
3
3
|
import { randomUUID } from './ai-agent-utils.js';
|
|
4
|
+
import { SPOKEN_TRANSCRIPT } from './voice-input.js';
|
|
4
5
|
import { combineChannelMiddleware, wrapChannelWithMiddleware, } from '../channel/channel-middleware-runner.js';
|
|
5
6
|
import { resolveMemoryServices, loadContextMessages, trimMessages, getWorkingMemoryMiddleware, } from './ai-agent-memory.js';
|
|
6
7
|
import { prepareAgentRun, resolveAgent, buildInstructions, buildToolDefs, createScopedChannel, resolveOwnerResourceId, agentSessionScope, assertResourceOwner, assertResourcePrincipalOwner, assertAgentAuthorized, ToolApprovalRequired, ToolCredentialRequired, APPROVAL_REQUIRED, CREDENTIAL_REQUIRED, } from './ai-agent-prepare.js';
|
|
@@ -411,6 +412,8 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
411
412
|
}),
|
|
412
413
|
...(agent.aiMiddleware ?? []),
|
|
413
414
|
];
|
|
415
|
+
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
416
|
+
const sharedNotes = {};
|
|
414
417
|
let modifiedMessages = runnerParams.messages;
|
|
415
418
|
let modifiedInstructions = runnerParams.instructions;
|
|
416
419
|
for (const mw of aiMiddlewares) {
|
|
@@ -418,6 +421,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
418
421
|
const result = await mw.modifyInput(singletonServices, {
|
|
419
422
|
messages: modifiedMessages,
|
|
420
423
|
instructions: modifiedInstructions,
|
|
424
|
+
shared: sharedNotes,
|
|
421
425
|
});
|
|
422
426
|
modifiedMessages = result.messages;
|
|
423
427
|
modifiedInstructions = result.instructions;
|
|
@@ -425,6 +429,23 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
425
429
|
}
|
|
426
430
|
runnerParams.messages = modifiedMessages;
|
|
427
431
|
runnerParams.instructions = modifiedInstructions;
|
|
432
|
+
// Sent on the raw channel, ahead of the run. A voice client sent audio and so
|
|
433
|
+
// has no idea what it said; until this arrives its own message is a blank
|
|
434
|
+
// bubble. Ahead of the run rather than alongside it because the answer starts
|
|
435
|
+
// streaming within a few hundred milliseconds, and a question that appears
|
|
436
|
+
// after its answer reads as the wrong question.
|
|
437
|
+
const transcript = sharedNotes[SPOKEN_TRANSCRIPT];
|
|
438
|
+
if (typeof transcript === 'string') {
|
|
439
|
+
channel.send({ type: 'transcript', text: transcript });
|
|
440
|
+
}
|
|
441
|
+
// What goes into thread history is what the model was actually asked. For a
|
|
442
|
+
// spoken turn that is not what arrived over the wire: the wire carried a
|
|
443
|
+
// base64 audio blob, and persisting it would write megabytes of unreadable
|
|
444
|
+
// data into the history while losing the only readable record of the turn.
|
|
445
|
+
// Identity-checked rather than assumed — a middleware is free to rewrite the
|
|
446
|
+
// message list into something with no relation to the turn.
|
|
447
|
+
const lastModified = modifiedMessages[modifiedMessages.length - 1];
|
|
448
|
+
const persistedUserMessage = lastModified?.id === userMessage.id ? lastModified : userMessage;
|
|
428
449
|
const runId = await aiRunState.createRun({
|
|
429
450
|
agentName,
|
|
430
451
|
threadId,
|
|
@@ -442,7 +463,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
442
463
|
runnerParams.abortSignal = interruptHandle.signal;
|
|
443
464
|
runnerParams.tools = trackToolExecution(runnerParams.tools, interruptHandle);
|
|
444
465
|
if (storage) {
|
|
445
|
-
await storage.saveMessages(threadId, [
|
|
466
|
+
await storage.saveMessages(threadId, [persistedUserMessage]);
|
|
446
467
|
}
|
|
447
468
|
const streamMiddleware = aiMiddlewares
|
|
448
469
|
.filter((mw) => mw.modifyOutputStream)
|
|
@@ -455,6 +476,7 @@ export async function streamAIAgent(agentName, input, channel, params, agentSess
|
|
|
455
476
|
event,
|
|
456
477
|
allEvents,
|
|
457
478
|
state,
|
|
479
|
+
shared: sharedNotes,
|
|
458
480
|
// Sends downstream directly, so a hook can hand back the fast event
|
|
459
481
|
// now and push the slow one when it is ready.
|
|
460
482
|
emit: next,
|
|
@@ -841,6 +863,8 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
841
863
|
}),
|
|
842
864
|
...(agent.aiMiddleware ?? []),
|
|
843
865
|
];
|
|
866
|
+
// One bag per run, shared by every middleware — see PikkuAIMiddlewareHooks.
|
|
867
|
+
const sharedNotes = {};
|
|
844
868
|
let modifiedMessages = trimmedMessages;
|
|
845
869
|
let modifiedInstructions = instructions;
|
|
846
870
|
for (const mw of aiMiddlewares) {
|
|
@@ -848,6 +872,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
848
872
|
const result = await mw.modifyInput(singletonServices, {
|
|
849
873
|
messages: modifiedMessages,
|
|
850
874
|
instructions: modifiedInstructions,
|
|
875
|
+
shared: sharedNotes,
|
|
851
876
|
});
|
|
852
877
|
modifiedMessages = result.messages;
|
|
853
878
|
modifiedInstructions = result.instructions;
|
|
@@ -869,6 +894,7 @@ async function continueAfterToolResult(run, agent, packageName, resolvedName, st
|
|
|
869
894
|
event,
|
|
870
895
|
allEvents,
|
|
871
896
|
state,
|
|
897
|
+
shared: sharedNotes,
|
|
872
898
|
// Sends downstream directly, so a hook can hand back the fast event
|
|
873
899
|
// now and push the slow one when it is ready.
|
|
874
900
|
emit: next,
|
|
@@ -163,6 +163,23 @@ export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> =
|
|
|
163
163
|
modifyInput?: (services: Services, ctx: {
|
|
164
164
|
messages: AIMessage[];
|
|
165
165
|
instructions: string;
|
|
166
|
+
/**
|
|
167
|
+
* Notes about this run that every middleware can read and write, as
|
|
168
|
+
* opposed to {@link modifyOutputStream}'s `state`, which is private to
|
|
169
|
+
* one middleware.
|
|
170
|
+
*
|
|
171
|
+
* It exists because middlewares transform the turn for each other, and
|
|
172
|
+
* the transformation can destroy what a later one needed to know. Voice
|
|
173
|
+
* is the case in point: `voiceInput` replaces the user's audio with its
|
|
174
|
+
* transcript, so by the time anything downstream looks, the turn is
|
|
175
|
+
* indistinguishable from one that was typed — and whether it was typed is
|
|
176
|
+
* exactly what `voiceOutput` needs in order to decide whether to answer
|
|
177
|
+
* aloud. Only the middleware that consumed the audio can still say so.
|
|
178
|
+
*
|
|
179
|
+
* Namespace what you put here (`voice:spokenTurn`); it is one bag for
|
|
180
|
+
* everybody. Cleared between runs.
|
|
181
|
+
*/
|
|
182
|
+
shared: Record<string, unknown>;
|
|
166
183
|
}) => Promise<{
|
|
167
184
|
messages: AIMessage[];
|
|
168
185
|
instructions: string;
|
|
@@ -173,7 +190,10 @@ export interface PikkuAIMiddlewareHooks<State extends Record<string, unknown> =
|
|
|
173
190
|
modifyOutputStream?: (services: Services, ctx: {
|
|
174
191
|
event: AIStreamEvent;
|
|
175
192
|
allEvents: readonly AIStreamEvent[];
|
|
193
|
+
/** Private to this middleware, for this run. Cross-middleware facts go in `shared`. */
|
|
176
194
|
state: State;
|
|
195
|
+
/** Per-run notes shared with every middleware — see {@link modifyInput}. */
|
|
196
|
+
shared: Record<string, unknown>;
|
|
177
197
|
/**
|
|
178
198
|
* Push an event into the stream *after* this call has returned.
|
|
179
199
|
*
|
|
@@ -412,12 +432,32 @@ export type AIStreamEvent = {
|
|
|
412
432
|
type: 'audio-delta';
|
|
413
433
|
data: string;
|
|
414
434
|
format: string;
|
|
435
|
+
/**
|
|
436
|
+
* The sentence this audio says.
|
|
437
|
+
*
|
|
438
|
+
* Carried alongside the bytes because a client that gets talked over has
|
|
439
|
+
* to tell the model what the user actually heard, and playback position
|
|
440
|
+
* is the only place that is knowable. Without it a barge-in can stop the
|
|
441
|
+
* sound but not report it, and the next turn is answered as though the
|
|
442
|
+
* whole reply had landed.
|
|
443
|
+
*/
|
|
444
|
+
text?: string;
|
|
415
445
|
agent?: string;
|
|
416
446
|
session?: string;
|
|
417
447
|
} | {
|
|
418
448
|
type: 'audio-done';
|
|
419
449
|
agent?: string;
|
|
420
450
|
session?: string;
|
|
451
|
+
} | {
|
|
452
|
+
/**
|
|
453
|
+
* What the user was heard to say, sent once at the start of a spoken
|
|
454
|
+
* turn. Only the server knows this — the client sent audio — and a chat
|
|
455
|
+
* surface needs it to show the user's own message.
|
|
456
|
+
*/
|
|
457
|
+
type: 'transcript';
|
|
458
|
+
text: string;
|
|
459
|
+
agent?: string;
|
|
460
|
+
session?: string;
|
|
421
461
|
} | {
|
|
422
462
|
type: 'data';
|
|
423
463
|
name: string;
|
|
@@ -2,7 +2,7 @@ export { agent, agentStream, agentResume, agentApprove, agentInterrupt, } from '
|
|
|
2
2
|
export { wrapChannelWithAGUI, type AGUIEvent } from './ai-agent-agui.js';
|
|
3
3
|
export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
|
|
4
4
|
export { streamAIAgent, resumeAIAgent, interruptAIAgent, } from './ai-agent-stream.js';
|
|
5
|
-
export { voiceInput, readsAsNonSpeech, NoSpeechDetectedError, } from './voice-input.js';
|
|
5
|
+
export { voiceInput, readsAsNonSpeech, NoSpeechDetectedError, SPOKEN_TURN, SPOKEN_TRANSCRIPT, } from './voice-input.js';
|
|
6
6
|
export { voiceOutput, unspeakableScripts, voiceForText, type SpeakableScripts, } from './voice-output.js';
|
|
7
7
|
export { AgentInterruptedError, awaitPendingInterruptNote, getInFlightTools, isAbortError, isRunInterruptible, persistOrphanedToolResults, registerInterruptibleRun, signalRunInterrupt, trackInterruptNote, trackToolExecution, } from './ai-agent-interrupt.js';
|
|
8
8
|
export type { AgentInterruption, AgentInterruptResult, InterruptibleRunHandle, OrphanedToolResult, } from './ai-agent-interrupt.js';
|
|
@@ -2,7 +2,7 @@ export { agent, agentStream, agentResume, agentApprove, agentInterrupt, } from '
|
|
|
2
2
|
export { wrapChannelWithAGUI } from './ai-agent-agui.js';
|
|
3
3
|
export { runAIAgent, resumeAIAgentSync } from './ai-agent-runner.js';
|
|
4
4
|
export { streamAIAgent, resumeAIAgent, interruptAIAgent, } from './ai-agent-stream.js';
|
|
5
|
-
export { voiceInput, readsAsNonSpeech, NoSpeechDetectedError, } from './voice-input.js';
|
|
5
|
+
export { voiceInput, readsAsNonSpeech, NoSpeechDetectedError, SPOKEN_TURN, SPOKEN_TRANSCRIPT, } from './voice-input.js';
|
|
6
6
|
export { voiceOutput, unspeakableScripts, voiceForText, } from './voice-output.js';
|
|
7
7
|
export { AgentInterruptedError, awaitPendingInterruptNote, getInFlightTools, isAbortError, isRunInterruptible, persistOrphanedToolResults, registerInterruptibleRun, signalRunInterrupt, trackInterruptNote, trackToolExecution, } from './ai-agent-interrupt.js';
|
|
8
8
|
export { ToolApprovalRequired, ToolCredentialRequired, canAccessThread, isOwnedByPrincipal, sessionPrincipals, threadOwnerConstraint, } from './ai-agent-prepare.js';
|
|
@@ -34,6 +34,26 @@ export declare class NoSpeechDetectedError extends Error {
|
|
|
34
34
|
export declare const readsAsNonSpeech: (result: {
|
|
35
35
|
text?: string;
|
|
36
36
|
}) => boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Shared-notes key recording whether the user's turn arrived as audio.
|
|
39
|
+
*
|
|
40
|
+
* Written here because this is the last point at which it is knowable: the
|
|
41
|
+
* transcript that replaces the audio is indistinguishable from something typed.
|
|
42
|
+
* Read by `voiceOutput` to decide whether to answer aloud.
|
|
43
|
+
*
|
|
44
|
+
* Absent — rather than `false` — when this middleware is not wired at all, which
|
|
45
|
+
* is what lets `voiceOutput` still speak for a caller that has no voice input.
|
|
46
|
+
*/
|
|
47
|
+
export declare const SPOKEN_TURN = "voice:spokenTurn";
|
|
48
|
+
/**
|
|
49
|
+
* Shared-notes key holding what the user was heard to say, when they spoke.
|
|
50
|
+
*
|
|
51
|
+
* Absent on a typed turn. Read by the stream wiring, which forwards it to the
|
|
52
|
+
* client as a `transcript` event — a voice client has no idea what its own
|
|
53
|
+
* audio said, and without this the user's turn shows up in the UI as an empty
|
|
54
|
+
* bubble followed by an answer to a question they cannot see.
|
|
55
|
+
*/
|
|
56
|
+
export declare const SPOKEN_TRANSCRIPT = "voice:transcript";
|
|
37
57
|
export declare const voiceInput: (config?: {
|
|
38
58
|
language?: string;
|
|
39
59
|
model?: string;
|