@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3
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 +72 -0
- package/dist/cli.js +3621 -3579
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +113 -12
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -3
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +84 -12
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +32 -7
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +438 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +77 -4
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +107 -8
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/hashline/diff.ts +57 -4
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +76 -5
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +145 -14
- package/src/modes/components/status-line/segments.ts +47 -22
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +114 -11
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -10
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +46 -27
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +40 -50
- package/src/session/agent-session.ts +1139 -600
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +192 -21
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +96 -3
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +37 -7
- package/src/tools/ask.ts +55 -4
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/tts.ts +2 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
|
@@ -39,13 +39,16 @@ import {
|
|
|
39
39
|
type SessionHeader,
|
|
40
40
|
type SessionInitEntry,
|
|
41
41
|
type SessionMessageEntry,
|
|
42
|
+
type SessionTitleSource,
|
|
42
43
|
type SessionTreeNode,
|
|
43
44
|
type ThinkingLevelChangeEntry,
|
|
45
|
+
TITLE_CHANGE_ENTRY_TYPE,
|
|
46
|
+
type TitleChangeEntry,
|
|
44
47
|
type TtsrInjectionEntry,
|
|
45
48
|
type UsageStatistics,
|
|
46
49
|
} from "./session-entries";
|
|
47
50
|
import { findMostRecentSession, listAllSessions, listSessions, type SessionInfo } from "./session-listing";
|
|
48
|
-
import { loadEntriesFromFile, resolveBlobRefsInEntries } from "./session-loader";
|
|
51
|
+
import { loadEntriesFromFile, readTitleSlotFromFile, resolveBlobRefsInEntries } from "./session-loader";
|
|
49
52
|
import { generateId, migrateToCurrentVersion } from "./session-migrations";
|
|
50
53
|
import {
|
|
51
54
|
computeDefaultSessionDir,
|
|
@@ -60,6 +63,7 @@ import {
|
|
|
60
63
|
type SessionStorage,
|
|
61
64
|
type SessionStorageWriter,
|
|
62
65
|
} from "./session-storage";
|
|
66
|
+
import { type SessionTitleUpdate, serializeTitleSlot } from "./session-title-slot";
|
|
63
67
|
|
|
64
68
|
const JSONL_SUFFIX_LENGTH = ".jsonl".length;
|
|
65
69
|
|
|
@@ -298,8 +302,10 @@ interface SessionManagerStateSnapshot {
|
|
|
298
302
|
sessionDir: string;
|
|
299
303
|
sessionId: string;
|
|
300
304
|
sessionName: string | undefined;
|
|
301
|
-
titleSource:
|
|
305
|
+
titleSource: SessionTitleSource | undefined;
|
|
302
306
|
sessionFile: string | undefined;
|
|
307
|
+
titleUpdatedAt: string;
|
|
308
|
+
hasTitleSlot: boolean;
|
|
303
309
|
onDisk: boolean;
|
|
304
310
|
needsRewrite: boolean;
|
|
305
311
|
header: SessionHeader;
|
|
@@ -334,9 +340,11 @@ export class SessionManager {
|
|
|
334
340
|
|
|
335
341
|
#sessionId = "";
|
|
336
342
|
#sessionName: string | undefined;
|
|
337
|
-
#titleSource:
|
|
343
|
+
#titleSource: SessionTitleSource | undefined;
|
|
338
344
|
#sessionFile: string | undefined;
|
|
339
345
|
#header!: SessionHeader;
|
|
346
|
+
#titleUpdatedAt = "";
|
|
347
|
+
#hasTitleSlot = true;
|
|
340
348
|
#entries: SessionEntry[] = [];
|
|
341
349
|
#index = new SessionEntryIndex();
|
|
342
350
|
|
|
@@ -471,8 +479,17 @@ export class SessionManager {
|
|
|
471
479
|
return `${JSON.stringify(prepareEntryForPersistence(entry, this.#blobs))}\n`;
|
|
472
480
|
}
|
|
473
481
|
|
|
482
|
+
#titleSlotLine(): string {
|
|
483
|
+
return serializeTitleSlot({
|
|
484
|
+
title: this.#sessionName,
|
|
485
|
+
source: this.#titleSource,
|
|
486
|
+
updatedAt: this.#titleUpdatedAt || this.#header.timestamp,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
474
490
|
#fileBody(): string {
|
|
475
|
-
let body = this.#
|
|
491
|
+
let body = this.#titleSlotLine();
|
|
492
|
+
body += this.#lineFor(this.#header);
|
|
476
493
|
for (const entry of this.#entries) body += this.#lineFor(entry);
|
|
477
494
|
return body;
|
|
478
495
|
}
|
|
@@ -501,6 +518,7 @@ export class SessionManager {
|
|
|
501
518
|
this.#storage.writeTextSync(this.#sessionFile, body);
|
|
502
519
|
this.#fileIsCurrent = true;
|
|
503
520
|
this.#rewriteRequired = false;
|
|
521
|
+
this.#hasTitleSlot = true;
|
|
504
522
|
} catch (err) {
|
|
505
523
|
this.#noteDiskFailure(err);
|
|
506
524
|
}
|
|
@@ -523,6 +541,7 @@ export class SessionManager {
|
|
|
523
541
|
await this.#storage.writeTextAtomic(sessionFile, this.#fileBody());
|
|
524
542
|
this.#fileIsCurrent = true;
|
|
525
543
|
this.#rewriteRequired = false;
|
|
544
|
+
this.#hasTitleSlot = true;
|
|
526
545
|
},
|
|
527
546
|
{ epoch },
|
|
528
547
|
);
|
|
@@ -561,12 +580,67 @@ export class SessionManager {
|
|
|
561
580
|
}
|
|
562
581
|
}
|
|
563
582
|
|
|
583
|
+
async #persistTitleChangeEntry(entry: TitleChangeEntry, update: SessionTitleUpdate): Promise<void> {
|
|
584
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
585
|
+
if (this.#diskFailure) throw this.#diskFailure;
|
|
586
|
+
|
|
587
|
+
if (!this.#shouldHaveSessionFile()) {
|
|
588
|
+
this.#fileIsCurrent = false;
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (
|
|
593
|
+
!this.#fileIsCurrent ||
|
|
594
|
+
this.#rewriteRequired ||
|
|
595
|
+
!this.#hasTitleSlot ||
|
|
596
|
+
!this.#storage.existsSync(this.#sessionFile)
|
|
597
|
+
) {
|
|
598
|
+
await this.#rewriteAtomically();
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const epoch = this.#diskEpoch;
|
|
603
|
+
const line = this.#lineFor(entry);
|
|
604
|
+
await this.#scheduleDiskWork(
|
|
605
|
+
async () => {
|
|
606
|
+
const sessionFile = this.#sessionFile;
|
|
607
|
+
if (!sessionFile) return;
|
|
608
|
+
try {
|
|
609
|
+
await this.#appendWriter().append(line);
|
|
610
|
+
await this.#storage.updateSessionTitle(sessionFile, update);
|
|
611
|
+
this.#fileIsCurrent = true;
|
|
612
|
+
} catch {
|
|
613
|
+
await this.#closeWriterHandle();
|
|
614
|
+
await this.#storage.writeTextAtomic(sessionFile, this.#fileBody());
|
|
615
|
+
this.#clearDiskError();
|
|
616
|
+
this.#fileIsCurrent = true;
|
|
617
|
+
this.#rewriteRequired = false;
|
|
618
|
+
this.#hasTitleSlot = true;
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
{ epoch },
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#notifyEntryAppended(entry: SessionEntry): void {
|
|
626
|
+
const callback = this.onEntryAppended;
|
|
627
|
+
if (callback) {
|
|
628
|
+
try {
|
|
629
|
+
callback(entry);
|
|
630
|
+
} catch (err) {
|
|
631
|
+
logger.warn("collab entry hook failed", { error: String(err) });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
564
636
|
#resetToNewSession(options?: NewSessionOptions, forcedSessionFile?: string): string | undefined {
|
|
565
637
|
this.#diskTail = Promise.resolve();
|
|
566
638
|
this.#clearDiskError();
|
|
567
639
|
this.#sessionId = mintSessionId();
|
|
568
640
|
this.#sessionName = undefined;
|
|
569
641
|
this.#titleSource = undefined;
|
|
642
|
+
this.#titleUpdatedAt = "";
|
|
643
|
+
this.#hasTitleSlot = true;
|
|
570
644
|
|
|
571
645
|
const timestamp = nowIso();
|
|
572
646
|
this.#header = {
|
|
@@ -577,6 +651,7 @@ export class SessionManager {
|
|
|
577
651
|
cwd: this.#cwd,
|
|
578
652
|
parentSession: options?.parentSession,
|
|
579
653
|
};
|
|
654
|
+
this.#titleUpdatedAt = timestamp;
|
|
580
655
|
|
|
581
656
|
this.#entries = [];
|
|
582
657
|
this.#index.clear();
|
|
@@ -611,6 +686,7 @@ export class SessionManager {
|
|
|
611
686
|
this.#sessionId = header.id;
|
|
612
687
|
this.#sessionName = header.title;
|
|
613
688
|
this.#titleSource = header.titleSource;
|
|
689
|
+
this.#titleUpdatedAt = header.timestamp;
|
|
614
690
|
this.#index.rebuild(entries);
|
|
615
691
|
}
|
|
616
692
|
|
|
@@ -626,15 +702,7 @@ export class SessionManager {
|
|
|
626
702
|
this.#entries.push(entry);
|
|
627
703
|
this.#index.insert(entry);
|
|
628
704
|
this.#appendToSessionFile(entry);
|
|
629
|
-
|
|
630
|
-
const callback = this.onEntryAppended;
|
|
631
|
-
if (callback) {
|
|
632
|
-
try {
|
|
633
|
-
callback(entry);
|
|
634
|
-
} catch (err) {
|
|
635
|
-
logger.warn("collab entry hook failed", { error: String(err) });
|
|
636
|
-
}
|
|
637
|
-
}
|
|
705
|
+
this.#notifyEntryAppended(entry);
|
|
638
706
|
}
|
|
639
707
|
|
|
640
708
|
#draftPath(): string | null {
|
|
@@ -693,6 +761,8 @@ export class SessionManager {
|
|
|
693
761
|
sessionId: this.#sessionId,
|
|
694
762
|
sessionName: this.#sessionName,
|
|
695
763
|
titleSource: this.#titleSource,
|
|
764
|
+
titleUpdatedAt: this.#titleUpdatedAt,
|
|
765
|
+
hasTitleSlot: this.#hasTitleSlot,
|
|
696
766
|
sessionFile: this.#sessionFile,
|
|
697
767
|
onDisk: this.#fileIsCurrent,
|
|
698
768
|
needsRewrite: this.#rewriteRequired,
|
|
@@ -717,6 +787,8 @@ export class SessionManager {
|
|
|
717
787
|
this.#applyEntries(snapshot.header, [...snapshot.entries]);
|
|
718
788
|
this.#sessionName = snapshot.sessionName;
|
|
719
789
|
this.#titleSource = snapshot.titleSource;
|
|
790
|
+
this.#titleUpdatedAt = snapshot.titleUpdatedAt;
|
|
791
|
+
this.#hasTitleSlot = snapshot.hasTitleSlot;
|
|
720
792
|
this.#artifactManager = null;
|
|
721
793
|
this.#artifactManagerSessionFile = null;
|
|
722
794
|
this.#adoptedArtifactManager = null;
|
|
@@ -733,6 +805,7 @@ export class SessionManager {
|
|
|
733
805
|
this.#sessionFile = resolvedSessionFile;
|
|
734
806
|
this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
|
|
735
807
|
|
|
808
|
+
const titleSlot = await readTitleSlotFromFile(resolvedSessionFile, this.#storage);
|
|
736
809
|
const fileEntries = await loadEntriesFromFile(resolvedSessionFile, this.#storage);
|
|
737
810
|
if (fileEntries.length === 0) {
|
|
738
811
|
// Explicit but empty/missing path (e.g. --session flag): start fresh but
|
|
@@ -763,6 +836,8 @@ export class SessionManager {
|
|
|
763
836
|
}
|
|
764
837
|
|
|
765
838
|
this.#applyEntries(header, fileEntries.slice(1) as SessionEntry[]);
|
|
839
|
+
this.#titleUpdatedAt = titleSlot?.updatedAt ?? header.timestamp;
|
|
840
|
+
this.#hasTitleSlot = titleSlot !== undefined;
|
|
766
841
|
this.#fileIsCurrent = true;
|
|
767
842
|
this.#rewriteRequired = migrated;
|
|
768
843
|
this.#forceFileCreation = true;
|
|
@@ -815,6 +890,8 @@ export class SessionManager {
|
|
|
815
890
|
};
|
|
816
891
|
this.#sessionName = this.#header.title;
|
|
817
892
|
this.#titleSource = this.#header.titleSource;
|
|
893
|
+
this.#titleUpdatedAt = timestamp;
|
|
894
|
+
this.#hasTitleSlot = true;
|
|
818
895
|
this.#fileIsCurrent = false;
|
|
819
896
|
this.#rewriteRequired = false;
|
|
820
897
|
this.#forceFileCreation = true;
|
|
@@ -1083,7 +1160,7 @@ export class SessionManager {
|
|
|
1083
1160
|
}
|
|
1084
1161
|
|
|
1085
1162
|
/** The source that set the session name: "user" (manual/RPC) or "auto" (generated title). */
|
|
1086
|
-
get titleSource():
|
|
1163
|
+
get titleSource(): SessionTitleSource | undefined {
|
|
1087
1164
|
return this.#titleSource;
|
|
1088
1165
|
}
|
|
1089
1166
|
|
|
@@ -1103,20 +1180,33 @@ export class SessionManager {
|
|
|
1103
1180
|
* @param source "user" for explicit renames; "auto" for generated titles.
|
|
1104
1181
|
* Auto titles are ignored once the user has set a name.
|
|
1105
1182
|
*/
|
|
1106
|
-
async setSessionName(name: string, source:
|
|
1183
|
+
async setSessionName(name: string, source: SessionTitleSource = "auto", trigger?: string): Promise<boolean> {
|
|
1107
1184
|
if (this.#titleSource === "user" && source === "auto") return false;
|
|
1108
1185
|
|
|
1109
1186
|
const title = SessionManager.#cleanTitle(name);
|
|
1110
1187
|
if (!title) return false;
|
|
1111
1188
|
|
|
1189
|
+
const previousTitle = this.#sessionName;
|
|
1190
|
+
const timestamp = nowIso();
|
|
1112
1191
|
this.#sessionName = title;
|
|
1113
1192
|
this.#titleSource = source;
|
|
1193
|
+
this.#titleUpdatedAt = timestamp;
|
|
1114
1194
|
this.#header.title = title;
|
|
1115
1195
|
this.#header.titleSource = source;
|
|
1116
1196
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1197
|
+
const entry: TitleChangeEntry = {
|
|
1198
|
+
type: TITLE_CHANGE_ENTRY_TYPE,
|
|
1199
|
+
...this.#freshEntryFields(),
|
|
1200
|
+
timestamp,
|
|
1201
|
+
title,
|
|
1202
|
+
source,
|
|
1203
|
+
};
|
|
1204
|
+
if (previousTitle) entry.previousTitle = previousTitle;
|
|
1205
|
+
if (trigger) entry.trigger = trigger;
|
|
1206
|
+
this.#entries.push(entry);
|
|
1207
|
+
this.#index.insert(entry);
|
|
1208
|
+
this.#notifyEntryAppended(entry);
|
|
1209
|
+
await this.#persistTitleChangeEntry(entry, { title, source, updatedAt: timestamp });
|
|
1120
1210
|
|
|
1121
1211
|
this.#notifySessionNameListeners();
|
|
1122
1212
|
return true;
|
|
@@ -1484,6 +1574,8 @@ export class SessionManager {
|
|
|
1484
1574
|
this.#sessionId = newSessionId;
|
|
1485
1575
|
this.#sessionName = header.title;
|
|
1486
1576
|
this.#titleSource = header.titleSource;
|
|
1577
|
+
this.#titleUpdatedAt = timestamp;
|
|
1578
|
+
this.#hasTitleSlot = true;
|
|
1487
1579
|
this.#index.rebuild(this.#entries);
|
|
1488
1580
|
this.#artifactManager = null;
|
|
1489
1581
|
this.#artifactManagerSessionFile = null;
|
|
@@ -1542,7 +1634,7 @@ export class SessionManager {
|
|
|
1542
1634
|
cwd: path.resolve(cwd),
|
|
1543
1635
|
};
|
|
1544
1636
|
const file = path.join(sessionDir, `${fileSafeTimestamp(timestamp)}_${id}.jsonl`);
|
|
1545
|
-
storage.writeTextSync(file, `${JSON.stringify(header)}\n`);
|
|
1637
|
+
storage.writeTextSync(file, `${serializeTitleSlot({ updatedAt: timestamp })}${JSON.stringify(header)}\n`);
|
|
1546
1638
|
return file;
|
|
1547
1639
|
}
|
|
1548
1640
|
|
|
@@ -1577,6 +1669,8 @@ export class SessionManager {
|
|
|
1577
1669
|
manager.#header.titleSource = sourceHeader?.titleSource;
|
|
1578
1670
|
manager.#sessionName = manager.#header.title;
|
|
1579
1671
|
manager.#titleSource = manager.#header.titleSource;
|
|
1672
|
+
manager.#titleUpdatedAt = nowIso();
|
|
1673
|
+
manager.#hasTitleSlot = true;
|
|
1580
1674
|
manager.#entries = history;
|
|
1581
1675
|
manager.#index.rebuild(history);
|
|
1582
1676
|
manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
@@ -110,9 +110,8 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string
|
|
|
110
110
|
const entries: Array<readonly [string, unknown]> = [];
|
|
111
111
|
for (const [childKey, value] of Object.entries(obj)) {
|
|
112
112
|
// Strip transient/redundant properties that shouldn't be persisted.
|
|
113
|
-
// - partialJson: streaming accumulator for tool call JSON parsing
|
|
114
113
|
// - jsonlEvents: raw subprocess streaming events (already saved to artifact files)
|
|
115
|
-
if (childKey === "
|
|
114
|
+
if (childKey === "jsonlEvents") {
|
|
116
115
|
changed = true;
|
|
117
116
|
continue;
|
|
118
117
|
}
|
|
@@ -142,6 +141,100 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string
|
|
|
142
141
|
return obj;
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Read the duplication-relevant fields of an OpenAI Responses reasoning item.
|
|
146
|
+
* Returns `undefined` for anything that is not a `type: "reasoning"` object, so
|
|
147
|
+
* non-reasoning payload entries and corrupt signatures are never matched.
|
|
148
|
+
*/
|
|
149
|
+
function readReasoningItem(item: unknown): { encrypted_content?: string; id?: string } | undefined {
|
|
150
|
+
if (item === null || typeof item !== "object") return undefined;
|
|
151
|
+
if (!("type" in item) || item.type !== "reasoning") return undefined;
|
|
152
|
+
const reasoning: { encrypted_content?: string; id?: string } = {};
|
|
153
|
+
if ("encrypted_content" in item && typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) {
|
|
154
|
+
reasoning.encrypted_content = item.encrypted_content;
|
|
155
|
+
}
|
|
156
|
+
if ("id" in item && typeof item.id === "string" && item.id.length > 0) reasoning.id = item.id;
|
|
157
|
+
return reasoning;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* True when a `thinkingSignature` (a JSON-encoded reasoning item) is already
|
|
162
|
+
* carried by a reasoning item in the message's provider payload — matched on
|
|
163
|
+
* `encrypted_content` (the load-bearing blob) when present, else on item `id`.
|
|
164
|
+
* A signature the payload does not cover is never reported as recoverable, so it
|
|
165
|
+
* is always kept.
|
|
166
|
+
*/
|
|
167
|
+
function signatureCoveredByPayload(
|
|
168
|
+
signature: string,
|
|
169
|
+
encrypted: ReadonlySet<string>,
|
|
170
|
+
ids: ReadonlySet<string>,
|
|
171
|
+
): boolean {
|
|
172
|
+
let parsed: unknown;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(signature);
|
|
175
|
+
} catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const reasoning = readReasoningItem(parsed);
|
|
179
|
+
if (!reasoning) return false;
|
|
180
|
+
if (reasoning.encrypted_content) return encrypted.has(reasoning.encrypted_content);
|
|
181
|
+
if (reasoning.id) return ids.has(reasoning.id);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Drop `thinkingSignature` from assistant thinking blocks whose reasoning item is
|
|
187
|
+
* already carried, verbatim, in the message's OpenAI Responses `providerPayload`.
|
|
188
|
+
*
|
|
189
|
+
* Responses/Codex turns mint each reasoning item once and store it twice:
|
|
190
|
+
* `providerPayload.items` (the authoritative native-history copy that replay and
|
|
191
|
+
* remote compaction read) and `content[].thinkingSignature`, which is literally
|
|
192
|
+
* `JSON.stringify(reasoningItem)` — including the large `encrypted_content` blob.
|
|
193
|
+
* Replay only ever reads the payload; the signature is a no-payload fallback that
|
|
194
|
+
* same-provider turns never reach and cross-model turns strip as untrustworthy.
|
|
195
|
+
* Persisting both stores the encrypted reasoning twice for zero token or replay
|
|
196
|
+
* benefit, so the on-disk copy drops the duplicate signature whenever its
|
|
197
|
+
* reasoning item is recoverable from the payload. The in-memory entry is left
|
|
198
|
+
* untouched; only the serialized line is slimmed.
|
|
199
|
+
*/
|
|
200
|
+
function stripReplayedReasoningSignatures(entry: FileEntry): FileEntry {
|
|
201
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") return entry;
|
|
202
|
+
const message = entry.message;
|
|
203
|
+
const payload = message.providerPayload;
|
|
204
|
+
if (payload?.type !== "openaiResponsesHistory" || !Array.isArray(payload.items)) return entry;
|
|
205
|
+
const hasSignedThinking = message.content.some(
|
|
206
|
+
block =>
|
|
207
|
+
block.type === "thinking" && typeof block.thinkingSignature === "string" && block.thinkingSignature.length > 0,
|
|
208
|
+
);
|
|
209
|
+
if (!hasSignedThinking) return entry;
|
|
210
|
+
|
|
211
|
+
const encrypted = new Set<string>();
|
|
212
|
+
const ids = new Set<string>();
|
|
213
|
+
for (const rawItem of payload.items) {
|
|
214
|
+
const reasoning = readReasoningItem(rawItem);
|
|
215
|
+
if (!reasoning) continue;
|
|
216
|
+
if (reasoning.encrypted_content) encrypted.add(reasoning.encrypted_content);
|
|
217
|
+
if (reasoning.id) ids.add(reasoning.id);
|
|
218
|
+
}
|
|
219
|
+
if (encrypted.size === 0 && ids.size === 0) return entry;
|
|
220
|
+
|
|
221
|
+
let changed = false;
|
|
222
|
+
const content = message.content.map(block => {
|
|
223
|
+
if (
|
|
224
|
+
block.type !== "thinking" ||
|
|
225
|
+
typeof block.thinkingSignature !== "string" ||
|
|
226
|
+
block.thinkingSignature.length === 0
|
|
227
|
+
) {
|
|
228
|
+
return block;
|
|
229
|
+
}
|
|
230
|
+
if (!signatureCoveredByPayload(block.thinkingSignature, encrypted, ids)) return block;
|
|
231
|
+
changed = true;
|
|
232
|
+
return { ...block, thinkingSignature: undefined };
|
|
233
|
+
});
|
|
234
|
+
if (!changed) return entry;
|
|
235
|
+
return { ...entry, message: { ...message, content } };
|
|
236
|
+
}
|
|
237
|
+
|
|
145
238
|
export function prepareEntryForPersistence(entry: FileEntry, blobStore: BlobStore): FileEntry {
|
|
146
|
-
return truncateForPersistence(entry, blobStore) as FileEntry;
|
|
239
|
+
return truncateForPersistence(stripReplayedReasoningSignatures(entry), blobStore) as FileEntry;
|
|
147
240
|
}
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as fsp from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { hasFsCode, isEnoent, logger, peekFileEnds, Snowflake, toError } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { overlayTitleSlotContent, type SessionTitleUpdate, serializeTitleSlot } from "./session-title-slot";
|
|
5
6
|
|
|
6
7
|
const utf8Decoder = new TextDecoder("utf-8");
|
|
7
8
|
|
|
@@ -31,6 +32,14 @@ export interface SessionStorage {
|
|
|
31
32
|
ensureDirSync(dir: string): void;
|
|
32
33
|
existsSync(path: string): boolean;
|
|
33
34
|
writeTextSync(path: string, content: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* Update the current session title through the storage backend.
|
|
37
|
+
*
|
|
38
|
+
* File-like backends rewrite the fixed-width JSONL title slot; indexed
|
|
39
|
+
* backends can store the semantic title fields and synthesize the slot when
|
|
40
|
+
* reading.
|
|
41
|
+
*/
|
|
42
|
+
updateSessionTitle(path: string, update: SessionTitleUpdate): Promise<void>;
|
|
34
43
|
statSync(path: string): SessionStorageStat;
|
|
35
44
|
listFilesSync(dir: string, pattern: string): string[];
|
|
36
45
|
|
|
@@ -163,6 +172,25 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
163
172
|
}
|
|
164
173
|
}
|
|
165
174
|
|
|
175
|
+
async updateSessionTitle(fpath: string, update: SessionTitleUpdate): Promise<void> {
|
|
176
|
+
const fd = fs.openSync(fpath, "r+");
|
|
177
|
+
try {
|
|
178
|
+
const buf = Buffer.from(serializeTitleSlot(update), "utf-8");
|
|
179
|
+
let offset = 0;
|
|
180
|
+
while (offset < buf.length) {
|
|
181
|
+
const written = fs.writeSync(fd, buf, offset, buf.length - offset, offset);
|
|
182
|
+
if (written === 0) {
|
|
183
|
+
throw new Error("Short write");
|
|
184
|
+
}
|
|
185
|
+
offset += written;
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
throw toError(err);
|
|
189
|
+
} finally {
|
|
190
|
+
fs.closeSync(fd);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
166
194
|
statSync(path: string): SessionStorageStat {
|
|
167
195
|
const stats = fs.statSync(path);
|
|
168
196
|
return { size: stats.size, mtimeMs: stats.mtimeMs, mtime: stats.mtime };
|
|
@@ -526,6 +554,14 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
526
554
|
this.#files.set(path, createMemoryFileEntry(content, Date.now()));
|
|
527
555
|
}
|
|
528
556
|
|
|
557
|
+
async updateSessionTitle(path: string, update: SessionTitleUpdate): Promise<void> {
|
|
558
|
+
const entry = this.#requireEntry(path);
|
|
559
|
+
this.#files.set(
|
|
560
|
+
path,
|
|
561
|
+
createMemoryFileEntry(overlayTitleSlotContent(materializeMemoryEntry(entry), update), Date.now()),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
529
565
|
/**
|
|
530
566
|
* Internal O(1) append used by {@link MemorySessionStorageWriter}. Lazily
|
|
531
567
|
* creates the entry. External callers should go through `openWriter()`
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SESSION_TITLE_SLOT_BYTES,
|
|
3
|
+
SESSION_TITLE_SLOT_ENTRY_TYPE,
|
|
4
|
+
type SessionTitleSlotEntry,
|
|
5
|
+
type SessionTitleSource,
|
|
6
|
+
} from "./session-entries";
|
|
7
|
+
|
|
8
|
+
const utf8Encoder = new TextEncoder();
|
|
9
|
+
|
|
10
|
+
/** Semantic title update persisted by session storage backends. */
|
|
11
|
+
export interface SessionTitleUpdate {
|
|
12
|
+
title?: string;
|
|
13
|
+
source?: SessionTitleSource;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function byteLength(value: string): number {
|
|
18
|
+
return utf8Encoder.encode(value).byteLength;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function titleSlotLine(title: string, source: SessionTitleSource | undefined, updatedAt: string, pad: string): string {
|
|
22
|
+
const slot: SessionTitleSlotEntry = source
|
|
23
|
+
? {
|
|
24
|
+
type: SESSION_TITLE_SLOT_ENTRY_TYPE,
|
|
25
|
+
v: 1,
|
|
26
|
+
title,
|
|
27
|
+
source,
|
|
28
|
+
updatedAt,
|
|
29
|
+
pad,
|
|
30
|
+
}
|
|
31
|
+
: {
|
|
32
|
+
type: SESSION_TITLE_SLOT_ENTRY_TYPE,
|
|
33
|
+
v: 1,
|
|
34
|
+
title,
|
|
35
|
+
updatedAt,
|
|
36
|
+
pad,
|
|
37
|
+
};
|
|
38
|
+
return `${JSON.stringify(slot)}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function truncateTitleForSlot(title: string, source: SessionTitleSource | undefined, updatedAt: string): string {
|
|
42
|
+
const codePoints = [...title];
|
|
43
|
+
let low = 0;
|
|
44
|
+
let high = codePoints.length;
|
|
45
|
+
let best = "";
|
|
46
|
+
|
|
47
|
+
while (low <= high) {
|
|
48
|
+
const mid = (low + high) >>> 1;
|
|
49
|
+
const candidate = codePoints.slice(0, mid).join("");
|
|
50
|
+
if (byteLength(titleSlotLine(candidate, source, updatedAt, "")) <= SESSION_TITLE_SLOT_BYTES) {
|
|
51
|
+
best = candidate;
|
|
52
|
+
low = mid + 1;
|
|
53
|
+
} else {
|
|
54
|
+
high = mid - 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return best;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isSessionTitleSource(value: unknown): value is SessionTitleSource {
|
|
62
|
+
return value === "auto" || value === "user";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseTitleSlotObject(value: unknown): SessionTitleSlotEntry | undefined {
|
|
66
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
67
|
+
const record = value as Record<string, unknown>;
|
|
68
|
+
if (record.type !== SESSION_TITLE_SLOT_ENTRY_TYPE || record.v !== 1) return undefined;
|
|
69
|
+
if (typeof record.title !== "string" || typeof record.updatedAt !== "string" || typeof record.pad !== "string") {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const source = record.source;
|
|
73
|
+
if (source !== undefined && !isSessionTitleSource(source)) return undefined;
|
|
74
|
+
const slot: SessionTitleSlotEntry = {
|
|
75
|
+
type: SESSION_TITLE_SLOT_ENTRY_TYPE,
|
|
76
|
+
v: 1,
|
|
77
|
+
title: record.title,
|
|
78
|
+
updatedAt: record.updatedAt,
|
|
79
|
+
pad: record.pad,
|
|
80
|
+
};
|
|
81
|
+
if (source) slot.source = source;
|
|
82
|
+
return slot;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Parse a physical title slot JSONL line. Returns undefined for legacy headers. */
|
|
86
|
+
export function parseTitleSlotLine(line: string): SessionTitleSlotEntry | undefined {
|
|
87
|
+
try {
|
|
88
|
+
return parseTitleSlotObject(JSON.parse(line)) ?? undefined;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Parse the fixed-width title slot from a physical session body. */
|
|
95
|
+
export function parseTitleSlotFromContent(content: string): SessionTitleSlotEntry | undefined {
|
|
96
|
+
const newlineIndex = content.indexOf("\n");
|
|
97
|
+
if (newlineIndex < 0) return undefined;
|
|
98
|
+
return parseTitleSlotLine(content.slice(0, newlineIndex));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Convert a parsed title slot to the semantic storage update shape. */
|
|
102
|
+
export function titleUpdateFromSlot(slot: SessionTitleSlotEntry | undefined): SessionTitleUpdate | undefined {
|
|
103
|
+
if (!slot) return undefined;
|
|
104
|
+
return {
|
|
105
|
+
title: slot.title,
|
|
106
|
+
source: slot.source,
|
|
107
|
+
updatedAt: slot.updatedAt,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Serialize the fixed-width first-line title slot, exactly 256 UTF-8 bytes including newline. */
|
|
112
|
+
export function serializeTitleSlot(options: SessionTitleUpdate): string {
|
|
113
|
+
const title = truncateTitleForSlot(options.title ?? "", options.source, options.updatedAt);
|
|
114
|
+
const unpadded = titleSlotLine(title, options.source, options.updatedAt, "");
|
|
115
|
+
const padBytes = SESSION_TITLE_SLOT_BYTES - byteLength(unpadded);
|
|
116
|
+
if (padBytes < 0) throw new Error("Session title slot metadata exceeds fixed slot size");
|
|
117
|
+
const line = titleSlotLine(title, options.source, options.updatedAt, " ".repeat(padBytes));
|
|
118
|
+
if (byteLength(line) !== SESSION_TITLE_SLOT_BYTES) {
|
|
119
|
+
throw new Error("Session title slot serialization failed to produce fixed-width output");
|
|
120
|
+
}
|
|
121
|
+
return line;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Replace the physical fixed-width title slot in a full session body. */
|
|
125
|
+
export function overlayTitleSlotContent(content: string, update: SessionTitleUpdate): string {
|
|
126
|
+
const slot = Buffer.from(serializeTitleSlot(update), "utf-8");
|
|
127
|
+
const existing = Buffer.from(content, "utf-8");
|
|
128
|
+
if (existing.length <= slot.length) return slot.toString("utf-8");
|
|
129
|
+
return Buffer.concat([slot, existing.subarray(slot.length)]).toString("utf-8");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Replace the physical fixed-width title slot in a prefix byte window. */
|
|
133
|
+
export function overlayTitleSlotPrefix(prefix: string, prefixBytes: number, update: SessionTitleUpdate): string {
|
|
134
|
+
if (prefixBytes <= 0) return "";
|
|
135
|
+
const slot = Buffer.from(serializeTitleSlot(update), "utf-8");
|
|
136
|
+
if (prefixBytes <= slot.length) return slot.subarray(0, prefixBytes).toString("utf-8");
|
|
137
|
+
const existing = Buffer.from(prefix, "utf-8");
|
|
138
|
+
return Buffer.concat([slot, existing.subarray(slot.length)])
|
|
139
|
+
.subarray(0, prefixBytes)
|
|
140
|
+
.toString("utf-8");
|
|
141
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings-aware stream wrapper shared by the main agent (sdk.ts) and the
|
|
3
|
+
* advisor agent (AgentSession.#buildAdvisorRuntime).
|
|
4
|
+
*
|
|
5
|
+
* Reads OpenRouter / Antigravity routing variants, Responses-family text
|
|
6
|
+
* verbosity, per-provider in-flight caps, and the loop guard out of `Settings`
|
|
7
|
+
* per request, layering them onto whatever options the caller passed. Before
|
|
8
|
+
* this helper existed, advisor turns called bare `streamSimple` while the main
|
|
9
|
+
* turn went through an inline closure that read these settings — so an advisor on
|
|
10
|
+
* OpenRouter never saw `providers.openrouterVariant`, breaking sticky routing
|
|
11
|
+
* and OpenRouter response-cache hits across advisor calls.
|
|
12
|
+
*/
|
|
13
|
+
import type { StreamFn } from "@oh-my-pi/pi-agent-core";
|
|
14
|
+
import { type SimpleStreamOptions, streamSimple } from "@oh-my-pi/pi-ai";
|
|
15
|
+
import { type Settings, validateProviderMaxInFlightRequests } from "../config/settings";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a {@link StreamFn} that reads provider routing/guard settings from
|
|
19
|
+
* `settings` per call and forwards to `base` (defaults to `streamSimple`).
|
|
20
|
+
*
|
|
21
|
+
* Caller-supplied `streamOptions` always win — the helper only fills holes.
|
|
22
|
+
*/
|
|
23
|
+
export function createSettingsAwareStreamFn(settings: Settings, base: StreamFn = streamSimple): StreamFn {
|
|
24
|
+
return (model, context, streamOptions) => {
|
|
25
|
+
const openrouterRoutingPreset = settings.get("providers.openrouterVariant");
|
|
26
|
+
const openrouterVariant =
|
|
27
|
+
openrouterRoutingPreset && openrouterRoutingPreset !== "default" ? openrouterRoutingPreset : undefined;
|
|
28
|
+
const antigravityEndpointMode = settings.get("providers.antigravityEndpoint");
|
|
29
|
+
const textVerbosity =
|
|
30
|
+
model.api === "openai-codex-responses" || model.api === "openai-responses"
|
|
31
|
+
? settings.get("textVerbosity")
|
|
32
|
+
: undefined;
|
|
33
|
+
const merged: SimpleStreamOptions = {
|
|
34
|
+
...streamOptions,
|
|
35
|
+
openrouterVariant: streamOptions?.openrouterVariant ?? openrouterVariant,
|
|
36
|
+
antigravityEndpointMode: streamOptions?.antigravityEndpointMode ?? antigravityEndpointMode,
|
|
37
|
+
textVerbosity: streamOptions?.textVerbosity ?? textVerbosity,
|
|
38
|
+
maxInFlightRequests: validateProviderMaxInFlightRequests(
|
|
39
|
+
streamOptions?.maxInFlightRequests ?? settings.get("providers.maxInFlightRequests"),
|
|
40
|
+
),
|
|
41
|
+
loopGuard: {
|
|
42
|
+
enabled: settings.get("model.loopGuard.enabled"),
|
|
43
|
+
checkAssistantContent: settings.get("model.loopGuard.checkAssistantContent"),
|
|
44
|
+
...streamOptions?.loopGuard,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
return base(model, context, merged);
|
|
48
|
+
};
|
|
49
|
+
}
|