@kuralle-agents/core 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +29 -57
- package/dist/runtime/agentReply.d.ts +2 -1
- package/dist/runtime/agentReply.js +14 -3
- package/dist/runtime/buildAgentToolSurface.d.ts +20 -0
- package/dist/runtime/buildAgentToolSurface.js +56 -0
- package/dist/runtime/channels/TextDriver.d.ts +1 -0
- package/dist/runtime/channels/TextDriver.js +36 -19
- package/dist/runtime/channels/VoiceDriver.d.ts +1 -0
- package/dist/runtime/channels/VoiceDriver.js +10 -2
- 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/grounding/index.d.ts +1 -1
- package/dist/runtime/grounding/index.js +1 -1
- package/dist/runtime/grounding/knowledge.d.ts +2 -0
- package/dist/runtime/grounding/knowledge.js +37 -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/grounding.d.ts +9 -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
|
@@ -5,19 +5,18 @@ import { TextDriver } from './channels/TextDriver.js';
|
|
|
5
5
|
import { createRunContext } from './ctx.js';
|
|
6
6
|
import { createEventBus, createTurnHandle } from '../events/TurnHandle.js';
|
|
7
7
|
import { CoreToolExecutor } from '../tools/effect/index.js';
|
|
8
|
-
import {
|
|
9
|
-
import { wireAgentSkills } from '../skills/wireAgentSkills.js';
|
|
8
|
+
import { buildAgentToolSurface } from './buildAgentToolSurface.js';
|
|
10
9
|
import { hostLoop } from './hostLoop.js';
|
|
11
10
|
import { isDegradableRuntimeError } from '../flow/degradableErrors.js';
|
|
12
11
|
import { SAFE_DEGRADED_MESSAGE } from '../flow/degrade.js';
|
|
12
|
+
import { adaptHostSelect } from './hostClassifyAdapter.js';
|
|
13
13
|
import { openRun } from './openRun.js';
|
|
14
14
|
import { closeRun } from './closeRun.js';
|
|
15
15
|
import { SessionRunStore } from './durable/SessionRunStore.js';
|
|
16
16
|
import { loadRecordedSteps } from './durable/replay.js';
|
|
17
17
|
import { markSessionOutcome } from './outcomeMarking.js';
|
|
18
18
|
import { resolveAgentPolicies } from './policies/resolvePolicies.js';
|
|
19
|
-
import { buildAutoRetrieveProvider, buildKnowledgeProvider, buildMemoryService, runMemoryIngest,
|
|
20
|
-
import { resolveAgentWorkspace } from './resolveAgentWorkspace.js';
|
|
19
|
+
import { buildAutoRetrieveProvider, buildKnowledgeProvider, buildMemoryService, runMemoryIngest, } from './grounding/index.js';
|
|
21
20
|
import { SessionMutex } from './SessionMutex.js';
|
|
22
21
|
export class Runtime {
|
|
23
22
|
config;
|
|
@@ -65,38 +64,16 @@ export class Runtime {
|
|
|
65
64
|
sessionStore: this.sessionStore,
|
|
66
65
|
});
|
|
67
66
|
const policies = resolveAgentPolicies(opened.agent);
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
76
|
-
const resolvedWorkspace = resolveAgentWorkspace(opened.agent.workspace);
|
|
77
|
-
if (resolvedWorkspace) {
|
|
78
|
-
agentTools.workspace = createFsTool({
|
|
79
|
-
fs: resolvedWorkspace.fs,
|
|
80
|
-
readOnly: resolvedWorkspace.readOnly,
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
const wiredWorkingMemory = await wireWorkingMemory(opened.agent, opened.session, this.config.defaultWorkingMemoryStore);
|
|
84
|
-
if (wiredWorkingMemory) {
|
|
85
|
-
agentTools.memory_block = wiredWorkingMemory.memoryBlockTool;
|
|
86
|
-
}
|
|
87
|
-
let skillPrompt;
|
|
88
|
-
let skillTools = {};
|
|
89
|
-
if (opened.agent.skills) {
|
|
90
|
-
const wired = await wireAgentSkills(opened.agent);
|
|
91
|
-
if (wired) {
|
|
92
|
-
skillTools = wired.tools;
|
|
93
|
-
Object.assign(agentTools, wired.tools);
|
|
94
|
-
skillPrompt = wired.promptSections.map((s) => s.content).join('\n\n');
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const workspaceTool = agentTools.workspace;
|
|
67
|
+
const knowledgeProvider = this.config.knowledge
|
|
68
|
+
? buildKnowledgeProvider(this.config.knowledge)
|
|
69
|
+
: undefined;
|
|
70
|
+
const openingSurface = await buildAgentToolSurface(opened.agent, opened.session, {
|
|
71
|
+
configTools: this.config.tools,
|
|
72
|
+
knowledgeProvider,
|
|
73
|
+
defaultWorkingMemoryStore: this.config.defaultWorkingMemoryStore,
|
|
74
|
+
});
|
|
98
75
|
const toolExecutor = new CoreToolExecutor({
|
|
99
|
-
tools:
|
|
76
|
+
tools: openingSurface.executorTools,
|
|
100
77
|
enforcer: policies.enforcer,
|
|
101
78
|
agentId: opened.agent.id,
|
|
102
79
|
onInterim: (message) => {
|
|
@@ -112,9 +89,6 @@ export class Runtime {
|
|
|
112
89
|
if (!model) {
|
|
113
90
|
throw new Error('Runtime requires agent.model or config.defaultModel');
|
|
114
91
|
}
|
|
115
|
-
const knowledgeProvider = this.config.knowledge
|
|
116
|
-
? buildKnowledgeProvider(this.config.knowledge)
|
|
117
|
-
: undefined;
|
|
118
92
|
runCtx = await createRunContext({
|
|
119
93
|
session: opened.session,
|
|
120
94
|
runState: freshRunState,
|
|
@@ -136,23 +110,15 @@ export class Runtime {
|
|
|
136
110
|
memoryService: this.config.memoryService
|
|
137
111
|
? buildMemoryService(this.config.memoryService, opened.agent)
|
|
138
112
|
: undefined,
|
|
139
|
-
fs: resolvedWorkspace?.fs,
|
|
113
|
+
fs: openingSurface.resolvedWorkspace?.fs,
|
|
140
114
|
});
|
|
141
115
|
// Agent base layer (ADR 0001): composed into every node turn by the drivers.
|
|
142
116
|
runCtx.baseInstructions = opened.agent.instructions;
|
|
143
|
-
runCtx.globalTools =
|
|
144
|
-
...(opened.agent.globalTools ?? {}),
|
|
145
|
-
...(workspaceTool && resolvedWorkspace?.readOnly !== false
|
|
146
|
-
? { workspace: workspaceTool }
|
|
147
|
-
: {}),
|
|
148
|
-
...skillTools,
|
|
149
|
-
};
|
|
117
|
+
runCtx.globalTools = openingSurface.globalTools;
|
|
150
118
|
runCtx.outOfBandControl = opened.agent.experimental?.outOfBandControl ?? false;
|
|
151
|
-
runCtx.skillPrompt = skillPrompt;
|
|
152
|
-
runCtx.workingMemoryPrompt =
|
|
153
|
-
runCtx.workingMemoryTools =
|
|
154
|
-
? { memory_block: wiredWorkingMemory.memoryBlockTool }
|
|
155
|
-
: undefined;
|
|
119
|
+
runCtx.skillPrompt = openingSurface.skillPrompt;
|
|
120
|
+
runCtx.workingMemoryPrompt = openingSurface.workingMemoryPrompt;
|
|
121
|
+
runCtx.workingMemoryTools = openingSurface.workingMemoryTools;
|
|
156
122
|
await this.hooks?.onStart?.(runCtx);
|
|
157
123
|
const driver = opts.driver ?? new TextDriver();
|
|
158
124
|
let activeAgent = opened.agent;
|
|
@@ -166,7 +132,8 @@ export class Runtime {
|
|
|
166
132
|
run: runCtx.runState,
|
|
167
133
|
driver,
|
|
168
134
|
ctx: runCtx,
|
|
169
|
-
|
|
135
|
+
classify: this.config.hostClassify ??
|
|
136
|
+
(this.config.hostSelect ? adaptHostSelect(this.config.hostSelect) : undefined),
|
|
170
137
|
});
|
|
171
138
|
if (loopResult.kind === 'handoff') {
|
|
172
139
|
if (this.terminalHandoffTargets.has(loopResult.to)) {
|
|
@@ -191,17 +158,22 @@ export class Runtime {
|
|
|
191
158
|
});
|
|
192
159
|
runCtx.runState.activeAgentId = loopResult.to;
|
|
193
160
|
activeAgent = target;
|
|
161
|
+
const targetSurface = await buildAgentToolSurface(target, opened.session, {
|
|
162
|
+
configTools: this.config.tools,
|
|
163
|
+
knowledgeProvider,
|
|
164
|
+
defaultWorkingMemoryStore: this.config.defaultWorkingMemoryStore,
|
|
165
|
+
});
|
|
194
166
|
runCtx.autoRetrieve = knowledgeProvider
|
|
195
167
|
? buildAutoRetrieveProvider(knowledgeProvider, target)
|
|
196
168
|
: undefined;
|
|
169
|
+
runCtx.globalTools = targetSurface.globalTools;
|
|
170
|
+
runCtx.skillPrompt = targetSurface.skillPrompt;
|
|
171
|
+
runCtx.workingMemoryPrompt = targetSurface.workingMemoryPrompt;
|
|
172
|
+
runCtx.workingMemoryTools = targetSurface.workingMemoryTools;
|
|
173
|
+
runCtx.fs = targetSurface.resolvedWorkspace?.fs;
|
|
197
174
|
runCtx.memoryService = this.config.memoryService
|
|
198
175
|
? buildMemoryService(this.config.memoryService, target)
|
|
199
176
|
: undefined;
|
|
200
|
-
const targetWorkingMemory = await wireWorkingMemory(target, opened.session, this.config.defaultWorkingMemoryStore);
|
|
201
|
-
runCtx.workingMemoryPrompt = targetWorkingMemory?.promptSection;
|
|
202
|
-
runCtx.workingMemoryTools = targetWorkingMemory
|
|
203
|
-
? { memory_block: targetWorkingMemory.memoryBlockTool }
|
|
204
|
-
: undefined;
|
|
205
177
|
await runCtx.runStore.putRunState(runCtx.runState);
|
|
206
178
|
continue;
|
|
207
179
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentConfig } from '../types/agentConfig.js';
|
|
2
|
+
import type { Session } from '../types/session.js';
|
|
3
|
+
import type { AnyTool } from '../types/effectTool.js';
|
|
4
|
+
import type { PersistentMemoryStore } from '../memory/blocks/types.js';
|
|
5
|
+
import type { KnowledgeProvider } from './KnowledgeProvider.js';
|
|
6
|
+
import { type ResolvedAgentWorkspace } from './resolveAgentWorkspace.js';
|
|
7
|
+
export interface AgentToolSurface {
|
|
8
|
+
executorTools: Record<string, AnyTool>;
|
|
9
|
+
globalTools: Record<string, AnyTool>;
|
|
10
|
+
workingMemoryTools?: Record<string, AnyTool>;
|
|
11
|
+
workingMemoryPrompt?: string;
|
|
12
|
+
skillPrompt?: string;
|
|
13
|
+
resolvedWorkspace?: ResolvedAgentWorkspace;
|
|
14
|
+
}
|
|
15
|
+
export interface BuildAgentToolSurfaceDeps {
|
|
16
|
+
configTools?: Record<string, AnyTool>;
|
|
17
|
+
knowledgeProvider?: KnowledgeProvider;
|
|
18
|
+
defaultWorkingMemoryStore?: PersistentMemoryStore;
|
|
19
|
+
}
|
|
20
|
+
export declare function buildAgentToolSurface(agent: AgentConfig, session: Session, deps: BuildAgentToolSurfaceDeps): Promise<AgentToolSurface>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createFsTool } from '../tools/fs/createFsTool.js';
|
|
2
|
+
import { wireAgentSkills } from '../skills/wireAgentSkills.js';
|
|
3
|
+
import { buildKnowledgeTool, wireWorkingMemory } from './grounding/index.js';
|
|
4
|
+
import { resolveAgentWorkspace, } from './resolveAgentWorkspace.js';
|
|
5
|
+
export async function buildAgentToolSurface(agent, session, deps) {
|
|
6
|
+
const executorTools = {
|
|
7
|
+
...(deps.configTools ?? {}),
|
|
8
|
+
...(agent.tools ?? {}),
|
|
9
|
+
...(agent.globalTools ?? {}),
|
|
10
|
+
};
|
|
11
|
+
const resolvedWorkspace = resolveAgentWorkspace(agent.workspace);
|
|
12
|
+
let workspaceTool;
|
|
13
|
+
if (resolvedWorkspace) {
|
|
14
|
+
workspaceTool = createFsTool({
|
|
15
|
+
fs: resolvedWorkspace.fs,
|
|
16
|
+
readOnly: resolvedWorkspace.readOnly,
|
|
17
|
+
});
|
|
18
|
+
executorTools.workspace = workspaceTool;
|
|
19
|
+
}
|
|
20
|
+
const wiredWorkingMemory = await wireWorkingMemory(agent, session, deps.defaultWorkingMemoryStore);
|
|
21
|
+
if (wiredWorkingMemory) {
|
|
22
|
+
executorTools.memory_block = wiredWorkingMemory.memoryBlockTool;
|
|
23
|
+
}
|
|
24
|
+
let skillPrompt;
|
|
25
|
+
let skillTools = {};
|
|
26
|
+
if (agent.skills) {
|
|
27
|
+
const wired = await wireAgentSkills(agent);
|
|
28
|
+
if (wired) {
|
|
29
|
+
skillTools = wired.tools;
|
|
30
|
+
Object.assign(executorTools, wired.tools);
|
|
31
|
+
skillPrompt = wired.promptSections.map((s) => s.content).join('\n\n');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const knowledgeTool = deps.knowledgeProvider
|
|
35
|
+
? buildKnowledgeTool(deps.knowledgeProvider, agent)
|
|
36
|
+
: undefined;
|
|
37
|
+
if (knowledgeTool) {
|
|
38
|
+
executorTools.knowledge_search = knowledgeTool;
|
|
39
|
+
}
|
|
40
|
+
const globalTools = {
|
|
41
|
+
...(agent.globalTools ?? {}),
|
|
42
|
+
...(workspaceTool && resolvedWorkspace?.readOnly !== false ? { workspace: workspaceTool } : {}),
|
|
43
|
+
...skillTools,
|
|
44
|
+
...(knowledgeTool ? { knowledge_search: knowledgeTool } : {}),
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
executorTools,
|
|
48
|
+
globalTools,
|
|
49
|
+
workingMemoryPrompt: wiredWorkingMemory?.promptSection,
|
|
50
|
+
workingMemoryTools: wiredWorkingMemory
|
|
51
|
+
? { memory_block: wiredWorkingMemory.memoryBlockTool }
|
|
52
|
+
: undefined,
|
|
53
|
+
skillPrompt,
|
|
54
|
+
resolvedWorkspace,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -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 = {}) {
|
|
@@ -81,7 +82,11 @@ export class TextDriver {
|
|
|
81
82
|
args: call.input,
|
|
82
83
|
toolCallId: call.toolCallId,
|
|
83
84
|
});
|
|
84
|
-
const { result: toolResult, control, failed } = await executeModelToolCall(ctx, { toolName: call.toolName, input: call.input, toolCallId: call.toolCallId },
|
|
85
|
+
const { result: toolResult, control, failed } = await executeModelToolCall(ctx, { toolName: call.toolName, input: call.input, toolCallId: call.toolCallId }, {
|
|
86
|
+
...ctx.globalTools,
|
|
87
|
+
...(ctx.workingMemoryTools ?? {}),
|
|
88
|
+
...node.localTools,
|
|
89
|
+
});
|
|
85
90
|
out.toolResults.push({
|
|
86
91
|
name: call.toolName,
|
|
87
92
|
args: call.input,
|
|
@@ -108,24 +113,36 @@ export class TextDriver {
|
|
|
108
113
|
}
|
|
109
114
|
},
|
|
110
115
|
};
|
|
111
|
-
const
|
|
112
|
-
ctx,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
116
|
+
const runGate = async (text, _final) => {
|
|
117
|
+
const r = await applyPostTurnPolicies(ctx, text, toolCallsMade, gather.citations ?? []);
|
|
118
|
+
return {
|
|
119
|
+
blocked: !r.proceed,
|
|
120
|
+
text: r.proceed ? r.text : (r.blockedMessage ?? r.text),
|
|
121
|
+
reason: r.control?.reason,
|
|
122
|
+
control: r.control,
|
|
123
|
+
confidence: r.confidence,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const speakFn = node.hostControl
|
|
127
|
+
? speakWithHostControl({
|
|
128
|
+
ctx,
|
|
129
|
+
mode,
|
|
130
|
+
turnId,
|
|
131
|
+
source,
|
|
132
|
+
runGate,
|
|
133
|
+
dispatchMode: node.hostControl.dispatchMode,
|
|
134
|
+
getToolControl: () => out.control,
|
|
135
|
+
})
|
|
136
|
+
: (await import('./streaming/speakGated.js')).speakGated({
|
|
137
|
+
ctx,
|
|
138
|
+
mode,
|
|
139
|
+
turnId,
|
|
140
|
+
source,
|
|
141
|
+
runGate,
|
|
142
|
+
});
|
|
143
|
+
const spoken = await speakFn;
|
|
127
144
|
out.text = spoken.text;
|
|
128
|
-
out.control = spoken.control;
|
|
145
|
+
out.control = spoken.control ?? out.control;
|
|
129
146
|
out.confidence = spoken.confidence;
|
|
130
147
|
ctx.emit({ type: 'turn-end' });
|
|
131
148
|
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.
|
|
@@ -198,7 +202,11 @@ export class VoiceDriver {
|
|
|
198
202
|
const onToolCall = (id, name, args) => {
|
|
199
203
|
void (async () => {
|
|
200
204
|
ctx.emit({ type: 'tool-call', toolName: name, args, toolCallId: id });
|
|
201
|
-
const { result: toolResult, control, failed } = await executeModelToolCall(ctx, { toolName: name, input: args, toolCallId: id },
|
|
205
|
+
const { result: toolResult, control, failed } = await executeModelToolCall(ctx, { toolName: name, input: args, toolCallId: id }, {
|
|
206
|
+
...ctx.globalTools,
|
|
207
|
+
...(ctx.workingMemoryTools ?? {}),
|
|
208
|
+
...localTools,
|
|
209
|
+
});
|
|
202
210
|
out.toolResults.push({ name, args, result: toolResult, toolCallId: id });
|
|
203
211
|
toolCallsMade.push({
|
|
204
212
|
toolCallId: id,
|
|
@@ -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
|
+
}
|