jeopi-agent-core 16.4.3 → 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
|
|
@@ -50,8 +50,11 @@ export declare function upsertFileOperations(summary: string, readFiles: string[
|
|
|
50
50
|
*/
|
|
51
51
|
export declare function truncateToolResultForSummary(text: string): string;
|
|
52
52
|
/**
|
|
53
|
-
* Serialize LLM messages
|
|
54
|
-
|
|
53
|
+
* Serialize LLM messages as plain summary input without provider control tokens.
|
|
54
|
+
*/
|
|
55
|
+
export declare function serializeConversationForSummary(messages: Message[], dialect?: Dialect): string;
|
|
56
|
+
/**
|
|
57
|
+
* Serialize LLM messages to transcript text.
|
|
55
58
|
* Call convertToLlm() first to handle custom message types.
|
|
56
59
|
*/
|
|
57
60
|
export declare function serializeConversation(messages: Message[], dialect?: Dialect): string;
|
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
|
+
"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.
|
|
39
|
-
"jeopi-catalog": "16.4.
|
|
40
|
-
"jeopi-natives": "16.4.
|
|
41
|
-
"jeopi-utils": "16.4.
|
|
42
|
-
"jeopi-wire": "16.4.
|
|
43
|
-
"jeopi-snapcompact": "16.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[]>,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
extractFileOpsFromMessage,
|
|
28
28
|
type FileOperations,
|
|
29
29
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
30
|
-
|
|
30
|
+
serializeConversationForSummary,
|
|
31
31
|
stripReadSelector,
|
|
32
32
|
truncateToolResultForSummary,
|
|
33
33
|
upsertFileOperations,
|
|
@@ -320,7 +320,7 @@ export async function generateBranchSummary(
|
|
|
320
320
|
// Transform to LLM-compatible messages, then serialize to text
|
|
321
321
|
// Serialization prevents the model from treating it as a conversation to continue
|
|
322
322
|
const llmMessages = (options.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
323
|
-
const conversationText =
|
|
323
|
+
const conversationText = serializeConversationForSummary(llmMessages, preferredDialect(model.id));
|
|
324
324
|
|
|
325
325
|
// Build prompt
|
|
326
326
|
const instructions = customInstructions || BRANCH_SUMMARY_PROMPT;
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
extractFileOpsFromMessage,
|
|
64
64
|
type FileOperations,
|
|
65
65
|
SUMMARIZATION_SYSTEM_PROMPT,
|
|
66
|
-
|
|
66
|
+
serializeConversationForSummary,
|
|
67
67
|
stripReadSelector,
|
|
68
68
|
upsertFileOperations,
|
|
69
69
|
} from "./utils";
|
|
@@ -786,7 +786,7 @@ export async function generateSummary(
|
|
|
786
786
|
// Serialize conversation to text so model doesn't try to continue it
|
|
787
787
|
// Convert to LLM messages first (handles custom app messages when caller provides a transformer).
|
|
788
788
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(currentMessages);
|
|
789
|
-
const conversationText =
|
|
789
|
+
const conversationText = serializeConversationForSummary(llmMessages, preferredDialect(model.id));
|
|
790
790
|
|
|
791
791
|
// Build the prompt with conversation wrapped in tags
|
|
792
792
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
@@ -983,7 +983,7 @@ async function generateShortSummary(
|
|
|
983
983
|
): Promise<string> {
|
|
984
984
|
const maxTokens = Math.min(512, Math.floor(0.2 * reserveTokens));
|
|
985
985
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(recentMessages);
|
|
986
|
-
const conversationText =
|
|
986
|
+
const conversationText = serializeConversationForSummary(llmMessages, preferredDialect(model.id));
|
|
987
987
|
|
|
988
988
|
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
|
989
989
|
if (historySummary) {
|
|
@@ -1518,7 +1518,7 @@ async function generateTurnPrefixSummary(
|
|
|
1518
1518
|
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
|
1519
1519
|
|
|
1520
1520
|
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
|
|
1521
|
-
const conversationText =
|
|
1521
|
+
const conversationText = serializeConversationForSummary(llmMessages, preferredDialect(model.id));
|
|
1522
1522
|
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
1523
1523
|
const summarizationMessages = [
|
|
1524
1524
|
{
|
package/src/compaction/utils.ts
CHANGED
|
@@ -207,9 +207,19 @@ export function truncateToolResultForSummary(text: string): string {
|
|
|
207
207
|
return `${text.slice(0, TOOL_RESULT_MAX_CHARS)}\n\n[... ${truncatedChars} more characters truncated]`;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
const HARMONY_CONTROL_TOKEN_RE = /<\|(start|end|message|channel|constrain|return|call)\|>/g;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Serialize LLM messages as plain summary input without provider control tokens.
|
|
214
|
+
*/
|
|
215
|
+
export function serializeConversationForSummary(messages: Message[], dialect?: Dialect): string {
|
|
216
|
+
const conversation = serializeConversation(messages, dialect);
|
|
217
|
+
if (dialect !== "harmony") return conversation;
|
|
218
|
+
return conversation.replace(HARMONY_CONTROL_TOKEN_RE, "<\\|$1\\|>");
|
|
219
|
+
}
|
|
220
|
+
|
|
210
221
|
/**
|
|
211
|
-
* Serialize LLM messages to text
|
|
212
|
-
* This prevents the model from treating it as a conversation to continue.
|
|
222
|
+
* Serialize LLM messages to transcript text.
|
|
213
223
|
* Call convertToLlm() first to handle custom message types.
|
|
214
224
|
*/
|
|
215
225
|
export function serializeConversation(messages: Message[], dialect?: Dialect): string {
|