@kuralle-agents/core 0.3.20 → 0.4.1
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 +1 -1
- package/dist/capabilities/ValidationCapability.d.ts +2 -0
- package/dist/eval/EvalRunner.js +1 -1
- package/dist/flow/collectDigression.js +0 -2
- package/dist/flow/collectUntilComplete.js +4 -1
- package/dist/flow/degrade.js +4 -1
- package/dist/foundation/DefaultConversationEventLog.js +1 -1
- package/dist/runtime/Runtime.js +10 -2
- package/dist/runtime/channels/TextDriver.js +88 -67
- package/dist/runtime/channels/VoiceDriver.js +84 -11
- package/dist/runtime/channels/streaming/SentenceAggregator.d.ts +12 -0
- package/dist/runtime/channels/streaming/SentenceAggregator.js +196 -0
- package/dist/runtime/channels/streaming/deferredTokenSource.d.ts +8 -0
- package/dist/runtime/channels/streaming/deferredTokenSource.js +45 -0
- package/dist/runtime/channels/streaming/mode.d.ts +4 -0
- package/dist/runtime/channels/streaming/mode.js +16 -0
- package/dist/runtime/channels/streaming/speakGated.d.ts +26 -0
- package/dist/runtime/channels/streaming/speakGated.js +103 -0
- package/dist/types/channel.d.ts +2 -0
- package/dist/types/processors.d.ts +14 -1
- package/dist/types/stream.d.ts +22 -1
- package/dist/types/voice.d.ts +12 -1
- package/guides/AGENTS.md +2 -2
- package/guides/FLOWS.md +1 -1
- package/guides/GETTING_STARTED.md +1 -1
- package/guides/TOOLS.md +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ const runtime = createRuntime({ agents: [agent], defaultAgentId: 'support' });
|
|
|
48
48
|
|
|
49
49
|
const handle = runtime.run({ input: 'Hello', sessionId: 'demo' });
|
|
50
50
|
for await (const part of handle.events) { // events is a property, not a method
|
|
51
|
-
if (part.type === 'text-delta') process.stdout.write(part.
|
|
51
|
+
if (part.type === 'text-delta') process.stdout.write(part.delta);
|
|
52
52
|
if (part.type === 'done') console.log('\nSession:', part.sessionId);
|
|
53
53
|
}
|
|
54
54
|
await handle; // resolves to TurnResult once the stream is consumed
|
|
@@ -3,6 +3,8 @@ import type { Session, SourceRef, ToolCallRecord } from '../types/index.js';
|
|
|
3
3
|
export interface ValidationCapability {
|
|
4
4
|
readonly name: string;
|
|
5
5
|
readonly order?: 'parallel' | 'serial';
|
|
6
|
+
/** Absent ⇒ `turn` (buffered, safe). Streaming is an explicit opt-in by the gate author. */
|
|
7
|
+
readonly streamGranularity?: 'sentence' | 'turn';
|
|
6
8
|
validate(input: ValidateInput): Promise<ValidateDecision>;
|
|
7
9
|
}
|
|
8
10
|
export interface ValidateInput {
|
package/dist/eval/EvalRunner.js
CHANGED
|
@@ -25,7 +25,7 @@ export class EvalRunner {
|
|
|
25
25
|
const handle = runtime.run({ input: turn.input, sessionId });
|
|
26
26
|
for await (const part of handle.events) {
|
|
27
27
|
if (part.type === 'text-delta') {
|
|
28
|
-
response += part.
|
|
28
|
+
response += part.delta;
|
|
29
29
|
}
|
|
30
30
|
if (part.type === 'tool-call') {
|
|
31
31
|
toolsCalled.push(part.toolName);
|
|
@@ -76,8 +76,6 @@ export async function runCollectDigression(options) {
|
|
|
76
76
|
};
|
|
77
77
|
const turn = await driver.runAgentTurn(resolveReplyNode(replyNode, run.state, { freeConversation: true }), ctx);
|
|
78
78
|
if (turn.text.trim()) {
|
|
79
|
-
ctx.emit({ type: 'text-delta', text: turn.text });
|
|
80
|
-
ctx.emit({ type: 'turn-end' });
|
|
81
79
|
appendAssistantMessage(run, turn.text);
|
|
82
80
|
}
|
|
83
81
|
if (turn.control?.type === 'handoff') {
|
|
@@ -111,7 +111,10 @@ export function emitCollectAsk(node, run, ctx) {
|
|
|
111
111
|
if (!text.trim()) {
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
const id = crypto.randomUUID();
|
|
115
|
+
ctx.emit({ type: 'text-start', id });
|
|
116
|
+
ctx.emit({ type: 'text-delta', id, delta: text });
|
|
117
|
+
ctx.emit({ type: 'text-end', id });
|
|
115
118
|
ctx.emit({ type: 'turn-end' });
|
|
116
119
|
appendAssistantMessage(run, text);
|
|
117
120
|
}
|
package/dist/flow/degrade.js
CHANGED
|
@@ -6,7 +6,10 @@ export function findEscalateNode(registry) {
|
|
|
6
6
|
export function appendSafeAssistantMessage(run, ctx, text = SAFE_DEGRADED_MESSAGE) {
|
|
7
7
|
const message = { role: 'assistant', content: text };
|
|
8
8
|
run.messages = [...run.messages, message];
|
|
9
|
-
|
|
9
|
+
const id = crypto.randomUUID();
|
|
10
|
+
ctx.emit({ type: 'text-start', id });
|
|
11
|
+
ctx.emit({ type: 'text-delta', id, delta: text });
|
|
12
|
+
ctx.emit({ type: 'text-end', id });
|
|
10
13
|
}
|
|
11
14
|
export async function degradeFlowError(flow, registry, run, driver, ctx, dispatchNode) {
|
|
12
15
|
appendSafeAssistantMessage(run, ctx);
|
|
@@ -33,7 +33,7 @@ export class DefaultConversationEventLog {
|
|
|
33
33
|
if (part.type === 'text-delta') {
|
|
34
34
|
const prevRaw = context.session.workingMemory[ASSISTANT_TEXT_KEY];
|
|
35
35
|
const prev = typeof prevRaw === 'string' ? prevRaw : '';
|
|
36
|
-
context.session.workingMemory[ASSISTANT_TEXT_KEY] = prev + part.
|
|
36
|
+
context.session.workingMemory[ASSISTANT_TEXT_KEY] = prev + part.delta;
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
const base = {
|
package/dist/runtime/Runtime.js
CHANGED
|
@@ -74,7 +74,12 @@ export class Runtime {
|
|
|
74
74
|
tools: effectTools,
|
|
75
75
|
enforcer: policies.enforcer,
|
|
76
76
|
agentId: opened.agent.id,
|
|
77
|
-
onInterim: (message) =>
|
|
77
|
+
onInterim: (message) => {
|
|
78
|
+
const id = crypto.randomUUID();
|
|
79
|
+
emit({ type: 'text-start', id });
|
|
80
|
+
emit({ type: 'text-delta', id, delta: message });
|
|
81
|
+
emit({ type: 'text-end', id });
|
|
82
|
+
},
|
|
78
83
|
});
|
|
79
84
|
const steps = await loadRecordedSteps(opened.runStore, opened.runState.runId);
|
|
80
85
|
const freshRunState = (await opened.runStore.getRunState(opened.runState.runId)) ?? opened.runState;
|
|
@@ -173,7 +178,10 @@ export class Runtime {
|
|
|
173
178
|
if (isDegradableRuntimeError(error)) {
|
|
174
179
|
const message = error instanceof Error ? error.message : String(error);
|
|
175
180
|
emit({ type: 'error', error: message });
|
|
176
|
-
|
|
181
|
+
const degradedId = crypto.randomUUID();
|
|
182
|
+
emit({ type: 'text-start', id: degradedId });
|
|
183
|
+
emit({ type: 'text-delta', id: degradedId, delta: SAFE_DEGRADED_MESSAGE });
|
|
184
|
+
emit({ type: 'text-end', id: degradedId });
|
|
177
185
|
runCtx.runState.messages = [
|
|
178
186
|
...runCtx.runState.messages,
|
|
179
187
|
{ role: 'assistant', content: SAFE_DEGRADED_MESSAGE },
|
|
@@ -6,6 +6,8 @@ import { consumePendingUserInput } from './inputBuffer.js';
|
|
|
6
6
|
import { runSilentExtraction } from './extractionTurn.js';
|
|
7
7
|
import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
|
|
8
8
|
import { resolveMaxSteps } from '../policies/limits.js';
|
|
9
|
+
import { speakGated } from './streaming/speakGated.js';
|
|
10
|
+
import { resolveStreamMode } from './streaming/mode.js';
|
|
9
11
|
import { appendGatherBlocks, resolveNodeGatherScope, runGatherPhase } from '../grounding/index.js';
|
|
10
12
|
import { isFlowTransitionControlTool } from '../../flow/flowControlTools.js';
|
|
11
13
|
import { resolveStructuredDecide } from '../../flow/choiceMatch.js';
|
|
@@ -24,7 +26,10 @@ export class TextDriver {
|
|
|
24
26
|
const preTurn = await applyPreTurnPolicies(ctx);
|
|
25
27
|
if (!preTurn.proceed) {
|
|
26
28
|
const blocked = preTurn.blockedMessage ?? 'Input blocked by guardrails';
|
|
27
|
-
|
|
29
|
+
const id = crypto.randomUUID();
|
|
30
|
+
ctx.emit({ type: 'text-start', id });
|
|
31
|
+
ctx.emit({ type: 'text-delta', id, delta: blocked });
|
|
32
|
+
ctx.emit({ type: 'text-end', id });
|
|
28
33
|
ctx.emit({ type: 'turn-end' });
|
|
29
34
|
return { text: blocked, toolResults: [] };
|
|
30
35
|
}
|
|
@@ -39,73 +44,89 @@ export class TextDriver {
|
|
|
39
44
|
const aiTools = this.resolveTools(node, ctx);
|
|
40
45
|
const maxSteps = resolveMaxSteps(ctx.limits, this.maxSteps);
|
|
41
46
|
const toolCallsMade = [];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
const mode = resolveStreamMode(ctx, node);
|
|
48
|
+
const turnId = crypto.randomUUID();
|
|
49
|
+
const source = {
|
|
50
|
+
async *[Symbol.asyncIterator]() {
|
|
51
|
+
for (let step = 0; step < maxSteps; step += 1) {
|
|
52
|
+
const result = streamText({
|
|
53
|
+
model,
|
|
54
|
+
system,
|
|
55
|
+
messages,
|
|
56
|
+
tools: aiTools,
|
|
57
|
+
abortSignal: ctx.abortSignal,
|
|
58
|
+
});
|
|
59
|
+
for await (const part of result.fullStream) {
|
|
60
|
+
if (part.type === 'text-delta') {
|
|
61
|
+
yield { delta: part.text };
|
|
62
|
+
}
|
|
63
|
+
if (part.type === 'error') {
|
|
64
|
+
const err = part.error;
|
|
65
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
66
|
+
ctx.emit({ type: 'error', error: message });
|
|
67
|
+
throw err instanceof Error ? err : new Error(message);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const finishReason = await result.finishReason;
|
|
71
|
+
const response = await result.response;
|
|
72
|
+
messages.push(...response.messages);
|
|
73
|
+
if (finishReason !== 'tool-calls') {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
const toolCalls = await result.toolCalls;
|
|
77
|
+
for (const call of toolCalls) {
|
|
78
|
+
ctx.emit({
|
|
79
|
+
type: 'tool-call',
|
|
80
|
+
toolName: call.toolName,
|
|
81
|
+
args: call.input,
|
|
82
|
+
toolCallId: call.toolCallId,
|
|
83
|
+
});
|
|
84
|
+
const { result: toolResult, control, failed } = await executeModelToolCall(ctx, { toolName: call.toolName, input: call.input, toolCallId: call.toolCallId }, node.localTools);
|
|
85
|
+
out.toolResults.push({
|
|
86
|
+
name: call.toolName,
|
|
87
|
+
args: call.input,
|
|
88
|
+
result: toolResult,
|
|
89
|
+
toolCallId: call.toolCallId,
|
|
90
|
+
});
|
|
91
|
+
toolCallsMade.push({
|
|
92
|
+
toolCallId: call.toolCallId,
|
|
93
|
+
toolName: call.toolName,
|
|
94
|
+
args: call.input,
|
|
95
|
+
result: toolResult,
|
|
96
|
+
success: !failed,
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
});
|
|
99
|
+
out.control ??= control;
|
|
100
|
+
ctx.emit({
|
|
101
|
+
type: 'tool-result',
|
|
102
|
+
toolName: call.toolName,
|
|
103
|
+
result: toolResult,
|
|
104
|
+
toolCallId: call.toolCallId,
|
|
105
|
+
});
|
|
106
|
+
messages.push(toolResultMessage({ toolName: call.toolName, input: call.input, toolCallId: call.toolCallId }, toolResult));
|
|
107
|
+
}
|
|
54
108
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const { result: toolResult, control, failed } = await executeModelToolCall(ctx, { toolName: call.toolName, input: call.input, toolCallId: call.toolCallId }, node.localTools);
|
|
77
|
-
out.toolResults.push({
|
|
78
|
-
name: call.toolName,
|
|
79
|
-
args: call.input,
|
|
80
|
-
result: toolResult,
|
|
81
|
-
toolCallId: call.toolCallId,
|
|
82
|
-
});
|
|
83
|
-
toolCallsMade.push({
|
|
84
|
-
toolCallId: call.toolCallId,
|
|
85
|
-
toolName: call.toolName,
|
|
86
|
-
args: call.input,
|
|
87
|
-
result: toolResult,
|
|
88
|
-
success: !failed,
|
|
89
|
-
timestamp: Date.now(),
|
|
90
|
-
});
|
|
91
|
-
out.control ??= control;
|
|
92
|
-
ctx.emit({
|
|
93
|
-
type: 'tool-result',
|
|
94
|
-
toolName: call.toolName,
|
|
95
|
-
result: toolResult,
|
|
96
|
-
toolCallId: call.toolCallId,
|
|
97
|
-
});
|
|
98
|
-
messages.push(toolResultMessage({ toolName: call.toolName, input: call.input, toolCallId: call.toolCallId }, toolResult));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
const postTurn = await applyPostTurnPolicies(ctx, draftText, toolCallsMade, gather.citations ?? []);
|
|
102
|
-
out.confidence = postTurn.confidence;
|
|
103
|
-
out.control = postTurn.control;
|
|
104
|
-
const emitText = postTurn.control ? (postTurn.blockedMessage ?? postTurn.text) : postTurn.text;
|
|
105
|
-
out.text = emitText;
|
|
106
|
-
if (emitText) {
|
|
107
|
-
ctx.emit({ type: 'text-delta', text: emitText });
|
|
108
|
-
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
const spoken = await speakGated({
|
|
112
|
+
ctx,
|
|
113
|
+
mode,
|
|
114
|
+
turnId,
|
|
115
|
+
source,
|
|
116
|
+
runGate: async (text, _final) => {
|
|
117
|
+
const r = await applyPostTurnPolicies(ctx, text, toolCallsMade, gather.citations ?? []);
|
|
118
|
+
return {
|
|
119
|
+
blocked: !r.proceed,
|
|
120
|
+
text: r.proceed ? r.text : (r.blockedMessage ?? r.text),
|
|
121
|
+
reason: r.control?.reason,
|
|
122
|
+
control: r.control,
|
|
123
|
+
confidence: r.confidence,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
out.text = spoken.text;
|
|
128
|
+
out.control = spoken.control;
|
|
129
|
+
out.confidence = spoken.confidence;
|
|
109
130
|
ctx.emit({ type: 'turn-end' });
|
|
110
131
|
return out;
|
|
111
132
|
}
|
|
@@ -4,8 +4,35 @@ import { buildNodePrompt, resolveInstructions, composeSystem } from '../../flow/
|
|
|
4
4
|
import { executeModelToolCall } from './executeModelTool.js';
|
|
5
5
|
import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
|
|
6
6
|
import { resolveMaxSteps } from '../policies/limits.js';
|
|
7
|
+
import { speakGated } from './streaming/speakGated.js';
|
|
8
|
+
import { resolveStreamMode } from './streaming/mode.js';
|
|
9
|
+
import { createDeferredTokenSource, } from './streaming/deferredTokenSource.js';
|
|
7
10
|
import { appendGatherBlocks, resolveNodeGatherScope, runGatherPhase } from '../grounding/index.js';
|
|
8
11
|
import { resolveVoiceGeminiTools } from './voiceTools.js';
|
|
12
|
+
function emitNativeRealtimePostHocGate(ctx, client, gate) {
|
|
13
|
+
if (gate.escalated) {
|
|
14
|
+
ctx.emit({
|
|
15
|
+
type: 'safety-blocked',
|
|
16
|
+
moderator: gate.moderator,
|
|
17
|
+
rationale: gate.rationale,
|
|
18
|
+
userFacingMessage: gate.safeText,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
ctx.emit({
|
|
23
|
+
type: 'pipeline-validation-block',
|
|
24
|
+
rationale: gate.rationale,
|
|
25
|
+
userFacingMessage: gate.safeText,
|
|
26
|
+
});
|
|
27
|
+
ctx.emit({
|
|
28
|
+
type: 'safety-blocked',
|
|
29
|
+
moderator: gate.moderator,
|
|
30
|
+
rationale: gate.rationale,
|
|
31
|
+
userFacingMessage: gate.safeText,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
client.requestResponse?.(gate.safeText);
|
|
35
|
+
}
|
|
9
36
|
export class VoiceDriver {
|
|
10
37
|
client;
|
|
11
38
|
toolDefs;
|
|
@@ -25,7 +52,10 @@ export class VoiceDriver {
|
|
|
25
52
|
const preTurn = await applyPreTurnPolicies(ctx);
|
|
26
53
|
if (!preTurn.proceed) {
|
|
27
54
|
const blocked = preTurn.blockedMessage ?? 'Input blocked by guardrails';
|
|
28
|
-
|
|
55
|
+
const id = crypto.randomUUID();
|
|
56
|
+
ctx.emit({ type: 'text-start', id });
|
|
57
|
+
ctx.emit({ type: 'text-delta', id, delta: blocked });
|
|
58
|
+
ctx.emit({ type: 'text-end', id });
|
|
29
59
|
ctx.emit({ type: 'turn-end' });
|
|
30
60
|
return { text: blocked, toolResults: [] };
|
|
31
61
|
}
|
|
@@ -38,17 +68,56 @@ export class VoiceDriver {
|
|
|
38
68
|
const geminiTools = resolveVoiceGeminiTools(node, { ...this.toolDefs, ...(ctx.globalTools ?? {}) }, { siloFlowControl: ctx.outOfBandControl && !node.freeConversation });
|
|
39
69
|
const toolCallsMade = [];
|
|
40
70
|
const maxSteps = resolveMaxSteps(ctx.limits, this.maxSteps);
|
|
71
|
+
const mode = resolveStreamMode(ctx, node);
|
|
72
|
+
const turnId = crypto.randomUUID();
|
|
73
|
+
const transcript = createDeferredTokenSource();
|
|
41
74
|
let draftText = '';
|
|
42
75
|
this.heardCharCount = 0;
|
|
76
|
+
let postHocGate = null;
|
|
77
|
+
const speakPromise = speakGated({
|
|
78
|
+
ctx,
|
|
79
|
+
mode,
|
|
80
|
+
turnId,
|
|
81
|
+
source: transcript.source,
|
|
82
|
+
runGate: async (text, _final) => {
|
|
83
|
+
const r = await applyPostTurnPolicies(ctx, text, toolCallsMade, gather.citations ?? []);
|
|
84
|
+
if (!r.proceed) {
|
|
85
|
+
const safeText = r.blockedMessage ?? r.text;
|
|
86
|
+
postHocGate = {
|
|
87
|
+
safeText,
|
|
88
|
+
rationale: r.control?.reason ?? 'blocked',
|
|
89
|
+
moderator: 'post-turn-gate',
|
|
90
|
+
escalated: r.control?.type === 'escalate',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
blocked: !r.proceed,
|
|
95
|
+
text: r.proceed ? r.text : (r.blockedMessage ?? r.text),
|
|
96
|
+
reason: r.control?.reason,
|
|
97
|
+
control: r.control,
|
|
98
|
+
confidence: r.confidence,
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
});
|
|
43
102
|
await this.reconfigure({ systemInstruction: system, tools: geminiTools });
|
|
44
103
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
45
|
-
const { outcome, assistantText } = await this.collectProviderTurn(ctx, out, step === 0, toolCallsMade, node.localTools);
|
|
104
|
+
const { outcome, assistantText } = await this.collectProviderTurn(ctx, out, step === 0, toolCallsMade, node.localTools, transcript);
|
|
46
105
|
draftText += assistantText;
|
|
47
106
|
if (outcome === 'interrupted') {
|
|
107
|
+
transcript.close('interrupted');
|
|
108
|
+
try {
|
|
109
|
+
await speakPromise;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* speakGated cancels in-flight stream on interrupt */
|
|
113
|
+
}
|
|
48
114
|
out.interrupted = true;
|
|
49
115
|
out.truncateAt = this.heardCharCount;
|
|
50
116
|
out.text = truncateToHeard(draftText, this.heardCharCount);
|
|
51
|
-
|
|
117
|
+
const id = crypto.randomUUID();
|
|
118
|
+
ctx.emit({ type: 'text-start', id });
|
|
119
|
+
ctx.emit({ type: 'text-delta', id, delta: out.text });
|
|
120
|
+
ctx.emit({ type: 'text-end', id });
|
|
52
121
|
ctx.emit({ type: 'turn-end' });
|
|
53
122
|
return out;
|
|
54
123
|
}
|
|
@@ -56,13 +125,15 @@ export class VoiceDriver {
|
|
|
56
125
|
break;
|
|
57
126
|
}
|
|
58
127
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
out.
|
|
62
|
-
|
|
63
|
-
out.
|
|
64
|
-
if (
|
|
65
|
-
|
|
128
|
+
transcript.close('complete');
|
|
129
|
+
const spoken = await speakPromise;
|
|
130
|
+
out.text = spoken.text;
|
|
131
|
+
out.control = spoken.control;
|
|
132
|
+
out.confidence = spoken.confidence;
|
|
133
|
+
if (postHocGate) {
|
|
134
|
+
// Advisory post-hoc gate: provider audio may already have played; correction does not un-speak it.
|
|
135
|
+
emitNativeRealtimePostHocGate(ctx, this.client, postHocGate);
|
|
136
|
+
out.gateScope = 'advisory';
|
|
66
137
|
}
|
|
67
138
|
ctx.emit({ type: 'turn-end' });
|
|
68
139
|
return out;
|
|
@@ -94,7 +165,7 @@ export class VoiceDriver {
|
|
|
94
165
|
getHeardCharCount() {
|
|
95
166
|
return this.heardCharCount;
|
|
96
167
|
}
|
|
97
|
-
async collectProviderTurn(ctx, out, triggerResponse, toolCallsMade, localTools) {
|
|
168
|
+
async collectProviderTurn(ctx, out, triggerResponse, toolCallsMade, localTools, transcript) {
|
|
98
169
|
return new Promise((resolve, reject) => {
|
|
99
170
|
let assistantText = '';
|
|
100
171
|
let settled = false;
|
|
@@ -111,12 +182,14 @@ export class VoiceDriver {
|
|
|
111
182
|
return;
|
|
112
183
|
settled = true;
|
|
113
184
|
cleanup();
|
|
185
|
+
transcript.close('error');
|
|
114
186
|
reject(error);
|
|
115
187
|
};
|
|
116
188
|
const onTranscript = (text, role) => {
|
|
117
189
|
if (role === 'assistant') {
|
|
118
190
|
assistantText += text;
|
|
119
191
|
this.heardCharCount += text.length;
|
|
192
|
+
transcript.push(text);
|
|
120
193
|
}
|
|
121
194
|
if (role === 'user' && sawInterrupt) {
|
|
122
195
|
this.pendingBargeInInput = text;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function matchEndOfSentence(text: string): number;
|
|
2
|
+
export declare class SentenceAggregator {
|
|
3
|
+
private buffer;
|
|
4
|
+
private needsLookahead;
|
|
5
|
+
private pendingPeriodConfirm;
|
|
6
|
+
push(tokenText: string): string[];
|
|
7
|
+
flush(): string | null;
|
|
8
|
+
private confirmPendingPeriod;
|
|
9
|
+
private checkSentenceWithLookahead;
|
|
10
|
+
private drainConfirmedAtBufferEnd;
|
|
11
|
+
private emitFirstSentenceIfFound;
|
|
12
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const ABBREVIATIONS = [
|
|
2
|
+
'mr.',
|
|
3
|
+
'mrs.',
|
|
4
|
+
'ms.',
|
|
5
|
+
'dr.',
|
|
6
|
+
'prof.',
|
|
7
|
+
'st.',
|
|
8
|
+
'e.g.',
|
|
9
|
+
'i.e.',
|
|
10
|
+
'etc.',
|
|
11
|
+
'vs.',
|
|
12
|
+
'no.',
|
|
13
|
+
];
|
|
14
|
+
const TERMINAL_PUNCT_RE = /(?:\.{3}|[!?]+|\.)/g;
|
|
15
|
+
const MIN_WORDS_TO_CONFIRM_PERIOD_AT_TOKEN_END = 3;
|
|
16
|
+
function hasWordCharBefore(text, index) {
|
|
17
|
+
return index > 0 && /\w/.test(text[index - 1]);
|
|
18
|
+
}
|
|
19
|
+
function isDecimalPeriod(text, periodIndex) {
|
|
20
|
+
if (text[periodIndex] !== '.')
|
|
21
|
+
return false;
|
|
22
|
+
const before = periodIndex > 0 ? text[periodIndex - 1] : '';
|
|
23
|
+
const after = periodIndex < text.length - 1 ? text[periodIndex + 1] : '';
|
|
24
|
+
return /\d/.test(before) && /\d/.test(after);
|
|
25
|
+
}
|
|
26
|
+
function isAbbreviationPeriod(text, periodIndex) {
|
|
27
|
+
if (text[periodIndex] !== '.')
|
|
28
|
+
return false;
|
|
29
|
+
const prefix = text.slice(0, periodIndex + 1).toLowerCase();
|
|
30
|
+
for (const abbr of ABBREVIATIONS) {
|
|
31
|
+
if (prefix.endsWith(abbr)) {
|
|
32
|
+
const start = prefix.length - abbr.length;
|
|
33
|
+
if (!hasWordCharBefore(prefix, start))
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
for (let len = 1; len < abbr.length; len++) {
|
|
37
|
+
const partial = abbr.slice(0, len);
|
|
38
|
+
if (!partial.endsWith('.'))
|
|
39
|
+
continue;
|
|
40
|
+
if (!prefix.endsWith(partial))
|
|
41
|
+
continue;
|
|
42
|
+
const start = prefix.length - partial.length;
|
|
43
|
+
if (!hasWordCharBefore(prefix, start))
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
function endsWithSentencePunctuation(text) {
|
|
50
|
+
if (/[!?]+$/.test(text))
|
|
51
|
+
return true;
|
|
52
|
+
if (/\.{3,}$/.test(text))
|
|
53
|
+
return true;
|
|
54
|
+
if (/\.$/.test(text) && !/\.{2,}$/.test(text))
|
|
55
|
+
return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function endsWithIncompleteEllipsis(text) {
|
|
59
|
+
return /\.{2}$/.test(text) && !/\.{3,}$/.test(text);
|
|
60
|
+
}
|
|
61
|
+
function remainderStartsNewSentence(text, endIndex) {
|
|
62
|
+
const rest = text.slice(endIndex);
|
|
63
|
+
return /^\s*$/.test(rest) || /^\s+[A-Z]/.test(rest);
|
|
64
|
+
}
|
|
65
|
+
function wordCount(text) {
|
|
66
|
+
const trimmed = text.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
return 0;
|
|
69
|
+
return trimmed.split(/\s+/).length;
|
|
70
|
+
}
|
|
71
|
+
function isValidSentenceEnd(text, endIndex) {
|
|
72
|
+
const punct = text.slice(0, endIndex).match(/(?:\.{3}|[!?]+|\.)$/)?.[0];
|
|
73
|
+
if (!punct)
|
|
74
|
+
return false;
|
|
75
|
+
const periodIndex = endIndex - punct.length;
|
|
76
|
+
if (punct === '.' || (punct.startsWith('.') && punct.length === 1)) {
|
|
77
|
+
if (endIndex < text.length && text[endIndex] === '.')
|
|
78
|
+
return false;
|
|
79
|
+
if (isDecimalPeriod(text, periodIndex))
|
|
80
|
+
return false;
|
|
81
|
+
if (isAbbreviationPeriod(text, periodIndex))
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (endIndex === text.length && endsWithIncompleteEllipsis(text.slice(0, endIndex))) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
export function matchEndOfSentence(text) {
|
|
90
|
+
if (!text)
|
|
91
|
+
return 0;
|
|
92
|
+
TERMINAL_PUNCT_RE.lastIndex = 0;
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = TERMINAL_PUNCT_RE.exec(text)) !== null) {
|
|
95
|
+
const end = match.index + match[0].length;
|
|
96
|
+
if (!isValidSentenceEnd(text, end))
|
|
97
|
+
continue;
|
|
98
|
+
if (remainderStartsNewSentence(text, end))
|
|
99
|
+
return end;
|
|
100
|
+
}
|
|
101
|
+
if (endsWithIncompleteEllipsis(text))
|
|
102
|
+
return 0;
|
|
103
|
+
const trailing = text.match(/(?:\.{3}|[!?]+|\.)$/);
|
|
104
|
+
if (trailing && isValidSentenceEnd(text, text.length))
|
|
105
|
+
return text.length;
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
export class SentenceAggregator {
|
|
109
|
+
buffer = '';
|
|
110
|
+
needsLookahead = false;
|
|
111
|
+
pendingPeriodConfirm = false;
|
|
112
|
+
push(tokenText) {
|
|
113
|
+
if (tokenText === '')
|
|
114
|
+
return [];
|
|
115
|
+
if (/^\s+$/.test(tokenText)) {
|
|
116
|
+
this.buffer += tokenText;
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const sentences = [];
|
|
120
|
+
if (this.pendingPeriodConfirm) {
|
|
121
|
+
const confirmed = this.confirmPendingPeriod(tokenText[0]);
|
|
122
|
+
if (confirmed !== null)
|
|
123
|
+
sentences.push(confirmed);
|
|
124
|
+
}
|
|
125
|
+
for (const char of tokenText) {
|
|
126
|
+
this.buffer += char;
|
|
127
|
+
const completed = this.checkSentenceWithLookahead(char);
|
|
128
|
+
if (completed !== null)
|
|
129
|
+
sentences.push(completed);
|
|
130
|
+
}
|
|
131
|
+
const drained = this.drainConfirmedAtBufferEnd();
|
|
132
|
+
if (drained !== null)
|
|
133
|
+
sentences.push(drained);
|
|
134
|
+
return sentences;
|
|
135
|
+
}
|
|
136
|
+
flush() {
|
|
137
|
+
if (this.buffer.length === 0)
|
|
138
|
+
return null;
|
|
139
|
+
const tail = this.buffer;
|
|
140
|
+
this.buffer = '';
|
|
141
|
+
this.needsLookahead = false;
|
|
142
|
+
this.pendingPeriodConfirm = false;
|
|
143
|
+
return tail;
|
|
144
|
+
}
|
|
145
|
+
confirmPendingPeriod(nextChar) {
|
|
146
|
+
this.pendingPeriodConfirm = false;
|
|
147
|
+
if (nextChar.trim() === '') {
|
|
148
|
+
this.needsLookahead = false;
|
|
149
|
+
return this.emitFirstSentenceIfFound();
|
|
150
|
+
}
|
|
151
|
+
this.needsLookahead = false;
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
checkSentenceWithLookahead(char) {
|
|
155
|
+
if (this.needsLookahead) {
|
|
156
|
+
if (char.trim() !== '') {
|
|
157
|
+
this.needsLookahead = false;
|
|
158
|
+
this.pendingPeriodConfirm = false;
|
|
159
|
+
return this.emitFirstSentenceIfFound();
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (this.buffer.length > 0 && endsWithSentencePunctuation(this.buffer)) {
|
|
164
|
+
this.needsLookahead = true;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
drainConfirmedAtBufferEnd() {
|
|
169
|
+
if (!this.needsLookahead)
|
|
170
|
+
return null;
|
|
171
|
+
if (this.buffer.endsWith('.') && !/\.{2,}$/.test(this.buffer)) {
|
|
172
|
+
if (wordCount(this.buffer) < MIN_WORDS_TO_CONFIRM_PERIOD_AT_TOKEN_END) {
|
|
173
|
+
this.pendingPeriodConfirm = true;
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const end = matchEndOfSentence(this.buffer);
|
|
178
|
+
if (end > 0 && end === this.buffer.length) {
|
|
179
|
+
this.needsLookahead = false;
|
|
180
|
+
this.pendingPeriodConfirm = false;
|
|
181
|
+
const sentence = this.buffer;
|
|
182
|
+
this.buffer = '';
|
|
183
|
+
return sentence.length > 0 ? sentence : null;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
emitFirstSentenceIfFound() {
|
|
188
|
+
const end = matchEndOfSentence(this.buffer);
|
|
189
|
+
if (end > 0) {
|
|
190
|
+
const sentence = this.buffer.slice(0, end);
|
|
191
|
+
this.buffer = this.buffer.slice(end);
|
|
192
|
+
return sentence.length > 0 ? sentence : null;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TokenSource } from './speakGated.js';
|
|
2
|
+
export type DeferredCloseReason = 'complete' | 'interrupted' | 'error';
|
|
3
|
+
export interface DeferredTokenSource {
|
|
4
|
+
source: TokenSource;
|
|
5
|
+
push(delta: string): void;
|
|
6
|
+
close(reason?: DeferredCloseReason): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function createDeferredTokenSource(): DeferredTokenSource;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export function createDeferredTokenSource() {
|
|
2
|
+
const queue = [];
|
|
3
|
+
let closed = false;
|
|
4
|
+
let closeReason;
|
|
5
|
+
const waiters = [];
|
|
6
|
+
const notify = () => {
|
|
7
|
+
for (const wake of waiters)
|
|
8
|
+
wake();
|
|
9
|
+
waiters.length = 0;
|
|
10
|
+
};
|
|
11
|
+
const source = {
|
|
12
|
+
async *[Symbol.asyncIterator]() {
|
|
13
|
+
while (true) {
|
|
14
|
+
while (queue.length > 0) {
|
|
15
|
+
yield queue.shift();
|
|
16
|
+
}
|
|
17
|
+
if (closed) {
|
|
18
|
+
if (closeReason === 'interrupted') {
|
|
19
|
+
throw new Error('interrupted');
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await new Promise((resolve) => {
|
|
24
|
+
waiters.push(resolve);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
source,
|
|
31
|
+
push(delta) {
|
|
32
|
+
if (closed || delta.length === 0)
|
|
33
|
+
return;
|
|
34
|
+
queue.push({ delta });
|
|
35
|
+
notify();
|
|
36
|
+
},
|
|
37
|
+
close(reason = 'complete') {
|
|
38
|
+
if (closed)
|
|
39
|
+
return;
|
|
40
|
+
closed = true;
|
|
41
|
+
closeReason = reason;
|
|
42
|
+
notify();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RunContext } from '../../../types/run-context.js';
|
|
2
|
+
import type { ResolvedNode } from '../../../types/channel.js';
|
|
3
|
+
export type StreamMode = 'token' | 'sentence' | 'turn';
|
|
4
|
+
export declare function resolveStreamMode(ctx: RunContext, node: ResolvedNode): StreamMode;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function nodeHasWholeAnswerGroundingGate(_ctx, node) {
|
|
2
|
+
return node.node.kind === 'reply' && node.node.confidenceGate != null;
|
|
3
|
+
}
|
|
4
|
+
export function resolveStreamMode(ctx, node) {
|
|
5
|
+
const grans = [
|
|
6
|
+
...(ctx.outputProcessors ?? []).map((p) => p.streamGranularity ?? 'turn'),
|
|
7
|
+
...(ctx.validationPolicies ?? []).map((p) => p.streamGranularity ?? 'turn'),
|
|
8
|
+
];
|
|
9
|
+
if (nodeHasWholeAnswerGroundingGate(ctx, node))
|
|
10
|
+
grans.push('turn');
|
|
11
|
+
if (grans.includes('turn'))
|
|
12
|
+
return 'turn';
|
|
13
|
+
if (grans.includes('sentence'))
|
|
14
|
+
return 'sentence';
|
|
15
|
+
return 'token';
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { TurnControl } from '../../../types/channel.js';
|
|
2
|
+
import type { RunContext } from '../../../types/run-context.js';
|
|
3
|
+
import type { StreamMode } from './mode.js';
|
|
4
|
+
export interface TokenSource {
|
|
5
|
+
[Symbol.asyncIterator](): AsyncIterator<{
|
|
6
|
+
delta: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
export interface GateOutcome {
|
|
10
|
+
blocked: boolean;
|
|
11
|
+
text: string;
|
|
12
|
+
reason?: string;
|
|
13
|
+
control?: TurnControl;
|
|
14
|
+
confidence?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function speakGated(args: {
|
|
17
|
+
ctx: RunContext;
|
|
18
|
+
mode: StreamMode;
|
|
19
|
+
turnId: string;
|
|
20
|
+
source: TokenSource;
|
|
21
|
+
runGate: (fullOrSentence: string, final: boolean) => Promise<GateOutcome>;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
text: string;
|
|
24
|
+
control?: TurnControl;
|
|
25
|
+
confidence?: number;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { SentenceAggregator } from './SentenceAggregator.js';
|
|
3
|
+
function emitMessage(ctx, id, text) {
|
|
4
|
+
ctx.emit({ type: 'text-start', id });
|
|
5
|
+
ctx.emit({ type: 'text-delta', id, delta: text });
|
|
6
|
+
ctx.emit({ type: 'text-end', id });
|
|
7
|
+
}
|
|
8
|
+
async function emitBlockedSafeMessage(ctx, turnId, started, outcome) {
|
|
9
|
+
if (started) {
|
|
10
|
+
ctx.emit({
|
|
11
|
+
type: 'text-cancel',
|
|
12
|
+
id: turnId,
|
|
13
|
+
reason: outcome.reason ?? 'blocked',
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const safeId = randomUUID();
|
|
17
|
+
emitMessage(ctx, safeId, outcome.text);
|
|
18
|
+
return {
|
|
19
|
+
text: outcome.text,
|
|
20
|
+
control: outcome.control,
|
|
21
|
+
confidence: outcome.confidence,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export async function speakGated(args) {
|
|
25
|
+
const { ctx, mode, turnId, source, runGate } = args;
|
|
26
|
+
if (mode === 'turn') {
|
|
27
|
+
let full = '';
|
|
28
|
+
try {
|
|
29
|
+
for await (const { delta } of source) {
|
|
30
|
+
full += delta;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
ctx.emit({ type: 'error', error: message });
|
|
36
|
+
throw err instanceof Error ? err : new Error(message);
|
|
37
|
+
}
|
|
38
|
+
const decision = await runGate(full, true);
|
|
39
|
+
emitMessage(ctx, turnId, decision.text);
|
|
40
|
+
return {
|
|
41
|
+
text: decision.text,
|
|
42
|
+
control: decision.control,
|
|
43
|
+
confidence: decision.confidence,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const agg = new SentenceAggregator();
|
|
47
|
+
let started = false;
|
|
48
|
+
let emitted = '';
|
|
49
|
+
const openOnce = () => {
|
|
50
|
+
if (!started) {
|
|
51
|
+
ctx.emit({ type: 'text-start', id: turnId });
|
|
52
|
+
started = true;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const emitCleared = (chunk) => {
|
|
56
|
+
openOnce();
|
|
57
|
+
ctx.emit({ type: 'text-delta', id: turnId, delta: chunk });
|
|
58
|
+
emitted += chunk;
|
|
59
|
+
};
|
|
60
|
+
const gateSentence = async (sentence, final) => {
|
|
61
|
+
const decision = await runGate(sentence.trim(), final);
|
|
62
|
+
if (decision.blocked) {
|
|
63
|
+
return emitBlockedSafeMessage(ctx, turnId, started, decision);
|
|
64
|
+
}
|
|
65
|
+
emitCleared(sentence);
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
for await (const { delta } of source) {
|
|
70
|
+
if (mode === 'token') {
|
|
71
|
+
openOnce();
|
|
72
|
+
ctx.emit({ type: 'text-delta', id: turnId, delta });
|
|
73
|
+
emitted += delta;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (const sentence of agg.push(delta)) {
|
|
77
|
+
const blocked = await gateSentence(sentence, false);
|
|
78
|
+
if (blocked)
|
|
79
|
+
return blocked;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (mode === 'sentence') {
|
|
83
|
+
const tail = agg.flush();
|
|
84
|
+
if (tail) {
|
|
85
|
+
const blocked = await gateSentence(tail, true);
|
|
86
|
+
if (blocked)
|
|
87
|
+
return blocked;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (started) {
|
|
91
|
+
ctx.emit({ type: 'text-end', id: turnId });
|
|
92
|
+
}
|
|
93
|
+
return { text: emitted };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
+
ctx.emit({ type: 'error', error: message });
|
|
98
|
+
if (started) {
|
|
99
|
+
ctx.emit({ type: 'text-cancel', id: turnId, reason: message });
|
|
100
|
+
}
|
|
101
|
+
throw err instanceof Error ? err : new Error(message);
|
|
102
|
+
}
|
|
103
|
+
}
|
package/dist/types/channel.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface TurnResult {
|
|
|
31
31
|
interrupted?: boolean;
|
|
32
32
|
truncateAt?: number;
|
|
33
33
|
confidence?: number;
|
|
34
|
+
/** Native realtime post-hoc gate: provider audio already played; gate is advisory only. */
|
|
35
|
+
gateScope?: 'advisory';
|
|
34
36
|
}
|
|
35
37
|
export type UserSignal = {
|
|
36
38
|
type: 'message';
|
|
@@ -37,6 +37,8 @@ export interface OutputProcessor {
|
|
|
37
37
|
id: string;
|
|
38
38
|
name?: string;
|
|
39
39
|
description?: string;
|
|
40
|
+
/** Absent ⇒ `turn` (buffered, safe). Streaming is an explicit opt-in by the gate author. */
|
|
41
|
+
streamGranularity?: 'sentence' | 'turn';
|
|
40
42
|
process: (args: {
|
|
41
43
|
text: string;
|
|
42
44
|
messages: ModelMessage[];
|
|
@@ -69,8 +71,19 @@ export interface AgentCapabilityDescriptor {
|
|
|
69
71
|
doesNotHandle?: string[];
|
|
70
72
|
}
|
|
71
73
|
export type AgentStreamPart = {
|
|
74
|
+
type: 'text-start';
|
|
75
|
+
id: string;
|
|
76
|
+
} | {
|
|
72
77
|
type: 'text-delta';
|
|
73
|
-
|
|
78
|
+
id: string;
|
|
79
|
+
delta: string;
|
|
80
|
+
} | {
|
|
81
|
+
type: 'text-end';
|
|
82
|
+
id: string;
|
|
83
|
+
} | {
|
|
84
|
+
type: 'text-cancel';
|
|
85
|
+
id: string;
|
|
86
|
+
reason: string;
|
|
74
87
|
} | {
|
|
75
88
|
type: 'tool-call';
|
|
76
89
|
toolName: string;
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -6,8 +6,19 @@ import type { ChoiceOption } from './selection.js';
|
|
|
6
6
|
* does not include `{ type: 'interactive' }`.
|
|
7
7
|
*/
|
|
8
8
|
export type HarnessStreamPart = {
|
|
9
|
+
type: 'text-start';
|
|
10
|
+
id: string;
|
|
11
|
+
} | {
|
|
9
12
|
type: 'text-delta';
|
|
10
|
-
|
|
13
|
+
id: string;
|
|
14
|
+
delta: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'text-end';
|
|
17
|
+
id: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'text-cancel';
|
|
20
|
+
id: string;
|
|
21
|
+
reason: string;
|
|
11
22
|
} | {
|
|
12
23
|
type: 'tool-call';
|
|
13
24
|
toolName: string;
|
|
@@ -56,6 +67,16 @@ export type HarnessStreamPart = {
|
|
|
56
67
|
prompt: string;
|
|
57
68
|
} | {
|
|
58
69
|
type: 'turn-end';
|
|
70
|
+
} | {
|
|
71
|
+
type: 'pipeline-validation-block';
|
|
72
|
+
rationale: string;
|
|
73
|
+
userFacingMessage?: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: 'safety-blocked';
|
|
76
|
+
moderator: string;
|
|
77
|
+
rationale: string;
|
|
78
|
+
userFacingMessage: string;
|
|
79
|
+
handlerOutcome?: 'queued' | 'connected' | 'failed';
|
|
59
80
|
} | {
|
|
60
81
|
type: 'error';
|
|
61
82
|
error: string;
|
package/dist/types/voice.d.ts
CHANGED
|
@@ -216,9 +216,20 @@ export type HarnessStreamPart = {
|
|
|
216
216
|
type: 'input';
|
|
217
217
|
text: string;
|
|
218
218
|
userId?: string;
|
|
219
|
+
} | {
|
|
220
|
+
type: 'text-start';
|
|
221
|
+
id: string;
|
|
219
222
|
} | {
|
|
220
223
|
type: 'text-delta';
|
|
221
|
-
|
|
224
|
+
id: string;
|
|
225
|
+
delta: string;
|
|
226
|
+
} | {
|
|
227
|
+
type: 'text-end';
|
|
228
|
+
id: string;
|
|
229
|
+
} | {
|
|
230
|
+
type: 'text-cancel';
|
|
231
|
+
id: string;
|
|
232
|
+
reason: string;
|
|
222
233
|
} | {
|
|
223
234
|
type: 'channel-switched';
|
|
224
235
|
from: ChannelId;
|
package/guides/AGENTS.md
CHANGED
|
@@ -43,7 +43,7 @@ const runtime = createRuntime({
|
|
|
43
43
|
|
|
44
44
|
const handle = runtime.run({ input: 'Hello', sessionId: 'demo' });
|
|
45
45
|
for await (const part of handle.events) {
|
|
46
|
-
if (part.type === 'text-delta') process.stdout.write(part.
|
|
46
|
+
if (part.type === 'text-delta') process.stdout.write(part.delta);
|
|
47
47
|
}
|
|
48
48
|
await handle;
|
|
49
49
|
```
|
|
@@ -225,7 +225,7 @@ const handle = runtime.run({
|
|
|
225
225
|
sessionId: 'demo',
|
|
226
226
|
});
|
|
227
227
|
for await (const part of handle.events) {
|
|
228
|
-
if (part.type === 'text-delta') process.stdout.write(part.
|
|
228
|
+
if (part.type === 'text-delta') process.stdout.write(part.delta);
|
|
229
229
|
}
|
|
230
230
|
await handle;
|
|
231
231
|
```
|
package/guides/FLOWS.md
CHANGED
|
@@ -60,7 +60,7 @@ const agent = defineAgent({
|
|
|
60
60
|
const runtime = createRuntime({ agents: [agent], defaultAgentId: 'booking' });
|
|
61
61
|
const handle = runtime.run({ input: 'Hi', sessionId: 'demo' });
|
|
62
62
|
for await (const part of handle.events) {
|
|
63
|
-
if (part.type === 'text-delta') process.stdout.write(part.
|
|
63
|
+
if (part.type === 'text-delta') process.stdout.write(part.delta);
|
|
64
64
|
}
|
|
65
65
|
await handle;
|
|
66
66
|
```
|
|
@@ -52,7 +52,7 @@ const runtime = createRuntime({
|
|
|
52
52
|
|
|
53
53
|
const handle = runtime.run({ input: 'Hello there', sessionId: 'demo' });
|
|
54
54
|
for await (const part of handle.events) {
|
|
55
|
-
if (part.type === 'text-delta') process.stdout.write(part.
|
|
55
|
+
if (part.type === 'text-delta') process.stdout.write(part.delta);
|
|
56
56
|
}
|
|
57
57
|
await handle;
|
|
58
58
|
```
|
package/guides/TOOLS.md
CHANGED
|
@@ -85,7 +85,7 @@ const handle = runtime.run({
|
|
|
85
85
|
sessionId: 'demo',
|
|
86
86
|
});
|
|
87
87
|
for await (const part of handle.events) {
|
|
88
|
-
if (part.type === 'text-delta') process.stdout.write(part.
|
|
88
|
+
if (part.type === 'text-delta') process.stdout.write(part.delta);
|
|
89
89
|
}
|
|
90
90
|
await handle;
|
|
91
91
|
```
|
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.4.1",
|
|
10
10
|
"description": "A framework for structured conversational AI agents",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"dotenv": "^16.4.0",
|
|
98
98
|
"typescript": "^5.3.0",
|
|
99
99
|
"zod": "^3.23.0",
|
|
100
|
-
"@kuralle-agents/realtime-audio": "0.
|
|
100
|
+
"@kuralle-agents/realtime-audio": "0.4.1"
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"chrono-node": "^2.6.0",
|