@oh-my-pi/pi-coding-agent 15.7.6 → 15.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +172 -198
- package/dist/types/async/job-manager.d.ts +3 -3
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/claude-trace-cli.d.ts +54 -0
- package/dist/types/cli/session-picker.d.ts +10 -3
- package/dist/types/cli/update-cli.d.ts +17 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +10 -1
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/edit/index.d.ts +6 -0
- package/dist/types/edit/streaming.d.ts +8 -0
- package/dist/types/eval/concurrency-bridge.d.ts +26 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/export/ttsr.d.ts +9 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +11 -0
- package/dist/types/main.d.ts +5 -0
- package/dist/types/mcp/transports/stdio.d.ts +19 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/hook-selector.d.ts +3 -0
- package/dist/types/modes/components/session-selector.d.ts +32 -5
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +9 -2
- package/dist/types/modes/types.d.ts +4 -2
- package/dist/types/registry/agent-registry.d.ts +1 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/history-storage.d.ts +16 -1
- package/dist/types/session/session-manager.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +6 -15
- package/dist/types/tools/find.d.ts +0 -9
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +16 -0
- package/dist/types/tools/sqlite-reader.d.ts +25 -8
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/clipboard.d.ts +4 -0
- package/dist/types/utils/jj.d.ts +49 -0
- package/dist/types/web/kagi.d.ts +76 -0
- package/dist/types/web/search/providers/exa.d.ts +7 -1
- package/dist/types/web/search/providers/kagi.d.ts +1 -0
- package/package.json +9 -9
- package/src/async/job-manager.ts +3 -3
- package/src/cli/args.ts +6 -2
- package/src/cli/claude-trace-cli.ts +783 -0
- package/src/cli/session-picker.ts +36 -10
- package/src/cli/update-cli.ts +35 -2
- package/src/commands/launch.ts +3 -0
- package/src/config/keybindings.ts +14 -1
- package/src/config/model-registry.ts +18 -7
- package/src/config/settings-schema.ts +2 -2
- package/src/config/settings.ts +23 -0
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
- package/src/discovery/claude-plugins.ts +7 -9
- package/src/edit/index.ts +10 -0
- package/src/edit/streaming.ts +65 -0
- package/src/eval/__tests__/agent-bridge.test.ts +58 -4
- package/src/eval/concurrency-bridge.ts +34 -0
- package/src/eval/js/shared/prelude.txt +20 -17
- package/src/eval/js/tool-bridge.ts +5 -0
- package/src/eval/py/prelude.py +23 -15
- package/src/export/ttsr.ts +18 -1
- package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
- package/src/extensibility/plugins/legacy-pi-compat.ts +115 -131
- package/src/extensibility/skills.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +12 -11
- package/src/main.ts +92 -24
- package/src/mcp/transports/stdio.ts +55 -22
- package/src/modes/acp/acp-event-mapper.ts +54 -4
- package/src/modes/components/custom-editor.ts +10 -0
- package/src/modes/components/hook-selector.ts +89 -31
- package/src/modes/components/oauth-selector.ts +12 -6
- package/src/modes/components/session-selector.ts +179 -24
- package/src/modes/components/tool-execution.ts +16 -3
- package/src/modes/controllers/command-controller.ts +2 -11
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/controllers/input-controller.ts +19 -1
- package/src/modes/controllers/selector-controller.ts +61 -21
- package/src/modes/interactive-mode.ts +125 -15
- package/src/modes/types.ts +5 -2
- package/src/prompts/agents/reviewer.md +2 -2
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/empty-stop-retry.md +6 -0
- package/src/prompts/system/orchestrate-notice.md +5 -3
- package/src/prompts/system/workflow-notice.md +2 -2
- package/src/prompts/tools/eval.md +5 -5
- package/src/prompts/tools/find.md +1 -1
- package/src/prompts/tools/irc.md +6 -6
- package/src/prompts/tools/search-tool-bm25.md +9 -2
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -1
- package/src/registry/agent-registry.ts +1 -1
- package/src/sdk.ts +85 -31
- package/src/session/agent-session.ts +209 -54
- package/src/session/history-storage.ts +56 -12
- package/src/session/session-manager.ts +34 -0
- package/src/task/output-manager.ts +40 -48
- package/src/task/render.ts +3 -8
- package/src/tools/browser/tab-worker.ts +8 -5
- package/src/tools/find.ts +5 -29
- package/src/tools/index.ts +1 -1
- package/src/tools/path-utils.ts +144 -1
- package/src/tools/read.ts +47 -0
- package/src/tools/search-tool-bm25.ts +7 -1
- package/src/tools/search.ts +2 -27
- package/src/tools/sqlite-reader.ts +92 -9
- package/src/tools/write.ts +6 -0
- package/src/utils/clipboard.ts +38 -1
- package/src/utils/git.ts +19 -23
- package/src/utils/jj.ts +225 -0
- package/src/utils/open.ts +37 -2
- package/src/web/kagi.ts +168 -49
- package/src/web/search/providers/anthropic.ts +1 -1
- package/src/web/search/providers/exa.ts +20 -86
- package/src/web/search/providers/kagi.ts +4 -0
package/src/sdk.ts
CHANGED
|
@@ -102,7 +102,7 @@ import { AgentSession } from "./session/agent-session";
|
|
|
102
102
|
import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
|
|
103
103
|
import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./session/auth-storage";
|
|
104
104
|
import { type CustomMessage, convertToLlm } from "./session/messages";
|
|
105
|
-
import { SessionManager } from "./session/session-manager";
|
|
105
|
+
import { getRestorableSessionModels, SessionManager } from "./session/session-manager";
|
|
106
106
|
import { closeAllConnections } from "./ssh/connection-manager";
|
|
107
107
|
import { unmountAll } from "./ssh/sshfs-mount";
|
|
108
108
|
import {
|
|
@@ -323,13 +323,13 @@ export interface CreateAgentSessionOptions {
|
|
|
323
323
|
parentHindsightSessionState?: HindsightSessionState;
|
|
324
324
|
/** Parent Mnemopi state to alias for subagent memory tools. */
|
|
325
325
|
parentMnemopiSessionState?: MnemopiSessionState;
|
|
326
|
-
/** Pre-allocated agent identity for IRC routing. Default: "
|
|
326
|
+
/** Pre-allocated agent identity for IRC routing. Default: "Main" for top-level, parentTaskPrefix-derived for sub. */
|
|
327
327
|
agentId?: string;
|
|
328
328
|
/** Display name for the agent in IRC. Default: "main" or "sub". */
|
|
329
329
|
agentDisplayName?: string;
|
|
330
330
|
/** Optional shared agent registry for IRC routing. Default: AgentRegistry.global(). */
|
|
331
331
|
agentRegistry?: AgentRegistry;
|
|
332
|
-
/** Parent task ID prefix for nested artifact naming (e.g., "
|
|
332
|
+
/** Parent task ID prefix for nested artifact naming (e.g., "Extensions") */
|
|
333
333
|
parentTaskPrefix?: string;
|
|
334
334
|
/** Inherited eval executor session id for subagents sharing parent eval state. */
|
|
335
335
|
parentEvalSessionId?: string;
|
|
@@ -1008,20 +1008,37 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1008
1008
|
);
|
|
1009
1009
|
let model = options.model;
|
|
1010
1010
|
let modelFallbackMessage: string | undefined;
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1014
|
-
|
|
1011
|
+
// Identify session model strings to restore in fallback order. We do an
|
|
1012
|
+
// initial pass here so model-dependent setup (thinking-level resolution,
|
|
1013
|
+
// host preconnect) can use the restored model; extension-registered
|
|
1014
|
+
// providers aren't visible yet, so we retry the preferred candidates once
|
|
1015
|
+
// extensions register below.
|
|
1016
|
+
const sessionModelStrings =
|
|
1017
|
+
!hasExplicitModel && hasExistingSession
|
|
1018
|
+
? getRestorableSessionModels(existingSession.models, sessionManager.getLastModelChangeRole())
|
|
1019
|
+
: [];
|
|
1020
|
+
let restoredSessionModelIndex = -1;
|
|
1021
|
+
if (!hasExplicitModel && !model && sessionModelStrings.length > 0) {
|
|
1015
1022
|
await logger.time("restoreSessionModel", async () => {
|
|
1016
|
-
|
|
1017
|
-
|
|
1023
|
+
let failedSessionModel: string | undefined;
|
|
1024
|
+
for (let i = 0; i < sessionModelStrings.length; i++) {
|
|
1025
|
+
const sessionModelStr = sessionModelStrings[i];
|
|
1026
|
+
const parsedModel = parseModelString(sessionModelStr);
|
|
1027
|
+
if (!parsedModel) {
|
|
1028
|
+
failedSessionModel ??= sessionModelStr;
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1018
1032
|
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
1019
1033
|
if (restoredModel && (await hasModelApiKey(restoredModel))) {
|
|
1020
1034
|
model = restoredModel;
|
|
1035
|
+
restoredSessionModelIndex = i;
|
|
1036
|
+
break;
|
|
1021
1037
|
}
|
|
1038
|
+
failedSessionModel ??= sessionModelStr;
|
|
1022
1039
|
}
|
|
1023
|
-
if (
|
|
1024
|
-
modelFallbackMessage = `Could not restore model ${
|
|
1040
|
+
if (failedSessionModel) {
|
|
1041
|
+
modelFallbackMessage = `Could not restore model ${failedSessionModel}`;
|
|
1025
1042
|
}
|
|
1026
1043
|
});
|
|
1027
1044
|
}
|
|
@@ -1039,26 +1056,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1039
1056
|
|
|
1040
1057
|
const taskDepth = options.taskDepth ?? 0;
|
|
1041
1058
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
//
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1059
|
+
// Resolves the session/agent thinking level using the same precedence we
|
|
1060
|
+
// apply at startup: explicit option → persisted session entry → default
|
|
1061
|
+
// role's explicit selector → selected model's defaultLevel → global
|
|
1062
|
+
// settings default. Run again after extension role reclaim so the final
|
|
1063
|
+
// model's own defaults aren't masked by an earlier fallback model's.
|
|
1064
|
+
const pickInitialThinkingLevel = (selectedModel: Model | undefined): ConfiguredThinkingLevel | undefined => {
|
|
1065
|
+
let level = options.thinkingLevel;
|
|
1066
|
+
if (level === undefined && hasExistingSession && hasThinkingEntry) {
|
|
1067
|
+
level = parseThinkingLevel(existingSession.thinkingLevel);
|
|
1068
|
+
}
|
|
1069
|
+
if (level === undefined && !hasExplicitModel && !hasThinkingEntry && defaultRoleSpec.explicitThinkingLevel) {
|
|
1070
|
+
level = defaultRoleSpec.thinkingLevel;
|
|
1071
|
+
}
|
|
1072
|
+
if (level === undefined && selectedModel?.thinking?.defaultLevel !== undefined) {
|
|
1073
|
+
level = selectedModel.thinking.defaultLevel;
|
|
1074
|
+
}
|
|
1075
|
+
if (level === undefined) {
|
|
1076
|
+
level = settings.get("defaultThinkingLevel");
|
|
1077
|
+
}
|
|
1078
|
+
return level;
|
|
1079
|
+
};
|
|
1080
|
+
let thinkingLevel = pickInitialThinkingLevel(model);
|
|
1081
|
+
let autoThinking = thinkingLevel === AUTO_THINKING;
|
|
1062
1082
|
// Concrete level the agent/session start with. With `auto` this is the
|
|
1063
1083
|
// provisional level shown until the first per-turn classification resolves;
|
|
1064
1084
|
// `auto` itself stays a session-only concept handled by AgentSession.
|
|
@@ -1444,6 +1464,40 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1444
1464
|
extensionsResult.runtime.pendingProviderRegistrations = [];
|
|
1445
1465
|
}
|
|
1446
1466
|
|
|
1467
|
+
// Retry session-model candidates now that extension providers are
|
|
1468
|
+
// registered. The initial restore runs before extensions load, so a role
|
|
1469
|
+
// model supplied by an extension would have either fallen back to the
|
|
1470
|
+
// saved default (`restoredSessionModelIndex > 0`) or failed entirely
|
|
1471
|
+
// (`restoredSessionModelIndex === -1`, with the settings default or
|
|
1472
|
+
// downstream fallback filling `model`). Reclaim it here so resume
|
|
1473
|
+
// honors the last active role in either case.
|
|
1474
|
+
const sessionRetryLimit = restoredSessionModelIndex >= 0 ? restoredSessionModelIndex : sessionModelStrings.length;
|
|
1475
|
+
if (!hasExplicitModel && sessionRetryLimit > 0) {
|
|
1476
|
+
for (let i = 0; i < sessionRetryLimit; i++) {
|
|
1477
|
+
const sessionModelStr = sessionModelStrings[i];
|
|
1478
|
+
const parsedModel = parseModelString(sessionModelStr);
|
|
1479
|
+
if (!parsedModel) continue;
|
|
1480
|
+
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
1481
|
+
if (restoredModel && (await hasModelApiKey(restoredModel))) {
|
|
1482
|
+
model = restoredModel;
|
|
1483
|
+
modelFallbackMessage = undefined;
|
|
1484
|
+
restoredSessionModelIndex = i;
|
|
1485
|
+
// Recompute thinking-level from scratch against the reclaimed
|
|
1486
|
+
// model: any value derived from the earlier fallback model's
|
|
1487
|
+
// `thinking.defaultLevel` must not become sticky.
|
|
1488
|
+
thinkingLevel = pickInitialThinkingLevel(restoredModel);
|
|
1489
|
+
autoThinking = thinkingLevel === AUTO_THINKING;
|
|
1490
|
+
effectiveThinkingLevel = thinkingLevel === AUTO_THINKING ? undefined : thinkingLevel;
|
|
1491
|
+
effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
|
|
1492
|
+
autoThinking
|
|
1493
|
+
? resolveProvisionalAutoLevel(restoredModel)
|
|
1494
|
+
: resolveThinkingLevelForModel(restoredModel, effectiveThinkingLevel),
|
|
1495
|
+
);
|
|
1496
|
+
preconnectModelHost(restoredModel.baseUrl);
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1447
1501
|
// Resolve deferred --model pattern now that extension models are registered.
|
|
1448
1502
|
if (!model && options.modelPattern) {
|
|
1449
1503
|
const availableModels = modelRegistry.getAll();
|
|
@@ -62,6 +62,7 @@ import type {
|
|
|
62
62
|
Message,
|
|
63
63
|
MessageAttribution,
|
|
64
64
|
Model,
|
|
65
|
+
ProviderResponseMetadata,
|
|
65
66
|
ProviderSessionState,
|
|
66
67
|
ServiceTier,
|
|
67
68
|
SimpleStreamOptions,
|
|
@@ -87,6 +88,7 @@ import { countTokens, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
|
87
88
|
import {
|
|
88
89
|
extractRetryHint,
|
|
89
90
|
getAgentDbPath,
|
|
91
|
+
getInstallId,
|
|
90
92
|
isEnoent,
|
|
91
93
|
isUnexpectedSocketCloseMessage,
|
|
92
94
|
logger,
|
|
@@ -164,6 +166,7 @@ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
|
|
|
164
166
|
import type { PlanModeState } from "../plan-mode/state";
|
|
165
167
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
166
168
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
169
|
+
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
|
167
170
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
168
171
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
169
172
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
@@ -227,7 +230,7 @@ import type {
|
|
|
227
230
|
SessionContext,
|
|
228
231
|
SessionManager,
|
|
229
232
|
} from "./session-manager";
|
|
230
|
-
import { getLatestCompactionEntry } from "./session-manager";
|
|
233
|
+
import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-manager";
|
|
231
234
|
import type { ShakeMode, ShakeResult } from "./shake-types";
|
|
232
235
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
233
236
|
import { YieldQueue } from "./yield-queue";
|
|
@@ -273,6 +276,8 @@ export type AgentSessionEvent =
|
|
|
273
276
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
274
277
|
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
275
278
|
|
|
279
|
+
const EMPTY_STOP_MAX_RETRIES = 3;
|
|
280
|
+
|
|
276
281
|
export interface AsyncJobSnapshot {
|
|
277
282
|
running: AsyncJobSnapshotItem[];
|
|
278
283
|
recent: AsyncJobSnapshotItem[];
|
|
@@ -358,7 +363,7 @@ export interface AgentSessionConfig {
|
|
|
358
363
|
* **MUST NOT** dispose it on their own teardown.
|
|
359
364
|
*/
|
|
360
365
|
ownedAsyncJobManager?: AsyncJobManager;
|
|
361
|
-
/** Agent identity (registry id like "
|
|
366
|
+
/** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
|
|
362
367
|
agentId?: string;
|
|
363
368
|
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
364
369
|
agentRegistry?: AgentRegistry;
|
|
@@ -581,15 +586,14 @@ function buildSessionMetadata(
|
|
|
581
586
|
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
582
587
|
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
583
588
|
userId.account_uuid = accountUuid;
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
userId.device_id = crypto.createHash("sha256").update(`omp-device-id-v1:${accountUuid}`).digest("hex");
|
|
589
|
+
// Claude Code's `device_id` is a stable 64-hex install identifier. Use
|
|
590
|
+
// omp's persistent install id as the root instead of deriving it from
|
|
591
|
+
// `account_uuid`: logging into a different Claude account on the same
|
|
592
|
+
// install should not make the device look new.
|
|
593
|
+
userId.device_id = crypto
|
|
594
|
+
.createHash("sha256")
|
|
595
|
+
.update(`omp-claude-device-id-v1:${getInstallId()}`)
|
|
596
|
+
.digest("hex");
|
|
593
597
|
}
|
|
594
598
|
}
|
|
595
599
|
return { user_id: JSON.stringify(userId) };
|
|
@@ -994,6 +998,7 @@ export class AgentSession {
|
|
|
994
998
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
995
999
|
#pendingRewindReport: string | undefined = undefined;
|
|
996
1000
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1001
|
+
#emptyStopRetryCount = 0;
|
|
997
1002
|
#promptGeneration = 0;
|
|
998
1003
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
999
1004
|
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
@@ -1104,10 +1109,12 @@ export class AgentSession {
|
|
|
1104
1109
|
this.#onResponse = configuredOnResponse
|
|
1105
1110
|
? async (response, model) => {
|
|
1106
1111
|
this.rawSseDebugBuffer.recordResponse(response, model);
|
|
1112
|
+
this.#ingestProviderUsageHeaders(response, model);
|
|
1107
1113
|
await configuredOnResponse(response, model);
|
|
1108
1114
|
}
|
|
1109
1115
|
: (response, model) => {
|
|
1110
1116
|
this.rawSseDebugBuffer.recordResponse(response, model);
|
|
1117
|
+
this.#ingestProviderUsageHeaders(response, model);
|
|
1111
1118
|
};
|
|
1112
1119
|
const configuredOnSseEvent = config.onSseEvent;
|
|
1113
1120
|
this.#onSseEvent = configuredOnSseEvent
|
|
@@ -1597,17 +1604,19 @@ export class AgentSession {
|
|
|
1597
1604
|
if (event.type === "message_update" && this.#ttsrManager?.hasRules()) {
|
|
1598
1605
|
const assistantEvent = event.assistantMessageEvent;
|
|
1599
1606
|
let matchContext: TtsrMatchContext | undefined;
|
|
1607
|
+
let streamingToolCall: ToolCall | undefined;
|
|
1600
1608
|
|
|
1601
1609
|
if (assistantEvent.type === "text_delta") {
|
|
1602
1610
|
matchContext = { source: "text" };
|
|
1603
1611
|
} else if (assistantEvent.type === "thinking_delta") {
|
|
1604
1612
|
matchContext = { source: "thinking" };
|
|
1605
1613
|
} else if (assistantEvent.type === "toolcall_delta") {
|
|
1606
|
-
|
|
1614
|
+
streamingToolCall = this.#getStreamingToolCallBlock(event.message, assistantEvent.contentIndex);
|
|
1615
|
+
matchContext = this.#getTtsrToolMatchContext(streamingToolCall, assistantEvent.contentIndex);
|
|
1607
1616
|
}
|
|
1608
1617
|
|
|
1609
1618
|
if (matchContext && "delta" in assistantEvent) {
|
|
1610
|
-
const matches = this.#
|
|
1619
|
+
const matches = this.#checkTtsrStream(assistantEvent.delta, matchContext, streamingToolCall);
|
|
1611
1620
|
if (matches.length > 0) {
|
|
1612
1621
|
// Decide first: a non-interrupting tool-source match attaches to the
|
|
1613
1622
|
// specific tool call's result instead of driving a loop-wide follow-up.
|
|
@@ -1766,6 +1775,7 @@ export class AgentSession {
|
|
|
1766
1775
|
if (
|
|
1767
1776
|
assistantMsg.stopReason !== "error" &&
|
|
1768
1777
|
assistantMsg.stopReason !== "aborted" &&
|
|
1778
|
+
!this.#isEmptyAssistantStop(assistantMsg) &&
|
|
1769
1779
|
this.#retryAttempt > 0
|
|
1770
1780
|
) {
|
|
1771
1781
|
if (this.#activeRetryFallback && this.model) {
|
|
@@ -1880,6 +1890,10 @@ export class AgentSession {
|
|
|
1880
1890
|
}
|
|
1881
1891
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
1882
1892
|
|
|
1893
|
+
if (await this.#handleEmptyAssistantStop(msg)) {
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1883
1897
|
// Check for retryable errors first (overloaded, rate limit, server errors)
|
|
1884
1898
|
if (this.#isRetryableError(msg)) {
|
|
1885
1899
|
const didRetry = await this.#handleRetryableError(msg);
|
|
@@ -2273,30 +2287,64 @@ export class AgentSession {
|
|
|
2273
2287
|
});
|
|
2274
2288
|
}
|
|
2275
2289
|
|
|
2276
|
-
/**
|
|
2277
|
-
#
|
|
2278
|
-
const context: TtsrMatchContext = { source: "tool" };
|
|
2290
|
+
/** Extract the tool-call block a toolcall_delta event refers to, if present. */
|
|
2291
|
+
#getStreamingToolCallBlock(message: AgentMessage, contentIndex: number): ToolCall | undefined {
|
|
2279
2292
|
if (message.role !== "assistant") {
|
|
2280
|
-
return
|
|
2293
|
+
return undefined;
|
|
2281
2294
|
}
|
|
2282
2295
|
|
|
2283
2296
|
const content = message.content;
|
|
2284
2297
|
if (!Array.isArray(content) || contentIndex < 0 || contentIndex >= content.length) {
|
|
2285
|
-
return
|
|
2298
|
+
return undefined;
|
|
2286
2299
|
}
|
|
2287
2300
|
|
|
2288
2301
|
const block = content[contentIndex];
|
|
2289
2302
|
if (!block || typeof block !== "object" || block.type !== "toolCall") {
|
|
2303
|
+
return undefined;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
return block as ToolCall;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
/** Build TTSR match context for tool call argument deltas. */
|
|
2310
|
+
#getTtsrToolMatchContext(toolCall: ToolCall | undefined, contentIndex: number): TtsrMatchContext {
|
|
2311
|
+
const context: TtsrMatchContext = { source: "tool" };
|
|
2312
|
+
if (!toolCall) {
|
|
2290
2313
|
return context;
|
|
2291
2314
|
}
|
|
2292
2315
|
|
|
2293
|
-
const toolCall = block as ToolCall;
|
|
2294
2316
|
context.toolName = toolCall.name;
|
|
2295
2317
|
context.streamKey = toolCall.id ? `toolcall:${toolCall.id}` : `tool:${toolCall.name}:${contentIndex}`;
|
|
2296
2318
|
context.filePaths = this.#extractTtsrFilePathsFromArgs(toolCall.arguments);
|
|
2297
2319
|
return context;
|
|
2298
2320
|
}
|
|
2299
2321
|
|
|
2322
|
+
/**
|
|
2323
|
+
* Match a stream delta against TTSR rules.
|
|
2324
|
+
*
|
|
2325
|
+
* Tool argument streams prefer the tool's `matcherDigest` normalization — the
|
|
2326
|
+
* real content the call introduces — over the raw argument delta, so rule
|
|
2327
|
+
* conditions written against source text keep working regardless of the
|
|
2328
|
+
* tool's wire format (hashline patches, JSON-escaped strings, ...).
|
|
2329
|
+
*/
|
|
2330
|
+
#checkTtsrStream(delta: string, matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Rule[] {
|
|
2331
|
+
const manager = this.#ttsrManager;
|
|
2332
|
+
if (!manager) {
|
|
2333
|
+
return [];
|
|
2334
|
+
}
|
|
2335
|
+
if (toolCall) {
|
|
2336
|
+
const tools = this.agent.state.tools;
|
|
2337
|
+
const tool =
|
|
2338
|
+
tools.find(t => t.name === toolCall.name) ??
|
|
2339
|
+
tools.find(t => t.customWireName !== undefined && t.customWireName === toolCall.name);
|
|
2340
|
+
const digest = tool?.matcherDigest?.(toolCall.arguments ?? {});
|
|
2341
|
+
if (digest !== undefined) {
|
|
2342
|
+
return manager.checkSnapshot(digest, matchContext);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
return manager.checkDelta(delta, matchContext);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2300
2348
|
/** Extract path-like arguments from tool call payload for TTSR glob matching. */
|
|
2301
2349
|
#extractTtsrFilePathsFromArgs(args: unknown): string[] | undefined {
|
|
2302
2350
|
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
@@ -2827,13 +2875,14 @@ export class AgentSession {
|
|
|
2827
2875
|
|
|
2828
2876
|
/**
|
|
2829
2877
|
* Set agent.sessionId from the session manager and install a dynamic
|
|
2830
|
-
* metadata resolver so every API request carries
|
|
2831
|
-
* like real Claude Code's `getAPIMetadata` output:
|
|
2832
|
-
*
|
|
2833
|
-
* a known account UUID is loaded
|
|
2834
|
-
*
|
|
2835
|
-
*
|
|
2836
|
-
*
|
|
2878
|
+
* metadata resolver so every Anthropic API request carries
|
|
2879
|
+
* `metadata.user_id` shaped like real Claude Code's `getAPIMetadata` output:
|
|
2880
|
+
* `{ session_id, account_uuid, device_id }`. `account_uuid` is included only
|
|
2881
|
+
* when an Anthropic OAuth credential with a known account UUID is loaded;
|
|
2882
|
+
* `device_id` is derived from the persistent omp install id. Resolving live
|
|
2883
|
+
* keeps the value in sync with auth-state changes (login/logout, token
|
|
2884
|
+
* refresh that surfaces a new account uuid) without needing to re-call
|
|
2885
|
+
* `#syncAgentSessionId()` on every such event.
|
|
2837
2886
|
*/
|
|
2838
2887
|
#syncAgentSessionId(sessionId?: string): void {
|
|
2839
2888
|
const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
@@ -4270,6 +4319,7 @@ export class AgentSession {
|
|
|
4270
4319
|
|
|
4271
4320
|
// Reset todo reminder count on new user prompt
|
|
4272
4321
|
this.#todoReminderCount = 0;
|
|
4322
|
+
this.#emptyStopRetryCount = 0;
|
|
4273
4323
|
|
|
4274
4324
|
await this.#maybeRestoreRetryFallbackPrimary();
|
|
4275
4325
|
|
|
@@ -6283,6 +6333,99 @@ export class AgentSession {
|
|
|
6283
6333
|
return lastToolCall?.name === "yield" && lastToolCall.id === toolCallId;
|
|
6284
6334
|
}
|
|
6285
6335
|
|
|
6336
|
+
async #handleEmptyAssistantStop(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
6337
|
+
if (!this.#isEmptyAssistantStop(assistantMessage)) {
|
|
6338
|
+
this.#emptyStopRetryCount = 0;
|
|
6339
|
+
return false;
|
|
6340
|
+
}
|
|
6341
|
+
|
|
6342
|
+
this.#emptyStopRetryCount++;
|
|
6343
|
+
if (this.#emptyStopRetryCount > EMPTY_STOP_MAX_RETRIES) {
|
|
6344
|
+
logger.warn("Assistant returned empty stop after retry cap", {
|
|
6345
|
+
attempts: this.#emptyStopRetryCount - 1,
|
|
6346
|
+
model: assistantMessage.model,
|
|
6347
|
+
provider: assistantMessage.provider,
|
|
6348
|
+
});
|
|
6349
|
+
if (this.#retryAttempt > 0) {
|
|
6350
|
+
await this.#emitSessionEvent({
|
|
6351
|
+
type: "auto_retry_end",
|
|
6352
|
+
success: false,
|
|
6353
|
+
attempt: this.#retryAttempt,
|
|
6354
|
+
finalError: "Assistant returned empty stop after retry cap",
|
|
6355
|
+
});
|
|
6356
|
+
this.#retryAttempt = 0;
|
|
6357
|
+
}
|
|
6358
|
+
this.#resolveRetry();
|
|
6359
|
+
return true;
|
|
6360
|
+
}
|
|
6361
|
+
|
|
6362
|
+
this.#removeEmptyStopFromActiveContext(assistantMessage);
|
|
6363
|
+
this.agent.appendMessage({
|
|
6364
|
+
role: "developer",
|
|
6365
|
+
content: [{ type: "text", text: this.#emptyStopRetryReminder() }],
|
|
6366
|
+
attribution: "agent",
|
|
6367
|
+
timestamp: Date.now(),
|
|
6368
|
+
});
|
|
6369
|
+
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
6370
|
+
return true;
|
|
6371
|
+
}
|
|
6372
|
+
|
|
6373
|
+
#isEmptyAssistantStop(assistantMessage: AssistantMessage): boolean {
|
|
6374
|
+
if (assistantMessage.stopReason !== "stop") return false;
|
|
6375
|
+
return !assistantMessage.content.some(content => {
|
|
6376
|
+
if (content.type === "text") return content.text.trim().length > 0;
|
|
6377
|
+
if (content.type === "thinking") return content.thinking.trim().length > 0;
|
|
6378
|
+
return content.type === "toolCall";
|
|
6379
|
+
});
|
|
6380
|
+
}
|
|
6381
|
+
|
|
6382
|
+
#emptyStopRetryReminder(): string {
|
|
6383
|
+
return prompt.render(emptyStopRetryTemplate, {
|
|
6384
|
+
retryCount: this.#emptyStopRetryCount,
|
|
6385
|
+
maxRetries: EMPTY_STOP_MAX_RETRIES,
|
|
6386
|
+
});
|
|
6387
|
+
}
|
|
6388
|
+
|
|
6389
|
+
#removeEmptyStopFromActiveContext(assistantMessage: AssistantMessage): void {
|
|
6390
|
+
const messages = this.agent.state.messages;
|
|
6391
|
+
const lastMessage = messages[messages.length - 1];
|
|
6392
|
+
if (
|
|
6393
|
+
lastMessage?.role === "assistant" &&
|
|
6394
|
+
this.#isSameAssistantMessage(lastMessage as AssistantMessage, assistantMessage)
|
|
6395
|
+
) {
|
|
6396
|
+
this.agent.replaceMessages(messages.slice(0, -1));
|
|
6397
|
+
}
|
|
6398
|
+
|
|
6399
|
+
const emptyStopEntry = this.sessionManager
|
|
6400
|
+
.getBranch()
|
|
6401
|
+
.slice()
|
|
6402
|
+
.reverse()
|
|
6403
|
+
.find(
|
|
6404
|
+
entry =>
|
|
6405
|
+
entry.type === "message" &&
|
|
6406
|
+
entry.message.role === "assistant" &&
|
|
6407
|
+
this.#isSameAssistantMessage(entry.message as AssistantMessage, assistantMessage),
|
|
6408
|
+
);
|
|
6409
|
+
if (!emptyStopEntry) {
|
|
6410
|
+
return;
|
|
6411
|
+
}
|
|
6412
|
+
if (emptyStopEntry.parentId === null) {
|
|
6413
|
+
this.sessionManager.resetLeaf();
|
|
6414
|
+
} else {
|
|
6415
|
+
this.sessionManager.branch(emptyStopEntry.parentId);
|
|
6416
|
+
}
|
|
6417
|
+
}
|
|
6418
|
+
|
|
6419
|
+
#isSameAssistantMessage(left: AssistantMessage, right: AssistantMessage): boolean {
|
|
6420
|
+
return (
|
|
6421
|
+
left === right ||
|
|
6422
|
+
(left.timestamp === right.timestamp &&
|
|
6423
|
+
left.provider === right.provider &&
|
|
6424
|
+
left.model === right.model &&
|
|
6425
|
+
left.stopReason === right.stopReason)
|
|
6426
|
+
);
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6286
6429
|
#enforceRewindBeforeYield(): boolean {
|
|
6287
6430
|
if (!this.#checkpointState || this.#pendingRewindReport) {
|
|
6288
6431
|
return false;
|
|
@@ -8723,28 +8866,34 @@ export class AgentSession {
|
|
|
8723
8866
|
}
|
|
8724
8867
|
|
|
8725
8868
|
// Restore model if saved
|
|
8726
|
-
const
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8869
|
+
const targetModelStrings = getRestorableSessionModels(
|
|
8870
|
+
sessionContext.models,
|
|
8871
|
+
this.sessionManager.getLastModelChangeRole(),
|
|
8872
|
+
);
|
|
8873
|
+
if (targetModelStrings.length > 0) {
|
|
8874
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
8875
|
+
let match: Model | undefined;
|
|
8876
|
+
for (const targetModelStr of targetModelStrings) {
|
|
8877
|
+
const slashIdx = targetModelStr.indexOf("/");
|
|
8878
|
+
if (slashIdx <= 0) continue;
|
|
8879
|
+
const provider = targetModelStr.slice(0, slashIdx);
|
|
8880
|
+
const modelId = targetModelStr.slice(slashIdx + 1);
|
|
8881
|
+
match = availableModels.find(m => m.provider === provider && m.id === modelId);
|
|
8882
|
+
if (match) break;
|
|
8883
|
+
}
|
|
8884
|
+
if (match) {
|
|
8885
|
+
const currentModel = this.model;
|
|
8886
|
+
const shouldResetProviderState =
|
|
8887
|
+
switchingToDifferentSession ||
|
|
8888
|
+
(currentModel !== undefined &&
|
|
8889
|
+
(currentModel.provider !== match.provider ||
|
|
8890
|
+
currentModel.id !== match.id ||
|
|
8891
|
+
currentModel.api !== match.api));
|
|
8892
|
+
if (shouldResetProviderState) {
|
|
8893
|
+
this.#setModelWithProviderSessionReset(match);
|
|
8894
|
+
} else {
|
|
8895
|
+
this.agent.setModel(match);
|
|
8896
|
+
this.#syncToolCallBatchCap(match);
|
|
8748
8897
|
}
|
|
8749
8898
|
}
|
|
8750
8899
|
}
|
|
@@ -9202,12 +9351,10 @@ export class AgentSession {
|
|
|
9202
9351
|
* Uses the last assistant message's usage data when available,
|
|
9203
9352
|
* otherwise estimates tokens for all messages.
|
|
9204
9353
|
*/
|
|
9205
|
-
getContextUsage(): ContextUsage | undefined {
|
|
9354
|
+
getContextUsage(options?: { contextWindow?: number }): ContextUsage | undefined {
|
|
9206
9355
|
const model = this.model;
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
const contextWindow = model.contextWindow ?? 0;
|
|
9210
|
-
if (contextWindow <= 0) return undefined;
|
|
9356
|
+
const contextWindow = options?.contextWindow ?? model?.contextWindow ?? 0;
|
|
9357
|
+
if (!Number.isFinite(contextWindow) || contextWindow <= 0) return undefined;
|
|
9211
9358
|
|
|
9212
9359
|
// After compaction, the last assistant usage reflects pre-compaction context size.
|
|
9213
9360
|
// We can only trust usage from an assistant that responded after the latest compaction.
|
|
@@ -9248,6 +9395,14 @@ export class AgentSession {
|
|
|
9248
9395
|
};
|
|
9249
9396
|
}
|
|
9250
9397
|
|
|
9398
|
+
#ingestProviderUsageHeaders(response: ProviderResponseMetadata, model?: Model): void {
|
|
9399
|
+
if (model?.provider !== "anthropic") return;
|
|
9400
|
+
this.#modelRegistry.authStorage.ingestUsageHeaders("anthropic", response.headers, {
|
|
9401
|
+
sessionId: this.agent.sessionId,
|
|
9402
|
+
baseUrl: this.#modelRegistry.getProviderBaseUrl?.("anthropic"),
|
|
9403
|
+
});
|
|
9404
|
+
}
|
|
9405
|
+
|
|
9251
9406
|
async fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null> {
|
|
9252
9407
|
const authStorage = this.#modelRegistry.authStorage;
|
|
9253
9408
|
if (!authStorage.fetchUsageReports) return null;
|
|
@@ -9265,7 +9420,7 @@ export class AgentSession {
|
|
|
9265
9420
|
} {
|
|
9266
9421
|
const messages = this.messages;
|
|
9267
9422
|
|
|
9268
|
-
// Find last assistant message with usage
|
|
9423
|
+
// Find last assistant message with valid usage.
|
|
9269
9424
|
let lastUsageIndex: number | null = null;
|
|
9270
9425
|
let lastUsage: Usage | undefined;
|
|
9271
9426
|
for (let i = messages.length - 1; i >= 0; i--) {
|