@kuralle-agents/core 0.8.0 → 0.9.0
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/README.md +8 -2
- package/dist/capabilities/validators/groundingValidator.d.ts +21 -0
- package/dist/capabilities/validators/groundingValidator.js +109 -0
- package/dist/escalation/escalation.d.ts +20 -0
- package/dist/escalation/escalation.js +85 -0
- package/dist/escalation/types.d.ts +40 -0
- package/dist/eval/simulation.d.ts +99 -0
- package/dist/eval/simulation.js +168 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +12 -1
- package/dist/memory/factMemoryService.d.ts +28 -0
- package/dist/memory/factMemoryService.js +137 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/processors/builtin/moderationGuard.d.ts +24 -0
- package/dist/processors/builtin/moderationGuard.js +101 -0
- package/dist/processors/builtin/piiGuard.d.ts +41 -0
- package/dist/processors/builtin/piiGuard.js +143 -0
- package/dist/processors/builtin/promptInjectionGuard.d.ts +16 -0
- package/dist/processors/builtin/promptInjectionGuard.js +28 -0
- package/dist/runtime/Runtime.d.ts +53 -0
- package/dist/runtime/Runtime.js +184 -10
- package/dist/runtime/channels/TextDriver.js +8 -2
- package/dist/runtime/channels/VoiceDriver.js +6 -0
- package/dist/runtime/channels/index.d.ts +1 -1
- package/dist/runtime/channels/index.js +1 -1
- package/dist/runtime/channels/inputBuffer.d.ts +4 -1
- package/dist/runtime/channels/inputBuffer.js +8 -0
- package/dist/runtime/closeRun.js +4 -0
- package/dist/runtime/compaction.d.ts +51 -0
- package/dist/runtime/compaction.js +111 -0
- package/dist/runtime/hostLoop.d.ts +2 -0
- package/dist/runtime/hostLoop.js +1 -1
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/openRun.d.ts +5 -0
- package/dist/runtime/openRun.js +17 -0
- package/dist/runtime/policies/agentTurn.d.ts +4 -0
- package/dist/runtime/policies/agentTurn.js +35 -4
- package/dist/runtime/userInput.d.ts +2 -0
- package/dist/runtime/userInput.js +20 -0
- package/dist/scheduler/index.d.ts +89 -0
- package/dist/scheduler/index.js +104 -0
- package/dist/types/channel.d.ts +2 -0
- package/dist/types/stream.d.ts +29 -0
- package/guides/GUARDRAILS.md +44 -0
- package/package.json +2 -2
|
@@ -12,6 +12,11 @@ export interface OpenRunOptions {
|
|
|
12
12
|
userId?: string;
|
|
13
13
|
input?: UserInputContent;
|
|
14
14
|
selection?: ResolvedSelection;
|
|
15
|
+
/** Agent-initiated turn: no user input; a wake note is appended instead. */
|
|
16
|
+
wake?: {
|
|
17
|
+
reason: string;
|
|
18
|
+
payload?: Record<string, unknown>;
|
|
19
|
+
};
|
|
15
20
|
agentId?: string;
|
|
16
21
|
seedMessages?: ModelMessage[];
|
|
17
22
|
historyDelta?: ModelMessage[];
|
package/dist/runtime/openRun.js
CHANGED
|
@@ -71,6 +71,23 @@ export async function openRun(agentsById, options) {
|
|
|
71
71
|
await options.sessionStore.save(sessionAfterPersist);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
if (options.wake && !hasInput) {
|
|
75
|
+
const payloadNote = options.wake.payload
|
|
76
|
+
? ` Context: ${JSON.stringify(options.wake.payload)}.`
|
|
77
|
+
: '';
|
|
78
|
+
const wakeMessage = {
|
|
79
|
+
role: 'system',
|
|
80
|
+
content: `[Scheduled wake: ${options.wake.reason}]${payloadNote} ` +
|
|
81
|
+
'There is no new user message. Re-engage the user proactively per your instructions; ' +
|
|
82
|
+
'if a task is in progress, follow up on it gently.',
|
|
83
|
+
};
|
|
84
|
+
runState.messages = [...runState.messages, wakeMessage];
|
|
85
|
+
runState.updatedAt = Date.now();
|
|
86
|
+
await runStore.putRunState(runState);
|
|
87
|
+
const sessionAfterPersist = (await options.sessionStore.get(options.sessionId)) ?? session;
|
|
88
|
+
sessionAfterPersist.messages = [...sessionAfterPersist.messages, wakeMessage];
|
|
89
|
+
await options.sessionStore.save(sessionAfterPersist);
|
|
90
|
+
}
|
|
74
91
|
const agent = agentsById.get(runState.activeAgentId);
|
|
75
92
|
if (!agent) {
|
|
76
93
|
throw new Error(`Unknown activeAgentId "${runState.activeAgentId}"`);
|
|
@@ -6,6 +6,10 @@ export interface PreTurnResult {
|
|
|
6
6
|
proceed: boolean;
|
|
7
7
|
userMessage: string;
|
|
8
8
|
blockedMessage?: string;
|
|
9
|
+
/** Id of the processor/policy that blocked (set when `proceed` is false). */
|
|
10
|
+
blockedBy?: string;
|
|
11
|
+
/** Machine-readable block reason (set when `proceed` is false). */
|
|
12
|
+
blockedReason?: string;
|
|
9
13
|
}
|
|
10
14
|
export interface PostTurnResult {
|
|
11
15
|
proceed: boolean;
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { appendConversationAudit } from '../../audit/record.js';
|
|
2
2
|
import { SAFE_DEGRADED_MESSAGE } from '../../flow/degrade.js';
|
|
3
3
|
import { runInputProcessors, runOutputProcessors } from '../../processors/ProcessorRunner.js';
|
|
4
|
+
function userTextProjection(content) {
|
|
5
|
+
if (typeof content === 'string')
|
|
6
|
+
return content;
|
|
7
|
+
if (!Array.isArray(content))
|
|
8
|
+
return '';
|
|
9
|
+
return content
|
|
10
|
+
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
11
|
+
.map((part) => part.text)
|
|
12
|
+
.join('\n');
|
|
13
|
+
}
|
|
14
|
+
// Guards must see multimodal turns too: a WhatsApp image whose CAPTION carries
|
|
15
|
+
// a card number is still PII; an injection attempt in a caption is still an
|
|
16
|
+
// injection. Project the text parts — file/image parts pass through untouched.
|
|
4
17
|
function latestUserMessage(messages) {
|
|
5
18
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
6
19
|
const message = messages[index];
|
|
7
|
-
if (message?.role === 'user'
|
|
8
|
-
return message.content;
|
|
20
|
+
if (message?.role === 'user') {
|
|
21
|
+
return userTextProjection(message.content);
|
|
9
22
|
}
|
|
10
23
|
}
|
|
11
24
|
return '';
|
|
@@ -91,6 +104,8 @@ async function runRefinementPolicies(ctx, userMessage) {
|
|
|
91
104
|
proceed: false,
|
|
92
105
|
userMessage: current,
|
|
93
106
|
blockedMessage: decision.userFacingMessage ?? decision.rationale ?? 'Input blocked',
|
|
107
|
+
blockedBy: policy.name,
|
|
108
|
+
blockedReason: decision.rationale,
|
|
94
109
|
};
|
|
95
110
|
}
|
|
96
111
|
if (decision.decision === 'rewrite') {
|
|
@@ -138,7 +153,7 @@ async function runValidationPolicies(ctx, userMessage, assistantOutput, toolCall
|
|
|
138
153
|
proceed: false,
|
|
139
154
|
text: safe,
|
|
140
155
|
blockedMessage: safe,
|
|
141
|
-
control: { type: 'escalate', reason: decision.rationale },
|
|
156
|
+
control: { type: 'escalate', reason: decision.rationale, category: decision.escalationReason },
|
|
142
157
|
confidence: decision.confidence,
|
|
143
158
|
};
|
|
144
159
|
}
|
|
@@ -170,10 +185,16 @@ export async function applyPreTurnPolicies(ctx) {
|
|
|
170
185
|
proceed: false,
|
|
171
186
|
userMessage,
|
|
172
187
|
blockedMessage: outcome.message,
|
|
188
|
+
blockedBy: outcome.processorId,
|
|
189
|
+
blockedReason: outcome.reason,
|
|
173
190
|
};
|
|
174
191
|
}
|
|
175
192
|
if (outcome.input !== userMessage) {
|
|
176
193
|
patchLatestUserMessage(ctx.runState.messages, outcome.input);
|
|
194
|
+
patchLatestUserMessage(ctx.session.messages, outcome.input);
|
|
195
|
+
// Persist immediately: a redaction (e.g. PII) must replace the raw
|
|
196
|
+
// value in the durable record, not only in this turn's memory.
|
|
197
|
+
await ctx.runStore.putRunState(ctx.runState);
|
|
177
198
|
}
|
|
178
199
|
}
|
|
179
200
|
return runRefinementPolicies(ctx, latestUserMessage(ctx.runState.messages));
|
|
@@ -210,7 +231,17 @@ function patchLatestUserMessage(messages, next) {
|
|
|
210
231
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
211
232
|
const message = messages[index];
|
|
212
233
|
if (message?.role === 'user') {
|
|
213
|
-
|
|
234
|
+
if (Array.isArray(message.content)) {
|
|
235
|
+
// Multimodal turn: rewrite the text while preserving file/image parts.
|
|
236
|
+
const mediaParts = message.content.filter((part) => part.type !== 'text');
|
|
237
|
+
messages[index] = {
|
|
238
|
+
role: 'user',
|
|
239
|
+
content: [{ type: 'text', text: next }, ...mediaParts],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
messages[index] = { role: 'user', content: next };
|
|
244
|
+
}
|
|
214
245
|
return;
|
|
215
246
|
}
|
|
216
247
|
}
|
|
@@ -10,6 +10,8 @@ import { type UserContent, type TranscriptionModel } from 'ai';
|
|
|
10
10
|
* input buffer are all persisted through the `SessionStore` (JSON/Redis/Postgres).
|
|
11
11
|
*/
|
|
12
12
|
export type UserInputContent = UserContent;
|
|
13
|
+
/** Merge multiple user inputs into one turn (ingress coalescing / mid-turn drain). */
|
|
14
|
+
export declare function mergeUserInputContents(items: UserInputContent[]): UserInputContent | undefined;
|
|
13
15
|
/** Text projection of user input — for confirm-gate parsing, choice matching, and
|
|
14
16
|
* extraction hints. Non-text parts are dropped. A plain string returns as-is. */
|
|
15
17
|
export declare function userInputToText(input: UserInputContent): string;
|
|
@@ -23,6 +23,26 @@ function audioSource(data) {
|
|
|
23
23
|
}
|
|
24
24
|
return data;
|
|
25
25
|
}
|
|
26
|
+
/** Merge multiple user inputs into one turn (ingress coalescing / mid-turn drain). */
|
|
27
|
+
export function mergeUserInputContents(items) {
|
|
28
|
+
if (items.length === 0)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (items.length === 1)
|
|
31
|
+
return items[0];
|
|
32
|
+
const parts = [];
|
|
33
|
+
for (const item of items) {
|
|
34
|
+
if (typeof item === 'string') {
|
|
35
|
+
if (item.length > 0)
|
|
36
|
+
parts.push({ type: 'text', text: item });
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
parts.push(...item);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (parts.length === 0)
|
|
43
|
+
return '';
|
|
44
|
+
return parts;
|
|
45
|
+
}
|
|
26
46
|
/** Text projection of user input — for confirm-gate parsing, choice matching, and
|
|
27
47
|
* extraction hints. Non-text parts are dropped. A plain string returns as-is. */
|
|
28
48
|
export function userInputToText(input) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Tool } from '../types/effectTool.js';
|
|
3
|
+
import type { HarnessStreamPart, TurnHandle } from '../types/stream.js';
|
|
4
|
+
/**
|
|
5
|
+
* Deferred-work scheduling for proactive (agent-initiated) turns.
|
|
6
|
+
*
|
|
7
|
+
* One `Scheduler` contract across the framework: the engagement layer's
|
|
8
|
+
* broadcast/drip jobs and the runtime's wake turns ride the same interface.
|
|
9
|
+
* Backends: `createInProcessScheduler` (dev, timer-based), Cloudflare DO
|
|
10
|
+
* alarms via `@kuralle-agents/cf-agent`, or any queue (BullMQ, Cloud Tasks)
|
|
11
|
+
* implementing the two methods.
|
|
12
|
+
*/
|
|
13
|
+
export interface ScheduledJob {
|
|
14
|
+
kind: string;
|
|
15
|
+
payload: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface Scheduler {
|
|
18
|
+
enqueue(job: ScheduledJob, opts?: {
|
|
19
|
+
delayMs?: number;
|
|
20
|
+
}): Promise<string>;
|
|
21
|
+
cancel(jobId: string): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export type InjectableTimer = {
|
|
24
|
+
set(fn: () => void, ms: number): unknown;
|
|
25
|
+
clear(handle: unknown): void;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Default in-process scheduler (timer-based). For single-process/dev.
|
|
29
|
+
* Inject a durable adapter (DO alarms, BullMQ, Cloud Tasks) for
|
|
30
|
+
* multi-process / serverless.
|
|
31
|
+
*/
|
|
32
|
+
export declare function createInProcessScheduler(opts: {
|
|
33
|
+
run: (job: ScheduledJob) => void | Promise<void>;
|
|
34
|
+
timer?: InjectableTimer;
|
|
35
|
+
}): Scheduler;
|
|
36
|
+
export declare const WAKE_JOB_KIND = "kuralle.wake";
|
|
37
|
+
export interface WakeOptions {
|
|
38
|
+
/** Why the agent is waking — composed into the wake note the model sees. */
|
|
39
|
+
reason: string;
|
|
40
|
+
/** Structured context for the wake turn (e.g. the abandoned cart id). */
|
|
41
|
+
payload?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
export interface WakeJobPayload extends WakeOptions {
|
|
44
|
+
sessionId: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function wakeJob(wake: WakeJobPayload): ScheduledJob;
|
|
47
|
+
export declare function isWakeJob(job: ScheduledJob): boolean;
|
|
48
|
+
/** What a wake turn produced — handed to the host's delivery function. */
|
|
49
|
+
export interface WakeDelivery {
|
|
50
|
+
sessionId: string;
|
|
51
|
+
reason: string;
|
|
52
|
+
payload?: Record<string, unknown>;
|
|
53
|
+
/** Full stream of the wake turn (text, tool events, interactive parts…). */
|
|
54
|
+
parts: HarnessStreamPart[];
|
|
55
|
+
/** Concatenated assistant text of the wake turn. */
|
|
56
|
+
text: string;
|
|
57
|
+
}
|
|
58
|
+
/** The runtime surface a wake runner needs (satisfied by `Runtime`). */
|
|
59
|
+
export interface WakeRunnable {
|
|
60
|
+
run(opts: {
|
|
61
|
+
sessionId: string;
|
|
62
|
+
wake: WakeOptions;
|
|
63
|
+
}): TurnHandle;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build the scheduler executor for wake jobs: runs the agent-initiated turn
|
|
67
|
+
* and hands the produced parts to `deliver` (e.g. the messaging outbound
|
|
68
|
+
* pipeline — which keeps the send window-safe). Compose with your own job
|
|
69
|
+
* kinds: `run: (job) => isWakeJob(job) ? runWake(job) : runMine(job)`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function createWakeJobRunner(runtime: WakeRunnable, opts: {
|
|
72
|
+
deliver: (delivery: WakeDelivery) => Promise<void>;
|
|
73
|
+
onError?: (error: unknown, job: ScheduledJob) => void;
|
|
74
|
+
}): (job: ScheduledJob) => Promise<void>;
|
|
75
|
+
declare const scheduleFollowupInput: z.ZodObject<{
|
|
76
|
+
delayMinutes: z.ZodNumber;
|
|
77
|
+
reason: z.ZodString;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
/**
|
|
80
|
+
* Durable tool letting the agent schedule its own follow-up wake turn
|
|
81
|
+
* ("I'll check back in an hour"). Safe for `globalTools` — it only schedules;
|
|
82
|
+
* the wake turn itself goes through the full guard/window pipeline.
|
|
83
|
+
*/
|
|
84
|
+
export declare function createScheduleFollowupTool(scheduler: Scheduler): Tool<z.infer<typeof scheduleFollowupInput>, {
|
|
85
|
+
scheduled: boolean;
|
|
86
|
+
jobId: string;
|
|
87
|
+
inMinutes: number;
|
|
88
|
+
}>;
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../tools/effect/defineTool.js';
|
|
3
|
+
const defaultTimer = {
|
|
4
|
+
set: (fn, ms) => setTimeout(fn, ms),
|
|
5
|
+
clear: (handle) => clearTimeout(handle),
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Default in-process scheduler (timer-based). For single-process/dev.
|
|
9
|
+
* Inject a durable adapter (DO alarms, BullMQ, Cloud Tasks) for
|
|
10
|
+
* multi-process / serverless.
|
|
11
|
+
*/
|
|
12
|
+
export function createInProcessScheduler(opts) {
|
|
13
|
+
const timer = opts.timer ?? defaultTimer;
|
|
14
|
+
let nextJobId = 0;
|
|
15
|
+
const handles = new Map();
|
|
16
|
+
return {
|
|
17
|
+
async enqueue(job, options) {
|
|
18
|
+
const jobId = String(++nextJobId);
|
|
19
|
+
const delayMs = options?.delayMs ?? 0;
|
|
20
|
+
const handle = timer.set(() => {
|
|
21
|
+
handles.delete(jobId);
|
|
22
|
+
void opts.run(job);
|
|
23
|
+
}, delayMs);
|
|
24
|
+
handles.set(jobId, handle);
|
|
25
|
+
return jobId;
|
|
26
|
+
},
|
|
27
|
+
async cancel(jobId) {
|
|
28
|
+
const handle = handles.get(jobId);
|
|
29
|
+
if (handle === undefined)
|
|
30
|
+
return;
|
|
31
|
+
timer.clear(handle);
|
|
32
|
+
handles.delete(jobId);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// ── Wake jobs (agent-initiated turns) ────────────────────────────────────────
|
|
37
|
+
export const WAKE_JOB_KIND = 'kuralle.wake';
|
|
38
|
+
export function wakeJob(wake) {
|
|
39
|
+
return { kind: WAKE_JOB_KIND, payload: { ...wake } };
|
|
40
|
+
}
|
|
41
|
+
export function isWakeJob(job) {
|
|
42
|
+
return job.kind === WAKE_JOB_KIND;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build the scheduler executor for wake jobs: runs the agent-initiated turn
|
|
46
|
+
* and hands the produced parts to `deliver` (e.g. the messaging outbound
|
|
47
|
+
* pipeline — which keeps the send window-safe). Compose with your own job
|
|
48
|
+
* kinds: `run: (job) => isWakeJob(job) ? runWake(job) : runMine(job)`.
|
|
49
|
+
*/
|
|
50
|
+
export function createWakeJobRunner(runtime, opts) {
|
|
51
|
+
return async (job) => {
|
|
52
|
+
if (!isWakeJob(job)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const { sessionId, reason, payload } = job.payload;
|
|
56
|
+
try {
|
|
57
|
+
const handle = runtime.run({ sessionId, wake: { reason, payload } });
|
|
58
|
+
const parts = [];
|
|
59
|
+
let text = '';
|
|
60
|
+
for await (const part of handle.events) {
|
|
61
|
+
parts.push(part);
|
|
62
|
+
if (part.type === 'text-delta') {
|
|
63
|
+
text += part.delta;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const result = await handle;
|
|
67
|
+
await opts.deliver({ sessionId, reason, payload, parts, text: text || result.text });
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (opts.onError) {
|
|
71
|
+
opts.onError(error, job);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
console.warn(`[Kuralle] wake turn failed for session ${sessionId}:`, error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const scheduleFollowupInput = z.object({
|
|
80
|
+
delayMinutes: z.number().min(1).describe('How many minutes from now to follow up'),
|
|
81
|
+
reason: z
|
|
82
|
+
.string()
|
|
83
|
+
.describe('Why the follow-up is needed, e.g. "user said they would decide after lunch"'),
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* Durable tool letting the agent schedule its own follow-up wake turn
|
|
87
|
+
* ("I'll check back in an hour"). Safe for `globalTools` — it only schedules;
|
|
88
|
+
* the wake turn itself goes through the full guard/window pipeline.
|
|
89
|
+
*/
|
|
90
|
+
export function createScheduleFollowupTool(scheduler) {
|
|
91
|
+
return defineTool({
|
|
92
|
+
name: 'schedule_followup',
|
|
93
|
+
description: 'Schedule a follow-up message to the user after a delay. Use when the user asks you to check back later, or a pending task warrants a proactive follow-up.',
|
|
94
|
+
input: scheduleFollowupInput,
|
|
95
|
+
execute: async (args, ctx) => {
|
|
96
|
+
const sessionId = ctx?.session.id;
|
|
97
|
+
if (!sessionId) {
|
|
98
|
+
throw new Error('schedule_followup requires a session context');
|
|
99
|
+
}
|
|
100
|
+
const jobId = await scheduler.enqueue(wakeJob({ sessionId, reason: args.reason }), { delayMs: Math.round(args.delayMinutes * 60_000) });
|
|
101
|
+
return { scheduled: true, jobId, inMinutes: args.delayMinutes };
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
package/dist/types/channel.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { RunContext } from './run-context.js';
|
|
|
4
4
|
import type { FlowNode } from './flow.js';
|
|
5
5
|
import type { AnyTool } from './effectTool.js';
|
|
6
6
|
import type { DispatchMode } from '../runtime/dispatchMode.js';
|
|
7
|
+
import type { EscalationReason } from '../escalation/types.js';
|
|
7
8
|
export type DriverOutputCapability = 'kuralle-controlled-text' | 'kuralle-controlled-tts' | 'native-realtime';
|
|
8
9
|
export interface HostControlContext {
|
|
9
10
|
dispatchMode: DispatchMode;
|
|
@@ -67,6 +68,7 @@ export type TurnControl = {
|
|
|
67
68
|
} | {
|
|
68
69
|
type: 'escalate';
|
|
69
70
|
reason: string;
|
|
71
|
+
category?: EscalationReason;
|
|
70
72
|
} | {
|
|
71
73
|
type: 'recover';
|
|
72
74
|
reason?: string;
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ConversationOutcome } from '../outcomes/types.js';
|
|
2
2
|
import type { ChoiceOption } from './selection.js';
|
|
3
|
+
import type { EscalationReason } from '../escalation/types.js';
|
|
3
4
|
/**
|
|
4
5
|
* Authoritative runtime stream union (`runFlow` / `Runtime` emit).
|
|
5
6
|
* `types/voice.ts` defines a separate voice/realtime union that intentionally
|
|
@@ -77,6 +78,34 @@ export type HarnessStreamPart = {
|
|
|
77
78
|
rationale: string;
|
|
78
79
|
userFacingMessage: string;
|
|
79
80
|
handlerOutcome?: 'queued' | 'connected' | 'failed';
|
|
81
|
+
} | {
|
|
82
|
+
/** An agent-initiated (scheduled wake) turn — there is no new user message. */
|
|
83
|
+
type: 'wake';
|
|
84
|
+
reason: string;
|
|
85
|
+
} | {
|
|
86
|
+
type: 'escalation';
|
|
87
|
+
reason: string;
|
|
88
|
+
category?: EscalationReason;
|
|
89
|
+
/** Result of the configured escalation handler. */
|
|
90
|
+
outcome: 'queued' | 'connected' | 'failed';
|
|
91
|
+
/** The LLM handoff brief included in the request, when generated. */
|
|
92
|
+
summary?: string;
|
|
93
|
+
} | {
|
|
94
|
+
type: 'context-compacted';
|
|
95
|
+
/** Estimated history tokens before/after compaction. */
|
|
96
|
+
beforeTokens: number;
|
|
97
|
+
afterTokens: number;
|
|
98
|
+
/** Number of older messages folded into the summary. */
|
|
99
|
+
summarizedCount: number;
|
|
100
|
+
} | {
|
|
101
|
+
type: 'compaction-skipped';
|
|
102
|
+
reason: string;
|
|
103
|
+
} | {
|
|
104
|
+
type: 'context-overflow-recovered';
|
|
105
|
+
/** Partial assistant/tool messages stripped from the failed turn. */
|
|
106
|
+
strippedCount: number;
|
|
107
|
+
/** Whether the forced post-recovery compaction actually compacted. */
|
|
108
|
+
compacted: boolean;
|
|
80
109
|
} | {
|
|
81
110
|
type: 'error';
|
|
82
111
|
error: string;
|
package/guides/GUARDRAILS.md
CHANGED
|
@@ -65,6 +65,50 @@ const runtime = new Runtime({
|
|
|
65
65
|
});
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## Built-in Guards (0.8.5)
|
|
69
|
+
|
|
70
|
+
Production-ready processors and validators ship with core — wire them into
|
|
71
|
+
`AgentConfig.guardrails` / `AgentConfig.validate`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import {
|
|
75
|
+
createPromptInjectionGuard,
|
|
76
|
+
createPiiInputGuard,
|
|
77
|
+
createPiiOutputGuard,
|
|
78
|
+
createModerationGuard,
|
|
79
|
+
createGroundingValidator,
|
|
80
|
+
} from '@kuralle-agents/core';
|
|
81
|
+
|
|
82
|
+
const agent = defineAgent({
|
|
83
|
+
id: 'shop',
|
|
84
|
+
instructions: '...',
|
|
85
|
+
guardrails: {
|
|
86
|
+
input: [
|
|
87
|
+
createPromptInjectionGuard(), // deterministic injection patterns → block
|
|
88
|
+
createPiiInputGuard(), // Luhn-checked cards + emails → redact (PCI default)
|
|
89
|
+
createModerationGuard({ model: controlModel }), // LLM policy classifier → block
|
|
90
|
+
],
|
|
91
|
+
output: [createPiiOutputGuard()],
|
|
92
|
+
},
|
|
93
|
+
validate: [
|
|
94
|
+
// state-grounded "no invented actions" gate — rewrite-not-block
|
|
95
|
+
createGroundingValidator({ model: controlModel }),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- PII detectors default to `['credit-card', 'email']`; `phone`/`iban` are
|
|
101
|
+
opt-in (they collide with order ids). Cards are Luhn-validated, IBANs
|
|
102
|
+
checksum-validated — order numbers don't false-positive.
|
|
103
|
+
- The moderation guard fails **open** by default (`onError: 'block'` for
|
|
104
|
+
zero-tolerance deployments); deterministic guards still run during an
|
|
105
|
+
outage.
|
|
106
|
+
- The grounding validator flags completed-action claims unsupported by this
|
|
107
|
+
turn's tool calls, flow state, or citations and rewrites them out; if no
|
|
108
|
+
safe rewrite exists, it blocks.
|
|
109
|
+
- A pre-turn block emits a `safety-blocked` stream part with the moderator id
|
|
110
|
+
and rationale, then the user-facing message.
|
|
111
|
+
|
|
68
112
|
## Output Redaction
|
|
69
113
|
|
|
70
114
|
`outputRedaction` is a defense-in-depth filter for streamed text.
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "git+https://github.com/kuralle/kuralle-agents.git",
|
|
7
7
|
"directory": "packages/kuralle-core"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.9.0",
|
|
10
10
|
"description": "A framework for structured conversational AI agents",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"typescript": "^5.3.0",
|
|
104
104
|
"vitest": "^3.2.4",
|
|
105
105
|
"zod": "^4.0.0",
|
|
106
|
-
"@kuralle-agents/realtime-audio": "0.
|
|
106
|
+
"@kuralle-agents/realtime-audio": "0.9.0"
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
|
109
109
|
"chrono-node": "^2.6.0"
|