@oh-my-pi/pi-ai 18.4.1 → 18.4.2
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 +8 -0
- package/dist/types/error/flags.d.ts +9 -2
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -18
- package/package.json +6 -6
- package/src/error/flags.ts +10 -3
- package/src/providers/cursor/exec-modern.ts +1 -1
- package/src/providers/cursor-pi-args.ts +13 -18
- package/src/providers/cursor.ts +179 -57
- package/src/stream.ts +25 -15
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.4.2] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed successful Cursor agent turns being treated as context overflows, which ran overflow compaction and showed "Compaction freed too little context to make progress" while `/context` read well under the window; overflow detection now uses the reported context size instead of input totals summed across a turn's model calls ([#13608](https://github.com/can1357/oh-my-pi/pull/13608) by [@H4vC](https://github.com/H4vC))
|
|
10
|
+
- Fixed Anthropic requests with thinking enabled failing on models whose output ceiling cannot fit the minimum thinking budget; thinking is now disabled for those requests instead ([#13359](https://github.com/can1357/oh-my-pi/pull/13359) by [@jchanghong023](https://github.com/jchanghong023))
|
|
11
|
+
- Fixed Cursor native Grep/Glob results showing no matches or raw output, Write failing to create files, StrReplace missing edits beyond the read limit, and Read/Shell/Delete results misreporting content or metadata ([#13600](https://github.com/can1357/oh-my-pi/issues/13600)).
|
|
12
|
+
|
|
5
13
|
## [18.4.1] - 2026-09-28
|
|
6
14
|
|
|
7
15
|
### Fixed
|
|
@@ -126,9 +126,16 @@ export declare function classifyMessage(message: {
|
|
|
126
126
|
export declare function attach<E extends object>(error: E, id: number): E;
|
|
127
127
|
/** Overflow-classification evidence, including errors received before token usage is available. */
|
|
128
128
|
export interface ContextOverflowMessage extends Pick<AssistantMessage, "errorId" | "stopReason" | "errorMessage"> {
|
|
129
|
-
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite">;
|
|
129
|
+
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite" | "contextTokens">;
|
|
130
130
|
}
|
|
131
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Provider-reported usage proves context-window excess — authoritative, compaction-owned (#9235).
|
|
133
|
+
*
|
|
134
|
+
* Prefers `contextTokens` when the provider reports it: providers that run
|
|
135
|
+
* several model calls per turn (Cursor's server-side tool loop) report
|
|
136
|
+
* `input`/`cacheRead` summed across those calls, which can exceed the window
|
|
137
|
+
* many times over while the conversation itself stays small.
|
|
138
|
+
*/
|
|
132
139
|
export declare function isUsageBackedContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean;
|
|
133
140
|
/** Classify overflow from error flags, available token usage, or provider error text. */
|
|
134
141
|
export declare function isContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean;
|
|
@@ -17,7 +17,7 @@ import type { ToolResultMessage } from "../../types.js";
|
|
|
17
17
|
* virtual registry. Re-exported here because this is where the frame builders
|
|
18
18
|
* and their translation are consumed together.
|
|
19
19
|
*/
|
|
20
|
-
export {
|
|
20
|
+
export { cursorExecReadPath, cursorRawReadPath, omitUndefinedArgs, piEscapeRegexLiteral, piGrepSkip, piJoinPath, piLimit, piLsPath, piReadDisplayPath, piReadPath, piReadPathHasRange, piTimeout, shellTimeoutSeconds, } from "../cursor-pi-args.js";
|
|
21
21
|
/** Flatten a tool result's content into the single `output` string the Pi frames carry. */
|
|
22
22
|
export declare function piOutputText(toolResult: ToolResultMessage): string;
|
|
23
23
|
/**
|
|
@@ -22,18 +22,14 @@
|
|
|
22
22
|
* A `pi_read` range composed onto the path as `read`'s inline `:raw:N+K`
|
|
23
23
|
* selector.
|
|
24
24
|
*
|
|
25
|
-
* `read` exposes no range kwargs
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* wrong for a caller that asked for exactly `limit` lines from `offset`. The
|
|
34
|
-
* wire result is an opaque `output` string, so the hashline and line-number
|
|
35
|
-
* gutter that `raw` also drops carry nothing the frame's contract needs.
|
|
36
|
-
* A range-free read keeps the ordinary form — whole-file reads want them.
|
|
25
|
+
* `read` exposes no range kwargs; `offset` and `limit` are composed onto the
|
|
26
|
+
* path. A negative offset needs the source line count and is resolved by the
|
|
27
|
+
* coding-agent bridge before calling this helper. `limit: 0` has no selector
|
|
28
|
+
* representation and returns `null`.
|
|
29
|
+
*
|
|
30
|
+
* Range selectors are raw because plain ranges add context lines. Cursor
|
|
31
|
+
* numbers the returned text itself, so it cannot use read's hashline gutter.
|
|
32
|
+
* Use [`cursorExecReadPath`] for a range-free read, which also needs `:raw`.
|
|
37
33
|
*/
|
|
38
34
|
export declare function piReadPath(readPath: string, offset?: number, limit?: number): string | null;
|
|
39
35
|
/**
|
|
@@ -55,14 +51,13 @@ export declare function piReadPathHasRange(readPath: string): boolean;
|
|
|
55
51
|
*/
|
|
56
52
|
export declare function cursorRawReadPath(readPath: string): string;
|
|
57
53
|
/**
|
|
58
|
-
*
|
|
54
|
+
* Raw selector for a Cursor exec read, including edit-owned materialization.
|
|
59
55
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* second `:raw` onto the already-composed selector.
|
|
56
|
+
* Compose a requested window before forcing `:raw` on whole-file reads. The
|
|
57
|
+
* caller drops `offset`/`limit` after composing so the handler cannot append
|
|
58
|
+
* another selector.
|
|
64
59
|
*/
|
|
65
|
-
export declare function
|
|
60
|
+
export declare function cursorExecReadPath(readPath: string, offset?: number, limit?: number): string | null;
|
|
66
61
|
/**
|
|
67
62
|
* The same range as {@link piReadPath}, rendered for a transcript block rather
|
|
68
63
|
* than for execution.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.4.
|
|
3
|
+
"version": "18.4.2",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.4.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.4.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.4.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.4.
|
|
158
|
+
"@oh-my-pi/omptype": "18.4.2",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.4.2",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.4.2",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.4.2",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.4.2"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/error/flags.ts
CHANGED
|
@@ -875,14 +875,21 @@ export function attach<E extends object>(error: E, id: number): E {
|
|
|
875
875
|
|
|
876
876
|
/** Overflow-classification evidence, including errors received before token usage is available. */
|
|
877
877
|
export interface ContextOverflowMessage extends Pick<AssistantMessage, "errorId" | "stopReason" | "errorMessage"> {
|
|
878
|
-
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite">;
|
|
878
|
+
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite" | "contextTokens">;
|
|
879
879
|
}
|
|
880
880
|
|
|
881
|
-
/**
|
|
881
|
+
/**
|
|
882
|
+
* Provider-reported usage proves context-window excess — authoritative, compaction-owned (#9235).
|
|
883
|
+
*
|
|
884
|
+
* Prefers `contextTokens` when the provider reports it: providers that run
|
|
885
|
+
* several model calls per turn (Cursor's server-side tool loop) report
|
|
886
|
+
* `input`/`cacheRead` summed across those calls, which can exceed the window
|
|
887
|
+
* many times over while the conversation itself stays small.
|
|
888
|
+
*/
|
|
882
889
|
export function isUsageBackedContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean {
|
|
883
890
|
const usage = message.usage;
|
|
884
891
|
if (!contextWindow || !usage) return false;
|
|
885
|
-
const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
892
|
+
const inputTokens = usage.contextTokens ?? usage.input + usage.cacheRead + usage.cacheWrite;
|
|
886
893
|
return inputTokens > contextWindow;
|
|
887
894
|
}
|
|
888
895
|
|
|
@@ -25,18 +25,14 @@ import * as path from "node:path";
|
|
|
25
25
|
* A `pi_read` range composed onto the path as `read`'s inline `:raw:N+K`
|
|
26
26
|
* selector.
|
|
27
27
|
*
|
|
28
|
-
* `read` exposes no range kwargs
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* wrong for a caller that asked for exactly `limit` lines from `offset`. The
|
|
37
|
-
* wire result is an opaque `output` string, so the hashline and line-number
|
|
38
|
-
* gutter that `raw` also drops carry nothing the frame's contract needs.
|
|
39
|
-
* A range-free read keeps the ordinary form — whole-file reads want them.
|
|
28
|
+
* `read` exposes no range kwargs; `offset` and `limit` are composed onto the
|
|
29
|
+
* path. A negative offset needs the source line count and is resolved by the
|
|
30
|
+
* coding-agent bridge before calling this helper. `limit: 0` has no selector
|
|
31
|
+
* representation and returns `null`.
|
|
32
|
+
*
|
|
33
|
+
* Range selectors are raw because plain ranges add context lines. Cursor
|
|
34
|
+
* numbers the returned text itself, so it cannot use read's hashline gutter.
|
|
35
|
+
* Use [`cursorExecReadPath`] for a range-free read, which also needs `:raw`.
|
|
40
36
|
*/
|
|
41
37
|
export function piReadPath(readPath: string, offset?: number, limit?: number): string | null {
|
|
42
38
|
if (limit !== undefined && Math.floor(limit) <= 0) return null;
|
|
@@ -100,14 +96,13 @@ export function cursorRawReadPath(readPath: string): string {
|
|
|
100
96
|
}
|
|
101
97
|
|
|
102
98
|
/**
|
|
103
|
-
*
|
|
99
|
+
* Raw selector for a Cursor exec read, including edit-owned materialization.
|
|
104
100
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* second `:raw` onto the already-composed selector.
|
|
101
|
+
* Compose a requested window before forcing `:raw` on whole-file reads. The
|
|
102
|
+
* caller drops `offset`/`limit` after composing so the handler cannot append
|
|
103
|
+
* another selector.
|
|
109
104
|
*/
|
|
110
|
-
export function
|
|
105
|
+
export function cursorExecReadPath(readPath: string, offset?: number, limit?: number): string | null {
|
|
111
106
|
const ranged = piReadPath(readPath, offset, limit);
|
|
112
107
|
if (ranged === null) return null;
|
|
113
108
|
return cursorRawReadPath(ranged);
|
package/src/providers/cursor.ts
CHANGED
|
@@ -59,7 +59,6 @@ import {
|
|
|
59
59
|
GrepContentResultSchema,
|
|
60
60
|
GrepCountResultSchema,
|
|
61
61
|
GrepErrorSchema,
|
|
62
|
-
type GrepFileCount,
|
|
63
62
|
GrepFileCountSchema,
|
|
64
63
|
GrepFileMatchSchema,
|
|
65
64
|
GrepFilesResultSchema,
|
|
@@ -99,6 +98,7 @@ import {
|
|
|
99
98
|
McpToolResultSchema,
|
|
100
99
|
ModelDetailsSchema,
|
|
101
100
|
ReadErrorSchema,
|
|
101
|
+
ReadFileNotFoundSchema,
|
|
102
102
|
ReadMcpResourceErrorSchema,
|
|
103
103
|
type ReadMcpResourceExecResult,
|
|
104
104
|
ReadMcpResourceExecResultSchema,
|
|
@@ -233,7 +233,8 @@ import {
|
|
|
233
233
|
buildPiWriteError,
|
|
234
234
|
buildPiWriteRejected,
|
|
235
235
|
buildPiWriteResult,
|
|
236
|
-
|
|
236
|
+
cursorExecReadPath,
|
|
237
|
+
cursorRawReadPath,
|
|
237
238
|
omitUndefinedArgs,
|
|
238
239
|
piEscapeRegexLiteral,
|
|
239
240
|
piGrepSkip,
|
|
@@ -1654,39 +1655,93 @@ async function handleExecServerMessage(
|
|
|
1654
1655
|
const args = execMsg.message.value;
|
|
1655
1656
|
if (!args.toolCallId) args.toolCallId = crypto.randomUUID();
|
|
1656
1657
|
const editOwned = isEditOwnedToolCallId(state, output, args.toolCallId);
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1659
|
-
//
|
|
1660
|
-
//
|
|
1661
|
-
//
|
|
1662
|
-
const
|
|
1663
|
-
const
|
|
1664
|
-
?
|
|
1658
|
+
// Cursor numbers raw content itself; StrReplace also writes it back.
|
|
1659
|
+
// Neither frame may receive hashline gutters or summarized bodies.
|
|
1660
|
+
// Edit-owned reads omit the extra transcript block owned by the edit card.
|
|
1661
|
+
// The handler needs the original negative offset to resolve it against
|
|
1662
|
+
// the file length; positive windows can be composed before dispatch.
|
|
1663
|
+
const negativeOffset = args.offset !== undefined && args.offset < 0;
|
|
1664
|
+
const composed = negativeOffset
|
|
1665
|
+
? cursorRawReadPath(args.path)
|
|
1666
|
+
: cursorExecReadPath(args.path, args.offset, args.limit);
|
|
1667
|
+
const handlerArgs =
|
|
1668
|
+
composed === null
|
|
1665
1669
|
? args
|
|
1666
|
-
: {
|
|
1667
|
-
|
|
1670
|
+
: {
|
|
1671
|
+
...args,
|
|
1672
|
+
path: composed,
|
|
1673
|
+
offset: negativeOffset ? args.offset : undefined,
|
|
1674
|
+
limit: negativeOffset ? args.limit : undefined,
|
|
1675
|
+
};
|
|
1668
1676
|
if (!editOwned) {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1677
|
+
synthesizeCursorExecToolCall(
|
|
1678
|
+
output,
|
|
1679
|
+
stream,
|
|
1680
|
+
state,
|
|
1681
|
+
args.toolCallId,
|
|
1682
|
+
"read",
|
|
1683
|
+
negativeOffset
|
|
1684
|
+
? { path: composed, offset: args.offset, limit: args.limit }
|
|
1685
|
+
: { path: piReadDisplayPath(args.path, args.offset, args.limit) },
|
|
1686
|
+
);
|
|
1675
1687
|
}
|
|
1676
|
-
const { execResult } = await resolveExecHandler(
|
|
1688
|
+
const { execResult: readResult, toolResult } = await resolveExecHandler(
|
|
1677
1689
|
handlerArgs,
|
|
1678
1690
|
execHandlers?.read?.bind(execHandlers),
|
|
1679
1691
|
editOwned ? undefined : onToolResult,
|
|
1680
|
-
|
|
1692
|
+
result =>
|
|
1681
1693
|
buildReadResultFromToolResult(
|
|
1682
1694
|
args.path,
|
|
1683
|
-
|
|
1695
|
+
result,
|
|
1684
1696
|
args.offset !== undefined || args.limit !== undefined || piReadPathHasRange(args.path),
|
|
1685
1697
|
),
|
|
1686
1698
|
reason => buildReadRejectedResult(args.path, reason),
|
|
1687
1699
|
error => buildReadErrorResult(args.path, error),
|
|
1688
1700
|
editOwned ? null : { toolCallId: args.toolCallId, toolName: "read" },
|
|
1689
1701
|
);
|
|
1702
|
+
let execResult = readResult;
|
|
1703
|
+
// StrReplace writes the returned bytes back. A truncated raw read would
|
|
1704
|
+
// make every replacement below the read limit invisible to the server.
|
|
1705
|
+
if (
|
|
1706
|
+
editOwned &&
|
|
1707
|
+
args.offset === undefined &&
|
|
1708
|
+
args.limit === undefined &&
|
|
1709
|
+
!piReadPathHasRange(args.path) &&
|
|
1710
|
+
toolResult &&
|
|
1711
|
+
!toolResult.isError &&
|
|
1712
|
+
toolResultWasTruncated(toolResult)
|
|
1713
|
+
) {
|
|
1714
|
+
const details = toolResult.details;
|
|
1715
|
+
const source = details && typeof details === "object" && "meta" in details && details.meta;
|
|
1716
|
+
const path = source && typeof source === "object" && "source" in source && source.source;
|
|
1717
|
+
if (
|
|
1718
|
+
path &&
|
|
1719
|
+
typeof path === "object" &&
|
|
1720
|
+
"type" in path &&
|
|
1721
|
+
path.type === "path" &&
|
|
1722
|
+
"value" in path &&
|
|
1723
|
+
typeof path.value === "string"
|
|
1724
|
+
) {
|
|
1725
|
+
try {
|
|
1726
|
+
const content = await readEditMaterialization(path.value);
|
|
1727
|
+
execResult =
|
|
1728
|
+
content === null
|
|
1729
|
+
? buildReadErrorResult(
|
|
1730
|
+
args.path,
|
|
1731
|
+
`File exceeds ${EDIT_MATERIALIZATION_MAX_BYTES} bytes; StrReplace cannot load it whole. Use a line-range read and a targeted edit instead.`,
|
|
1732
|
+
)
|
|
1733
|
+
: buildReadResultFromToolResult(args.path, {
|
|
1734
|
+
...toolResult,
|
|
1735
|
+
content: [{ type: "text", text: content }],
|
|
1736
|
+
details: { fileSize: Buffer.byteLength(content, "utf8") },
|
|
1737
|
+
});
|
|
1738
|
+
} catch (error) {
|
|
1739
|
+
execResult = buildReadErrorResult(args.path, error instanceof Error ? error.message : String(error));
|
|
1740
|
+
}
|
|
1741
|
+
} else {
|
|
1742
|
+
execResult = buildReadErrorResult(args.path, "Unable to read the complete file for StrReplace");
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1690
1745
|
sendExecClientMessage(h2Request, execMsg, "readResult", execResult);
|
|
1691
1746
|
return;
|
|
1692
1747
|
}
|
|
@@ -2842,9 +2897,50 @@ function readFileSizeFromDetails(toolResult: ToolResultMessage): number | undefi
|
|
|
2842
2897
|
return typeof fileSize === "number" && Number.isSafeInteger(fileSize) && fileSize >= 0 ? fileSize : undefined;
|
|
2843
2898
|
}
|
|
2844
2899
|
|
|
2900
|
+
/**
|
|
2901
|
+
* Largest file a native StrReplace materializes whole; matches the local
|
|
2902
|
+
* `read` tool's whole-file snapshot cap (`SNAPSHOT_MAX_BYTES`).
|
|
2903
|
+
*/
|
|
2904
|
+
const EDIT_MATERIALIZATION_MAX_BYTES = 4 * 1024 * 1024;
|
|
2905
|
+
|
|
2906
|
+
/**
|
|
2907
|
+
* Read a file for native StrReplace materialization, or `null` when it exceeds
|
|
2908
|
+
* {@link EDIT_MATERIALIZATION_MAX_BYTES}. The buffer is sized from the handle's
|
|
2909
|
+
* stat and reading stops one byte past it, so a file growing during the read
|
|
2910
|
+
* cannot exhaust memory.
|
|
2911
|
+
*
|
|
2912
|
+
* Throws when the file grew during the read while still under the cap.
|
|
2913
|
+
*/
|
|
2914
|
+
async function readEditMaterialization(filePath: string): Promise<string | null> {
|
|
2915
|
+
const handle = await fs.open(filePath, "r");
|
|
2916
|
+
try {
|
|
2917
|
+
const { size } = await handle.stat();
|
|
2918
|
+
if (size > EDIT_MATERIALIZATION_MAX_BYTES) return null;
|
|
2919
|
+
const buffer = Buffer.allocUnsafe(size + 1);
|
|
2920
|
+
let length = 0;
|
|
2921
|
+
while (length < buffer.length) {
|
|
2922
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
|
|
2923
|
+
if (bytesRead === 0) break;
|
|
2924
|
+
length += bytesRead;
|
|
2925
|
+
}
|
|
2926
|
+
if (length > size) {
|
|
2927
|
+
if (size === EDIT_MATERIALIZATION_MAX_BYTES) return null;
|
|
2928
|
+
throw new Error(`File changed while reading: ${filePath}`);
|
|
2929
|
+
}
|
|
2930
|
+
return buffer.toString("utf8", 0, length);
|
|
2931
|
+
} finally {
|
|
2932
|
+
await handle.close();
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2845
2936
|
function buildReadResultFromToolResult(path: string, toolResult: ToolResultMessage, rangeApplied = false) {
|
|
2846
2937
|
const text = toolResultToText(toolResult);
|
|
2847
2938
|
if (toolResult.isError) {
|
|
2939
|
+
if (/^Path '.*' not found$/.test(text)) {
|
|
2940
|
+
return create(ReadResultSchema, {
|
|
2941
|
+
result: { case: "fileNotFound", value: create(ReadFileNotFoundSchema, { path }) },
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2848
2944
|
return buildReadErrorResult(path, text || "Read failed");
|
|
2849
2945
|
}
|
|
2850
2946
|
// Counting the payload is only the file's length when the payload is the
|
|
@@ -2942,7 +3038,7 @@ function buildDeleteResultFromToolResult(path: string, toolResult: ToolResultMes
|
|
|
2942
3038
|
value: create(DeleteSuccessSchema, {
|
|
2943
3039
|
path,
|
|
2944
3040
|
deletedFile: path,
|
|
2945
|
-
fileSize: BigInt(0),
|
|
3041
|
+
fileSize: BigInt(readFileSizeFromDetails(toolResult) ?? 0),
|
|
2946
3042
|
prevContent: "",
|
|
2947
3043
|
}),
|
|
2948
3044
|
},
|
|
@@ -2973,7 +3069,14 @@ function buildShellResultFromToolResult(
|
|
|
2973
3069
|
) {
|
|
2974
3070
|
const output = toolResultToText(toolResult);
|
|
2975
3071
|
if (toolResult.isError) {
|
|
2976
|
-
|
|
3072
|
+
const details = toolResult.details;
|
|
3073
|
+
const code = details && typeof details === "object" && "exitCode" in details ? details.exitCode : undefined;
|
|
3074
|
+
return buildShellFailureResult(
|
|
3075
|
+
args.command,
|
|
3076
|
+
args.workingDirectory,
|
|
3077
|
+
output || "Shell failed",
|
|
3078
|
+
typeof code === "number" && Number.isInteger(code) ? code : 1,
|
|
3079
|
+
);
|
|
2977
3080
|
}
|
|
2978
3081
|
return create(ShellResultSchema, {
|
|
2979
3082
|
result: {
|
|
@@ -2991,14 +3094,14 @@ function buildShellResultFromToolResult(
|
|
|
2991
3094
|
});
|
|
2992
3095
|
}
|
|
2993
3096
|
|
|
2994
|
-
function buildShellFailureResult(command: string, workingDirectory: string, error: string) {
|
|
3097
|
+
function buildShellFailureResult(command: string, workingDirectory: string, error: string, exitCode = 1) {
|
|
2995
3098
|
return create(ShellResultSchema, {
|
|
2996
3099
|
result: {
|
|
2997
3100
|
case: "failure",
|
|
2998
3101
|
value: create(ShellFailureSchema, {
|
|
2999
3102
|
command,
|
|
3000
3103
|
workingDirectory,
|
|
3001
|
-
exitCode
|
|
3104
|
+
exitCode,
|
|
3002
3105
|
signal: "",
|
|
3003
3106
|
stdout: "",
|
|
3004
3107
|
stderr: error,
|
|
@@ -3101,16 +3204,16 @@ function buildGrepResultFromToolResult(
|
|
|
3101
3204
|
|
|
3102
3205
|
const outputMode = args.outputMode || "content";
|
|
3103
3206
|
const clientTruncated = toolResultDetailBoolean(toolResult, "truncated");
|
|
3104
|
-
const
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3207
|
+
const details = toolResult.details;
|
|
3208
|
+
const files =
|
|
3209
|
+
details && typeof details === "object" && "files" in details && Array.isArray(details.files)
|
|
3210
|
+
? details.files.filter((file): file is string => typeof file === "string")
|
|
3211
|
+
: [];
|
|
3108
3212
|
|
|
3109
3213
|
const workspaceKey = args.path || ".";
|
|
3110
3214
|
let unionResult: GrepUnionResult;
|
|
3111
3215
|
|
|
3112
3216
|
if (outputMode === "files_with_matches") {
|
|
3113
|
-
const files = lines;
|
|
3114
3217
|
unionResult = create(GrepUnionResultSchema, {
|
|
3115
3218
|
result: {
|
|
3116
3219
|
case: "files",
|
|
@@ -3128,20 +3231,21 @@ function buildGrepResultFromToolResult(
|
|
|
3128
3231
|
},
|
|
3129
3232
|
});
|
|
3130
3233
|
} else if (outputMode === "count") {
|
|
3131
|
-
const
|
|
3132
|
-
.
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3234
|
+
const fileMatches =
|
|
3235
|
+
details && typeof details === "object" && "fileMatches" in details && Array.isArray(details.fileMatches)
|
|
3236
|
+
? details.fileMatches
|
|
3237
|
+
: [];
|
|
3238
|
+
const counts = fileMatches
|
|
3239
|
+
.filter(
|
|
3240
|
+
(entry): entry is { path: string; count: number } =>
|
|
3241
|
+
entry !== null &&
|
|
3242
|
+
typeof entry === "object" &&
|
|
3243
|
+
"path" in entry &&
|
|
3244
|
+
typeof entry.path === "string" &&
|
|
3245
|
+
"count" in entry &&
|
|
3246
|
+
typeof entry.count === "number",
|
|
3247
|
+
)
|
|
3248
|
+
.map(({ path, count }) => create(GrepFileCountSchema, { file: path, count }));
|
|
3145
3249
|
const totalMatches = counts.reduce((sum, entry) => sum + entry.count, 0);
|
|
3146
3250
|
unionResult = create(GrepUnionResultSchema, {
|
|
3147
3251
|
result: {
|
|
@@ -3159,22 +3263,40 @@ function buildGrepResultFromToolResult(
|
|
|
3159
3263
|
} else {
|
|
3160
3264
|
const matchMap = new Map<string, Array<{ line: number; content: string; isContextLine: boolean }>>();
|
|
3161
3265
|
let totalMatchedLines = 0;
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
const
|
|
3167
|
-
if (
|
|
3266
|
+
const directories = new Map<number, string>();
|
|
3267
|
+
let currentFile = files.length === 1 ? files[0] : undefined;
|
|
3268
|
+
|
|
3269
|
+
for (const line of text.split("\n")) {
|
|
3270
|
+
const header = /^(#+) (.+)$/.exec(line);
|
|
3271
|
+
if (header) {
|
|
3272
|
+
const depth = header[1]!.length;
|
|
3273
|
+
const name = header[2]!;
|
|
3274
|
+
for (const level of directories.keys()) {
|
|
3275
|
+
if (level >= depth) directories.delete(level);
|
|
3276
|
+
}
|
|
3277
|
+
const parent = directories.get(depth - 1);
|
|
3278
|
+
const candidate = parent ? `${parent}/${name}` : name;
|
|
3279
|
+
if (name.endsWith("/")) {
|
|
3280
|
+
directories.set(depth, candidate.slice(0, -1));
|
|
3281
|
+
currentFile = undefined;
|
|
3282
|
+
} else {
|
|
3283
|
+
currentFile = files.includes(candidate) ? candidate : candidate.replace(/#[0-9a-f]{4,}$/i, "");
|
|
3284
|
+
}
|
|
3168
3285
|
continue;
|
|
3169
3286
|
}
|
|
3170
|
-
const
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
matchMap.set(file, list);
|
|
3175
|
-
if (!isContextLine) {
|
|
3176
|
-
totalMatchedLines += 1;
|
|
3287
|
+
const singleHeader = /^\[(.+)#[0-9a-f]{4,}\]$/i.exec(line);
|
|
3288
|
+
if (singleHeader) {
|
|
3289
|
+
currentFile = files[0] ?? singleHeader[1]!;
|
|
3290
|
+
continue;
|
|
3177
3291
|
}
|
|
3292
|
+
const match = /^([* ])(\d+)[:|](.*)$/.exec(line);
|
|
3293
|
+
if (!match || !currentFile) continue;
|
|
3294
|
+
const [, marker, lineNumber, content] = match;
|
|
3295
|
+
const isContextLine = marker === " ";
|
|
3296
|
+
const list = matchMap.get(currentFile) ?? [];
|
|
3297
|
+
list.push({ line: Number(lineNumber), content, isContextLine });
|
|
3298
|
+
matchMap.set(currentFile, list);
|
|
3299
|
+
if (!isContextLine) totalMatchedLines++;
|
|
3178
3300
|
}
|
|
3179
3301
|
|
|
3180
3302
|
const matches = Array.from(matchMap.entries()).map(([file, matches]) =>
|
package/src/stream.ts
CHANGED
|
@@ -1932,29 +1932,39 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1932
1932
|
}
|
|
1933
1933
|
|
|
1934
1934
|
if (ANTHROPIC_USE_INTERLEAVED_THINKING) {
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
maxTokens
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1935
|
+
if (
|
|
1936
|
+
model.maxTokens !== null &&
|
|
1937
|
+
model.maxTokens !== undefined &&
|
|
1938
|
+
model.maxTokens < thinkingBudget + OUTPUT_FALLBACK_BUFFER
|
|
1939
|
+
) {
|
|
1940
|
+
thinkingBudget = model.maxTokens - OUTPUT_FALLBACK_BUFFER;
|
|
1941
|
+
}
|
|
1942
|
+
if (thinkingBudget >= ANTHROPIC_THINKING.minimal) {
|
|
1943
|
+
return castApi<"anthropic-messages">({
|
|
1944
|
+
...base,
|
|
1945
|
+
maxTokens: maxTokensWithThinking,
|
|
1946
|
+
requestModelId: resolveWireModelId(model, reasoning),
|
|
1947
|
+
thinkingEnabled: true,
|
|
1948
|
+
thinkingBudgetTokens: thinkingBudget,
|
|
1949
|
+
effort,
|
|
1950
|
+
toolChoice: mapAnthropicToolChoice(options?.toolChoice),
|
|
1951
|
+
thinkingDisplay: options?.hideThinkingSummary ? "omitted" : undefined,
|
|
1952
|
+
serviceTier: options?.serviceTier,
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1946
1955
|
}
|
|
1947
1956
|
|
|
1948
1957
|
// Caller's maxTokens is desired output, so add thinking budget on top. With no caller/model cap, use a finite total fallback.
|
|
1949
1958
|
const maxTokens = maxTokensWithThinkingBudget(base.maxTokens, model.maxTokens, thinkingBudget);
|
|
1950
1959
|
|
|
1951
|
-
//
|
|
1952
|
-
|
|
1953
|
-
|
|
1960
|
+
// Keep the provider's output buffer after thinking, reducing the
|
|
1961
|
+
// budget before its wire-level clamp could fall below the API minimum.
|
|
1962
|
+
if (maxTokens < thinkingBudget + OUTPUT_FALLBACK_BUFFER) {
|
|
1963
|
+
thinkingBudget = maxTokens - OUTPUT_FALLBACK_BUFFER;
|
|
1954
1964
|
}
|
|
1955
1965
|
|
|
1956
1966
|
// If thinking budget is too low, disable thinking
|
|
1957
|
-
if (thinkingBudget
|
|
1967
|
+
if (thinkingBudget < ANTHROPIC_THINKING.minimal) {
|
|
1958
1968
|
return castApi<"anthropic-messages">({
|
|
1959
1969
|
...base,
|
|
1960
1970
|
requestModelId: resolveWireModelId(model, undefined),
|