@kuralle-agents/core 0.3.5 → 0.3.7

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.
@@ -4,5 +4,10 @@ import type { ResolvedNode } from '../types/channel.js';
4
4
  import type { Tool } from '../types/effectTool.js';
5
5
  export declare function resolveInstructions(instructions: Instructions, state: FlowState): string;
6
6
  export declare function buildNodePrompt(node: ReplyNode, state: FlowState): string;
7
+ /** Compose the agent base layer (ADR 0001) into a node's system prompt: the
8
+ * agent's base instructions (persona / safety / grounding) prefix the node's
9
+ * own instructions. Node instructions layer ON TOP — they never replace the
10
+ * base. Base resolves against the current state so dynamic base prompts work. */
11
+ export declare function composeSystem(base: Instructions | undefined, nodeSystem: string, state: FlowState): string;
7
12
  export declare function resolveReplyNode(node: ReplyNode, state: FlowState): ResolvedNode;
8
13
  export declare function resolveCollectExtractionNode(collectNode: CollectNode, missing: string[], state: FlowState, submitTool: Tool): ResolvedNode;
@@ -15,6 +15,14 @@ export function resolveInstructions(instructions, state) {
15
15
  export function buildNodePrompt(node, state) {
16
16
  return resolveInstructions(node.instructions, state);
17
17
  }
18
+ /** Compose the agent base layer (ADR 0001) into a node's system prompt: the
19
+ * agent's base instructions (persona / safety / grounding) prefix the node's
20
+ * own instructions. Node instructions layer ON TOP — they never replace the
21
+ * base. Base resolves against the current state so dynamic base prompts work. */
22
+ export function composeSystem(base, nodeSystem, state) {
23
+ const baseText = base ? resolveInstructions(base, state) : '';
24
+ return [baseText, nodeSystem].filter((s) => s && s.trim()).join('\n\n');
25
+ }
18
26
  function buildNodeTools(node, state) {
19
27
  if (!node.tools) {
20
28
  return {};
@@ -61,6 +61,10 @@ export class Runtime {
61
61
  const effectTools = {
62
62
  ...(this.config.tools ?? {}),
63
63
  ...(opened.agent.effectTools ?? {}),
64
+ // Global tools (ADR 0001) are model-visible in speaking turns via the
65
+ // drivers; register their executors here too so a model call can actually
66
+ // run them. Visibility stays gated (not exposed during collect extraction).
67
+ ...(opened.agent.globalTools ?? {}),
64
68
  };
65
69
  const toolExecutor = new CoreToolExecutor({
66
70
  tools: effectTools,
@@ -97,6 +101,9 @@ export class Runtime {
97
101
  ? buildMemoryService(this.config.memoryService, opened.agent)
98
102
  : undefined,
99
103
  });
104
+ // Agent base layer (ADR 0001): composed into every node turn by the drivers.
105
+ runCtx.baseInstructions = opened.agent.instructions;
106
+ runCtx.globalTools = opened.agent.globalTools;
100
107
  await this.hooks?.onStart?.(runCtx);
101
108
  const driver = opts.driver ?? new TextDriver();
102
109
  let activeAgent = opened.agent;
@@ -1,5 +1,5 @@
1
1
  import { streamText, generateObject } from 'ai';
2
- import { buildNodePrompt, resolveInstructions } from '../../flow/nodeBuilders.js';
2
+ import { buildNodePrompt, resolveInstructions, composeSystem } from '../../flow/nodeBuilders.js';
3
3
  import { buildToolSet } from '../../tools/effect/index.js';
4
4
  import { classifyControl } from '../../flow/classifyControl.js';
5
5
  import { consumePendingUserInput } from './inputBuffer.js';
@@ -29,10 +29,11 @@ export class TextDriver {
29
29
  const gather = await runGatherPhase(ctx);
30
30
  const out = { text: '', toolResults: [] };
31
31
  const model = replyNode.model ?? ctx.model;
32
- const baseSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
32
+ const nodeSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
33
+ const baseSystem = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state);
33
34
  const system = appendGatherBlocks(baseSystem, [gather.retrievalBlock, gather.memoryBlock]);
34
35
  const messages = [...ctx.runState.messages];
35
- const aiTools = this.resolveTools(node);
36
+ const aiTools = this.resolveTools(node, ctx.globalTools);
36
37
  const maxSteps = resolveMaxSteps(ctx.limits, this.maxSteps);
37
38
  const toolCallsMade = [];
38
39
  let draftText = '';
@@ -135,7 +136,7 @@ export class TextDriver {
135
136
  return runSilentExtraction(node, ctx, model, resolveMaxSteps(ctx.limits, this.maxSteps));
136
137
  }
137
138
  async runStructured(node, ctx) {
138
- const base = resolveInstructions(node.instructions, ctx.runState.state);
139
+ const base = composeSystem(ctx.baseInstructions, resolveInstructions(node.instructions, ctx.runState.state), ctx.runState.state);
139
140
  const schema = node.schema;
140
141
  // When the node offers choices (e.g. via withChoices), constrain the model
141
142
  // to return exactly one option id. Otherwise an unconstrained string schema
@@ -159,8 +160,8 @@ export class TextDriver {
159
160
  const input = consumePendingUserInput(ctx.session);
160
161
  return { type: 'message', input };
161
162
  }
162
- resolveTools(resolved) {
163
- const merged = { ...this.toolDefs, ...(resolved.localTools ?? {}) };
163
+ resolveTools(resolved, globalTools) {
164
+ const merged = { ...this.toolDefs, ...(globalTools ?? {}), ...(resolved.localTools ?? {}) };
164
165
  const aiTools = { ...resolved.tools };
165
166
  for (const [name, tool] of Object.entries(merged)) {
166
167
  if (tool && !aiTools[name]) {
@@ -1,6 +1,6 @@
1
1
  import { generateObject } from 'ai';
2
2
  import { runSilentExtraction } from './extractionTurn.js';
3
- import { buildNodePrompt, resolveInstructions } from '../../flow/nodeBuilders.js';
3
+ import { buildNodePrompt, resolveInstructions, composeSystem } from '../../flow/nodeBuilders.js';
4
4
  import { classifyControl } from '../../flow/classifyControl.js';
5
5
  import { applyPreTurnPolicies, applyPostTurnPolicies } from '../policies/agentTurn.js';
6
6
  import { resolveMaxSteps } from '../policies/limits.js';
@@ -31,9 +31,10 @@ export class VoiceDriver {
31
31
  }
32
32
  const gather = await runGatherPhase(ctx);
33
33
  const out = { text: '', toolResults: [] };
34
- const baseSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
34
+ const nodeSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
35
+ const baseSystem = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state);
35
36
  const system = appendGatherBlocks(baseSystem, [gather.retrievalBlock, gather.memoryBlock]);
36
- const geminiTools = resolveVoiceGeminiTools(node, this.toolDefs);
37
+ const geminiTools = resolveVoiceGeminiTools(node, { ...this.toolDefs, ...(ctx.globalTools ?? {}) });
37
38
  const toolCallsMade = [];
38
39
  const maxSteps = resolveMaxSteps(ctx.limits, this.maxSteps);
39
40
  let draftText = '';
@@ -63,7 +64,7 @@ export class VoiceDriver {
63
64
  return out;
64
65
  }
65
66
  async runStructured(node, ctx) {
66
- const system = resolveInstructions(node.instructions, ctx.runState.state);
67
+ const system = composeSystem(ctx.baseInstructions, resolveInstructions(node.instructions, ctx.runState.state), ctx.runState.state);
67
68
  const schema = node.schema;
68
69
  const { object } = await generateObject({
69
70
  model: ctx.model,
@@ -1,6 +1,6 @@
1
1
  import { streamText } from 'ai';
2
2
  import { buildToolSet } from '../../tools/effect/index.js';
3
- import { buildNodePrompt } from '../../flow/nodeBuilders.js';
3
+ import { buildNodePrompt, composeSystem } from '../../flow/nodeBuilders.js';
4
4
  /**
5
5
  * Shared, NON-SPEAKING field extraction for `collect` nodes, used by every
6
6
  * ChannelDriver so text and voice behave identically. It runs the model with the
@@ -13,7 +13,8 @@ import { buildNodePrompt } from '../../flow/nodeBuilders.js';
13
13
  */
14
14
  export async function runSilentExtraction(node, ctx, model, maxSteps) {
15
15
  const replyNode = node.node;
16
- const system = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
16
+ const nodeSystem = node.prompt || buildNodePrompt(replyNode, ctx.runState.state);
17
+ const system = composeSystem(ctx.baseInstructions, nodeSystem, ctx.runState.state);
17
18
  const messages = [...ctx.runState.messages];
18
19
  const aiTools = resolveExtractionTools(node);
19
20
  const out = { text: '', toolResults: [] };
@@ -16,6 +16,12 @@ export interface AgentConfig {
16
16
  model?: LanguageModel;
17
17
  tools?: ToolSet;
18
18
  effectTools?: Record<string, AnyTool>;
19
+ /** Safe, always-available tools made model-visible in EVERY speaking node turn
20
+ * (the agent "base layer", ADR 0001) — e.g. a returns/FAQ knowledge-base
21
+ * lookup the user might ask for mid-flow. This is an explicit allow-list:
22
+ * NEVER put consequential/mutating tools here (they must stay flow-gated), and
23
+ * they are not exposed during non-speaking collect extraction. */
24
+ globalTools?: Record<string, AnyTool>;
19
25
  flows?: Flow[];
20
26
  routes?: Route[];
21
27
  routing?: RoutingPolicy;
@@ -8,6 +8,7 @@ import type { RefinementCapability } from '../capabilities/RefinementCapability.
8
8
  import type { ValidationCapability } from '../capabilities/ValidationCapability.js';
9
9
  import type { Limits } from './guardrails.js';
10
10
  import type { AnyTool } from './effectTool.js';
11
+ import type { Instructions } from './agentConfig.js';
11
12
  export interface EffectToolExecutor {
12
13
  execute(args: {
13
14
  name: string;
@@ -57,6 +58,12 @@ export interface RunContext {
57
58
  * context. Reset to false on every `createRunContext` (i.e. every turn).
58
59
  */
59
60
  turnInputConsumed?: boolean;
61
+ /** Agent base layer (ADR 0001), set when entering a flow. `baseInstructions`
62
+ * is composed as a prefix into every node turn's system prompt (persona /
63
+ * safety / grounding floor); `globalTools` are safe tools made model-visible
64
+ * in every speaking turn. */
65
+ baseInstructions?: Instructions;
66
+ globalTools?: Record<string, AnyTool>;
60
67
  tool(name: string, args: unknown, options?: {
61
68
  toolCallId?: string;
62
69
  def?: AnyTool;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "git+https://github.com/kuralle/kuralle-agents.git",
7
7
  "directory": "packages/kuralle-core"
8
8
  },
9
- "version": "0.3.5",
9
+ "version": "0.3.7",
10
10
  "description": "A framework for structured conversational AI agents",
11
11
  "publishConfig": {
12
12
  "access": "public"
@@ -97,7 +97,7 @@
97
97
  "dotenv": "^16.4.0",
98
98
  "typescript": "^5.3.0",
99
99
  "zod": "^3.23.0",
100
- "@kuralle-agents/realtime-audio": "0.3.5"
100
+ "@kuralle-agents/realtime-audio": "0.3.7"
101
101
  },
102
102
  "dependencies": {
103
103
  "chrono-node": "^2.6.0",