@kuralle-agents/core 0.3.2 → 0.3.3

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.
@@ -19,6 +19,22 @@ export async function collectUntilComplete(node, run, driver, ctx) {
19
19
  const data = projectCollectData(node, run.state);
20
20
  return normalizeTransition(await node.onComplete(data, run.state));
21
21
  }
22
+ // Acquire THIS turn's fresh input before extracting. If input is pending,
23
+ // consume it so extraction reads the user's actual reply. If nothing is
24
+ // pending AND the turn's input was already consumed by a prior node, pause:
25
+ // the prompt was presented on node-enter, so await the next turn rather than
26
+ // running extraction over stale history (which makes the model fabricate
27
+ // required fields). On the run's first input-node the turn's input is in
28
+ // `messages` with nothing pending and `turnInputConsumed` false, so we fall
29
+ // through and extract it.
30
+ if (hasPendingUserInput(ctx.session)) {
31
+ const signal = await driver.awaitUser(ctx);
32
+ appendUserMessage(run, signal.input);
33
+ }
34
+ else if (ctx.turnInputConsumed) {
35
+ return { kind: 'stay' };
36
+ }
37
+ ctx.turnInputConsumed = true;
22
38
  const turns = incrementCollectTurns(run.state, node.id);
23
39
  const maxTurns = node.maxTurns ?? 10;
24
40
  if (turns > maxTurns) {
@@ -33,14 +49,6 @@ export async function collectUntilComplete(node, run, driver, ctx) {
33
49
  const turn = await driver.runAgentTurn(resolved, ctx);
34
50
  mergeExtractionFromTurn(node, run, turn);
35
51
  appendAssistantMessage(run, turn.text);
36
- if (schemaSatisfied(node, run.state)) {
37
- continue;
38
- }
39
- if (!hasPendingUserInput(ctx.session)) {
40
- return { kind: 'stay' };
41
- }
42
- const signal = await driver.awaitUser(ctx);
43
- appendUserMessage(run, signal.input);
44
52
  }
45
53
  }
46
54
  function mergeExtractionFromTurn(node, run, turn) {
@@ -61,13 +61,13 @@ async function dispatchNode(node, run, driver, ctx) {
61
61
  if (!driver.runStructured) {
62
62
  throw new Error('ChannelDriver.runStructured is required for decide nodes');
63
63
  }
64
- // An interactive choice node (withChoices) reached without the user's reply
65
- // this turn: its choices were already presented on node-enter, so wait for
66
- // the user to actually pick rather than auto-deciding on stale conversation
67
- // context. Returning `stay` lets the loop park as `awaitingUser` (it already
68
- // persists state + ends the turn when no input is pending). (A plain decide
69
- // with no choices is a pure branch and still runs.)
70
- if (node.choices?.length && !hasPendingUserInput(ctx.session)) {
64
+ // An interactive choice node (withChoices) reached when the turn's input was
65
+ // already consumed by a prior node: its choices were presented on node-enter,
66
+ // so wait for the user to actually pick rather than auto-deciding on stale
67
+ // context. Returning `stay` lets the loop park as `awaitingUser`. (A plain
68
+ // decide with no choices is a pure branch and still runs; and an interactive
69
+ // decide that IS the turn's first input-node still decides on that input.)
70
+ if (node.choices?.length && !hasPendingUserInput(ctx.session) && ctx.turnInputConsumed) {
71
71
  return { kind: 'stay' };
72
72
  }
73
73
  // On resume, the new turn's input is buffered as pending and is not yet in
@@ -79,6 +79,8 @@ async function dispatchNode(node, run, driver, ctx) {
79
79
  const signal = await driver.awaitUser(ctx);
80
80
  appendUserMessage(run, signal.input);
81
81
  }
82
+ // This decide consumes the turn's input for its decision.
83
+ ctx.turnInputConsumed = true;
82
84
  const structured = await driver.runStructured(node, ctx);
83
85
  return normalizeTransition(await node.decide(structured, run.state));
84
86
  }
@@ -108,6 +108,7 @@ function makeCtx(deps) {
108
108
  memoryService: deps.memoryService,
109
109
  bargeIn: deps.bargeIn,
110
110
  abortSignal: deps.abortSignal,
111
+ turnInputConsumed: false,
111
112
  tool: async (name, args, options) => {
112
113
  // needsApproval gate: a tool flagged `needsApproval` must be approved by a human
113
114
  // before it runs. Approval is a durable pause (the `__approval` signal); on resume
@@ -49,6 +49,14 @@ export interface RunContext {
49
49
  bargeIn?: AbortSignal;
50
50
  abortSignal?: AbortSignal;
51
51
  telemetry?: TelemetrySettings;
52
+ /**
53
+ * Ephemeral, per-run-invocation flag: has the current turn's user input been
54
+ * consumed yet by an input-node (collect/decide)? Input-nodes extract/decide
55
+ * from the turn's fresh input; once it is consumed, later nodes in the same
56
+ * turn pause (present prompt, await next turn) instead of acting on stale
57
+ * context. Reset to false on every `createRunContext` (i.e. every turn).
58
+ */
59
+ turnInputConsumed?: boolean;
52
60
  tool(name: string, args: unknown, options?: {
53
61
  toolCallId?: string;
54
62
  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.2",
9
+ "version": "0.3.3",
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.2"
100
+ "@kuralle-agents/realtime-audio": "0.3.3"
101
101
  },
102
102
  "dependencies": {
103
103
  "chrono-node": "^2.6.0",