@kuralle-agents/core 0.13.0 → 0.14.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 +4 -4
- 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 +2 -2
- package/dist/flow/extraction.js +10 -4
- package/dist/flow/reduceTransition.d.ts +2 -2
- package/dist/flow/reduceTransition.js +7 -3
- package/dist/flow/runFlow.js +34 -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 +3 -1
- package/dist/runtime/Runtime.js +84 -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 +24 -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 +26 -6
- 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/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,9 +1,10 @@
|
|
|
1
1
|
import type { TurnResult } from '../types/channel.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { StreamPart, TurnHandle } from '../types/stream.js';
|
|
3
3
|
import type { AgentSpan, AgentTrace } from '../types/trace.js';
|
|
4
4
|
import type { RunOptions } from './Runtime.js';
|
|
5
5
|
export interface TraceRecorderOptions {
|
|
6
6
|
sessionId?: string;
|
|
7
|
+
agentId?: string;
|
|
7
8
|
input?: unknown;
|
|
8
9
|
onSpan?: (span: AgentSpan) => void;
|
|
9
10
|
}
|
|
@@ -16,8 +17,10 @@ export declare class TraceRecorder {
|
|
|
16
17
|
private readonly toolCallIds;
|
|
17
18
|
private readonly emitted;
|
|
18
19
|
private readonly onSpan?;
|
|
20
|
+
private currentAgentId?;
|
|
19
21
|
constructor(options?: TraceRecorderOptions);
|
|
20
|
-
|
|
22
|
+
setInitiatingAgent(agentId: string): void;
|
|
23
|
+
record(part: StreamPart): void;
|
|
21
24
|
finish(result: TurnResult): AgentTrace;
|
|
22
25
|
private openSpan;
|
|
23
26
|
private takeToolSpan;
|
|
@@ -28,6 +31,21 @@ export declare class TraceRecorder {
|
|
|
28
31
|
private close;
|
|
29
32
|
private emitSpan;
|
|
30
33
|
}
|
|
31
|
-
|
|
34
|
+
/**
|
|
35
|
+
* What the standalone {@link runOnce} needs from a runtime. `run` is required;
|
|
36
|
+
* the optional accessors let it resolve the same agent `Runtime.runOnce` would.
|
|
37
|
+
*
|
|
38
|
+
* It must NOT call `runtime.runOnce` — that method delegates here, so delegating
|
|
39
|
+
* back is infinite recursion.
|
|
40
|
+
*/
|
|
41
|
+
export interface RunOnceRuntime {
|
|
32
42
|
run(opts: RunOptions): TurnHandle;
|
|
33
|
-
|
|
43
|
+
getSessionStore?(): {
|
|
44
|
+
get(id: string): Promise<{
|
|
45
|
+
activeAgentId?: string;
|
|
46
|
+
currentAgent?: string;
|
|
47
|
+
} | null>;
|
|
48
|
+
};
|
|
49
|
+
getDefaultAgentId?(): string;
|
|
50
|
+
}
|
|
51
|
+
export declare function runOnce(runtime: RunOnceRuntime, opts: RunOptions): Promise<AgentTrace>;
|
|
@@ -7,11 +7,13 @@ export class TraceRecorder {
|
|
|
7
7
|
toolCallIds = new Map();
|
|
8
8
|
emitted = new Set();
|
|
9
9
|
onSpan;
|
|
10
|
+
currentAgentId;
|
|
10
11
|
constructor(options = {}) {
|
|
11
12
|
const startedAt = Date.now();
|
|
12
13
|
const sessionId = options.sessionId ?? '';
|
|
13
14
|
const traceId = crypto.randomUUID().replaceAll('-', '');
|
|
14
15
|
this.onSpan = options.onSpan;
|
|
16
|
+
this.currentAgentId = options.agentId;
|
|
15
17
|
this.root = {
|
|
16
18
|
traceId,
|
|
17
19
|
spanId: newSpanId(),
|
|
@@ -21,6 +23,7 @@ export class TraceRecorder {
|
|
|
21
23
|
status: 'ok',
|
|
22
24
|
attributes: {
|
|
23
25
|
sessionId,
|
|
26
|
+
...(options.agentId ? { agentId: options.agentId } : {}),
|
|
24
27
|
...(options.input !== undefined ? { input: toJsonValue(options.input) } : {}),
|
|
25
28
|
},
|
|
26
29
|
};
|
|
@@ -35,22 +38,26 @@ export class TraceRecorder {
|
|
|
35
38
|
startedAt,
|
|
36
39
|
};
|
|
37
40
|
}
|
|
41
|
+
setInitiatingAgent(agentId) {
|
|
42
|
+
this.currentAgentId = agentId;
|
|
43
|
+
this.root.attributes.agentId = agentId;
|
|
44
|
+
}
|
|
38
45
|
record(part) {
|
|
39
46
|
try {
|
|
40
47
|
const at = Date.now();
|
|
41
48
|
switch (part.type) {
|
|
42
49
|
case 'text-delta':
|
|
43
|
-
this.trace.answer += part.delta;
|
|
50
|
+
this.trace.answer += part.payload.delta;
|
|
44
51
|
break;
|
|
45
52
|
case 'flow-enter':
|
|
46
53
|
this.closeNode(at);
|
|
47
54
|
this.closeFlow(at);
|
|
48
55
|
this.currentFlow = this.openSpan({
|
|
49
|
-
name: `flow:${part.flow}`,
|
|
56
|
+
name: `flow:${part.payload.flow}`,
|
|
50
57
|
kind: 'flow',
|
|
51
58
|
parentSpanId: this.root.spanId,
|
|
52
59
|
at,
|
|
53
|
-
attributes: { activeFlow: part.flow },
|
|
60
|
+
attributes: { activeFlow: part.payload.flow },
|
|
54
61
|
});
|
|
55
62
|
break;
|
|
56
63
|
case 'flow-end':
|
|
@@ -60,13 +67,13 @@ export class TraceRecorder {
|
|
|
60
67
|
case 'node-enter':
|
|
61
68
|
this.closeNode(at);
|
|
62
69
|
this.currentNode = this.openSpan({
|
|
63
|
-
name: `node:${part.nodeName}`,
|
|
70
|
+
name: `node:${part.payload.nodeName}`,
|
|
64
71
|
kind: 'node',
|
|
65
72
|
parentSpanId: this.currentFlow?.spanId ?? this.root.spanId,
|
|
66
73
|
at,
|
|
67
74
|
attributes: {
|
|
68
75
|
...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
|
|
69
|
-
nodeId: part.nodeName,
|
|
76
|
+
nodeId: part.payload.nodeName,
|
|
70
77
|
},
|
|
71
78
|
});
|
|
72
79
|
break;
|
|
@@ -75,37 +82,40 @@ export class TraceRecorder {
|
|
|
75
82
|
break;
|
|
76
83
|
case 'tool-call': {
|
|
77
84
|
const span = this.openSpan({
|
|
78
|
-
name: `tool:${part.toolName}`,
|
|
85
|
+
name: `tool:${part.payload.toolName}`,
|
|
79
86
|
kind: 'tool',
|
|
80
87
|
parentSpanId: this.currentNode?.spanId ?? this.root.spanId,
|
|
81
88
|
at,
|
|
82
89
|
attributes: {
|
|
83
90
|
...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
|
|
84
91
|
...(this.currentNode ? { nodeId: nodeName(this.currentNode) } : {}),
|
|
85
|
-
toolName: part.toolName,
|
|
86
|
-
input: toJsonValue(part.args),
|
|
92
|
+
toolName: part.payload.toolName,
|
|
93
|
+
input: toJsonValue(part.payload.args),
|
|
87
94
|
},
|
|
88
95
|
});
|
|
89
|
-
this.toolCallIds.set(span, part.toolCallId);
|
|
96
|
+
this.toolCallIds.set(span, part.payload.toolCallId);
|
|
90
97
|
this.openTools.push(span);
|
|
91
98
|
this.trace.usedTool = true;
|
|
92
|
-
this.trace.toolCalls.push({
|
|
99
|
+
this.trace.toolCalls.push({
|
|
100
|
+
name: part.payload.toolName,
|
|
101
|
+
args: toJsonValue(part.payload.args),
|
|
102
|
+
});
|
|
93
103
|
break;
|
|
94
104
|
}
|
|
95
105
|
case 'tool-result': {
|
|
96
106
|
this.trace.usedTool = true;
|
|
97
|
-
const result = toJsonValue(part.result);
|
|
98
|
-
this.trace.toolResults.push({ name: part.toolName, result });
|
|
99
|
-
const span = this.takeToolSpan(part.toolName, part.toolCallId) ??
|
|
107
|
+
const result = toJsonValue(part.payload.result);
|
|
108
|
+
this.trace.toolResults.push({ name: part.payload.toolName, result });
|
|
109
|
+
const span = this.takeToolSpan(part.payload.toolName, part.payload.toolCallId) ??
|
|
100
110
|
this.openSpan({
|
|
101
|
-
name: `tool:${part.toolName}`,
|
|
111
|
+
name: `tool:${part.payload.toolName}`,
|
|
102
112
|
kind: 'tool',
|
|
103
113
|
parentSpanId: this.currentNode?.spanId ?? this.root.spanId,
|
|
104
114
|
at,
|
|
105
115
|
attributes: {
|
|
106
116
|
...(this.currentFlow ? { activeFlow: activeFlowName(this.currentFlow) } : {}),
|
|
107
117
|
...(this.currentNode ? { nodeId: nodeName(this.currentNode) } : {}),
|
|
108
|
-
toolName: part.toolName,
|
|
118
|
+
toolName: part.payload.toolName,
|
|
109
119
|
},
|
|
110
120
|
});
|
|
111
121
|
span.attributes.output = result;
|
|
@@ -114,35 +124,43 @@ export class TraceRecorder {
|
|
|
114
124
|
break;
|
|
115
125
|
}
|
|
116
126
|
case 'handoff': {
|
|
127
|
+
const handoffFrom = this.currentAgentId;
|
|
117
128
|
const span = this.openSpan({
|
|
118
|
-
name: `handoff:${part.targetAgent}`,
|
|
129
|
+
name: `handoff:${part.payload.targetAgent}`,
|
|
119
130
|
kind: 'handoff',
|
|
120
131
|
parentSpanId: this.root.spanId,
|
|
121
132
|
at,
|
|
122
|
-
attributes: {
|
|
133
|
+
attributes: {
|
|
134
|
+
...(handoffFrom ? { handoffFrom } : {}),
|
|
135
|
+
handoffTo: part.payload.targetAgent,
|
|
136
|
+
},
|
|
123
137
|
});
|
|
124
138
|
span.endTime = at;
|
|
125
139
|
this.emitSpan(span);
|
|
140
|
+
this.currentAgentId = part.payload.targetAgent;
|
|
126
141
|
break;
|
|
127
142
|
}
|
|
128
143
|
case 'error': {
|
|
129
144
|
const span = this.openTools.at(-1) ?? this.currentNode ?? this.currentFlow ?? this.root;
|
|
130
145
|
span.status = 'error';
|
|
131
|
-
span.attributes.error = part.error;
|
|
146
|
+
span.attributes.error = part.payload.error;
|
|
132
147
|
this.root.status = 'error';
|
|
133
|
-
this.root.attributes.error = part.error;
|
|
148
|
+
this.root.attributes.error = part.payload.error;
|
|
134
149
|
break;
|
|
135
150
|
}
|
|
136
151
|
case 'done':
|
|
137
|
-
this.setSessionId(part.sessionId);
|
|
152
|
+
this.setSessionId(part.payload.sessionId);
|
|
138
153
|
this.root.attributes.output = this.trace.answer;
|
|
139
|
-
if (part.usage) {
|
|
140
|
-
if (typeof part.usage.inputTokens === 'number')
|
|
141
|
-
this.root.attributes.tokensIn = part.usage.inputTokens;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
154
|
+
if (part.payload.usage) {
|
|
155
|
+
if (typeof part.payload.usage.inputTokens === 'number') {
|
|
156
|
+
this.root.attributes.tokensIn = part.payload.usage.inputTokens;
|
|
157
|
+
}
|
|
158
|
+
if (typeof part.payload.usage.outputTokens === 'number') {
|
|
159
|
+
this.root.attributes.tokensOut = part.payload.usage.outputTokens;
|
|
160
|
+
}
|
|
161
|
+
if (typeof part.payload.usage.contextTokens === 'number') {
|
|
162
|
+
this.root.attributes.contextTokens = part.payload.usage.contextTokens;
|
|
163
|
+
}
|
|
146
164
|
}
|
|
147
165
|
this.close(at);
|
|
148
166
|
break;
|
|
@@ -174,7 +192,11 @@ export class TraceRecorder {
|
|
|
174
192
|
kind: args.kind,
|
|
175
193
|
startTime: args.at,
|
|
176
194
|
status: 'ok',
|
|
177
|
-
attributes: {
|
|
195
|
+
attributes: {
|
|
196
|
+
sessionId: this.trace.sessionId,
|
|
197
|
+
...(this.currentAgentId ? { agentId: this.currentAgentId } : {}),
|
|
198
|
+
...args.attributes,
|
|
199
|
+
},
|
|
178
200
|
};
|
|
179
201
|
this.trace.spans.push(span);
|
|
180
202
|
return span;
|
|
@@ -239,8 +261,22 @@ export class TraceRecorder {
|
|
|
239
261
|
}
|
|
240
262
|
}
|
|
241
263
|
export async function runOnce(runtime, opts) {
|
|
264
|
+
// Resolve attribution the same way Runtime.runOnce does: caller's agentId
|
|
265
|
+
// first, then persisted state, then the runtime default. Without this the
|
|
266
|
+
// standalone helper produced spans with no `agentId` and handoff spans with
|
|
267
|
+
// no `handoffFrom` — the exact feature it is documented to provide.
|
|
268
|
+
let agentId = opts.agentId;
|
|
269
|
+
if (!agentId && opts.sessionId && runtime.getSessionStore) {
|
|
270
|
+
const existing = await runtime.getSessionStore().get(opts.sessionId);
|
|
271
|
+
agentId = existing?.activeAgentId ?? existing?.currentAgent;
|
|
272
|
+
}
|
|
273
|
+
agentId ??= runtime.getDefaultAgentId?.();
|
|
242
274
|
const handle = runtime.run(opts);
|
|
243
|
-
const recorder = new TraceRecorder({
|
|
275
|
+
const recorder = new TraceRecorder({
|
|
276
|
+
sessionId: opts.sessionId,
|
|
277
|
+
agentId,
|
|
278
|
+
input: opts.input,
|
|
279
|
+
});
|
|
244
280
|
for await (const part of handle.events) {
|
|
245
281
|
recorder.record(part);
|
|
246
282
|
}
|
|
@@ -28,7 +28,7 @@ export async function buildAgentToolSurface(agent, session, deps) {
|
|
|
28
28
|
let skillPrompt;
|
|
29
29
|
let skillTools = {};
|
|
30
30
|
if (agent.skills) {
|
|
31
|
-
const wired = await wireAgentSkills(agent);
|
|
31
|
+
const wired = await wireAgentSkills(agent, resolvedWorkspace?.fs);
|
|
32
32
|
if (wired) {
|
|
33
33
|
skillTools = wired.tools;
|
|
34
34
|
Object.assign(executorTools, wired.tools);
|
|
@@ -29,16 +29,19 @@ export class TextDriver {
|
|
|
29
29
|
if (!preTurn.proceed) {
|
|
30
30
|
const blocked = preTurn.blockedMessage ?? 'Input blocked by guardrails';
|
|
31
31
|
ctx.emit({
|
|
32
|
+
channel: 'internal',
|
|
32
33
|
type: 'safety-blocked',
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
payload: {
|
|
35
|
+
moderator: preTurn.blockedBy ?? 'input-guardrails',
|
|
36
|
+
rationale: preTurn.blockedReason ?? 'input blocked',
|
|
37
|
+
userFacingMessage: blocked,
|
|
38
|
+
},
|
|
36
39
|
});
|
|
37
40
|
const id = crypto.randomUUID();
|
|
38
|
-
ctx.emit({ type: 'text-start', id });
|
|
39
|
-
ctx.emit({ type: 'text-delta', id, delta: blocked });
|
|
40
|
-
ctx.emit({ type: 'text-end', id });
|
|
41
|
-
ctx.emit({ type: 'turn-end' });
|
|
41
|
+
ctx.emit({ channel: 'client', type: 'text-start', payload: { id } });
|
|
42
|
+
ctx.emit({ channel: 'client', type: 'text-delta', payload: { id, delta: blocked } });
|
|
43
|
+
ctx.emit({ channel: 'client', type: 'text-end', payload: { id } });
|
|
44
|
+
ctx.emit({ channel: 'internal', type: 'turn-end', payload: {} });
|
|
42
45
|
return { text: blocked, toolResults: [] };
|
|
43
46
|
}
|
|
44
47
|
const scope = resolveNodeGatherScope(replyNode, ctx.runState.state, ctx.runState.messages);
|
|
@@ -75,7 +78,7 @@ export class TextDriver {
|
|
|
75
78
|
if (part.type === 'error') {
|
|
76
79
|
const err = part.error;
|
|
77
80
|
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
-
ctx.emit({ type: 'error', error: message });
|
|
81
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
|
|
79
82
|
throw err instanceof Error ? err : new Error(message);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
@@ -160,7 +163,7 @@ export class TextDriver {
|
|
|
160
163
|
if (turnUsage && turnUsage.totalTokens > 0) {
|
|
161
164
|
out.usage = turnUsage;
|
|
162
165
|
}
|
|
163
|
-
ctx.emit({ type: 'turn-end' });
|
|
166
|
+
ctx.emit({ channel: 'internal', type: 'turn-end', payload: {} });
|
|
164
167
|
return out;
|
|
165
168
|
}
|
|
166
169
|
// Non-speaking field extraction for collect nodes (shared helper so text and
|
|
@@ -11,6 +11,12 @@ export interface ModelToolCallOutcome {
|
|
|
11
11
|
result: unknown;
|
|
12
12
|
control?: TurnControl;
|
|
13
13
|
failed: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Set when the call unwound with a control-flow signal rather than a failure. Held as a
|
|
16
|
+
* value so a parallel batch can still finalize its siblings; the dispatcher rethrows it
|
|
17
|
+
* once the batch has settled. Never surfaced to the model.
|
|
18
|
+
*/
|
|
19
|
+
signal?: unknown;
|
|
14
20
|
}
|
|
15
21
|
export declare function executeModelToolCall(ctx: RunContext, call: ModelToolCall, localTools?: Record<string, AnyTool>, durableOpts?: {
|
|
16
22
|
callsite?: string;
|
|
@@ -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,16 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { SentenceAggregator } from './SentenceAggregator.js';
|
|
3
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 });
|
|
4
|
+
ctx.emit({ channel: 'client', type: 'text-start', payload: { id } });
|
|
5
|
+
ctx.emit({ channel: 'client', type: 'text-delta', payload: { id, delta: text } });
|
|
6
|
+
ctx.emit({ channel: 'client', type: 'text-end', payload: { id } });
|
|
7
7
|
}
|
|
8
8
|
async function emitBlockedSafeMessage(ctx, turnId, started, outcome) {
|
|
9
9
|
if (started) {
|
|
10
10
|
ctx.emit({
|
|
11
|
+
channel: 'client',
|
|
11
12
|
type: 'text-cancel',
|
|
12
|
-
id: turnId,
|
|
13
|
-
reason: outcome.reason ?? 'blocked',
|
|
13
|
+
payload: { id: turnId, reason: outcome.reason ?? 'blocked' },
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
const safeId = randomUUID();
|
|
@@ -32,7 +32,7 @@ export async function speakGated(args) {
|
|
|
32
32
|
}
|
|
33
33
|
catch (err) {
|
|
34
34
|
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
-
ctx.emit({ type: 'error', error: message });
|
|
35
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
|
|
36
36
|
throw err instanceof Error ? err : new Error(message);
|
|
37
37
|
}
|
|
38
38
|
const decision = await runGate(full, true);
|
|
@@ -48,13 +48,17 @@ export async function speakGated(args) {
|
|
|
48
48
|
let emitted = '';
|
|
49
49
|
const openOnce = () => {
|
|
50
50
|
if (!started) {
|
|
51
|
-
ctx.emit({ type: 'text-start', id: turnId });
|
|
51
|
+
ctx.emit({ channel: 'client', type: 'text-start', payload: { id: turnId } });
|
|
52
52
|
started = true;
|
|
53
53
|
}
|
|
54
54
|
};
|
|
55
55
|
const emitCleared = (chunk) => {
|
|
56
56
|
openOnce();
|
|
57
|
-
ctx.emit({
|
|
57
|
+
ctx.emit({
|
|
58
|
+
channel: 'client',
|
|
59
|
+
type: 'text-delta',
|
|
60
|
+
payload: { id: turnId, delta: chunk },
|
|
61
|
+
});
|
|
58
62
|
emitted += chunk;
|
|
59
63
|
};
|
|
60
64
|
const gateSentence = async (sentence, final) => {
|
|
@@ -69,7 +73,11 @@ export async function speakGated(args) {
|
|
|
69
73
|
for await (const { delta } of source) {
|
|
70
74
|
if (mode === 'token') {
|
|
71
75
|
openOnce();
|
|
72
|
-
ctx.emit({
|
|
76
|
+
ctx.emit({
|
|
77
|
+
channel: 'client',
|
|
78
|
+
type: 'text-delta',
|
|
79
|
+
payload: { id: turnId, delta },
|
|
80
|
+
});
|
|
73
81
|
emitted += delta;
|
|
74
82
|
continue;
|
|
75
83
|
}
|
|
@@ -88,15 +96,19 @@ export async function speakGated(args) {
|
|
|
88
96
|
}
|
|
89
97
|
}
|
|
90
98
|
if (started) {
|
|
91
|
-
ctx.emit({ type: 'text-end', id: turnId });
|
|
99
|
+
ctx.emit({ channel: 'client', type: 'text-end', payload: { id: turnId } });
|
|
92
100
|
}
|
|
93
101
|
return { text: emitted };
|
|
94
102
|
}
|
|
95
103
|
catch (err) {
|
|
96
104
|
const message = err instanceof Error ? err.message : String(err);
|
|
97
|
-
ctx.emit({ type: 'error', error: message });
|
|
105
|
+
ctx.emit({ channel: 'client', type: 'error', payload: { error: message } });
|
|
98
106
|
if (started) {
|
|
99
|
-
ctx.emit({
|
|
107
|
+
ctx.emit({
|
|
108
|
+
channel: 'client',
|
|
109
|
+
type: 'text-cancel',
|
|
110
|
+
payload: { id: turnId, reason: message },
|
|
111
|
+
});
|
|
100
112
|
}
|
|
101
113
|
throw err instanceof Error ? err : new Error(message);
|
|
102
114
|
}
|
|
@@ -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;
|