@oh-my-pi/pi-coding-agent 14.8.0 → 14.9.0
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 +38 -0
- package/package.json +16 -7
- package/src/config/model-resolver.ts +92 -35
- package/src/config/prompt-templates.ts +1 -1
- package/src/debug/index.ts +21 -0
- package/src/debug/raw-sse-buffer.ts +229 -0
- package/src/debug/raw-sse.ts +213 -0
- package/src/edit/index.ts +9 -10
- package/src/edit/streaming.ts +6 -5
- package/src/eval/js/context-manager.ts +91 -47
- package/src/extensibility/extensions/loader.ts +9 -3
- package/src/extensibility/extensions/types.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +99 -20
- package/src/hashline/anchors.ts +113 -0
- package/src/hashline/apply.ts +732 -0
- package/src/hashline/bigrams.json +649 -0
- package/src/hashline/constants.ts +8 -0
- package/src/hashline/diff-preview.ts +43 -0
- package/src/hashline/diff.ts +56 -0
- package/src/hashline/execute.ts +268 -0
- package/src/{edit/modes/hashline.lark → hashline/grammar.lark} +1 -1
- package/src/{edit/line-hash.ts → hashline/hash.ts} +5 -651
- package/src/hashline/index.ts +14 -0
- package/src/hashline/input.ts +110 -0
- package/src/hashline/parser.ts +220 -0
- package/src/hashline/prefixes.ts +101 -0
- package/src/hashline/recovery.ts +72 -0
- package/src/hashline/stream.ts +123 -0
- package/src/hashline/types.ts +69 -0
- package/src/hashline/utils.ts +3 -0
- package/src/index.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/render.ts +4 -0
- package/src/memories/index.ts +13 -4
- package/src/modes/components/assistant-message.ts +55 -9
- package/src/modes/components/welcome.ts +114 -38
- package/src/modes/controllers/event-controller.ts +3 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +8 -1
- package/src/modes/interactive-mode.ts +50 -11
- package/src/modes/prompt-action-autocomplete.ts +3 -0
- package/src/modes/rpc/rpc-client.ts +53 -2
- package/src/modes/rpc/rpc-mode.ts +67 -1
- package/src/modes/rpc/rpc-types.ts +17 -2
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +3 -1
- package/src/prompts/agents/reviewer.md +14 -0
- package/src/prompts/tools/hashline.md +57 -10
- package/src/sdk.ts +4 -3
- package/src/session/agent-session.ts +195 -30
- package/src/session/compaction/branch-summarization.ts +4 -2
- package/src/session/compaction/compaction.ts +22 -3
- package/src/task/executor.ts +21 -2
- package/src/task/index.ts +4 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/match-line-format.ts +1 -1
- package/src/tools/read.ts +1 -1
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/title-generator.ts +11 -0
- package/src/edit/modes/hashline.ts +0 -2039
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import * as crypto from "node:crypto";
|
|
16
17
|
import * as fs from "node:fs";
|
|
17
18
|
import * as path from "node:path";
|
|
18
19
|
|
|
@@ -67,6 +68,7 @@ import {
|
|
|
67
68
|
} from "../config/model-resolver";
|
|
68
69
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
69
70
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
71
|
+
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
70
72
|
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
|
|
71
73
|
import {
|
|
72
74
|
disposeKernelSessionsByOwner,
|
|
@@ -148,7 +150,9 @@ import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
|
148
150
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
149
151
|
import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
|
|
150
152
|
import { buildNamedToolChoice } from "../utils/tool-choice";
|
|
153
|
+
import type { AuthStorage } from "./auth-storage";
|
|
151
154
|
import {
|
|
155
|
+
type CompactionPreparation,
|
|
152
156
|
type CompactionResult,
|
|
153
157
|
calculateContextTokens,
|
|
154
158
|
calculatePromptTokens,
|
|
@@ -157,6 +161,7 @@ import {
|
|
|
157
161
|
estimateTokens,
|
|
158
162
|
generateBranchSummary,
|
|
159
163
|
prepareCompaction,
|
|
164
|
+
type SummaryOptions,
|
|
160
165
|
shouldCompact,
|
|
161
166
|
} from "./compaction";
|
|
162
167
|
import { DEFAULT_PRUNE_CONFIG, pruneToolOutputs } from "./compaction/pruning";
|
|
@@ -249,6 +254,10 @@ export interface AgentSessionConfig {
|
|
|
249
254
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
250
255
|
/** Provider response hook used by the active session request path */
|
|
251
256
|
onResponse?: SimpleStreamOptions["onResponse"];
|
|
257
|
+
/** Raw SSE hook used by the active session request path */
|
|
258
|
+
onSseEvent?: SimpleStreamOptions["onSseEvent"];
|
|
259
|
+
/** Per-session raw SSE diagnostic buffer */
|
|
260
|
+
rawSseDebugBuffer?: RawSseDebugBuffer;
|
|
252
261
|
/** Current session message-to-LLM conversion pipeline */
|
|
253
262
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
254
263
|
/** System prompt builder that can consider tool availability. Returns ordered provider-facing blocks. */
|
|
@@ -280,6 +289,13 @@ export interface AgentSessionConfig {
|
|
|
280
289
|
agentId?: string;
|
|
281
290
|
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
282
291
|
agentRegistry?: AgentRegistry;
|
|
292
|
+
/**
|
|
293
|
+
* Override the provider-facing session ID for all API requests from this session.
|
|
294
|
+
* When absent, `sessionManager.getSessionId()` is used. Needed when benchmark or
|
|
295
|
+
* SDK callers issue probes / prewarming with an explicit `--provider-session-id`
|
|
296
|
+
* so that credential sticky selection is consistent with the session's streaming calls.
|
|
297
|
+
*/
|
|
298
|
+
providerSessionId?: string;
|
|
283
299
|
}
|
|
284
300
|
|
|
285
301
|
/** Options for AgentSession.prompt() */
|
|
@@ -400,6 +416,56 @@ function todoClearKey(phaseName: string, taskContent: string): string {
|
|
|
400
416
|
return `${phaseName}\u0000${taskContent}`;
|
|
401
417
|
}
|
|
402
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Build the per-request `metadata` payload for the Anthropic provider, shaped
|
|
421
|
+
* like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
|
|
422
|
+
* device_id }`) so the backend buckets requests under one session and attributes
|
|
423
|
+
* them to the authenticated OAuth account when available. Resolved at request
|
|
424
|
+
* time so token refreshes and login/logout transitions don't strand a stale
|
|
425
|
+
* account UUID in memory. `account_uuid` and `device_id` are omitted for
|
|
426
|
+
* non-Anthropic providers to avoid leaking the user's Claude identity to
|
|
427
|
+
* third-party APIs (including Anthropic-format-compatible proxies such as
|
|
428
|
+
* cloudflare-ai-gateway or gitlab-duo).
|
|
429
|
+
*
|
|
430
|
+
* `provider` is the target provider string (e.g. `"anthropic"`) and gates the
|
|
431
|
+
* `account_uuid` and `device_id` lookups — only `"anthropic"` requests carry them.
|
|
432
|
+
*
|
|
433
|
+
* `sessionId` is forwarded to the auth-storage session-sticky lookup so that
|
|
434
|
+
* multi-credential setups attribute to the same OAuth account used for the
|
|
435
|
+
* actual API request rather than always picking the first credential.
|
|
436
|
+
*
|
|
437
|
+
* `authStorage` is treated as optional so test fixtures that stub `modelRegistry`
|
|
438
|
+
* without a real storage layer still work; the resolver simply skips the lookup
|
|
439
|
+
* and emits `{ session_id }` alone, matching the no-OAuth-credential path.
|
|
440
|
+
*/
|
|
441
|
+
function buildSessionMetadata(
|
|
442
|
+
sessionId: string,
|
|
443
|
+
provider: string,
|
|
444
|
+
authStorage: AuthStorage | undefined,
|
|
445
|
+
): Record<string, unknown> {
|
|
446
|
+
const userId: Record<string, string> = { session_id: sessionId };
|
|
447
|
+
// Only look up account_uuid when the request is going to Anthropic. Injecting
|
|
448
|
+
// a Claude OAuth account_uuid into requests bound for other providers (including
|
|
449
|
+
// Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
|
|
450
|
+
// would leak the user's Anthropic identity to unrelated third-party APIs.
|
|
451
|
+
if (provider === "anthropic") {
|
|
452
|
+
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
|
|
453
|
+
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
454
|
+
userId.account_uuid = accountUuid;
|
|
455
|
+
// Derive device_id from account_uuid so the payload matches the real CC
|
|
456
|
+
// getAPIMetadata shape without hardware fingerprinting. A SHA-256 of a
|
|
457
|
+
// namespaced account UUID produces a stable 64-hex value that is
|
|
458
|
+
// indistinguishable from a randomly generated device ID on the wire, is
|
|
459
|
+
// deterministic per account (survives reinstalls), and is auditable: it
|
|
460
|
+
// is derived solely from the OAuth UUID the user already consented to
|
|
461
|
+
// share with Anthropic. Omitted when no OAuth credential is available
|
|
462
|
+
// (API-key callers) to avoid sending a hash of an empty string.
|
|
463
|
+
userId.device_id = crypto.createHash("sha256").update(`omp-device-id-v1:${accountUuid}`).digest("hex");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return { user_id: JSON.stringify(userId) };
|
|
467
|
+
}
|
|
468
|
+
|
|
403
469
|
const noOpUIContext: ExtensionUIContext = {
|
|
404
470
|
select: async (_title, _options, _dialogOptions) => undefined,
|
|
405
471
|
confirm: async (_title, _message, _dialogOptions) => false,
|
|
@@ -503,6 +569,7 @@ export class AgentSession {
|
|
|
503
569
|
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
|
504
570
|
#agentId: string | undefined;
|
|
505
571
|
#agentRegistry: AgentRegistry | undefined;
|
|
572
|
+
#providerSessionId: string | undefined;
|
|
506
573
|
// Extension system
|
|
507
574
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
508
575
|
#turnIndex = 0;
|
|
@@ -525,6 +592,7 @@ export class AgentSession {
|
|
|
525
592
|
#transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
|
|
526
593
|
#onPayload: SimpleStreamOptions["onPayload"] | undefined;
|
|
527
594
|
#onResponse: SimpleStreamOptions["onResponse"] | undefined;
|
|
595
|
+
#onSseEvent: SimpleStreamOptions["onSseEvent"] | undefined;
|
|
528
596
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
529
597
|
#rebuildSystemPrompt:
|
|
530
598
|
| ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
|
|
@@ -576,6 +644,7 @@ export class AgentSession {
|
|
|
576
644
|
#promptGeneration = 0;
|
|
577
645
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
578
646
|
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
647
|
+
readonly rawSseDebugBuffer: RawSseDebugBuffer;
|
|
579
648
|
|
|
580
649
|
#startPowerAssertion(): void {
|
|
581
650
|
if (process.platform !== "darwin") {
|
|
@@ -622,7 +691,19 @@ export class AgentSession {
|
|
|
622
691
|
this.#toolRegistry = config.toolRegistry ?? new Map();
|
|
623
692
|
this.#transformContext = config.transformContext ?? (messages => messages);
|
|
624
693
|
this.#onPayload = config.onPayload;
|
|
625
|
-
this
|
|
694
|
+
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
695
|
+
const configuredOnResponse = config.onResponse;
|
|
696
|
+
this.#onResponse = async (response, model) => {
|
|
697
|
+
this.rawSseDebugBuffer.recordResponse(response, model);
|
|
698
|
+
await configuredOnResponse?.(response, model);
|
|
699
|
+
};
|
|
700
|
+
const configuredOnSseEvent = config.onSseEvent;
|
|
701
|
+
this.#onSseEvent = (event, model) => {
|
|
702
|
+
this.rawSseDebugBuffer.recordEvent(event, model);
|
|
703
|
+
configuredOnSseEvent?.(event, model);
|
|
704
|
+
};
|
|
705
|
+
this.agent.setProviderResponseInterceptor(this.#onResponse);
|
|
706
|
+
this.agent.setRawSseEventInterceptor(this.#onSseEvent);
|
|
626
707
|
this.#convertToLlm = config.convertToLlm ?? convertToLlm;
|
|
627
708
|
this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
|
|
628
709
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
@@ -652,6 +733,7 @@ export class AgentSession {
|
|
|
652
733
|
this.#obfuscator = config.obfuscator;
|
|
653
734
|
this.#agentId = config.agentId;
|
|
654
735
|
this.#agentRegistry = config.agentRegistry;
|
|
736
|
+
this.#providerSessionId = config.providerSessionId;
|
|
655
737
|
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
|
656
738
|
const event: AgentEvent = {
|
|
657
739
|
type: "message_update",
|
|
@@ -662,6 +744,7 @@ export class AgentSession {
|
|
|
662
744
|
this.#maybeAbortStreamingEdit(event);
|
|
663
745
|
});
|
|
664
746
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
747
|
+
this.#syncAgentSessionId();
|
|
665
748
|
this.#syncTodoPhasesFromBranch();
|
|
666
749
|
|
|
667
750
|
// Always subscribe to agent events for internal handling
|
|
@@ -1987,7 +2070,24 @@ export class AgentSession {
|
|
|
1987
2070
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
1988
2071
|
}
|
|
1989
2072
|
|
|
1990
|
-
/**
|
|
2073
|
+
/**
|
|
2074
|
+
* Set agent.sessionId from the session manager and install a dynamic
|
|
2075
|
+
* metadata resolver so every API request carries `metadata.user_id` shaped
|
|
2076
|
+
* like real Claude Code's `getAPIMetadata` output: `{ session_id,
|
|
2077
|
+
* account_uuid }` (the latter only when an Anthropic OAuth credential with
|
|
2078
|
+
* a known account UUID is loaded). Resolving live keeps the value in sync
|
|
2079
|
+
* with auth-state changes (login/logout, token refresh that surfaces a new
|
|
2080
|
+
* account uuid) without needing to re-call `#syncAgentSessionId()` on every
|
|
2081
|
+
* such event.
|
|
2082
|
+
*/
|
|
2083
|
+
#syncAgentSessionId(sessionId?: string): void {
|
|
2084
|
+
const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
2085
|
+
this.agent.sessionId = sid;
|
|
2086
|
+
this.agent.setMetadataResolver((provider: string) =>
|
|
2087
|
+
buildSessionMetadata(sid, provider, this.#modelRegistry.authStorage),
|
|
2088
|
+
);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
1991
2091
|
#rekeyHindsightMemoryForCurrentSessionId(): void {
|
|
1992
2092
|
if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
|
|
1993
2093
|
const sid = this.agent.sessionId;
|
|
@@ -2692,13 +2792,22 @@ export class AgentSession {
|
|
|
2692
2792
|
}
|
|
2693
2793
|
|
|
2694
2794
|
/** Apply session-level stream hooks to a direct side request. */
|
|
2695
|
-
prepareSimpleStreamOptions(options: SimpleStreamOptions): SimpleStreamOptions {
|
|
2795
|
+
prepareSimpleStreamOptions(options: SimpleStreamOptions, provider = "anthropic"): SimpleStreamOptions {
|
|
2696
2796
|
const sessionOnPayload = this.#onPayload;
|
|
2697
2797
|
const sessionOnResponse = this.#onResponse;
|
|
2698
|
-
|
|
2798
|
+
const sessionMetadata = this.agent.metadataForProvider(provider);
|
|
2799
|
+
const sessionOnSseEvent = this.#onSseEvent;
|
|
2800
|
+
if (!sessionOnPayload && !sessionOnResponse && !sessionMetadata && !sessionOnSseEvent) return options;
|
|
2699
2801
|
|
|
2700
2802
|
const preparedOptions: SimpleStreamOptions = { ...options };
|
|
2701
2803
|
|
|
2804
|
+
// Stamp session metadata (e.g. user_id={session_id}) onto direct-call requests so
|
|
2805
|
+
// they share the same session bucket as Agent.prompt-routed requests on Anthropic
|
|
2806
|
+
// OAuth. Caller-provided metadata wins so explicit overrides are respected.
|
|
2807
|
+
if (sessionMetadata && !options.metadata) {
|
|
2808
|
+
preparedOptions.metadata = sessionMetadata;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2702
2811
|
if (sessionOnPayload) {
|
|
2703
2812
|
if (!options.onPayload) {
|
|
2704
2813
|
preparedOptions.onPayload = sessionOnPayload;
|
|
@@ -2725,6 +2834,18 @@ export class AgentSession {
|
|
|
2725
2834
|
}
|
|
2726
2835
|
}
|
|
2727
2836
|
|
|
2837
|
+
if (sessionOnSseEvent) {
|
|
2838
|
+
if (!options.onSseEvent) {
|
|
2839
|
+
preparedOptions.onSseEvent = sessionOnSseEvent;
|
|
2840
|
+
} else {
|
|
2841
|
+
const requestOnSseEvent = options.onSseEvent;
|
|
2842
|
+
preparedOptions.onSseEvent = (event, model) => {
|
|
2843
|
+
sessionOnSseEvent(event, model);
|
|
2844
|
+
requestOnSseEvent(event, model);
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2728
2849
|
return preparedOptions;
|
|
2729
2850
|
}
|
|
2730
2851
|
|
|
@@ -2750,7 +2871,7 @@ export class AgentSession {
|
|
|
2750
2871
|
|
|
2751
2872
|
/** Current session ID */
|
|
2752
2873
|
get sessionId(): string {
|
|
2753
|
-
return this.sessionManager.getSessionId();
|
|
2874
|
+
return this.#providerSessionId ?? this.sessionManager.getSessionId();
|
|
2754
2875
|
}
|
|
2755
2876
|
|
|
2756
2877
|
/** Current session display name, if set */
|
|
@@ -3810,7 +3931,7 @@ export class AgentSession {
|
|
|
3810
3931
|
}
|
|
3811
3932
|
await this.sessionManager.newSession(options);
|
|
3812
3933
|
this.setTodoPhases([]);
|
|
3813
|
-
this
|
|
3934
|
+
this.#syncAgentSessionId();
|
|
3814
3935
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3815
3936
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
3816
3937
|
this.#steeringMessages = [];
|
|
@@ -3905,7 +4026,7 @@ export class AgentSession {
|
|
|
3905
4026
|
}
|
|
3906
4027
|
|
|
3907
4028
|
// Update agent session ID
|
|
3908
|
-
this
|
|
4029
|
+
this.#syncAgentSessionId();
|
|
3909
4030
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3910
4031
|
|
|
3911
4032
|
// Emit session_switch event with reason "fork" to hooks
|
|
@@ -4286,14 +4407,7 @@ export class AgentSession {
|
|
|
4286
4407
|
}
|
|
4287
4408
|
|
|
4288
4409
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4289
|
-
const compactionModel = this.model;
|
|
4290
|
-
const apiKey = await this.#modelRegistry.getApiKey(compactionModel, this.sessionId);
|
|
4291
|
-
if (!apiKey) {
|
|
4292
|
-
throw new Error(`No API key for ${compactionModel.provider}`);
|
|
4293
|
-
}
|
|
4294
|
-
|
|
4295
4410
|
const pathEntries = this.sessionManager.getBranch();
|
|
4296
|
-
|
|
4297
4411
|
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
4298
4412
|
if (!preparation) {
|
|
4299
4413
|
// Check why we can't compact
|
|
@@ -4363,10 +4477,8 @@ export class AgentSession {
|
|
|
4363
4477
|
preserveData ??= hookCompaction.preserveData;
|
|
4364
4478
|
} else {
|
|
4365
4479
|
// Generate compaction result
|
|
4366
|
-
const result = await
|
|
4480
|
+
const result = await this.#compactWithFallbackModel(
|
|
4367
4481
|
preparation,
|
|
4368
|
-
compactionModel,
|
|
4369
|
-
apiKey,
|
|
4370
4482
|
customInstructions,
|
|
4371
4483
|
compactionAbortController.signal,
|
|
4372
4484
|
{
|
|
@@ -4616,7 +4728,7 @@ export class AgentSession {
|
|
|
4616
4728
|
this.#asyncJobManager?.cancelAll();
|
|
4617
4729
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
4618
4730
|
this.agent.reset();
|
|
4619
|
-
this
|
|
4731
|
+
this.#syncAgentSessionId();
|
|
4620
4732
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
4621
4733
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
4622
4734
|
this.#steeringMessages = [];
|
|
@@ -5286,6 +5398,50 @@ export class AgentSession {
|
|
|
5286
5398
|
|
|
5287
5399
|
return candidates;
|
|
5288
5400
|
}
|
|
5401
|
+
#isCompactionAuthFailure(error: unknown): boolean {
|
|
5402
|
+
if (!(error instanceof Error)) return false;
|
|
5403
|
+
return /auth_unavailable|no auth available/i.test(error.message);
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
#buildCompactionAuthError(): Error {
|
|
5407
|
+
const currentModel = this.model;
|
|
5408
|
+
if (!currentModel) {
|
|
5409
|
+
return new Error(
|
|
5410
|
+
"Compaction requires a model with usable credentials, but no authenticated compaction model is available.",
|
|
5411
|
+
);
|
|
5412
|
+
}
|
|
5413
|
+
return new Error(
|
|
5414
|
+
`Compaction requires usable credentials for ${currentModel.provider}/${currentModel.id}. ` +
|
|
5415
|
+
`Configure ${currentModel.provider} credentials or assign an authenticated fallback role such as modelRoles.smol.`,
|
|
5416
|
+
);
|
|
5417
|
+
}
|
|
5418
|
+
|
|
5419
|
+
async #compactWithFallbackModel(
|
|
5420
|
+
preparation: CompactionPreparation,
|
|
5421
|
+
customInstructions: string | undefined,
|
|
5422
|
+
signal: AbortSignal,
|
|
5423
|
+
options?: SummaryOptions,
|
|
5424
|
+
): Promise<CompactionResult> {
|
|
5425
|
+
const candidates = this.#getCompactionModelCandidates(this.#modelRegistry.getAvailable());
|
|
5426
|
+
|
|
5427
|
+
for (const candidate of candidates) {
|
|
5428
|
+
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
5429
|
+
if (!apiKey) continue;
|
|
5430
|
+
|
|
5431
|
+
try {
|
|
5432
|
+
return await compact(preparation, candidate, apiKey, customInstructions, signal, {
|
|
5433
|
+
...options,
|
|
5434
|
+
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
5435
|
+
});
|
|
5436
|
+
} catch (error) {
|
|
5437
|
+
if (!this.#isCompactionAuthFailure(error)) {
|
|
5438
|
+
throw error;
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
throw this.#buildCompactionAuthError();
|
|
5444
|
+
}
|
|
5289
5445
|
|
|
5290
5446
|
/**
|
|
5291
5447
|
* Internal: Run auto-compaction with events.
|
|
@@ -5487,6 +5643,7 @@ export class AgentSession {
|
|
|
5487
5643
|
promptOverride: hookPrompt,
|
|
5488
5644
|
extraContext: hookContext,
|
|
5489
5645
|
remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
|
|
5646
|
+
metadata: this.agent.metadataForProvider(candidate.provider),
|
|
5490
5647
|
initiatorOverride: "agent",
|
|
5491
5648
|
});
|
|
5492
5649
|
break;
|
|
@@ -5496,6 +5653,10 @@ export class AgentSession {
|
|
|
5496
5653
|
}
|
|
5497
5654
|
|
|
5498
5655
|
const message = error instanceof Error ? error.message : String(error);
|
|
5656
|
+
if (this.#isCompactionAuthFailure(error)) {
|
|
5657
|
+
lastError = this.#buildCompactionAuthError();
|
|
5658
|
+
break;
|
|
5659
|
+
}
|
|
5499
5660
|
const retryAfterMs = this.#parseRetryAfterMsFromError(message);
|
|
5500
5661
|
const shouldRetry =
|
|
5501
5662
|
retrySettings.enabled &&
|
|
@@ -6606,15 +6767,18 @@ export class AgentSession {
|
|
|
6606
6767
|
systemPrompt: this.systemPrompt,
|
|
6607
6768
|
messages: llmMessages,
|
|
6608
6769
|
};
|
|
6609
|
-
const options = this.prepareSimpleStreamOptions(
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6770
|
+
const options = this.prepareSimpleStreamOptions(
|
|
6771
|
+
{
|
|
6772
|
+
apiKey,
|
|
6773
|
+
sessionId: this.sessionId,
|
|
6774
|
+
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
6775
|
+
hideThinkingSummary: this.agent.hideThinkingSummary,
|
|
6776
|
+
serviceTier: this.serviceTier,
|
|
6777
|
+
signal: args.signal,
|
|
6778
|
+
toolChoice: "none",
|
|
6779
|
+
},
|
|
6780
|
+
model.provider,
|
|
6781
|
+
);
|
|
6618
6782
|
|
|
6619
6783
|
let replyText = "";
|
|
6620
6784
|
let assistantMessage: AssistantMessage | undefined;
|
|
@@ -6791,7 +6955,7 @@ export class AgentSession {
|
|
|
6791
6955
|
|
|
6792
6956
|
try {
|
|
6793
6957
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
6794
|
-
this
|
|
6958
|
+
this.#syncAgentSessionId();
|
|
6795
6959
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6796
6960
|
|
|
6797
6961
|
const sessionContext = this.buildDisplaySessionContext();
|
|
@@ -6869,7 +7033,7 @@ export class AgentSession {
|
|
|
6869
7033
|
return true;
|
|
6870
7034
|
} catch (error) {
|
|
6871
7035
|
this.sessionManager.restoreState(previousSessionState);
|
|
6872
|
-
this
|
|
7036
|
+
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
6873
7037
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6874
7038
|
let restoreMcpError: unknown;
|
|
6875
7039
|
try {
|
|
@@ -6961,7 +7125,7 @@ export class AgentSession {
|
|
|
6961
7125
|
this.sessionManager.createBranchedSession(selectedEntry.parentId);
|
|
6962
7126
|
}
|
|
6963
7127
|
this.#syncTodoPhasesFromBranch();
|
|
6964
|
-
this
|
|
7128
|
+
this.#syncAgentSessionId();
|
|
6965
7129
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6966
7130
|
this.#resetHindsightConversationTrackingIfHindsight();
|
|
6967
7131
|
|
|
@@ -7082,6 +7246,7 @@ export class AgentSession {
|
|
|
7082
7246
|
signal: this.#branchSummaryAbortController.signal,
|
|
7083
7247
|
customInstructions: options.customInstructions,
|
|
7084
7248
|
reserveTokens: branchSummarySettings.reserveTokens,
|
|
7249
|
+
metadata: this.agent.metadataForProvider(model.provider),
|
|
7085
7250
|
});
|
|
7086
7251
|
this.#branchSummaryAbortController = undefined;
|
|
7087
7252
|
if (result.aborted) {
|
|
@@ -75,6 +75,8 @@ export interface GenerateBranchSummaryOptions {
|
|
|
75
75
|
customInstructions?: string;
|
|
76
76
|
/** Tokens reserved for prompt + LLM response (default 16384) */
|
|
77
77
|
reserveTokens?: number;
|
|
78
|
+
/** Optional metadata forwarded to the underlying API request (e.g. user_id for session attribution). */
|
|
79
|
+
metadata?: Record<string, unknown>;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
// ============================================================================
|
|
@@ -258,7 +260,7 @@ export async function generateBranchSummary(
|
|
|
258
260
|
entries: SessionEntry[],
|
|
259
261
|
options: GenerateBranchSummaryOptions,
|
|
260
262
|
): Promise<BranchSummaryResult> {
|
|
261
|
-
const { model, apiKey, signal, customInstructions, reserveTokens = 16384 } = options;
|
|
263
|
+
const { model, apiKey, signal, customInstructions, reserveTokens = 16384, metadata } = options;
|
|
262
264
|
|
|
263
265
|
// Token budget = context window minus reserved space for prompt + response
|
|
264
266
|
const contextWindow = model.contextWindow || 128000;
|
|
@@ -291,7 +293,7 @@ export async function generateBranchSummary(
|
|
|
291
293
|
const response = await completeSimple(
|
|
292
294
|
model,
|
|
293
295
|
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
294
|
-
{ apiKey, signal, maxTokens: 2048 },
|
|
296
|
+
{ apiKey, signal, maxTokens: 2048, metadata },
|
|
295
297
|
);
|
|
296
298
|
|
|
297
299
|
// Check if aborted or errored
|
|
@@ -965,6 +965,7 @@ export interface SummaryOptions {
|
|
|
965
965
|
remoteEndpoint?: string;
|
|
966
966
|
remoteInstructions?: string;
|
|
967
967
|
initiatorOverride?: MessageAttribution;
|
|
968
|
+
metadata?: Record<string, unknown>;
|
|
968
969
|
}
|
|
969
970
|
|
|
970
971
|
export async function generateSummary(
|
|
@@ -1020,7 +1021,14 @@ export async function generateSummary(
|
|
|
1020
1021
|
const response = await completeSimple(
|
|
1021
1022
|
model,
|
|
1022
1023
|
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
1023
|
-
{
|
|
1024
|
+
{
|
|
1025
|
+
maxTokens,
|
|
1026
|
+
signal,
|
|
1027
|
+
apiKey,
|
|
1028
|
+
reasoning: Effort.High,
|
|
1029
|
+
initiatorOverride: options?.initiatorOverride,
|
|
1030
|
+
metadata: options?.metadata,
|
|
1031
|
+
},
|
|
1024
1032
|
);
|
|
1025
1033
|
|
|
1026
1034
|
if (response.stopReason === "error") {
|
|
@@ -1069,7 +1077,14 @@ async function generateShortSummary(
|
|
|
1069
1077
|
systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT],
|
|
1070
1078
|
messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }],
|
|
1071
1079
|
},
|
|
1072
|
-
{
|
|
1080
|
+
{
|
|
1081
|
+
maxTokens,
|
|
1082
|
+
signal,
|
|
1083
|
+
apiKey,
|
|
1084
|
+
reasoning: Effort.High,
|
|
1085
|
+
initiatorOverride: options?.initiatorOverride,
|
|
1086
|
+
metadata: options?.metadata,
|
|
1087
|
+
},
|
|
1073
1088
|
);
|
|
1074
1089
|
|
|
1075
1090
|
if (response.stopReason === "error") {
|
|
@@ -1249,6 +1264,7 @@ export async function compact(
|
|
|
1249
1264
|
remoteEndpoint: settings.remoteEnabled === false ? undefined : settings.remoteEndpoint,
|
|
1250
1265
|
remoteInstructions: options?.remoteInstructions,
|
|
1251
1266
|
initiatorOverride: options?.initiatorOverride,
|
|
1267
|
+
metadata: options?.metadata,
|
|
1252
1268
|
};
|
|
1253
1269
|
|
|
1254
1270
|
let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
|
|
@@ -1304,6 +1320,7 @@ export async function compact(
|
|
|
1304
1320
|
apiKey,
|
|
1305
1321
|
signal,
|
|
1306
1322
|
summaryOptions.initiatorOverride,
|
|
1323
|
+
summaryOptions.metadata,
|
|
1307
1324
|
),
|
|
1308
1325
|
]);
|
|
1309
1326
|
// Merge into single summary
|
|
@@ -1339,6 +1356,7 @@ export async function compact(
|
|
|
1339
1356
|
extraContext: options?.extraContext,
|
|
1340
1357
|
remoteEndpoint: summaryOptions.remoteEndpoint,
|
|
1341
1358
|
initiatorOverride: summaryOptions.initiatorOverride,
|
|
1359
|
+
metadata: summaryOptions.metadata,
|
|
1342
1360
|
},
|
|
1343
1361
|
);
|
|
1344
1362
|
|
|
@@ -1370,6 +1388,7 @@ async function generateTurnPrefixSummary(
|
|
|
1370
1388
|
apiKey: string,
|
|
1371
1389
|
signal?: AbortSignal,
|
|
1372
1390
|
initiatorOverride?: MessageAttribution,
|
|
1391
|
+
metadata?: Record<string, unknown>,
|
|
1373
1392
|
): Promise<string> {
|
|
1374
1393
|
const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
|
|
1375
1394
|
|
|
@@ -1387,7 +1406,7 @@ async function generateTurnPrefixSummary(
|
|
|
1387
1406
|
const response = await completeSimple(
|
|
1388
1407
|
model,
|
|
1389
1408
|
{ systemPrompt: [SUMMARIZATION_SYSTEM_PROMPT], messages: summarizationMessages },
|
|
1390
|
-
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride },
|
|
1409
|
+
{ maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride, metadata },
|
|
1391
1410
|
);
|
|
1392
1411
|
|
|
1393
1412
|
if (response.stopReason === "error") {
|
package/src/task/executor.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
|
10
10
|
import type { TSchema } from "@sinclair/typebox";
|
|
11
11
|
import Ajv, { type ValidateFunction } from "ajv";
|
|
12
12
|
import { ModelRegistry } from "../config/model-registry";
|
|
13
|
-
import {
|
|
13
|
+
import { resolveModelOverrideWithAuthFallback } from "../config/model-resolver";
|
|
14
14
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
15
15
|
import { Settings } from "../config/settings";
|
|
16
16
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
@@ -144,6 +144,11 @@ export interface ExecutorOptions {
|
|
|
144
144
|
index: number;
|
|
145
145
|
id: string;
|
|
146
146
|
modelOverride?: string | string[];
|
|
147
|
+
/**
|
|
148
|
+
* Active model selector of the parent session, used as an auth-aware fallback
|
|
149
|
+
* if the resolved subagent model has no working credentials. See #985.
|
|
150
|
+
*/
|
|
151
|
+
parentActiveModelPattern?: string;
|
|
147
152
|
thinkingLevel?: ThinkingLevel;
|
|
148
153
|
outputSchema?: unknown;
|
|
149
154
|
/** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
|
|
@@ -944,7 +949,21 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
944
949
|
model,
|
|
945
950
|
thinkingLevel: resolvedThinkingLevel,
|
|
946
951
|
explicitThinkingLevel,
|
|
947
|
-
|
|
952
|
+
authFallbackUsed,
|
|
953
|
+
} = await resolveModelOverrideWithAuthFallback(
|
|
954
|
+
modelPatterns,
|
|
955
|
+
options.parentActiveModelPattern,
|
|
956
|
+
modelRegistry,
|
|
957
|
+
settings,
|
|
958
|
+
);
|
|
959
|
+
if (authFallbackUsed && model) {
|
|
960
|
+
logger.warn("Subagent model has no working credentials; falling back to parent session model", {
|
|
961
|
+
requested: modelPatterns,
|
|
962
|
+
parentModel: options.parentActiveModelPattern,
|
|
963
|
+
resolvedProvider: model.provider,
|
|
964
|
+
resolvedModel: model.id,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
948
967
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
949
968
|
? resolvedThinkingLevel
|
|
950
969
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
package/src/task/index.ts
CHANGED
|
@@ -583,11 +583,12 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
583
583
|
// Apply per-agent model override from settings (highest priority)
|
|
584
584
|
const agentModelOverrides = this.session.settings.get("task.agentModelOverrides");
|
|
585
585
|
const settingsModelOverride = agentModelOverrides[agentName];
|
|
586
|
+
const parentActiveModelPattern = this.session.getActiveModelString?.();
|
|
586
587
|
const modelOverride = resolveAgentModelPatterns({
|
|
587
588
|
settingsOverride: settingsModelOverride,
|
|
588
589
|
agentModel: effectiveAgent.model,
|
|
589
590
|
settings: this.session.settings,
|
|
590
|
-
activeModelPattern:
|
|
591
|
+
activeModelPattern: parentActiveModelPattern,
|
|
591
592
|
fallbackModelPattern: this.session.getModelString?.(),
|
|
592
593
|
});
|
|
593
594
|
const thinkingLevelOverride = effectiveAgent.thinkingLevel;
|
|
@@ -843,6 +844,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
843
844
|
id: task.id,
|
|
844
845
|
taskDepth,
|
|
845
846
|
modelOverride,
|
|
847
|
+
parentActiveModelPattern,
|
|
846
848
|
thinkingLevel: thinkingLevelOverride,
|
|
847
849
|
outputSchema: effectiveOutputSchema,
|
|
848
850
|
sessionFile,
|
|
@@ -900,6 +902,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
900
902
|
id: task.id,
|
|
901
903
|
taskDepth,
|
|
902
904
|
modelOverride,
|
|
905
|
+
parentActiveModelPattern,
|
|
903
906
|
thinkingLevel: thinkingLevelOverride,
|
|
904
907
|
outputSchema: effectiveOutputSchema,
|
|
905
908
|
sessionFile,
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -5,8 +5,8 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
-
import { computeLineHash, HL_BODY_SEP } from "../edit/line-hash";
|
|
9
8
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
9
|
+
import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
|
|
10
10
|
import type { Theme } from "../modes/theme/theme";
|
|
11
11
|
import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
|
|
12
12
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
package/src/tools/read.ts
CHANGED
|
@@ -9,9 +9,9 @@ import { Text } from "@oh-my-pi/pi-tui";
|
|
|
9
9
|
import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
10
|
import { type Static, Type } from "@sinclair/typebox";
|
|
11
11
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
12
|
-
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../edit/line-hash";
|
|
13
12
|
import { isNotebookPath, readEditableNotebookText } from "../edit/notebook";
|
|
14
13
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
14
|
+
import { formatHashLine, formatHashLines, formatLineHash, HL_BODY_SEP } from "../hashline/hash";
|
|
15
15
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
16
16
|
import type { InternalUrl } from "../internal-urls/types";
|
|
17
17
|
import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
|
|
@@ -11,7 +11,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
11
11
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
12
12
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
13
13
|
import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
14
|
-
import { formatHashLines } from "../
|
|
14
|
+
import { formatHashLines } from "../hashline/hash";
|
|
15
15
|
import type { FileMentionMessage } from "../session/messages";
|
|
16
16
|
import {
|
|
17
17
|
DEFAULT_MAX_BYTES,
|