@kuralle-agents/core 0.6.1 → 0.7.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 +2 -4
- package/dist/capabilities/index.d.ts +0 -3
- package/dist/capabilities/index.js +0 -2
- package/dist/flow/classifyControl.js +4 -0
- package/dist/flow/collectDigression.js +0 -1
- package/dist/flow/controlEvaluator.js +3 -0
- package/dist/flow/flowControlTools.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/runtime/Runtime.d.ts +3 -1
- package/dist/runtime/Runtime.js +3 -1
- package/dist/runtime/agentReply.d.ts +2 -1
- package/dist/runtime/agentReply.js +14 -3
- package/dist/runtime/channels/TextDriver.d.ts +1 -0
- package/dist/runtime/channels/TextDriver.js +31 -18
- package/dist/runtime/channels/VoiceDriver.d.ts +1 -0
- package/dist/runtime/channels/VoiceDriver.js +5 -1
- package/dist/runtime/channels/streaming/hostControlSpeak.d.ts +30 -0
- package/dist/runtime/channels/streaming/hostControlSpeak.js +80 -0
- package/dist/runtime/deriveAgent.d.ts +10 -1
- package/dist/runtime/deriveAgent.js +63 -13
- package/dist/runtime/dispatchMode.d.ts +5 -0
- package/dist/runtime/dispatchMode.js +18 -0
- package/dist/runtime/hostClassifyAdapter.d.ts +2 -0
- package/dist/runtime/hostClassifyAdapter.js +21 -0
- package/dist/runtime/hostControlGuard.d.ts +23 -0
- package/dist/runtime/hostControlGuard.js +67 -0
- package/dist/runtime/hostControlTools.d.ts +12 -0
- package/dist/runtime/hostControlTools.js +89 -0
- package/dist/runtime/hostLoop.d.ts +4 -1
- package/dist/runtime/hostLoop.js +122 -34
- package/dist/runtime/select.d.ts +17 -4
- package/dist/runtime/select.js +107 -76
- package/dist/tools/enterFlow.d.ts +14 -0
- package/dist/tools/enterFlow.js +31 -0
- package/dist/types/channel.d.ts +12 -0
- package/dist/types/route.d.ts +3 -3
- package/guides/AGENTS.md +7 -10
- package/guides/FLOWS.md +0 -4
- package/guides/RUNTIME.md +2 -2
- package/package.json +2 -2
- package/dist/capabilities/HandoffCapability.d.ts +0 -18
- package/dist/capabilities/HandoffCapability.js +0 -69
- package/dist/capabilities/TriageCapability.d.ts +0 -15
- package/dist/capabilities/TriageCapability.js +0 -60
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { classifyHostTarget, } from './select.js';
|
|
2
|
+
import { availableHostFlows, collectTransferTargets } from './hostControlTools.js';
|
|
3
|
+
export function startHostControlGuard(options) {
|
|
4
|
+
const classify = options.classify ?? classifyHostTarget;
|
|
5
|
+
return classify({
|
|
6
|
+
agent: options.agent,
|
|
7
|
+
run: options.run,
|
|
8
|
+
model: options.model,
|
|
9
|
+
allowKeep: true,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
export function isValidControl(control, agent, run) {
|
|
13
|
+
switch (control.type) {
|
|
14
|
+
case 'enterFlow': {
|
|
15
|
+
const flows = availableHostFlows(agent, run);
|
|
16
|
+
return flows.some((f) => f.name === control.flowName);
|
|
17
|
+
}
|
|
18
|
+
case 'handoff':
|
|
19
|
+
return collectTransferTargets(agent).some((t) => t.id === control.target);
|
|
20
|
+
case 'end':
|
|
21
|
+
case 'escalate':
|
|
22
|
+
case 'recover':
|
|
23
|
+
return true;
|
|
24
|
+
default:
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function isValidGuardVerdict(verdict, agent, run) {
|
|
29
|
+
if (verdict.action === 'keep') {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (verdict.action === 'enterFlow' && verdict.flowName) {
|
|
33
|
+
return availableHostFlows(agent, run).some((f) => f.name === verdict.flowName);
|
|
34
|
+
}
|
|
35
|
+
if (verdict.action === 'transfer' && verdict.targetAgentId) {
|
|
36
|
+
return collectTransferTargets(agent).some((t) => t.id === verdict.targetAgentId);
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
export function guardVerdictToControl(verdict) {
|
|
41
|
+
if (verdict.action === 'enterFlow' && verdict.flowName) {
|
|
42
|
+
return { type: 'enterFlow', flowName: verdict.flowName, reason: verdict.reason };
|
|
43
|
+
}
|
|
44
|
+
if (verdict.action === 'transfer' && verdict.targetAgentId) {
|
|
45
|
+
return { type: 'handoff', target: verdict.targetAgentId, reason: verdict.reason };
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Main model control wins when valid. The guard is a forgot-to-route net, NOT a
|
|
51
|
+
* second-guesser: it only overrides when the answering model produced neither a
|
|
52
|
+
* control tool NOR a substantive answer (`mainAnswered`). If the model answered,
|
|
53
|
+
* the answer stands — a guard verdict must not hijack a correct keep answer
|
|
54
|
+
* (which caused observed mis-routes of Q&A turns into flows).
|
|
55
|
+
*/
|
|
56
|
+
export function resolveHostControl(mainControl, guardVerdict, agent, run, mainAnswered) {
|
|
57
|
+
if (mainControl && isValidControl(mainControl, agent, run)) {
|
|
58
|
+
return mainControl;
|
|
59
|
+
}
|
|
60
|
+
if (!mainAnswered &&
|
|
61
|
+
guardVerdict &&
|
|
62
|
+
guardVerdict.action !== 'keep' &&
|
|
63
|
+
isValidGuardVerdict(guardVerdict, agent, run)) {
|
|
64
|
+
return guardVerdictToControl(guardVerdict);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentConfig } from '../types/agentConfig.js';
|
|
2
|
+
import type { RunState } from './durable/types.js';
|
|
3
|
+
import type { AnyTool } from '../types/effectTool.js';
|
|
4
|
+
import type { Flow } from '../types/flow.js';
|
|
5
|
+
export interface TransferTarget {
|
|
6
|
+
id: string;
|
|
7
|
+
descriptions: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function availableHostFlows(agent: AgentConfig, run: RunState): Flow[];
|
|
10
|
+
export declare function collectTransferTargets(agent: AgentConfig): TransferTarget[];
|
|
11
|
+
export declare function buildHostControlTools(agent: AgentConfig, run: RunState): Record<string, AnyTool>;
|
|
12
|
+
export declare function hasHostControlTargets(agent: AgentConfig, run: RunState): boolean;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../tools/effect/defineTool.js';
|
|
3
|
+
import { createEnterFlowTool } from '../tools/enterFlow.js';
|
|
4
|
+
export function availableHostFlows(agent, run) {
|
|
5
|
+
const flows = agent.flows ?? [];
|
|
6
|
+
const completedRaw = run.state.__completedFlows;
|
|
7
|
+
const completed = Array.isArray(completedRaw) ? completedRaw : [];
|
|
8
|
+
return flows.filter((flow) => !completed.includes(flow.name) && flow.name !== run.activeFlow);
|
|
9
|
+
}
|
|
10
|
+
export function collectTransferTargets(agent) {
|
|
11
|
+
const byId = new Map();
|
|
12
|
+
for (const route of agent.routes ?? []) {
|
|
13
|
+
if (!route.agent) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const desc = `When: ${route.when}`;
|
|
17
|
+
const existing = byId.get(route.agent);
|
|
18
|
+
if (existing) {
|
|
19
|
+
existing.descriptions.push(desc);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
byId.set(route.agent, { id: route.agent, descriptions: [desc] });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const child of agent.agents ?? []) {
|
|
26
|
+
const desc = child.description ?? child.name ?? child.id;
|
|
27
|
+
const existing = byId.get(child.id);
|
|
28
|
+
if (existing) {
|
|
29
|
+
if (!existing.descriptions.includes(desc)) {
|
|
30
|
+
existing.descriptions.push(desc);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
byId.set(child.id, { id: child.id, descriptions: [desc] });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const handoffId of agent.handoffs ?? []) {
|
|
38
|
+
const child = agent.agents?.find((a) => a.id === handoffId);
|
|
39
|
+
const desc = child?.description ?? child?.name ?? handoffId;
|
|
40
|
+
const existing = byId.get(handoffId);
|
|
41
|
+
if (existing) {
|
|
42
|
+
if (!existing.descriptions.includes(desc)) {
|
|
43
|
+
existing.descriptions.push(desc);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
byId.set(handoffId, { id: handoffId, descriptions: [desc] });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [...byId.values()];
|
|
51
|
+
}
|
|
52
|
+
function createTransferToAgentTool(targets) {
|
|
53
|
+
const ids = targets.map((t) => t.id);
|
|
54
|
+
const lines = targets.map((t) => `- ${t.id}: ${t.descriptions.join('; ')}`).join('\n');
|
|
55
|
+
return defineTool({
|
|
56
|
+
name: 'transfer_to_agent',
|
|
57
|
+
description: 'Transfer the conversation to a specialized agent when the user needs a specialist. ' +
|
|
58
|
+
'Call this INSTEAD of answering in prose or saying a filler/acknowledgement like "Sure" or "One moment"; ' +
|
|
59
|
+
'do not announce the transfer to the user.\n\n' +
|
|
60
|
+
`Available targets:\n${lines}`,
|
|
61
|
+
input: z.object({
|
|
62
|
+
targetAgentId: z.enum(ids).describe('Target agent id'),
|
|
63
|
+
reason: z.string().describe('Why this transfer — include relevant context'),
|
|
64
|
+
summary: z.string().optional().describe('Optional summary of progress so far'),
|
|
65
|
+
}),
|
|
66
|
+
execute: async ({ targetAgentId, reason, summary }) => ({
|
|
67
|
+
__handoff: true,
|
|
68
|
+
targetAgentId,
|
|
69
|
+
targetAgent: targetAgentId,
|
|
70
|
+
reason,
|
|
71
|
+
summary,
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export function buildHostControlTools(agent, run) {
|
|
76
|
+
const tools = {};
|
|
77
|
+
const flows = availableHostFlows(agent, run);
|
|
78
|
+
if (flows.length > 0) {
|
|
79
|
+
tools.enter_flow = createEnterFlowTool(flows);
|
|
80
|
+
}
|
|
81
|
+
const targets = collectTransferTargets(agent);
|
|
82
|
+
if (targets.length > 0) {
|
|
83
|
+
tools.transfer_to_agent = createTransferToAgentTool(targets);
|
|
84
|
+
}
|
|
85
|
+
return tools;
|
|
86
|
+
}
|
|
87
|
+
export function hasHostControlTargets(agent, run) {
|
|
88
|
+
return (availableHostFlows(agent, run).length > 0 || collectTransferTargets(agent).length > 0);
|
|
89
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { AgentConfig } from '../types/agentConfig.js';
|
|
|
2
2
|
import type { ChannelDriver } from '../types/channel.js';
|
|
3
3
|
import type { RunContext } from '../types/run-context.js';
|
|
4
4
|
import type { RunState } from './durable/types.js';
|
|
5
|
-
import {
|
|
5
|
+
import { type ClassifyHostOptions, type HostGuardVerdict } from './select.js';
|
|
6
|
+
import type { selectHostTarget } from './select.js';
|
|
6
7
|
export type HostLoopResult = {
|
|
7
8
|
kind: 'handoff';
|
|
8
9
|
to: string;
|
|
@@ -20,6 +21,8 @@ export interface HostLoopOptions {
|
|
|
20
21
|
run: RunState;
|
|
21
22
|
driver: ChannelDriver;
|
|
22
23
|
ctx: RunContext;
|
|
24
|
+
classify?: (opts: ClassifyHostOptions) => Promise<HostGuardVerdict>;
|
|
25
|
+
/** @deprecated Test injection — use classify. */
|
|
23
26
|
select?: typeof selectHostTarget;
|
|
24
27
|
}
|
|
25
28
|
export declare function hostLoop(options: HostLoopOptions): Promise<HostLoopResult>;
|
package/dist/runtime/hostLoop.js
CHANGED
|
@@ -2,12 +2,17 @@ import { runFlow } from '../flow/runFlow.js';
|
|
|
2
2
|
import { resolveReplyNode } from '../flow/nodeBuilders.js';
|
|
3
3
|
import { SuspendError } from './durable/RunStore.js';
|
|
4
4
|
import { buildAgentReplyNode } from './agentReply.js';
|
|
5
|
-
import {
|
|
6
|
-
import { selectHostTarget } from './select.js';
|
|
5
|
+
import { deriveAgentShape } from './deriveAgent.js';
|
|
7
6
|
import { assertWithinTurnLimit, incrementTurnCount, LimitsExceededError, } from './policies/limits.js';
|
|
7
|
+
import { classifyHostTarget, verdictToSelection, } from './select.js';
|
|
8
|
+
import { hasHostControlTargets } from './hostControlTools.js';
|
|
9
|
+
import { isValidControl, resolveHostControl, startHostControlGuard, } from './hostControlGuard.js';
|
|
10
|
+
import { resolveDispatchMode, isAdvisoryDispatch } from './dispatchMode.js';
|
|
11
|
+
import { adaptHostSelect } from './hostClassifyAdapter.js';
|
|
8
12
|
export async function hostLoop(options) {
|
|
9
13
|
const { agent, run, driver, ctx } = options;
|
|
10
|
-
const
|
|
14
|
+
const classify = options.classify ??
|
|
15
|
+
(options.select ? adaptHostSelect(options.select) : classifyHostTarget);
|
|
11
16
|
try {
|
|
12
17
|
if (run.activeFlow) {
|
|
13
18
|
const flow = findFlowByName(agent, run.activeFlow);
|
|
@@ -16,23 +21,14 @@ export async function hostLoop(options) {
|
|
|
16
21
|
}
|
|
17
22
|
return await runActiveFlow(flow, run, driver, ctx, agent);
|
|
18
23
|
}
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
alwaysRoute,
|
|
26
|
-
});
|
|
27
|
-
if (selection.kind === 'enterFlow') {
|
|
28
|
-
return await runActiveFlow(selection.flow, run, driver, ctx, agent);
|
|
29
|
-
}
|
|
30
|
-
if (selection.kind === 'route') {
|
|
31
|
-
ctx.emit({ type: 'handoff', targetAgent: selection.agentId, reason: selection.reason });
|
|
32
|
-
return { kind: 'handoff', to: selection.agentId, reason: selection.reason };
|
|
33
|
-
}
|
|
24
|
+
const shape = deriveAgentShape(agent);
|
|
25
|
+
if (shape.isPureDispatcher) {
|
|
26
|
+
return await runPureDispatcher(agent, run, driver, ctx, classify);
|
|
27
|
+
}
|
|
28
|
+
if (shape.isAnsweringAgent) {
|
|
29
|
+
return await runAnsweringAgent(agent, run, driver, ctx, classify);
|
|
34
30
|
}
|
|
35
|
-
return await runFreeConversation(agent, run, driver, ctx);
|
|
31
|
+
return await runFreeConversation(agent, run, driver, ctx, classify);
|
|
36
32
|
}
|
|
37
33
|
catch (error) {
|
|
38
34
|
if (error instanceof SuspendError) {
|
|
@@ -45,6 +41,21 @@ export async function hostLoop(options) {
|
|
|
45
41
|
throw error;
|
|
46
42
|
}
|
|
47
43
|
}
|
|
44
|
+
async function runPureDispatcher(agent, run, driver, ctx, classify) {
|
|
45
|
+
incrementTurnCount(run);
|
|
46
|
+
assertWithinTurnLimit(run, ctx.limits);
|
|
47
|
+
const model = agent.routing?.model ?? ctx.controlModel;
|
|
48
|
+
const verdict = await classify({
|
|
49
|
+
agent,
|
|
50
|
+
run,
|
|
51
|
+
model,
|
|
52
|
+
allowKeep: false,
|
|
53
|
+
});
|
|
54
|
+
return await executeHostControl(agent, run, driver, ctx, guardVerdictToControl(verdict, agent));
|
|
55
|
+
}
|
|
56
|
+
async function runAnsweringAgent(agent, run, driver, ctx, classify) {
|
|
57
|
+
return await runFreeConversation(agent, run, driver, ctx, classify);
|
|
58
|
+
}
|
|
48
59
|
async function runActiveFlow(flow, run, driver, ctx, agent) {
|
|
49
60
|
incrementTurnCount(run);
|
|
50
61
|
assertWithinTurnLimit(run, ctx.limits);
|
|
@@ -66,33 +77,110 @@ async function runActiveFlow(flow, run, driver, ctx, agent) {
|
|
|
66
77
|
await ctx.runStore.putRunState(run);
|
|
67
78
|
return { kind: 'turnComplete' };
|
|
68
79
|
}
|
|
69
|
-
async function runFreeConversation(agent, run, driver, ctx) {
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
80
|
+
async function runFreeConversation(agent, run, driver, ctx, classify) {
|
|
81
|
+
const shape = deriveAgentShape(agent);
|
|
82
|
+
if (!shape.isAnsweringAgent) {
|
|
72
83
|
return { kind: 'turnComplete' };
|
|
73
84
|
}
|
|
74
85
|
incrementTurnCount(run);
|
|
75
86
|
assertWithinTurnLimit(run, ctx.limits);
|
|
76
|
-
const
|
|
77
|
-
const
|
|
87
|
+
const capability = driver.outputCapability ?? 'kuralle-controlled-text';
|
|
88
|
+
const dispatchMode = resolveDispatchMode(agent, capability);
|
|
89
|
+
const advisoryDispatch = isAdvisoryDispatch(capability);
|
|
90
|
+
const needsGuard = hasHostControlTargets(agent, run);
|
|
91
|
+
const controlModel = agent.routing?.model ?? ctx.controlModel;
|
|
92
|
+
const startGuard = needsGuard
|
|
93
|
+
? () => startHostControlGuard({
|
|
94
|
+
agent,
|
|
95
|
+
run,
|
|
96
|
+
model: controlModel,
|
|
97
|
+
classify,
|
|
98
|
+
})
|
|
99
|
+
: undefined;
|
|
100
|
+
const replyNode = buildAgentReplyNode(agent, run);
|
|
101
|
+
const resolved = resolveReplyNode(replyNode, run.state, { freeConversation: true });
|
|
102
|
+
if (needsGuard) {
|
|
103
|
+
// The driver only buffers/streams per dispatch mode; the guard has a single
|
|
104
|
+
// owner (this loop, on the empty-turn branch below) so it runs at most once.
|
|
105
|
+
resolved.hostControl = { dispatchMode, advisoryDispatch };
|
|
106
|
+
}
|
|
107
|
+
const turn = await driver.runAgentTurn(resolved, ctx);
|
|
108
|
+
if (turn.control && isValidControl(turn.control, agent, run)) {
|
|
109
|
+
emitHostGuardTelemetry(ctx, { invoked: false, reason: 'main-control' });
|
|
110
|
+
return await executeHostControl(agent, run, driver, ctx, turn.control);
|
|
111
|
+
}
|
|
78
112
|
if (turn.text.trim()) {
|
|
113
|
+
emitHostGuardTelemetry(ctx, { invoked: false, reason: 'answered' });
|
|
79
114
|
const message = { role: 'assistant', content: turn.text };
|
|
80
115
|
run.messages = [...run.messages, message];
|
|
81
116
|
await ctx.runStore.putRunState(run);
|
|
117
|
+
return { kind: 'turnComplete' };
|
|
118
|
+
}
|
|
119
|
+
if (startGuard) {
|
|
120
|
+
const guardVerdict = await startGuard();
|
|
121
|
+
const control = resolveHostControl(undefined, guardVerdict, agent, run, false);
|
|
122
|
+
if (control) {
|
|
123
|
+
emitHostGuardTelemetry(ctx, {
|
|
124
|
+
invoked: true,
|
|
125
|
+
reason: 'empty-routed',
|
|
126
|
+
verdict: guardVerdictToTelemetryVerdict(guardVerdict),
|
|
127
|
+
});
|
|
128
|
+
return await executeHostControl(agent, run, driver, ctx, control);
|
|
129
|
+
}
|
|
130
|
+
emitHostGuardTelemetry(ctx, {
|
|
131
|
+
invoked: true,
|
|
132
|
+
reason: 'empty-kept',
|
|
133
|
+
verdict: guardVerdictToTelemetryVerdict(guardVerdict),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return { kind: 'turnComplete' };
|
|
137
|
+
}
|
|
138
|
+
function guardVerdictToTelemetryVerdict(verdict) {
|
|
139
|
+
if (verdict.action === 'enterFlow')
|
|
140
|
+
return 'enterFlow';
|
|
141
|
+
if (verdict.action === 'transfer')
|
|
142
|
+
return 'transfer';
|
|
143
|
+
return 'keep';
|
|
144
|
+
}
|
|
145
|
+
function emitHostGuardTelemetry(ctx, data) {
|
|
146
|
+
ctx.emit({ type: 'custom', name: 'host-guard', data });
|
|
147
|
+
}
|
|
148
|
+
function guardVerdictToControl(verdict, agent) {
|
|
149
|
+
const selection = verdictToSelection(verdict, agent);
|
|
150
|
+
if (!selection || selection.kind === 'keep') {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
if (selection.kind === 'enterFlow') {
|
|
154
|
+
return { type: 'enterFlow', flowName: selection.flow.name };
|
|
155
|
+
}
|
|
156
|
+
return { type: 'handoff', target: selection.agentId, reason: selection.reason };
|
|
157
|
+
}
|
|
158
|
+
async function executeHostControl(agent, run, driver, ctx, control) {
|
|
159
|
+
if (!control) {
|
|
160
|
+
ctx.emit({ type: 'error', error: 'No valid host control target resolved' });
|
|
161
|
+
return { kind: 'ended', reason: 'dispatch_failed' };
|
|
162
|
+
}
|
|
163
|
+
if (control.type === 'enterFlow') {
|
|
164
|
+
const flow = findFlowByName(agent, control.flowName);
|
|
165
|
+
if (flow) {
|
|
166
|
+
return await runActiveFlow(flow, run, driver, ctx, agent);
|
|
167
|
+
}
|
|
168
|
+
ctx.emit({ type: 'error', error: `Flow not found: ${control.flowName}` });
|
|
169
|
+
return { kind: 'ended', reason: 'flow_not_found' };
|
|
82
170
|
}
|
|
83
|
-
if (
|
|
84
|
-
ctx.emit({ type: 'handoff', targetAgent:
|
|
85
|
-
return { kind: 'handoff', to:
|
|
171
|
+
if (control.type === 'handoff') {
|
|
172
|
+
ctx.emit({ type: 'handoff', targetAgent: control.target, reason: control.reason });
|
|
173
|
+
return { kind: 'handoff', to: control.target, reason: control.reason };
|
|
86
174
|
}
|
|
87
|
-
if (
|
|
88
|
-
return { kind: 'ended', reason:
|
|
175
|
+
if (control.type === 'end') {
|
|
176
|
+
return { kind: 'ended', reason: control.reason };
|
|
89
177
|
}
|
|
90
|
-
if (
|
|
91
|
-
ctx.emit({ type: 'handoff', targetAgent: 'human', reason:
|
|
92
|
-
return { kind: 'handoff', to: 'human', reason:
|
|
178
|
+
if (control.type === 'escalate') {
|
|
179
|
+
ctx.emit({ type: 'handoff', targetAgent: 'human', reason: control.reason });
|
|
180
|
+
return { kind: 'handoff', to: 'human', reason: control.reason };
|
|
93
181
|
}
|
|
94
|
-
if (
|
|
95
|
-
return { kind: 'ended', reason:
|
|
182
|
+
if (control.type === 'recover') {
|
|
183
|
+
return { kind: 'ended', reason: control.reason ?? 'error_degraded' };
|
|
96
184
|
}
|
|
97
185
|
return { kind: 'turnComplete' };
|
|
98
186
|
}
|
package/dist/runtime/select.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { LanguageModel } from 'ai';
|
|
|
2
2
|
import type { AgentConfig } from '../types/agentConfig.js';
|
|
3
3
|
import type { Flow } from '../types/flow.js';
|
|
4
4
|
import type { RunState } from './durable/types.js';
|
|
5
|
+
import { availableHostFlows } from './hostControlTools.js';
|
|
5
6
|
export type HostSelection = {
|
|
6
7
|
kind: 'enterFlow';
|
|
7
8
|
flow: Flow;
|
|
@@ -12,12 +13,24 @@ export type HostSelection = {
|
|
|
12
13
|
} | {
|
|
13
14
|
kind: 'keep';
|
|
14
15
|
};
|
|
15
|
-
export interface
|
|
16
|
+
export interface HostGuardVerdict {
|
|
17
|
+
action: 'keep' | 'enterFlow' | 'transfer';
|
|
18
|
+
flowName?: string;
|
|
19
|
+
targetAgentId?: string;
|
|
20
|
+
reason?: string;
|
|
21
|
+
confidence?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ClassifyHostOptions {
|
|
16
24
|
agent: AgentConfig;
|
|
17
25
|
run: RunState;
|
|
18
26
|
model: LanguageModel;
|
|
19
|
-
|
|
20
|
-
/** Flow names to exclude from enterFlow (e.g. the active flow during in-flow digression). */
|
|
27
|
+
allowKeep: boolean;
|
|
21
28
|
excludeFlowNames?: string[];
|
|
22
29
|
}
|
|
23
|
-
export declare function
|
|
30
|
+
export declare function classifyHostTarget(options: ClassifyHostOptions): Promise<HostGuardVerdict>;
|
|
31
|
+
/** @deprecated Use classifyHostTarget — kept as alias for test injection. */
|
|
32
|
+
export declare function selectHostTarget(options: Omit<ClassifyHostOptions, 'allowKeep'> & {
|
|
33
|
+
alwaysRoute?: boolean;
|
|
34
|
+
}): Promise<HostSelection>;
|
|
35
|
+
export declare function verdictToSelection(verdict: HostGuardVerdict, agent: AgentConfig): HostSelection | undefined;
|
|
36
|
+
export { availableHostFlows };
|