@kuralle-agents/core 0.13.0 → 0.15.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 +4 -4
- package/dist/ai-sdk/uiMessageStream.d.ts +2 -2
- package/dist/ai-sdk/uiMessageStream.js +42 -29
- package/dist/capabilities/LivePromptAssembler.d.ts +1 -1
- package/dist/capabilities/LivePromptAssembler.js +1 -1
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/eval/EvalRunner.js +4 -4
- package/dist/eval/simulation.js +4 -3
- package/dist/events/TurnHandle.d.ts +3 -3
- package/dist/flow/collectUntilComplete.js +30 -7
- package/dist/flow/degrade.js +8 -4
- package/dist/flow/emitInteractive.d.ts +2 -2
- package/dist/flow/emitInteractive.js +6 -3
- package/dist/flow/extraction.d.ts +21 -2
- package/dist/flow/extraction.js +62 -7
- package/dist/flow/reduceTransition.d.ts +2 -2
- package/dist/flow/reduceTransition.js +7 -3
- package/dist/flow/runFlow.d.ts +11 -0
- package/dist/flow/runFlow.js +68 -12
- package/dist/foundation/AgentStateController.d.ts +1 -1
- package/dist/foundation/ToolExecutor.d.ts +2 -2
- package/dist/foundation/index.d.ts +0 -11
- package/dist/foundation/index.js +0 -5
- package/dist/index.d.ts +9 -14
- package/dist/index.js +5 -6
- package/dist/outcomes/streamPart.d.ts +2 -2
- package/dist/outcomes/streamPart.js +2 -3
- package/dist/prompts/PromptBuilder.d.ts +3 -3
- package/dist/prompts/PromptBuilder.js +6 -6
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/types.d.ts +7 -2
- package/dist/runtime/InMemoryRetrievalCache.d.ts +1 -1
- package/dist/runtime/KnowledgeProvider.d.ts +3 -2
- package/dist/runtime/KnowledgeProvider.js +54 -30
- package/dist/runtime/Runtime.d.ts +9 -1
- package/dist/runtime/Runtime.js +123 -30
- package/dist/runtime/TraceRecorder.d.ts +22 -4
- package/dist/runtime/TraceRecorder.js +65 -29
- package/dist/runtime/buildAgentToolSurface.js +1 -1
- package/dist/runtime/channels/TextDriver.js +12 -9
- package/dist/runtime/channels/executeModelTool.d.ts +6 -0
- package/dist/runtime/channels/executeModelTool.js +70 -10
- package/dist/runtime/channels/extractionTurn.js +1 -1
- package/dist/runtime/channels/index.d.ts +0 -3
- package/dist/runtime/channels/index.js +0 -6
- package/dist/runtime/channels/streaming/hostControlSpeak.js +5 -1
- package/dist/runtime/channels/streaming/speakGated.js +33 -12
- package/dist/runtime/citations/index.d.ts +3 -3
- package/dist/runtime/closeRun.js +3 -2
- package/dist/runtime/controlFlowSignal.d.ts +24 -0
- package/dist/runtime/controlFlowSignal.js +29 -0
- package/dist/runtime/ctx.d.ts +3 -4
- package/dist/runtime/ctx.js +39 -5
- package/dist/runtime/durable/RunStore.js +1 -1
- package/dist/runtime/grounding/gather.d.ts +1 -1
- package/dist/runtime/grounding/inMemoryKnowledge.d.ts +1 -1
- package/dist/runtime/grounding/knowledge.d.ts +1 -1
- package/dist/runtime/hostLoop.js +39 -10
- package/dist/runtime/index.d.ts +1 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/outcomeMarking.d.ts +2 -2
- package/dist/runtime/outcomeMarking.js +2 -1
- package/dist/runtime/policies/agentTurn.d.ts +1 -1
- package/dist/runtime/runHookSafely.d.ts +3 -0
- package/dist/runtime/runHookSafely.js +19 -0
- package/dist/scheduler/index.d.ts +2 -2
- package/dist/scheduler/index.js +1 -1
- package/dist/services/TracingService.js +2 -3
- package/dist/skills/collectSkills.d.ts +11 -3
- package/dist/skills/collectSkills.js +51 -8
- package/dist/skills/compositeSkillStore.d.ts +26 -0
- package/dist/skills/compositeSkillStore.js +54 -0
- package/dist/skills/defineSkill.d.ts +21 -0
- package/dist/skills/defineSkill.js +26 -0
- package/dist/skills/fsSkillStore.d.ts +3 -0
- package/dist/skills/fsSkillStore.js +79 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.js +4 -0
- package/dist/skills/parseSkillFrontmatter.d.ts +14 -0
- package/dist/skills/parseSkillFrontmatter.js +183 -0
- package/dist/skills/wireAgentSkills.d.ts +2 -1
- package/dist/skills/wireAgentSkills.js +2 -2
- package/dist/testing/mocks.d.ts +3 -3
- package/dist/testing/mocks.js +7 -3
- package/dist/tools/controlResults.d.ts +14 -0
- package/dist/tools/controlResults.js +15 -0
- package/dist/tools/effect/ToolExecutor.d.ts +6 -0
- package/dist/tools/effect/ToolExecutor.js +71 -35
- package/dist/tools/effect/defineTool.d.ts +9 -0
- package/dist/tools/effect/defineTool.js +1 -0
- package/dist/tools/effect/errors.d.ts +2 -5
- package/dist/tools/effect/errors.js +2 -5
- package/dist/tools/fs/createFsTool.js +2 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -1
- package/dist/types/effectTool.d.ts +2 -0
- package/dist/types/flow.d.ts +1 -1
- package/dist/types/guardrails.d.ts +5 -0
- package/dist/types/hooks.d.ts +2 -2
- package/dist/types/index.d.ts +25 -16
- package/dist/types/index.js +8 -13
- package/dist/types/knowledge.d.ts +213 -0
- package/dist/types/processors.d.ts +0 -58
- package/dist/types/run-context.d.ts +3 -7
- package/dist/types/runtime.d.ts +3 -74
- package/dist/types/session.d.ts +1 -1
- package/dist/types/skills.d.ts +15 -1
- package/dist/types/stream.d.ts +133 -71
- package/dist/types/stream.js +34 -1
- package/dist/types/trace.d.ts +2 -0
- package/guides/AGENTS.md +3 -3
- package/guides/FLOWS.md +1 -1
- package/guides/GETTING_STARTED.md +1 -1
- package/guides/RUNTIME.md +20 -15
- package/guides/TOOLS.md +1 -1
- package/package.json +3 -13
- package/dist/foundation/AgentDefinition.d.ts +0 -17
- package/dist/foundation/ConversationEventLog.d.ts +0 -71
- package/dist/foundation/ConversationEventLog.js +0 -1
- package/dist/foundation/ConversationState.d.ts +0 -33
- package/dist/foundation/ConversationState.js +0 -1
- package/dist/foundation/DefaultConversationEventLog.d.ts +0 -27
- package/dist/foundation/DefaultConversationEventLog.js +0 -205
- package/dist/foundation/DefaultConversationState.d.ts +0 -36
- package/dist/foundation/DefaultConversationState.js +0 -103
- package/dist/foundation/DefaultToolExecutor.d.ts +0 -57
- package/dist/foundation/DefaultToolExecutor.js +0 -132
- package/dist/foundation/createFoundation.d.ts +0 -32
- package/dist/foundation/createFoundation.js +0 -33
- package/dist/hooks/HookRunner.d.ts +0 -37
- package/dist/hooks/HookRunner.js +0 -111
- package/dist/hooks/builtin/logging.d.ts +0 -5
- package/dist/hooks/builtin/logging.js +0 -98
- package/dist/hooks/builtin/metrics.d.ts +0 -7
- package/dist/hooks/builtin/metrics.js +0 -60
- package/dist/hooks/builtin/observability.d.ts +0 -20
- package/dist/hooks/builtin/observability.js +0 -572
- package/dist/hooks/helpers.d.ts +0 -22
- package/dist/hooks/helpers.js +0 -183
- package/dist/hooks/index.d.ts +0 -8
- package/dist/hooks/index.js +0 -6
- package/dist/realtime/RealtimeAudioClient.d.ts +0 -166
- package/dist/realtime/RealtimeAudioClient.js +0 -14
- package/dist/realtime/RealtimeSessionHandle.d.ts +0 -6
- package/dist/realtime/RealtimeSessionHandle.js +0 -1
- package/dist/realtime/index.d.ts +0 -2
- package/dist/realtime/index.js +0 -1
- package/dist/runtime/channels/VoiceDriver.d.ts +0 -31
- package/dist/runtime/channels/VoiceDriver.js +0 -321
- package/dist/runtime/channels/voiceTools.d.ts +0 -9
- package/dist/runtime/channels/voiceTools.js +0 -51
- package/dist/tools/errorHandling.d.ts +0 -35
- package/dist/tools/errorHandling.js +0 -145
- package/dist/types/voice.d.ts +0 -517
- package/dist/types/voice.js +0 -14
- package/dist/utils/isRecord.d.ts +0 -1
- package/dist/utils/isRecord.js +0 -3
- package/dist/utils/messageNormalization.d.ts +0 -2
- package/dist/utils/messageNormalization.js +0 -137
- /package/dist/{foundation/AgentDefinition.js → types/knowledge.js} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { classifyControl } from '../../flow/classifyControl.js';
|
|
2
|
-
import { toolErrorResult } from '../../tools/controlResults.js';
|
|
2
|
+
import { toolDeniedResult, toolErrorResult } from '../../tools/controlResults.js';
|
|
3
3
|
import { idempotencyKey, logicalRunId } from '../durable/idempotency.js';
|
|
4
4
|
import { findStepByKey } from '../durable/replay.js';
|
|
5
|
+
import { isApprovalDenial, isControlFlowSignal } from '../controlFlowSignal.js';
|
|
5
6
|
export async function executeModelToolCall(ctx, call, localTools, durableOpts) {
|
|
6
7
|
try {
|
|
7
8
|
const localTool = localTools?.[call.toolName];
|
|
@@ -24,30 +25,79 @@ export async function executeModelToolCall(ctx, call, localTools, durableOpts) {
|
|
|
24
25
|
return { result: toolResult, control: classifyControl(toolResult), failed: false };
|
|
25
26
|
}
|
|
26
27
|
catch (error) {
|
|
28
|
+
if (isControlFlowSignal(error)) {
|
|
29
|
+
// A suspend. Not a failure, so no client-facing error and no tool result: returning it
|
|
30
|
+
// as a value keeps this function's never-rejects contract (which `Promise.all` below
|
|
31
|
+
// depends on) while letting the dispatcher rethrow it once the batch has settled.
|
|
32
|
+
return { result: undefined, failed: true, signal: error };
|
|
33
|
+
}
|
|
34
|
+
if (isApprovalDenial(error)) {
|
|
35
|
+
// A human declined. The model asked for this call, so the model is told — otherwise
|
|
36
|
+
// the turn dies and the user never hears why. No client error part: nothing broke.
|
|
37
|
+
return { result: toolDeniedResult(error.toolName, error.by), failed: true };
|
|
38
|
+
}
|
|
27
39
|
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
-
ctx.emit({ type: 'error', error: message });
|
|
40
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
|
|
29
41
|
return { result: toolErrorResult(error), failed: true };
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
44
|
function isParallelSafeTool(def) {
|
|
33
45
|
return def?.parallelSafe === true || def?.replay === false;
|
|
34
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Runs `task` over every item, at most `limit` at a time, preserving result order.
|
|
49
|
+
*
|
|
50
|
+
* Unbounded `Promise.all` over a model-emitted batch lets the model decide how many
|
|
51
|
+
* sockets, subprocesses, or rate-limited API calls open at once. `limit` is a ceiling on
|
|
52
|
+
* that; omitting it keeps the previous unbounded behaviour.
|
|
53
|
+
*
|
|
54
|
+
* `task` is expected not to reject (see the caller's contract). If one does, the rejection
|
|
55
|
+
* propagates and in-flight siblings still run to completion — they are not cancelled, so
|
|
56
|
+
* their journal writes complete rather than being abandoned half-done.
|
|
57
|
+
*/
|
|
58
|
+
async function runWithConcurrency(items, task, limit) {
|
|
59
|
+
if (limit === undefined || limit <= 0 || limit >= items.length) {
|
|
60
|
+
return Promise.all(items.map((item) => task(item)));
|
|
61
|
+
}
|
|
62
|
+
const results = new Array(items.length);
|
|
63
|
+
let next = 0;
|
|
64
|
+
const worker = async () => {
|
|
65
|
+
while (next < items.length) {
|
|
66
|
+
const index = next++;
|
|
67
|
+
results[index] = await task(items[index]);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
35
73
|
export async function dispatchModelToolCalls(ctx, toolCalls, localTools, onEach) {
|
|
74
|
+
/** Resolves to a control-flow signal when the call suspended, otherwise `undefined`. */
|
|
36
75
|
const runOne = async (call, durableOpts) => {
|
|
37
76
|
ctx.emit({
|
|
77
|
+
channel: 'internal',
|
|
38
78
|
type: 'tool-call',
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
payload: {
|
|
80
|
+
toolName: call.toolName,
|
|
81
|
+
args: call.input,
|
|
82
|
+
toolCallId: call.toolCallId,
|
|
83
|
+
},
|
|
42
84
|
});
|
|
43
85
|
const outcome = await executeModelToolCall(ctx, call, localTools, durableOpts);
|
|
86
|
+
if (outcome.signal !== undefined) {
|
|
87
|
+
// Suspended: no result exists, so nothing may reach the model or the transcript.
|
|
88
|
+
return outcome.signal;
|
|
89
|
+
}
|
|
44
90
|
onEach({ call, outcome });
|
|
45
91
|
ctx.emit({
|
|
92
|
+
channel: 'internal',
|
|
46
93
|
type: 'tool-result',
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
94
|
+
payload: {
|
|
95
|
+
toolName: call.toolName,
|
|
96
|
+
result: outcome.result,
|
|
97
|
+
toolCallId: call.toolCallId,
|
|
98
|
+
},
|
|
50
99
|
});
|
|
100
|
+
return undefined;
|
|
51
101
|
};
|
|
52
102
|
const runParallel = async (parallel) => {
|
|
53
103
|
const callsites = ctx.reserveCallsites(parallel.length);
|
|
@@ -76,12 +126,22 @@ export async function dispatchModelToolCalls(ctx, toolCalls, localTools, onEach)
|
|
|
76
126
|
const index = needsIndex ? indices?.[unresolvedCursor++] : hit?.index;
|
|
77
127
|
return { call, callsite: callsites[i], index };
|
|
78
128
|
});
|
|
79
|
-
|
|
129
|
+
// executeModelToolCall never rejects — tool errors resolve with failed: true and control-flow
|
|
130
|
+
// signals resolve as outcome.signal — so no sibling can be abandoned mid-flight with its
|
|
131
|
+
// finalizeStep unawaited. A suspend is rethrown only after every sibling has settled: you
|
|
132
|
+
// cannot cancel an in-flight promise, so failing fast here would let the work happen anyway
|
|
133
|
+
// while the journal recorded it as still running.
|
|
134
|
+
const signals = await runWithConcurrency(assignments, ({ call, callsite, index }) => runOne(call, { callsite, index }), ctx.limits?.maxToolConcurrency);
|
|
135
|
+
const suspended = signals.find((signal) => signal !== undefined);
|
|
136
|
+
if (suspended !== undefined)
|
|
137
|
+
throw suspended;
|
|
80
138
|
};
|
|
81
139
|
for (let cursor = 0; cursor < toolCalls.length;) {
|
|
82
140
|
const call = toolCalls[cursor];
|
|
83
141
|
if (!isParallelSafeTool(localTools[call.toolName])) {
|
|
84
|
-
await runOne(call);
|
|
142
|
+
const signal = await runOne(call);
|
|
143
|
+
if (signal !== undefined)
|
|
144
|
+
throw signal;
|
|
85
145
|
cursor += 1;
|
|
86
146
|
continue;
|
|
87
147
|
}
|
|
@@ -36,7 +36,7 @@ export async function runSilentExtraction(node, ctx, model, maxSteps) {
|
|
|
36
36
|
if (part.type === 'error') {
|
|
37
37
|
const err = part.error;
|
|
38
38
|
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
-
ctx.emit({ type: 'error', error: message });
|
|
39
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
|
|
40
40
|
throw err instanceof Error ? err : new Error(message);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
export type { ChannelDriver } from '../../types/channel.js';
|
|
2
2
|
export { TextDriver, buildNodePrompt } from './TextDriver.js';
|
|
3
3
|
export type { TextDriverConfig } from './TextDriver.js';
|
|
4
|
-
export { VoiceDriver } from './VoiceDriver.js';
|
|
5
|
-
export type { VoiceDriverConfig } from './VoiceDriver.js';
|
|
6
|
-
export { resolveVoiceGeminiTools, v2ToolsToGemini } from './voiceTools.js';
|
|
7
4
|
export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './inputBuffer.js';
|
|
@@ -1,8 +1,2 @@
|
|
|
1
1
|
export { TextDriver, buildNodePrompt } from './TextDriver.js';
|
|
2
|
-
// PAUSED: the realtime VoiceDriver is not on the primary (text) path and is not
|
|
3
|
-
// re-exported from the package's headline API. It stays here for the realtime
|
|
4
|
-
// stack (`@kuralle-agents/realtime-audio`) via the `/runtime` subpath. Text is
|
|
5
|
-
// the primary primitive; cascaded voice runs over text (see livekit-plugin).
|
|
6
|
-
export { VoiceDriver } from './VoiceDriver.js';
|
|
7
|
-
export { resolveVoiceGeminiTools, v2ToolsToGemini } from './voiceTools.js';
|
|
8
2
|
export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './inputBuffer.js';
|
|
@@ -72,7 +72,11 @@ export async function speakWithHostControl(args) {
|
|
|
72
72
|
toolControl = getToolControl();
|
|
73
73
|
if (toolControl) {
|
|
74
74
|
if (spoken.text) {
|
|
75
|
-
ctx.emit({
|
|
75
|
+
ctx.emit({
|
|
76
|
+
channel: 'client',
|
|
77
|
+
type: 'text-cancel',
|
|
78
|
+
payload: { id: turnId, reason: 'host-control' },
|
|
79
|
+
});
|
|
76
80
|
}
|
|
77
81
|
return { text: '', control: toolControl };
|
|
78
82
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isControlFlowSignal } from '../../controlFlowSignal.js';
|
|
2
3
|
import { SentenceAggregator } from './SentenceAggregator.js';
|
|
3
4
|
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 });
|
|
5
|
+
ctx.emit({ channel: 'client', type: 'text-start', payload: { id } });
|
|
6
|
+
ctx.emit({ channel: 'client', type: 'text-delta', payload: { id, delta: text } });
|
|
7
|
+
ctx.emit({ channel: 'client', type: 'text-end', payload: { id } });
|
|
7
8
|
}
|
|
8
9
|
async function emitBlockedSafeMessage(ctx, turnId, started, outcome) {
|
|
9
10
|
if (started) {
|
|
10
11
|
ctx.emit({
|
|
12
|
+
channel: 'client',
|
|
11
13
|
type: 'text-cancel',
|
|
12
|
-
id: turnId,
|
|
13
|
-
reason: outcome.reason ?? 'blocked',
|
|
14
|
+
payload: { id: turnId, reason: outcome.reason ?? 'blocked' },
|
|
14
15
|
});
|
|
15
16
|
}
|
|
16
17
|
const safeId = randomUUID();
|
|
@@ -31,8 +32,14 @@ export async function speakGated(args) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
catch (err) {
|
|
35
|
+
// A suspend unwinds through here on its way to the host loop. It is not a failure and
|
|
36
|
+
// must not reach the user as one — the same rule executeModelToolCall and runFlow
|
|
37
|
+
// already apply. This third path was missed, so an approval pause still surfaced as
|
|
38
|
+
// "error: Run suspended waiting for __approval" mid-conversation.
|
|
39
|
+
if (isControlFlowSignal(err))
|
|
40
|
+
throw err;
|
|
34
41
|
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
-
ctx.emit({ type: 'error', error: message });
|
|
42
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
|
|
36
43
|
throw err instanceof Error ? err : new Error(message);
|
|
37
44
|
}
|
|
38
45
|
const decision = await runGate(full, true);
|
|
@@ -48,13 +55,17 @@ export async function speakGated(args) {
|
|
|
48
55
|
let emitted = '';
|
|
49
56
|
const openOnce = () => {
|
|
50
57
|
if (!started) {
|
|
51
|
-
ctx.emit({ type: 'text-start', id: turnId });
|
|
58
|
+
ctx.emit({ channel: 'client', type: 'text-start', payload: { id: turnId } });
|
|
52
59
|
started = true;
|
|
53
60
|
}
|
|
54
61
|
};
|
|
55
62
|
const emitCleared = (chunk) => {
|
|
56
63
|
openOnce();
|
|
57
|
-
ctx.emit({
|
|
64
|
+
ctx.emit({
|
|
65
|
+
channel: 'client',
|
|
66
|
+
type: 'text-delta',
|
|
67
|
+
payload: { id: turnId, delta: chunk },
|
|
68
|
+
});
|
|
58
69
|
emitted += chunk;
|
|
59
70
|
};
|
|
60
71
|
const gateSentence = async (sentence, final) => {
|
|
@@ -69,7 +80,11 @@ export async function speakGated(args) {
|
|
|
69
80
|
for await (const { delta } of source) {
|
|
70
81
|
if (mode === 'token') {
|
|
71
82
|
openOnce();
|
|
72
|
-
ctx.emit({
|
|
83
|
+
ctx.emit({
|
|
84
|
+
channel: 'client',
|
|
85
|
+
type: 'text-delta',
|
|
86
|
+
payload: { id: turnId, delta },
|
|
87
|
+
});
|
|
73
88
|
emitted += delta;
|
|
74
89
|
continue;
|
|
75
90
|
}
|
|
@@ -88,15 +103,21 @@ export async function speakGated(args) {
|
|
|
88
103
|
}
|
|
89
104
|
}
|
|
90
105
|
if (started) {
|
|
91
|
-
ctx.emit({ type: 'text-end', id: turnId });
|
|
106
|
+
ctx.emit({ channel: 'client', type: 'text-end', payload: { id: turnId } });
|
|
92
107
|
}
|
|
93
108
|
return { text: emitted };
|
|
94
109
|
}
|
|
95
110
|
catch (err) {
|
|
111
|
+
if (isControlFlowSignal(err))
|
|
112
|
+
throw err;
|
|
96
113
|
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
-
ctx.emit({ type: 'error', error: message });
|
|
114
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
|
|
98
115
|
if (started) {
|
|
99
|
-
ctx.emit({
|
|
116
|
+
ctx.emit({
|
|
117
|
+
channel: 'client',
|
|
118
|
+
type: 'text-cancel',
|
|
119
|
+
payload: { id: turnId, reason: message },
|
|
120
|
+
});
|
|
100
121
|
}
|
|
101
122
|
throw err instanceof Error ? err : new Error(message);
|
|
102
123
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type { AgentKnowledgeOverrides,
|
|
1
|
+
import type { AgentKnowledgeOverrides, StreamPart, KnowledgeRetrievalResult, RetrievalCacheAdapter, SourceRef } from '../../types/index.js';
|
|
2
2
|
export interface CitationRetrievalProvider {
|
|
3
3
|
retrieve(query: string, cache: RetrievalCacheAdapter | undefined, agentOverrides?: AgentKnowledgeOverrides, isVoice?: boolean): Promise<{
|
|
4
4
|
results: KnowledgeRetrievalResult[];
|
|
5
5
|
citations?: SourceRef[];
|
|
6
|
-
events:
|
|
6
|
+
events: StreamPart[];
|
|
7
7
|
}>;
|
|
8
8
|
}
|
|
9
9
|
export interface CitationRetrievalResult {
|
|
10
10
|
results: KnowledgeRetrievalResult[];
|
|
11
11
|
citations: SourceRef[];
|
|
12
|
-
events:
|
|
12
|
+
events: StreamPart[];
|
|
13
13
|
}
|
|
14
14
|
export declare function retrieveWithCitations(provider: CitationRetrievalProvider, query: string, cache: RetrievalCacheAdapter | undefined, agentOverrides?: AgentKnowledgeOverrides, isVoice?: boolean): Promise<CitationRetrievalResult>;
|
|
15
15
|
export declare function normalizeCitations(results: KnowledgeRetrievalResult[], nativeCitations?: readonly SourceRef[]): SourceRef[];
|
package/dist/runtime/closeRun.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isTerminalOutcome, markSessionOutcome } from './outcomeMarking.js';
|
|
2
2
|
import { mutateSessionWithRetry } from '../session/utils.js';
|
|
3
3
|
import { syncPendingUserInput } from './channels/inputBuffer.js';
|
|
4
|
+
import { runHookSafely } from './runHookSafely.js';
|
|
4
5
|
export async function closeRun(options) {
|
|
5
6
|
const { session, runState, runStore, sessionStore, hooks, ctx } = options;
|
|
6
7
|
runState.updatedAt = Date.now();
|
|
@@ -16,10 +17,10 @@ export async function closeRun(options) {
|
|
|
16
17
|
outcomeRecord = await markSessionOutcome(sessionStore, session, options.terminalOutcome, { reason: options.outcomeReason, markedBy: 'hook' }, ctx.emit);
|
|
17
18
|
}
|
|
18
19
|
if (outcomeRecord && isTerminalOutcome(outcomeRecord.outcome)) {
|
|
19
|
-
await hooks?.onConversationEnd?.({
|
|
20
|
+
await runHookSafely('onConversationEnd', () => hooks?.onConversationEnd?.({
|
|
20
21
|
session,
|
|
21
22
|
outcome: outcomeRecord,
|
|
22
|
-
});
|
|
23
|
+
}));
|
|
23
24
|
}
|
|
24
25
|
await mutateSessionWithRetry(sessionStore, session.id, (latest) => {
|
|
25
26
|
latest.currentAgent = runState.activeAgentId;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ToolApprovalDeniedError } from '../tools/effect/errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* The run is not finished: stop the turn, keep the journal, resume when a signal arrives.
|
|
4
|
+
* It unwinds the stack like an error but is not a failure, so it must never be reported to
|
|
5
|
+
* the model as a tool error, shown to the user as an error, or journaled as one.
|
|
6
|
+
*
|
|
7
|
+
* Only `SuspendError` qualifies. An approval *denial* is deliberately not a signal — see
|
|
8
|
+
* `isApprovalDenial`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isControlFlowSignal(error: unknown): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* A human answered "no". Neither a failure nor a suspend — the opposite of a suspend, in
|
|
13
|
+
* fact: a suspend defers the decision, a denial resolves it.
|
|
14
|
+
*
|
|
15
|
+
* Two rules follow, and they differ by who is driving the call:
|
|
16
|
+
*
|
|
17
|
+
* - It must never be degraded into "something went wrong on my side". Nothing went wrong;
|
|
18
|
+
* saying so would be a lie to the user.
|
|
19
|
+
* - On the **model** path it becomes a tool result, because there is no author code to
|
|
20
|
+
* catch it and the agent needs to be able to tell the user the request was declined.
|
|
21
|
+
* On the **flow** path it keeps propagating, because an `action` node's author chose to
|
|
22
|
+
* call the tool and can catch it or branch on `ctx.approve()` instead.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isApprovalDenial(error: unknown): error is ToolApprovalDeniedError;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SuspendError } from './durable/RunStore.js';
|
|
2
|
+
import { ToolApprovalDeniedError } from '../tools/effect/errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* The run is not finished: stop the turn, keep the journal, resume when a signal arrives.
|
|
5
|
+
* It unwinds the stack like an error but is not a failure, so it must never be reported to
|
|
6
|
+
* the model as a tool error, shown to the user as an error, or journaled as one.
|
|
7
|
+
*
|
|
8
|
+
* Only `SuspendError` qualifies. An approval *denial* is deliberately not a signal — see
|
|
9
|
+
* `isApprovalDenial`.
|
|
10
|
+
*/
|
|
11
|
+
export function isControlFlowSignal(error) {
|
|
12
|
+
return error instanceof SuspendError;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A human answered "no". Neither a failure nor a suspend — the opposite of a suspend, in
|
|
16
|
+
* fact: a suspend defers the decision, a denial resolves it.
|
|
17
|
+
*
|
|
18
|
+
* Two rules follow, and they differ by who is driving the call:
|
|
19
|
+
*
|
|
20
|
+
* - It must never be degraded into "something went wrong on my side". Nothing went wrong;
|
|
21
|
+
* saying so would be a lie to the user.
|
|
22
|
+
* - On the **model** path it becomes a tool result, because there is no author code to
|
|
23
|
+
* catch it and the agent needs to be able to tell the user the request was declined.
|
|
24
|
+
* On the **flow** path it keeps propagating, because an `action` node's author chose to
|
|
25
|
+
* call the tool and can catch it or branch on `ctx.approve()` instead.
|
|
26
|
+
*/
|
|
27
|
+
export function isApprovalDenial(error) {
|
|
28
|
+
return error instanceof ToolApprovalDeniedError;
|
|
29
|
+
}
|
package/dist/runtime/ctx.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LanguageModel } from 'ai';
|
|
2
2
|
import type { Session } from '../types/session.js';
|
|
3
|
-
import type { EffectToolExecutor,
|
|
4
|
-
import type {
|
|
3
|
+
import type { EffectToolExecutor, MemoryService, AutoRetrieveProvider, RunContext } from '../types/run-context.js';
|
|
4
|
+
import type { StreamPart } from '../types/stream.js';
|
|
5
5
|
import type { RefinementCapability } from '../capabilities/RefinementCapability.js';
|
|
6
6
|
import type { ValidationCapability } from '../capabilities/ValidationCapability.js';
|
|
7
7
|
import type { InputProcessor, OutputProcessor } from '../types/processors.js';
|
|
@@ -19,7 +19,6 @@ export interface CtxDeps {
|
|
|
19
19
|
runStore: RunStore;
|
|
20
20
|
steps: StepRecord[];
|
|
21
21
|
toolExecutor: EffectToolExecutor;
|
|
22
|
-
hookRunner?: HookRunner;
|
|
23
22
|
model: LanguageModel;
|
|
24
23
|
controlModel?: LanguageModel;
|
|
25
24
|
outOfBandControl?: boolean;
|
|
@@ -34,7 +33,7 @@ export interface CtxDeps {
|
|
|
34
33
|
bargeIn?: AbortSignal;
|
|
35
34
|
abortSignal?: AbortSignal;
|
|
36
35
|
clock?: EffectClock;
|
|
37
|
-
emit?: (part:
|
|
36
|
+
emit?: (part: StreamPart) => void;
|
|
38
37
|
}
|
|
39
38
|
export declare function createRunContext(deps: CtxDeps): Promise<RunContext>;
|
|
40
39
|
export {};
|
package/dist/runtime/ctx.js
CHANGED
|
@@ -14,6 +14,22 @@ function makeCtx(deps) {
|
|
|
14
14
|
const emit = deps.emit ?? (() => { });
|
|
15
15
|
// Mutable holder: handoff reassigns runCtx.toolExecutor; ctx.tool must see the swap.
|
|
16
16
|
const toolExecutorHolder = { executor: deps.toolExecutor };
|
|
17
|
+
let pendingAppendTail = Promise.resolve();
|
|
18
|
+
const serializePendingAppend = (operation) => {
|
|
19
|
+
const previous = pendingAppendTail;
|
|
20
|
+
let release;
|
|
21
|
+
pendingAppendTail = new Promise((resolve) => {
|
|
22
|
+
release = resolve;
|
|
23
|
+
});
|
|
24
|
+
return previous.then(async () => {
|
|
25
|
+
try {
|
|
26
|
+
return await operation();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
release();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
};
|
|
17
33
|
const consumeCallsite = () => {
|
|
18
34
|
const site = String(effectOrdinal);
|
|
19
35
|
effectOrdinal += 1;
|
|
@@ -85,9 +101,28 @@ function makeCtx(deps) {
|
|
|
85
101
|
}
|
|
86
102
|
}
|
|
87
103
|
const isRetry = hit?.status === 'running';
|
|
88
|
-
const stepIndex = options?.index ?? hit?.index
|
|
104
|
+
const stepIndex = options?.index ?? hit?.index;
|
|
89
105
|
if (!isRetry) {
|
|
90
|
-
|
|
106
|
+
const append = async () => {
|
|
107
|
+
let index = stepIndex;
|
|
108
|
+
if (index === undefined) {
|
|
109
|
+
if (deps.runStore.reserveSteps) {
|
|
110
|
+
index = (await deps.runStore.reserveSteps(deps.runState.runId, 1))[0];
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// ctx.tool owns ordinal assignment: use the store's atomic reservation when available,
|
|
114
|
+
// and serialize legacy stores that do not expose reserveSteps within this context.
|
|
115
|
+
index = steps.length;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await appendPendingStep(key, kind, name, index);
|
|
119
|
+
};
|
|
120
|
+
if (stepIndex === undefined && !deps.runStore.reserveSteps) {
|
|
121
|
+
await serializePendingAppend(append);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
await append();
|
|
125
|
+
}
|
|
91
126
|
}
|
|
92
127
|
let result;
|
|
93
128
|
try {
|
|
@@ -131,7 +166,7 @@ function makeCtx(deps) {
|
|
|
131
166
|
deps.runState.status = 'paused';
|
|
132
167
|
deps.runState.updatedAt = Date.now();
|
|
133
168
|
await deps.runStore.putRunState(deps.runState);
|
|
134
|
-
emit({ type: 'paused', waitingFor: signalName });
|
|
169
|
+
emit({ channel: 'internal', type: 'paused', payload: { waitingFor: signalName } });
|
|
135
170
|
throw new SuspendError(signalName);
|
|
136
171
|
};
|
|
137
172
|
const pauseEffect = async (signalName, meta) => {
|
|
@@ -160,7 +195,6 @@ function makeCtx(deps) {
|
|
|
160
195
|
set toolExecutor(executor) {
|
|
161
196
|
toolExecutorHolder.executor = executor;
|
|
162
197
|
},
|
|
163
|
-
hookRunner: deps.hookRunner ?? {},
|
|
164
198
|
model: deps.model,
|
|
165
199
|
controlModel: deps.controlModel ?? deps.model,
|
|
166
200
|
outOfBandControl: deps.outOfBandControl ?? false,
|
|
@@ -223,7 +257,7 @@ function makeCtx(deps) {
|
|
|
223
257
|
toolCtx: options?.toolCtx,
|
|
224
258
|
});
|
|
225
259
|
if (def?.replay === false) {
|
|
226
|
-
const auditKey = `${key}:${steps.length}`;
|
|
260
|
+
const auditKey = `${key}:${steps.length}:${options?.index ?? callsite}`;
|
|
227
261
|
return replayOrExecute(auditKey, 'tool', name, executeTool, { index: options?.index });
|
|
228
262
|
}
|
|
229
263
|
return replayOrExecute(key, 'tool', name, executeTool, { index: options?.index });
|
|
@@ -3,7 +3,7 @@ export class LogConflictError extends Error {
|
|
|
3
3
|
expectedIndex;
|
|
4
4
|
actualIndex;
|
|
5
5
|
constructor(runId, expectedIndex, actualIndex) {
|
|
6
|
-
super(`Log conflict for run ${runId}: expected append at index ${expectedIndex}, current length is ${actualIndex}
|
|
6
|
+
super(`Log conflict for run ${runId}: expected append at index ${expectedIndex}, current length is ${actualIndex}. For parallel durable effects, use ctx.tool directly or reserve callsites with ctx.reserveCallsites(count) before supplying explicit indices.`);
|
|
7
7
|
this.name = 'LogConflictError';
|
|
8
8
|
this.runId = runId;
|
|
9
9
|
this.expectedIndex = expectedIndex;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SourceRef } from '../../types/
|
|
1
|
+
import type { SourceRef } from '../../types/knowledge.js';
|
|
2
2
|
import type { GatherScope, RunContext } from '../../types/run-context.js';
|
|
3
3
|
export type { GatherScope } from '../../types/run-context.js';
|
|
4
4
|
export interface GatherResult {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KnowledgeProviderConfig, KnowledgeRetrieverAdapter } from '../../types/
|
|
1
|
+
import type { KnowledgeProviderConfig, KnowledgeRetrieverAdapter } from '../../types/knowledge.js';
|
|
2
2
|
export interface InMemoryKnowledgeDocument {
|
|
3
3
|
id?: string;
|
|
4
4
|
text: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentConfig } from '../../types/agentConfig.js';
|
|
2
2
|
import type { AutoRetrieveProvider } from '../../types/run-context.js';
|
|
3
3
|
import type { AnyTool } from '../../types/effectTool.js';
|
|
4
|
-
import type { KnowledgeProviderConfig } from '../../types/
|
|
4
|
+
import type { KnowledgeProviderConfig } from '../../types/knowledge.js';
|
|
5
5
|
import { KnowledgeProvider } from '../KnowledgeProvider.js';
|
|
6
6
|
export declare function buildKnowledgeProvider(config: KnowledgeProviderConfig): KnowledgeProvider;
|
|
7
7
|
export declare function buildAutoRetrieveProvider(provider: KnowledgeProvider, agent: AgentConfig): AutoRetrieveProvider | undefined;
|
package/dist/runtime/hostLoop.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function hostLoop(options) {
|
|
|
36
36
|
return { kind: 'paused' };
|
|
37
37
|
}
|
|
38
38
|
if (error instanceof LimitsExceededError) {
|
|
39
|
-
ctx.emit({ type: 'error', error: error.message });
|
|
39
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: error.message } });
|
|
40
40
|
return { kind: 'ended', reason: error.message };
|
|
41
41
|
}
|
|
42
42
|
throw error;
|
|
@@ -80,10 +80,19 @@ async function runActiveFlow(flow, run, driver, ctx, agent) {
|
|
|
80
80
|
await ctx.runStore.putRunState(run);
|
|
81
81
|
return { kind: 'turnComplete' };
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// A flow that ended because a node threw is NOT completed. Marking it so made the
|
|
84
|
+
// failure permanent for the turn: `select` and the host-control tools both exclude a
|
|
85
|
+
// completed flow from re-entry, so a failed intake could not be retried even though the
|
|
86
|
+
// error was recoverable (a mistyped unit id, a transient tool fault). Observed live —
|
|
87
|
+
// a session held __completedFlows: ["raise_work_order"] with an errored journal step
|
|
88
|
+
// and no work order to show for it.
|
|
89
|
+
const degraded = result.kind === 'ended' && result.reason === 'error_degraded';
|
|
90
|
+
if (!degraded) {
|
|
91
|
+
const completed = run.state.__completedFlows;
|
|
92
|
+
const completedFlows = Array.isArray(completed) ? completed : [];
|
|
93
|
+
if (!completedFlows.includes(flow.name)) {
|
|
94
|
+
run.state.__completedFlows = [...completedFlows, flow.name];
|
|
95
|
+
}
|
|
87
96
|
}
|
|
88
97
|
run.activeFlow = undefined;
|
|
89
98
|
run.activeNode = undefined;
|
|
@@ -160,7 +169,11 @@ function guardVerdictToTelemetryVerdict(verdict) {
|
|
|
160
169
|
return 'keep';
|
|
161
170
|
}
|
|
162
171
|
function emitHostGuardTelemetry(ctx, data) {
|
|
163
|
-
ctx.emit({
|
|
172
|
+
ctx.emit({
|
|
173
|
+
channel: 'internal',
|
|
174
|
+
type: 'custom',
|
|
175
|
+
payload: { name: 'host-guard', data },
|
|
176
|
+
});
|
|
164
177
|
}
|
|
165
178
|
function guardVerdictToControl(verdict, agent) {
|
|
166
179
|
const selection = verdictToSelection(verdict, agent);
|
|
@@ -174,7 +187,11 @@ function guardVerdictToControl(verdict, agent) {
|
|
|
174
187
|
}
|
|
175
188
|
async function executeHostControl(agent, run, driver, ctx, control) {
|
|
176
189
|
if (!control) {
|
|
177
|
-
ctx.emit({
|
|
190
|
+
ctx.emit({
|
|
191
|
+
channel: 'client',
|
|
192
|
+
type: 'error',
|
|
193
|
+
payload: { error: 'No valid host control target resolved' },
|
|
194
|
+
});
|
|
178
195
|
return { kind: 'ended', reason: 'dispatch_failed' };
|
|
179
196
|
}
|
|
180
197
|
if (control.type === 'enterFlow') {
|
|
@@ -182,18 +199,30 @@ async function executeHostControl(agent, run, driver, ctx, control) {
|
|
|
182
199
|
if (flow) {
|
|
183
200
|
return await runActiveFlow(flow, run, driver, ctx, agent);
|
|
184
201
|
}
|
|
185
|
-
ctx.emit({
|
|
202
|
+
ctx.emit({
|
|
203
|
+
channel: 'client',
|
|
204
|
+
type: 'error',
|
|
205
|
+
payload: { error: `Flow not found: ${control.flowName}` },
|
|
206
|
+
});
|
|
186
207
|
return { kind: 'ended', reason: 'flow_not_found' };
|
|
187
208
|
}
|
|
188
209
|
if (control.type === 'handoff') {
|
|
189
|
-
ctx.emit({
|
|
210
|
+
ctx.emit({
|
|
211
|
+
channel: 'internal',
|
|
212
|
+
type: 'handoff',
|
|
213
|
+
payload: { targetAgent: control.target, reason: control.reason },
|
|
214
|
+
});
|
|
190
215
|
return { kind: 'handoff', to: control.target, reason: control.reason };
|
|
191
216
|
}
|
|
192
217
|
if (control.type === 'end') {
|
|
193
218
|
return { kind: 'ended', reason: control.reason };
|
|
194
219
|
}
|
|
195
220
|
if (control.type === 'escalate') {
|
|
196
|
-
|
|
221
|
+
// No handoff part is emitted here. Runtime owns it: it emits one for any
|
|
222
|
+
// target in `terminalHandoffTargets` (default ['human']) on this exact
|
|
223
|
+
// return value. Emitting here too produced two handoff spans per escalation
|
|
224
|
+
// and a meaningless `human -> human` self-edge, which then mis-attributed
|
|
225
|
+
// every later span in the turn to `human`.
|
|
197
226
|
return { kind: 'handoff', to: 'human', reason: control.reason, category: control.category };
|
|
198
227
|
}
|
|
199
228
|
if (control.type === 'recover') {
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -3,8 +3,7 @@ export type { RuntimeLike } from './RuntimeLike.js';
|
|
|
3
3
|
export { TraceRecorder, runOnce, type TraceRecorderOptions } from './TraceRecorder.js';
|
|
4
4
|
export type { AgentSpan, AgentTrace, SpanKind } from '../types/trace.js';
|
|
5
5
|
export { SessionWorkingMemory } from './WorkingMemory.js';
|
|
6
|
-
export { TextDriver
|
|
7
|
-
export type { VoiceDriverConfig } from './channels/index.js';
|
|
6
|
+
export { TextDriver } from './channels/index.js';
|
|
8
7
|
export { setPendingUserInput, consumePendingUserInput, consumeAllPendingUserInput, peekPendingUserInput, hasPendingUserInput, } from './channels/index.js';
|
|
9
8
|
export { userInputToText, mergeUserInputContents, hasMediaParts, transcribeAudioParts, type UserInputContent, } from './userInput.js';
|
|
10
9
|
export { isAbortSignal, type InterruptionEvent, type AbortOptions, type CancellationReason, } from '../types/index.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { createRuntime, Runtime, } from './Runtime.js';
|
|
2
2
|
export { TraceRecorder, runOnce } from './TraceRecorder.js';
|
|
3
3
|
export { SessionWorkingMemory } from './WorkingMemory.js';
|
|
4
|
-
export { TextDriver
|
|
4
|
+
export { TextDriver } from './channels/index.js';
|
|
5
5
|
// Pending-input buffer helpers — required by custom ChannelDriver authors to
|
|
6
6
|
// implement awaitUser the same FIFO-aware way the built-in drivers do (the
|
|
7
7
|
// buffer is an ordered queue since 0.3.13/H3, not a single slot).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Session } from '../types/session.js';
|
|
2
2
|
import type { SessionStore } from '../session/SessionStore.js';
|
|
3
3
|
import type { ConversationOutcome, ConversationOutcomeMarkedBy, ConversationOutcomeRecord } from '../outcomes/types.js';
|
|
4
|
-
import type {
|
|
4
|
+
import type { StreamPart } from '../types/stream.js';
|
|
5
5
|
export declare function isTerminalOutcome(outcome: ConversationOutcome): boolean;
|
|
6
6
|
export declare function markSessionOutcome(sessionStore: SessionStore, session: Session, outcome: ConversationOutcome, opts?: {
|
|
7
7
|
reason?: string;
|
|
8
8
|
markedBy?: ConversationOutcomeMarkedBy;
|
|
9
|
-
}, emit?: (part:
|
|
9
|
+
}, emit?: (part: StreamPart) => void): Promise<ConversationOutcomeRecord>;
|
|
@@ -22,8 +22,9 @@ export async function markSessionOutcome(sessionStore, session, outcome, opts =
|
|
|
22
22
|
await sessionStore.save(session);
|
|
23
23
|
if (emit) {
|
|
24
24
|
emit({
|
|
25
|
+
channel: 'client',
|
|
25
26
|
type: 'conversation-outcome',
|
|
26
|
-
outcome: record.outcome,
|
|
27
|
+
payload: { outcome: record.outcome },
|
|
27
28
|
});
|
|
28
29
|
}
|
|
29
30
|
return record;
|