@oh-my-pi/pi-coding-agent 13.3.6 → 13.3.8
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 +115 -0
- package/package.json +9 -18
- package/scripts/format-prompts.ts +7 -172
- package/src/capability/mcp.ts +5 -0
- package/src/cli/args.ts +1 -0
- package/src/config/prompt-templates.ts +9 -55
- package/src/config/settings-schema.ts +24 -0
- package/src/discovery/builtin.ts +1 -0
- package/src/discovery/codex.ts +1 -2
- package/src/discovery/helpers.ts +0 -5
- package/src/discovery/mcp-json.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/client.ts +8 -0
- package/src/lsp/config.ts +2 -3
- package/src/lsp/index.ts +379 -99
- package/src/lsp/render.ts +21 -31
- package/src/lsp/types.ts +21 -8
- package/src/lsp/utils.ts +193 -1
- package/src/mcp/config-writer.ts +3 -0
- package/src/mcp/config.ts +1 -0
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/types.ts +5 -0
- package/src/modes/components/settings-defs.ts +9 -0
- package/src/modes/components/status-line.ts +1 -1
- package/src/modes/controllers/mcp-command-controller.ts +6 -2
- package/src/modes/interactive-mode.ts +8 -1
- package/src/modes/theme/mermaid-cache.ts +4 -4
- package/src/modes/theme/theme.ts +33 -0
- package/src/prompts/system/custom-system-prompt.md +0 -10
- package/src/prompts/system/subagent-user-prompt.md +2 -0
- package/src/prompts/system/system-prompt.md +12 -9
- package/src/prompts/tools/ast-find.md +20 -0
- package/src/prompts/tools/ast-replace.md +21 -0
- package/src/prompts/tools/bash.md +2 -0
- package/src/prompts/tools/hashline.md +26 -8
- package/src/prompts/tools/lsp.md +22 -5
- package/src/prompts/tools/task.md +0 -1
- package/src/sdk.ts +11 -5
- package/src/session/agent-session.ts +293 -83
- package/src/system-prompt.ts +3 -34
- package/src/task/executor.ts +8 -7
- package/src/task/index.ts +8 -55
- package/src/task/template.ts +2 -4
- package/src/task/types.ts +0 -5
- package/src/task/worktree.ts +6 -2
- package/src/tools/ast-find.ts +316 -0
- package/src/tools/ast-replace.ts +294 -0
- package/src/tools/bash.ts +2 -1
- package/src/tools/browser.ts +2 -8
- package/src/tools/fetch.ts +55 -18
- package/src/tools/index.ts +8 -0
- package/src/tools/jtd-to-json-schema.ts +29 -13
- package/src/tools/path-utils.ts +34 -0
- package/src/tools/python.ts +2 -1
- package/src/tools/renderers.ts +4 -0
- package/src/tools/ssh.ts +2 -1
- package/src/tools/submit-result.ts +143 -44
- package/src/tools/todo-write.ts +34 -0
- package/src/tools/tool-timeouts.ts +29 -0
- package/src/utils/mime.ts +37 -14
- package/src/utils/prompt-format.ts +172 -0
- package/src/web/scrapers/arxiv.ts +12 -12
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/iacr.ts +17 -9
- package/src/web/scrapers/readthedocs.ts +3 -3
- package/src/web/scrapers/twitter.ts +11 -11
- package/src/web/scrapers/wikipedia.ts +4 -5
- package/src/utils/ignore-files.ts +0 -119
|
@@ -355,6 +355,13 @@ export class AgentSession {
|
|
|
355
355
|
#pendingTtsrInjections: Rule[] = [];
|
|
356
356
|
#ttsrAbortPending = false;
|
|
357
357
|
#ttsrRetryToken = 0;
|
|
358
|
+
#ttsrResumePromise: Promise<void> | undefined = undefined;
|
|
359
|
+
#ttsrResumeResolve: (() => void) | undefined = undefined;
|
|
360
|
+
#postPromptTaskCounter = 0;
|
|
361
|
+
#postPromptTaskIds = new Set<number>();
|
|
362
|
+
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
363
|
+
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
364
|
+
#postPromptTasksAbortController = new AbortController();
|
|
358
365
|
|
|
359
366
|
#streamingEditAbortTriggered = false;
|
|
360
367
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
@@ -509,6 +516,7 @@ export class AgentSession {
|
|
|
509
516
|
if (this.#shouldInterruptForTtsrMatch(matchContext)) {
|
|
510
517
|
// Abort the stream immediately — do not gate on extension callbacks
|
|
511
518
|
this.#ttsrAbortPending = true;
|
|
519
|
+
this.#ensureTtsrResumePromise();
|
|
512
520
|
this.agent.abort();
|
|
513
521
|
// Notify extensions (fire-and-forget, does not block abort)
|
|
514
522
|
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
@@ -517,49 +525,58 @@ export class AgentSession {
|
|
|
517
525
|
const generation = this.#promptGeneration;
|
|
518
526
|
const targetMessageTimestamp =
|
|
519
527
|
event.message.role === "assistant" ? event.message.timestamp : undefined;
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
528
|
+
this.#schedulePostPromptTask(
|
|
529
|
+
async () => {
|
|
530
|
+
if (this.#ttsrRetryToken !== retryToken) {
|
|
531
|
+
this.#resolveTtsrResume();
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
524
534
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
535
|
+
const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
|
|
536
|
+
if (
|
|
537
|
+
!this.#ttsrAbortPending ||
|
|
538
|
+
this.#promptGeneration !== generation ||
|
|
539
|
+
targetAssistantIndex === -1
|
|
540
|
+
) {
|
|
541
|
+
this.#ttsrAbortPending = false;
|
|
542
|
+
this.#pendingTtsrInjections = [];
|
|
543
|
+
this.#resolveTtsrResume();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
531
546
|
this.#ttsrAbortPending = false;
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
547
|
+
const ttsrSettings = this.#ttsrManager?.getSettings();
|
|
548
|
+
if (ttsrSettings?.contextMode === "discard") {
|
|
549
|
+
// Remove the partial/aborted assistant turn from agent state
|
|
550
|
+
this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
|
|
551
|
+
}
|
|
552
|
+
// Inject TTSR rules as system reminder before retry
|
|
553
|
+
const injection = this.#getTtsrInjectionContent();
|
|
554
|
+
if (injection) {
|
|
555
|
+
const details = { rules: injection.rules.map(rule => rule.name) };
|
|
556
|
+
this.agent.appendMessage({
|
|
557
|
+
role: "custom",
|
|
558
|
+
customType: "ttsr-injection",
|
|
559
|
+
content: injection.content,
|
|
560
|
+
display: false,
|
|
561
|
+
details,
|
|
562
|
+
timestamp: Date.now(),
|
|
563
|
+
});
|
|
564
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
565
|
+
"ttsr-injection",
|
|
566
|
+
injection.content,
|
|
567
|
+
false,
|
|
568
|
+
details,
|
|
569
|
+
);
|
|
570
|
+
this.#markTtsrInjected(details.rules);
|
|
571
|
+
}
|
|
572
|
+
try {
|
|
573
|
+
await this.agent.continue();
|
|
574
|
+
} catch {
|
|
575
|
+
this.#resolveTtsrResume();
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
{ delayMs: 50 },
|
|
579
|
+
);
|
|
563
580
|
return;
|
|
564
581
|
}
|
|
565
582
|
}
|
|
@@ -607,6 +624,13 @@ export class AgentSession {
|
|
|
607
624
|
if (event.message.role === "assistant") {
|
|
608
625
|
this.#lastAssistantMessage = event.message;
|
|
609
626
|
const assistantMsg = event.message as AssistantMessage;
|
|
627
|
+
// Resolve TTSR resume gate before checking for new deferred injections.
|
|
628
|
+
// Gate on #ttsrAbortPending, not stopReason: a non-TTSR abort (e.g. streaming
|
|
629
|
+
// edit) also produces stopReason === "aborted" but has no continuation coming.
|
|
630
|
+
// Only skip when #ttsrAbortPending is true (TTSR continuation is imminent).
|
|
631
|
+
if (!this.#ttsrAbortPending) {
|
|
632
|
+
this.#resolveTtsrResume();
|
|
633
|
+
}
|
|
610
634
|
this.#queueDeferredTtsrInjectionIfNeeded(assistantMsg);
|
|
611
635
|
if (this.#handoffAbortController) {
|
|
612
636
|
this.#skipPostTurnMaintenanceAssistantTimestamp = assistantMsg.timestamp;
|
|
@@ -683,8 +707,9 @@ export class AgentSession {
|
|
|
683
707
|
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
|
684
708
|
}
|
|
685
709
|
|
|
686
|
-
|
|
687
|
-
|
|
710
|
+
const compactionTask = this.#checkCompaction(msg);
|
|
711
|
+
this.#trackPostPromptTask(compactionTask);
|
|
712
|
+
await compactionTask;
|
|
688
713
|
// Check for incomplete todos (unless there was an error or abort)
|
|
689
714
|
if (msg.stopReason !== "error" && msg.stopReason !== "aborted") {
|
|
690
715
|
await this.#checkTodoCompletion();
|
|
@@ -701,6 +726,141 @@ export class AgentSession {
|
|
|
701
726
|
}
|
|
702
727
|
}
|
|
703
728
|
|
|
729
|
+
/** Create the TTSR resume gate promise if one doesn't already exist. */
|
|
730
|
+
#ensureTtsrResumePromise(): void {
|
|
731
|
+
if (this.#ttsrResumePromise) return;
|
|
732
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
733
|
+
this.#ttsrResumePromise = promise;
|
|
734
|
+
this.#ttsrResumeResolve = resolve;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** Resolve and clear the TTSR resume gate. */
|
|
738
|
+
#resolveTtsrResume(): void {
|
|
739
|
+
if (!this.#ttsrResumeResolve) return;
|
|
740
|
+
this.#ttsrResumeResolve();
|
|
741
|
+
this.#ttsrResumeResolve = undefined;
|
|
742
|
+
this.#ttsrResumePromise = undefined;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
#ensurePostPromptTasksPromise(): void {
|
|
746
|
+
if (this.#postPromptTasksPromise) return;
|
|
747
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
748
|
+
this.#postPromptTasksPromise = promise;
|
|
749
|
+
this.#postPromptTasksResolve = resolve;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
#resolvePostPromptTasks(): void {
|
|
753
|
+
if (!this.#postPromptTasksResolve) return;
|
|
754
|
+
this.#postPromptTasksResolve();
|
|
755
|
+
this.#postPromptTasksResolve = undefined;
|
|
756
|
+
this.#postPromptTasksPromise = undefined;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
#trackPostPromptTask(task: Promise<void>): void {
|
|
760
|
+
const taskId = ++this.#postPromptTaskCounter;
|
|
761
|
+
this.#postPromptTaskIds.add(taskId);
|
|
762
|
+
this.#ensurePostPromptTasksPromise();
|
|
763
|
+
void task
|
|
764
|
+
.catch(() => {})
|
|
765
|
+
.finally(() => {
|
|
766
|
+
this.#postPromptTaskIds.delete(taskId);
|
|
767
|
+
if (this.#postPromptTaskIds.size === 0) {
|
|
768
|
+
this.#resolvePostPromptTasks();
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
#schedulePostPromptTask(
|
|
774
|
+
task: (signal: AbortSignal) => Promise<void>,
|
|
775
|
+
options?: { delayMs?: number; generation?: number; onSkip?: () => void },
|
|
776
|
+
): void {
|
|
777
|
+
const delayMs = options?.delayMs ?? 0;
|
|
778
|
+
const signal = this.#postPromptTasksAbortController.signal;
|
|
779
|
+
const scheduled = (async () => {
|
|
780
|
+
if (delayMs > 0) {
|
|
781
|
+
try {
|
|
782
|
+
await abortableSleep(delayMs, signal);
|
|
783
|
+
} catch {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (signal.aborted) {
|
|
788
|
+
options?.onSkip?.();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (options?.generation !== undefined && this.#promptGeneration !== options.generation) {
|
|
792
|
+
options.onSkip?.();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
await task(signal);
|
|
796
|
+
})();
|
|
797
|
+
this.#trackPostPromptTask(scheduled);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
#scheduleAgentContinue(options?: {
|
|
801
|
+
delayMs?: number;
|
|
802
|
+
generation?: number;
|
|
803
|
+
shouldContinue?: () => boolean;
|
|
804
|
+
onSkip?: () => void;
|
|
805
|
+
onError?: () => void;
|
|
806
|
+
}): void {
|
|
807
|
+
this.#schedulePostPromptTask(
|
|
808
|
+
async () => {
|
|
809
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
810
|
+
options.onSkip?.();
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
try {
|
|
814
|
+
await this.agent.continue();
|
|
815
|
+
} catch {
|
|
816
|
+
options?.onError?.();
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
delayMs: options?.delayMs,
|
|
821
|
+
generation: options?.generation,
|
|
822
|
+
onSkip: options?.onSkip,
|
|
823
|
+
},
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
#cancelPostPromptTasks(): void {
|
|
828
|
+
this.#postPromptTasksAbortController.abort();
|
|
829
|
+
this.#postPromptTasksAbortController = new AbortController();
|
|
830
|
+
this.#postPromptTaskIds.clear();
|
|
831
|
+
this.#resolvePostPromptTasks();
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Wait for retry, TTSR resume, and any background continuation to settle.
|
|
835
|
+
* Loops because a TTSR continuation can trigger a retry (or vice-versa),
|
|
836
|
+
* and fire-and-forget `agent.continue()` may still be streaming after
|
|
837
|
+
* the TTSR resume gate resolves.
|
|
838
|
+
*/
|
|
839
|
+
async #waitForPostPromptRecovery(): Promise<void> {
|
|
840
|
+
while (true) {
|
|
841
|
+
if (this.#retryPromise) {
|
|
842
|
+
await this.#retryPromise;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (this.#ttsrResumePromise) {
|
|
846
|
+
await this.#ttsrResumePromise;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (this.#postPromptTasksPromise) {
|
|
850
|
+
await this.#postPromptTasksPromise;
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
// Tracked post-prompt tasks cover deferred continuations scheduled from
|
|
854
|
+
// event handlers. Keep the streaming fallback for direct agent activity
|
|
855
|
+
// outside the scheduler.
|
|
856
|
+
if (this.agent.state.isStreaming) {
|
|
857
|
+
await this.agent.waitForIdle();
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
704
864
|
/** Get TTSR injection payload and clear pending injections. */
|
|
705
865
|
#getTtsrInjectionContent(): { content: string; rules: Rule[] } | undefined {
|
|
706
866
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
@@ -792,14 +952,26 @@ export class AgentSession {
|
|
|
792
952
|
details: { rules: injection.rules.map(rule => rule.name) },
|
|
793
953
|
timestamp: Date.now(),
|
|
794
954
|
});
|
|
955
|
+
this.#ensureTtsrResumePromise();
|
|
795
956
|
// Mark as injected after this custom message is delivered and persisted (handled in message_end).
|
|
796
957
|
// followUp() only enqueues; resume on the next tick once streaming settles.
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
958
|
+
this.#scheduleAgentContinue({
|
|
959
|
+
delayMs: 1,
|
|
960
|
+
generation: this.#promptGeneration,
|
|
961
|
+
onSkip: () => {
|
|
962
|
+
this.#resolveTtsrResume();
|
|
963
|
+
},
|
|
964
|
+
shouldContinue: () => {
|
|
965
|
+
if (this.agent.state.isStreaming || !this.agent.hasQueuedMessages()) {
|
|
966
|
+
this.#resolveTtsrResume();
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
return true;
|
|
970
|
+
},
|
|
971
|
+
onError: () => {
|
|
972
|
+
this.#resolveTtsrResume();
|
|
973
|
+
},
|
|
974
|
+
});
|
|
803
975
|
}
|
|
804
976
|
|
|
805
977
|
/** Build TTSR match context for tool call argument deltas. */
|
|
@@ -1271,6 +1443,7 @@ export class AgentSession {
|
|
|
1271
1443
|
} catch (error) {
|
|
1272
1444
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
1273
1445
|
}
|
|
1446
|
+
this.#cancelPostPromptTasks();
|
|
1274
1447
|
const drained = await this.#asyncJobManager?.dispose({ timeoutMs: 3_000 });
|
|
1275
1448
|
const deliveryState = this.#asyncJobManager?.getDeliveryState();
|
|
1276
1449
|
if (drained === false && deliveryState) {
|
|
@@ -1322,6 +1495,16 @@ export class AgentSession {
|
|
|
1322
1495
|
return this.agent.state.isStreaming || this.#promptInFlight;
|
|
1323
1496
|
}
|
|
1324
1497
|
|
|
1498
|
+
/** Wait until streaming and deferred recovery work are fully settled. */
|
|
1499
|
+
async waitForIdle(): Promise<void> {
|
|
1500
|
+
await this.agent.waitForIdle();
|
|
1501
|
+
await this.#waitForPostPromptRecovery();
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/** Most recent assistant message in agent state. */
|
|
1505
|
+
getLastAssistantMessage(): AssistantMessage | undefined {
|
|
1506
|
+
return this.#findLastAssistantMessage();
|
|
1507
|
+
}
|
|
1325
1508
|
/** Current effective system prompt (includes any per-turn extension modifications) */
|
|
1326
1509
|
get systemPrompt(): string {
|
|
1327
1510
|
return this.agent.state.systemPrompt;
|
|
@@ -1721,7 +1904,7 @@ export class AgentSession {
|
|
|
1721
1904
|
async #promptWithMessage(
|
|
1722
1905
|
message: AgentMessage,
|
|
1723
1906
|
expandedText: string,
|
|
1724
|
-
options?: Pick<PromptOptions, "toolChoice" | "images"
|
|
1907
|
+
options?: Pick<PromptOptions, "toolChoice" | "images"> & { skipPostPromptRecoveryWait?: boolean },
|
|
1725
1908
|
): Promise<void> {
|
|
1726
1909
|
this.#promptInFlight = true;
|
|
1727
1910
|
const generation = this.#promptGeneration;
|
|
@@ -1826,7 +2009,9 @@ export class AgentSession {
|
|
|
1826
2009
|
|
|
1827
2010
|
const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
|
|
1828
2011
|
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions);
|
|
1829
|
-
|
|
2012
|
+
if (!options?.skipPostPromptRecoveryWait) {
|
|
2013
|
+
await this.#waitForPostPromptRecovery();
|
|
2014
|
+
}
|
|
1830
2015
|
} finally {
|
|
1831
2016
|
this.#promptInFlight = false;
|
|
1832
2017
|
}
|
|
@@ -1886,7 +2071,7 @@ export class AgentSession {
|
|
|
1886
2071
|
},
|
|
1887
2072
|
hasQueuedMessages: () => this.queuedMessageCount > 0,
|
|
1888
2073
|
getContextUsage: () => this.getContextUsage(),
|
|
1889
|
-
waitForIdle: () => this.
|
|
2074
|
+
waitForIdle: () => this.waitForIdle(),
|
|
1890
2075
|
newSession: async options => {
|
|
1891
2076
|
const success = await this.newSession({ parentSession: options?.parentSession });
|
|
1892
2077
|
if (!success) {
|
|
@@ -2217,6 +2402,8 @@ export class AgentSession {
|
|
|
2217
2402
|
async abort(): Promise<void> {
|
|
2218
2403
|
this.abortRetry();
|
|
2219
2404
|
this.#promptGeneration++;
|
|
2405
|
+
this.#resolveTtsrResume();
|
|
2406
|
+
this.#cancelPostPromptTasks();
|
|
2220
2407
|
this.agent.abort();
|
|
2221
2408
|
await this.agent.waitForIdle();
|
|
2222
2409
|
// Clear promptInFlight: waitForIdle resolves when the agent loop's finally
|
|
@@ -3004,6 +3191,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3004
3191
|
// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false
|
|
3005
3192
|
if (skipAbortedCheck && assistantMessage.stopReason === "aborted") return;
|
|
3006
3193
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
3194
|
+
const generation = this.#promptGeneration;
|
|
3007
3195
|
// Skip overflow check if the message came from a different model.
|
|
3008
3196
|
// This handles the case where user switched from a smaller-context model (e.g. opus)
|
|
3009
3197
|
// to a larger-context model (e.g. codex) - the overflow error from the old model
|
|
@@ -3029,9 +3217,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3029
3217
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
3030
3218
|
if (promoted) {
|
|
3031
3219
|
// Retry on the promoted (larger) model without compacting
|
|
3032
|
-
|
|
3033
|
-
this.agent.continue().catch(() => {});
|
|
3034
|
-
}, 100);
|
|
3220
|
+
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
3035
3221
|
return;
|
|
3036
3222
|
}
|
|
3037
3223
|
|
|
@@ -3132,7 +3318,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3132
3318
|
content: [{ type: "text", text: reminder }],
|
|
3133
3319
|
timestamp: Date.now(),
|
|
3134
3320
|
});
|
|
3135
|
-
this
|
|
3321
|
+
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
3136
3322
|
}
|
|
3137
3323
|
|
|
3138
3324
|
/**
|
|
@@ -3302,6 +3488,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3302
3488
|
async #runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
|
|
3303
3489
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
3304
3490
|
|
|
3491
|
+
const generation = this.#promptGeneration;
|
|
3305
3492
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason });
|
|
3306
3493
|
// Properly abort and null existing controller before replacing
|
|
3307
3494
|
if (this.#autoCompactionAbortController) {
|
|
@@ -3541,10 +3728,15 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3541
3728
|
await this.#emitSessionEvent({ type: "auto_compaction_end", result, aborted: false, willRetry });
|
|
3542
3729
|
|
|
3543
3730
|
if (!willRetry && compactionSettings.autoContinue !== false) {
|
|
3544
|
-
await this
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3731
|
+
await this.#promptWithMessage(
|
|
3732
|
+
{
|
|
3733
|
+
role: "developer",
|
|
3734
|
+
content: [{ type: "text", text: "Continue if you have next steps." }],
|
|
3735
|
+
timestamp: Date.now(),
|
|
3736
|
+
},
|
|
3737
|
+
"Continue if you have next steps.",
|
|
3738
|
+
{ skipPostPromptRecoveryWait: true },
|
|
3739
|
+
);
|
|
3548
3740
|
}
|
|
3549
3741
|
|
|
3550
3742
|
if (willRetry) {
|
|
@@ -3554,15 +3746,15 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3554
3746
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
3555
3747
|
}
|
|
3556
3748
|
|
|
3557
|
-
|
|
3558
|
-
this.agent.continue().catch(() => {});
|
|
3559
|
-
}, 100);
|
|
3749
|
+
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
3560
3750
|
} else if (this.agent.hasQueuedMessages()) {
|
|
3561
3751
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
3562
3752
|
// Kick the loop so queued messages are actually delivered.
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3753
|
+
this.#scheduleAgentContinue({
|
|
3754
|
+
delayMs: 100,
|
|
3755
|
+
generation,
|
|
3756
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
3757
|
+
});
|
|
3566
3758
|
}
|
|
3567
3759
|
} catch (error) {
|
|
3568
3760
|
if (this.#autoCompactionAbortController?.signal.aborted) {
|
|
@@ -3685,6 +3877,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3685
3877
|
const retrySettings = this.settings.getGroup("retry");
|
|
3686
3878
|
if (!retrySettings.enabled) return false;
|
|
3687
3879
|
|
|
3880
|
+
const generation = this.#promptGeneration;
|
|
3688
3881
|
this.#retryAttempt++;
|
|
3689
3882
|
|
|
3690
3883
|
// Create retry promise on first attempt so waitForRetry() can await it
|
|
@@ -3764,12 +3957,8 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3764
3957
|
}
|
|
3765
3958
|
this.#retryAbortController = undefined;
|
|
3766
3959
|
|
|
3767
|
-
// Retry via continue()
|
|
3768
|
-
|
|
3769
|
-
this.agent.continue().catch(() => {
|
|
3770
|
-
// Retry failed - will be caught by next agent_end
|
|
3771
|
-
});
|
|
3772
|
-
}, 0);
|
|
3960
|
+
// Retry via continue() outside the agent_end event callback chain.
|
|
3961
|
+
this.#scheduleAgentContinue({ delayMs: 1, generation });
|
|
3773
3962
|
|
|
3774
3963
|
return true;
|
|
3775
3964
|
}
|
|
@@ -3783,16 +3972,6 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3783
3972
|
this.#resolveRetry();
|
|
3784
3973
|
}
|
|
3785
3974
|
|
|
3786
|
-
/**
|
|
3787
|
-
* Wait for any in-progress retry to complete.
|
|
3788
|
-
* Returns immediately if no retry is in progress.
|
|
3789
|
-
*/
|
|
3790
|
-
async #waitForRetry(): Promise<void> {
|
|
3791
|
-
if (this.#retryPromise) {
|
|
3792
|
-
await this.#retryPromise;
|
|
3793
|
-
}
|
|
3794
|
-
}
|
|
3795
|
-
|
|
3796
3975
|
async #promptAgentWithIdleRetry(messages: AgentMessage[], options?: { toolChoice?: ToolChoice }): Promise<void> {
|
|
3797
3976
|
const deadline = Date.now() + 30_000;
|
|
3798
3977
|
for (;;) {
|
|
@@ -3844,6 +4023,22 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3844
4023
|
onChunk?: (chunk: string) => void,
|
|
3845
4024
|
options?: { excludeFromContext?: boolean },
|
|
3846
4025
|
): Promise<BashResult> {
|
|
4026
|
+
const excludeFromContext = options?.excludeFromContext === true;
|
|
4027
|
+
const cwd = this.sessionManager.getCwd();
|
|
4028
|
+
|
|
4029
|
+
if (this.#extensionRunner?.hasHandlers("user_bash")) {
|
|
4030
|
+
const hookResult = await this.#extensionRunner.emitUserBash({
|
|
4031
|
+
type: "user_bash",
|
|
4032
|
+
command,
|
|
4033
|
+
excludeFromContext,
|
|
4034
|
+
cwd,
|
|
4035
|
+
});
|
|
4036
|
+
if (hookResult?.result) {
|
|
4037
|
+
this.recordBashResult(command, hookResult.result, options);
|
|
4038
|
+
return hookResult.result;
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
|
|
3847
4042
|
this.#bashAbortController = new AbortController();
|
|
3848
4043
|
|
|
3849
4044
|
try {
|
|
@@ -3942,12 +4137,27 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3942
4137
|
onChunk?: (chunk: string) => void,
|
|
3943
4138
|
options?: { excludeFromContext?: boolean },
|
|
3944
4139
|
): Promise<PythonResult> {
|
|
4140
|
+
const excludeFromContext = options?.excludeFromContext === true;
|
|
4141
|
+
const cwd = this.sessionManager.getCwd();
|
|
4142
|
+
|
|
4143
|
+
if (this.#extensionRunner?.hasHandlers("user_python")) {
|
|
4144
|
+
const hookResult = await this.#extensionRunner.emitUserPython({
|
|
4145
|
+
type: "user_python",
|
|
4146
|
+
code,
|
|
4147
|
+
excludeFromContext,
|
|
4148
|
+
cwd,
|
|
4149
|
+
});
|
|
4150
|
+
if (hookResult?.result) {
|
|
4151
|
+
this.recordPythonResult(code, hookResult.result, options);
|
|
4152
|
+
return hookResult.result;
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
|
|
3945
4156
|
this.#pythonAbortController = new AbortController();
|
|
3946
4157
|
|
|
3947
4158
|
try {
|
|
3948
4159
|
// Use the same session ID as the Python tool for kernel sharing
|
|
3949
4160
|
const sessionFile = this.sessionManager.getSessionFile();
|
|
3950
|
-
const cwd = this.sessionManager.getCwd();
|
|
3951
4161
|
const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
|
|
3952
4162
|
|
|
3953
4163
|
const result = await executePythonCommand(code, {
|
package/src/system-prompt.ts
CHANGED
|
@@ -16,24 +16,6 @@ import { loadSkills, type Skill } from "./extensibility/skills";
|
|
|
16
16
|
import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
|
|
17
17
|
import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
|
|
18
18
|
|
|
19
|
-
type PreloadedSkill = { name: string; content: string };
|
|
20
|
-
|
|
21
|
-
async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<PreloadedSkill[]> {
|
|
22
|
-
const contents = await Promise.all(
|
|
23
|
-
preloadedSkills.map(async skill => {
|
|
24
|
-
try {
|
|
25
|
-
const content = await Bun.file(skill.filePath).text();
|
|
26
|
-
return { name: skill.name, content };
|
|
27
|
-
} catch (err) {
|
|
28
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
-
throw new Error(`Failed to load skill "${skill.name}" from ${skill.filePath}: ${message}`);
|
|
30
|
-
}
|
|
31
|
-
}),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
return contents;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
19
|
function firstNonEmpty(...values: (string | undefined | null)[]): string | null {
|
|
38
20
|
for (const value of values) {
|
|
39
21
|
const trimmed = value?.trim();
|
|
@@ -350,11 +332,9 @@ export interface BuildSystemPromptOptions {
|
|
|
350
332
|
cwd?: string;
|
|
351
333
|
/** Pre-loaded context files (skips discovery if provided). */
|
|
352
334
|
contextFiles?: Array<{ path: string; content: string; depth?: number }>;
|
|
353
|
-
/**
|
|
335
|
+
/** Skills provided directly to system prompt construction. */
|
|
354
336
|
skills?: Skill[];
|
|
355
|
-
/**
|
|
356
|
-
preloadedSkills?: Skill[];
|
|
357
|
-
/** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
|
|
337
|
+
/** Pre-loaded rulebook rules (descriptions, excluding TTSR and always-apply). */
|
|
358
338
|
rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
|
|
359
339
|
/** Intent field name injected into every tool schema. If set, explains the field in the prompt. */
|
|
360
340
|
intentField?: string;
|
|
@@ -378,13 +358,11 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
378
358
|
cwd,
|
|
379
359
|
contextFiles: providedContextFiles,
|
|
380
360
|
skills: providedSkills,
|
|
381
|
-
preloadedSkills: providedPreloadedSkills,
|
|
382
361
|
rules,
|
|
383
362
|
intentField,
|
|
384
363
|
eagerTasks = false,
|
|
385
364
|
} = options;
|
|
386
365
|
const resolvedCwd = cwd ?? getProjectDir();
|
|
387
|
-
const preloadedSkills = providedPreloadedSkills;
|
|
388
366
|
|
|
389
367
|
const prepPromise = (() => {
|
|
390
368
|
const systemPromptCustomizationPromise = logger.timeAsync("loadSystemPromptFiles", loadSystemPromptFiles, {
|
|
@@ -400,9 +378,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
400
378
|
: skillsSettings?.enabled !== false
|
|
401
379
|
? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
|
|
402
380
|
: Promise.resolve([]);
|
|
403
|
-
const preloadedSkillContentsPromise = preloadedSkills
|
|
404
|
-
? logger.timeAsync("loadPreloadedSkills", loadPreloadedSkillContents, preloadedSkills)
|
|
405
|
-
: [];
|
|
406
381
|
|
|
407
382
|
return Promise.all([
|
|
408
383
|
resolvePromptInput(customPrompt, "system prompt"),
|
|
@@ -411,7 +386,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
411
386
|
contextFilesPromise,
|
|
412
387
|
agentsMdSearchPromise,
|
|
413
388
|
skillsPromise,
|
|
414
|
-
preloadedSkillContentsPromise,
|
|
415
389
|
]).then(
|
|
416
390
|
([
|
|
417
391
|
resolvedCustomPrompt,
|
|
@@ -420,7 +394,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
420
394
|
contextFiles,
|
|
421
395
|
agentsMdSearch,
|
|
422
396
|
skills,
|
|
423
|
-
preloadedSkillContents,
|
|
424
397
|
]) => ({
|
|
425
398
|
resolvedCustomPrompt,
|
|
426
399
|
resolvedAppendPrompt,
|
|
@@ -428,7 +401,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
428
401
|
contextFiles,
|
|
429
402
|
agentsMdSearch,
|
|
430
403
|
skills,
|
|
431
|
-
preloadedSkillContents,
|
|
432
404
|
}),
|
|
433
405
|
);
|
|
434
406
|
})();
|
|
@@ -451,7 +423,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
451
423
|
files: [],
|
|
452
424
|
};
|
|
453
425
|
let skills: Skill[] = providedSkills ?? [];
|
|
454
|
-
let preloadedSkillContents: PreloadedSkill[] = [];
|
|
455
426
|
|
|
456
427
|
if (prepResult.type === "timeout") {
|
|
457
428
|
logger.warn("System prompt preparation timed out; using minimal startup context", {
|
|
@@ -474,7 +445,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
474
445
|
contextFiles = prepResult.value.contextFiles;
|
|
475
446
|
agentsMdSearch = prepResult.value.agentsMdSearch;
|
|
476
447
|
skills = prepResult.value.skills;
|
|
477
|
-
preloadedSkillContents = prepResult.value.preloadedSkillContents;
|
|
478
448
|
}
|
|
479
449
|
|
|
480
450
|
const now = new Date();
|
|
@@ -517,7 +487,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
517
487
|
|
|
518
488
|
// Filter skills to only include those with read tool
|
|
519
489
|
const hasRead = tools?.has("read");
|
|
520
|
-
const filteredSkills =
|
|
490
|
+
const filteredSkills = hasRead ? skills : [];
|
|
521
491
|
|
|
522
492
|
const environment = await logger.timeAsync("getEnvironmentInfo", getEnvironmentInfo);
|
|
523
493
|
const data = {
|
|
@@ -531,7 +501,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
531
501
|
contextFiles,
|
|
532
502
|
agentsMdSearch,
|
|
533
503
|
skills: filteredSkills,
|
|
534
|
-
preloadedSkills: preloadedSkillContents,
|
|
535
504
|
rules: rules ?? [],
|
|
536
505
|
date,
|
|
537
506
|
dateTime,
|