@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13
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 +57 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
- package/dist/cli.js +3069 -3047
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +4 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +22 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +24 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +5 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +139 -8
- package/src/session/agent-session-types.ts +2 -0
- package/src/session/agent-session.ts +66 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +321 -184
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
|
@@ -98,7 +98,7 @@ import {
|
|
|
98
98
|
withTimeout,
|
|
99
99
|
} from "@oh-my-pi/pi-utils";
|
|
100
100
|
import { type AdvisorConfig, type AdvisorRuntimeStatus, loadAdvisorTranscriptCosts } from "../advisor";
|
|
101
|
-
import { type AsyncJob, AsyncJobManager } from "../async";
|
|
101
|
+
import { ASYNC_JOB_MANAGER_SHUTDOWN_REASON, type AsyncJob, AsyncJobManager } from "../async";
|
|
102
102
|
import { shouldEnableAppendOnlyContext } from "../config/append-only-context-mode";
|
|
103
103
|
import type { ModelRegistry } from "../config/model-registry";
|
|
104
104
|
import type { ResolvedModelRoleValue } from "../config/model-resolver";
|
|
@@ -313,6 +313,7 @@ import {
|
|
|
313
313
|
queueChipText,
|
|
314
314
|
toRestoredQueuedMessage,
|
|
315
315
|
} from "./queued-messages";
|
|
316
|
+
import type { ServingModel } from "./retry-fallback-chains";
|
|
316
317
|
import { type AdvisorStats, SessionAdvisors, type SessionAdvisorsHost } from "./session-advisors";
|
|
317
318
|
import type { BuildSessionContextOptions, SessionContext } from "./session-context";
|
|
318
319
|
import { getRestorableSessionModels } from "./session-context";
|
|
@@ -1242,6 +1243,7 @@ export class AgentSession {
|
|
|
1242
1243
|
createComputerTool: config.createComputerTool,
|
|
1243
1244
|
createInspectImageTool: config.createInspectImageTool,
|
|
1244
1245
|
builtInToolNames: config.builtInToolNames,
|
|
1246
|
+
mcpManagerToolNames: config.mcpManagerToolNames,
|
|
1245
1247
|
presentationPinnedToolNames: config.presentationPinnedToolNames,
|
|
1246
1248
|
ensureWriteRegistered: config.ensureWriteRegistered,
|
|
1247
1249
|
rebuildSystemPrompt: config.rebuildSystemPrompt,
|
|
@@ -1488,7 +1490,7 @@ export class AgentSession {
|
|
|
1488
1490
|
this.#planReferenceSent = false;
|
|
1489
1491
|
},
|
|
1490
1492
|
syncTodoPhasesFromBranch: () => this.#todo.syncFromBranch(),
|
|
1491
|
-
resetAdvisorRuntimes: () => this.#advisors.resetAllRuntimes(),
|
|
1493
|
+
resetAdvisorRuntimes: (reason?: string) => this.#advisors.resetAllRuntimes(reason),
|
|
1492
1494
|
rebaseAfterCompaction: () => this.#stats.rebaseAfterCompaction(),
|
|
1493
1495
|
recordAnchoredHistoryRewrite: tokensRemoved => this.#stats.recordAnchoredHistoryRewrite(tokensRemoved),
|
|
1494
1496
|
getContextBreakdown: options => this.getContextBreakdown(options),
|
|
@@ -1783,10 +1785,10 @@ export class AgentSession {
|
|
|
1783
1785
|
*
|
|
1784
1786
|
* No-op when no manager is reachable or this session has no agent id.
|
|
1785
1787
|
*/
|
|
1786
|
-
#cancelOwnAsyncJobs(): void {
|
|
1788
|
+
#cancelOwnAsyncJobs(reason?: unknown): void {
|
|
1787
1789
|
if (!this.#agentId) return;
|
|
1788
1790
|
const manager = this.#asyncJobManager;
|
|
1789
|
-
manager?.cancelAll({ ownerId: this.#agentId });
|
|
1791
|
+
manager?.cancelAll({ ownerId: this.#agentId }, reason);
|
|
1790
1792
|
manager?.evictCompletedJobs({ ownerId: this.#agentId });
|
|
1791
1793
|
// Invalidate this owner's in-flight/drained deliveries against the new
|
|
1792
1794
|
// generation, then drop any async-result follow-up already queued, so a
|
|
@@ -3743,8 +3745,14 @@ export class AgentSession {
|
|
|
3743
3745
|
// dead-letter rather than enqueue a follow-up into a disposing session.
|
|
3744
3746
|
this.#unregisterAsyncDeliverySink?.();
|
|
3745
3747
|
this.#unregisterAsyncDeliverySink = undefined;
|
|
3746
|
-
this.#cancelOwnAsyncJobs();
|
|
3747
3748
|
const manager = this.#ownedAsyncJobManager;
|
|
3749
|
+
// The shutdown reason is reserved for the top-level session that OWNS the
|
|
3750
|
+
// manager — the genuine process/handled-shutdown path — so the task
|
|
3751
|
+
// executor parks (rather than tombstones) interrupted subagents. A
|
|
3752
|
+
// subagent session dispose (e.g. `release({ tombstone: true })` during an
|
|
3753
|
+
// explicit hard kill) leaves `#ownedAsyncJobManager` undefined and must
|
|
3754
|
+
// propagate a generic cancellation so its nested children stay terminal.
|
|
3755
|
+
this.#cancelOwnAsyncJobs(manager ? ASYNC_JOB_MANAGER_SHUTDOWN_REASON : undefined);
|
|
3748
3756
|
if (!manager) return;
|
|
3749
3757
|
|
|
3750
3758
|
try {
|
|
@@ -4097,9 +4105,13 @@ export class AgentSession {
|
|
|
4097
4105
|
return this.agent.state.model;
|
|
4098
4106
|
}
|
|
4099
4107
|
|
|
4100
|
-
/**
|
|
4101
|
-
|
|
4102
|
-
|
|
4108
|
+
/**
|
|
4109
|
+
* Model this session's produced work is attributed to. Holds the last model
|
|
4110
|
+
* that actually served while a fallback is armed but unproven, so observers
|
|
4111
|
+
* never credit a run to a candidate that produced nothing.
|
|
4112
|
+
*/
|
|
4113
|
+
get servingModel(): ServingModel | undefined {
|
|
4114
|
+
return this.#recovery.servingModel;
|
|
4103
4115
|
}
|
|
4104
4116
|
|
|
4105
4117
|
/** Install the interactive decision surface for reserve-triggered model changes. */
|
|
@@ -4296,6 +4308,41 @@ export class AgentSession {
|
|
|
4296
4308
|
return this.#tools.hasBuiltInTool(name);
|
|
4297
4309
|
}
|
|
4298
4310
|
|
|
4311
|
+
/** Updates source provenance when a live registry entry is replaced or restored. */
|
|
4312
|
+
setToolBuiltIn(name: string, builtIn: boolean): void {
|
|
4313
|
+
this.#tools.setToolBuiltIn(name, builtIn);
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
/** Whether the live registry entry is owned by the RPC host. */
|
|
4317
|
+
hasRpcHostTool(name: string): boolean {
|
|
4318
|
+
return this.#tools.hasRpcHostTool(name);
|
|
4319
|
+
}
|
|
4320
|
+
|
|
4321
|
+
/** Whether the current MCP entry came from the manager snapshot. */
|
|
4322
|
+
hasMCPManagerTool(name: string): boolean {
|
|
4323
|
+
return this.#tools.hasMCPManagerTool(name);
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4326
|
+
/** Restores manager ownership after a lifecycle registration rollback. */
|
|
4327
|
+
setMCPManagerTool(name: string, managerOwned: boolean): void {
|
|
4328
|
+
this.#tools.setMCPManagerTool(name, managerOwned);
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
/** Current extension-owned MCP entry retained across manager refreshes. */
|
|
4332
|
+
getExtensionMCPTool(name: string): AgentTool | undefined {
|
|
4333
|
+
return this.#tools.getExtensionMCPTool(name);
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
/** Updates extension MCP ownership after a lifecycle registration commit or rollback. */
|
|
4337
|
+
setExtensionMCPTool(name: string, tool: AgentTool | undefined): void {
|
|
4338
|
+
this.#tools.setExtensionMCPTool(name, tool);
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
/** Runs a registry/presentation mutation in this session's shared queue. */
|
|
4342
|
+
runToolRegistryMutation<T>(mutation: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
|
4343
|
+
return this.#tools.runToolRegistryMutation(mutation, signal);
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4299
4346
|
/** Names of every registered tool. */
|
|
4300
4347
|
getAllToolNames(): string[] {
|
|
4301
4348
|
return this.#tools.getAllToolNames();
|
|
@@ -4349,8 +4396,13 @@ export class AgentSession {
|
|
|
4349
4396
|
}
|
|
4350
4397
|
|
|
4351
4398
|
/** Restores an exact top-level versus `xd://` tool partition. */
|
|
4352
|
-
setActiveToolPresentation(
|
|
4353
|
-
|
|
4399
|
+
setActiveToolPresentation(
|
|
4400
|
+
toolNames: string[],
|
|
4401
|
+
mountedToolNames: string[],
|
|
4402
|
+
forcePromptRefresh = false,
|
|
4403
|
+
signal?: AbortSignal,
|
|
4404
|
+
): Promise<void> {
|
|
4405
|
+
return this.#tools.setActiveToolPresentation(toolNames, mountedToolNames, forcePromptRefresh, signal);
|
|
4354
4406
|
}
|
|
4355
4407
|
|
|
4356
4408
|
/**
|
|
@@ -6497,6 +6549,7 @@ export class AgentSession {
|
|
|
6497
6549
|
async fork(): Promise<boolean> {
|
|
6498
6550
|
this.#assertVibeSessionTransitionAllowed("fork the session");
|
|
6499
6551
|
const previousSessionFile = this.sessionFile;
|
|
6552
|
+
const previousSessionId = this.sessionManager.getSessionId();
|
|
6500
6553
|
|
|
6501
6554
|
// Emit session_before_switch event with reason "fork" (can be cancelled)
|
|
6502
6555
|
if (this.#extensionRunner?.hasHandlers("session_before_switch")) {
|
|
@@ -6535,6 +6588,9 @@ export class AgentSession {
|
|
|
6535
6588
|
}
|
|
6536
6589
|
this.#bash.markSessionTransition(bashTransition);
|
|
6537
6590
|
this.#bash.finishSessionTransition(bashTransition, true);
|
|
6591
|
+
// The fork clones the transcript and keeps this recovery state running
|
|
6592
|
+
// under a fresh id, so the work already produced is still this session's.
|
|
6593
|
+
this.#recovery.reanchorServedAttribution(previousSessionId);
|
|
6538
6594
|
|
|
6539
6595
|
// Copy artifacts directory if it exists
|
|
6540
6596
|
const oldArtifactDir = forkResult.oldSessionFile.slice(0, -6);
|
package/src/session/messages.ts
CHANGED
|
@@ -498,6 +498,76 @@ export function isEmptyErrorTurn(message: Pick<AssistantMessage, "stopReason" |
|
|
|
498
498
|
});
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
+
/** Non-whitespace text. Tolerates malformed blocks: transcripts replayed off
|
|
502
|
+
* disk predate current shapes, and a missing field must not throw. */
|
|
503
|
+
function hasText(content: { text?: unknown }): boolean {
|
|
504
|
+
return typeof content.text === "string" && content.text.trim().length > 0;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* A block that is real output from the model.
|
|
509
|
+
*
|
|
510
|
+
* Everything the assistant can emit counts except two: unsigned thinking, which
|
|
511
|
+
* is not provider-authenticated and not actionable, and Anthropic's `fallback`
|
|
512
|
+
* marker, which records that the request was routed elsewhere rather than
|
|
513
|
+
* carrying any output. A native image response often arrives with no text and
|
|
514
|
+
* no tool call at all, so recognising only those would call it nothing.
|
|
515
|
+
*/
|
|
516
|
+
function isActionableContent(content: AssistantMessage["content"][number] | undefined): boolean {
|
|
517
|
+
switch (content?.type) {
|
|
518
|
+
case "toolCall":
|
|
519
|
+
case "image":
|
|
520
|
+
case "redactedThinking":
|
|
521
|
+
case "anthropicServerTool":
|
|
522
|
+
return true;
|
|
523
|
+
case "text":
|
|
524
|
+
return hasText(content);
|
|
525
|
+
case "thinking":
|
|
526
|
+
return typeof content.thinkingSignature === "string" && content.thinkingSignature.trim().length > 0;
|
|
527
|
+
default:
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** A `stop`/`toolUse` turn that produced nothing actionable. Any other stop
|
|
533
|
+
* reason is not an "empty stop": an `error`/`aborted` turn is a failure rather
|
|
534
|
+
* than an empty completion, and a `length` stop was cut off mid-output. */
|
|
535
|
+
export function isEmptyAssistantStop(message: Pick<AssistantMessage, "stopReason" | "content">): boolean {
|
|
536
|
+
switch (message.stopReason) {
|
|
537
|
+
case "stop":
|
|
538
|
+
return !message.content.some(isActionableContent);
|
|
539
|
+
case "toolUse":
|
|
540
|
+
// An orphaned toolUse stop (no tool_use block) corrupts Anthropic history:
|
|
541
|
+
// a later tool_result has nothing to anchor to. Thinking alone cannot anchor
|
|
542
|
+
// a tool_result, so it does not rescue a toolUse stop here.
|
|
543
|
+
return !message.content.some(
|
|
544
|
+
content => content?.type === "toolCall" || (content?.type === "text" && hasText(content)),
|
|
545
|
+
);
|
|
546
|
+
default:
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* True when this assistant turn actually produced output, making its model the
|
|
553
|
+
* one that served the run.
|
|
554
|
+
*
|
|
555
|
+
* Attribution asks this from two places that MUST reach the same verdict: the
|
|
556
|
+
* live session, which flips a fallback to "served", and the offline walk
|
|
557
|
+
* replaying a transcript. `error` and `aborted` are both failures — a stalled or
|
|
558
|
+
* dropped stream is finalized as `aborted` with its partial block still
|
|
559
|
+
* attached, so a stop reason alone is not proof.
|
|
560
|
+
*
|
|
561
|
+
* Actionable content is required on top of the empty-stop rule, which only
|
|
562
|
+
* inspects `stop`/`toolUse`. A `length` stop burns the whole output budget
|
|
563
|
+
* without necessarily emitting anything usable, and every other stop reason
|
|
564
|
+
* bypasses that rule entirely.
|
|
565
|
+
*/
|
|
566
|
+
export function assistantTurnProducedOutput(message: Pick<AssistantMessage, "stopReason" | "content">): boolean {
|
|
567
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") return false;
|
|
568
|
+
return !isEmptyAssistantStop(message) && message.content.some(isActionableContent);
|
|
569
|
+
}
|
|
570
|
+
|
|
501
571
|
/** Sentinel `errorMessage` the agent stamps on any abort that carried no custom
|
|
502
572
|
* reason (bare `abort()`). Renderers treat it as "no specific reason given". */
|
|
503
573
|
export const GENERIC_ABORT_SENTINEL = "Request was aborted";
|
|
@@ -1081,17 +1151,16 @@ const convertCache = new WeakMap<AgentMessage, ConvertMemoEntry>();
|
|
|
1081
1151
|
// The tail-identity guard on exact-repeat catches the streaming snapshot swap
|
|
1082
1152
|
// (partial → trailing is a fresh identity), so a settled tail is never served
|
|
1083
1153
|
// from a stale mid-stream fragment.
|
|
1154
|
+
interface ConvertArrayMemo {
|
|
1155
|
+
generation: number;
|
|
1156
|
+
length: number;
|
|
1157
|
+
output: Message[];
|
|
1158
|
+
tail: AgentMessage | undefined;
|
|
1159
|
+
prefixOutputLen: number;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1084
1162
|
let convertGeneration = 0;
|
|
1085
|
-
|
|
1086
|
-
let lastConvertLength = 0;
|
|
1087
|
-
let lastConvertOutput: Message[] | undefined;
|
|
1088
|
-
let lastConvertGeneration = -1;
|
|
1089
|
-
let lastConvertTail: AgentMessage | undefined;
|
|
1090
|
-
// Output-message count contributed by messages[0 .. lastConvertLength-1), i.e.
|
|
1091
|
-
// every message except the last. The last message is neighbor-sensitive (its LLM
|
|
1092
|
-
// view drops the trailing thinking run only while an interrupted-thinking marker
|
|
1093
|
-
// follows), so growth reconverts it rather than reusing its old fragment.
|
|
1094
|
-
let lastConvertPrefixOutputLen = 0;
|
|
1163
|
+
const convertArrayCache = new WeakMap<AgentMessage[], ConvertArrayMemo>();
|
|
1095
1164
|
|
|
1096
1165
|
registerMessageCacheInvalidator(message => {
|
|
1097
1166
|
convertCache.delete(message);
|
|
@@ -1246,15 +1315,16 @@ function convertOneCached(m: AgentMessage, interruptedNext: boolean): Message[]
|
|
|
1246
1315
|
*/
|
|
1247
1316
|
export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
1248
1317
|
const len = messages.length;
|
|
1249
|
-
const
|
|
1318
|
+
const memo = convertArrayCache.get(messages);
|
|
1319
|
+
const sameGeneration = memo !== undefined && memo.generation === convertGeneration;
|
|
1250
1320
|
const tail = len > 0 ? messages[len - 1] : undefined;
|
|
1251
1321
|
|
|
1252
1322
|
// Exact-repeat: same array, same length, same trailing identity → reuse the
|
|
1253
1323
|
// outer array. The tail-identity check rejects the streaming snapshot swap
|
|
1254
1324
|
// (partial → settled trailing keeps array identity/length but mints a fresh
|
|
1255
1325
|
// tail), so a settled tail never reads a stale mid-stream fragment.
|
|
1256
|
-
if (
|
|
1257
|
-
return
|
|
1326
|
+
if (sameGeneration && memo.length === len && tail === memo.tail) {
|
|
1327
|
+
return memo.output;
|
|
1258
1328
|
}
|
|
1259
1329
|
|
|
1260
1330
|
// Slice-on-growth: same array grew by append. Every interior message is
|
|
@@ -1267,15 +1337,14 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
1267
1337
|
let out: Message[];
|
|
1268
1338
|
let start: number;
|
|
1269
1339
|
if (
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
lastConvertPrefixOutputLen <= lastConvertOutput.length
|
|
1340
|
+
sameGeneration &&
|
|
1341
|
+
len > memo.length &&
|
|
1342
|
+
memo.length > 0 &&
|
|
1343
|
+
messages[memo.length - 1] === memo.tail &&
|
|
1344
|
+
memo.prefixOutputLen <= memo.output.length
|
|
1276
1345
|
) {
|
|
1277
|
-
out =
|
|
1278
|
-
start =
|
|
1346
|
+
out = memo.output.slice(0, memo.prefixOutputLen);
|
|
1347
|
+
start = memo.length - 1;
|
|
1279
1348
|
} else {
|
|
1280
1349
|
out = [];
|
|
1281
1350
|
start = 0;
|
|
@@ -1294,12 +1363,13 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
1294
1363
|
if (len === 0) prefixOutputLen = 0;
|
|
1295
1364
|
|
|
1296
1365
|
// Record for the next call's shortcuts. `out` is a fresh array (slice or new),
|
|
1297
|
-
// so a prior caller holding the previous
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1366
|
+
// so a prior caller holding the previous memo output never sees it grow.
|
|
1367
|
+
convertArrayCache.set(messages, {
|
|
1368
|
+
generation: convertGeneration,
|
|
1369
|
+
length: len,
|
|
1370
|
+
output: out,
|
|
1371
|
+
tail,
|
|
1372
|
+
prefixOutputLen,
|
|
1373
|
+
});
|
|
1304
1374
|
return out;
|
|
1305
1375
|
}
|
|
@@ -50,6 +50,20 @@ export interface ActiveRetryFallbackState {
|
|
|
50
50
|
originalThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
51
51
|
lastAppliedFallbackThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
52
52
|
pinned: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Set once a turn on the fallback target settles successfully. Until then the
|
|
55
|
+
* switch is only a routing decision — nothing has been produced by the new
|
|
56
|
+
* model, so no observer may report the run as having used it.
|
|
57
|
+
*/
|
|
58
|
+
served?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Model a session's produced work is attributed to. */
|
|
62
|
+
export interface ServingModel {
|
|
63
|
+
/** Full selector including routing and thinking level. */
|
|
64
|
+
selector: string;
|
|
65
|
+
/** Whether fallback routing, rather than the configured primary, owns it. */
|
|
66
|
+
isFallback: boolean;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
const RETRY_BACKOFF_MAX_DELAY_MS = 8_000;
|
|
@@ -418,8 +418,8 @@ export class SessionAdvisors {
|
|
|
418
418
|
}
|
|
419
419
|
|
|
420
420
|
/** Re-primes advisor transcript views after an in-conversation history rewrite. */
|
|
421
|
-
resetAllRuntimes(): void {
|
|
422
|
-
this.#resetAllAdvisorRuntimes();
|
|
421
|
+
resetAllRuntimes(reason?: string): void {
|
|
422
|
+
this.#resetAllAdvisorRuntimes(reason);
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
/** Whether live runtimes still match the resolved advisor configuration. */
|
|
@@ -535,7 +535,7 @@ export class SessionAdvisors {
|
|
|
535
535
|
for (const a of this.#advisors) {
|
|
536
536
|
a.agentUnsubscribe?.();
|
|
537
537
|
a.agentUnsubscribe = undefined;
|
|
538
|
-
a.runtime.reset();
|
|
538
|
+
a.runtime.reset("conversation-boundary");
|
|
539
539
|
a.adviseTool.resetDeliveredNotes();
|
|
540
540
|
a.emissionGuard.reset();
|
|
541
541
|
this.#attachAdvisorRecorderFeed(a);
|
|
@@ -785,14 +785,22 @@ export class SessionAdvisors {
|
|
|
785
785
|
mcpResources: this.#advisorMcpResources,
|
|
786
786
|
});
|
|
787
787
|
const baseAdvisorStreamFn = this.#advisorStreamFn ?? streamSimple;
|
|
788
|
-
const advisorStreamFn: StreamFn = (requestModel, context, options) =>
|
|
789
|
-
|
|
790
|
-
requestModel,
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
788
|
+
const advisorStreamFn: StreamFn = (requestModel, context, options) => {
|
|
789
|
+
if (requestModel.api === "openai-codex-responses") {
|
|
790
|
+
return baseAdvisorStreamFn(requestModel, context, {
|
|
791
|
+
...options,
|
|
792
|
+
codexSseMaxAttempts: ADVISOR_CODEX_SSE_MAX_ATTEMPTS,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
if (
|
|
796
|
+
requestModel.api === "google-generative-ai" ||
|
|
797
|
+
requestModel.api === "google-gemini-cli" ||
|
|
798
|
+
requestModel.api === "google-vertex"
|
|
799
|
+
) {
|
|
800
|
+
return baseAdvisorStreamFn(requestModel, context, { ...options, acceptEmptyResponse: true });
|
|
801
|
+
}
|
|
802
|
+
return baseAdvisorStreamFn(requestModel, context, options);
|
|
803
|
+
};
|
|
796
804
|
const advisorAgent = new Agent({
|
|
797
805
|
initialState: {
|
|
798
806
|
systemPrompt,
|
|
@@ -832,8 +840,17 @@ export class SessionAdvisors {
|
|
|
832
840
|
let quarantined: string | undefined;
|
|
833
841
|
try {
|
|
834
842
|
quarantinedAdvisorOutput = undefined;
|
|
835
|
-
|
|
836
|
-
|
|
843
|
+
// Multi-message input (candidate 4) must serialize deterministically
|
|
844
|
+
// for quarantine source text; reuse the session history formatter
|
|
845
|
+
// rather than ad-hoc joins so all message kinds (text/tool/
|
|
846
|
+
// custom/structured) are preserved exactly as rendered.
|
|
847
|
+
currentAdvisorInput = Array.isArray(input)
|
|
848
|
+
? formatSessionHistoryMarkdown(input, { watchedRoles: true })
|
|
849
|
+
: input;
|
|
850
|
+
// Agent.prompt's overloads accept string OR AgentMessage[] but not
|
|
851
|
+
// the union, so narrow first; both branches intentionally identical.
|
|
852
|
+
if (Array.isArray(input)) await advisorAgent.prompt(input);
|
|
853
|
+
else await advisorAgent.prompt(input);
|
|
837
854
|
quarantined = quarantinedAdvisorOutput;
|
|
838
855
|
} finally {
|
|
839
856
|
quarantinedAdvisorOutput = undefined;
|
|
@@ -1057,8 +1074,8 @@ export class SessionAdvisors {
|
|
|
1057
1074
|
}
|
|
1058
1075
|
|
|
1059
1076
|
/** Re-prime every advisor's transcript view after an in-conversation history rewrite. */
|
|
1060
|
-
#resetAllAdvisorRuntimes(): void {
|
|
1061
|
-
for (const a of this.#advisors) a.runtime.reset();
|
|
1077
|
+
#resetAllAdvisorRuntimes(reason?: string): void {
|
|
1078
|
+
for (const a of this.#advisors) a.runtime.reset(reason);
|
|
1062
1079
|
}
|
|
1063
1080
|
|
|
1064
1081
|
#stopAdvisorRuntime(): void {
|
|
@@ -55,6 +55,14 @@ export interface HistoryFormatOptions {
|
|
|
55
55
|
*/
|
|
56
56
|
toolResultIndex?: ReadonlyMap<string, ToolResultMessage>;
|
|
57
57
|
consumedToolCallIds?: Set<string>;
|
|
58
|
+
/**
|
|
59
|
+
* Chunked rendering state: a mutable holder for the watched-role label
|
|
60
|
+
* (`**user**:` / `**agent**:`) that ended the previous chunk. Lets a caller
|
|
61
|
+
* formatting one logical transcript across several calls (advisor
|
|
62
|
+
* multi-message split) keep consecutive same-role collapsing byte-identical
|
|
63
|
+
* to the single-block render: pass one object across all chunk calls.
|
|
64
|
+
*/
|
|
65
|
+
watchedRoleState?: { lastLabel: string | undefined };
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
/** Max length of the primary-arg summary inside `→ tool(...)` lines. */
|
|
@@ -313,7 +321,9 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
313
321
|
// (the watched agent emits one assistant message per tool call, so otherwise
|
|
314
322
|
// every call repeats `**agent**:`). Cleared whenever a
|
|
315
323
|
// non-role-labeled line is emitted so the next turn re-labels.
|
|
316
|
-
|
|
324
|
+
// Chunked callers seed the previous chunk's trailing label so collapsing
|
|
325
|
+
// stays byte-identical to the single-block render.
|
|
326
|
+
let lastWatchedLabel: string | undefined = opts?.watchedRoleState?.lastLabel;
|
|
317
327
|
// Emit a watched-mode role label, collapsing consecutive same-role turns
|
|
318
328
|
// under one label (matching the user/assistant paths). Used for the
|
|
319
329
|
// user-attributed `!`/`$` execution lines so the advisor never reads them
|
|
@@ -455,5 +465,9 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
455
465
|
}
|
|
456
466
|
}
|
|
457
467
|
|
|
468
|
+
if (opts?.watchedRoleState) {
|
|
469
|
+
opts.watchedRoleState.lastLabel = lastWatchedLabel;
|
|
470
|
+
}
|
|
471
|
+
|
|
458
472
|
return `${lines.join("\n").trim()}\n`;
|
|
459
473
|
}
|
|
@@ -235,7 +235,7 @@ export interface SessionMaintenanceHost {
|
|
|
235
235
|
resetCodexProviderAfterCompaction(compaction: CodexCompactionContext): void;
|
|
236
236
|
resetPlanReference(): void;
|
|
237
237
|
syncTodoPhasesFromBranch(): void;
|
|
238
|
-
resetAdvisorRuntimes(): void;
|
|
238
|
+
resetAdvisorRuntimes(reason?: string): void;
|
|
239
239
|
rebaseAfterCompaction(): void;
|
|
240
240
|
recordAnchoredHistoryRewrite(tokensRemoved: number): void;
|
|
241
241
|
getContextBreakdown(options?: {
|
|
@@ -345,7 +345,7 @@ export class SessionMaintenance {
|
|
|
345
345
|
await this.#host.sessionManager.rewriteEntries();
|
|
346
346
|
const sessionContext = this.#host.buildDisplaySessionContext();
|
|
347
347
|
this.#host.agent.replaceMessages(sessionContext.messages);
|
|
348
|
-
this.#host.resetAdvisorRuntimes();
|
|
348
|
+
this.#host.resetAdvisorRuntimes("prune-tool-outputs");
|
|
349
349
|
this.#host.syncTodoPhasesFromBranch();
|
|
350
350
|
this.#host.closeCodexProviderSessionsForHistoryRewrite();
|
|
351
351
|
return result;
|
|
@@ -388,7 +388,7 @@ export class SessionMaintenance {
|
|
|
388
388
|
await this.#host.sessionManager.rewriteEntries();
|
|
389
389
|
const sessionContext = this.#host.buildDisplaySessionContext();
|
|
390
390
|
this.#host.agent.replaceMessages(sessionContext.messages);
|
|
391
|
-
this.#host.resetAdvisorRuntimes();
|
|
391
|
+
this.#host.resetAdvisorRuntimes("prune-stale-tool-results");
|
|
392
392
|
this.#host.syncTodoPhasesFromBranch();
|
|
393
393
|
this.#host.closeCodexProviderSessionsForHistoryRewrite();
|
|
394
394
|
return result;
|
|
@@ -439,7 +439,7 @@ export class SessionMaintenance {
|
|
|
439
439
|
await this.#host.sessionManager.rewriteEntries();
|
|
440
440
|
const sessionContext = this.#host.buildDisplaySessionContext();
|
|
441
441
|
this.#host.agent.replaceMessages(sessionContext.messages);
|
|
442
|
-
this.#host.resetAdvisorRuntimes();
|
|
442
|
+
this.#host.resetAdvisorRuntimes("drop-images");
|
|
443
443
|
this.#host.closeCodexProviderSessionsForHistoryRewrite();
|
|
444
444
|
return { removed };
|
|
445
445
|
}
|
|
@@ -527,7 +527,7 @@ export class SessionMaintenance {
|
|
|
527
527
|
await this.#host.sessionManager.rewriteEntries();
|
|
528
528
|
const sessionContext = this.#host.buildDisplaySessionContext();
|
|
529
529
|
this.#host.agent.replaceMessages(sessionContext.messages);
|
|
530
|
-
this.#host.resetAdvisorRuntimes();
|
|
530
|
+
this.#host.resetAdvisorRuntimes("shake");
|
|
531
531
|
this.#host.closeCodexProviderSessionsForHistoryRewrite();
|
|
532
532
|
|
|
533
533
|
return {
|
|
@@ -879,7 +879,7 @@ export class SessionMaintenance {
|
|
|
879
879
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
880
880
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
881
881
|
this.#host.resetPlanReference();
|
|
882
|
-
this.#host.resetAdvisorRuntimes();
|
|
882
|
+
this.#host.resetAdvisorRuntimes("compact");
|
|
883
883
|
this.#host.syncTodoPhasesFromBranch();
|
|
884
884
|
if (codexCompaction) {
|
|
885
885
|
this.#host.resetCodexProviderAfterCompaction(codexCompaction);
|
|
@@ -2094,7 +2094,7 @@ export class SessionMaintenance {
|
|
|
2094
2094
|
// and advisor cursors / todo phases were derived from the replaced
|
|
2095
2095
|
// history.
|
|
2096
2096
|
this.#host.resetPlanReference();
|
|
2097
|
-
this.#host.resetAdvisorRuntimes();
|
|
2097
|
+
this.#host.resetAdvisorRuntimes("compaction-rescue");
|
|
2098
2098
|
this.#host.syncTodoPhasesFromBranch();
|
|
2099
2099
|
this.#host.closeCodexProviderSessionsForHistoryRewrite();
|
|
2100
2100
|
// Extensions must see the entry that is now active, not (only) the one
|
|
@@ -2763,7 +2763,7 @@ export class SessionMaintenance {
|
|
|
2763
2763
|
// plan reference. Clear the sent-flag so #buildPlanReferenceMessage re-reads
|
|
2764
2764
|
// the plan from disk and re-injects it on the next turn (issue #1246).
|
|
2765
2765
|
this.#host.resetPlanReference();
|
|
2766
|
-
this.#host.resetAdvisorRuntimes();
|
|
2766
|
+
this.#host.resetAdvisorRuntimes("auto-compaction");
|
|
2767
2767
|
this.#host.syncTodoPhasesFromBranch();
|
|
2768
2768
|
if (codexCompaction) {
|
|
2769
2769
|
this.#host.resetCodexProviderAfterCompaction(codexCompaction);
|
|
@@ -1285,6 +1285,10 @@ export class SessionManager {
|
|
|
1285
1285
|
|
|
1286
1286
|
/** Switch to a different session file (resume / branch). */
|
|
1287
1287
|
async setSessionFile(sessionFile: string): Promise<void> {
|
|
1288
|
+
await this.#setSessionFile(sessionFile);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
async #setSessionFile(sessionFile: string, loadedEntries?: FileEntry[]): Promise<void> {
|
|
1288
1292
|
await this.#drainAndCloseWriter();
|
|
1289
1293
|
this.#clearDiskError();
|
|
1290
1294
|
this.#draftOnlySessionCleanupArmed = false;
|
|
@@ -1294,7 +1298,7 @@ export class SessionManager {
|
|
|
1294
1298
|
this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
|
|
1295
1299
|
|
|
1296
1300
|
const titleSlot = await readTitleSlotFromFile(resolvedSessionFile, this.#storage);
|
|
1297
|
-
const fileEntries = await loadEntriesFromFile(resolvedSessionFile, this.#storage);
|
|
1301
|
+
const fileEntries = loadedEntries ?? (await loadEntriesFromFile(resolvedSessionFile, this.#storage));
|
|
1298
1302
|
if (fileEntries.length === 0) {
|
|
1299
1303
|
// Explicit but empty/missing path (e.g. --session flag): start fresh but
|
|
1300
1304
|
// keep the requested path and materialize the header immediately.
|
|
@@ -2601,7 +2605,7 @@ export class SessionManager {
|
|
|
2601
2605
|
: path.dirname(path.resolve(filePath)));
|
|
2602
2606
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2603
2607
|
manager.#suppressBreadcrumb = options?.suppressBreadcrumb === true;
|
|
2604
|
-
await manager
|
|
2608
|
+
await manager.#setSessionFile(filePath, loaded);
|
|
2605
2609
|
return manager;
|
|
2606
2610
|
}
|
|
2607
2611
|
|