@sayknow-cli/agent-core 0.4.6 → 0.5.0
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,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.12.0] - 2026-07-28
|
|
6
|
+
|
|
7
|
+
## [0.11.11] - 2026-07-26
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Managed runs now release their logical-run ownership before terminal observers are notified, so terminal overflow recovery cannot leave a stale owner behind.
|
|
12
|
+
- The OpenAI remote-compaction endpoint is now resolved from trusted environment sources only. `OPENAI_BASE_URL` was read through the merged view that includes the caller's `cwd/.env`, so a repository could redirect compaction requests that carry the OpenAI credential; it now uses the non-project resolver, leaving shell and user-level configuration unchanged.
|
|
13
|
+
- Repeated malformed tool calls now get one tool-free recovery response, preventing argument-validation loops from ending without an answer while leaving ordinary execution-error retries unchanged. The recovery turn commits its assistant to the durable context, forces `toolChoice: "none"` alongside an empty tool list without consuming a queued tool choice, and never executes a tool call it did not advertise. Its recovery prompt is request-only, so append-only tool prefixes stay stable and the durable message log is unchanged.
|
|
14
|
+
- Argument-validation loops now reach a deterministic terminal state. If a model keeps emitting only malformed tool calls after the one-shot recovery turn, the run stops with an explanatory error instead of calling the provider indefinitely. The bound counts consecutive all-malformed turns rather than repeated argument signatures, so a model rotating invalid argument shapes is bounded too; any healthy tool turn resets it.
|
|
15
|
+
|
|
16
|
+
## [0.11.8] - 2026-07-23
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Managed model fallback now accepts `reasoning_summary_start`, `reasoning_summary_delta`, and `reasoning_summary_end` assistant events instead of failing them as local snapshot errors.
|
|
5
21
|
## [0.11.3] - 2026-07-19
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
@@ -39,6 +39,8 @@ export interface RemoteCompactionResponse {
|
|
|
39
39
|
shortSummary?: string;
|
|
40
40
|
}
|
|
41
41
|
export declare function shouldUseOpenAiRemoteCompaction(model: Model): boolean;
|
|
42
|
+
/** Test seam: the compaction endpoint as resolved from trusted env. */
|
|
43
|
+
export declare function resolveOpenAiCompactEndpointForTest(model: Model, authCredentialType?: "api_key" | "oauth"): string;
|
|
42
44
|
export declare function getPreservedOpenAiRemoteCompactionData(preserveData: Record<string, unknown> | undefined): OpenAiRemoteCompactionPreserveData | undefined;
|
|
43
45
|
export declare function withOpenAiRemoteCompactionPreserveData(preserveData: Record<string, unknown> | undefined, remoteCompaction: OpenAiRemoteCompactionPreserveData | undefined): Record<string, unknown> | undefined;
|
|
44
46
|
export declare function estimateOpenAiCompactInputTokens(input: Array<Record<string, unknown>>, instructions: string): number;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@sayknow-cli/agent-core",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://sayknow-cli.com",
|
|
7
7
|
"author": "jaybeyond",
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@sayknow-cli/ai": "0.
|
|
39
|
-
"@sayknow-cli/natives": "0.
|
|
40
|
-
"@sayknow-cli/utils": "0.
|
|
38
|
+
"@sayknow-cli/ai": "0.5.0",
|
|
39
|
+
"@sayknow-cli/natives": "0.5.0",
|
|
40
|
+
"@sayknow-cli/utils": "0.5.0",
|
|
41
41
|
"@opentelemetry/api": "^1.9.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type ToolResultMessage,
|
|
17
17
|
type TSchema,
|
|
18
18
|
transportFailureFacts,
|
|
19
|
+
type UserMessage,
|
|
19
20
|
validateToolArguments,
|
|
20
21
|
zodToWireSchema,
|
|
21
22
|
} from "@sayknow-cli/ai";
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
shouldMitigateHarmonyLeak,
|
|
33
34
|
signalListLabel,
|
|
34
35
|
} from "./harmony-leak";
|
|
36
|
+
import repeatedToolFailureRecoveryPrompt from "./prompts/repeated-tool-failure-recovery.md" with { type: "text" };
|
|
35
37
|
import { type AgentRunCoverage, type AgentRunSummary, ToolCallBlockedError } from "./run-collector";
|
|
36
38
|
import {
|
|
37
39
|
type AgentTelemetry,
|
|
@@ -100,6 +102,17 @@ class ManagedAttemptSnapshotError extends Error {
|
|
|
100
102
|
const managedAttemptTextEncoder = new TextEncoder();
|
|
101
103
|
|
|
102
104
|
const ABORTED: unique symbol = Symbol("agent-loop-aborted");
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Terminal bound for argument-validation loops: how many CONSECUTIVE turns may
|
|
108
|
+
* consist entirely of malformed tool calls before the run stops.
|
|
109
|
+
*
|
|
110
|
+
* The one-shot tools-free recovery turn fires first; this is the deterministic
|
|
111
|
+
* backstop for a model that keeps emitting unusable calls after it. Counted per
|
|
112
|
+
* turn rather than per argument signature so a model rotating invalid shapes is
|
|
113
|
+
* bounded too.
|
|
114
|
+
*/
|
|
115
|
+
const MAX_CONSECUTIVE_MALFORMED_TURNS = 5;
|
|
103
116
|
function managedContextOverflow(message: AssistantMessage, config: AgentLoopConfig): boolean {
|
|
104
117
|
const transportFailure = managedTransportFailure(message);
|
|
105
118
|
// Managed empty-stop responses may be repaired by the managed shell below; only
|
|
@@ -644,14 +657,24 @@ function managedAssistantEventSnapshot(event: AssistantMessageEvent, message: As
|
|
|
644
657
|
return contentIndex as number;
|
|
645
658
|
};
|
|
646
659
|
if (type === "start") return { type, partial: message };
|
|
647
|
-
if (
|
|
660
|
+
if (
|
|
661
|
+
type === "text_start" ||
|
|
662
|
+
type === "thinking_start" ||
|
|
663
|
+
type === "reasoning_summary_start" ||
|
|
664
|
+
type === "toolcall_start"
|
|
665
|
+
)
|
|
648
666
|
return { type, contentIndex: indexed(), partial: message };
|
|
649
|
-
if (
|
|
667
|
+
if (
|
|
668
|
+
type === "text_delta" ||
|
|
669
|
+
type === "thinking_delta" ||
|
|
670
|
+
type === "reasoning_summary_delta" ||
|
|
671
|
+
type === "toolcall_delta"
|
|
672
|
+
) {
|
|
650
673
|
const delta = managedProperty(snapshot, "delta");
|
|
651
674
|
if (typeof delta !== "string") throw new ManagedAttemptSnapshotError();
|
|
652
675
|
return { type, contentIndex: indexed(), delta, partial: message };
|
|
653
676
|
}
|
|
654
|
-
if (type === "text_end" || type === "thinking_end") {
|
|
677
|
+
if (type === "text_end" || type === "thinking_end" || type === "reasoning_summary_end") {
|
|
655
678
|
const content = managedProperty(snapshot, "content");
|
|
656
679
|
if (typeof content !== "string") throw new ManagedAttemptSnapshotError();
|
|
657
680
|
return { type, contentIndex: indexed(), content, partial: message };
|
|
@@ -1237,6 +1260,21 @@ async function runLoopBody(
|
|
|
1237
1260
|
// Fires at most one repaired resend per run for the poisoned-history
|
|
1238
1261
|
// `invalid_prompt` circuit breaker below.
|
|
1239
1262
|
let invalidPromptRepairAttempted = false;
|
|
1263
|
+
let previousMalformedToolSignatures = new Set<string>();
|
|
1264
|
+
const recoveryState: {
|
|
1265
|
+
pending: boolean;
|
|
1266
|
+
inserted: boolean;
|
|
1267
|
+
syntheticMessage?: UserMessage;
|
|
1268
|
+
} = { pending: false, inserted: false };
|
|
1269
|
+
let malformedToolRecoveryAttempted = false;
|
|
1270
|
+
// Deterministic terminal circuit breaker for argument-validation loops.
|
|
1271
|
+
//
|
|
1272
|
+
// Counts CONSECUTIVE turns whose tool calls were all malformed, regardless of
|
|
1273
|
+
// whether the arguments repeat. Signature-based "repeated" detection alone is
|
|
1274
|
+
// not a bound: a model that rotates invalid argument shapes never trips it, so
|
|
1275
|
+
// the loop could run forever. Any turn that produces a non-malformed batch
|
|
1276
|
+
// resets the counter, so healthy runs are unaffected.
|
|
1277
|
+
let consecutiveMalformedTurns = 0;
|
|
1240
1278
|
|
|
1241
1279
|
// Outer loop: continues when queued follow-up messages arrive after agent would stop
|
|
1242
1280
|
while (true) {
|
|
@@ -1321,6 +1359,15 @@ async function runLoopBody(
|
|
|
1321
1359
|
attemptTransaction.stageAssistantMessageEvent(partial, event),
|
|
1322
1360
|
}
|
|
1323
1361
|
: config;
|
|
1362
|
+
if (recoveryState.pending && !recoveryState.inserted) {
|
|
1363
|
+
recoveryState.syntheticMessage = {
|
|
1364
|
+
role: "user",
|
|
1365
|
+
content: repeatedToolFailureRecoveryPrompt,
|
|
1366
|
+
synthetic: true,
|
|
1367
|
+
timestamp: Date.now(),
|
|
1368
|
+
};
|
|
1369
|
+
recoveryState.inserted = true;
|
|
1370
|
+
}
|
|
1324
1371
|
message = await streamAssistantResponse(
|
|
1325
1372
|
currentContext,
|
|
1326
1373
|
attemptConfig,
|
|
@@ -1331,6 +1378,9 @@ async function runLoopBody(
|
|
|
1331
1378
|
stepCounter,
|
|
1332
1379
|
streamFn,
|
|
1333
1380
|
harmonyRetryAttempt,
|
|
1381
|
+
recoveryState.pending && recoveryState.syntheticMessage
|
|
1382
|
+
? { syntheticMessage: recoveryState.syntheticMessage }
|
|
1383
|
+
: undefined,
|
|
1334
1384
|
);
|
|
1335
1385
|
const detection = detectHarmonyLeakInAssistantMessage(message);
|
|
1336
1386
|
if (detection && shouldMitigateHarmonyLeak(config.model, detection)) {
|
|
@@ -1483,6 +1533,7 @@ async function runLoopBody(
|
|
|
1483
1533
|
if (config.fallbackManaged && message.stopReason !== "error" && message.stopReason !== "aborted") {
|
|
1484
1534
|
await config.onManagedAttemptAccepted?.();
|
|
1485
1535
|
}
|
|
1536
|
+
const wasRecoveryAttempt = recoveryState.pending;
|
|
1486
1537
|
|
|
1487
1538
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
1488
1539
|
// Create placeholder tool results for any tool calls in the aborted message
|
|
@@ -1517,26 +1568,67 @@ async function runLoopBody(
|
|
|
1517
1568
|
hasMoreToolCalls = toolCalls.length > 0;
|
|
1518
1569
|
|
|
1519
1570
|
const toolResults: ToolResultMessage[] = [];
|
|
1571
|
+
let repeatedMalformedToolCall = false;
|
|
1520
1572
|
if (hasMoreToolCalls) {
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1573
|
+
if (wasRecoveryAttempt) {
|
|
1574
|
+
for (const toolCall of toolCalls) {
|
|
1575
|
+
const result = createAbortedToolResult(
|
|
1576
|
+
toolCall,
|
|
1577
|
+
stream,
|
|
1578
|
+
"error",
|
|
1579
|
+
"Tool calls are disabled during repeated malformed tool-call recovery.",
|
|
1580
|
+
);
|
|
1581
|
+
currentContext.messages.push(result);
|
|
1582
|
+
newMessages.push(result);
|
|
1583
|
+
toolResults.push(result);
|
|
1584
|
+
recordSkippedTool(telemetry, {
|
|
1585
|
+
toolCallId: toolCall.id,
|
|
1586
|
+
toolName: toolCall.name,
|
|
1587
|
+
status: "skipped",
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
} else {
|
|
1591
|
+
const executionResult = await executeToolCalls(
|
|
1592
|
+
currentContext,
|
|
1593
|
+
message,
|
|
1594
|
+
loopSignal,
|
|
1595
|
+
stream,
|
|
1596
|
+
config,
|
|
1597
|
+
telemetry,
|
|
1598
|
+
invokeAgentSpan,
|
|
1599
|
+
);
|
|
1530
1600
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1601
|
+
toolResults.push(...executionResult.toolResults);
|
|
1602
|
+
steeringMessagesFromExecution = executionResult.steeringMessages;
|
|
1603
|
+
|
|
1604
|
+
const malformedSignatures = executionResult.malformedToolCallSignatures;
|
|
1605
|
+
const allToolCallsMalformed =
|
|
1606
|
+
toolResults.length > 0 && malformedSignatures.length === toolResults.length;
|
|
1607
|
+
if (allToolCallsMalformed) {
|
|
1608
|
+
consecutiveMalformedTurns += 1;
|
|
1609
|
+
const uniqueMalformedSignatures = new Set(malformedSignatures);
|
|
1610
|
+
repeatedMalformedToolCall =
|
|
1611
|
+
uniqueMalformedSignatures.size < malformedSignatures.length ||
|
|
1612
|
+
[...uniqueMalformedSignatures].some(signature => previousMalformedToolSignatures.has(signature));
|
|
1613
|
+
previousMalformedToolSignatures = uniqueMalformedSignatures;
|
|
1614
|
+
} else {
|
|
1615
|
+
consecutiveMalformedTurns = 0;
|
|
1616
|
+
previousMalformedToolSignatures = new Set();
|
|
1617
|
+
}
|
|
1533
1618
|
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1619
|
+
for (const result of toolResults) {
|
|
1620
|
+
currentContext.messages.push(result);
|
|
1621
|
+
newMessages.push(result);
|
|
1622
|
+
}
|
|
1537
1623
|
}
|
|
1538
1624
|
}
|
|
1539
1625
|
|
|
1626
|
+
if (wasRecoveryAttempt) {
|
|
1627
|
+
recoveryState.pending = false;
|
|
1628
|
+
recoveryState.inserted = false;
|
|
1629
|
+
recoveryState.syntheticMessage = undefined;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1540
1632
|
stream.push({ type: "turn_end", message, toolResults });
|
|
1541
1633
|
|
|
1542
1634
|
if (steeringMessagesFromExecution && steeringMessagesFromExecution.length > 0) {
|
|
@@ -1550,6 +1642,27 @@ async function runLoopBody(
|
|
|
1550
1642
|
stream.end(newMessages);
|
|
1551
1643
|
return;
|
|
1552
1644
|
}
|
|
1645
|
+
if (repeatedMalformedToolCall && !malformedToolRecoveryAttempted) {
|
|
1646
|
+
recoveryState.pending = true;
|
|
1647
|
+
recoveryState.inserted = false;
|
|
1648
|
+
recoveryState.syntheticMessage = undefined;
|
|
1649
|
+
malformedToolRecoveryAttempted = true;
|
|
1650
|
+
} else if (consecutiveMalformedTurns >= MAX_CONSECUTIVE_MALFORMED_TURNS) {
|
|
1651
|
+
// Deterministic terminal circuit breaker. The one-shot recovery turn
|
|
1652
|
+
// above already had its chance; if the model is still emitting only
|
|
1653
|
+
// malformed tool calls after it, the run cannot make progress and must
|
|
1654
|
+
// stop rather than burn the provider budget. Terminates on consecutive
|
|
1655
|
+
// count, not argument signatures, so rotating invalid shapes are bounded
|
|
1656
|
+
// too.
|
|
1657
|
+
message.stopReason = "error";
|
|
1658
|
+
const breakerMessage = `Stopping after ${consecutiveMalformedTurns} consecutive turns of malformed tool calls; the model did not produce a usable tool call or answer.`;
|
|
1659
|
+
message.errorMessage = message.errorMessage
|
|
1660
|
+
? `${message.errorMessage} | ${breakerMessage}`
|
|
1661
|
+
: breakerMessage;
|
|
1662
|
+
stream.push(buildAgentEndEvent(newMessages, telemetry, stepCounter.count));
|
|
1663
|
+
stream.end(newMessages);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1553
1666
|
}
|
|
1554
1667
|
|
|
1555
1668
|
// Agent would stop here. Check for follow-up messages.
|
|
@@ -1605,6 +1718,7 @@ async function streamAssistantResponse(
|
|
|
1605
1718
|
stepCounter: StepCounter,
|
|
1606
1719
|
streamFn?: StreamFn,
|
|
1607
1720
|
harmonyRetryAttempt = 0,
|
|
1721
|
+
recoveryMode?: { syntheticMessage: UserMessage },
|
|
1608
1722
|
): Promise<AssistantMessage> {
|
|
1609
1723
|
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
|
1610
1724
|
let messages = context.messages;
|
|
@@ -1629,7 +1743,24 @@ async function streamAssistantResponse(
|
|
|
1629
1743
|
tools: normalizeTools(context.tools, !!config.intentTracing),
|
|
1630
1744
|
};
|
|
1631
1745
|
}
|
|
1632
|
-
|
|
1746
|
+
if (recoveryMode) {
|
|
1747
|
+
if (config.appendOnlyContext) {
|
|
1748
|
+
const syntheticMessages = normalizeMessagesForProvider(
|
|
1749
|
+
await config.convertToLlm([recoveryMode.syntheticMessage]),
|
|
1750
|
+
config.model,
|
|
1751
|
+
);
|
|
1752
|
+
llmContext = { ...llmContext, messages: [...llmContext.messages, ...syntheticMessages], tools: [] };
|
|
1753
|
+
} else {
|
|
1754
|
+
llmContext = {
|
|
1755
|
+
...llmContext,
|
|
1756
|
+
messages: normalizeMessagesForProvider(
|
|
1757
|
+
await config.convertToLlm([...messages, recoveryMode.syntheticMessage]),
|
|
1758
|
+
config.model,
|
|
1759
|
+
),
|
|
1760
|
+
tools: [],
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1633
1764
|
const streamFunction = streamFn || streamSimple;
|
|
1634
1765
|
|
|
1635
1766
|
// Resolve API key (important for expiring tokens) — do this before resolving
|
|
@@ -1644,7 +1775,7 @@ async function streamAssistantResponse(
|
|
|
1644
1775
|
|
|
1645
1776
|
const resolvedMetadata = config.metadataResolver ? config.metadataResolver(config.model.provider) : config.metadata;
|
|
1646
1777
|
|
|
1647
|
-
const dynamicToolChoice = config.getToolChoice?.();
|
|
1778
|
+
const dynamicToolChoice = recoveryMode ? undefined : config.getToolChoice?.();
|
|
1648
1779
|
const dynamicReasoning = config.getReasoning?.();
|
|
1649
1780
|
const harmonyMitigationEnabled = isHarmonyLeakMitigationTarget(config.model);
|
|
1650
1781
|
const harmonyAbortController = harmonyMitigationEnabled ? new AbortController() : undefined;
|
|
@@ -1655,7 +1786,7 @@ async function streamAssistantResponse(
|
|
|
1655
1786
|
: signal;
|
|
1656
1787
|
const effectiveTemperature =
|
|
1657
1788
|
harmonyRetryAttempt > 0 && config.temperature !== undefined ? config.temperature + 0.05 : config.temperature;
|
|
1658
|
-
const effectiveToolChoice = dynamicToolChoice ?? config.toolChoice;
|
|
1789
|
+
const effectiveToolChoice = recoveryMode ? "none" : (dynamicToolChoice ?? config.toolChoice);
|
|
1659
1790
|
const effectiveReasoning = dynamicReasoning ?? config.reasoning;
|
|
1660
1791
|
|
|
1661
1792
|
const chatStepNumber = stepCounter.count;
|
|
@@ -1900,7 +2031,11 @@ async function executeToolCalls(
|
|
|
1900
2031
|
config: AgentLoopConfig,
|
|
1901
2032
|
telemetry: AgentTelemetry | undefined,
|
|
1902
2033
|
invokeAgentSpan: Span | undefined,
|
|
1903
|
-
): Promise<{
|
|
2034
|
+
): Promise<{
|
|
2035
|
+
toolResults: ToolResultMessage[];
|
|
2036
|
+
steeringMessages?: AgentMessage[];
|
|
2037
|
+
malformedToolCallSignatures: string[];
|
|
2038
|
+
}> {
|
|
1904
2039
|
const tools = currentContext.tools;
|
|
1905
2040
|
const {
|
|
1906
2041
|
getSteeringMessages,
|
|
@@ -1941,6 +2076,7 @@ async function executeToolCalls(
|
|
|
1941
2076
|
skipped: false,
|
|
1942
2077
|
toolResultMessage: undefined as ToolResultMessage | undefined,
|
|
1943
2078
|
resultEmitted: false,
|
|
2079
|
+
argumentValidationFailed: false,
|
|
1944
2080
|
}));
|
|
1945
2081
|
|
|
1946
2082
|
const checkSteering = async (): Promise<void> => {
|
|
@@ -2060,6 +2196,7 @@ async function executeToolCalls(
|
|
|
2060
2196
|
await runInActiveSpan(toolSpan, async () => {
|
|
2061
2197
|
try {
|
|
2062
2198
|
if (toolCall.incompleteArguments) {
|
|
2199
|
+
record.argumentValidationFailed = true;
|
|
2063
2200
|
// The provider flagged this call's argument JSON as truncated
|
|
2064
2201
|
// (the model hit its output-token limit mid-call). Executing the
|
|
2065
2202
|
// best-effort partial parse would run the tool on wrong input, so
|
|
@@ -2094,6 +2231,7 @@ async function executeToolCalls(
|
|
|
2094
2231
|
if (tool.lenientArgValidation) {
|
|
2095
2232
|
effectiveArgs = argsForExecution;
|
|
2096
2233
|
} else {
|
|
2234
|
+
record.argumentValidationFailed = true;
|
|
2097
2235
|
throw validationError;
|
|
2098
2236
|
}
|
|
2099
2237
|
}
|
|
@@ -2245,7 +2383,12 @@ async function executeToolCalls(
|
|
|
2245
2383
|
}
|
|
2246
2384
|
}
|
|
2247
2385
|
|
|
2248
|
-
|
|
2386
|
+
const malformedToolCallSignatures = records.flatMap(record =>
|
|
2387
|
+
record.argumentValidationFailed && record.toolResultMessage?.isError
|
|
2388
|
+
? [`${record.toolCall.name}:${JSON.stringify(record.toolCall.arguments)}`]
|
|
2389
|
+
: [],
|
|
2390
|
+
);
|
|
2391
|
+
return { toolResults: emittedToolResults, steeringMessages, malformedToolCallSignatures };
|
|
2249
2392
|
}
|
|
2250
2393
|
|
|
2251
2394
|
/**
|
package/src/agent.ts
CHANGED
|
@@ -1196,6 +1196,9 @@ export class Agent {
|
|
|
1196
1196
|
*/
|
|
1197
1197
|
requestRunTerminal(logicalRunId: ManagedLogicalRunId, request: RunTerminalRequest): boolean {
|
|
1198
1198
|
if (this.#terminalizedLogicalRunIds.has(logicalRunId)) return false;
|
|
1199
|
+
if (this.#managedLogicalRunOwner === logicalRunId) {
|
|
1200
|
+
this.#managedLogicalRunOwner = undefined;
|
|
1201
|
+
}
|
|
1199
1202
|
this.#finalizeRun(
|
|
1200
1203
|
logicalRunId,
|
|
1201
1204
|
{
|
package/src/compaction/openai.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
neutralizeResponsesInputControlTokens,
|
|
29
29
|
normalizeResponsesToolCallId,
|
|
30
30
|
} from "@sayknow-cli/ai/utils";
|
|
31
|
-
import { $
|
|
31
|
+
import { $credentialEnv, logger } from "@sayknow-cli/utils";
|
|
32
32
|
|
|
33
33
|
const OPENAI_DEFAULT_BASE_URL = "https://api.openai.com/v1";
|
|
34
34
|
|
|
@@ -81,7 +81,8 @@ function resolveOpenAiCompactEndpoint(model: Model, authCredentialType?: "api_ke
|
|
|
81
81
|
return resolveOpenAiCodexCompactEndpoint(model.baseUrl);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
// Trusted sources only: the compaction endpoint carries the OpenAI credential.
|
|
85
|
+
const envBaseUrl = $credentialEnv("OPENAI_BASE_URL");
|
|
85
86
|
const configuredBaseUrl = model.baseUrl?.trim();
|
|
86
87
|
const rawBase =
|
|
87
88
|
authCredentialType === "oauth"
|
|
@@ -94,6 +95,11 @@ function resolveOpenAiCompactEndpoint(model: Model, authCredentialType?: "api_ke
|
|
|
94
95
|
return `${normalizedBase}/v1/responses/compact`;
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/** Test seam: the compaction endpoint as resolved from trusted env. */
|
|
99
|
+
export function resolveOpenAiCompactEndpointForTest(model: Model, authCredentialType?: "api_key" | "oauth"): string {
|
|
100
|
+
return resolveOpenAiCompactEndpoint(model, authCredentialType);
|
|
101
|
+
}
|
|
102
|
+
|
|
97
103
|
function resolveOpenAiCodexCompactEndpoint(baseUrl: string | undefined): string {
|
|
98
104
|
const rawBase = baseUrl && baseUrl.length > 0 ? baseUrl : CODEX_BASE_URL;
|
|
99
105
|
const normalizedBase = rawBase.endsWith("/") ? rawBase.slice(0, -1) : rawBase;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
The immediately preceding tool calls failed because their arguments were malformed. Do not call any tools. Answer the original user request now using the conversation and any successful tool results already available. If the evidence is incomplete, state that limitation instead of returning an empty response.
|