@oh-my-pi/pi-coding-agent 15.4.3 → 15.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -5
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-broker-cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +42 -1
- package/dist/types/edit/index.d.ts +2 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +8 -2
- package/dist/types/extensibility/hooks/types.d.ts +4 -0
- package/dist/types/hashline/executor.d.ts +6 -3
- package/dist/types/lsp/index.d.ts +9 -1
- package/dist/types/mcp/client.d.ts +2 -1
- package/dist/types/mcp/oauth-discovery.d.ts +4 -3
- package/dist/types/mcp/timeout.d.ts +9 -0
- package/dist/types/mcp/types.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/task/types.d.ts +4 -0
- package/dist/types/tools/approval.d.ts +46 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/ast-edit.d.ts +2 -0
- package/dist/types/tools/ast-grep.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +11 -1
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/calculator.d.ts +1 -0
- package/dist/types/tools/checkpoint.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +9 -1
- package/dist/types/tools/eval.d.ts +2 -0
- package/dist/types/tools/find.d.ts +10 -0
- package/dist/types/tools/gh.d.ts +2 -1
- package/dist/types/tools/hindsight-recall.d.ts +1 -0
- package/dist/types/tools/hindsight-reflect.d.ts +1 -0
- package/dist/types/tools/hindsight-retain.d.ts +1 -0
- package/dist/types/tools/inspect-image.d.ts +1 -0
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/tools/read.d.ts +1 -0
- package/dist/types/tools/recipe/index.d.ts +1 -0
- package/dist/types/tools/render-mermaid.d.ts +1 -0
- package/dist/types/tools/resolve.d.ts +1 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -0
- package/dist/types/tools/search.d.ts +1 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo-write.d.ts +1 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tools/yield.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/args.ts +14 -0
- package/src/cli/auth-broker-cli.ts +171 -22
- package/src/commands/auth-broker.ts +3 -0
- package/src/commands/launch.ts +16 -0
- package/src/config/mcp-schema.json +2 -2
- package/src/config/model-registry.ts +19 -4
- package/src/config/prompt-templates.ts +0 -125
- package/src/config/settings-schema.ts +59 -1
- package/src/config/settings.ts +2 -1
- package/src/dap/session.ts +35 -2
- package/src/discovery/builtin.ts +2 -2
- package/src/discovery/mcp-json.ts +1 -1
- package/src/edit/index.ts +26 -0
- package/src/edit/modes/patch.ts +1 -1
- package/src/edit/streaming.ts +12 -2
- package/src/exec/bash-executor.ts +6 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +18 -14
- package/src/extensibility/custom-tools/types.ts +16 -2
- package/src/extensibility/extensions/wrapper.ts +36 -1
- package/src/extensibility/hooks/types.ts +8 -1
- package/src/hashline/apply.ts +47 -2
- package/src/hashline/executor.ts +46 -24
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/edits.ts +82 -29
- package/src/lsp/index.ts +38 -1
- package/src/lsp/utils.ts +1 -1
- package/src/main.ts +6 -0
- package/src/mcp/client.ts +8 -6
- package/src/mcp/oauth-discovery.ts +120 -32
- package/src/mcp/oauth-flow.ts +34 -6
- package/src/mcp/timeout.ts +59 -0
- package/src/mcp/transports/http.ts +42 -44
- package/src/mcp/transports/stdio.ts +8 -5
- package/src/mcp/types.ts +1 -1
- package/src/modes/components/hook-editor.ts +11 -3
- package/src/modes/components/mcp-add-wizard.ts +6 -2
- package/src/modes/components/model-selector.ts +33 -11
- package/src/modes/controllers/command-controller.ts +6 -4
- package/src/modes/controllers/mcp-command-controller.ts +8 -4
- package/src/prompts/review-custom-request.md +22 -0
- package/src/prompts/review-headless-request.md +16 -0
- package/src/prompts/review-request.md +2 -3
- package/src/prompts/system/project-prompt.md +4 -0
- package/src/prompts/tools/debug.md +1 -0
- package/src/prompts/tools/find.md +4 -2
- package/src/prompts/tools/hashline.md +43 -93
- package/src/sdk.ts +47 -73
- package/src/session/agent-session.ts +93 -27
- package/src/session/streaming-output.ts +1 -1
- package/src/slash-commands/helpers/usage-report.ts +3 -1
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +19 -0
- package/src/task/render.ts +12 -2
- package/src/task/types.ts +4 -0
- package/src/tools/approval.ts +185 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-edit.ts +25 -1
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +69 -1
- package/src/tools/browser/tab-supervisor.ts +1 -1
- package/src/tools/browser.ts +15 -0
- package/src/tools/calculator.ts +1 -0
- package/src/tools/checkpoint.ts +2 -0
- package/src/tools/debug.ts +38 -0
- package/src/tools/eval.ts +15 -0
- package/src/tools/find.ts +17 -8
- package/src/tools/gh.ts +21 -1
- package/src/tools/hindsight-recall.ts +1 -0
- package/src/tools/hindsight-reflect.ts +1 -0
- package/src/tools/hindsight-retain.ts +1 -0
- package/src/tools/image-gen.ts +1 -0
- package/src/tools/inspect-image.ts +1 -0
- package/src/tools/irc.ts +1 -0
- package/src/tools/job.ts +1 -0
- package/src/tools/path-utils.ts +14 -1
- package/src/tools/read.ts +1 -0
- package/src/tools/recipe/index.ts +1 -0
- package/src/tools/render-mermaid.ts +1 -0
- package/src/tools/report-tool-issue.ts +1 -0
- package/src/tools/resolve.ts +1 -0
- package/src/tools/review.ts +1 -0
- package/src/tools/search-tool-bm25.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/ssh.ts +8 -0
- package/src/tools/todo-write.ts +1 -0
- package/src/tools/write.ts +12 -1
- package/src/tools/yield.ts +1 -0
- package/src/web/search/index.ts +2 -0
|
@@ -1,109 +1,59 @@
|
|
|
1
1
|
Your patch language is a compact, line-anchored edit format.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
<payload>
|
|
4
|
+
Patch payload is a series of hunks: `¶PATH#HASH` header followed by any number of operations. `HASH` should be copied as is from read/search. Missing? Re-`read`.
|
|
5
|
+
- No context rows, no gutters.
|
|
6
|
+
- NEVER prefix payload with diff syntax.
|
|
7
|
+
- NEVER restate unchanged lines "for context".
|
|
8
|
+
- Payload indentation is literal.
|
|
9
|
+
</payload>
|
|
6
10
|
|
|
7
11
|
<ops>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
A-B:PAYLOAD replace the inclusive range A..B with PAYLOAD
|
|
13
|
-
A:PAYLOAD shorthand for A-A:PAYLOAD
|
|
14
|
-
A-B! delete the inclusive range A..B (payload forbidden)
|
|
15
|
-
A! shorthand for A-A!
|
|
12
|
+
LINE↑PAYLOAD insert before (or BOF↑)
|
|
13
|
+
LINE↓PAYLOAD insert after (or EOF↓)
|
|
14
|
+
A-B:PAYLOAD replace A..B (or A: == A..A)
|
|
15
|
+
A-B! delete A..B (or A! == A..A)
|
|
16
16
|
</ops>
|
|
17
17
|
|
|
18
|
-
<payload>
|
|
19
|
-
- The first payload line is whatever follows the sigil on the op line. Additional payload lines follow on the next lines and append after the first.
|
|
20
|
-
- An empty inline IS an empty first line. So bare `A↓` / `A↑` insert one blank line; bare `A:` / `A-B:` replace with one blank line. `A↓\nfoo` inserts blank-then-`foo`, NOT just `foo`.
|
|
21
|
-
- Payload ends at the next op, next `¶PATH`, envelope marker, or EOF. Blank lines immediately before a next op or `¶PATH` are dropped; blank lines between content lines are preserved.
|
|
22
|
-
</payload>
|
|
23
|
-
|
|
24
18
|
<rules>
|
|
25
|
-
-
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
28
|
-
- Smallest op wins: add with `↑`/`↓`; replace with `:`; delete with `!`.
|
|
29
|
-
- Anchors reference the file as last read. ONE patch, ONE coordinate space — later ops still use original line numbers.
|
|
19
|
+
- **Payload is only what's NEW.** `:` replaces inside; `↑`/`↓` add at anchor. NEVER repeat anchor lines or neighbors.
|
|
20
|
+
- **Go small.** Add → `↑`/`↓`; replace → `:`; delete → `!`.
|
|
21
|
+
- **Line numbers are frozen references to what you have seen.** Later ops still use original line numbers.
|
|
30
22
|
</rules>
|
|
31
23
|
|
|
32
24
|
<common-failures>
|
|
33
|
-
- **NEVER replay past your range.** Stop before B+1; extend B if
|
|
34
|
-
- **
|
|
35
|
-
- **Read lines look like replace ops.** `84:content` already means "make line 84 equal to content" — don't echo a context line before it.
|
|
25
|
+
- **NEVER replay past your range.** Stop before B+1; extend B if needed.
|
|
26
|
+
- **Read lines look like replace ops.** `84:content` = "make line 84 content" — don't echo context before it.
|
|
36
27
|
- **NEVER fabricate file hashes.** Missing? Re-`read`.
|
|
37
|
-
- **`A!` deletes silently.** Deleting a line that closes/opens a block (`}`, `} else {`, `})`, `*/`) breaks structure with no parse error.
|
|
38
28
|
</common-failures>
|
|
39
29
|
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
{{hrefr 1}}:const TITLE = "Mrs";
|
|
55
|
-
|
|
56
|
-
# Replace a multiline statement — first line inline, rest below
|
|
57
|
-
¶mod.ts#1a2b
|
|
58
|
-
{{hrefr 3}}-{{hrefr 6}}: return [
|
|
59
|
-
"Mrs",
|
|
60
|
-
name?.trim() || "guest",
|
|
61
|
-
].join(" ");
|
|
62
|
-
|
|
63
|
-
# Insert ABOVE / BELOW a line
|
|
64
|
-
¶mod.ts#1a2b
|
|
65
|
-
{{hrefr 4}}↓ "Dr",
|
|
66
|
-
{{hrefr 5}}↑ "Dr",
|
|
67
|
-
|
|
68
|
-
# Delete one line / blank a line / insert a blank line
|
|
69
|
-
¶mod.ts#1a2b
|
|
70
|
-
{{hrefr 5}}!
|
|
71
|
-
{{hrefr 6}}:
|
|
72
|
-
{{hrefr 7}}↑
|
|
73
|
-
|
|
74
|
-
# Create a file / append to one (hash optional for boundary-only inserts)
|
|
75
|
-
¶new.ts
|
|
76
|
-
BOF↓export const done = true;
|
|
77
|
-
¶mod.ts
|
|
78
|
-
EOF↓export const done = true;
|
|
79
|
-
|
|
80
|
-
# Multi-file patch
|
|
81
|
-
¶src/a.ts#1a2b
|
|
82
|
-
12:const enabled = true;
|
|
83
|
-
¶src/b.ts#3c4d
|
|
84
|
-
20!
|
|
85
|
-
</examples>
|
|
30
|
+
<example>
|
|
31
|
+
```a.ts#1a2b
|
|
32
|
+
1:const X = "a";
|
|
33
|
+
2:export function f() { return X; }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
# replace, insert after, delete
|
|
37
|
+
```
|
|
38
|
+
¶a.ts#1a2b
|
|
39
|
+
1:const X = "b";
|
|
40
|
+
1↓const Y = "c";
|
|
41
|
+
2!
|
|
42
|
+
```
|
|
43
|
+
</example>
|
|
86
44
|
|
|
87
45
|
<anti-pattern>
|
|
88
|
-
# WRONG —
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# RIGHT — one-line insert
|
|
95
|
-
¶mod.ts#1a2b
|
|
96
|
-
{{hrefr 1}}↓const DEBUG = false;
|
|
97
|
-
|
|
98
|
-
# WRONG — bisects a multiline statement
|
|
99
|
-
¶mod.ts#1a2b
|
|
100
|
-
{{hrefr 4}}-{{hrefr 5}}: "Dr",
|
|
101
|
-
name?.trim() || "guest",
|
|
102
|
-
|
|
103
|
-
# RIGHT — widen to the full statement
|
|
104
|
-
¶mod.ts#1a2b
|
|
105
|
-
{{hrefr 3}}-{{hrefr 6}}: return [
|
|
106
|
-
"Dr",
|
|
107
|
-
name?.trim() || "guest",
|
|
108
|
-
].join(" ");
|
|
46
|
+
# WRONG — INSERT used to change a line (old line survives)
|
|
47
|
+
1↓const X = "b";
|
|
48
|
+
# WRONG — echoing read-style lines as context before the real op
|
|
49
|
+
1:const X = "a";
|
|
50
|
+
1-2:const X = "b";
|
|
51
|
+
export const Y = X;
|
|
109
52
|
</anti-pattern>
|
|
53
|
+
|
|
54
|
+
<critical>
|
|
55
|
+
- One op per range, ever.
|
|
56
|
+
- Pick op precisely. Update: `:`, add: `↑`/`↓`, remove: `!`.
|
|
57
|
+
- Payload is only what's NEW; never repeat anchor lines or neighbors.
|
|
58
|
+
- Anchor exactly; don't anchor neighbors.
|
|
59
|
+
</critical>
|
package/src/sdk.ts
CHANGED
|
@@ -60,7 +60,6 @@ import {
|
|
|
60
60
|
} from "./extensibility/custom-commands";
|
|
61
61
|
import { discoverAndLoadCustomTools } from "./extensibility/custom-tools";
|
|
62
62
|
import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
|
|
63
|
-
import { CustomToolAdapter } from "./extensibility/custom-tools/wrapper";
|
|
64
63
|
import {
|
|
65
64
|
discoverAndLoadExtensions,
|
|
66
65
|
type ExtensionContext,
|
|
@@ -343,6 +342,9 @@ export interface CreateAgentSessionOptions {
|
|
|
343
342
|
* `@opentelemetry/api` package returns a no-op tracer in that case.
|
|
344
343
|
*/
|
|
345
344
|
telemetry?: AgentTelemetryConfig;
|
|
345
|
+
|
|
346
|
+
/** Whether to auto-approve all tool calls (--auto-approve CLI flag). Default: false */
|
|
347
|
+
autoApprove?: boolean;
|
|
346
348
|
}
|
|
347
349
|
|
|
348
350
|
/** Result from createAgentSession */
|
|
@@ -835,7 +837,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
835
837
|
// buffer — so we can't rely on it to catch startup events for the extension runner.
|
|
836
838
|
const startupCredentialDisabledEvents: CredentialDisabledEvent[] = [];
|
|
837
839
|
let credentialDisabledTarget: ExtensionRunner | undefined;
|
|
838
|
-
|
|
840
|
+
const unsubscribeCredentialDisabled: (() => void) | undefined = authStorage.onCredentialDisabled(event => {
|
|
839
841
|
if (credentialDisabledTarget) {
|
|
840
842
|
// Discard return: any handler error is routed through runner.onError listeners.
|
|
841
843
|
void credentialDisabledTarget.emitCredentialDisabled(event);
|
|
@@ -1455,29 +1457,25 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1455
1457
|
}
|
|
1456
1458
|
}
|
|
1457
1459
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1460
|
+
// The runner is created unconditionally — even with zero extensions loaded — because the
|
|
1461
|
+
// `ExtensionToolWrapper` installed below is the only place the per-tool approval gate runs.
|
|
1462
|
+
// A conditional runner means the approval system silently disappears for users with no
|
|
1463
|
+
// extensions, contradicting non-yolo `tools.approvalMode` settings without feedback.
|
|
1464
|
+
// (Today `createAutoresearchExtension` is unconditionally pushed below, so this scenario
|
|
1465
|
+
// is unreachable; the unconditional construction makes that invariant explicit instead of
|
|
1466
|
+
// implicit, so a future change to make autoresearch optional cannot silently re-open the hole.)
|
|
1467
|
+
const extensionRunner: ExtensionRunner = new ExtensionRunner(
|
|
1468
|
+
extensionsResult.extensions,
|
|
1469
|
+
extensionsResult.runtime,
|
|
1470
|
+
cwd,
|
|
1471
|
+
sessionManager,
|
|
1472
|
+
modelRegistry,
|
|
1473
|
+
);
|
|
1468
1474
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
void extensionRunner.emitCredentialDisabled(event);
|
|
1474
|
-
}
|
|
1475
|
-
} else {
|
|
1476
|
-
// No runner to forward to; release our subscription. The embedder's own
|
|
1477
|
-
// onCredentialDisabled (if any) keeps firing through its own subscription.
|
|
1478
|
-
startupCredentialDisabledEvents.length = 0;
|
|
1479
|
-
unsubscribeCredentialDisabled?.();
|
|
1480
|
-
unsubscribeCredentialDisabled = undefined;
|
|
1475
|
+
credentialDisabledTarget = extensionRunner;
|
|
1476
|
+
for (const event of startupCredentialDisabledEvents.splice(0)) {
|
|
1477
|
+
// Discard return: any handler error is routed through runner.onError listeners.
|
|
1478
|
+
void extensionRunner.emitCredentialDisabled(event);
|
|
1481
1479
|
}
|
|
1482
1480
|
|
|
1483
1481
|
const getSessionContext = () => ({
|
|
@@ -1490,38 +1488,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1490
1488
|
session.abort();
|
|
1491
1489
|
},
|
|
1492
1490
|
settings,
|
|
1491
|
+
autoApprove: options.autoApprove ?? false,
|
|
1493
1492
|
});
|
|
1494
1493
|
const toolContextStore = new ToolContextStore(getSessionContext);
|
|
1495
1494
|
|
|
1496
|
-
const registeredTools = extensionRunner
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
return { definition, extensionPath: "<sdk>" };
|
|
1506
|
-
}) ?? []),
|
|
1507
|
-
];
|
|
1508
|
-
wrappedExtensionTools = wrapRegisteredTools(allCustomTools, extensionRunner);
|
|
1509
|
-
} else {
|
|
1510
|
-
// Without extension runner: wrap CustomTools directly with CustomToolAdapter
|
|
1511
|
-
// ToolDefinition items require ExtensionContext and cannot be used without a runner
|
|
1512
|
-
const customToolContext = (): CustomToolContext => ({
|
|
1513
|
-
sessionManager,
|
|
1514
|
-
modelRegistry,
|
|
1515
|
-
model: agent?.state.model,
|
|
1516
|
-
isIdle: () => !session?.isStreaming,
|
|
1517
|
-
hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
|
|
1518
|
-
abort: () => session?.abort(),
|
|
1519
|
-
settings,
|
|
1520
|
-
});
|
|
1521
|
-
wrappedExtensionTools = (options.customTools ?? [])
|
|
1522
|
-
.filter(isCustomTool)
|
|
1523
|
-
.map(tool => CustomToolAdapter.wrap(tool, customToolContext));
|
|
1524
|
-
}
|
|
1495
|
+
const registeredTools = extensionRunner.getAllRegisteredTools();
|
|
1496
|
+
const allCustomTools = [
|
|
1497
|
+
...registeredTools,
|
|
1498
|
+
...(options.customTools?.map(tool => {
|
|
1499
|
+
const definition = isCustomTool(tool) ? customToolToDefinition(tool) : tool;
|
|
1500
|
+
return { definition, extensionPath: "<sdk>" };
|
|
1501
|
+
}) ?? []),
|
|
1502
|
+
];
|
|
1503
|
+
const wrappedExtensionTools: Tool[] = wrapRegisteredTools(allCustomTools, extensionRunner);
|
|
1525
1504
|
|
|
1526
1505
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
1527
1506
|
const toolRegistry = new Map<string, Tool>();
|
|
@@ -1537,10 +1516,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1537
1516
|
for (const tool of wrappedExtensionTools) {
|
|
1538
1517
|
toolRegistry.set(tool.name, tool);
|
|
1539
1518
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1519
|
+
// Wrap every tool with `ExtensionToolWrapper` so the per-tool approval gate runs on every
|
|
1520
|
+
// call site, regardless of whether any user extensions are loaded. See the runner-construction
|
|
1521
|
+
// comment above for the safety invariant this enforces.
|
|
1522
|
+
for (const tool of toolRegistry.values()) {
|
|
1523
|
+
toolRegistry.set(tool.name, new ExtensionToolWrapper(tool, extensionRunner));
|
|
1544
1524
|
}
|
|
1545
1525
|
if (model?.provider === "cursor") {
|
|
1546
1526
|
toolRegistry.delete("edit");
|
|
@@ -1564,7 +1544,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1564
1544
|
})) as unknown as AgentTool | null;
|
|
1565
1545
|
if (!sshTool) return null;
|
|
1566
1546
|
const wrapped = wrapToolWithMetaNotice(sshTool);
|
|
1567
|
-
return
|
|
1547
|
+
return new ExtensionToolWrapper(wrapped, extensionRunner) as AgentTool;
|
|
1568
1548
|
};
|
|
1569
1549
|
|
|
1570
1550
|
let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
|
|
@@ -1824,21 +1804,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1824
1804
|
if (!obfuscator?.hasSecrets()) return converted;
|
|
1825
1805
|
return obfuscateMessages(obfuscator, converted);
|
|
1826
1806
|
};
|
|
1827
|
-
const transformContext =
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
: undefined;
|
|
1837
|
-
const onResponse: SimpleStreamOptions["onResponse"] | undefined = extensionRunner
|
|
1838
|
-
? async (response, model) => {
|
|
1839
|
-
await extensionRunner.emitAfterProviderResponse(response, model);
|
|
1840
|
-
}
|
|
1841
|
-
: undefined;
|
|
1807
|
+
const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
|
|
1808
|
+
return await extensionRunner.emitContext(messages);
|
|
1809
|
+
};
|
|
1810
|
+
const onPayload = async (payload: unknown, _model?: Model) => {
|
|
1811
|
+
return await extensionRunner.emitBeforeProviderRequest(payload);
|
|
1812
|
+
};
|
|
1813
|
+
const onResponse: SimpleStreamOptions["onResponse"] = async (response, model) => {
|
|
1814
|
+
await extensionRunner.emitAfterProviderResponse(response, model);
|
|
1815
|
+
};
|
|
1842
1816
|
|
|
1843
1817
|
const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
|
|
1844
1818
|
toolContextStore.setUIContext(uiContext, hasUI);
|
|
@@ -898,7 +898,7 @@ export class AgentSession {
|
|
|
898
898
|
* combined with `Date.now()` so tags stay unique even across rapid
|
|
899
899
|
* same-tick enqueues. */
|
|
900
900
|
#customDisplayTagCounter = 0;
|
|
901
|
-
#postPromptTasks = new Set<Promise<
|
|
901
|
+
#postPromptTasks = new Set<Promise<unknown>>();
|
|
902
902
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
903
903
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
904
904
|
#postPromptTasksAbortController = new AbortController();
|
|
@@ -1786,12 +1786,19 @@ export class AgentSession {
|
|
|
1786
1786
|
|
|
1787
1787
|
const compactionTask = this.#checkCompaction(msg);
|
|
1788
1788
|
this.#trackPostPromptTask(compactionTask);
|
|
1789
|
-
await compactionTask;
|
|
1789
|
+
const compactionDeferredHandoff = await compactionTask;
|
|
1790
1790
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
1791
1791
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
1792
1792
|
if (hasToolCalls) {
|
|
1793
1793
|
return;
|
|
1794
1794
|
}
|
|
1795
|
+
// When checkCompaction scheduled a deferred handoff, skip the rewind/todo passes:
|
|
1796
|
+
// any reminder we append here would race the handoff's session reset, and
|
|
1797
|
+
// #scheduleAgentContinue would start a fresh streaming turn alongside the handoff
|
|
1798
|
+
// LLM call (visible as "Auto-handoff" loader + an assistant message still streaming).
|
|
1799
|
+
if (compactionDeferredHandoff) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1795
1802
|
if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
|
|
1796
1803
|
if (this.#enforceRewindBeforeYield()) {
|
|
1797
1804
|
return;
|
|
@@ -1840,7 +1847,7 @@ export class AgentSession {
|
|
|
1840
1847
|
this.#postPromptTasksPromise = undefined;
|
|
1841
1848
|
}
|
|
1842
1849
|
|
|
1843
|
-
#trackPostPromptTask(task: Promise<
|
|
1850
|
+
#trackPostPromptTask(task: Promise<unknown>): void {
|
|
1844
1851
|
this.#postPromptTasks.add(task);
|
|
1845
1852
|
this.#ensurePostPromptTasksPromise();
|
|
1846
1853
|
void task
|
|
@@ -1889,8 +1896,17 @@ export class AgentSession {
|
|
|
1889
1896
|
}): void {
|
|
1890
1897
|
this.#schedulePostPromptTask(
|
|
1891
1898
|
async () => {
|
|
1899
|
+
// Defense in depth: if compaction/handoff slipped onto the post-prompt queue
|
|
1900
|
+
// alongside us (e.g. via a scheduler we don't own), refuse to start a fresh
|
|
1901
|
+
// streaming turn — agent.continue() here would race the handoff's session
|
|
1902
|
+
// reset. The first-class fix is in #checkCompaction/the agent_end handler,
|
|
1903
|
+
// but this guard catches anything that bypasses that path.
|
|
1904
|
+
if (this.isCompacting || this.isGeneratingHandoff) {
|
|
1905
|
+
options?.onSkip?.();
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1892
1908
|
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
1893
|
-
options
|
|
1909
|
+
options?.onSkip?.();
|
|
1894
1910
|
return;
|
|
1895
1911
|
}
|
|
1896
1912
|
try {
|
|
@@ -2756,6 +2772,21 @@ export class AgentSession {
|
|
|
2756
2772
|
} catch (error) {
|
|
2757
2773
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
2758
2774
|
}
|
|
2775
|
+
// Abort post-prompt work so the drain below can complete. Without this, a
|
|
2776
|
+
// deferred-handoff task that has already advanced into
|
|
2777
|
+
// `await this.handoff(...) → generateHandoff(...)` keeps awaiting a live LLM stream
|
|
2778
|
+
// — Promise.allSettled() in #cancelPostPromptTasks then waits forever, freezing
|
|
2779
|
+
// /exit and Ctrl+C-double-tap. The post-prompt task's own AbortSignal does not
|
|
2780
|
+
// propagate into the inner handoff/compaction controllers, so we abort them
|
|
2781
|
+
// explicitly. agent.abort() is needed for an agent.continue() that may have
|
|
2782
|
+
// raced the deferred handoff (its streaming loop is awaited by the wrapper IIFE).
|
|
2783
|
+
//
|
|
2784
|
+
// Tool work (bash/eval/python) is NOT aborted here — those have their own
|
|
2785
|
+
// dispose paths and shared kernels are contractually allowed to survive a
|
|
2786
|
+
// session's dispose.
|
|
2787
|
+
this.abortRetry();
|
|
2788
|
+
this.abortCompaction();
|
|
2789
|
+
this.agent.abort();
|
|
2759
2790
|
await this.#cancelPostPromptTasks();
|
|
2760
2791
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
2761
2792
|
// leak its background bash/task work into the parent's manager. Only
|
|
@@ -4050,10 +4081,13 @@ export class AgentSession {
|
|
|
4050
4081
|
);
|
|
4051
4082
|
}
|
|
4052
4083
|
|
|
4053
|
-
// Check if we need to compact before sending (catches aborted responses)
|
|
4084
|
+
// Check if we need to compact before sending (catches aborted responses). Run
|
|
4085
|
+
// inline (allowDefer=false) so the handoff/maintenance fully settles before this
|
|
4086
|
+
// prompt's agent loop starts — otherwise a deferred handoff would fire on the
|
|
4087
|
+
// next microtask alongside the new turn.
|
|
4054
4088
|
const lastAssistant = this.#findLastAssistantMessage();
|
|
4055
4089
|
if (lastAssistant && !options?.skipCompactionCheck) {
|
|
4056
|
-
await this.#checkCompaction(lastAssistant, false);
|
|
4090
|
+
await this.#checkCompaction(lastAssistant, false, false);
|
|
4057
4091
|
}
|
|
4058
4092
|
|
|
4059
4093
|
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
@@ -5602,10 +5636,23 @@ export class AgentSession {
|
|
|
5602
5636
|
*
|
|
5603
5637
|
* @param assistantMessage The assistant message to check
|
|
5604
5638
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
5639
|
+
* @param allowDefer If true, threshold-driven handoff strategy may schedule itself as a
|
|
5640
|
+
* deferred post-prompt task instead of running inline. Callers running inside the
|
|
5641
|
+
* `agent_end` handler set this to true so `session.prompt()` resolves cleanly; callers
|
|
5642
|
+
* on the pre-prompt path (where the next agent turn is about to start) set it to false
|
|
5643
|
+
* to avoid racing the deferred handoff against the new turn.
|
|
5644
|
+
* @returns true when a deferred handoff was scheduled. Callers MUST then skip any
|
|
5645
|
+
* subsequent `#scheduleAgentContinue` / reminder appends for this turn — the
|
|
5646
|
+
* handoff will replace session state and a concurrent `agent.continue()` would
|
|
5647
|
+
* stream into the soon-to-be-discarded session.
|
|
5605
5648
|
*/
|
|
5606
|
-
async #checkCompaction(
|
|
5649
|
+
async #checkCompaction(
|
|
5650
|
+
assistantMessage: AssistantMessage,
|
|
5651
|
+
skipAbortedCheck = true,
|
|
5652
|
+
allowDefer = true,
|
|
5653
|
+
): Promise<boolean> {
|
|
5607
5654
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
5608
|
-
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
|
|
5655
|
+
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return false;
|
|
5609
5656
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
5610
5657
|
const generation = this.#promptGeneration;
|
|
5611
5658
|
// Skip overflow check if the message came from a different model.
|
|
@@ -5634,22 +5681,22 @@ export class AgentSession {
|
|
|
5634
5681
|
if (promoted) {
|
|
5635
5682
|
// Retry on the promoted (larger) model without compacting
|
|
5636
5683
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
5637
|
-
return;
|
|
5684
|
+
return false;
|
|
5638
5685
|
}
|
|
5639
5686
|
|
|
5640
5687
|
// No promotion target available fall through to compaction
|
|
5641
5688
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
5642
5689
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
5643
|
-
await this.#runAutoCompaction("overflow", true);
|
|
5690
|
+
await this.#runAutoCompaction("overflow", true, false, allowDefer);
|
|
5644
5691
|
}
|
|
5645
|
-
return;
|
|
5692
|
+
return false;
|
|
5646
5693
|
}
|
|
5647
5694
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
5648
|
-
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
5695
|
+
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
5649
5696
|
|
|
5650
5697
|
// Case 2: Threshold - turn succeeded but context is getting large
|
|
5651
5698
|
// Skip if this was an error (non-overflow errors don't have usage data)
|
|
5652
|
-
if (assistantMessage.stopReason === "error") return;
|
|
5699
|
+
if (assistantMessage.stopReason === "error") return false;
|
|
5653
5700
|
const pruneResult = await this.#pruneToolOutputs();
|
|
5654
5701
|
let contextTokens = calculateContextTokens(assistantMessage.usage);
|
|
5655
5702
|
if (pruneResult) {
|
|
@@ -5659,9 +5706,10 @@ export class AgentSession {
|
|
|
5659
5706
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
5660
5707
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
5661
5708
|
if (!promoted) {
|
|
5662
|
-
await this.#runAutoCompaction("threshold", false);
|
|
5709
|
+
return await this.#runAutoCompaction("threshold", false, false, allowDefer);
|
|
5663
5710
|
}
|
|
5664
5711
|
}
|
|
5712
|
+
return false;
|
|
5665
5713
|
}
|
|
5666
5714
|
#assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
|
|
5667
5715
|
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
@@ -6352,17 +6400,34 @@ export class AgentSession {
|
|
|
6352
6400
|
|
|
6353
6401
|
/**
|
|
6354
6402
|
* Internal: Run auto-compaction with events.
|
|
6403
|
+
*
|
|
6404
|
+
* @param allowDefer If true (default), threshold-driven handoff strategy is allowed to
|
|
6405
|
+
* schedule itself as a deferred post-prompt task and return `true` immediately. The
|
|
6406
|
+
* caller MUST treat that as "compaction will happen async — do not also schedule
|
|
6407
|
+
* `agent.continue()` for this turn", otherwise the deferred handoff races a fresh
|
|
6408
|
+
* streaming turn (the symptom: "Auto-handoff" loader + assistant message still
|
|
6409
|
+
* streaming). Callers on a path that is about to start a new agent turn (e.g.
|
|
6410
|
+
* the pre-prompt check in `#promptWithMessage`) pass `false` to force inline
|
|
6411
|
+
* execution so the handoff completes before the new turn begins.
|
|
6412
|
+
* @returns true when a deferred handoff was scheduled. Inline runs always return false.
|
|
6355
6413
|
*/
|
|
6356
6414
|
async #runAutoCompaction(
|
|
6357
6415
|
reason: "overflow" | "threshold" | "idle",
|
|
6358
6416
|
willRetry: boolean,
|
|
6359
6417
|
deferred = false,
|
|
6360
|
-
|
|
6418
|
+
allowDefer = true,
|
|
6419
|
+
): Promise<boolean> {
|
|
6361
6420
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6362
|
-
if (compactionSettings.strategy === "off") return;
|
|
6363
|
-
if (reason !== "idle" && !compactionSettings.enabled) return;
|
|
6421
|
+
if (compactionSettings.strategy === "off") return false;
|
|
6422
|
+
if (reason !== "idle" && !compactionSettings.enabled) return false;
|
|
6364
6423
|
const generation = this.#promptGeneration;
|
|
6365
|
-
if (
|
|
6424
|
+
if (
|
|
6425
|
+
!deferred &&
|
|
6426
|
+
allowDefer &&
|
|
6427
|
+
reason !== "overflow" &&
|
|
6428
|
+
reason !== "idle" &&
|
|
6429
|
+
compactionSettings.strategy === "handoff"
|
|
6430
|
+
) {
|
|
6366
6431
|
this.#schedulePostPromptTask(
|
|
6367
6432
|
async signal => {
|
|
6368
6433
|
await Promise.resolve();
|
|
@@ -6371,7 +6436,7 @@ export class AgentSession {
|
|
|
6371
6436
|
},
|
|
6372
6437
|
{ generation },
|
|
6373
6438
|
);
|
|
6374
|
-
return;
|
|
6439
|
+
return true;
|
|
6375
6440
|
}
|
|
6376
6441
|
|
|
6377
6442
|
let action: "context-full" | "handoff" =
|
|
@@ -6400,7 +6465,7 @@ export class AgentSession {
|
|
|
6400
6465
|
aborted: true,
|
|
6401
6466
|
willRetry: false,
|
|
6402
6467
|
});
|
|
6403
|
-
return;
|
|
6468
|
+
return false;
|
|
6404
6469
|
}
|
|
6405
6470
|
logger.warn("Auto-handoff returned no document; falling back to context-full maintenance", {
|
|
6406
6471
|
reason,
|
|
@@ -6418,7 +6483,7 @@ export class AgentSession {
|
|
|
6418
6483
|
if (!autoCompactionSignal.aborted && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
6419
6484
|
this.#scheduleAutoContinuePrompt(generation);
|
|
6420
6485
|
}
|
|
6421
|
-
return;
|
|
6486
|
+
return false;
|
|
6422
6487
|
}
|
|
6423
6488
|
}
|
|
6424
6489
|
|
|
@@ -6431,7 +6496,7 @@ export class AgentSession {
|
|
|
6431
6496
|
willRetry: false,
|
|
6432
6497
|
skipped: true,
|
|
6433
6498
|
});
|
|
6434
|
-
return;
|
|
6499
|
+
return false;
|
|
6435
6500
|
}
|
|
6436
6501
|
|
|
6437
6502
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
@@ -6444,7 +6509,7 @@ export class AgentSession {
|
|
|
6444
6509
|
willRetry: false,
|
|
6445
6510
|
skipped: true,
|
|
6446
6511
|
});
|
|
6447
|
-
return;
|
|
6512
|
+
return false;
|
|
6448
6513
|
}
|
|
6449
6514
|
|
|
6450
6515
|
const pathEntries = this.sessionManager.getBranch();
|
|
@@ -6466,7 +6531,7 @@ export class AgentSession {
|
|
|
6466
6531
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
6467
6532
|
});
|
|
6468
6533
|
}
|
|
6469
|
-
return;
|
|
6534
|
+
return false;
|
|
6470
6535
|
}
|
|
6471
6536
|
|
|
6472
6537
|
let hookCompaction: CompactionResult | undefined;
|
|
@@ -6490,7 +6555,7 @@ export class AgentSession {
|
|
|
6490
6555
|
aborted: true,
|
|
6491
6556
|
willRetry: false,
|
|
6492
6557
|
});
|
|
6493
|
-
return;
|
|
6558
|
+
return false;
|
|
6494
6559
|
}
|
|
6495
6560
|
|
|
6496
6561
|
if (hookResult?.compaction) {
|
|
@@ -6621,7 +6686,7 @@ export class AgentSession {
|
|
|
6621
6686
|
aborted: true,
|
|
6622
6687
|
willRetry: false,
|
|
6623
6688
|
});
|
|
6624
|
-
return;
|
|
6689
|
+
return false;
|
|
6625
6690
|
}
|
|
6626
6691
|
|
|
6627
6692
|
this.sessionManager.appendCompaction(
|
|
@@ -6692,7 +6757,7 @@ export class AgentSession {
|
|
|
6692
6757
|
aborted: true,
|
|
6693
6758
|
willRetry: false,
|
|
6694
6759
|
});
|
|
6695
|
-
return;
|
|
6760
|
+
return false;
|
|
6696
6761
|
}
|
|
6697
6762
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
6698
6763
|
await this.#emitSessionEvent({
|
|
@@ -6711,6 +6776,7 @@ export class AgentSession {
|
|
|
6711
6776
|
this.#autoCompactionAbortController = undefined;
|
|
6712
6777
|
}
|
|
6713
6778
|
}
|
|
6779
|
+
return false;
|
|
6714
6780
|
}
|
|
6715
6781
|
|
|
6716
6782
|
/**
|
|
@@ -9,7 +9,7 @@ import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
|
9
9
|
|
|
10
10
|
export const DEFAULT_MAX_LINES = 3000;
|
|
11
11
|
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
12
|
-
export const DEFAULT_MAX_COLUMN =
|
|
12
|
+
export const DEFAULT_MAX_COLUMN = 512; // Max chars per grep match line
|
|
13
13
|
|
|
14
14
|
const NL = "\n";
|
|
15
15
|
const ELLIPSIS = "…";
|
|
@@ -56,7 +56,9 @@ function renderUsageReports(reports: UsageReport[], nowMs: number): string {
|
|
|
56
56
|
lines.push(`- ${limit.label}${tier}${window ? ` — ${window}` : ""}`);
|
|
57
57
|
lines.push(` ${formatUsageReportAccount(report, limit, index)}: ${formatUsageAmount(limit)}`);
|
|
58
58
|
lines.push(` ${renderAsciiBar(limit.amount.usedFraction)}`);
|
|
59
|
-
if (limit.window?.resetsAt
|
|
59
|
+
if (limit.window?.resetsAt && limit.window.resetsAt > nowMs) {
|
|
60
|
+
lines.push(` resets in ${formatDuration(limit.window.resetsAt - nowMs)}`);
|
|
61
|
+
}
|
|
60
62
|
if (limit.notes && limit.notes.length > 0) lines.push(` ${limit.notes.join(" • ")}`);
|
|
61
63
|
}
|
|
62
64
|
}
|