jeopi-agent-core 16.4.4 → 16.4.5

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.4.5] - 2026-08-03
6
+
7
+ ### Fixed
8
+
9
+ - Fixed the agent stopping mid-task with raw `<invoke name="…">` markup printed in the reply. When a native-tool-calling model emitted its tool call as visible text instead of a `tool_use` block, the turn carried zero runnable calls, so the loop treated it as a finished answer and the announced work never ran. Such turns are now re-materialized into real tool calls and dispatched normally; the leaked markup is stripped from the visible text. Turns that merely quote tool syntax inside a fenced code block are left untouched, and owned/in-band dialect sessions are unaffected (their stream wrapper already handles this).
10
+
5
11
  ## [16.4.3] - 2026-07-22
6
12
 
7
13
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "jeopi-agent-core",
4
- "version": "16.4.4",
4
+ "version": "16.4.5",
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.4.4",
39
- "jeopi-catalog": "16.4.4",
40
- "jeopi-natives": "16.4.4",
41
- "jeopi-utils": "16.4.4",
42
- "jeopi-wire": "16.4.4",
43
- "jeopi-snapcompact": "16.4.4",
38
+ "jeopi-ai": "16.4.5",
39
+ "jeopi-catalog": "16.4.5",
40
+ "jeopi-natives": "16.4.5",
41
+ "jeopi-utils": "16.4.5",
42
+ "jeopi-wire": "16.4.5",
43
+ "jeopi-snapcompact": "16.4.5",
44
44
  "@opentelemetry/api": "^1.9.1"
45
45
  },
46
46
  "devDependencies": {
package/src/agent-loop.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  encodeInbandToolHistory,
25
25
  renderInbandToolPrompt,
26
26
  renderToolExamples,
27
+ salvageLeakedToolCalls,
27
28
  wrapInbandToolStream,
28
29
  } from "jeopi-ai/dialect";
29
30
  import * as AIError from "jeopi-ai/error";
@@ -38,7 +39,7 @@ import {
38
39
  signalListLabel,
39
40
  } from "jeopi-ai/utils/harmony-leak";
40
41
  import { preferredDialect } from "jeopi-catalog/identity";
41
- import { sanitizeText, structuredCloneJSON } from "jeopi-utils";
42
+ import { logger, sanitizeText, structuredCloneJSON } from "jeopi-utils";
42
43
  import { INTENT_FIELD } from "jeopi-wire";
43
44
  import { type AgentRunCoverage, type AgentRunSummary, ToolCallBlockedError } from "./run-collector";
44
45
  import {
@@ -1233,6 +1234,12 @@ async function streamAssistantResponse(
1233
1234
  };
1234
1235
  }
1235
1236
 
1237
+ // Tool catalog as sent natively this turn. Empty under an owned dialect (the
1238
+ // wire carries no `tools`), which is exactly when leaked-markup salvage must
1239
+ // NOT run — that path already re-materializes in-band calls via the stream
1240
+ // wrapper, and double-parsing would duplicate every call.
1241
+ const nativeWireTools: Context["tools"] = ownedDialect ? undefined : llmContext.tools;
1242
+
1236
1243
  const streamFunction = streamFn || streamSimple;
1237
1244
 
1238
1245
  const dynamicReasoning = config.getReasoning?.();
@@ -1427,6 +1434,7 @@ async function streamAssistantResponse(
1427
1434
  throw new HarmonyLeakInterruption(detection, removed, recovered);
1428
1435
  }
1429
1436
  }
1437
+ finalMessage = applyLeakedToolCallSalvage(finalMessage, nativeWireTools, config);
1430
1438
  finalMessage = snapshotAssistantMessage(finalMessage);
1431
1439
  // Expand inline macros (and any other registered rewrite) on the
1432
1440
  // finalized message before it reaches the context, the UI, or tool
@@ -1529,6 +1537,7 @@ async function streamAssistantResponse(
1529
1537
  throw new HarmonyLeakInterruption(detection, removed, recovered);
1530
1538
  }
1531
1539
  }
1540
+ trailing = applyLeakedToolCallSalvage(trailing, nativeWireTools, config);
1532
1541
  trailing = snapshotAssistantMessage(trailing);
1533
1542
  if (addedPartial) {
1534
1543
  context.messages[context.messages.length - 1] = trailing;
@@ -1606,6 +1615,30 @@ function recoverTransientErrorToolTurn(
1606
1615
  };
1607
1616
  }
1608
1617
 
1618
+ /**
1619
+ * Re-materialize tool calls a native-tool-calling model wrote as visible text.
1620
+ *
1621
+ * Without this the turn holds zero runnable tool calls, so the loop treats it
1622
+ * as a finished answer: the raw `<invoke …>` markup renders to the user and the
1623
+ * work the model announced never runs. `salvageLeakedToolCalls` only fires on
1624
+ * unambiguous leaks (see its guards); anything else returns the message
1625
+ * untouched.
1626
+ */
1627
+ function applyLeakedToolCallSalvage(
1628
+ message: AssistantMessage,
1629
+ nativeWireTools: Context["tools"],
1630
+ config: AgentLoopConfig,
1631
+ ): AssistantMessage {
1632
+ const salvaged = salvageLeakedToolCalls(message, nativeWireTools);
1633
+ if (!salvaged) return message;
1634
+ logger.warn("Recovered tool calls emitted as text", {
1635
+ model: config.model.id,
1636
+ provider: config.model.provider,
1637
+ tools: salvaged.calls.map(call => call.name),
1638
+ });
1639
+ return salvaged.message;
1640
+ }
1641
+
1609
1642
  function emitDiscardedHarmonyPartial(
1610
1643
  partialMessage: AssistantMessage | null,
1611
1644
  stream: EventStream<AgentEvent, AgentMessage[]>,