@oh-my-pi/pi-coding-agent 16.2.4 → 16.2.6
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 +37 -0
- package/dist/cli.js +3211 -3231
- package/dist/types/cli/bench-cli.d.ts +6 -1
- package/dist/types/commands/bench.d.ts +8 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/edit/index.d.ts +1 -0
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/edit/snapshot-details.d.ts +33 -0
- package/dist/types/mcp/oauth-flow.d.ts +4 -6
- package/dist/types/modes/components/status-line/types.d.ts +10 -0
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/session/agent-session.d.ts +11 -0
- package/dist/types/session/session-manager.d.ts +3 -4
- package/dist/types/task/provider-concurrency.d.ts +40 -0
- package/dist/types/utils/git.d.ts +11 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/cli/args.ts +32 -1
- package/src/cli/bench-cli.ts +89 -21
- package/src/cli/web-search-cli.ts +6 -1
- package/src/cli/worktree-cli.ts +8 -5
- package/src/commands/bench.ts +10 -1
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-discovery.ts +98 -20
- package/src/config/model-registry.ts +13 -6
- package/src/config/settings-schema.ts +5 -2
- package/src/edit/hashline/execute.ts +15 -9
- package/src/edit/index.ts +19 -6
- package/src/edit/modes/patch.ts +3 -2
- package/src/edit/modes/replace.ts +3 -2
- package/src/edit/renderer.ts +4 -0
- package/src/edit/snapshot-details.ts +77 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/oauth-flow.ts +10 -8
- package/src/mcp/transports/stdio.ts +9 -17
- package/src/modes/components/status-line/component.ts +29 -2
- package/src/modes/components/status-line/segments.ts +22 -8
- package/src/modes/components/status-line/types.ts +7 -0
- package/src/modes/controllers/event-controller.ts +17 -8
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/tool-args-reveal.ts +100 -22
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/bench.md +4 -10
- package/src/prompts/tools/irc.md +19 -29
- package/src/prompts/tools/job.md +8 -14
- package/src/prompts/tools/lsp.md +19 -30
- package/src/prompts/tools/task.md +42 -62
- package/src/sdk.ts +14 -5
- package/src/session/agent-session.ts +107 -13
- package/src/session/session-listing.ts +9 -8
- package/src/session/session-loader.ts +98 -3
- package/src/session/session-manager.ts +34 -4
- package/src/subprocess/worker-client.ts +12 -4
- package/src/task/executor.ts +4 -62
- package/src/task/provider-concurrency.ts +100 -0
- package/src/task/worktree.ts +13 -4
- package/src/task/yield-assembly.ts +27 -39
- package/src/tools/path-utils.ts +4 -2
- package/src/tools/read.ts +11 -1
- package/src/utils/git.ts +13 -0
- package/src/web/search/index.ts +14 -8
- package/src/web/search/providers/duckduckgo.ts +136 -78
- package/src/web/search/providers/gemini.ts +268 -185
- package/src/web/search/types.ts +1 -1
package/src/prompts/tools/lsp.md
CHANGED
|
@@ -1,39 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
Symbol-aware code intelligence from language servers — the accurate path for navigation, refactors, and diagnostics where text search or edits would miss callsites.
|
|
2
2
|
|
|
3
3
|
<operations>
|
|
4
|
-
- `
|
|
5
|
-
- `definition
|
|
6
|
-
- `
|
|
7
|
-
- `
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- `
|
|
11
|
-
- `
|
|
12
|
-
- `rename_file
|
|
13
|
-
- `code_actions`: list quick-fixes/refactors/import actions; apply one when `apply: true` + `query` matches title or index
|
|
14
|
-
- `status`: active language servers
|
|
15
|
-
- `capabilities`: per-server capabilities
|
|
16
|
-
- `request`: raw LSP request — `query` = method name (e.g. `rust-analyzer/expandMacro`, `workspace/executeCommand`); `payload` = JSON params
|
|
17
|
-
- `reload`: restart one server (via `file`) or all (`file: "*"`)
|
|
18
|
-
</operations>
|
|
4
|
+
Position-based — pass `file` + `line` + `symbol` (substring on that line; append `#N` for the Nth match, e.g. `kind#2`):
|
|
5
|
+
- `definition`, `type_definition`, `implementation`, `references`, `hover` — standard LSP lookups
|
|
6
|
+
- `rename` — rename the symbol everywhere; **applies by default**, `apply: false` previews; needs `new_name`
|
|
7
|
+
- `code_actions` — quick-fixes/refactors/imports at that position; lists by default (`query` filters by kind, e.g. `quickfix`, `source.organizeImports`), **applies one only with `apply: true` + `query`** (then `query` = action title substring or numeric index)
|
|
8
|
+
|
|
9
|
+
File / workspace:
|
|
10
|
+
- `diagnostics` — errors/warnings for a path, a glob (`src/**/*.ts`), or the whole workspace (`file: "*"`)
|
|
11
|
+
- `symbols` — `file` lists that file's symbols; `file: "*"` + `query` searches the workspace
|
|
12
|
+
- `rename_file` — move `file` → `new_name` on disk AND rewrite imports/references through the server; applies by default
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
24
|
-
|
|
25
|
-
- `new_name`: required for `rename` (new identifier) and `rename_file` (destination path)
|
|
26
|
-
- `apply`: apply edits for rename/rename_file/code_actions (default true for rename/rename_file; code_actions list mode unless true)
|
|
27
|
-
- `payload`: JSON params for `action: request`
|
|
28
|
-
- `timeout`: seconds
|
|
29
|
-
</parameters>
|
|
14
|
+
Servers:
|
|
15
|
+
- `status`, `capabilities` — what's running / per-server capabilities (one via `file`, all via `*`)
|
|
16
|
+
- `reload` — restart one server (`file`) or all (`*`); `reload *` also re-reads project LSP config
|
|
17
|
+
- `request` — raw escape hatch: `query` = method (`rust-analyzer/expandMacro`, `workspace/executeCommand`), `payload` = JSON params (else auto-built from `file`/`line`/`symbol`)
|
|
18
|
+
</operations>
|
|
30
19
|
|
|
31
20
|
<caution>
|
|
32
|
-
-
|
|
21
|
+
- `line` is 1-indexed. Project-aware `definition`/`references`/`rename` ERROR without `symbol` rather than guess the wrong identifier; a missing match or out-of-range `#N` is an explicit error, never a silent fallback.
|
|
33
22
|
</caution>
|
|
34
23
|
|
|
35
24
|
<critical>
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
25
|
+
- Symbol-aware work (rename, references, definition/type/impl, code actions) MUST use `lsp` whenever a server is available — it follows shadowing, re-exports, and cross-file usages that text tools miss.
|
|
26
|
+
- NEVER do a cross-file rename with `ast_edit`, `sed`, or hand edits when `lsp` `rename`/`rename_file` can — text renames silently drop callsites.
|
|
27
|
+
- Reach for `code_actions` on imports, quick-fixes, and server-known refactors before editing by hand.
|
|
39
28
|
</critical>
|
|
@@ -1,91 +1,71 @@
|
|
|
1
|
-
{{#if asyncEnabled}}{{#if batchEnabled}}
|
|
1
|
+
{{#if asyncEnabled}}{{#if batchEnabled}}Delegate work to background subagents by passing multiple items in a single `tasks[]` batch.{{else}}Delegate work to ONE background subagent per call.{{/if}}
|
|
2
|
+
Execution does not block your turn: you receive agent and job IDs immediately, and the final results deliver themselves when the subagents finish.{{else}}{{#if batchEnabled}}Run subagents synchronously by passing items in a `tasks[]` batch.{{else}}Run ONE subagent synchronously per call.{{/if}}
|
|
3
|
+
Execution blocks your turn: the call only returns once the work is completely finished.{{/if}}
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
|
|
5
|
+
# Delegation Strategy
|
|
6
|
+
- **Maximize parallelism:** Break work into the widest possible {{#if batchEnabled}}array of `tasks[]`{{else}}set of parallel `task` calls{{/if}}. NEVER serialize work that can run concurrently. Tasks touching different files or independent refactors should run in parallel; agents resolve their own file collisions live.
|
|
7
|
+
- **Sequence only when necessary:** The only reason to run A before B is if B strictly requires A's output to function (e.g., a core API contract or schema migration). {{#if ircEnabled}}If the missing piece is small, run them in parallel and have B ask A via `irc`!{{/if}}
|
|
8
|
+
- **Role matching:** Assign each subagent a specific `role` (e.g. "Security Reviewer", "DB Migrator"). Do not spawn generic workers.
|
|
9
|
+
- **No overhead:** Each assignment MUST instruct its agent to skip formatters, linters, and project-wide test suites. You will run those once at the end.
|
|
10
|
+
- **Do your own thinking:** NEVER assign reasoning, architecture, or design to `quick_task` or `explore`. They are for mechanical lookups only. Keep hard decisions in your own context or use `task`, `plan`, or `oracle`.
|
|
11
|
+
- **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `explore`) when the affected files are genuinely unknown.
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
{{/if}}
|
|
11
|
-
{{#if ircEnabled}}
|
|
12
|
-
- Coordinate with agents via `irc` using their ids. Agents reach you and their siblings live the same way.
|
|
13
|
-
{{/if}}
|
|
14
|
-
|
|
15
|
-
<parameters>
|
|
16
|
-
- `agent`: agent type to spawn
|
|
13
|
+
# Inputs
|
|
14
|
+
- `agent`: The base agent type to use (e.g., `task`, `explore`).
|
|
17
15
|
{{#if batchEnabled}}
|
|
18
|
-
- `context`:
|
|
19
|
-
- `tasks`:
|
|
20
|
-
- `assignment`:
|
|
21
|
-
- `id`: stable
|
|
22
|
-
- `description`: UI label only
|
|
23
|
-
- `role`: specialist
|
|
16
|
+
- `context`: Shared project state, constraints, and contracts. Applies to the entire batch; do not duplicate this background into individual tasks.
|
|
17
|
+
- `tasks[]`: Array of subagents to spawn.
|
|
18
|
+
- `assignment`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
|
|
19
|
+
- `id`: A stable CamelCase identifier (≤32 chars). Generated automatically if omitted.
|
|
20
|
+
- `description`: A UI label only; the subagent NEVER sees it.
|
|
21
|
+
- `role`: The specialist this subagent embodies. Tailor per spawn; do not clone a generic worker.
|
|
24
22
|
{{#if isolationEnabled}}
|
|
25
|
-
- `isolated`:
|
|
23
|
+
- `isolated`: Run in a dedicated worktree and return patches. Isolated agents are destroyed upon completion and cannot be addressed afterward.
|
|
26
24
|
{{/if}}
|
|
27
25
|
{{else}}
|
|
28
|
-
- `
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
26
|
+
- `assignment`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
|
|
27
|
+
- `id`: A stable CamelCase identifier (≤32 chars). Generated automatically if omitted.
|
|
28
|
+
- `description`: A UI label only; the subagent NEVER sees it.
|
|
29
|
+
- `role`: The specialist this subagent embodies. Tailor per spawn; do not clone a generic worker.
|
|
32
30
|
{{#if isolationEnabled}}
|
|
33
|
-
- `isolated`:
|
|
31
|
+
- `isolated`: Run in a dedicated worktree and return patches. Isolated agents are destroyed upon completion and cannot be addressed afterward.
|
|
34
32
|
{{/if}}
|
|
35
33
|
{{/if}}
|
|
36
|
-
</parameters>
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates, formatters, and project-wide build/test/lint. You run them once at the end across the union of changed files.
|
|
41
|
-
- No globs, no "update all", no package-wide scope. Fan out.
|
|
42
|
-
- **Tailor every spawn with a `role`.** A role naming the specialist (e.g. "Parser edge-case tester", "SSE backpressure specialist") makes a sharper agent than a bare generic `task`/`quick_task` worker; decompose into named specialists, never clones of one generic worker. A role-less generic spawn is the exception.
|
|
43
|
-
- NEVER slow down or serialize because tasks might overlap on some files. Agents resolve collisions among themselves in real time.
|
|
44
|
-
- Subagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in {{#if batchEnabled}}`context` or the item's `assignment`{{else}}the `assignment`{{/if}}.
|
|
35
|
+
# Context and Communication
|
|
36
|
+
Subagents start blank. They have no access to your conversation history.
|
|
45
37
|
{{#if batchEnabled}}
|
|
46
|
-
-
|
|
38
|
+
- Pass large payloads using `local://<path>` URIs, never inline text.
|
|
47
39
|
{{else}}
|
|
48
|
-
-
|
|
40
|
+
- *Note: The single-spawn shape has no `context` field.* Write shared project state ONCE to a `local://` file (e.g., `local://ctx.md`) and reference that URL in your assignments. Pass large payloads using `local://<path>` URIs, never inline text.
|
|
49
41
|
{{/if}}
|
|
50
|
-
- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.
|
|
51
|
-
- **Read-only agents**: Agents tagged READ-ONLY (e.g. `explore`) have no edit/write/command tools. NEVER hand them an assignment that requires changing files or running commands. Use them to investigate and report back; do the edits yourself or delegate to a writing agent (`task`, `oracle`, `designer`).
|
|
52
|
-
- **No reasoning offload**: NEVER offload reasoning, analysis, design, or decision-making to `quick_task` or `explore` — they run minimal-effort / small models for mechanical lookups and data collection only. Keep judgment and synthesis in your own context; delegate hard thinking to `task`, `plan`, or `oracle`.
|
|
53
|
-
</rules>
|
|
54
|
-
|
|
55
|
-
<parallelization>
|
|
56
42
|
{{#if ircEnabled}}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Test: can task B run correctly without seeing A's output? If no, sequence A → B.
|
|
62
|
-
Sequential when one task produces a contract (types, API, schema, core module) the other consumes.
|
|
63
|
-
Parallel when tasks touch disjoint files or are independent refactors/tests.
|
|
43
|
+
- Once spawned, coordinate with live agents via `irc` using their IDs. If task B depends on task A, B SHOULD message A directly.
|
|
44
|
+
{{/if}}
|
|
45
|
+
{{#if asyncEnabled}}
|
|
46
|
+
- If you run out of things to do and are genuinely blocked waiting for a subagent, use `job poll`. Use `job cancel` only for stalled work.
|
|
64
47
|
{{/if}}
|
|
65
|
-
{{#if ircEnabled}}Sequenced follow-ups SHOULD message the agent that produced the prerequisite — it already holds the context.{{/if}}
|
|
66
|
-
</parallelization>
|
|
67
48
|
|
|
49
|
+
# Format Contracts
|
|
68
50
|
{{#if batchEnabled}}
|
|
69
|
-
|
|
70
|
-
# Goal ←
|
|
71
|
-
# Constraints ←
|
|
72
|
-
# Contract ←
|
|
73
|
-
</context-fmt>
|
|
51
|
+
The `context` field MUST follow this format:
|
|
52
|
+
# Goal ← what the batch accomplishes
|
|
53
|
+
# Constraints ← rules and session decisions
|
|
54
|
+
# Contract ← shared interfaces
|
|
74
55
|
{{/if}}
|
|
75
56
|
|
|
76
|
-
|
|
57
|
+
The `assignment` field MUST follow this format:
|
|
77
58
|
# Target ← exact files and symbols; explicit non-goals
|
|
78
59
|
# Change ← step-by-step add/remove/rename; APIs and patterns
|
|
79
60
|
# Acceptance ← observable result; no project-wide commands
|
|
80
|
-
</assignment-fmt>
|
|
81
61
|
|
|
82
|
-
|
|
62
|
+
# Available Agents
|
|
83
63
|
{{#if spawningDisabled}}
|
|
84
|
-
Agent spawning is disabled
|
|
64
|
+
Agent spawning is currently disabled.
|
|
85
65
|
{{else}}
|
|
86
66
|
{{#list agents join="\n"}}
|
|
87
|
-
|
|
67
|
+
### {{name}}{{#if readOnly}} (READ-ONLY: no edit/write/command tools){{/if}}
|
|
88
68
|
{{description}}
|
|
69
|
+
{{#if readOnly}}Use ONLY for investigation and reporting; do the edits yourself or assign them to a writing agent.{{/if}}
|
|
89
70
|
{{/list}}
|
|
90
71
|
{{/if}}
|
|
91
|
-
</agents>
|
package/src/sdk.ts
CHANGED
|
@@ -130,6 +130,7 @@ import {
|
|
|
130
130
|
loadProjectContextFiles as loadContextFilesInternal,
|
|
131
131
|
} from "./system-prompt";
|
|
132
132
|
import { AgentOutputManager } from "./task/output-manager";
|
|
133
|
+
import { wrapStreamFnWithProviderConcurrency } from "./task/provider-concurrency";
|
|
133
134
|
import {
|
|
134
135
|
AUTO_THINKING,
|
|
135
136
|
type ConfiguredThinkingLevel,
|
|
@@ -2535,11 +2536,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2535
2536
|
// One-shot launch-latency marker: fired the first time the loop dispatches
|
|
2536
2537
|
// a chat request to the provider transport. See onFirstChatDispatch.
|
|
2537
2538
|
let notifyFirstChatDispatch = options.onFirstChatDispatch;
|
|
2538
|
-
// Shared, settings-aware stream wrapper used by
|
|
2539
|
-
//
|
|
2540
|
-
// sticky-routing variants, antigravity endpoint routing,
|
|
2541
|
-
// and the loop guard consistent across every
|
|
2542
|
-
|
|
2539
|
+
// Shared, settings-aware stream wrapper used by the main agent, advisor,
|
|
2540
|
+
// and side-channel requests (`/btw`, `/omfg`, IRC auto-replies, handoff).
|
|
2541
|
+
// Keeps OpenRouter sticky-routing variants, antigravity endpoint routing,
|
|
2542
|
+
// in-flight caps, and the loop guard consistent across every provider call
|
|
2543
|
+
// the session drives. Wrapped in a per-provider concurrency limiter so
|
|
2544
|
+
// each LLM HTTP request — not the whole subagent lifecycle — holds the
|
|
2545
|
+
// slot, preventing the nested-spawn deadlock from issue #3749.
|
|
2546
|
+
const settingsAwareStreamFn = wrapStreamFnWithProviderConcurrency(
|
|
2547
|
+
settings,
|
|
2548
|
+
createSettingsAwareStreamFn(settings),
|
|
2549
|
+
);
|
|
2543
2550
|
agent = new Agent({
|
|
2544
2551
|
initialState: {
|
|
2545
2552
|
systemPrompt,
|
|
@@ -2709,7 +2716,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2709
2716
|
transformProviderContext,
|
|
2710
2717
|
onPayload,
|
|
2711
2718
|
onResponse,
|
|
2719
|
+
sideStreamFn: settingsAwareStreamFn,
|
|
2712
2720
|
advisorStreamFn: settingsAwareStreamFn,
|
|
2721
|
+
preferWebsockets: preferOpenAICodexWebsockets,
|
|
2713
2722
|
convertToLlm: convertToLlmFinal,
|
|
2714
2723
|
rebuildSystemPrompt,
|
|
2715
2724
|
reloadSshTool,
|
|
@@ -534,6 +534,13 @@ export interface AgentSessionConfig {
|
|
|
534
534
|
* inherits this so its requests undergo the same shaping as the main turn.
|
|
535
535
|
*/
|
|
536
536
|
transformProviderContext?: (context: Context, model: Model) => Context | Promise<Context>;
|
|
537
|
+
/**
|
|
538
|
+
* Stream wrapper passed to side-channel requests (`/btw`, `/omfg`, IRC
|
|
539
|
+
* auto-replies, and handoff generation) so they apply the same provider
|
|
540
|
+
* shaping and host-level request wrappers as normal agent turns. Defaults
|
|
541
|
+
* to plain `streamSimple` when omitted.
|
|
542
|
+
*/
|
|
543
|
+
sideStreamFn?: StreamFn;
|
|
537
544
|
/**
|
|
538
545
|
* Stream wrapper passed to the advisor agent so its requests apply the
|
|
539
546
|
* session's `providers.openrouterVariant`, `providers.antigravityEndpoint`,
|
|
@@ -542,6 +549,8 @@ export interface AgentSessionConfig {
|
|
|
542
549
|
* main agent. Defaults to plain `streamSimple` when omitted.
|
|
543
550
|
*/
|
|
544
551
|
advisorStreamFn?: StreamFn;
|
|
552
|
+
/** Hint that OpenAI Codex requests should prefer websocket transport when supported. */
|
|
553
|
+
preferWebsockets?: boolean;
|
|
545
554
|
/** Provider payload hook used by the active session request path */
|
|
546
555
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
547
556
|
/** Provider response hook used by the active session request path */
|
|
@@ -1459,7 +1468,9 @@ export class AgentSession {
|
|
|
1459
1468
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
1460
1469
|
#onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
|
1461
1470
|
#transformProviderContext: ((context: Context, model: Model) => Context | Promise<Context>) | undefined;
|
|
1471
|
+
#sideStreamFn: StreamFn;
|
|
1462
1472
|
#advisorStreamFn: StreamFn | undefined;
|
|
1473
|
+
#preferWebsockets: boolean | undefined;
|
|
1463
1474
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
1464
1475
|
#rebuildSystemPrompt:
|
|
1465
1476
|
| ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
|
|
@@ -1788,7 +1799,9 @@ export class AgentSession {
|
|
|
1788
1799
|
this.#requestedToolNames = config.requestedToolNames;
|
|
1789
1800
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
1790
1801
|
this.#transformProviderContext = config.transformProviderContext;
|
|
1802
|
+
this.#sideStreamFn = config.sideStreamFn ?? streamSimple;
|
|
1791
1803
|
this.#advisorStreamFn = config.advisorStreamFn;
|
|
1804
|
+
this.#preferWebsockets = config.preferWebsockets;
|
|
1792
1805
|
this.#onPayload = config.onPayload;
|
|
1793
1806
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
1794
1807
|
// Avoid wrapping in an `async` closure when no user callback is configured: the
|
|
@@ -2130,6 +2143,7 @@ export class AgentSession {
|
|
|
2130
2143
|
sessionId: advisorSessionId,
|
|
2131
2144
|
promptCacheKey: advisorSessionId,
|
|
2132
2145
|
providerSessionState: this.#providerSessionState,
|
|
2146
|
+
preferWebsockets: this.#preferWebsockets,
|
|
2133
2147
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
2134
2148
|
streamFn: this.#advisorStreamFn,
|
|
2135
2149
|
onPayload: this.#onPayload,
|
|
@@ -2615,6 +2629,11 @@ export class AgentSession {
|
|
|
2615
2629
|
return this.#providerSessionState;
|
|
2616
2630
|
}
|
|
2617
2631
|
|
|
2632
|
+
/** Hint forwarded to provider calls that support websocket transport. */
|
|
2633
|
+
get preferWebsockets(): boolean | undefined {
|
|
2634
|
+
return this.#preferWebsockets;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2618
2637
|
getHindsightSessionState(): HindsightSessionState | undefined {
|
|
2619
2638
|
return this.#hindsightSessionState;
|
|
2620
2639
|
}
|
|
@@ -9195,6 +9214,10 @@ export class AgentSession {
|
|
|
9195
9214
|
model,
|
|
9196
9215
|
{
|
|
9197
9216
|
streamOptions: handoffStreamOptions,
|
|
9217
|
+
completeImpl: async (requestModel, requestContext, requestOptions) => {
|
|
9218
|
+
const stream = await this.#sideStreamFn(requestModel, requestContext, requestOptions);
|
|
9219
|
+
return stream.result();
|
|
9220
|
+
},
|
|
9198
9221
|
telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
|
|
9199
9222
|
// Honor the user's /model thinking selection on the handoff path.
|
|
9200
9223
|
// Clamped per-model inside generateHandoffFromContext via
|
|
@@ -9482,16 +9505,18 @@ export class AgentSession {
|
|
|
9482
9505
|
const errorIsFromBeforeCompaction =
|
|
9483
9506
|
compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
|
|
9484
9507
|
if (sameModel && !errorIsFromBeforeCompaction && AIError.isContextOverflow(assistantMessage, contextWindow)) {
|
|
9485
|
-
//
|
|
9486
|
-
//
|
|
9487
|
-
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9508
|
+
// Clear the failed turn from active context so the retry (or the next
|
|
9509
|
+
// user prompt) does not replay it. The persisted branch entry stays
|
|
9510
|
+
// for now: when no recovery path runs, the user-facing transcript
|
|
9511
|
+
// MUST keep the only assistant message explaining why the turn
|
|
9512
|
+
// stopped. The branch entry is dropped further down, but only on the
|
|
9513
|
+
// paths that actually schedule a retry/compaction.
|
|
9514
|
+
this.#removeAssistantMessageFromActiveContext(assistantMessage);
|
|
9491
9515
|
|
|
9492
9516
|
// Try context promotion first - switch to a larger model and retry without compacting
|
|
9493
9517
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
9494
9518
|
if (promoted) {
|
|
9519
|
+
await this.#dropPersistedAssistantTurn(assistantMessage);
|
|
9495
9520
|
// Retry on the promoted (larger) model without compacting
|
|
9496
9521
|
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
9497
9522
|
return COMPACTION_CHECK_CONTINUATION;
|
|
@@ -9500,7 +9525,9 @@ export class AgentSession {
|
|
|
9500
9525
|
// No promotion target available fall through to compaction
|
|
9501
9526
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
9502
9527
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
9503
|
-
return await this.#
|
|
9528
|
+
return await this.#runRecoveryCompactionWithRollback("overflow", assistantMessage, allowDefer, {
|
|
9529
|
+
autoContinue,
|
|
9530
|
+
});
|
|
9504
9531
|
}
|
|
9505
9532
|
return COMPACTION_CHECK_NONE;
|
|
9506
9533
|
}
|
|
@@ -9512,13 +9539,14 @@ export class AgentSession {
|
|
|
9512
9539
|
// otherwise compaction/handoff. Unlike overflow, the *input* is fine, so we
|
|
9513
9540
|
// allow the handoff strategy to actually run.
|
|
9514
9541
|
if (sameModel && !errorIsFromBeforeCompaction && assistantMessage.stopReason === "length") {
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9542
|
+
// Same active-context vs persisted-history split as the overflow path
|
|
9543
|
+
// above: clear the dead turn from agent state so it cannot be replayed,
|
|
9544
|
+
// but keep it on the branch unless promotion or compaction actually runs.
|
|
9545
|
+
this.#removeAssistantMessageFromActiveContext(assistantMessage);
|
|
9519
9546
|
|
|
9520
9547
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
9521
9548
|
if (promoted) {
|
|
9549
|
+
await this.#dropPersistedAssistantTurn(assistantMessage);
|
|
9522
9550
|
logger.debug("Context promotion triggered by response.incomplete (length stop)", {
|
|
9523
9551
|
from: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
9524
9552
|
});
|
|
@@ -9532,7 +9560,7 @@ export class AgentSession {
|
|
|
9532
9560
|
model: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
9533
9561
|
strategy: incompleteCompactionSettings.strategy,
|
|
9534
9562
|
});
|
|
9535
|
-
return await this.#
|
|
9563
|
+
return await this.#runRecoveryCompactionWithRollback("incomplete", assistantMessage, allowDefer, {
|
|
9536
9564
|
autoContinue,
|
|
9537
9565
|
triggerContextTokens: calculateContextTokens(assistantMessage.usage),
|
|
9538
9566
|
});
|
|
@@ -9797,6 +9825,54 @@ export class AgentSession {
|
|
|
9797
9825
|
}
|
|
9798
9826
|
}
|
|
9799
9827
|
|
|
9828
|
+
/**
|
|
9829
|
+
* Drop a recoverable assistant turn from the persisted session branch once a
|
|
9830
|
+
* recovery path (context promotion or compaction) is committed. Waits for the
|
|
9831
|
+
* in-flight `message_end` persistence slot first so the branch entry exists
|
|
9832
|
+
* before we reparent past it. Active context removal is the caller's
|
|
9833
|
+
* responsibility — recovery paths clear it eagerly so the retry never
|
|
9834
|
+
* replays the failed turn, while no-recovery paths leave the persisted entry
|
|
9835
|
+
* (and the user-visible transcript line) in place.
|
|
9836
|
+
*/
|
|
9837
|
+
async #dropPersistedAssistantTurn(assistantMessage: AssistantMessage): Promise<void> {
|
|
9838
|
+
await this.#waitForSessionMessagePersistence(assistantMessage);
|
|
9839
|
+
this.#discardAssistantTurn(assistantMessage);
|
|
9840
|
+
}
|
|
9841
|
+
|
|
9842
|
+
/**
|
|
9843
|
+
* Drop the failed assistant turn from persisted history, run
|
|
9844
|
+
* {@link #runAutoCompaction} for an `overflow` / `incomplete` recovery, and
|
|
9845
|
+
* restore the assistant entry if compaction did not actually commit
|
|
9846
|
+
* anything (no usable model/preparation, hook cancel, or compaction error).
|
|
9847
|
+
*
|
|
9848
|
+
* Compaction has to see a clean branch — otherwise its `prepareCompaction`
|
|
9849
|
+
* pass would keep the failed turn in the kept region and the retry would
|
|
9850
|
+
* replay it. But a NONE return that was not paired with a fresh compaction
|
|
9851
|
+
* summary means no recovery is in progress, and leaving the branch
|
|
9852
|
+
* reparented would erase the only user-visible explanation for why the turn
|
|
9853
|
+
* stopped. Reverting the drop in that case preserves the transcript while
|
|
9854
|
+
* still letting a real recovery path own the rewrite.
|
|
9855
|
+
*/
|
|
9856
|
+
async #runRecoveryCompactionWithRollback(
|
|
9857
|
+
reason: "overflow" | "incomplete",
|
|
9858
|
+
assistantMessage: AssistantMessage,
|
|
9859
|
+
allowDefer: boolean,
|
|
9860
|
+
options: { autoContinue: boolean; triggerContextTokens?: number },
|
|
9861
|
+
): Promise<CompactionCheckResult> {
|
|
9862
|
+
const compactionEntryBefore = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
9863
|
+
await this.#dropPersistedAssistantTurn(assistantMessage);
|
|
9864
|
+
const result = await this.#runAutoCompaction(reason, true, false, allowDefer, options);
|
|
9865
|
+
const recoveryCommitted =
|
|
9866
|
+
result.continuationScheduled || result.deferredHandoff || result.automaticContinuationBlocked === true;
|
|
9867
|
+
if (!recoveryCommitted) {
|
|
9868
|
+
const compactionEntryAfter = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
9869
|
+
if (compactionEntryAfter === compactionEntryBefore) {
|
|
9870
|
+
this.sessionManager.appendMessage(assistantMessage);
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
return result;
|
|
9874
|
+
}
|
|
9875
|
+
|
|
9800
9876
|
/**
|
|
9801
9877
|
* Drop an assistant turn from BOTH the live agent context and the persisted
|
|
9802
9878
|
* session branch (reparenting the leaf to the turn's parent), so a discarded
|
|
@@ -10694,6 +10770,18 @@ export class AgentSession {
|
|
|
10694
10770
|
tools: this.agent.state.tools,
|
|
10695
10771
|
sessionId: this.sessionId,
|
|
10696
10772
|
promptCacheKey: this.sessionId,
|
|
10773
|
+
// Route every summarization HTTP request through the
|
|
10774
|
+
// session's side-stream transport so the provider
|
|
10775
|
+
// concurrency cap (e.g. providers.ollama-cloud.maxConcurrency)
|
|
10776
|
+
// brackets compaction the same way it brackets the live
|
|
10777
|
+
// agent turn — without this, multiple ollama-cloud
|
|
10778
|
+
// subagents auto/manually compacting issued uncapped
|
|
10779
|
+
// summary requests in parallel (chatgpt-codex review on
|
|
10780
|
+
// #3751).
|
|
10781
|
+
completeImpl: async (requestModel, requestContext, requestOptions) => {
|
|
10782
|
+
const stream = await this.#sideStreamFn(requestModel, requestContext, requestOptions);
|
|
10783
|
+
return stream.result();
|
|
10784
|
+
},
|
|
10697
10785
|
},
|
|
10698
10786
|
);
|
|
10699
10787
|
} catch (error) {
|
|
@@ -12904,7 +12992,7 @@ export class AgentSession {
|
|
|
12904
12992
|
let providerReplyText = "";
|
|
12905
12993
|
let emittedReplyText = "";
|
|
12906
12994
|
let assistantMessage: AssistantMessage | undefined;
|
|
12907
|
-
const stream =
|
|
12995
|
+
const stream = await this.#sideStreamFn(model, obfuscateProviderContext(this.#obfuscator, context), options);
|
|
12908
12996
|
for await (const event of stream) {
|
|
12909
12997
|
if (event.type === "text_delta") {
|
|
12910
12998
|
providerReplyText += event.delta;
|
|
@@ -13522,6 +13610,12 @@ export class AgentSession {
|
|
|
13522
13610
|
metadata: this.agent.metadataForProvider(model.provider),
|
|
13523
13611
|
convertToLlm: messages => this.#convertToLlmForSideRequest(messages),
|
|
13524
13612
|
telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
|
|
13613
|
+
// Same per-provider concurrency cap rationale as the compaction
|
|
13614
|
+
// path above (chatgpt-codex review on #3751).
|
|
13615
|
+
completeImpl: async (requestModel, requestContext, requestOptions) => {
|
|
13616
|
+
const stream = await this.#sideStreamFn(requestModel, requestContext, requestOptions);
|
|
13617
|
+
return stream.result();
|
|
13618
|
+
},
|
|
13525
13619
|
});
|
|
13526
13620
|
this.#branchSummaryAbortController = undefined;
|
|
13527
13621
|
if (result.aborted) {
|
|
@@ -245,20 +245,21 @@ function countMessageMarkers(content: string): number {
|
|
|
245
245
|
return count;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
function extractFirstDisplayMessageFromPrefix(content: string): string | undefined {
|
|
249
|
+
let fallback: string | undefined;
|
|
250
|
+
let index = content.indexOf('"role"');
|
|
251
251
|
|
|
252
|
-
let index = roleIndex;
|
|
253
252
|
while (index !== -1) {
|
|
254
253
|
const role = extractStringProperty(content, "role", index);
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
const text = extractStringProperty(content, "content", index) ?? extractStringProperty(content, "text", index);
|
|
255
|
+
if (text) {
|
|
256
|
+
if (role === "user") return text;
|
|
257
|
+
if (!fallback && (role === "developer" || role === "assistant")) fallback = text;
|
|
257
258
|
}
|
|
258
259
|
index = content.indexOf('"role"', index + 6);
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
return
|
|
262
|
+
return fallback;
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
interface SessionListHeader {
|
|
@@ -394,7 +395,7 @@ async function scanSessionFile(
|
|
|
394
395
|
}
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
firstMessage ||=
|
|
398
|
+
firstMessage ||= extractFirstDisplayMessageFromPrefix(content) ?? "";
|
|
398
399
|
const messageCount = Math.max(parsedMessageCount, countMessageMarkers(content));
|
|
399
400
|
return {
|
|
400
401
|
path: file,
|