jeopi-agent-core 16.2.28 → 16.2.30

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.2.30] - 2026-07-10
6
+
7
+ ### Fixed
8
+
9
+ - The agent loop now auto-continues a text response truncated by the output-token limit (`stopReason: "length"` with no tool calls, e.g. Sonnet 5 hitting `max_tokens` mid-answer): a synthetic "continue from where you were cut off" user turn is injected and the loop re-samples instead of silently stopping with a cut-off message. Capped at 8 consecutive continuations (`MAX_LENGTH_TRUNCATE_CONTINUATIONS`); any tool call resets the counter. Truncated turns that carry tool calls keep the existing abandoned-toolcall path.
10
+ - Steering/aside dequeues at the turn boundary no longer overwrite already-queued pending messages (the injected length-continuation turn was previously dropped when steering arrived in the same window); they now append.
11
+
5
12
  ## [16.2.26] - 2026-07-05
6
13
 
7
14
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-agent-core",
4
- "version": "16.2.28",
4
+ "version": "16.2.30",
5
5
  "description": "General-purpose agent with transport abstraction, state management, and attachment support",
6
6
  "homepage": "https://github.com/akillness/jeopi",
7
7
  "author": "Can Boluk",
@@ -35,12 +35,12 @@
35
35
  "fmt": "biome format --write ."
36
36
  },
37
37
  "dependencies": {
38
- "jeopi-ai": "16.2.28",
39
- "jeopi-catalog": "16.2.28",
40
- "jeopi-natives": "16.2.28",
41
- "jeopi-utils": "16.2.28",
42
- "jeopi-wire": "16.2.28",
43
- "jeopi-snapcompact": "16.2.28",
38
+ "jeopi-ai": "16.2.30",
39
+ "jeopi-catalog": "16.2.30",
40
+ "jeopi-natives": "16.2.30",
41
+ "jeopi-utils": "16.2.30",
42
+ "jeopi-wire": "16.2.30",
43
+ "jeopi-snapcompact": "16.2.30",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  type ToolResultMessage,
17
17
  type TSchema,
18
18
  toolWireSchema,
19
+ type UserMessage,
19
20
  validateToolArguments,
20
21
  } from "jeopi-ai";
21
22
  import {
@@ -84,6 +85,12 @@ const ABORTED: unique symbol = Symbol("agent-loop-aborted");
84
85
  */
85
86
  const MAX_PAUSED_TURN_CONTINUATIONS = 8;
86
87
 
88
+ /**
89
+ * Cap on consecutive re-samples triggered by output truncation stop
90
+ * (`stopReason === "length"`) without an intervening tool call.
91
+ */
92
+ const MAX_LENGTH_TRUNCATE_CONTINUATIONS = 8;
93
+
87
94
  /**
88
95
  * Cap on consecutive forced escalations for a single soft tool requirement.
89
96
  * A forced `toolChoice` guarantees the call, so this is purely defensive: if a
@@ -758,6 +765,7 @@ async function runLoopBody(
758
765
  let harmonyRetryAttempt = 0;
759
766
  let harmonyTruncateResumeCount = 0;
760
767
  let pausedTurnContinuations = 0;
768
+ let lengthTruncateContinuations = 0;
761
769
 
762
770
  // Soft tool requirement lifecycle (reminder → escalate; see SoftToolRequirement).
763
771
  // `forcedToolChoice` carries a one-turn escalation into the next model call. It
@@ -1042,6 +1050,7 @@ async function runLoopBody(
1042
1050
 
1043
1051
  if (toolCalls.length > 0) {
1044
1052
  pausedTurnContinuations = 0;
1053
+ lengthTruncateContinuations = 0;
1045
1054
  } else if (
1046
1055
  !hasMoreToolCalls &&
1047
1056
  message.stopReason === "stop" &&
@@ -1055,6 +1064,22 @@ async function runLoopBody(
1055
1064
  // mid-work turn.
1056
1065
  pausedTurnContinuations++;
1057
1066
  hasMoreToolCalls = true;
1067
+ } else if (
1068
+ !hasMoreToolCalls &&
1069
+ message.stopReason === "length" &&
1070
+ lengthTruncateContinuations < MAX_LENGTH_TRUNCATE_CONTINUATIONS &&
1071
+ !deadlinePassed
1072
+ ) {
1073
+ // Prompt the model to continue the truncated response
1074
+ lengthTruncateContinuations++;
1075
+ const continuationMessage: UserMessage = {
1076
+ role: "user",
1077
+ content: "Continue your response exactly from where it was cut off.",
1078
+ synthetic: true,
1079
+ timestamp: Date.now(),
1080
+ };
1081
+ pendingMessages = [continuationMessage];
1082
+ hasMoreToolCalls = true;
1058
1083
  }
1059
1084
 
1060
1085
  await emitTurnEnd(stream, currentContext, message, toolResults, config, signal, {
@@ -1075,12 +1100,13 @@ async function runLoopBody(
1075
1100
  if (hasMoreToolCalls) {
1076
1101
  // Mid-work: fold any non-interrupting asides into the next turn alongside steering.
1077
1102
  const asides = signal?.aborted ? [] : resolveAsides(await config.getAsideMessages?.());
1078
- pendingMessages = asides.length > 0 ? [...steering, ...asides] : steering;
1103
+ const basePending = asides.length > 0 ? [...steering, ...asides] : steering;
1104
+ pendingMessages = [...pendingMessages, ...basePending];
1079
1105
  } else {
1080
1106
  // Stop boundary: only steering (live user input) forces another turn here. Leave
1081
1107
  // asides for the outer drain below so a passive aside can't trigger an extra model
1082
1108
  // turn ahead of a queued follow-up — the outer drain batches asides + follow-ups together.
1083
- pendingMessages = steering;
1109
+ pendingMessages = [...pendingMessages, ...steering];
1084
1110
  }
1085
1111
  }
1086
1112