@pikku/core 0.12.74 → 0.12.78
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 +397 -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/in-memory-workflow-service.d.ts +1 -0
- package/dist/services/in-memory-workflow-service.js +20 -0
- 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/pikku-workflow-service.d.ts +35 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +51 -0
- 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/in-memory-workflow-service.ts +29 -0
- 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/pikku-workflow-service.ts +67 -0
- 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-stalled-recovery.test.ts +106 -0
- package/src/wirings/workflow/workflow.types.ts +7 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tsconfig.type-tests.json +12 -0
|
@@ -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;
|
|
@@ -58,22 +58,44 @@ async function fetchAsUint8Array(url, allowedAudioHosts) {
|
|
|
58
58
|
}
|
|
59
59
|
return new Uint8Array(buffer);
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Shared-notes key recording whether the user's turn arrived as audio.
|
|
63
|
+
*
|
|
64
|
+
* Written here because this is the last point at which it is knowable: the
|
|
65
|
+
* transcript that replaces the audio is indistinguishable from something typed.
|
|
66
|
+
* Read by `voiceOutput` to decide whether to answer aloud.
|
|
67
|
+
*
|
|
68
|
+
* Absent — rather than `false` — when this middleware is not wired at all, which
|
|
69
|
+
* is what lets `voiceOutput` still speak for a caller that has no voice input.
|
|
70
|
+
*/
|
|
71
|
+
export const SPOKEN_TURN = 'voice:spokenTurn';
|
|
72
|
+
/**
|
|
73
|
+
* Shared-notes key holding what the user was heard to say, when they spoke.
|
|
74
|
+
*
|
|
75
|
+
* Absent on a typed turn. Read by the stream wiring, which forwards it to the
|
|
76
|
+
* client as a `transcript` event — a voice client has no idea what its own
|
|
77
|
+
* audio said, and without this the user's turn shows up in the UI as an empty
|
|
78
|
+
* bubble followed by an answer to a question they cannot see.
|
|
79
|
+
*/
|
|
80
|
+
export const SPOKEN_TRANSCRIPT = 'voice:transcript';
|
|
61
81
|
export const voiceInput = (config) => pikkuAIMiddleware({
|
|
62
|
-
modifyInput: async (services, { messages, instructions }) => {
|
|
82
|
+
modifyInput: async (services, { messages, instructions, shared }) => {
|
|
63
83
|
const aiAgentRunner = services.aiAgentRunner;
|
|
64
84
|
if (!aiAgentRunner?.transcribe)
|
|
65
85
|
return { messages, instructions };
|
|
66
86
|
const last = messages[messages.length - 1];
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
const parts = last?.role === 'user' && typeof last.content !== 'string'
|
|
88
|
+
? last.content
|
|
89
|
+
: undefined;
|
|
90
|
+
const hasAudio = !!parts?.some((p) => p.type === 'file' && !!p.mediaType?.startsWith('audio/'));
|
|
91
|
+
// Recorded on every turn this middleware sees, spoken or not, so that a
|
|
92
|
+
// typed turn is an explicit `false` downstream rather than a silence that
|
|
93
|
+
// could equally mean nobody was asked.
|
|
94
|
+
shared[SPOKEN_TURN] = hasAudio;
|
|
95
|
+
if (!hasAudio || !parts)
|
|
75
96
|
return { messages, instructions };
|
|
76
97
|
const updatedContent = [];
|
|
98
|
+
const heard = [];
|
|
77
99
|
for (const p of parts) {
|
|
78
100
|
if (!(p.type === 'file' && p.mediaType?.startsWith('audio/'))) {
|
|
79
101
|
updatedContent.push(p);
|
|
@@ -85,6 +107,9 @@ export const voiceInput = (config) => pikkuAIMiddleware({
|
|
|
85
107
|
const audioData = p.data
|
|
86
108
|
? base64ToUint8Array(p.data)
|
|
87
109
|
: await fetchAsUint8Array(p.url, config.allowedAudioHosts);
|
|
110
|
+
if (audioData.byteLength > MAX_AUDIO_SIZE) {
|
|
111
|
+
throw new Error('Audio file exceeds maximum size');
|
|
112
|
+
}
|
|
88
113
|
const result = await aiAgentRunner.transcribe({
|
|
89
114
|
model: config.model,
|
|
90
115
|
audio: audioData,
|
|
@@ -103,12 +128,22 @@ export const voiceInput = (config) => pikkuAIMiddleware({
|
|
|
103
128
|
// not hear.
|
|
104
129
|
if (readsAsNonSpeech(result))
|
|
105
130
|
continue;
|
|
131
|
+
heard.push(result.text);
|
|
106
132
|
updatedContent.push({ type: 'text', text: result.text });
|
|
107
133
|
}
|
|
108
134
|
// Every part was audio, and none of it was speech. There is nothing left
|
|
109
135
|
// to send, and a message with no content is not a question.
|
|
110
136
|
if (updatedContent.length === 0)
|
|
111
137
|
throw new NoSpeechDetectedError();
|
|
138
|
+
// Only when something was actually heard. A turn can carry audio that all
|
|
139
|
+
// reads as non-speech and still have content — an image with a silent
|
|
140
|
+
// caption clip — which leaves nothing above to throw. Recording `''` here
|
|
141
|
+
// would send a transcript event saying the user said nothing, and a client
|
|
142
|
+
// that tells "not transcribed yet" from "transcribed" by the key being
|
|
143
|
+
// absent would render the turn as an empty bubble rather than a pending one.
|
|
144
|
+
if (heard.length > 0) {
|
|
145
|
+
shared[SPOKEN_TRANSCRIPT] = heard.join(' ');
|
|
146
|
+
}
|
|
112
147
|
return {
|
|
113
148
|
messages: [
|
|
114
149
|
...messages.slice(0, -1),
|
|
@@ -59,6 +59,21 @@ export declare const voiceOutput: (config?: {
|
|
|
59
59
|
* the right default for a provider that genuinely is multilingual.
|
|
60
60
|
*/
|
|
61
61
|
speakableScripts?: SpeakableScripts;
|
|
62
|
+
/**
|
|
63
|
+
* Speak every reply, including answers to turns that were typed.
|
|
64
|
+
*
|
|
65
|
+
* The default is to speak only what was spoken to. An agent is usually one
|
|
66
|
+
* agent serving both kinds of caller — the same thread is typed at from a
|
|
67
|
+
* desk and talked to from a phone — and synthesizing a reply nobody is
|
|
68
|
+
* listening to costs real money on every sentence of every turn. Whether the
|
|
69
|
+
* user is in a call is not something the client needs to declare: sending
|
|
70
|
+
* audio is what saying so looks like.
|
|
71
|
+
*
|
|
72
|
+
* Set this for the cases where speech is not a reply to speech at all — a
|
|
73
|
+
* read-aloud accessibility mode, a kiosk, anything whose whole output is
|
|
74
|
+
* meant to be heard.
|
|
75
|
+
*/
|
|
76
|
+
always?: boolean;
|
|
62
77
|
}) => import("./ai-agent.types.js").PikkuAIMiddlewareHooks<VoiceOutputState, import("../../types/core.types.js").CoreSingletonServices<{
|
|
63
78
|
logLevel?: import("../../services/logger.js").LogLevel;
|
|
64
79
|
secrets?: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pikkuAIMiddleware } from '../../types/core.types.js';
|
|
2
|
+
import { SPOKEN_TURN } from './voice-input.js';
|
|
2
3
|
function bufferToBase64(data) {
|
|
3
4
|
let binary = '';
|
|
4
5
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -100,10 +101,17 @@ async function synthesizeAudio(aiAgentRunner, input) {
|
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
export const voiceOutput = (config) => pikkuAIMiddleware({
|
|
103
|
-
modifyOutputStream: async (services, { event, state, emit, signal }) => {
|
|
104
|
+
modifyOutputStream: async (services, { event, state, shared, emit, signal }) => {
|
|
104
105
|
const { aiAgentRunner, logger } = services;
|
|
105
106
|
if (!aiAgentRunner?.generateSpeech)
|
|
106
107
|
return event;
|
|
108
|
+
// Only an explicit `false` silences the reply. `voiceInput` sets this on
|
|
109
|
+
// every turn it handles, so `false` means a real user really typed; the
|
|
110
|
+
// key being absent means nothing reported either way — no voice input is
|
|
111
|
+
// wired — and that caller's replies are spoken exactly as they were before
|
|
112
|
+
// this option existed.
|
|
113
|
+
if (!config?.always && shared[SPOKEN_TURN] === false)
|
|
114
|
+
return event;
|
|
107
115
|
/**
|
|
108
116
|
* Queue a finished sentence for speech.
|
|
109
117
|
*
|
|
@@ -167,6 +175,7 @@ export const voiceOutput = (config) => pikkuAIMiddleware({
|
|
|
167
175
|
type: 'audio-delta',
|
|
168
176
|
data: bufferToBase64(result.audio.bytes),
|
|
169
177
|
format: result.audio.format,
|
|
178
|
+
text,
|
|
170
179
|
});
|
|
171
180
|
});
|
|
172
181
|
};
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import type { ApprovalRequester, Capabilities } from '../../channel/channel-rpc.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Logger } from '../../../services/logger.js';
|
|
3
|
+
/**
|
|
4
|
+
* Everything a renderer gets when the command ran on the server: a logger, and
|
|
5
|
+
* nothing else. There is no container on this side of the socket to resolve a
|
|
6
|
+
* service from, so this is the whole of it rather than a subset of
|
|
7
|
+
* `CoreSingletonServices` — which requires `config`, `variables` and `secrets`
|
|
8
|
+
* the client does not have and cannot invent.
|
|
9
|
+
*/
|
|
10
|
+
export type ClientCLIRenderServices = {
|
|
11
|
+
logger: Logger;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A renderer that runs on the client of a CLI-over-channel connection.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not `CorePikkuCLIRender`: that type's `Services` parameter is
|
|
17
|
+
* constrained to `CoreSingletonServices`, so the honest logger-only shape
|
|
18
|
+
* cannot be spelled through it.
|
|
19
|
+
*/
|
|
20
|
+
export type CorePikkuCLIClientRender<Data> = (services: ClientCLIRenderServices, data: Data, session?: undefined) => void | Promise<void>;
|
|
3
21
|
/**
|
|
4
22
|
* Runs a CLI command entirely on the server. Unlike `executeCLIViaChannel`,
|
|
5
23
|
* argv is forwarded untouched and never parsed here, so the client carries no
|
|
@@ -13,8 +31,8 @@ import type { CorePikkuCLIRender } from '../cli.types.js';
|
|
|
13
31
|
export declare function executeRawCLIViaChannel({ pikkuWS, args, renderers, defaultRenderer, capabilities, approve, }: {
|
|
14
32
|
pikkuWS: any;
|
|
15
33
|
args?: string[];
|
|
16
|
-
renderers?: Record<string,
|
|
17
|
-
defaultRenderer?:
|
|
34
|
+
renderers?: Record<string, CorePikkuCLIClientRender<any>>;
|
|
35
|
+
defaultRenderer?: CorePikkuCLIClientRender<any>;
|
|
18
36
|
capabilities?: Capabilities;
|
|
19
37
|
approve?: ApprovalRequester;
|
|
20
38
|
}): Promise<number>;
|
|
@@ -4,11 +4,19 @@ import { approverForMode, takeApprovalFlags } from './cli-approval.js';
|
|
|
4
4
|
const defaultJSONRenderer = (_services, data) => {
|
|
5
5
|
console.log(JSON.stringify(data));
|
|
6
6
|
};
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
// `console` is the whole logger a CLI wants — its output is the command's output.
|
|
8
|
+
// `setLevel` is a no-op rather than an omission because `Logger` requires it and
|
|
9
|
+
// there is no level to set: a renderer writes what the user asked to see.
|
|
10
|
+
const clientServices = {
|
|
11
|
+
logger: {
|
|
12
|
+
info: (...args) => console.info(...args),
|
|
13
|
+
warn: (...args) => console.warn(...args),
|
|
14
|
+
error: (...args) => console.error(...args),
|
|
15
|
+
debug: (...args) => console.debug(...args),
|
|
16
|
+
trace: (...args) => console.trace(...args),
|
|
17
|
+
setLevel: () => { },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
12
20
|
/**
|
|
13
21
|
* Runs a CLI command entirely on the server. Unlike `executeCLIViaChannel`,
|
|
14
22
|
* argv is forwarded untouched and never parsed here, so the client carries no
|
|
@@ -2,5 +2,6 @@ export { executeCLIViaChannel } from './cli-channel-runner.js';
|
|
|
2
2
|
export { handleRawCLI } from './cli-raw-channel-runner.js';
|
|
3
3
|
export type { RawCLIFrame, RawCLIResult } from './cli-raw-channel-runner.js';
|
|
4
4
|
export { executeRawCLIViaChannel } from './cli-raw-client-runner.js';
|
|
5
|
+
export type { ClientCLIRenderServices, CorePikkuCLIClientRender, } from './cli-raw-client-runner.js';
|
|
5
6
|
export { APPROVAL_FLAGS, approverForMode, createTerminalApprover, takeApprovalFlags, } from './cli-approval.js';
|
|
6
7
|
export type { ApprovalMode } from './cli-approval.js';
|
|
@@ -10,6 +10,10 @@ import type { CorePersonas } from './persona.types.js';
|
|
|
10
10
|
* running: a persona that exists only to be acted upon (the account an admin
|
|
11
11
|
* bans, the colleague a document is shared with) is seeded and never run.
|
|
12
12
|
*
|
|
13
|
+
* Exactly one `definePersonas(...)` is allowed per codebase, so there is one
|
|
14
|
+
* place to read the cast from and one place to add to. A second call — even in
|
|
15
|
+
* the same file — fails the build.
|
|
16
|
+
*
|
|
13
17
|
* @example
|
|
14
18
|
* ```typescript
|
|
15
19
|
* definePersonas({
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
* running: a persona that exists only to be acted upon (the account an admin
|
|
10
10
|
* bans, the colleague a document is shared with) is seeded and never run.
|
|
11
11
|
*
|
|
12
|
+
* Exactly one `definePersonas(...)` is allowed per codebase, so there is one
|
|
13
|
+
* place to read the cast from and one place to add to. A second call — even in
|
|
14
|
+
* the same file — fails the build.
|
|
15
|
+
*
|
|
12
16
|
* @example
|
|
13
17
|
* ```typescript
|
|
14
18
|
* definePersonas({
|
|
@@ -36,6 +36,16 @@ export type CorePersona = {
|
|
|
36
36
|
name: string;
|
|
37
37
|
jobTitle?: string;
|
|
38
38
|
description?: string;
|
|
39
|
+
/**
|
|
40
|
+
* A picture of this person, as any URL a browser can load: a file your app
|
|
41
|
+
* serves, an asset on a CDN, a Gravatar link.
|
|
42
|
+
*
|
|
43
|
+
* Optional because it is decoration. Omitted, the console draws a colour and
|
|
44
|
+
* an icon derived from the persona's id, which is stable, needs no network and
|
|
45
|
+
* is more legible than the identicon a synthetic address would resolve to —
|
|
46
|
+
* which is also why nothing is derived here.
|
|
47
|
+
*/
|
|
48
|
+
avatarUrl?: string;
|
|
39
49
|
/**
|
|
40
50
|
* The roles this person holds. Only system roles may be named — a custom
|
|
41
51
|
* role can be deleted from the console, so a persona pinned to one silently
|
|
@@ -107,6 +117,7 @@ export type PersonaMeta = {
|
|
|
107
117
|
name: string;
|
|
108
118
|
jobTitle?: string;
|
|
109
119
|
description?: string;
|
|
120
|
+
avatarUrl?: string;
|
|
110
121
|
roles: string[];
|
|
111
122
|
personality?: string;
|
|
112
123
|
goals: string[];
|
|
@@ -25,7 +25,8 @@ const canonicalizePayload = (data) => stableStringify(JSON.parse(JSON.stringify(
|
|
|
25
25
|
const signingPayload = (encodedClaim, data) => `${QUEUE_IDENTITY_CLAIM_VERSION}.${encodedClaim}.${canonicalizePayload(data)}`;
|
|
26
26
|
export const getQueueIdentitySecret = async (secrets) => {
|
|
27
27
|
try {
|
|
28
|
-
return (await secrets?.getSecret(QUEUE_IDENTITY_SECRET_NAME)) ||
|
|
28
|
+
return ((await secrets?.getSecret(QUEUE_IDENTITY_SECRET_NAME))?.reveal() ||
|
|
29
|
+
undefined);
|
|
29
30
|
}
|
|
30
31
|
catch {
|
|
31
32
|
return undefined;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommonWireMeta } from '../../types/core.types.js';
|
|
2
2
|
import type { CorePikkuFunctionConfig } from '../../function/functions.types.js';
|
|
3
3
|
import type { QueueConfigMapping } from './validate-worker-config.js';
|
|
4
|
+
import type { Safe } from '../../secret-value.js';
|
|
4
5
|
export interface PikkuWorkerConfig {
|
|
5
6
|
name?: string;
|
|
6
7
|
/** Total worker concurrency. */
|
|
@@ -95,7 +96,7 @@ export interface JobOptions {
|
|
|
95
96
|
}
|
|
96
97
|
export interface QueueService {
|
|
97
98
|
readonly supportsResults: boolean;
|
|
98
|
-
add<T>(queueName: string, data: T
|
|
99
|
+
add<T>(queueName: string, data: Safe<T>, options?: JobOptions): Promise<string>;
|
|
99
100
|
getJob<T, R>(queueName: string, jobId: string): Promise<QueueJob<T, R> | null>;
|
|
100
101
|
}
|
|
101
102
|
export interface QueueWorkers {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Safe } from '../../secret-value.js';
|
|
1
2
|
import type { Logger } from '../../services/logger.js';
|
|
2
3
|
import type { SecretService } from '../../services/secret-service.js';
|
|
3
4
|
import type { JobOptions, QueueJob, QueueService } from './queue.types.js';
|
|
@@ -11,6 +12,6 @@ export declare class SignedQueueService implements QueueService {
|
|
|
11
12
|
private readonly logger;
|
|
12
13
|
constructor(queueService: QueueService, secrets: SecretService, logger: Logger);
|
|
13
14
|
get supportsResults(): boolean;
|
|
14
|
-
add<T>(queueName: string, data: T
|
|
15
|
+
add<T>(queueName: string, data: Safe<T>, options?: JobOptions): Promise<string>;
|
|
15
16
|
getJob<T, R>(queueName: string, jobId: string): Promise<QueueJob<T, R> | null>;
|
|
16
17
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { CoreServices, PikkuRawWire } from '../../types/core.types.js';
|
|
2
2
|
import { PikkuError } from '../../errors/error-handler.js';
|
|
3
|
+
import { type SecretValue } from '../../secret-value.js';
|
|
3
4
|
export type RemoteAddonAuthBinding = {
|
|
4
5
|
credentialId: string;
|
|
5
6
|
} | {
|
|
6
7
|
secretId: string;
|
|
7
8
|
} | {
|
|
8
|
-
resolve: (services: CoreServices, wire: PikkuRawWire) => string | Promise<string
|
|
9
|
+
resolve: (services: CoreServices, wire: PikkuRawWire) => string | SecretValue<string> | Promise<string | SecretValue<string>>;
|
|
9
10
|
};
|
|
10
11
|
export declare class RemoteAddonAuthError extends PikkuError {
|
|
11
12
|
readonly namespace: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PikkuError, addError } from '../../errors/error-handler.js';
|
|
2
|
+
import { isSecretValue } from '../../secret-value.js';
|
|
2
3
|
export class RemoteAddonAuthError extends PikkuError {
|
|
3
4
|
namespace;
|
|
4
5
|
constructor(namespace, detail) {
|
|
@@ -28,8 +29,11 @@ export async function resolveRemoteAddonToken(auth, services, wire, namespace) {
|
|
|
28
29
|
else if (typeof auth.resolve === 'function') {
|
|
29
30
|
token = await auth.resolve(services, wire);
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
+
// The wire is where a secret is meant to end up, so unwrap it here rather
|
|
33
|
+
// than at each branch — `resolve` and `getCredential` may hand one back too.
|
|
34
|
+
const resolved = isSecretValue(token) ? token.reveal() : token;
|
|
35
|
+
if (resolved === null || resolved === undefined || resolved === '') {
|
|
32
36
|
throw new RemoteAddonAuthError(namespace, 'resolved token was empty');
|
|
33
37
|
}
|
|
34
|
-
return String(
|
|
38
|
+
return String(resolved);
|
|
35
39
|
}
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* signed in as a real user against staging or production.
|
|
18
18
|
*/
|
|
19
19
|
export type { ApiCatalogueEntry, IntentRecord, IntentSource, IntentStatus, StepRecord, VirtualUserAction, VirtualUserBudget, VirtualUserDisposition, VirtualUserFinding, VirtualUserFindingKind, VirtualUserRunResult, VirtualUserTally, VirtualUserTarget, } from './virtual-user.types.js';
|
|
20
|
+
export { PRODUCTION_DISPOSITION } from './virtual-user.types.js';
|
|
20
21
|
export { runVirtualUser, rememberIds, type RunVirtualUserParams, type VirtualUserCallContext, } from './run-virtual-user.js';
|
|
22
|
+
export { personaScopes, prepareVirtualUserRun, type VirtualUserPreparation, } from './prepare-virtual-user-run.js';
|
|
23
|
+
export type { VirtualUserRunOutcome, VirtualUserRunRecord, VirtualUserRunStart, VirtualUserRunStore, } from './virtual-user-run-store.js';
|
|
21
24
|
export { DISPOSITIONS, dispositionProfile, type DispositionProfile, type VirtualUserTuning, } from './virtual-user-dispositions.js';
|
|
22
25
|
export { catalogueClassification, catalogueIndex, catalogueLookup, describeEntry, isReadOnly, reachableCatalogue, renderCatalogue, unreachableCatalogue, } from './virtual-user-catalogue.js';
|
|
23
26
|
export { reachableAgents, type AgentReachability, type ReachableAgent, } from './virtual-user-agents.js';
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
export { PRODUCTION_DISPOSITION } from './virtual-user.types.js';
|
|
1
2
|
export { runVirtualUser, rememberIds, } from './run-virtual-user.js';
|
|
3
|
+
export { personaScopes, prepareVirtualUserRun, } from './prepare-virtual-user-run.js';
|
|
2
4
|
export { DISPOSITIONS, dispositionProfile, } from './virtual-user-dispositions.js';
|
|
3
5
|
export { catalogueClassification, catalogueIndex, catalogueLookup, describeEntry, isReadOnly, reachableCatalogue, renderCatalogue, unreachableCatalogue, } from './virtual-user-catalogue.js';
|
|
4
6
|
export { reachableAgents, } from './virtual-user-agents.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FunctionsMeta } from '../../types/core.types.js';
|
|
2
|
+
import type { WorkflowsMeta } from '../workflow/workflow.types.js';
|
|
3
|
+
import type { SystemRoleDefinitions, SystemRoleDefinitionsMeta } from '../role/role.types.js';
|
|
4
|
+
import { type SchemaMap } from './virtual-user-derive.js';
|
|
5
|
+
import { type AgentReachability, type ReachableAgent } from './virtual-user-agents.js';
|
|
6
|
+
import type { ApiCatalogueEntry, IntentSource } from './virtual-user.types.js';
|
|
7
|
+
/**
|
|
8
|
+
* The scopes a persona holds, resolved through its roles.
|
|
9
|
+
*
|
|
10
|
+
* Roles are the only thing a persona declares; scopes are what a function
|
|
11
|
+
* checks. Narrowing a virtual user's catalogue needs the second, so the
|
|
12
|
+
* expansion happens once, here, against the same `defineSystemRole` definitions
|
|
13
|
+
* the seed grants from.
|
|
14
|
+
*/
|
|
15
|
+
export declare const personaScopes: (persona: {
|
|
16
|
+
roles?: readonly string[];
|
|
17
|
+
}, roleScopes: Record<string, readonly string[]>) => string[];
|
|
18
|
+
/** Everything a run needs that is derived rather than decided. */
|
|
19
|
+
export interface VirtualUserPreparation {
|
|
20
|
+
/** Every RPC the app exposes, narrowed to what this persona can reach. */
|
|
21
|
+
catalogue: ApiCatalogueEntry[];
|
|
22
|
+
intents: IntentSource[];
|
|
23
|
+
scopes: string[];
|
|
24
|
+
agents: ReachableAgent[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Derive what a virtual user needs from what the project already generates.
|
|
28
|
+
*
|
|
29
|
+
* Shared because there are two callers with the same problem and different
|
|
30
|
+
* sources for it: `pikku persona run` reads the inspector state at build time,
|
|
31
|
+
* and the scaffolded `runVirtualUser` RPC reads `metaService` at runtime. They
|
|
32
|
+
* must agree — a persona whose catalogue is narrower over RPC than on the CLI
|
|
33
|
+
* finds different things from the same seed, which is exactly the property a
|
|
34
|
+
* seed exists to give.
|
|
35
|
+
*
|
|
36
|
+
* Nothing here is authored for the virtual user's benefit: the function meta is
|
|
37
|
+
* the catalogue, the scenario meta is the intents, and the role definitions are
|
|
38
|
+
* what turn a persona's declared roles into the scopes a function checks.
|
|
39
|
+
*/
|
|
40
|
+
export declare const prepareVirtualUserRun: (input: {
|
|
41
|
+
persona: {
|
|
42
|
+
roles?: readonly string[];
|
|
43
|
+
};
|
|
44
|
+
functionsMeta: FunctionsMeta;
|
|
45
|
+
schemas?: SchemaMap;
|
|
46
|
+
workflowsMeta?: WorkflowsMeta;
|
|
47
|
+
/**
|
|
48
|
+
* Either shape: the inspector holds an array, `metaService` hands back the
|
|
49
|
+
* same definitions keyed by name. Accepting both is what lets the CLI and the
|
|
50
|
+
* scaffolded RPC share this.
|
|
51
|
+
*/
|
|
52
|
+
systemRoles?: SystemRoleDefinitions | SystemRoleDefinitionsMeta;
|
|
53
|
+
agentsMeta?: Readonly<Record<string, AgentReachability>>;
|
|
54
|
+
}) => VirtualUserPreparation;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { flattenSystemRoleDefinitions } from '../role/validate-role-definitions.js';
|
|
2
|
+
import { deriveCatalogue, deriveIntents, } from './virtual-user-derive.js';
|
|
3
|
+
import { reachableAgents, } from './virtual-user-agents.js';
|
|
4
|
+
/**
|
|
5
|
+
* The scopes a persona holds, resolved through its roles.
|
|
6
|
+
*
|
|
7
|
+
* Roles are the only thing a persona declares; scopes are what a function
|
|
8
|
+
* checks. Narrowing a virtual user's catalogue needs the second, so the
|
|
9
|
+
* expansion happens once, here, against the same `defineSystemRole` definitions
|
|
10
|
+
* the seed grants from.
|
|
11
|
+
*/
|
|
12
|
+
export const personaScopes = (persona, roleScopes) => {
|
|
13
|
+
const scopes = new Set();
|
|
14
|
+
for (const role of persona.roles ?? []) {
|
|
15
|
+
for (const scope of roleScopes[role] ?? []) {
|
|
16
|
+
scopes.add(scope);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...scopes].sort();
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Derive what a virtual user needs from what the project already generates.
|
|
23
|
+
*
|
|
24
|
+
* Shared because there are two callers with the same problem and different
|
|
25
|
+
* sources for it: `pikku persona run` reads the inspector state at build time,
|
|
26
|
+
* and the scaffolded `runVirtualUser` RPC reads `metaService` at runtime. They
|
|
27
|
+
* must agree — a persona whose catalogue is narrower over RPC than on the CLI
|
|
28
|
+
* finds different things from the same seed, which is exactly the property a
|
|
29
|
+
* seed exists to give.
|
|
30
|
+
*
|
|
31
|
+
* Nothing here is authored for the virtual user's benefit: the function meta is
|
|
32
|
+
* the catalogue, the scenario meta is the intents, and the role definitions are
|
|
33
|
+
* what turn a persona's declared roles into the scopes a function checks.
|
|
34
|
+
*/
|
|
35
|
+
export const prepareVirtualUserRun = (input) => {
|
|
36
|
+
const catalogue = deriveCatalogue(input.functionsMeta, input.schemas ?? {});
|
|
37
|
+
const intents = deriveIntents(input.workflowsMeta ?? {}, input.functionsMeta);
|
|
38
|
+
const declared = input.systemRoles ?? [];
|
|
39
|
+
const roleScopes = {};
|
|
40
|
+
for (const role of flattenSystemRoleDefinitions(Array.isArray(declared) ? declared : Object.values(declared))) {
|
|
41
|
+
roleScopes[role.name] = role.scopes;
|
|
42
|
+
}
|
|
43
|
+
const scopes = personaScopes(input.persona, roleScopes);
|
|
44
|
+
// Gated by the same scopes as the RPCs, because an agent is reached rather
|
|
45
|
+
// than declared: `CoreAIAgent.scopes` is checked against the session, so a
|
|
46
|
+
// persona finds the specialists its roles unlock and no others.
|
|
47
|
+
const agents = reachableAgents(input.agentsMeta ?? {}, scopes);
|
|
48
|
+
return { catalogue, intents, scopes, agents };
|
|
49
|
+
};
|