@oh-my-pi/pi-agent-core 15.12.4 → 15.13.1
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 +61 -16
- package/dist/types/agent.d.ts +1 -1
- package/package.json +6 -6
- package/src/agent-loop.ts +181 -2
- package/src/agent.ts +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.13.1] - 2026-06-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added repetition-loop detection to the streaming agent loop for Gemini-family providers. A runaway run of a repeated text or thinking unit is detected mid-stream from a bounded rolling tail (O(1) per delta), the provider request is aborted, the repeated tail is collapsed to a single representative copy, and the turn ends gracefully with an `error` stop reason. Legitimate all-numeric/whitespace/punctuation runs (hexdumps, zero-fills, numeric tables) are not misclassified as loops ([#2549](https://github.com/can1357/oh-my-pi/pull/2549) by [@usr-bin-roygbiv](https://github.com/usr-bin-roygbiv)).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed repetition loop handling to collapse repeated `thinking` blocks to a single representative copy when a loop is detected
|
|
14
|
+
- Fixed repetition-loop detection to ignore repeats that contain only digits, whitespace, or punctuation so legitimate numeric outputs no longer stop with a repetition-loop error
|
|
15
|
+
- Fixed false-positive repetition-loop checks across `text` and `thinking` stream boundaries by tracking loop detection per block type
|
|
16
|
+
|
|
17
|
+
## [15.12.6] - 2026-06-14
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Fixed dynamic forced tool choices from queue hooks being filtered against the active per-turn tool set before provider dispatch. ([#1701](https://github.com/can1357/oh-my-pi/issues/1701))
|
|
22
|
+
|
|
5
23
|
## [15.12.4] - 2026-06-13
|
|
24
|
+
|
|
6
25
|
### Fixed
|
|
7
26
|
|
|
8
27
|
- Fixed remote compaction input trimming to use unlimited context when `model.contextWindow` is unset
|
|
9
28
|
|
|
10
29
|
## [15.12.1] - 2026-06-12
|
|
30
|
+
|
|
11
31
|
### Breaking Changes
|
|
12
32
|
|
|
13
33
|
- Changed `pruneSupersededToolResults` to allow `supersedeKey` to be omitted so useless-result pruning can run without read-style supersede grouping
|
|
@@ -23,6 +43,7 @@
|
|
|
23
43
|
- Changed `pruneSupersededToolResults` to allow omitted `supersedeKey` when `pruneUseless` is enabled, so useless-result pruning can run without read-style supersede grouping
|
|
24
44
|
|
|
25
45
|
## [15.11.4] - 2026-06-12
|
|
46
|
+
|
|
26
47
|
### Added
|
|
27
48
|
|
|
28
49
|
- Added `hasSteeringMessages` to `AgentLoopConfig` (wired by `Agent` to its steering queue): a peek used by the immediate-interrupt poll during tool execution, so the loop can detect queued steering without dequeuing and the queue keeps owning its messages until the injection boundary
|
|
@@ -48,7 +69,9 @@
|
|
|
48
69
|
### Fixed
|
|
49
70
|
|
|
50
71
|
- Fixed whitespace-only error tool results so Anthropic requests no longer 400 with `tool_result: content cannot be empty if is_error is true` and wedge the session on every subsequent turn
|
|
72
|
+
|
|
51
73
|
## [15.11.0] - 2026-06-10
|
|
74
|
+
|
|
52
75
|
### Breaking Changes
|
|
53
76
|
|
|
54
77
|
- Removed `compaction/index.ts` re-export of snapcompact helpers, so snapcompact utilities are no longer available from the agent compaction barrel and should be imported from `@oh-my-pi/snapcompact`
|
|
@@ -229,10 +252,6 @@
|
|
|
229
252
|
|
|
230
253
|
- Fixed compaction summarizer throws losing the provider's HTTP status. `generateSummary`, `generateHandoff`, `generateShortSummary`, and `generateTurnPrefixSummary` now route their `stopReason === "error"` throws through a `createSummarizationError` helper that copies `AssistantMessage.errorStatus` onto the thrown `Error` as `.status`, letting downstream consumers (e.g. `AgentSession.#isCompactionAuthFailure` in `@oh-my-pi/pi-coding-agent`) branch on real provider 401/403s without regex-scraping the message body.
|
|
231
254
|
|
|
232
|
-
### Changed
|
|
233
|
-
|
|
234
|
-
- Changed `Agent.appendMessage`, `popMessage`, `clearMessages`, and `reset` to mutate `state.messages` and `state.pendingToolCalls` in place instead of allocating a fresh array/Set on every transition. Subscribers that capture `state.messages` by reference now observe updates without needing to re-read `state` after each event. The public type signature is unchanged (always `AgentMessage[]` / `Set<string>`).
|
|
235
|
-
|
|
236
255
|
## [15.5.0] - 2026-05-26
|
|
237
256
|
|
|
238
257
|
### Added
|
|
@@ -646,7 +665,7 @@
|
|
|
646
665
|
|
|
647
666
|
### Changed
|
|
648
667
|
|
|
649
|
-
- Switched from local `@oh-my-pi/pi-ai` to upstream `@
|
|
668
|
+
- Switched from local `@oh-my-pi/pi-ai` to upstream `@mariozechner/pi-ai` package
|
|
650
669
|
|
|
651
670
|
### Added
|
|
652
671
|
|
|
@@ -699,39 +718,65 @@
|
|
|
699
718
|
|
|
700
719
|
Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mono](https://github.com/badlogic/pi-mono).
|
|
701
720
|
|
|
721
|
+
## [0.38.0] - 2026-01-08
|
|
722
|
+
|
|
723
|
+
### Added
|
|
724
|
+
|
|
725
|
+
- `thinkingBudgets` option on `Agent` and `AgentOptions` to customize token budgets per thinking level ([#529](https://github.com/badlogic/pi-mono/pull/529) by [@melihmucuk](https://github.com/melihmucuk))
|
|
726
|
+
|
|
727
|
+
## [0.37.3] - 2026-01-06
|
|
728
|
+
|
|
729
|
+
### Added
|
|
730
|
+
|
|
731
|
+
- `sessionId` option on `Agent` to forward session identifiers to LLM providers for session-based caching.
|
|
732
|
+
|
|
733
|
+
## [0.37.0] - 2026-01-05
|
|
734
|
+
|
|
735
|
+
### Fixed
|
|
736
|
+
|
|
737
|
+
- `minimal` thinking level now maps to `minimal` reasoning effort instead of being treated as `low`.
|
|
738
|
+
|
|
739
|
+
## [0.32.0] - 2026-01-03
|
|
740
|
+
|
|
741
|
+
### Breaking Changes
|
|
742
|
+
|
|
743
|
+
- **Queue API replaced with steer/followUp**: The `queueMessage()` method has been split into two methods with different delivery semantics ([#403](https://github.com/badlogic/pi-mono/issues/403)):
|
|
744
|
+
- `steer(msg)`: Interrupts the agent mid-run. Delivered after current tool execution, skips remaining tools.
|
|
745
|
+
- `followUp(msg)`: Waits until the agent finishes. Delivered only when there are no more tool calls or steering messages.
|
|
746
|
+
- **Queue mode renamed**: `queueMode` option renamed to `steeringMode`. Added new `followUpMode` option. Both control whether messages are delivered one-at-a-time or all at once.
|
|
747
|
+
- **AgentLoopConfig callbacks renamed**: `getQueuedMessages` split into `getSteeringMessages` and `getFollowUpMessages`.
|
|
748
|
+
- **Agent methods renamed**:
|
|
749
|
+
- `queueMessage()` → `steer()` and `followUp()`
|
|
750
|
+
- `clearMessageQueue()` → `clearSteeringQueue()`, `clearFollowUpQueue()`, `clearAllQueues()`
|
|
751
|
+
- `setQueueMode()`/`getQueueMode()` → `setSteeringMode()`/`getSteeringMode()` and `setFollowUpMode()`/`getFollowUpMode()`
|
|
752
|
+
|
|
753
|
+
### Fixed
|
|
754
|
+
|
|
755
|
+
- `prompt()` and `continue()` now throw if called while the agent is already streaming, preventing race conditions and corrupted state. Use `steer()` or `followUp()` to queue messages during streaming, or `await` the previous call.
|
|
756
|
+
|
|
702
757
|
## [0.31.0] - 2026-01-02
|
|
703
758
|
|
|
704
759
|
### Breaking Changes
|
|
705
760
|
|
|
706
761
|
- **Transport abstraction removed**: `ProviderTransport`, `AppTransport`, and `AgentTransport` interface have been removed. Use the `streamFn` option directly for custom streaming implementations.
|
|
707
|
-
|
|
708
762
|
- **Agent options renamed**:
|
|
709
763
|
- `transport` → removed (use `streamFn` instead)
|
|
710
764
|
- `messageTransformer` → `convertToLlm`
|
|
711
765
|
- `preprocessor` → `transformContext`
|
|
712
|
-
|
|
713
766
|
- **`AppMessage` renamed to `AgentMessage`**: All references to `AppMessage` have been renamed to `AgentMessage` for consistency.
|
|
714
|
-
|
|
715
767
|
- **`CustomMessages` renamed to `CustomAgentMessages`**: The declaration merging interface has been renamed.
|
|
716
|
-
|
|
717
768
|
- **`UserMessageWithAttachments` and `Attachment` types removed**: Attachment handling is now the responsibility of the `convertToLlm` function.
|
|
718
|
-
|
|
719
769
|
- **Agent loop moved from `@oh-my-pi/pi-ai`**: The `agentLoop`, `agentLoopContinue`, and related types have moved to this package. Import from `@oh-my-pi/pi-agent` instead.
|
|
720
770
|
|
|
721
771
|
### Added
|
|
722
772
|
|
|
723
773
|
- `streamFn` option on `Agent` for custom stream implementations. Default uses `streamSimple` from pi-ai.
|
|
724
|
-
|
|
725
774
|
- `streamProxy()` utility function for browser apps that need to proxy LLM calls through a backend server. Replaces the removed `AppTransport`.
|
|
726
|
-
|
|
727
775
|
- `getApiKey` option for dynamic API key resolution (useful for expiring OAuth tokens like GitHub Copilot).
|
|
728
|
-
|
|
729
776
|
- `agentLoop()` and `agentLoopContinue()` low-level functions for running the agent loop without the `Agent` class wrapper.
|
|
730
|
-
|
|
731
777
|
- New exported types: `AgentLoopConfig`, `AgentContext`, `AgentTool`, `AgentToolResult`, `AgentToolUpdateCallback`, `StreamFn`.
|
|
732
778
|
|
|
733
779
|
### Changed
|
|
734
780
|
|
|
735
781
|
- `Agent` constructor now has all options optional (empty options use defaults).
|
|
736
|
-
|
|
737
|
-
- `queueMessage()` is now synchronous (no longer returns a Promise).
|
|
782
|
+
- `queueMessage()` is now synchronous (no longer returns a Promise).
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -296,7 +296,7 @@ export declare class Agent {
|
|
|
296
296
|
*/
|
|
297
297
|
setAsideMessageProvider(fn: (() => AsideMessage[] | Promise<AsideMessage[]>) | undefined): void;
|
|
298
298
|
emitExternalEvent(event: AgentEvent): void;
|
|
299
|
-
setSystemPrompt(v: string[]): void;
|
|
299
|
+
setSystemPrompt(v: string[] | string): void;
|
|
300
300
|
setModel(m: Model): void;
|
|
301
301
|
setThinkingLevel(l: Effort | undefined): void;
|
|
302
302
|
setDisableReasoning(disabled: boolean): void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-agent-core",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.13.1",
|
|
5
5
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"fmt": "biome format --write ."
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@oh-my-pi/pi-ai": "15.
|
|
39
|
-
"@oh-my-pi/pi-catalog": "15.
|
|
40
|
-
"@oh-my-pi/pi-natives": "15.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.
|
|
42
|
-
"@oh-my-pi/snapcompact": "15.
|
|
38
|
+
"@oh-my-pi/pi-ai": "15.13.1",
|
|
39
|
+
"@oh-my-pi/pi-catalog": "15.13.1",
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.13.1",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.13.1",
|
|
42
|
+
"@oh-my-pi/snapcompact": "15.13.1",
|
|
43
43
|
"@opentelemetry/api": "^1.9.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
package/src/agent-loop.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
validateToolArguments,
|
|
16
16
|
zodToWireSchema,
|
|
17
17
|
} from "@oh-my-pi/pi-ai";
|
|
18
|
-
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
18
|
+
import { logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
19
19
|
import {
|
|
20
20
|
createHarmonyAuditEvent,
|
|
21
21
|
detectHarmonyLeakInAssistantMessage,
|
|
@@ -483,6 +483,7 @@ function injectIntentIntoSchema(schema: unknown, mode: "require" | "optional" =
|
|
|
483
483
|
properties: {
|
|
484
484
|
[INTENT_FIELD]: {
|
|
485
485
|
type: "string",
|
|
486
|
+
description: "Concise intent in present participle form (2-6 words) strictly on a single line, no newlines",
|
|
486
487
|
},
|
|
487
488
|
...properties,
|
|
488
489
|
},
|
|
@@ -708,6 +709,7 @@ async function runLoopBody(
|
|
|
708
709
|
});
|
|
709
710
|
}
|
|
710
711
|
stream.push({ type: "turn_end", message, toolResults });
|
|
712
|
+
|
|
711
713
|
stream.push(buildAgentEndEvent(newMessages, telemetry, stepCounter.count));
|
|
712
714
|
stream.end(newMessages);
|
|
713
715
|
return;
|
|
@@ -917,6 +919,10 @@ async function streamAssistantResponse(
|
|
|
917
919
|
? AbortSignal.any([signal, harmonyAbortController.signal])
|
|
918
920
|
: harmonyAbortController.signal
|
|
919
921
|
: signal;
|
|
922
|
+
const repetitionAbortController = new AbortController();
|
|
923
|
+
const finalRequestSignal = requestSignal
|
|
924
|
+
? AbortSignal.any([requestSignal, repetitionAbortController.signal])
|
|
925
|
+
: repetitionAbortController.signal;
|
|
920
926
|
const effectiveTemperature =
|
|
921
927
|
harmonyRetryAttempt > 0 && config.temperature !== undefined ? config.temperature + 0.05 : config.temperature;
|
|
922
928
|
const effectiveToolChoice = dynamicToolChoice ?? config.toolChoice;
|
|
@@ -984,7 +990,7 @@ async function streamAssistantResponse(
|
|
|
984
990
|
reasoning: effectiveReasoning,
|
|
985
991
|
disableReasoning: effectiveDisableReasoning,
|
|
986
992
|
temperature: effectiveTemperature,
|
|
987
|
-
signal:
|
|
993
|
+
signal: finalRequestSignal,
|
|
988
994
|
onResponse: captureOnResponse,
|
|
989
995
|
});
|
|
990
996
|
|
|
@@ -1013,6 +1019,56 @@ async function streamAssistantResponse(
|
|
|
1013
1019
|
return aborted;
|
|
1014
1020
|
};
|
|
1015
1021
|
|
|
1022
|
+
const finishRepetitionStream = async (
|
|
1023
|
+
kind: "text" | "thinking",
|
|
1024
|
+
pattern: string,
|
|
1025
|
+
count: number,
|
|
1026
|
+
): Promise<AssistantMessage> => {
|
|
1027
|
+
repetitionAbortController.abort();
|
|
1028
|
+
try {
|
|
1029
|
+
const cleanup = responseIterator.return?.();
|
|
1030
|
+
if (cleanup) void cleanup.catch(() => {});
|
|
1031
|
+
} catch {
|
|
1032
|
+
// ignore
|
|
1033
|
+
}
|
|
1034
|
+
if (partialMessage) {
|
|
1035
|
+
truncateRepetition(partialMessage, kind, pattern);
|
|
1036
|
+
partialMessage.stopReason = "error";
|
|
1037
|
+
partialMessage.errorMessage = `Repetition loop detected: assistant repeated "${pattern.trim()}" ${count} times consecutively.`;
|
|
1038
|
+
}
|
|
1039
|
+
const finalMsg = snapshotAssistantMessage(
|
|
1040
|
+
partialMessage ?? {
|
|
1041
|
+
role: "assistant",
|
|
1042
|
+
content: [],
|
|
1043
|
+
api: config.model.api,
|
|
1044
|
+
provider: config.model.provider,
|
|
1045
|
+
model: config.model.id,
|
|
1046
|
+
usage: {
|
|
1047
|
+
input: 0,
|
|
1048
|
+
output: 0,
|
|
1049
|
+
cacheRead: 0,
|
|
1050
|
+
cacheWrite: 0,
|
|
1051
|
+
totalTokens: 0,
|
|
1052
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1053
|
+
},
|
|
1054
|
+
stopReason: "error",
|
|
1055
|
+
errorMessage: `Repetition loop detected.`,
|
|
1056
|
+
timestamp: Date.now(),
|
|
1057
|
+
},
|
|
1058
|
+
);
|
|
1059
|
+
if (addedPartial) {
|
|
1060
|
+
context.messages[context.messages.length - 1] = finalMsg;
|
|
1061
|
+
} else {
|
|
1062
|
+
context.messages.push(finalMsg);
|
|
1063
|
+
}
|
|
1064
|
+
if (!addedPartial) {
|
|
1065
|
+
stream.push({ type: "message_start", message: snapshotAssistantMessage(finalMsg) });
|
|
1066
|
+
}
|
|
1067
|
+
stream.push({ type: "message_end", message: snapshotAssistantMessage(finalMsg) });
|
|
1068
|
+
await finishChat(finalMsg);
|
|
1069
|
+
return finalMsg;
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1016
1072
|
// Set up a single abort race: register the abort listener once for the whole
|
|
1017
1073
|
// stream and reuse the same race promise for every iterator.next() instead of
|
|
1018
1074
|
// allocating Promise.withResolvers and add/removeEventListener per event.
|
|
@@ -1029,6 +1085,14 @@ async function streamAssistantResponse(
|
|
|
1029
1085
|
detachAbortListener = () => requestSignal.removeEventListener("abort", onAbort);
|
|
1030
1086
|
}
|
|
1031
1087
|
|
|
1088
|
+
// Rolling tail of streamed text/thinking used for repetition-loop detection.
|
|
1089
|
+
// Bounded to REPETITION_WINDOW chars and reset when the active block kind
|
|
1090
|
+
// switches (text <-> thinking) so detection stays O(1) per delta and never
|
|
1091
|
+
// miscounts a repeated unit across a thinking/answer boundary.
|
|
1092
|
+
let repetitionTail = "";
|
|
1093
|
+
let repetitionKind: "text" | "thinking" | undefined;
|
|
1094
|
+
const isGeminiModel = config.model.provider.includes("google") || config.model.provider.includes("gemini");
|
|
1095
|
+
|
|
1032
1096
|
try {
|
|
1033
1097
|
while (true) {
|
|
1034
1098
|
let next: IteratorResult<AssistantMessageEvent>;
|
|
@@ -1113,6 +1177,27 @@ async function streamAssistantResponse(
|
|
|
1113
1177
|
assistantMessageEvent: snapshotAssistantMessageEvent(event),
|
|
1114
1178
|
message: snapshotAssistantMessage(partialMessage),
|
|
1115
1179
|
});
|
|
1180
|
+
|
|
1181
|
+
if (isGeminiModel && (event.type === "text_delta" || event.type === "thinking_delta")) {
|
|
1182
|
+
const kind = event.type === "text_delta" ? "text" : "thinking";
|
|
1183
|
+
if (repetitionKind !== kind) {
|
|
1184
|
+
repetitionKind = kind;
|
|
1185
|
+
repetitionTail = "";
|
|
1186
|
+
}
|
|
1187
|
+
repetitionTail += event.delta;
|
|
1188
|
+
if (repetitionTail.length > REPETITION_WINDOW) {
|
|
1189
|
+
repetitionTail = repetitionTail.slice(-REPETITION_WINDOW);
|
|
1190
|
+
}
|
|
1191
|
+
const repetition = detectRepetition(repetitionTail);
|
|
1192
|
+
if (repetition) {
|
|
1193
|
+
const [pattern, count] = repetition;
|
|
1194
|
+
logger.warn("Repetition loop detected during assistant stream, aborting.", {
|
|
1195
|
+
pattern,
|
|
1196
|
+
count,
|
|
1197
|
+
});
|
|
1198
|
+
return await finishRepetitionStream(kind, pattern, count);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1116
1201
|
}
|
|
1117
1202
|
break;
|
|
1118
1203
|
}
|
|
@@ -1719,3 +1804,97 @@ function createSkippedToolResult(): AgentToolResult<any> {
|
|
|
1719
1804
|
details: {},
|
|
1720
1805
|
};
|
|
1721
1806
|
}
|
|
1807
|
+
|
|
1808
|
+
const REPETITION_WINDOW = 250;
|
|
1809
|
+
const REPETITION_MIN_REPEATED_CHARS = 180;
|
|
1810
|
+
|
|
1811
|
+
function detectRepetition(text: string): [pattern: string, count: number] | null {
|
|
1812
|
+
if (text.length < REPETITION_MIN_REPEATED_CHARS) return null;
|
|
1813
|
+
|
|
1814
|
+
const windowSize = Math.min(text.length, REPETITION_WINDOW);
|
|
1815
|
+
const searchSpace = text.slice(-windowSize);
|
|
1816
|
+
|
|
1817
|
+
for (let len = 2; len <= 60; len++) {
|
|
1818
|
+
if (searchSpace.length < len * 4) continue;
|
|
1819
|
+
|
|
1820
|
+
const pattern = searchSpace.slice(-len);
|
|
1821
|
+
// Only treat a repeated unit as a pathological loop when it carries real
|
|
1822
|
+
// linguistic content (a letter or a pictographic emoji). Runs made purely of
|
|
1823
|
+
// digits, whitespace or punctuation are legitimate in tabular / hex / numeric
|
|
1824
|
+
// output (e.g. "00 00 00", "0, 0, 0", "| -- | -- |") and must not trip.
|
|
1825
|
+
if (!/[\p{L}\p{Extended_Pictographic}]/u.test(pattern)) continue;
|
|
1826
|
+
|
|
1827
|
+
let count = 0;
|
|
1828
|
+
let pos = searchSpace.length;
|
|
1829
|
+
while (pos >= len) {
|
|
1830
|
+
const chunk = searchSpace.slice(pos - len, pos);
|
|
1831
|
+
if (chunk === pattern) {
|
|
1832
|
+
count++;
|
|
1833
|
+
pos -= len;
|
|
1834
|
+
} else {
|
|
1835
|
+
break;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (count >= 4 && len * count >= REPETITION_MIN_REPEATED_CHARS) {
|
|
1840
|
+
return [pattern, count];
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
return null;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function truncateRepetition(message: AssistantMessage, kind: "text" | "thinking", pattern: string): void {
|
|
1847
|
+
// A repetition loop streams into a single growing block (real providers) or a run
|
|
1848
|
+
// of same-kind blocks (some transports), always at the tail of the message. Gather
|
|
1849
|
+
// that trailing contiguous run and collapse its repeated copies down to one, so the
|
|
1850
|
+
// committed transcript keeps a representative sample instead of the full runaway.
|
|
1851
|
+
const matches = (block: AssistantContentBlock): boolean =>
|
|
1852
|
+
kind === "text" ? block.type === "text" : block.type === "thinking";
|
|
1853
|
+
const readBlock = (block: AssistantContentBlock): string =>
|
|
1854
|
+
block.type === "text" ? block.text : block.type === "thinking" ? block.thinking : "";
|
|
1855
|
+
const clearThinkingReplayAnchors = (block: AssistantContentBlock): void => {
|
|
1856
|
+
if (block.type !== "thinking") return;
|
|
1857
|
+
block.thinkingSignature = undefined;
|
|
1858
|
+
block.itemId = undefined;
|
|
1859
|
+
};
|
|
1860
|
+
const writeBlock = (block: AssistantContentBlock, value: string): void => {
|
|
1861
|
+
if (block.type === "text") {
|
|
1862
|
+
block.text = value;
|
|
1863
|
+
} else if (block.type === "thinking") {
|
|
1864
|
+
block.thinking = value;
|
|
1865
|
+
clearThinkingReplayAnchors(block);
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
const trailing: AssistantContentBlock[] = [];
|
|
1870
|
+
for (let i = message.content.length - 1; i >= 0; i--) {
|
|
1871
|
+
const block = message.content[i];
|
|
1872
|
+
if (!matches(block)) break;
|
|
1873
|
+
trailing.unshift(block);
|
|
1874
|
+
}
|
|
1875
|
+
if (trailing.length === 0) return;
|
|
1876
|
+
if (kind === "thinking") {
|
|
1877
|
+
for (const block of trailing) clearThinkingReplayAnchors(block);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
let joined = "";
|
|
1881
|
+
for (const block of trailing) joined += readBlock(block);
|
|
1882
|
+
|
|
1883
|
+
let kept = joined;
|
|
1884
|
+
while (kept.length >= pattern.length * 2 && kept.slice(kept.length - pattern.length * 2) === pattern + pattern) {
|
|
1885
|
+
kept = kept.slice(0, kept.length - pattern.length);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
let remainingToRemove = joined.length - kept.length;
|
|
1889
|
+
for (let i = trailing.length - 1; i >= 0 && remainingToRemove > 0; i--) {
|
|
1890
|
+
const block = trailing[i];
|
|
1891
|
+
const value = readBlock(block);
|
|
1892
|
+
if (value.length <= remainingToRemove) {
|
|
1893
|
+
remainingToRemove -= value.length;
|
|
1894
|
+
writeBlock(block, "");
|
|
1895
|
+
} else {
|
|
1896
|
+
writeBlock(block, value.slice(0, value.length - remainingToRemove));
|
|
1897
|
+
remainingToRemove = 0;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
package/src/agent.ts
CHANGED
|
@@ -657,8 +657,8 @@ export class Agent {
|
|
|
657
657
|
}
|
|
658
658
|
|
|
659
659
|
// State mutators
|
|
660
|
-
setSystemPrompt(v: string[]) {
|
|
661
|
-
this.#state.systemPrompt = v;
|
|
660
|
+
setSystemPrompt(v: string[] | string) {
|
|
661
|
+
this.#state.systemPrompt = typeof v === "string" ? [v] : v;
|
|
662
662
|
}
|
|
663
663
|
|
|
664
664
|
setModel(m: Model) {
|
|
@@ -974,8 +974,13 @@ export class Agent {
|
|
|
974
974
|
}
|
|
975
975
|
: undefined;
|
|
976
976
|
|
|
977
|
-
const getToolChoice = () =>
|
|
978
|
-
this.#getToolChoice?.()
|
|
977
|
+
const getToolChoice = () => {
|
|
978
|
+
const queuedToolChoice = this.#getToolChoice?.();
|
|
979
|
+
if (queuedToolChoice !== undefined) {
|
|
980
|
+
return refreshToolChoiceForActiveTools(queuedToolChoice, this.#state.tools);
|
|
981
|
+
}
|
|
982
|
+
return refreshToolChoiceForActiveTools(options?.toolChoice, this.#state.tools);
|
|
983
|
+
};
|
|
979
984
|
|
|
980
985
|
const config: AgentLoopConfig = {
|
|
981
986
|
model,
|