@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
package/README.md
CHANGED
|
@@ -99,17 +99,15 @@ Rule of thumb: if you're pasting more than ~20 lines of procedure into a system
|
|
|
99
99
|
```ts
|
|
100
100
|
const triage = defineAgent({
|
|
101
101
|
id: 'triage',
|
|
102
|
-
instructions: 'Route to the right specialist.',
|
|
103
102
|
model: openai('gpt-4o-mini'),
|
|
104
103
|
routes: [
|
|
105
104
|
{ agent: 'billing', when: 'billing question' },
|
|
106
|
-
{ agent: 'support', when: 'support request' },
|
|
105
|
+
{ agent: 'support', when: 'support request or anything else' },
|
|
107
106
|
],
|
|
108
|
-
routing: { mode: 'structured', default: 'support' },
|
|
109
107
|
});
|
|
110
108
|
```
|
|
111
109
|
|
|
112
|
-
`
|
|
110
|
+
With only `routes`/`agents` and no answering surface (no `instructions`/`flows`/`tools`), `triage` derives as a **pure dispatcher**: it silently classifies and routes. The decision is model-reasoned over the `when` descriptions and never surfaces to the user. Model every fallback as a normal route with a semantic `when` (e.g. "or anything else") — there is no `routing.default`. Optionally set `routing: { model }` to pick the control-reasoning model.
|
|
113
111
|
|
|
114
112
|
## Sessions
|
|
115
113
|
|
|
@@ -141,11 +141,8 @@ export declare class CapabilityHost {
|
|
|
141
141
|
/** Number of registered capabilities. */
|
|
142
142
|
get capabilityCount(): number;
|
|
143
143
|
}
|
|
144
|
-
export { TriageCapability } from './TriageCapability.js';
|
|
145
144
|
export { ExtractionCapability } from './ExtractionCapability.js';
|
|
146
145
|
export type { ExtractionCapabilityConfig } from './ExtractionCapability.js';
|
|
147
|
-
export { HandoffCapability } from './HandoffCapability.js';
|
|
148
|
-
export type { HandoffTarget } from './HandoffCapability.js';
|
|
149
146
|
export { GuardrailCapability } from './GuardrailCapability.js';
|
|
150
147
|
export { AutoRetrieveCapability } from './AutoRetrieveCapability.js';
|
|
151
148
|
export type { AutoRetrieveCapabilityConfig, RetrieveProvider } from './AutoRetrieveCapability.js';
|
|
@@ -116,9 +116,7 @@ function assemblePromptSections(basePrompt, sections) {
|
|
|
116
116
|
return parts.join('\n\n');
|
|
117
117
|
}
|
|
118
118
|
// ─── Re-exports ──────────────────────────────────────────────────────────────
|
|
119
|
-
export { TriageCapability } from './TriageCapability.js';
|
|
120
119
|
export { ExtractionCapability } from './ExtractionCapability.js';
|
|
121
|
-
export { HandoffCapability } from './HandoffCapability.js';
|
|
122
120
|
export { GuardrailCapability } from './GuardrailCapability.js';
|
|
123
121
|
export { AutoRetrieveCapability } from './AutoRetrieveCapability.js';
|
|
124
122
|
export { PassThroughRefinement } from './refinement/PassThrough.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isHandoffResult } from '../tools/handoff.js';
|
|
2
2
|
import { isFinalResult } from '../tools/final.js';
|
|
3
|
+
import { isEnterFlowResult } from '../tools/enterFlow.js';
|
|
3
4
|
import { isEscalateResult, isRecoverResult } from '../tools/controlResults.js';
|
|
4
5
|
export function classifyControl(result) {
|
|
5
6
|
if (isHandoffResult(result)) {
|
|
@@ -9,6 +10,9 @@ export function classifyControl(result) {
|
|
|
9
10
|
reason: result.reason,
|
|
10
11
|
};
|
|
11
12
|
}
|
|
13
|
+
if (isEnterFlowResult(result)) {
|
|
14
|
+
return { type: 'enterFlow', flowName: result.flowName, reason: result.reason };
|
|
15
|
+
}
|
|
12
16
|
if (isFinalResult(result)) {
|
|
13
17
|
return { type: 'end', reason: result.text };
|
|
14
18
|
}
|
|
@@ -9,6 +9,9 @@ function controlToTransition(control) {
|
|
|
9
9
|
return { kind: 'escalate', reason: control.reason };
|
|
10
10
|
case 'recover':
|
|
11
11
|
return { kind: 'end', reason: control.reason ?? 'error_degraded' };
|
|
12
|
+
case 'enterFlow':
|
|
13
|
+
// enter_flow is host-turn-only; never injected into in-flow reply nodes.
|
|
14
|
+
throw new Error(`enter_flow control reached the in-flow evaluator (flow "${control.flowName}") — it is host-only`);
|
|
12
15
|
}
|
|
13
16
|
}
|
|
14
17
|
export async function evaluateReplyControl(signal) {
|
package/dist/index.d.ts
CHANGED
|
@@ -60,8 +60,8 @@ export type { HandoffInputFilter, HandoffInputData, HandoffInputResult, } from '
|
|
|
60
60
|
export { DefaultToolExecutor, DefaultConversationState, DefaultConversationEventLog, DefaultAgentStateController, createFoundation, } from './foundation/index.js';
|
|
61
61
|
export type { ToolExecutor, ConversationState, ConversationEventLog, ConversationEvent, AgentStateController, Foundation, FoundationConfig, } from './foundation/index.js';
|
|
62
62
|
export type * from './types/index.js';
|
|
63
|
-
export { CapabilityHost,
|
|
64
|
-
export type { Capability, CapabilityAction, ExtractionToolResponseEnvelope, FlowReconfigureTransition, ToolDeclaration, PromptSection as CapabilityPromptSection, GeminiFunctionDeclaration, ExtractionCapabilityConfig,
|
|
63
|
+
export { CapabilityHost, ExtractionCapability, GuardrailCapability, AutoRetrieveCapability, PassThroughRefinement, PassThroughValidation, toGeminiDeclarations, toAISDKTools, } from './capabilities/index.js';
|
|
64
|
+
export type { Capability, CapabilityAction, ExtractionToolResponseEnvelope, FlowReconfigureTransition, ToolDeclaration, PromptSection as CapabilityPromptSection, GeminiFunctionDeclaration, ExtractionCapabilityConfig, RetrieveProvider, AutoRetrieveCapabilityConfig, RefinementCapability, RefineInput, RefineDecision, ValidationCapability, ValidateInput, ValidateDecision, SourceRef, } from './capabilities/index.js';
|
|
65
65
|
export type { EscalationOutcome, EscalationReason } from './escalation/types.js';
|
|
66
66
|
export { filterAuditEntries } from './audit/index.js';
|
|
67
67
|
export type { AuditConfig, AuditEntryBase, AuditEntryType, AuditListOptions, AuditReplayOptions, ConversationAuditEntry, ConversationAuditLog, } from './audit/types.js';
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ export { DEFAULT_CONTEXT_BUDGET, VOICE_CONTEXT_BUDGET, computeMessageHistoryBudg
|
|
|
36
36
|
export { TokenAccumulator } from './runtime/TokenAccumulator.js';
|
|
37
37
|
export { handoffFilters, removeToolHistory, keepRecentMessages, removeKeys, composeFilters, } from './runtime/handoffFilters.js';
|
|
38
38
|
export { DefaultToolExecutor, DefaultConversationState, DefaultConversationEventLog, DefaultAgentStateController, createFoundation, } from './foundation/index.js';
|
|
39
|
-
export { CapabilityHost,
|
|
39
|
+
export { CapabilityHost, ExtractionCapability, GuardrailCapability, AutoRetrieveCapability, PassThroughRefinement, PassThroughValidation, toGeminiDeclarations, toAISDKTools, } from './capabilities/index.js';
|
|
40
40
|
export { filterAuditEntries } from './audit/index.js';
|
|
41
41
|
export { defineAgent, defineFlow, reply, collect, action, decide, confirmGate, } from './authoring/index.js';
|
|
42
42
|
export { defineTool } from './types/effectTool.js';
|
|
@@ -10,7 +10,7 @@ import type { TurnHandle } from '../types/stream.js';
|
|
|
10
10
|
import type { SignalDelivery } from './durable/types.js';
|
|
11
11
|
import type { ResolvedSelection } from '../types/selection.js';
|
|
12
12
|
import type { ConversationOutcome, ConversationOutcomeMarkedBy } from '../outcomes/types.js';
|
|
13
|
-
import type { selectHostTarget } from './select.js';
|
|
13
|
+
import type { classifyHostTarget, selectHostTarget } from './select.js';
|
|
14
14
|
import type { KnowledgeProviderConfig } from '../types/voice.js';
|
|
15
15
|
import type { MemoryService as V1MemoryService } from '../memory/MemoryService.js';
|
|
16
16
|
import type { PersistentMemoryStore } from '../memory/blocks/types.js';
|
|
@@ -23,6 +23,8 @@ export interface HarnessConfig {
|
|
|
23
23
|
terminalHandoffTargets?: string[];
|
|
24
24
|
hooks?: Hooks;
|
|
25
25
|
voiceMode?: boolean;
|
|
26
|
+
hostClassify?: typeof classifyHostTarget;
|
|
27
|
+
/** @deprecated Use hostClassify — test injection adapter for HostSelection stubs. */
|
|
26
28
|
hostSelect?: typeof selectHostTarget;
|
|
27
29
|
tools?: Record<string, AnyTool>;
|
|
28
30
|
knowledge?: KnowledgeProviderConfig;
|
package/dist/runtime/Runtime.js
CHANGED
|
@@ -10,6 +10,7 @@ import { wireAgentSkills } from '../skills/wireAgentSkills.js';
|
|
|
10
10
|
import { hostLoop } from './hostLoop.js';
|
|
11
11
|
import { isDegradableRuntimeError } from '../flow/degradableErrors.js';
|
|
12
12
|
import { SAFE_DEGRADED_MESSAGE } from '../flow/degrade.js';
|
|
13
|
+
import { adaptHostSelect } from './hostClassifyAdapter.js';
|
|
13
14
|
import { openRun } from './openRun.js';
|
|
14
15
|
import { closeRun } from './closeRun.js';
|
|
15
16
|
import { SessionRunStore } from './durable/SessionRunStore.js';
|
|
@@ -166,7 +167,8 @@ export class Runtime {
|
|
|
166
167
|
run: runCtx.runState,
|
|
167
168
|
driver,
|
|
168
169
|
ctx: runCtx,
|
|
169
|
-
|
|
170
|
+
classify: this.config.hostClassify ??
|
|
171
|
+
(this.config.hostSelect ? adaptHostSelect(this.config.hostSelect) : undefined),
|
|
170
172
|
});
|
|
171
173
|
if (loopResult.kind === 'handoff') {
|
|
172
174
|
if (this.terminalHandoffTargets.has(loopResult.to)) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { AgentConfig } from '../types/agentConfig.js';
|
|
2
2
|
import type { ReplyNode } from '../types/flow.js';
|
|
3
|
-
|
|
3
|
+
import type { RunState } from './durable/types.js';
|
|
4
|
+
export declare function buildAgentReplyNode(agent: AgentConfig, run?: RunState): ReplyNode;
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { buildToolSet } from '../tools/effect/defineTool.js';
|
|
2
|
-
|
|
2
|
+
import { deriveAgentShape } from './deriveAgent.js';
|
|
3
|
+
import { buildHostControlTools } from './hostControlTools.js';
|
|
4
|
+
export function buildAgentReplyNode(agent, run) {
|
|
5
|
+
const shape = deriveAgentShape(agent);
|
|
6
|
+
const tools = { ...agent.tools };
|
|
7
|
+
if (shape.isAnsweringAgent && run) {
|
|
8
|
+
Object.assign(tools, buildHostControlTools(agent, run));
|
|
9
|
+
}
|
|
3
10
|
const label = agent.name ?? agent.id;
|
|
4
11
|
const instructions = agent.instructions ??
|
|
5
|
-
|
|
12
|
+
(shape.isAnsweringAgent
|
|
13
|
+
? `You are ${label}. Help the user concisely. Do not mention internal routing or flows. ` +
|
|
14
|
+
'If the user wants a specialist or a procedure, CALL the control tool (enter_flow or transfer_to_agent) — ' +
|
|
15
|
+
'do not first say a filler or acknowledgement like "Sure" or "One moment".'
|
|
16
|
+
: '');
|
|
6
17
|
return {
|
|
7
18
|
kind: 'reply',
|
|
8
19
|
id: `${agent.id}__host`,
|
|
9
20
|
instructions,
|
|
10
|
-
tools:
|
|
21
|
+
tools: Object.keys(tools).length > 0 ? buildToolSet(tools) : undefined,
|
|
11
22
|
model: agent.model,
|
|
12
23
|
};
|
|
13
24
|
}
|
|
@@ -9,6 +9,7 @@ export interface TextDriverConfig {
|
|
|
9
9
|
maxSteps?: number;
|
|
10
10
|
}
|
|
11
11
|
export declare class TextDriver implements ChannelDriver {
|
|
12
|
+
readonly outputCapability: "kuralle-controlled-text";
|
|
12
13
|
private readonly toolDefs;
|
|
13
14
|
private readonly maxSteps;
|
|
14
15
|
constructor(config?: TextDriverConfig);
|
|
@@ -6,12 +6,13 @@ import { consumePendingUserInput } from './inputBuffer.js';
|
|
|
6
6
|
import { runSilentExtraction } from './extractionTurn.js';
|
|
7
7
|
import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
|
|
8
8
|
import { resolveMaxSteps } from '../policies/limits.js';
|
|
9
|
-
import {
|
|
9
|
+
import { speakWithHostControl } from './streaming/hostControlSpeak.js';
|
|
10
10
|
import { resolveStreamMode } from './streaming/mode.js';
|
|
11
11
|
import { appendGatherBlocks, resolveNodeGatherScope, runGatherPhase } from '../grounding/index.js';
|
|
12
12
|
import { isFlowTransitionControlTool } from '../../flow/flowControlTools.js';
|
|
13
13
|
import { resolveStructuredDecide } from '../../flow/choiceMatch.js';
|
|
14
14
|
export class TextDriver {
|
|
15
|
+
outputCapability = 'kuralle-controlled-text';
|
|
15
16
|
toolDefs;
|
|
16
17
|
maxSteps;
|
|
17
18
|
constructor(config = {}) {
|
|
@@ -108,24 +109,36 @@ export class TextDriver {
|
|
|
108
109
|
}
|
|
109
110
|
},
|
|
110
111
|
};
|
|
111
|
-
const
|
|
112
|
-
ctx,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
112
|
+
const runGate = async (text, _final) => {
|
|
113
|
+
const r = await applyPostTurnPolicies(ctx, text, toolCallsMade, gather.citations ?? []);
|
|
114
|
+
return {
|
|
115
|
+
blocked: !r.proceed,
|
|
116
|
+
text: r.proceed ? r.text : (r.blockedMessage ?? r.text),
|
|
117
|
+
reason: r.control?.reason,
|
|
118
|
+
control: r.control,
|
|
119
|
+
confidence: r.confidence,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
const speakFn = node.hostControl
|
|
123
|
+
? speakWithHostControl({
|
|
124
|
+
ctx,
|
|
125
|
+
mode,
|
|
126
|
+
turnId,
|
|
127
|
+
source,
|
|
128
|
+
runGate,
|
|
129
|
+
dispatchMode: node.hostControl.dispatchMode,
|
|
130
|
+
getToolControl: () => out.control,
|
|
131
|
+
})
|
|
132
|
+
: (await import('./streaming/speakGated.js')).speakGated({
|
|
133
|
+
ctx,
|
|
134
|
+
mode,
|
|
135
|
+
turnId,
|
|
136
|
+
source,
|
|
137
|
+
runGate,
|
|
138
|
+
});
|
|
139
|
+
const spoken = await speakFn;
|
|
127
140
|
out.text = spoken.text;
|
|
128
|
-
out.control = spoken.control;
|
|
141
|
+
out.control = spoken.control ?? out.control;
|
|
129
142
|
out.confidence = spoken.confidence;
|
|
130
143
|
ctx.emit({ type: 'turn-end' });
|
|
131
144
|
return out;
|
|
@@ -12,6 +12,7 @@ export interface VoiceDriverConfig {
|
|
|
12
12
|
maxSteps?: number;
|
|
13
13
|
}
|
|
14
14
|
export declare class VoiceDriver implements ChannelDriver {
|
|
15
|
+
readonly outputCapability: "native-realtime";
|
|
15
16
|
private readonly client;
|
|
16
17
|
private readonly toolDefs;
|
|
17
18
|
private readonly maxSteps;
|
|
@@ -34,6 +34,7 @@ function emitNativeRealtimePostHocGate(ctx, client, gate) {
|
|
|
34
34
|
client.requestResponse?.(gate.safeText);
|
|
35
35
|
}
|
|
36
36
|
export class VoiceDriver {
|
|
37
|
+
outputCapability = 'native-realtime';
|
|
37
38
|
client;
|
|
38
39
|
toolDefs;
|
|
39
40
|
maxSteps;
|
|
@@ -128,7 +129,10 @@ export class VoiceDriver {
|
|
|
128
129
|
transcript.close('complete');
|
|
129
130
|
const spoken = await speakPromise;
|
|
130
131
|
out.text = spoken.text;
|
|
131
|
-
|
|
132
|
+
// Preserve host-control raised during provider tool execution (enter_flow /
|
|
133
|
+
// transfer_to_agent); the post-hoc gate must not clobber it (else native
|
|
134
|
+
// realtime silently drops valid routing). Same fix as TextDriver.
|
|
135
|
+
out.control = spoken.control ?? out.control;
|
|
132
136
|
out.confidence = spoken.confidence;
|
|
133
137
|
if (postHocGate) {
|
|
134
138
|
// Advisory post-hoc gate: provider audio may already have played; correction does not un-speak it.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TurnControl } from '../../../types/channel.js';
|
|
2
|
+
import type { RunContext } from '../../../types/run-context.js';
|
|
3
|
+
import { type TokenSource, type GateOutcome } from './speakGated.js';
|
|
4
|
+
import type { StreamMode } from './mode.js';
|
|
5
|
+
/**
|
|
6
|
+
* Streaming dispatch gate. Its only job is to control EMISSION per dispatch mode
|
|
7
|
+
* and surface the model's own control tool — it does NOT consult the host guard.
|
|
8
|
+
* The guard has a single owner (`hostLoop`): when this returns empty text and no
|
|
9
|
+
* control, `hostLoop` runs the guard exactly once. Keeping the guard out of here
|
|
10
|
+
* avoids double-evaluation and keeps guard telemetry attributable.
|
|
11
|
+
*
|
|
12
|
+
* - relaxed: stream live; if the model's own control tool fires, cancel any
|
|
13
|
+
* streamed text and return that control.
|
|
14
|
+
* - strict: emit NOTHING until the model's intent is known — buffer until the
|
|
15
|
+
* first substantive token (answering → flush + stream the rest live) or source
|
|
16
|
+
* end (no answer → return empty; `hostLoop` then guards with no leak).
|
|
17
|
+
*/
|
|
18
|
+
export declare function speakWithHostControl(args: {
|
|
19
|
+
ctx: RunContext;
|
|
20
|
+
mode: StreamMode;
|
|
21
|
+
turnId: string;
|
|
22
|
+
source: TokenSource;
|
|
23
|
+
runGate: (fullOrSentence: string, final: boolean) => Promise<GateOutcome>;
|
|
24
|
+
dispatchMode: 'strict' | 'relaxed';
|
|
25
|
+
getToolControl: () => TurnControl | undefined;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
text: string;
|
|
28
|
+
control?: TurnControl;
|
|
29
|
+
confidence?: number;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { speakGated } from './speakGated.js';
|
|
2
|
+
/**
|
|
3
|
+
* Streaming dispatch gate. Its only job is to control EMISSION per dispatch mode
|
|
4
|
+
* and surface the model's own control tool — it does NOT consult the host guard.
|
|
5
|
+
* The guard has a single owner (`hostLoop`): when this returns empty text and no
|
|
6
|
+
* control, `hostLoop` runs the guard exactly once. Keeping the guard out of here
|
|
7
|
+
* avoids double-evaluation and keeps guard telemetry attributable.
|
|
8
|
+
*
|
|
9
|
+
* - relaxed: stream live; if the model's own control tool fires, cancel any
|
|
10
|
+
* streamed text and return that control.
|
|
11
|
+
* - strict: emit NOTHING until the model's intent is known — buffer until the
|
|
12
|
+
* first substantive token (answering → flush + stream the rest live) or source
|
|
13
|
+
* end (no answer → return empty; `hostLoop` then guards with no leak).
|
|
14
|
+
*/
|
|
15
|
+
export async function speakWithHostControl(args) {
|
|
16
|
+
const { ctx, mode, turnId, source, runGate, dispatchMode, getToolControl } = args;
|
|
17
|
+
if (dispatchMode === 'strict') {
|
|
18
|
+
const it = source[Symbol.asyncIterator]();
|
|
19
|
+
const buffered = [];
|
|
20
|
+
let answered = false;
|
|
21
|
+
while (!getToolControl()) {
|
|
22
|
+
const r = await it.next();
|
|
23
|
+
if (r.done) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
buffered.push(r.value);
|
|
27
|
+
if (r.value.delta.trim().length > 0) {
|
|
28
|
+
answered = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const toolControl = getToolControl();
|
|
33
|
+
if (toolControl) {
|
|
34
|
+
return { text: '', control: toolControl };
|
|
35
|
+
}
|
|
36
|
+
// No substantive text → emit nothing; hostLoop owns the empty-turn guard.
|
|
37
|
+
if (!answered) {
|
|
38
|
+
return { text: '', control: undefined };
|
|
39
|
+
}
|
|
40
|
+
// Answering: flush the buffered prefix, then stream the remainder live.
|
|
41
|
+
const flushSource = {
|
|
42
|
+
async *[Symbol.asyncIterator]() {
|
|
43
|
+
for (const chunk of buffered) {
|
|
44
|
+
yield chunk;
|
|
45
|
+
}
|
|
46
|
+
let cur = await it.next();
|
|
47
|
+
while (!cur.done) {
|
|
48
|
+
if (getToolControl()) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
yield cur.value;
|
|
52
|
+
cur = await it.next();
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
return speakGated({ ctx, mode, turnId, source: flushSource, runGate });
|
|
57
|
+
}
|
|
58
|
+
// Relaxed: stream live; stop if the model's own control tool fires.
|
|
59
|
+
let toolControl;
|
|
60
|
+
const wrappedSource = {
|
|
61
|
+
async *[Symbol.asyncIterator]() {
|
|
62
|
+
for await (const chunk of source) {
|
|
63
|
+
toolControl = getToolControl();
|
|
64
|
+
if (toolControl) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
yield chunk;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const spoken = await speakGated({ ctx, mode, turnId, source: wrappedSource, runGate });
|
|
72
|
+
toolControl = getToolControl();
|
|
73
|
+
if (toolControl) {
|
|
74
|
+
if (spoken.text) {
|
|
75
|
+
ctx.emit({ type: 'text-cancel', id: turnId, reason: 'host-control' });
|
|
76
|
+
}
|
|
77
|
+
return { text: '', control: toolControl };
|
|
78
|
+
}
|
|
79
|
+
return spoken;
|
|
80
|
+
}
|
|
@@ -6,5 +6,14 @@ export interface DerivedAgentCapabilities {
|
|
|
6
6
|
hasHandoffs: boolean;
|
|
7
7
|
precedence: 'routes' | 'flows' | 'free';
|
|
8
8
|
}
|
|
9
|
+
export interface AgentShape {
|
|
10
|
+
hasDispatchTargets: boolean;
|
|
11
|
+
hasLocalProcedure: boolean;
|
|
12
|
+
hasLocalAnsweringSurface: boolean;
|
|
13
|
+
isAnsweringAgent: boolean;
|
|
14
|
+
isPureDispatcher: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function hasLocalAnsweringSurface(agent: AgentConfig): boolean;
|
|
17
|
+
export declare function hasDispatchTargets(agent: AgentConfig): boolean;
|
|
18
|
+
export declare function deriveAgentShape(agent: AgentConfig): AgentShape;
|
|
9
19
|
export declare function deriveAgentCapabilities(agent: AgentConfig): DerivedAgentCapabilities;
|
|
10
|
-
export declare function shouldRunHostSelector(agent: AgentConfig, activeFlow?: string, alwaysRoute?: boolean): boolean;
|
|
@@ -1,8 +1,68 @@
|
|
|
1
|
+
function hasPopulatedInstructions(instructions) {
|
|
2
|
+
if (instructions === undefined) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
if (typeof instructions === 'string') {
|
|
6
|
+
return instructions.trim().length > 0;
|
|
7
|
+
}
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
export function hasLocalAnsweringSurface(agent) {
|
|
11
|
+
if (hasPopulatedInstructions(agent.instructions)) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
if (agent.tools && Object.keys(agent.tools).length > 0) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (agent.globalTools && Object.keys(agent.globalTools).length > 0) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
if (agent.knowledge) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (agent.memory) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (agent.skills) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (agent.workspace) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
export function hasDispatchTargets(agent) {
|
|
35
|
+
const routes = agent.routes ?? [];
|
|
36
|
+
if (routes.some((route) => route.agent || route.flow)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if ((agent.agents?.length ?? 0) > 0) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if ((agent.handoffs?.length ?? 0) > 0) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
export function deriveAgentShape(agent) {
|
|
48
|
+
const hasLocalProcedure = (agent.flows?.length ?? 0) > 0;
|
|
49
|
+
const answeringSurface = hasLocalAnsweringSurface(agent);
|
|
50
|
+
const dispatchTargets = hasDispatchTargets(agent);
|
|
51
|
+
const isAnsweringAgent = hasLocalProcedure || answeringSurface;
|
|
52
|
+
const isPureDispatcher = dispatchTargets && !isAnsweringAgent;
|
|
53
|
+
return {
|
|
54
|
+
hasDispatchTargets: dispatchTargets,
|
|
55
|
+
hasLocalProcedure,
|
|
56
|
+
hasLocalAnsweringSurface: answeringSurface,
|
|
57
|
+
isAnsweringAgent,
|
|
58
|
+
isPureDispatcher,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
1
61
|
export function deriveAgentCapabilities(agent) {
|
|
62
|
+
const shape = deriveAgentShape(agent);
|
|
2
63
|
const hasRoutes = (agent.routes?.length ?? 0) > 0;
|
|
3
|
-
const hasFlows =
|
|
64
|
+
const hasFlows = shape.hasLocalProcedure;
|
|
4
65
|
const hasHandoffs = (agent.agents?.length ?? 0) > 0 || (agent.handoffs?.length ?? 0) > 0;
|
|
5
|
-
const hasFreeConversation = true;
|
|
6
66
|
const precedence = hasRoutes
|
|
7
67
|
? 'routes'
|
|
8
68
|
: hasFlows
|
|
@@ -11,18 +71,8 @@ export function deriveAgentCapabilities(agent) {
|
|
|
11
71
|
return {
|
|
12
72
|
hasRoutes,
|
|
13
73
|
hasFlows,
|
|
14
|
-
hasFreeConversation,
|
|
74
|
+
hasFreeConversation: shape.isAnsweringAgent,
|
|
15
75
|
hasHandoffs,
|
|
16
76
|
precedence,
|
|
17
77
|
};
|
|
18
78
|
}
|
|
19
|
-
export function shouldRunHostSelector(agent, activeFlow, alwaysRoute) {
|
|
20
|
-
if (activeFlow) {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
if (alwaysRoute) {
|
|
24
|
-
return true;
|
|
25
|
-
}
|
|
26
|
-
const { hasRoutes, hasFlows } = deriveAgentCapabilities(agent);
|
|
27
|
-
return hasRoutes || hasFlows;
|
|
28
|
-
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AgentConfig } from '../types/agentConfig.js';
|
|
2
|
+
import type { DriverOutputCapability } from '../types/channel.js';
|
|
3
|
+
export type DispatchMode = 'strict' | 'relaxed';
|
|
4
|
+
export declare function resolveDispatchMode(agent: AgentConfig, capability: DriverOutputCapability): DispatchMode;
|
|
5
|
+
export declare function isAdvisoryDispatch(capability: DriverOutputCapability): boolean;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function resolveDispatchMode(agent, capability) {
|
|
2
|
+
if (agent.routing?.dispatch === 'strict') {
|
|
3
|
+
return 'strict';
|
|
4
|
+
}
|
|
5
|
+
switch (capability) {
|
|
6
|
+
case 'kuralle-controlled-text':
|
|
7
|
+
return 'relaxed';
|
|
8
|
+
case 'kuralle-controlled-tts':
|
|
9
|
+
return 'strict';
|
|
10
|
+
case 'native-realtime':
|
|
11
|
+
return 'strict';
|
|
12
|
+
default:
|
|
13
|
+
return 'relaxed';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function isAdvisoryDispatch(capability) {
|
|
17
|
+
return capability === 'native-realtime';
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function adaptHostSelect(select) {
|
|
2
|
+
return async (options) => {
|
|
3
|
+
const selection = await select({
|
|
4
|
+
agent: options.agent,
|
|
5
|
+
run: options.run,
|
|
6
|
+
model: options.model,
|
|
7
|
+
excludeFlowNames: options.excludeFlowNames,
|
|
8
|
+
});
|
|
9
|
+
if (selection.kind === 'keep') {
|
|
10
|
+
return { action: 'keep', confidence: 1 };
|
|
11
|
+
}
|
|
12
|
+
if (selection.kind === 'enterFlow') {
|
|
13
|
+
return { action: 'enterFlow', flowName: selection.flow.name };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
action: 'transfer',
|
|
17
|
+
targetAgentId: selection.agentId,
|
|
18
|
+
reason: selection.reason,
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LanguageModel } from 'ai';
|
|
2
|
+
import type { AgentConfig } from '../types/agentConfig.js';
|
|
3
|
+
import type { TurnControl } from '../types/channel.js';
|
|
4
|
+
import type { RunState } from './durable/types.js';
|
|
5
|
+
import { type HostGuardVerdict, type ClassifyHostOptions } from './select.js';
|
|
6
|
+
export type { HostGuardVerdict };
|
|
7
|
+
export declare function startHostControlGuard(options: {
|
|
8
|
+
agent: AgentConfig;
|
|
9
|
+
run: RunState;
|
|
10
|
+
model: LanguageModel;
|
|
11
|
+
classify?: (opts: ClassifyHostOptions) => Promise<HostGuardVerdict>;
|
|
12
|
+
}): Promise<HostGuardVerdict>;
|
|
13
|
+
export declare function isValidControl(control: TurnControl, agent: AgentConfig, run: RunState): boolean;
|
|
14
|
+
export declare function isValidGuardVerdict(verdict: HostGuardVerdict, agent: AgentConfig, run: RunState): boolean;
|
|
15
|
+
export declare function guardVerdictToControl(verdict: HostGuardVerdict): TurnControl | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Main model control wins when valid. The guard is a forgot-to-route net, NOT a
|
|
18
|
+
* second-guesser: it only overrides when the answering model produced neither a
|
|
19
|
+
* control tool NOR a substantive answer (`mainAnswered`). If the model answered,
|
|
20
|
+
* the answer stands — a guard verdict must not hijack a correct keep answer
|
|
21
|
+
* (which caused observed mis-routes of Q&A turns into flows).
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveHostControl(mainControl: TurnControl | undefined, guardVerdict: HostGuardVerdict | undefined, agent: AgentConfig, run: RunState, mainAnswered: boolean): TurnControl | undefined;
|