@gajae-code/coding-agent 0.11.2 → 0.11.3
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 +10 -0
- package/dist/types/cli/launch-disposition.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +85 -10
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +2 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/subagent.d.ts +1 -0
- package/dist/types/utils/file-mentions.d.ts +3 -3
- package/package.json +8 -8
- package/src/cli/launch-disposition.ts +42 -0
- package/src/config/settings-schema.ts +55 -10
- package/src/gjc-runtime/state-runtime.ts +10 -3
- package/src/main.ts +99 -22
- package/src/modes/prompt-action-autocomplete.ts +7 -0
- package/src/sdk/bus/index.ts +4 -1
- package/src/sdk/host/host.ts +13 -1
- package/src/sdk/session.ts +4 -1
- package/src/tools/output-meta.ts +6 -2
- package/src/tools/read.ts +182 -63
- package/src/tools/subagent.ts +43 -2
- package/src/utils/file-mentions.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.3] - 2026-07-19
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- The `read` tool is now receipt-by-default: bare and unparseable reads return a bounded receipt (≤50 lines / 10 KiB) with a re-read-with-selector footer only when truncated, `:raw` stays pure verbatim up to a max(2 MiB, spill threshold) ceiling, structural summaries cap unit-granularly at 20 KiB while preserving elision and source-recovery footers, and directories are byte/line capped and never spill. Only an explicit full-content selector (`:raw` or an explicit range) with real content is spill-eligible. Subagent previews now enforce a real byte/code-point cap via per-shape render budgets plus a shape-aware artifact-eligibility tag enforced centrally in output-meta.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Workflow-state handoff no longer self-locks the active-state cache, so a same-turn skill handoff (e.g. ultragoal → ralplan) completes instead of stalling behind a lock the handoff itself still holds (#2638).
|
|
12
|
+
- SDK host shutdown now retries a failed broker unregister instead of short-circuiting with a stale broker-index entry, while retained startup-cleanup owner-release failures remain isolated from the red extension-error path (#2625).
|
|
13
|
+
- Non-TTY launches now fail fast when stdin is empty and automatically use print mode for positional prompts and `@file` inputs, preventing orphaned interactive TUI processes (#2507).
|
|
14
|
+
|
|
5
15
|
## [0.11.2] - 2026-07-19
|
|
6
16
|
|
|
7
17
|
### Changed
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type LaunchMode = "text" | "json" | "acp";
|
|
2
|
+
export interface LaunchDisposition {
|
|
3
|
+
autoPrint: boolean;
|
|
4
|
+
isInteractive: boolean;
|
|
5
|
+
/** Set when an interactive launch is impossible (non-TTY stdin, nothing to run). */
|
|
6
|
+
nonInteractiveError?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Decides between interactive, auto-print, and fail-fast launches.
|
|
10
|
+
*
|
|
11
|
+
* A non-TTY stdin means the TUI can never receive input, so an interactive
|
|
12
|
+
* launch would execute the initial prompt and then block in the event loop
|
|
13
|
+
* forever. Prepared command-line input degrades to print mode; without any
|
|
14
|
+
* input to run we fail fast instead of hanging. Explicit `--print` and
|
|
15
|
+
* `--mode` launches are left untouched.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveLaunchDisposition(args: {
|
|
18
|
+
stdinIsTTY: boolean | undefined;
|
|
19
|
+
pipedInput: string | undefined;
|
|
20
|
+
hasPreparedInput: boolean;
|
|
21
|
+
print: boolean;
|
|
22
|
+
mode: LaunchMode | undefined;
|
|
23
|
+
}): LaunchDisposition;
|
|
@@ -662,15 +662,15 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
662
662
|
};
|
|
663
663
|
readonly "tools.readArtifactSpillThreshold": {
|
|
664
664
|
readonly type: "number";
|
|
665
|
-
readonly default:
|
|
665
|
+
readonly default: 256;
|
|
666
666
|
readonly ui: {
|
|
667
667
|
readonly tab: "tools";
|
|
668
|
-
readonly label: "
|
|
669
|
-
readonly description: "
|
|
668
|
+
readonly label: "Read artifact spill threshold (KB)";
|
|
669
|
+
readonly description: "Explicit large reads above this combined size are saved as an artifact with a bounded head-and-tail snippet inline. Bare reads, directories, and converted-document receipts remain inline.";
|
|
670
670
|
readonly options: readonly [{
|
|
671
671
|
readonly value: "0";
|
|
672
672
|
readonly label: "Off";
|
|
673
|
-
readonly description: "
|
|
673
|
+
readonly description: "No read-specific spill (backstop only)";
|
|
674
674
|
}, {
|
|
675
675
|
readonly value: "50";
|
|
676
676
|
readonly label: "50 KB";
|
|
@@ -682,7 +682,7 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
682
682
|
}, {
|
|
683
683
|
readonly value: "256";
|
|
684
684
|
readonly label: "256 KB";
|
|
685
|
-
readonly description: "~64K tokens";
|
|
685
|
+
readonly description: "Default; ~64K tokens";
|
|
686
686
|
}, {
|
|
687
687
|
readonly value: "512";
|
|
688
688
|
readonly label: "512 KB";
|
|
@@ -696,11 +696,11 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
696
696
|
};
|
|
697
697
|
readonly "tools.fileMentionInlineBytes": {
|
|
698
698
|
readonly type: "number";
|
|
699
|
-
readonly default:
|
|
699
|
+
readonly default: 10;
|
|
700
700
|
readonly ui: {
|
|
701
701
|
readonly tab: "tools";
|
|
702
702
|
readonly label: "File-mention inline cap (KB)";
|
|
703
|
-
readonly description: "Inline byte cap for auto-read `@path` file mentions,
|
|
703
|
+
readonly description: "Inline byte cap for auto-read `@path` file mentions, aligned with the 10 KiB bare-read receipt so incidental mentions stay within the same bounded context budget. The full file is still available via the read tool.";
|
|
704
704
|
readonly options: readonly [{
|
|
705
705
|
readonly value: "5";
|
|
706
706
|
readonly label: "5 KB";
|
|
@@ -708,15 +708,15 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
708
708
|
}, {
|
|
709
709
|
readonly value: "10";
|
|
710
710
|
readonly label: "10 KB";
|
|
711
|
-
readonly description: "~2.5K tokens";
|
|
711
|
+
readonly description: "Default; ~2.5K tokens";
|
|
712
712
|
}, {
|
|
713
713
|
readonly value: "20";
|
|
714
714
|
readonly label: "20 KB";
|
|
715
|
-
readonly description: "
|
|
715
|
+
readonly description: "~5K tokens";
|
|
716
716
|
}, {
|
|
717
717
|
readonly value: "50";
|
|
718
718
|
readonly label: "50 KB";
|
|
719
|
-
readonly description: "~12.5K tokens (matches read
|
|
719
|
+
readonly description: "~12.5K tokens (matches bare-read receipt)";
|
|
720
720
|
}];
|
|
721
721
|
};
|
|
722
722
|
};
|
|
@@ -2437,6 +2437,81 @@ export declare const SETTINGS_SCHEMA: {
|
|
|
2437
2437
|
}];
|
|
2438
2438
|
};
|
|
2439
2439
|
};
|
|
2440
|
+
readonly "read.receiptBudgetLines": {
|
|
2441
|
+
readonly type: "number";
|
|
2442
|
+
readonly default: 50;
|
|
2443
|
+
readonly ui: {
|
|
2444
|
+
readonly tab: "editing";
|
|
2445
|
+
readonly label: "Read Receipt Line Budget";
|
|
2446
|
+
readonly description: "Maximum lines included in a bare read receipt before a selector footer is shown";
|
|
2447
|
+
readonly options: readonly [{
|
|
2448
|
+
readonly value: "25";
|
|
2449
|
+
readonly label: "25 lines";
|
|
2450
|
+
}, {
|
|
2451
|
+
readonly value: "50";
|
|
2452
|
+
readonly label: "50 lines";
|
|
2453
|
+
readonly description: "Default";
|
|
2454
|
+
}, {
|
|
2455
|
+
readonly value: "100";
|
|
2456
|
+
readonly label: "100 lines";
|
|
2457
|
+
}, {
|
|
2458
|
+
readonly value: "200";
|
|
2459
|
+
readonly label: "200 lines";
|
|
2460
|
+
}];
|
|
2461
|
+
};
|
|
2462
|
+
};
|
|
2463
|
+
readonly "read.receiptBudgetBytes": {
|
|
2464
|
+
readonly type: "number";
|
|
2465
|
+
readonly default: 10;
|
|
2466
|
+
readonly ui: {
|
|
2467
|
+
readonly tab: "editing";
|
|
2468
|
+
readonly label: "Read Receipt Byte Budget (KB)";
|
|
2469
|
+
readonly description: "Maximum UTF-8 body size for a bare read receipt before a selector footer is shown";
|
|
2470
|
+
readonly options: readonly [{
|
|
2471
|
+
readonly value: "5";
|
|
2472
|
+
readonly label: "5 KB";
|
|
2473
|
+
readonly description: "~1.25K tokens";
|
|
2474
|
+
}, {
|
|
2475
|
+
readonly value: "10";
|
|
2476
|
+
readonly label: "10 KB";
|
|
2477
|
+
readonly description: "Default; ~2.5K tokens";
|
|
2478
|
+
}, {
|
|
2479
|
+
readonly value: "20";
|
|
2480
|
+
readonly label: "20 KB";
|
|
2481
|
+
readonly description: "~5K tokens";
|
|
2482
|
+
}, {
|
|
2483
|
+
readonly value: "50";
|
|
2484
|
+
readonly label: "50 KB";
|
|
2485
|
+
readonly description: "~12.5K tokens";
|
|
2486
|
+
}];
|
|
2487
|
+
};
|
|
2488
|
+
};
|
|
2489
|
+
readonly "read.summaryMaxBytes": {
|
|
2490
|
+
readonly type: "number";
|
|
2491
|
+
readonly default: 20;
|
|
2492
|
+
readonly ui: {
|
|
2493
|
+
readonly tab: "editing";
|
|
2494
|
+
readonly label: "Read Summary Size Budget (KB)";
|
|
2495
|
+
readonly description: "Maximum UTF-8 size for a structural read summary before additional units are elided";
|
|
2496
|
+
readonly options: readonly [{
|
|
2497
|
+
readonly value: "10";
|
|
2498
|
+
readonly label: "10 KB";
|
|
2499
|
+
readonly description: "~2.5K tokens";
|
|
2500
|
+
}, {
|
|
2501
|
+
readonly value: "20";
|
|
2502
|
+
readonly label: "20 KB";
|
|
2503
|
+
readonly description: "Default; ~5K tokens";
|
|
2504
|
+
}, {
|
|
2505
|
+
readonly value: "50";
|
|
2506
|
+
readonly label: "50 KB";
|
|
2507
|
+
readonly description: "~12.5K tokens";
|
|
2508
|
+
}, {
|
|
2509
|
+
readonly value: "100";
|
|
2510
|
+
readonly label: "100 KB";
|
|
2511
|
+
readonly description: "~25K tokens";
|
|
2512
|
+
}];
|
|
2513
|
+
};
|
|
2514
|
+
};
|
|
2440
2515
|
readonly "read.summarize.enabled": {
|
|
2441
2516
|
readonly type: "boolean";
|
|
2442
2517
|
readonly default: true;
|
package/dist/types/main.d.ts
CHANGED
|
@@ -118,6 +118,7 @@ export interface RunRootCommandDependencies {
|
|
|
118
118
|
};
|
|
119
119
|
initTheme?: typeof initTheme;
|
|
120
120
|
readPipedInput?: typeof readPipedInput;
|
|
121
|
+
stdinIsTTY?: boolean;
|
|
121
122
|
runStartupCredentialAutoImportIfNeeded?: typeof runStartupCredentialAutoImportIfNeeded;
|
|
122
123
|
getChangelogForDisplay?: typeof getChangelogForDisplay;
|
|
123
124
|
createInteractiveMode?: CreateInteractiveMode;
|
|
@@ -30,6 +30,10 @@ export declare class PromptActionAutocompleteProvider implements AutocompletePro
|
|
|
30
30
|
items: AutocompleteItem[];
|
|
31
31
|
prefix: string;
|
|
32
32
|
} | null>;
|
|
33
|
+
getForceFileSuggestions(lines: string[], cursorLine: number, cursorCol: number): Promise<{
|
|
34
|
+
items: AutocompleteItem[];
|
|
35
|
+
prefix: string;
|
|
36
|
+
} | null>;
|
|
33
37
|
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string): {
|
|
34
38
|
lines: string[];
|
|
35
39
|
cursorLine: number;
|
|
@@ -338,6 +338,8 @@ export declare function createNotificationsExtension(api: ExtensionAPI, options?
|
|
|
338
338
|
/** Suppress auto-delivery for a GJC-spawned child under `sessionScope=primary`. */
|
|
339
339
|
spawnedByGjc?: boolean;
|
|
340
340
|
controller?: NotificationSessionController;
|
|
341
|
+
/** Whether this host mode can own the root SDK endpoint. Default: true. */
|
|
342
|
+
sdkHostModeSupported?: boolean;
|
|
341
343
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
342
344
|
runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{
|
|
343
345
|
replyText: string;
|
|
@@ -150,6 +150,8 @@ export interface CreateAgentSessionOptions {
|
|
|
150
150
|
hasUI?: boolean;
|
|
151
151
|
/** Whether this host mode can own a notification session endpoint. Default: true. */
|
|
152
152
|
notificationHostModeSupported?: boolean;
|
|
153
|
+
/** Whether this host mode can own the root SDK endpoint. Default: true. */
|
|
154
|
+
sdkHostModeSupported?: boolean;
|
|
153
155
|
/**
|
|
154
156
|
* Opt-in OpenTelemetry instrumentation forwarded to the underlying Agent.
|
|
155
157
|
* Passing `{}` enables the loop's GenAI-semantic-convention spans. See
|
|
@@ -27,6 +27,8 @@ export interface ReadToolDetails {
|
|
|
27
27
|
method?: string;
|
|
28
28
|
notes?: string[];
|
|
29
29
|
meta?: OutputMeta;
|
|
30
|
+
/** Whether this explicit content read may be spilled by output metadata. */
|
|
31
|
+
spillEligible?: boolean;
|
|
30
32
|
/** Raw text + start line for user-visible TUI rendering, set when content is text-like.
|
|
31
33
|
* Mirrors the same lines the model receives but without hashline/line-number prefixes,
|
|
32
34
|
* so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
|
|
@@ -98,6 +98,7 @@ export declare class SubagentTool implements AgentTool<typeof subagentSchema, Su
|
|
|
98
98
|
constructor(session: ToolSession);
|
|
99
99
|
execute(_toolCallId: string, params: SubagentParams, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<SubagentToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<SubagentToolDetails>>;
|
|
100
100
|
}
|
|
101
|
+
export declare function capCodePointsAndBytes(text: string, maxCodePoints: number, maxBytes: number): string;
|
|
101
102
|
/**
|
|
102
103
|
* Canonical, value-based rendered-state signature for the `subagent` await panel.
|
|
103
104
|
*
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
2
2
|
/**
|
|
3
|
-
* Default inline cap for auto-read file mentions,
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Default inline cap for auto-read file mentions, aligned with the read-tool's
|
|
4
|
+
* 10 KiB bare-read receipt so an incidental `@path` mention injects a bounded
|
|
5
|
+
* snippet. Overridable per call.
|
|
6
6
|
*/
|
|
7
7
|
export declare const DEFAULT_FILE_MENTION_INLINE_BYTES: number;
|
|
8
8
|
/** Extract all @filepath mentions from text */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.3",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gajae-code.com",
|
|
7
7
|
"author": "Yeachan-Heo and Gajae Code Contributors",
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
54
54
|
"@babel/parser": "^7.29.3",
|
|
55
55
|
"@mozilla/readability": "^0.6.0",
|
|
56
|
-
"@gajae-code/stats": "0.11.
|
|
57
|
-
"@gajae-code/agent-core": "0.11.
|
|
58
|
-
"@gajae-code/ai": "0.11.
|
|
59
|
-
"@gajae-code/bridge-client": "0.11.
|
|
60
|
-
"@gajae-code/natives": "0.11.
|
|
61
|
-
"@gajae-code/tui": "0.11.
|
|
62
|
-
"@gajae-code/utils": "0.11.
|
|
56
|
+
"@gajae-code/stats": "0.11.3",
|
|
57
|
+
"@gajae-code/agent-core": "0.11.3",
|
|
58
|
+
"@gajae-code/ai": "0.11.3",
|
|
59
|
+
"@gajae-code/bridge-client": "0.11.3",
|
|
60
|
+
"@gajae-code/natives": "0.11.3",
|
|
61
|
+
"@gajae-code/tui": "0.11.3",
|
|
62
|
+
"@gajae-code/utils": "0.11.3",
|
|
63
63
|
"@puppeteer/browsers": "^2.13.0",
|
|
64
64
|
"@types/turndown": "5.0.6",
|
|
65
65
|
"@xterm/headless": "^6.0.0",
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type LaunchMode = "text" | "json" | "acp";
|
|
2
|
+
|
|
3
|
+
export interface LaunchDisposition {
|
|
4
|
+
autoPrint: boolean;
|
|
5
|
+
isInteractive: boolean;
|
|
6
|
+
/** Set when an interactive launch is impossible (non-TTY stdin, nothing to run). */
|
|
7
|
+
nonInteractiveError?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decides between interactive, auto-print, and fail-fast launches.
|
|
12
|
+
*
|
|
13
|
+
* A non-TTY stdin means the TUI can never receive input, so an interactive
|
|
14
|
+
* launch would execute the initial prompt and then block in the event loop
|
|
15
|
+
* forever. Prepared command-line input degrades to print mode; without any
|
|
16
|
+
* input to run we fail fast instead of hanging. Explicit `--print` and
|
|
17
|
+
* `--mode` launches are left untouched.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveLaunchDisposition(args: {
|
|
20
|
+
stdinIsTTY: boolean | undefined;
|
|
21
|
+
pipedInput: string | undefined;
|
|
22
|
+
hasPreparedInput: boolean;
|
|
23
|
+
print: boolean;
|
|
24
|
+
mode: LaunchMode | undefined;
|
|
25
|
+
}): LaunchDisposition {
|
|
26
|
+
const explicitNonInteractive = args.print || args.mode !== undefined;
|
|
27
|
+
const hasPreparedInput = args.pipedInput !== undefined || args.hasPreparedInput;
|
|
28
|
+
const hasNonTtyStdin = args.stdinIsTTY !== true;
|
|
29
|
+
const autoPrint = !explicitNonInteractive && hasNonTtyStdin && hasPreparedInput;
|
|
30
|
+
const isInteractive = !explicitNonInteractive && !autoPrint;
|
|
31
|
+
|
|
32
|
+
if (isInteractive && args.stdinIsTTY !== true) {
|
|
33
|
+
return {
|
|
34
|
+
autoPrint: false,
|
|
35
|
+
isInteractive: false,
|
|
36
|
+
nonInteractiveError:
|
|
37
|
+
"stdin is not a TTY and no prompt or prepared input was provided; use -p/--print (or pass a prompt or @file) for non-interactive runs.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { autoPrint, isInteractive };
|
|
42
|
+
}
|
|
@@ -669,17 +669,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
669
669
|
|
|
670
670
|
"tools.readArtifactSpillThreshold": {
|
|
671
671
|
type: "number",
|
|
672
|
-
default:
|
|
672
|
+
default: 256,
|
|
673
673
|
ui: {
|
|
674
674
|
tab: "tools",
|
|
675
|
-
label: "
|
|
675
|
+
label: "Read artifact spill threshold (KB)",
|
|
676
676
|
description:
|
|
677
|
-
"
|
|
677
|
+
"Explicit large reads above this combined size are saved as an artifact with a bounded head-and-tail snippet inline. Bare reads, directories, and converted-document receipts remain inline.",
|
|
678
678
|
options: [
|
|
679
|
-
{ value: "0", label: "Off", description: "
|
|
679
|
+
{ value: "0", label: "Off", description: "No read-specific spill (backstop only)" },
|
|
680
680
|
{ value: "50", label: "50 KB", description: "~12.5K tokens" },
|
|
681
681
|
{ value: "100", label: "100 KB", description: "~25K tokens" },
|
|
682
|
-
{ value: "256", label: "256 KB", description: "~64K tokens" },
|
|
682
|
+
{ value: "256", label: "256 KB", description: "Default; ~64K tokens" },
|
|
683
683
|
{ value: "512", label: "512 KB", description: "~128K tokens" },
|
|
684
684
|
{ value: "1000", label: "1 MB", description: "~250K tokens" },
|
|
685
685
|
],
|
|
@@ -688,17 +688,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
688
688
|
|
|
689
689
|
"tools.fileMentionInlineBytes": {
|
|
690
690
|
type: "number",
|
|
691
|
-
default:
|
|
691
|
+
default: 10,
|
|
692
692
|
ui: {
|
|
693
693
|
tab: "tools",
|
|
694
694
|
label: "File-mention inline cap (KB)",
|
|
695
695
|
description:
|
|
696
|
-
"Inline byte cap for auto-read `@path` file mentions,
|
|
696
|
+
"Inline byte cap for auto-read `@path` file mentions, aligned with the 10 KiB bare-read receipt so incidental mentions stay within the same bounded context budget. The full file is still available via the read tool.",
|
|
697
697
|
options: [
|
|
698
698
|
{ value: "5", label: "5 KB", description: "~1.25K tokens" },
|
|
699
|
-
{ value: "10", label: "10 KB", description: "~2.5K tokens" },
|
|
700
|
-
{ value: "20", label: "20 KB", description: "
|
|
701
|
-
{ value: "50", label: "50 KB", description: "~12.5K tokens (matches read
|
|
699
|
+
{ value: "10", label: "10 KB", description: "Default; ~2.5K tokens" },
|
|
700
|
+
{ value: "20", label: "20 KB", description: "~5K tokens" },
|
|
701
|
+
{ value: "50", label: "50 KB", description: "~12.5K tokens (matches bare-read receipt)" },
|
|
702
702
|
],
|
|
703
703
|
},
|
|
704
704
|
},
|
|
@@ -2016,6 +2016,51 @@ export const SETTINGS_SCHEMA = {
|
|
|
2016
2016
|
],
|
|
2017
2017
|
},
|
|
2018
2018
|
},
|
|
2019
|
+
"read.receiptBudgetLines": {
|
|
2020
|
+
type: "number",
|
|
2021
|
+
default: 50,
|
|
2022
|
+
ui: {
|
|
2023
|
+
tab: "editing",
|
|
2024
|
+
label: "Read Receipt Line Budget",
|
|
2025
|
+
description: "Maximum lines included in a bare read receipt before a selector footer is shown",
|
|
2026
|
+
options: [
|
|
2027
|
+
{ value: "25", label: "25 lines" },
|
|
2028
|
+
{ value: "50", label: "50 lines", description: "Default" },
|
|
2029
|
+
{ value: "100", label: "100 lines" },
|
|
2030
|
+
{ value: "200", label: "200 lines" },
|
|
2031
|
+
],
|
|
2032
|
+
},
|
|
2033
|
+
},
|
|
2034
|
+
"read.receiptBudgetBytes": {
|
|
2035
|
+
type: "number",
|
|
2036
|
+
default: 10,
|
|
2037
|
+
ui: {
|
|
2038
|
+
tab: "editing",
|
|
2039
|
+
label: "Read Receipt Byte Budget (KB)",
|
|
2040
|
+
description: "Maximum UTF-8 body size for a bare read receipt before a selector footer is shown",
|
|
2041
|
+
options: [
|
|
2042
|
+
{ value: "5", label: "5 KB", description: "~1.25K tokens" },
|
|
2043
|
+
{ value: "10", label: "10 KB", description: "Default; ~2.5K tokens" },
|
|
2044
|
+
{ value: "20", label: "20 KB", description: "~5K tokens" },
|
|
2045
|
+
{ value: "50", label: "50 KB", description: "~12.5K tokens" },
|
|
2046
|
+
],
|
|
2047
|
+
},
|
|
2048
|
+
},
|
|
2049
|
+
"read.summaryMaxBytes": {
|
|
2050
|
+
type: "number",
|
|
2051
|
+
default: 20,
|
|
2052
|
+
ui: {
|
|
2053
|
+
tab: "editing",
|
|
2054
|
+
label: "Read Summary Size Budget (KB)",
|
|
2055
|
+
description: "Maximum UTF-8 size for a structural read summary before additional units are elided",
|
|
2056
|
+
options: [
|
|
2057
|
+
{ value: "10", label: "10 KB", description: "~2.5K tokens" },
|
|
2058
|
+
{ value: "20", label: "20 KB", description: "Default; ~5K tokens" },
|
|
2059
|
+
{ value: "50", label: "50 KB", description: "~12.5K tokens" },
|
|
2060
|
+
{ value: "100", label: "100 KB", description: "~25K tokens" },
|
|
2061
|
+
],
|
|
2062
|
+
},
|
|
2063
|
+
},
|
|
2019
2064
|
|
|
2020
2065
|
"read.summarize.enabled": {
|
|
2021
2066
|
type: "boolean",
|
|
@@ -1950,9 +1950,16 @@ async function handleHandoff(
|
|
|
1950
1950
|
positionalSkill: string | undefined,
|
|
1951
1951
|
): Promise<StateCommandResult> {
|
|
1952
1952
|
const selectors = await resolveSelectors(args, cwd, positionalSkill, "handoff");
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
)
|
|
1953
|
+
// Serialize concurrent handoffs on a dedicated sentinel lock, NOT on the
|
|
1954
|
+
// derived `skill-active-state.json` cache. The inner transaction
|
|
1955
|
+
// (applyHandoffToActiveState / syncSkillActiveState -> rebuildActiveSnapshot)
|
|
1956
|
+
// re-locks that cache file, and `withFileLock` is not reentrant: holding the
|
|
1957
|
+
// active-state lock here made the inner rebuild self-contend and fail after
|
|
1958
|
+
// all retries whenever `cwd === process.cwd()` (the real CLI case). Pass
|
|
1959
|
+
// `{ cwd }` so the sentinel resolves against the handoff cwd rather than
|
|
1960
|
+
// `process.cwd()`.
|
|
1961
|
+
const handoffLock = path.join(sessionStateDir(cwd, selectors.gjcSessionId), "handoff");
|
|
1962
|
+
return withWorkflowStateLock(handoffLock, async () => handleHandoffUnlocked(args, cwd, positionalSkill), { cwd });
|
|
1956
1963
|
}
|
|
1957
1964
|
|
|
1958
1965
|
async function handleContract(
|
package/src/main.ts
CHANGED
|
@@ -23,6 +23,7 @@ import chalk from "chalk";
|
|
|
23
23
|
import type { Args } from "./cli/args";
|
|
24
24
|
import { processFileArguments } from "./cli/file-processor";
|
|
25
25
|
import { buildInitialMessage } from "./cli/initial-message";
|
|
26
|
+
import { resolveLaunchDisposition } from "./cli/launch-disposition";
|
|
26
27
|
import { runListModelsCommand } from "./cli/list-models";
|
|
27
28
|
import { selectSession } from "./cli/session-picker";
|
|
28
29
|
import { findConfigFile } from "./config";
|
|
@@ -282,7 +283,7 @@ export function resolveAcpStartupOptions(
|
|
|
282
283
|
}
|
|
283
284
|
|
|
284
285
|
async function readPipedInput(): Promise<string | undefined> {
|
|
285
|
-
if (process.stdin.isTTY
|
|
286
|
+
if (process.stdin.isTTY === true) return undefined;
|
|
286
287
|
try {
|
|
287
288
|
const text = await Bun.stdin.text();
|
|
288
289
|
if (text.trim().length === 0) return undefined;
|
|
@@ -292,6 +293,30 @@ async function readPipedInput(): Promise<string | undefined> {
|
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
295
|
|
|
296
|
+
async function readPipedInputIfReady(): Promise<string | undefined> {
|
|
297
|
+
if (process.stdin.isTTY === true) return undefined;
|
|
298
|
+
|
|
299
|
+
const ready = Promise.withResolvers<void>();
|
|
300
|
+
const onReadable = () => ready.resolve();
|
|
301
|
+
const onEnd = () => ready.resolve();
|
|
302
|
+
const onError = () => ready.resolve();
|
|
303
|
+
process.stdin.once("readable", onReadable);
|
|
304
|
+
process.stdin.once("end", onEnd);
|
|
305
|
+
process.stdin.once("error", onError);
|
|
306
|
+
await Promise.race([ready.promise, new Promise<void>(resolve => setImmediate(resolve))]);
|
|
307
|
+
process.stdin.removeListener("readable", onReadable);
|
|
308
|
+
process.stdin.removeListener("end", onEnd);
|
|
309
|
+
process.stdin.removeListener("error", onError);
|
|
310
|
+
|
|
311
|
+
const chunks: Buffer[] = [];
|
|
312
|
+
for (let chunk = process.stdin.read(); chunk !== null; chunk = process.stdin.read()) {
|
|
313
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
314
|
+
}
|
|
315
|
+
if (chunks.length === 0) return undefined;
|
|
316
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
317
|
+
return text.trim() ? text : undefined;
|
|
318
|
+
}
|
|
319
|
+
|
|
295
320
|
export interface InteractiveModeNotify {
|
|
296
321
|
kind: "warn" | "error" | "info";
|
|
297
322
|
message: string;
|
|
@@ -1014,6 +1039,7 @@ export interface RunRootCommandDependencies {
|
|
|
1014
1039
|
startupUpdate?: { check: () => Promise<string | undefined> };
|
|
1015
1040
|
initTheme?: typeof initTheme;
|
|
1016
1041
|
readPipedInput?: typeof readPipedInput;
|
|
1042
|
+
stdinIsTTY?: boolean;
|
|
1017
1043
|
runStartupCredentialAutoImportIfNeeded?: typeof runStartupCredentialAutoImportIfNeeded;
|
|
1018
1044
|
getChangelogForDisplay?: typeof getChangelogForDisplay;
|
|
1019
1045
|
createInteractiveMode?: CreateInteractiveMode;
|
|
@@ -1164,8 +1190,16 @@ export async function runRootCommand(
|
|
|
1164
1190
|
if (parsedArgs.noTitle || parsedArgs.mode === "acp") {
|
|
1165
1191
|
Bun.env.PI_NO_TITLE = "1";
|
|
1166
1192
|
}
|
|
1193
|
+
const hasPreparedInput = parsedArgs.messages.length > 0 || parsedArgs.fileArgs.length > 0;
|
|
1167
1194
|
const { pipedInput, fileText, fileImages } = await logger.time("prepareInitialMessage", async () => {
|
|
1168
|
-
const pipedInput =
|
|
1195
|
+
const pipedInput =
|
|
1196
|
+
parsedArgs.mode === "acp"
|
|
1197
|
+
? undefined
|
|
1198
|
+
: deps.readPipedInput
|
|
1199
|
+
? await deps.readPipedInput()
|
|
1200
|
+
: hasPreparedInput
|
|
1201
|
+
? await readPipedInputIfReady()
|
|
1202
|
+
: await readPipedInput();
|
|
1169
1203
|
if (parsedArgs.fileArgs.length === 0) {
|
|
1170
1204
|
return { pipedInput, fileText: undefined, fileImages: undefined };
|
|
1171
1205
|
}
|
|
@@ -1180,14 +1214,25 @@ export async function runRootCommand(
|
|
|
1180
1214
|
fileImages,
|
|
1181
1215
|
stdinContent: pipedInput,
|
|
1182
1216
|
});
|
|
1183
|
-
const
|
|
1217
|
+
const disposition = resolveLaunchDisposition({
|
|
1218
|
+
stdinIsTTY: deps.stdinIsTTY ?? process.stdin.isTTY,
|
|
1219
|
+
pipedInput,
|
|
1220
|
+
hasPreparedInput,
|
|
1221
|
+
print: Boolean(parsedArgs.print),
|
|
1222
|
+
mode: parsedArgs.mode,
|
|
1223
|
+
});
|
|
1224
|
+
if (disposition.nonInteractiveError) {
|
|
1225
|
+
process.stderr.write(`${chalk.red(disposition.nonInteractiveError)}\n`);
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
const autoPrint = disposition.autoPrint;
|
|
1184
1229
|
const startupUpdateRoute = classifyStartupUpdateRoute(parsedArgs, autoPrint);
|
|
1185
1230
|
const startupUpdate = new StartupUpdateOrchestrator(
|
|
1186
1231
|
startupUpdateRoute,
|
|
1187
1232
|
() => settingsInstance.get("startup.checkUpdate"),
|
|
1188
1233
|
deps.startupUpdate?.check ?? (() => checkForNewVersion(VERSION)),
|
|
1189
1234
|
);
|
|
1190
|
-
const isInteractive =
|
|
1235
|
+
const isInteractive = disposition.isInteractive;
|
|
1191
1236
|
const mode = parsedArgs.mode || "text";
|
|
1192
1237
|
|
|
1193
1238
|
// Initialize discovery system with settings for provider persistence
|
|
@@ -1319,6 +1364,7 @@ export async function runRootCommand(
|
|
|
1319
1364
|
sessionOptions.modelRegistry = modelRegistry;
|
|
1320
1365
|
sessionOptions.hasUI = isInteractive;
|
|
1321
1366
|
sessionOptions.notificationHostModeSupported = isInteractive;
|
|
1367
|
+
sessionOptions.sdkHostModeSupported = isInteractive;
|
|
1322
1368
|
sessionOptions.settings = settingsInstance;
|
|
1323
1369
|
const hasRootStartupProfile = Boolean(settingsInstance.get("modelProfile.default") || parsedArgs.mpreset);
|
|
1324
1370
|
|
|
@@ -1351,6 +1397,30 @@ export async function runRootCommand(
|
|
|
1351
1397
|
applyCliRuntimeApiKeyOverride(authStorage, parsedArgs.apiKey, sessionOptions.model);
|
|
1352
1398
|
}
|
|
1353
1399
|
|
|
1400
|
+
if (
|
|
1401
|
+
!deps.suppressProcessExit &&
|
|
1402
|
+
deps.createAgentSession === undefined &&
|
|
1403
|
+
parsedArgs.noSession === true &&
|
|
1404
|
+
!parsedArgs.continue &&
|
|
1405
|
+
parsedArgs.resume === undefined &&
|
|
1406
|
+
parsedArgs.fork === undefined &&
|
|
1407
|
+
!isInteractive &&
|
|
1408
|
+
mode !== "acp" &&
|
|
1409
|
+
!hasRootStartupProfile &&
|
|
1410
|
+
!sessionOptions.model &&
|
|
1411
|
+
!sessionOptions.modelPattern &&
|
|
1412
|
+
modelRegistry.getAvailable().length === 0
|
|
1413
|
+
) {
|
|
1414
|
+
process.stderr.write(`${chalk.red(`No models available. ${formatModelOnboardingGuidance()}`)}\n`);
|
|
1415
|
+
process.stderr.write(
|
|
1416
|
+
`${chalk.yellow(`\nAdvanced manual config remains available at ${ModelsConfigFile.path()}`)}\n`,
|
|
1417
|
+
);
|
|
1418
|
+
process.exitCode = 1;
|
|
1419
|
+
authStorage.close();
|
|
1420
|
+
stopThemeWatcher();
|
|
1421
|
+
await postmortem.cleanup();
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1354
1424
|
const createAgentSessionImpl = deps.createAgentSession ?? createAgentSession;
|
|
1355
1425
|
const createSession: CreateSessionForMain = async (options, context): Promise<CreateAgentSessionResult> => {
|
|
1356
1426
|
const result = await logger.time("createAgentSession", createAgentSessionImpl, options);
|
|
@@ -1365,10 +1435,16 @@ export async function runRootCommand(
|
|
|
1365
1435
|
};
|
|
1366
1436
|
|
|
1367
1437
|
if (mode === "acp") {
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1438
|
+
try {
|
|
1439
|
+
await (deps.runAcpMode ?? (await import("./modes/acp")).runAcpMode)({
|
|
1440
|
+
agentDir: settingsInstance.getAgentDir(),
|
|
1441
|
+
...(acpStartupOptions ? { startupOptions: acpStartupOptions } : {}),
|
|
1442
|
+
});
|
|
1443
|
+
} finally {
|
|
1444
|
+
authStorage.close();
|
|
1445
|
+
stopThemeWatcher();
|
|
1446
|
+
if (!deps.suppressProcessExit) await postmortem.cleanup();
|
|
1447
|
+
}
|
|
1372
1448
|
} else {
|
|
1373
1449
|
const { session, setToolUIContext, modelFallbackMessage, lspServers, mcpManager, eventBus } = await createSession(
|
|
1374
1450
|
sessionOptions,
|
|
@@ -1503,20 +1579,21 @@ export async function runRootCommand(
|
|
|
1503
1579
|
}
|
|
1504
1580
|
} else {
|
|
1505
1581
|
const runPrint = deps.runPrintMode ?? (await import("./modes/print-mode")).runPrintMode;
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1582
|
+
try {
|
|
1583
|
+
await runPrint(session, {
|
|
1584
|
+
mode,
|
|
1585
|
+
messages: parsedArgs.messages,
|
|
1586
|
+
initialMessage,
|
|
1587
|
+
initialImages,
|
|
1588
|
+
suppressProcessExit: deps.suppressProcessExit,
|
|
1589
|
+
});
|
|
1590
|
+
if ($env.PI_TIMING) {
|
|
1591
|
+
logger.printTimings();
|
|
1592
|
+
}
|
|
1593
|
+
} finally {
|
|
1594
|
+
stopThemeWatcher();
|
|
1595
|
+
authStorage.close();
|
|
1596
|
+
if (!deps.suppressProcessExit) await postmortem.cleanup();
|
|
1520
1597
|
}
|
|
1521
1598
|
}
|
|
1522
1599
|
}
|
|
@@ -248,6 +248,13 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
248
248
|
|
|
249
249
|
return this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol);
|
|
250
250
|
}
|
|
251
|
+
getForceFileSuggestions(
|
|
252
|
+
lines: string[],
|
|
253
|
+
cursorLine: number,
|
|
254
|
+
cursorCol: number,
|
|
255
|
+
): Promise<{ items: AutocompleteItem[]; prefix: string } | null> {
|
|
256
|
+
return this.#baseProvider.getForceFileSuggestions(lines, cursorLine, cursorCol);
|
|
257
|
+
}
|
|
251
258
|
|
|
252
259
|
applyCompletion(
|
|
253
260
|
lines: string[],
|
package/src/sdk/bus/index.ts
CHANGED
|
@@ -2741,6 +2741,8 @@ export function createNotificationsExtension(
|
|
|
2741
2741
|
/** Suppress auto-delivery for a GJC-spawned child under `sessionScope=primary`. */
|
|
2742
2742
|
spawnedByGjc?: boolean;
|
|
2743
2743
|
controller?: NotificationSessionController;
|
|
2744
|
+
/** Whether this host mode can own the root SDK endpoint. Default: true. */
|
|
2745
|
+
sdkHostModeSupported?: boolean;
|
|
2744
2746
|
|
|
2745
2747
|
onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
|
|
2746
2748
|
runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{ replyText: string }>;
|
|
@@ -2956,7 +2958,8 @@ export function createNotificationsExtension(
|
|
|
2956
2958
|
const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
|
|
2957
2959
|
const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
|
|
2958
2960
|
const notificationsEnabledForSession = controller.query(ctx).effectiveEnabled;
|
|
2959
|
-
const sdkEnabledForSession =
|
|
2961
|
+
const sdkEnabledForSession =
|
|
2962
|
+
(options.sdkHostModeSupported ?? true) && shouldHostSdk(settings, isNotificationEligibleContext(ctx));
|
|
2960
2963
|
const lifecycleRequired = lifecycleStartupCapability !== undefined;
|
|
2961
2964
|
const failLifecycleStartup = (
|
|
2962
2965
|
reason: "disabled" | "ineligible" | "failed",
|
package/src/sdk/host/host.ts
CHANGED
|
@@ -102,6 +102,7 @@ export class SessionSdkHost {
|
|
|
102
102
|
readonly reverse: ReverseLeaseRuntime;
|
|
103
103
|
readonly #options: SessionSdkHostOptions;
|
|
104
104
|
#started = false;
|
|
105
|
+
#stopPromise?: Promise<"stopped">;
|
|
105
106
|
#unsubscribe?: () => void;
|
|
106
107
|
#registration?: { writer: BrokerIndexWriter; generation: number };
|
|
107
108
|
|
|
@@ -154,16 +155,27 @@ export class SessionSdkHost {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
async stop(): Promise<"stopped" | "already"> {
|
|
158
|
+
if (this.#stopPromise) return this.#stopPromise;
|
|
157
159
|
if (!this.#started) return "already";
|
|
160
|
+
const stopPromise = this.#stopStartedHost();
|
|
161
|
+
this.#stopPromise = stopPromise;
|
|
162
|
+
try {
|
|
163
|
+
return await stopPromise;
|
|
164
|
+
} finally {
|
|
165
|
+
if (this.#stopPromise === stopPromise) this.#stopPromise = undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async #stopStartedHost(): Promise<"stopped"> {
|
|
158
170
|
this.#unsubscribe?.();
|
|
159
171
|
this.#unsubscribe = undefined;
|
|
160
|
-
this.#started = false;
|
|
161
172
|
if (this.#registration?.writer.unregister)
|
|
162
173
|
await this.#registration.writer.unregister({
|
|
163
174
|
sessionId: this.#options.sessionId,
|
|
164
175
|
stateRoot: this.#options.stateRoot,
|
|
165
176
|
endpointGeneration: this.events.generation,
|
|
166
177
|
});
|
|
178
|
+
this.#started = false;
|
|
167
179
|
return "stopped";
|
|
168
180
|
}
|
|
169
181
|
|
package/src/sdk/session.ts
CHANGED
|
@@ -390,6 +390,8 @@ export interface CreateAgentSessionOptions {
|
|
|
390
390
|
hasUI?: boolean;
|
|
391
391
|
/** Whether this host mode can own a notification session endpoint. Default: true. */
|
|
392
392
|
notificationHostModeSupported?: boolean;
|
|
393
|
+
/** Whether this host mode can own the root SDK endpoint. Default: true. */
|
|
394
|
+
sdkHostModeSupported?: boolean;
|
|
393
395
|
|
|
394
396
|
/**
|
|
395
397
|
* Opt-in OpenTelemetry instrumentation forwarded to the underlying Agent.
|
|
@@ -1763,7 +1765,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1763
1765
|
currentAgentType: options.currentAgentType,
|
|
1764
1766
|
spawnedByGjc,
|
|
1765
1767
|
}) ||
|
|
1766
|
-
shouldHostSdk(notificationCfg, isTopLevelSdkSession)
|
|
1768
|
+
(shouldHostSdk(notificationCfg, isTopLevelSdkSession) && (options.sdkHostModeSupported ?? true))
|
|
1767
1769
|
) {
|
|
1768
1770
|
inlineExtensions.push(async api => {
|
|
1769
1771
|
try {
|
|
@@ -1774,6 +1776,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1774
1776
|
settings,
|
|
1775
1777
|
controller: notificationSessionController,
|
|
1776
1778
|
spawnedByGjc,
|
|
1779
|
+
sdkHostModeSupported: options.sdkHostModeSupported,
|
|
1777
1780
|
runEphemeralTurn: async (promptText, signal) => {
|
|
1778
1781
|
if (!session) throw new Error("Ephemeral turns are unavailable.");
|
|
1779
1782
|
const { replyText } = await session.runEphemeralTurn({ promptText, signal });
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -605,6 +605,10 @@ async function spillLargeResultToArtifact(
|
|
|
605
605
|
toolName: string,
|
|
606
606
|
context: AgentToolContext | undefined,
|
|
607
607
|
): Promise<AgentToolResult> {
|
|
608
|
+
if (toolName === "read" && (result.details as { spillEligible?: boolean } | undefined)?.spillEligible !== true) {
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
|
|
608
612
|
const artifactCapability = artifactCapabilityForContext(context);
|
|
609
613
|
if (!artifactCapability) return result;
|
|
610
614
|
const { threshold, readThreshold, tailBytes, tailLines, headBytes } = getSpillConfig(context?.settings);
|
|
@@ -709,8 +713,8 @@ async function spillLargeResultToArtifact(
|
|
|
709
713
|
/**
|
|
710
714
|
* Absolute inline-size backstop enforced after {@link spillLargeResultToArtifact}.
|
|
711
715
|
*
|
|
712
|
-
* The threshold-based spill above has escape hatches: it
|
|
713
|
-
*
|
|
716
|
+
* The threshold-based spill above has escape hatches: it skips ineligible `read`
|
|
717
|
+
* results and results that already carry an `artifactId` (a tool may set
|
|
714
718
|
* partial truncation meta yet still emit oversized inline text). This backstop
|
|
715
719
|
* closes those gaps: when `tools.maxInlineResultBytes` is configured (> 0), any
|
|
716
720
|
* final result whose inline text exceeds the cap is force-saved to an artifact
|
package/src/tools/read.ts
CHANGED
|
@@ -95,6 +95,8 @@ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx"
|
|
|
95
95
|
|
|
96
96
|
const MAX_SUMMARY_BYTES = 2 * 1024 * 1024;
|
|
97
97
|
const MAX_SUMMARY_LINES = 20_000;
|
|
98
|
+
const RAW_COLLECTOR_MAX_BYTES = MAX_SUMMARY_BYTES;
|
|
99
|
+
|
|
98
100
|
/**
|
|
99
101
|
* Per-line column cap for file reads. Lines wider than the value of
|
|
100
102
|
* `tools.outputMaxColumns` are ellipsis-truncated at display time; the file
|
|
@@ -193,6 +195,14 @@ function formatSummaryElisionFooter(readPath: string, elidedSpans: number, elide
|
|
|
193
195
|
const linePart = elidedLines > 0 ? `${elidedLines} ${lineWord} across ` : "";
|
|
194
196
|
return `[${linePart}${elidedSpans} elided ${spanWord}; read ${readPath}:raw or a line range like ${readPath}:1-9999 for verbatim content]`;
|
|
195
197
|
}
|
|
198
|
+
|
|
199
|
+
function formatReceiptFooter(readPath: string, shownLines: number, totalLines: number, shownBytes: number): string {
|
|
200
|
+
return `[Showing first ${shownLines} of ${totalLines} lines (~${Math.ceil(shownBytes / 1024)} KiB); re-read ${readPath}:1-${totalLines} or ${readPath}:raw for the full file]`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function formatSummaryCapFooter(readPath: string, maxBytes: number, totalLines: number): string {
|
|
204
|
+
return `[Summary truncated at ${Math.ceil(maxBytes / 1024)} KiB; re-read ${readPath}:raw or ${readPath}:1-${totalLines} for the full source]`;
|
|
205
|
+
}
|
|
196
206
|
const READ_CHUNK_SIZE = 8 * 1024;
|
|
197
207
|
|
|
198
208
|
/**
|
|
@@ -532,6 +542,9 @@ export interface ReadToolDetails {
|
|
|
532
542
|
method?: string;
|
|
533
543
|
notes?: string[];
|
|
534
544
|
meta?: OutputMeta;
|
|
545
|
+
/** Whether this explicit content read may be spilled by output metadata. */
|
|
546
|
+
spillEligible?: boolean;
|
|
547
|
+
|
|
535
548
|
/** Raw text + start line for user-visible TUI rendering, set when content is text-like.
|
|
536
549
|
* Mirrors the same lines the model receives but without hashline/line-number prefixes,
|
|
537
550
|
* so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
|
|
@@ -816,10 +829,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
816
829
|
options: {
|
|
817
830
|
details?: ReadToolDetails;
|
|
818
831
|
sourcePath?: string;
|
|
832
|
+
spillEligible?: boolean;
|
|
833
|
+
receiptPath?: string;
|
|
819
834
|
sourceUrl?: string;
|
|
820
835
|
sourceInternal?: string;
|
|
821
836
|
entityLabel: string;
|
|
822
837
|
ignoreResultLimits?: boolean;
|
|
838
|
+
resultByteCeiling?: number;
|
|
823
839
|
raw?: boolean;
|
|
824
840
|
immutable?: boolean;
|
|
825
841
|
wrapUntrusted?: boolean;
|
|
@@ -871,9 +887,30 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
871
887
|
}
|
|
872
888
|
|
|
873
889
|
const endLine = endLineExpanded;
|
|
874
|
-
|
|
890
|
+
let selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
891
|
+
|
|
892
|
+
if (options.receiptPath) {
|
|
893
|
+
const budgetLines = this.session.settings.get("read.receiptBudgetLines");
|
|
894
|
+
const budgetBytes = this.session.settings.get("read.receiptBudgetBytes") * 1024;
|
|
895
|
+
const receiptLines: string[] = [];
|
|
896
|
+
let bytes = 0;
|
|
897
|
+
for (const line of allLines) {
|
|
898
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (receiptLines.length > 0 ? 1 : 0);
|
|
899
|
+
if (receiptLines.length >= budgetLines || bytes + lineBytes > budgetBytes) break;
|
|
900
|
+
receiptLines.push(line);
|
|
901
|
+
bytes += lineBytes;
|
|
902
|
+
}
|
|
903
|
+
selectedContent = receiptLines.join("\n");
|
|
904
|
+
}
|
|
875
905
|
const userLimitedLines = limit !== undefined ? endLine - startLine : undefined;
|
|
876
|
-
const truncation = ignoreResultLimits
|
|
906
|
+
const truncation = ignoreResultLimits
|
|
907
|
+
? noTruncResult(selectedContent)
|
|
908
|
+
: truncateHead(
|
|
909
|
+
selectedContent,
|
|
910
|
+
options.resultByteCeiling === undefined
|
|
911
|
+
? undefined
|
|
912
|
+
: { maxBytes: options.resultByteCeiling, maxLines: Number.MAX_SAFE_INTEGER },
|
|
913
|
+
);
|
|
877
914
|
|
|
878
915
|
const shouldAddHashLines = displayMode.hashLines;
|
|
879
916
|
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
@@ -927,7 +964,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
927
964
|
} else {
|
|
928
965
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
929
966
|
}
|
|
967
|
+
if (options.receiptPath && (truncation.truncated || countTextLines(selectedContent) < totalLines)) {
|
|
968
|
+
outputText += `\n\n${formatReceiptFooter(options.receiptPath, countTextLines(selectedContent), totalLines, Buffer.byteLength(selectedContent, "utf-8"))}`;
|
|
969
|
+
}
|
|
930
970
|
|
|
971
|
+
details.spillEligible = options.spillEligible === true && selectedContent.length > 0;
|
|
931
972
|
resultBuilder.text(options.wrapUntrusted ? wrapUntrustedContent(outputText) : outputText);
|
|
932
973
|
if (truncationInfo) {
|
|
933
974
|
resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
|
|
@@ -987,6 +1028,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
987
1028
|
}
|
|
988
1029
|
const finalText =
|
|
989
1030
|
notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
|
|
1031
|
+
details.spillEligible = parts.length > 0;
|
|
1032
|
+
|
|
990
1033
|
resultBuilder.text(finalText);
|
|
991
1034
|
return resultBuilder.done();
|
|
992
1035
|
}
|
|
@@ -1006,6 +1049,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1006
1049
|
signal: AbortSignal | undefined,
|
|
1007
1050
|
): Promise<{
|
|
1008
1051
|
outputText: string;
|
|
1052
|
+
contentLineCount: number;
|
|
1009
1053
|
columnTruncated: number;
|
|
1010
1054
|
bridgeResult?: AgentToolResult<ReadToolDetails>;
|
|
1011
1055
|
}> {
|
|
@@ -1027,7 +1071,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1027
1071
|
const firstText = bridgeResult.content.find((c): c is TextContent => c.type === "text");
|
|
1028
1072
|
if (firstText) firstText.text = `${notice}\n${firstText.text}`;
|
|
1029
1073
|
}
|
|
1030
|
-
return { outputText: "", columnTruncated: 0, bridgeResult };
|
|
1074
|
+
return { outputText: "", contentLineCount: 0, columnTruncated: 0, bridgeResult };
|
|
1031
1075
|
} catch (error) {
|
|
1032
1076
|
logger.warn("ACP fs readTextFile failed; falling back to disk", { path: absolutePath, error });
|
|
1033
1077
|
}
|
|
@@ -1040,6 +1084,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1040
1084
|
const blocks: string[] = [];
|
|
1041
1085
|
const notices: string[] = [];
|
|
1042
1086
|
let columnTruncated = 0;
|
|
1087
|
+
let contentLineCount = 0;
|
|
1043
1088
|
|
|
1044
1089
|
for (const range of ranges) {
|
|
1045
1090
|
const rangeStart = range.startLine - 1; // 0-indexed
|
|
@@ -1074,6 +1119,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1074
1119
|
}
|
|
1075
1120
|
}
|
|
1076
1121
|
|
|
1122
|
+
contentLineCount += collectedLines.length;
|
|
1077
1123
|
if (collectedLines.length > 0) {
|
|
1078
1124
|
getFileReadCache(this.session).recordContiguous(absolutePath, range.startLine, collectedLines);
|
|
1079
1125
|
}
|
|
@@ -1086,7 +1132,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1086
1132
|
if (notices.length > 0) {
|
|
1087
1133
|
outputText = outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n");
|
|
1088
1134
|
}
|
|
1089
|
-
return { outputText, columnTruncated };
|
|
1135
|
+
return { outputText, contentLineCount, columnTruncated };
|
|
1090
1136
|
}
|
|
1091
1137
|
|
|
1092
1138
|
async #readArchiveDirectory(
|
|
@@ -1369,6 +1415,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1369
1415
|
displayText: string;
|
|
1370
1416
|
elidedSpans: number;
|
|
1371
1417
|
elidedLines: number;
|
|
1418
|
+
capped: boolean;
|
|
1372
1419
|
} {
|
|
1373
1420
|
const displayMode = resolveFileDisplayMode(this.session);
|
|
1374
1421
|
const shouldAddHashLines = displayMode.hashLines;
|
|
@@ -1428,17 +1475,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1428
1475
|
|
|
1429
1476
|
const modelParts: string[] = [];
|
|
1430
1477
|
const displayParts: string[] = [];
|
|
1478
|
+
const capBytes = this.session.settings.get("read.summaryMaxBytes") * 1024;
|
|
1479
|
+
let usedBytes = 0;
|
|
1431
1480
|
let elidedSpans = 0;
|
|
1432
1481
|
let elidedLines = 0;
|
|
1482
|
+
let capDropped = false;
|
|
1433
1483
|
for (const unit of units) {
|
|
1484
|
+
let model: string;
|
|
1485
|
+
let display: string;
|
|
1486
|
+
let implicitElidedLines = 0;
|
|
1434
1487
|
if (unit.kind === "elided") {
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
elidedLines += unit.endLine - unit.startLine + 1;
|
|
1439
|
-
continue;
|
|
1440
|
-
}
|
|
1441
|
-
if (unit.kind === "merged") {
|
|
1488
|
+
model = display = "...";
|
|
1489
|
+
implicitElidedLines = unit.endLine - unit.startLine + 1;
|
|
1490
|
+
} else if (unit.kind === "merged") {
|
|
1442
1491
|
const formatted = formatMergedBraceLine(
|
|
1443
1492
|
unit.startLine,
|
|
1444
1493
|
unit.endLine,
|
|
@@ -1447,18 +1496,39 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1447
1496
|
shouldAddHashLines,
|
|
1448
1497
|
shouldAddLineNumbers,
|
|
1449
1498
|
);
|
|
1450
|
-
|
|
1451
|
-
|
|
1499
|
+
model = formatted.model;
|
|
1500
|
+
display = formatted.display;
|
|
1501
|
+
implicitElidedLines = Math.max(0, unit.endLine - unit.startLine - 1);
|
|
1502
|
+
} else {
|
|
1503
|
+
model = formatSingleLine(unit.line, unit.text, shouldAddHashLines, shouldAddLineNumbers);
|
|
1504
|
+
display = unit.text;
|
|
1505
|
+
}
|
|
1506
|
+
const separatorBytes = modelParts.length > 0 ? 1 : 0;
|
|
1507
|
+
if (usedBytes + separatorBytes + Buffer.byteLength(model, "utf-8") > capBytes) {
|
|
1508
|
+
capDropped = true;
|
|
1452
1509
|
elidedSpans++;
|
|
1453
|
-
|
|
1454
|
-
elidedLines += Math.max(0, unit.endLine - unit.startLine - 1);
|
|
1510
|
+
elidedLines += unit.kind === "line" ? 1 : unit.endLine - unit.startLine + 1;
|
|
1455
1511
|
continue;
|
|
1456
1512
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1513
|
+
usedBytes += separatorBytes + Buffer.byteLength(model, "utf-8");
|
|
1514
|
+
modelParts.push(model);
|
|
1515
|
+
displayParts.push(display);
|
|
1516
|
+
if (unit.kind === "elided" || unit.kind === "merged") {
|
|
1517
|
+
elidedSpans++;
|
|
1518
|
+
elidedLines += implicitElidedLines;
|
|
1519
|
+
}
|
|
1459
1520
|
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1521
|
+
if (capDropped) {
|
|
1522
|
+
modelParts.push("…");
|
|
1523
|
+
displayParts.push("…");
|
|
1524
|
+
}
|
|
1525
|
+
return {
|
|
1526
|
+
text: modelParts.join("\n"),
|
|
1527
|
+
displayText: displayParts.join("\n"),
|
|
1528
|
+
elidedSpans,
|
|
1529
|
+
elidedLines,
|
|
1530
|
+
capped: capDropped,
|
|
1531
|
+
};
|
|
1462
1532
|
}
|
|
1463
1533
|
|
|
1464
1534
|
async execute(
|
|
@@ -1649,25 +1719,38 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1649
1719
|
details: { resolvedPath: absolutePath },
|
|
1650
1720
|
sourcePath: absolutePath,
|
|
1651
1721
|
entityLabel: "notebook",
|
|
1722
|
+
spillEligible: parsed.kind !== "none",
|
|
1723
|
+
resultByteCeiling:
|
|
1724
|
+
parsed.kind !== "none"
|
|
1725
|
+
? Math.max(
|
|
1726
|
+
RAW_COLLECTOR_MAX_BYTES,
|
|
1727
|
+
this.session.settings.get("tools.readArtifactSpillThreshold") * 1024,
|
|
1728
|
+
)
|
|
1729
|
+
: undefined,
|
|
1730
|
+
receiptPath: parsed.kind === "none" ? localReadPath : undefined,
|
|
1652
1731
|
});
|
|
1653
1732
|
} else if (shouldConvertWithMarkit) {
|
|
1654
|
-
// Convert document via markit.
|
|
1655
1733
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1656
1734
|
if (result.ok) {
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1735
|
+
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1736
|
+
return this.#buildInMemoryTextResult(result.content, offset, limit, {
|
|
1737
|
+
details: { resolvedPath: absolutePath },
|
|
1738
|
+
sourcePath: absolutePath,
|
|
1739
|
+
entityLabel: "converted document",
|
|
1740
|
+
raw: isRawSelector(parsed),
|
|
1741
|
+
spillEligible: parsed.kind !== "none",
|
|
1742
|
+
resultByteCeiling:
|
|
1743
|
+
parsed.kind !== "none"
|
|
1744
|
+
? Math.max(
|
|
1745
|
+
RAW_COLLECTOR_MAX_BYTES,
|
|
1746
|
+
this.session.settings.get("tools.readArtifactSpillThreshold") * 1024,
|
|
1747
|
+
)
|
|
1748
|
+
: undefined,
|
|
1749
|
+
receiptPath: parsed.kind === "none" ? localReadPath : undefined,
|
|
1750
|
+
});
|
|
1670
1751
|
}
|
|
1752
|
+
content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
|
|
1753
|
+
details = { spillEligible: false };
|
|
1671
1754
|
} else {
|
|
1672
1755
|
if (
|
|
1673
1756
|
parsed.kind === "none" &&
|
|
@@ -1682,8 +1765,22 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1682
1765
|
renderedSummary.elidedSpans,
|
|
1683
1766
|
renderedSummary.elidedLines,
|
|
1684
1767
|
);
|
|
1685
|
-
const
|
|
1768
|
+
const summaryTotalLines = Math.max(...summary.segments.map(segment => segment.endLine));
|
|
1769
|
+
const modelText = [
|
|
1770
|
+
renderedSummary.text,
|
|
1771
|
+
footer,
|
|
1772
|
+
renderedSummary.capped
|
|
1773
|
+
? formatSummaryCapFooter(
|
|
1774
|
+
localReadPath,
|
|
1775
|
+
this.session.settings.get("read.summaryMaxBytes") * 1024,
|
|
1776
|
+
summaryTotalLines,
|
|
1777
|
+
)
|
|
1778
|
+
: "",
|
|
1779
|
+
]
|
|
1780
|
+
.filter(Boolean)
|
|
1781
|
+
.join("\n\n");
|
|
1686
1782
|
details = {
|
|
1783
|
+
spillEligible: false,
|
|
1687
1784
|
displayContent: { text: renderedSummary.displayText, startLine: 1 },
|
|
1688
1785
|
summary: {
|
|
1689
1786
|
lines: countTextLines(renderedSummary.text),
|
|
@@ -1710,7 +1807,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1710
1807
|
if (multiResult.bridgeResult) return multiResult.bridgeResult;
|
|
1711
1808
|
content = [{ type: "text", text: multiResult.outputText }];
|
|
1712
1809
|
sourcePath = absolutePath;
|
|
1713
|
-
details = {};
|
|
1810
|
+
details = { spillEligible: multiResult.contentLineCount > 0 };
|
|
1714
1811
|
if (multiResult.columnTruncated > 0) {
|
|
1715
1812
|
columnTruncated = multiResult.columnTruncated;
|
|
1716
1813
|
}
|
|
@@ -1750,13 +1847,25 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1750
1847
|
const startLine = requestedStart - leadingContext;
|
|
1751
1848
|
const startLineDisplay = startLine + 1;
|
|
1752
1849
|
|
|
1850
|
+
const isBareReceipt = parsed.kind === "none" && !content;
|
|
1851
|
+
const rawSelector = isRawSelector(parsed);
|
|
1753
1852
|
const DEFAULT_LIMIT = this.#defaultLimit;
|
|
1754
|
-
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
|
1755
|
-
const
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1853
|
+
const effectiveLimit = rawSelector ? Number.MAX_SAFE_INTEGER : (limit ?? DEFAULT_LIMIT);
|
|
1854
|
+
const receiptBudgetBytes = this.session.settings.get("read.receiptBudgetBytes") * 1024;
|
|
1855
|
+
const maxLinesToCollect = rawSelector
|
|
1856
|
+
? Number.MAX_SAFE_INTEGER
|
|
1857
|
+
: isBareReceipt
|
|
1858
|
+
? this.session.settings.get("read.receiptBudgetLines")
|
|
1859
|
+
: Math.min(effectiveLimit + leadingContext + trailingContext, DEFAULT_MAX_LINES);
|
|
1860
|
+
const selectedLineLimit = rawSelector ? null : effectiveLimit + leadingContext + trailingContext;
|
|
1861
|
+
const maxBytesForRead = rawSelector
|
|
1862
|
+
? Math.max(
|
|
1863
|
+
RAW_COLLECTOR_MAX_BYTES,
|
|
1864
|
+
this.session.settings.get("tools.readArtifactSpillThreshold") * 1024,
|
|
1865
|
+
)
|
|
1866
|
+
: isBareReceipt
|
|
1867
|
+
? receiptBudgetBytes
|
|
1868
|
+
: Math.max(DEFAULT_MAX_BYTES, maxLinesToCollect * 512);
|
|
1760
1869
|
|
|
1761
1870
|
const streamResult = await streamLinesFromFile(
|
|
1762
1871
|
absolutePath,
|
|
@@ -1793,7 +1902,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1793
1902
|
// verbatim bytes for paste-back-into-tool workflows. Total byte/line
|
|
1794
1903
|
// counts in `truncation` keep reflecting the source, not the trimmed
|
|
1795
1904
|
// view — column truncation surfaces separately via `.limits()`.
|
|
1796
|
-
|
|
1905
|
+
|
|
1797
1906
|
const shouldAddHashLines = !rawSelector && displayMode.hashLines;
|
|
1798
1907
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
1799
1908
|
if (!rawSelector && !shouldAddHashLines && maxColumns > 0) {
|
|
@@ -1855,7 +1964,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1855
1964
|
firstLineBytes,
|
|
1856
1965
|
)}, exceeds ${formatBytes(maxBytesForRead)} limit. Unable to display a valid UTF-8 snippet.]`;
|
|
1857
1966
|
}
|
|
1858
|
-
details = { truncation };
|
|
1967
|
+
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1968
|
+
|
|
1859
1969
|
sourcePath = absolutePath;
|
|
1860
1970
|
truncationInfo = {
|
|
1861
1971
|
result: truncation,
|
|
@@ -1863,7 +1973,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1863
1973
|
};
|
|
1864
1974
|
} else if (truncation.truncated) {
|
|
1865
1975
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
1866
|
-
details = { truncation };
|
|
1976
|
+
details = { truncation, spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1977
|
+
|
|
1867
1978
|
sourcePath = absolutePath;
|
|
1868
1979
|
truncationInfo = {
|
|
1869
1980
|
result: truncation,
|
|
@@ -1875,14 +1986,22 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1875
1986
|
|
|
1876
1987
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
1877
1988
|
outputText += `\n\n[${remaining} more lines in file. Use :${nextOffset} to continue]`;
|
|
1878
|
-
details = {};
|
|
1989
|
+
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1990
|
+
|
|
1879
1991
|
sourcePath = absolutePath;
|
|
1880
1992
|
} else {
|
|
1881
1993
|
// No truncation, no user limit exceeded
|
|
1882
1994
|
outputText = formatText(truncation.content, startLineDisplay);
|
|
1883
|
-
details = {};
|
|
1995
|
+
details = { spillEligible: !isBareReceipt && collectedLines.length > 0 };
|
|
1996
|
+
|
|
1884
1997
|
sourcePath = absolutePath;
|
|
1885
1998
|
}
|
|
1999
|
+
if (isBareReceipt && (truncation.truncated || collectedLines.length < totalFileLines)) {
|
|
2000
|
+
outputText += `\n\n${formatReceiptFooter(localReadPath, collectedLines.length, totalFileLines, collectedBytes)}`;
|
|
2001
|
+
}
|
|
2002
|
+
if (rawSelector && truncation.truncated) {
|
|
2003
|
+
outputText += `\n\n[Raw read capped at ${formatBytes(maxBytesForRead)} of ${formatBytes(fileSize)}; re-read a line range to narrow the view]`;
|
|
2004
|
+
}
|
|
1886
2005
|
|
|
1887
2006
|
if (capturedDisplayContent) {
|
|
1888
2007
|
details.displayContent = capturedDisplayContent;
|
|
@@ -2109,23 +2228,23 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2109
2228
|
}
|
|
2110
2229
|
throwIfAborted(signal);
|
|
2111
2230
|
|
|
2112
|
-
const
|
|
2113
|
-
const
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
return
|
|
2231
|
+
const receiptLines = this.session.settings.get("read.receiptBudgetLines");
|
|
2232
|
+
const receiptBytes = this.session.settings.get("read.receiptBudgetBytes") * 1024;
|
|
2233
|
+
const sourceLines = tree.totalLines <= 1 ? ["(empty directory)"] : tree.rendered.split("\n");
|
|
2234
|
+
const bodyLines: string[] = [];
|
|
2235
|
+
let bodyBytes = 0;
|
|
2236
|
+
for (const line of sourceLines) {
|
|
2237
|
+
const bytes = Buffer.byteLength(line, "utf-8") + (bodyLines.length > 0 ? 1 : 0);
|
|
2238
|
+
if (bodyLines.length >= receiptLines || bodyBytes + bytes > receiptBytes) break;
|
|
2239
|
+
bodyLines.push(line);
|
|
2240
|
+
bodyBytes += bytes;
|
|
2241
|
+
}
|
|
2242
|
+
let output = bodyLines.join("\n");
|
|
2243
|
+
if (bodyLines.length < sourceLines.length || tree.truncated) {
|
|
2244
|
+
output += `\n\n[Listing truncated at ${bodyLines.length} lines / ${Math.ceil(bodyBytes / 1024)} KiB; read a deeper subpath (e.g. ${absolutePath}/<subdir>) to narrow the view]`;
|
|
2245
|
+
}
|
|
2246
|
+
const details: ReadToolDetails = { isDirectory: true, spillEligible: false, resolvedPath: tree.rootPath };
|
|
2247
|
+
return toolResult(details).text(output).sourcePath(tree.rootPath).done();
|
|
2129
2248
|
}
|
|
2130
2249
|
}
|
|
2131
2250
|
|
package/src/tools/subagent.ts
CHANGED
|
@@ -17,6 +17,10 @@ const MAX_LIST_LIMIT = 50;
|
|
|
17
17
|
const RECEIPT_PREVIEW_WIDTH = 280;
|
|
18
18
|
const PREVIEW_WIDTH = 2_000;
|
|
19
19
|
const FULL_PREVIEW_WIDTH = 12_000;
|
|
20
|
+
const RECEIPT_PREVIEW_BYTES = 1_024;
|
|
21
|
+
const PREVIEW_BYTES = 8_192;
|
|
22
|
+
const FULL_PREVIEW_BYTES = 49_152;
|
|
23
|
+
|
|
20
24
|
const STEER_QUEUED_GUIDANCE =
|
|
21
25
|
"The steer message is queued for the subagent's next steering boundary and has not necessarily taken effect yet.";
|
|
22
26
|
|
|
@@ -783,7 +787,40 @@ function isSubagentJob(job: AsyncJob): boolean {
|
|
|
783
787
|
}
|
|
784
788
|
|
|
785
789
|
function sanitizeText(text: string, width: number): string {
|
|
786
|
-
return
|
|
790
|
+
return capCodePointsAndBytes(
|
|
791
|
+
truncateToWidth(replaceTabs(text), width, Ellipsis.Unicode),
|
|
792
|
+
width,
|
|
793
|
+
previewByteCap(width),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function previewByteCap(width: number): number {
|
|
798
|
+
return width === FULL_PREVIEW_WIDTH
|
|
799
|
+
? FULL_PREVIEW_BYTES
|
|
800
|
+
: width === PREVIEW_WIDTH
|
|
801
|
+
? PREVIEW_BYTES
|
|
802
|
+
: RECEIPT_PREVIEW_BYTES;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function capCodePointsAndBytes(text: string, maxCodePoints: number, maxBytes: number): string {
|
|
806
|
+
const ellipsis = "…";
|
|
807
|
+
const collapsed = text.replace(/…{2,}$/u, ellipsis);
|
|
808
|
+
const codePoints = [...collapsed];
|
|
809
|
+
if (codePoints.length <= maxCodePoints && Buffer.byteLength(collapsed) <= maxBytes) return collapsed;
|
|
810
|
+
|
|
811
|
+
const source = collapsed.endsWith(ellipsis) ? collapsed.slice(0, -ellipsis.length) : collapsed;
|
|
812
|
+
const ellipsisBytes = Buffer.byteLength(ellipsis);
|
|
813
|
+
let preview = "";
|
|
814
|
+
let previewBytes = 0;
|
|
815
|
+
let previewCodePoints = 0;
|
|
816
|
+
for (const codePoint of source) {
|
|
817
|
+
const codePointBytes = Buffer.byteLength(codePoint);
|
|
818
|
+
if (previewCodePoints + 1 + 1 > maxCodePoints || previewBytes + codePointBytes + ellipsisBytes > maxBytes) break;
|
|
819
|
+
preview += codePoint;
|
|
820
|
+
previewBytes += codePointBytes;
|
|
821
|
+
previewCodePoints++;
|
|
822
|
+
}
|
|
823
|
+
return `${preview}${ellipsis}`;
|
|
787
824
|
}
|
|
788
825
|
|
|
789
826
|
function previewJobOutput(
|
|
@@ -799,7 +836,11 @@ function previewJobOutput(
|
|
|
799
836
|
const width =
|
|
800
837
|
verbosity === "full" ? FULL_PREVIEW_WIDTH : verbosity === "preview" ? PREVIEW_WIDTH : RECEIPT_PREVIEW_WIDTH;
|
|
801
838
|
const normalized = replaceTabs(source.text);
|
|
802
|
-
const preview =
|
|
839
|
+
const preview = capCodePointsAndBytes(
|
|
840
|
+
truncateToWidth(normalized, width, Ellipsis.Unicode),
|
|
841
|
+
width,
|
|
842
|
+
previewByteCap(width),
|
|
843
|
+
);
|
|
803
844
|
return { type: source.type, preview, truncated: preview !== normalized };
|
|
804
845
|
}
|
|
805
846
|
|
|
@@ -160,11 +160,11 @@ async function resolveMentionPath(
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
|
-
* Default inline cap for auto-read file mentions,
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* Default inline cap for auto-read file mentions, aligned with the read-tool's
|
|
164
|
+
* 10 KiB bare-read receipt so an incidental `@path` mention injects a bounded
|
|
165
|
+
* snippet. Overridable per call.
|
|
166
166
|
*/
|
|
167
|
-
export const DEFAULT_FILE_MENTION_INLINE_BYTES =
|
|
167
|
+
export const DEFAULT_FILE_MENTION_INLINE_BYTES = 10 * 1024;
|
|
168
168
|
|
|
169
169
|
function buildTextOutput(textContent: string, maxInlineBytes?: number): { output: string; lineCount: number } {
|
|
170
170
|
const cap = maxInlineBytes && maxInlineBytes > 0 ? maxInlineBytes : DEFAULT_FILE_MENTION_INLINE_BYTES;
|