@oh-my-pi/pi-coding-agent 16.1.19 → 16.1.20
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 +15 -0
- package/dist/cli.js +3616 -3592
- package/dist/types/cli/gallery-cli.d.ts +6 -0
- package/dist/types/commands/gallery.d.ts +1 -1
- package/dist/types/config/service-tier.d.ts +34 -0
- package/dist/types/config/settings-schema.d.ts +36 -33
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/task/parallel.d.ts +17 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/package.json +13 -13
- package/src/cli/gallery-cli.ts +31 -2
- package/src/cli/gallery-fixtures/agentic.ts +13 -4
- package/src/commands/gallery.ts +11 -3
- package/src/config/service-tier.ts +87 -0
- package/src/config/settings-schema.ts +48 -23
- package/src/eval/agent-bridge.ts +2 -0
- package/src/main.ts +1 -0
- package/src/mcp/transports/stdio.ts +5 -0
- package/src/modes/controllers/input-controller.ts +143 -66
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +72 -15
- package/src/session/session-history-format.ts +21 -4
- package/src/task/executor.ts +79 -2
- package/src/task/index.ts +11 -6
- package/src/task/parallel.ts +59 -7
- package/src/tools/index.ts +3 -1
- package/src/tools/irc.ts +16 -2
- package/src/utils/shell-snapshot-fn-env.sh +60 -0
- package/src/utils/shell-snapshot.ts +77 -20
|
@@ -534,6 +534,7 @@ export class InputController {
|
|
|
534
534
|
setupEditorSubmitHandler(): void {
|
|
535
535
|
this.ctx.editor.onSubmit = async (text: string) => {
|
|
536
536
|
text = text.trim();
|
|
537
|
+
const hasPendingImages = this.ctx.editor.pendingImages.length > 0;
|
|
537
538
|
if ((!isSettingsInitialized() || settings.get("emojiAutocomplete")) && text) text = expandEmoticons(text);
|
|
538
539
|
|
|
539
540
|
// Focused subagent session: the editor is a plain chat box for it.
|
|
@@ -546,7 +547,7 @@ export class InputController {
|
|
|
546
547
|
|
|
547
548
|
// Empty submit while streaming with queued messages: abort the active
|
|
548
549
|
// turn and let the post-unwind drain deliver the agent-core queue.
|
|
549
|
-
if (!text && this.ctx.session.isStreaming) {
|
|
550
|
+
if (!text && !hasPendingImages && this.ctx.session.isStreaming) {
|
|
550
551
|
if (this.ctx.session.queuedMessageCount > 0) {
|
|
551
552
|
const aborting = this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
|
|
552
553
|
await aborting;
|
|
@@ -556,7 +557,7 @@ export class InputController {
|
|
|
556
557
|
return;
|
|
557
558
|
}
|
|
558
559
|
|
|
559
|
-
if (!text) return;
|
|
560
|
+
if (!text && !hasPendingImages) return;
|
|
560
561
|
|
|
561
562
|
// Continue shortcuts: "." or "c" resume the agent with a hidden agent-authored
|
|
562
563
|
// developer directive (no visible user message) instead of an empty turn, so the
|
|
@@ -579,6 +580,7 @@ export class InputController {
|
|
|
579
580
|
let inputImages = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
580
581
|
let inputImageLinks =
|
|
581
582
|
this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
583
|
+
let hasInputImages = (inputImages?.length ?? 0) > 0;
|
|
582
584
|
|
|
583
585
|
if (runner?.hasHandlers("input")) {
|
|
584
586
|
const result = await runner.emitInput(text, inputImages, "interactive");
|
|
@@ -596,24 +598,27 @@ export class InputController {
|
|
|
596
598
|
this.ctx.sessionManager.putBlob.bind(this.ctx.sessionManager),
|
|
597
599
|
);
|
|
598
600
|
}
|
|
601
|
+
hasInputImages = (inputImages?.length ?? 0) > 0;
|
|
599
602
|
}
|
|
600
603
|
|
|
601
|
-
if (!text) return;
|
|
604
|
+
if (!text && !hasInputImages) return;
|
|
602
605
|
|
|
603
606
|
// Handle built-in slash commands
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
if (
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
607
|
+
if (text) {
|
|
608
|
+
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
609
|
+
ctx: this.ctx,
|
|
610
|
+
});
|
|
611
|
+
if (slashResult === true) {
|
|
612
|
+
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (typeof slashResult === "string") {
|
|
616
|
+
// Command handled but returned remaining text to use as prompt.
|
|
617
|
+
// Record the original slash command text so Up Arrow recalls
|
|
618
|
+
// "/loop 10 fix bug" rather than just "fix bug".
|
|
619
|
+
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
|
620
|
+
text = slashResult;
|
|
621
|
+
}
|
|
617
622
|
}
|
|
618
623
|
|
|
619
624
|
// Collab guest: prompts execute on the host; local slash/skill/bash/
|
|
@@ -647,7 +652,7 @@ export class InputController {
|
|
|
647
652
|
// free-text Enter semantics applied a few lines below at the streaming
|
|
648
653
|
// branch). Ctrl+Enter routes through `handleFollowUp` and dispatches the
|
|
649
654
|
// same helper with `"followUp"`.
|
|
650
|
-
if (await this.#invokeSkillCommand(text, "steer")) {
|
|
655
|
+
if (text && (await this.#invokeSkillCommand(text, "steer"))) {
|
|
651
656
|
return;
|
|
652
657
|
}
|
|
653
658
|
|
|
@@ -714,11 +719,25 @@ export class InputController {
|
|
|
714
719
|
// (a user-role `message_start` event) leaves any draft the user has
|
|
715
720
|
// typed since queuing intact. Same protection as #783, applied to
|
|
716
721
|
// the streaming/queue path.
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
+
try {
|
|
723
|
+
await this.ctx.withLocalSubmission(
|
|
724
|
+
text,
|
|
725
|
+
() => this.ctx.session.prompt(text, { streamingBehavior: "steer", images }),
|
|
726
|
+
{ imageCount: images?.length ?? 0 },
|
|
727
|
+
);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
// Don't lose the queued steer draft: restore text and images so
|
|
730
|
+
// the user can retry after dispatch validation/queue failures.
|
|
731
|
+
this.ctx.editor.setText(text);
|
|
732
|
+
if (images && images.length > 0) {
|
|
733
|
+
this.ctx.editor.pendingImages = [...images];
|
|
734
|
+
this.ctx.editor.pendingImageLinks = inputImageLinks
|
|
735
|
+
? [...inputImageLinks]
|
|
736
|
+
: images.map(() => undefined);
|
|
737
|
+
this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
|
|
738
|
+
}
|
|
739
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
740
|
+
}
|
|
722
741
|
this.ctx.updatePendingMessagesDisplay();
|
|
723
742
|
this.ctx.ui.requestRender();
|
|
724
743
|
return;
|
|
@@ -833,7 +852,10 @@ export class InputController {
|
|
|
833
852
|
/** Submit editor text to the focused subagent session (chat-only focus policy). */
|
|
834
853
|
async #submitToFocusedSession(text: string, streamingBehavior: "steer" | "followUp"): Promise<void> {
|
|
835
854
|
const target = this.ctx.viewSession;
|
|
836
|
-
|
|
855
|
+
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
856
|
+
const imageLinks =
|
|
857
|
+
images && this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
858
|
+
if (!text && !images) {
|
|
837
859
|
if (target.isStreaming && target.queuedMessageCount > 0) {
|
|
838
860
|
const aborting = target.abort({ reason: USER_INTERRUPT_LABEL });
|
|
839
861
|
await aborting;
|
|
@@ -842,11 +864,10 @@ export class InputController {
|
|
|
842
864
|
}
|
|
843
865
|
return;
|
|
844
866
|
}
|
|
845
|
-
if (text.startsWith("/") || text.startsWith("!") || parsePythonCommandInput(text)) {
|
|
867
|
+
if (text && (text.startsWith("/") || text.startsWith("!") || parsePythonCommandInput(text))) {
|
|
846
868
|
this.ctx.showStatus("Commands run in the main session — press ←← to return first");
|
|
847
869
|
return; // editor text not cleared: Editor does not auto-clear on submit
|
|
848
870
|
}
|
|
849
|
-
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
850
871
|
this.ctx.editor.clearDraft(text);
|
|
851
872
|
try {
|
|
852
873
|
// prompt() handles idle (new turn) and streaming (queues per streamingBehavior).
|
|
@@ -854,7 +875,14 @@ export class InputController {
|
|
|
854
875
|
imageCount: images?.length ?? 0,
|
|
855
876
|
});
|
|
856
877
|
} catch (error) {
|
|
857
|
-
|
|
878
|
+
// Hand the message back, mirroring the main submit error path: restore
|
|
879
|
+
// pasted images so the user can retry an image-only or text+image draft.
|
|
880
|
+
this.ctx.editor.setText(text);
|
|
881
|
+
if (images && images.length > 0) {
|
|
882
|
+
this.ctx.editor.pendingImages = [...images];
|
|
883
|
+
this.ctx.editor.pendingImageLinks = imageLinks ? [...imageLinks] : images.map(() => undefined);
|
|
884
|
+
this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
|
|
885
|
+
}
|
|
858
886
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
859
887
|
}
|
|
860
888
|
this.ctx.updatePendingMessagesDisplay();
|
|
@@ -902,19 +930,19 @@ export class InputController {
|
|
|
902
930
|
}
|
|
903
931
|
|
|
904
932
|
handleCtrlZ(): void {
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
933
|
+
// Job-control suspend is POSIX-only: on Windows `process.kill(_, "SIGSTOP")`
|
|
934
|
+
// throws `TypeError: Unknown signal: SIGSTOP` and takes the whole agent down
|
|
935
|
+
// via an uncaught exception (issue #2036, originally for SIGTSTP — same
|
|
936
|
+
// shape for SIGSTOP). No-op on platforms that cannot suspend.
|
|
909
937
|
if (process.platform === "win32") {
|
|
910
938
|
this.ctx.showStatus("Suspend (Ctrl+Z) is not supported on this platform");
|
|
911
939
|
return;
|
|
912
940
|
}
|
|
913
941
|
|
|
914
|
-
// Capture the listener so we can detach it if the signal never
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
942
|
+
// Capture the listener so we can detach it if the signal never fires;
|
|
943
|
+
// otherwise a failed suspend would leave a stale SIGCONT handler that
|
|
944
|
+
// fires on the next unrelated continue and tries to re-`start()` an
|
|
945
|
+
// already-running TUI.
|
|
918
946
|
const onResume = (): void => {
|
|
919
947
|
this.ctx.ui.start();
|
|
920
948
|
this.ctx.ui.requestRender(true);
|
|
@@ -926,14 +954,41 @@ export class InputController {
|
|
|
926
954
|
this.ctx.ui.stop();
|
|
927
955
|
|
|
928
956
|
try {
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
|
|
957
|
+
// SIGSTOP — not SIGTSTP — to the foreground process group (pid=0).
|
|
958
|
+
//
|
|
959
|
+
// SIGTSTP: brush-core (the embedded shell behind every bash tool call)
|
|
960
|
+
// installs a tokio SIGTSTP listener on `Process::wait` to detect when
|
|
961
|
+
// its children have been stopped (`crates/brush-core-vendored/src/sys/
|
|
962
|
+
// unix/signal.rs::tstp_signal_listener` → `tokio::signal::unix::
|
|
963
|
+
// signal(SIGTSTP)`). Per tokio's documented contract, the first call
|
|
964
|
+
// for a given SignalKind permanently replaces the kernel-default
|
|
965
|
+
// handler for the lifetime of the process. So once the user has
|
|
966
|
+
// issued even one bash command — e.g. `/usr/bin/true` — SIGTSTP no
|
|
967
|
+
// longer stops omp: tokio swallows it and the TUI ends up torn down
|
|
968
|
+
// while the process keeps running with no live terminal (issue
|
|
969
|
+
// [#3461]). SIGSTOP cannot be caught, blocked, or ignored, so the
|
|
970
|
+
// kernel stops the process regardless of installed handlers.
|
|
971
|
+
//
|
|
972
|
+
// pid=0 (foreground process group, not just our PID): omp is not
|
|
973
|
+
// always the shell's direct child. Package-manager launchers (`npx`,
|
|
974
|
+
// `pnpm exec`, `bunx`, …) wait on the real CLI from a parent shim
|
|
975
|
+
// that shares omp's process group, and a `omp … | tee log` style
|
|
976
|
+
// pipeline puts a sibling foreground job member in the same group
|
|
977
|
+
// too. The shell sees the job as stopped only when its direct
|
|
978
|
+
// child / pipeline leader is stopped, so suspending only our PID
|
|
979
|
+
// leaves wrappers and pipeline peers running and the terminal
|
|
980
|
+
// hung — exactly the failure shape we're fixing. Stopping the whole
|
|
981
|
+
// group keeps the shell's job-control view consistent. Long-lived
|
|
982
|
+
// children that must survive the suspend (MCP stdio servers via
|
|
983
|
+
// the `detached: true` spawn in `mcp/transports/stdio.ts`, every
|
|
984
|
+
// brush external command via brush's per-child `setsid` in
|
|
985
|
+
// `crates/brush-core-vendored/src/commands.rs`) are already in
|
|
986
|
+
// their own sessions, so pgid=0 does not reach them.
|
|
987
|
+
process.kill(0, "SIGSTOP");
|
|
932
988
|
} catch (err) {
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
// on a frozen prompt.
|
|
989
|
+
// The runtime refused the signal (e.g. seccomp filter blocks SIGSTOP
|
|
990
|
+
// delivery to the process group). Tear the resume hook down and
|
|
991
|
+
// bring the TUI back so the user is not stranded on a frozen prompt.
|
|
937
992
|
process.removeListener("SIGCONT", onResume);
|
|
938
993
|
this.ctx.ui.start();
|
|
939
994
|
this.ctx.ui.requestRender(true);
|
|
@@ -1024,7 +1079,10 @@ export class InputController {
|
|
|
1024
1079
|
/** Send editor text as a follow-up message (queued behind current stream). */
|
|
1025
1080
|
async handleFollowUp(): Promise<void> {
|
|
1026
1081
|
let text = this.ctx.editor.getText().trim();
|
|
1027
|
-
|
|
1082
|
+
const images = this.ctx.editor.pendingImages.length > 0 ? [...this.ctx.editor.pendingImages] : undefined;
|
|
1083
|
+
const imageLinks =
|
|
1084
|
+
images && this.ctx.editor.pendingImageLinks.length > 0 ? [...this.ctx.editor.pendingImageLinks] : undefined;
|
|
1085
|
+
if (!text && !images) return;
|
|
1028
1086
|
|
|
1029
1087
|
// Focused subagent session: follow-ups go to it; non-chat input is gated.
|
|
1030
1088
|
if (this.ctx.focusedAgentId) {
|
|
@@ -1044,38 +1102,53 @@ export class InputController {
|
|
|
1044
1102
|
return;
|
|
1045
1103
|
}
|
|
1046
1104
|
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1105
|
+
if (text) {
|
|
1106
|
+
const slashResult = await executeBuiltinSlashCommand(text, {
|
|
1107
|
+
ctx: this.ctx,
|
|
1108
|
+
});
|
|
1109
|
+
if (slashResult === true) {
|
|
1110
|
+
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (typeof slashResult === "string") {
|
|
1114
|
+
// Command handled but returned remaining text to use as prompt.
|
|
1115
|
+
// Record the original slash command text so Up Arrow recalls it.
|
|
1116
|
+
if (!shouldSkipHistory(text)) this.ctx.editor.addToHistory(text);
|
|
1117
|
+
text = slashResult;
|
|
1118
|
+
}
|
|
1059
1119
|
}
|
|
1060
1120
|
|
|
1061
1121
|
// Skill commands invoke through the custom-message path regardless of
|
|
1062
1122
|
// which keybinding submitted them. Enter routes them as `steer`;
|
|
1063
1123
|
// Ctrl+Enter (this handler) routes them as `followUp`.
|
|
1064
|
-
if (await this.#invokeSkillCommand(text, "followUp")) {
|
|
1124
|
+
if (text && (await this.#invokeSkillCommand(text, "followUp"))) {
|
|
1065
1125
|
return;
|
|
1066
1126
|
}
|
|
1067
1127
|
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
|
|
1128
|
+
// Hand the message back on dispatch failure (model/API-key validation,
|
|
1129
|
+
// queue rejection): restore both text AND pending images so an image-only
|
|
1130
|
+
// or text+image draft can be retried, mirroring the main submit error path.
|
|
1131
|
+
const restoreOnError = (error: unknown) => {
|
|
1132
|
+
this.ctx.editor.setText(text);
|
|
1133
|
+
if (images && images.length > 0) {
|
|
1134
|
+
this.ctx.editor.pendingImages = [...images];
|
|
1135
|
+
this.ctx.editor.pendingImageLinks = imageLinks ? [...imageLinks] : images.map(() => undefined);
|
|
1136
|
+
this.ctx.editor.imageLinks = this.ctx.editor.pendingImageLinks;
|
|
1137
|
+
}
|
|
1138
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
1139
|
+
};
|
|
1071
1140
|
|
|
1072
1141
|
if (this.ctx.session.isStreaming) {
|
|
1073
1142
|
this.ctx.editor.clearDraft(text);
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1143
|
+
try {
|
|
1144
|
+
await this.ctx.withLocalSubmission(
|
|
1145
|
+
text,
|
|
1146
|
+
() => this.ctx.session.prompt(text, { streamingBehavior: "followUp", images }),
|
|
1147
|
+
{ imageCount: images?.length ?? 0 },
|
|
1148
|
+
);
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
restoreOnError(error);
|
|
1151
|
+
}
|
|
1079
1152
|
this.ctx.updatePendingMessagesDisplay();
|
|
1080
1153
|
this.ctx.ui.requestRender();
|
|
1081
1154
|
return;
|
|
@@ -1083,9 +1156,13 @@ export class InputController {
|
|
|
1083
1156
|
|
|
1084
1157
|
// Not streaming — just submit normally
|
|
1085
1158
|
this.ctx.editor.clearDraft(text);
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1159
|
+
try {
|
|
1160
|
+
await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text, { images }), {
|
|
1161
|
+
imageCount: images?.length ?? 0,
|
|
1162
|
+
});
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
restoreOnError(error);
|
|
1165
|
+
}
|
|
1089
1166
|
}
|
|
1090
1167
|
|
|
1091
1168
|
restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
|
package/src/sdk.ts
CHANGED
|
@@ -1520,6 +1520,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1520
1520
|
getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
|
|
1521
1521
|
getActiveModelString,
|
|
1522
1522
|
getActiveModel: () => agent?.state.model ?? model,
|
|
1523
|
+
getServiceTier: () => session?.serviceTier,
|
|
1523
1524
|
getImageAttachments: () => session?.getImageAttachments() ?? [],
|
|
1524
1525
|
getPlanModeState: () => session?.getPlanModeState(),
|
|
1525
1526
|
getPlanReferencePath: () => session?.getPlanReferencePath() ?? "local://PLAN.md",
|
|
@@ -160,6 +160,7 @@ import {
|
|
|
160
160
|
} from "../config/model-resolver";
|
|
161
161
|
import { MODEL_ROLE_IDS, MODEL_ROLES } from "../config/model-roles";
|
|
162
162
|
import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
|
|
163
|
+
import { resolveServiceTierSetting } from "../config/service-tier";
|
|
163
164
|
import type { Settings, SkillsSettings } from "../config/settings";
|
|
164
165
|
import { getDefault, onAppendOnlyModeChanged } from "../config/settings";
|
|
165
166
|
import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
|
|
@@ -1887,6 +1888,16 @@ export class AgentSession {
|
|
|
1887
1888
|
}
|
|
1888
1889
|
const advisorSessionId = this.sessionId ? `${this.sessionId}-advisor` : undefined;
|
|
1889
1890
|
|
|
1891
|
+
// Advisor service tier (`serviceTierAdvisor`): "none" (default) runs the
|
|
1892
|
+
// advisor on standard processing; "inherit" tracks the session's live tier
|
|
1893
|
+
// per request (like the main agent, including /fast toggles) via a resolver;
|
|
1894
|
+
// a concrete value pins the advisor to that tier regardless of the session.
|
|
1895
|
+
const advisorTierSetting = this.settings.get("serviceTierAdvisor");
|
|
1896
|
+
const advisorServiceTier =
|
|
1897
|
+
advisorTierSetting === "inherit" ? undefined : resolveServiceTierSetting(advisorTierSetting, undefined);
|
|
1898
|
+
const advisorServiceTierResolver =
|
|
1899
|
+
advisorTierSetting === "inherit" ? (model: Model) => this.#effectiveServiceTier(model) : undefined;
|
|
1900
|
+
|
|
1890
1901
|
// Thread the primary's telemetry into the advisor loop so the advisor
|
|
1891
1902
|
// model's GenAI spans + usage/cost hooks fire like every other model call,
|
|
1892
1903
|
// stamped with the advisor's own identity. `conversationId` is cleared so
|
|
@@ -1916,6 +1927,8 @@ export class AgentSession {
|
|
|
1916
1927
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
1917
1928
|
intentTracing: false,
|
|
1918
1929
|
telemetry: advisorTelemetry,
|
|
1930
|
+
serviceTier: advisorServiceTier,
|
|
1931
|
+
serviceTierResolver: advisorServiceTierResolver,
|
|
1919
1932
|
});
|
|
1920
1933
|
advisorAgent.setDisableReasoning(shouldDisableReasoning(advisorThinkingLevel));
|
|
1921
1934
|
|
|
@@ -2797,6 +2810,9 @@ export class AgentSession {
|
|
|
2797
2810
|
return;
|
|
2798
2811
|
}
|
|
2799
2812
|
|
|
2813
|
+
const successfulYieldMessage = this.#findSuccessfulYieldAssistantMessage(settledMessages);
|
|
2814
|
+
const yieldOnThisMessage = this.#assistantEndedWithSuccessfulYield(msg);
|
|
2815
|
+
|
|
2800
2816
|
const maintenanceRoute = (route: string, extra?: Record<string, unknown>) => {
|
|
2801
2817
|
logger.debug("agent_end maintenance routing", {
|
|
2802
2818
|
route,
|
|
@@ -2808,7 +2824,7 @@ export class AgentSession {
|
|
|
2808
2824
|
hasText: msg.content.some(content => content.type === "text"),
|
|
2809
2825
|
goalModeEnabled: this.#goalModeState?.enabled === true,
|
|
2810
2826
|
goalStatus: this.#goalModeState?.goal.status,
|
|
2811
|
-
successfulYield:
|
|
2827
|
+
successfulYield: successfulYieldMessage !== undefined,
|
|
2812
2828
|
...extra,
|
|
2813
2829
|
});
|
|
2814
2830
|
};
|
|
@@ -2833,21 +2849,24 @@ export class AgentSession {
|
|
|
2833
2849
|
}
|
|
2834
2850
|
|
|
2835
2851
|
const activeGoal = this.#goalModeState?.enabled === true && this.#goalModeState.goal.status === "active";
|
|
2836
|
-
const yieldOnThisMessage = this.#assistantEndedWithSuccessfulYield(msg);
|
|
2837
2852
|
// A successful `yield` in this run is terminal for execution purposes.
|
|
2838
2853
|
// Suppress empty-stop retry, unexpected-stop retry, queued-message drain,
|
|
2839
2854
|
// and compaction-driven continuations for the rest of this prompt cycle:
|
|
2840
2855
|
// the executor consumed the yield as the terminal result, so a trailing
|
|
2841
2856
|
// empty/aborted assistant stop must NOT revive the agent loop. The
|
|
2842
2857
|
// `#yieldTerminationPending` sticky flag clears on the next `prompt()`.
|
|
2843
|
-
if (
|
|
2858
|
+
if (successfulYieldMessage || this.#yieldTerminationPending) {
|
|
2844
2859
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2845
|
-
if (
|
|
2846
|
-
maintenanceRoute(
|
|
2847
|
-
|
|
2860
|
+
if (successfulYieldMessage && activeGoal) {
|
|
2861
|
+
maintenanceRoute(
|
|
2862
|
+
yieldOnThisMessage
|
|
2863
|
+
? "successful-yield-active-goal-checkCompaction"
|
|
2864
|
+
: "post-yield-trailing-stop-active-goal-checkCompaction",
|
|
2865
|
+
);
|
|
2866
|
+
const compactionTask = this.#checkCompaction(successfulYieldMessage);
|
|
2848
2867
|
this.#trackPostPromptTask(compactionTask);
|
|
2849
2868
|
await compactionTask;
|
|
2850
|
-
} else if (
|
|
2869
|
+
} else if (successfulYieldMessage) {
|
|
2851
2870
|
maintenanceRoute("successful-yield-no-active-goal");
|
|
2852
2871
|
} else {
|
|
2853
2872
|
maintenanceRoute("post-yield-trailing-stop-suppressed");
|
|
@@ -6813,7 +6832,12 @@ export class AgentSession {
|
|
|
6813
6832
|
* the transcript can distinguish a deliberate user interrupt from an opaque
|
|
6814
6833
|
* abort. Omit it for internal/lifecycle aborts.
|
|
6815
6834
|
*/
|
|
6816
|
-
async abort(options?: {
|
|
6835
|
+
async abort(options?: {
|
|
6836
|
+
goalReason?: "interrupted" | "internal";
|
|
6837
|
+
reason?: string;
|
|
6838
|
+
/** Internal `/compact` startup keeps the manual-compaction marker alive while aborting the active turn. */
|
|
6839
|
+
preserveCompaction?: boolean;
|
|
6840
|
+
}): Promise<void> {
|
|
6817
6841
|
const userInterrupt = options?.reason === USER_INTERRUPT_LABEL;
|
|
6818
6842
|
if (userInterrupt) this.#advisorAutoResumeSuppressed = true;
|
|
6819
6843
|
// Pull advisor concerns out of the steer/follow-up queues before any await so
|
|
@@ -6828,7 +6852,17 @@ export class AgentSession {
|
|
|
6828
6852
|
this.abortRetry();
|
|
6829
6853
|
this.#promptGeneration++;
|
|
6830
6854
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6831
|
-
|
|
6855
|
+
if (options?.preserveCompaction) {
|
|
6856
|
+
// Manual `/compact` installed its own #compactionAbortController before
|
|
6857
|
+
// this internal abort and must keep it alive (that marker is what makes
|
|
6858
|
+
// isCompacting report true during startup). Any in-flight
|
|
6859
|
+
// auto-compaction MUST still be cancelled, though: otherwise a
|
|
6860
|
+
// background maintenance pass races the manual run and both
|
|
6861
|
+
// appendCompaction/replaceMessages, double-rewriting session history.
|
|
6862
|
+
this.#autoCompactionAbortController?.abort();
|
|
6863
|
+
} else {
|
|
6864
|
+
this.abortCompaction();
|
|
6865
|
+
}
|
|
6832
6866
|
this.abortHandoff();
|
|
6833
6867
|
this.abortBash();
|
|
6834
6868
|
this.abortEval();
|
|
@@ -7801,12 +7835,12 @@ export class AgentSession {
|
|
|
7801
7835
|
if (compactMode?.rejectsFocus && customInstructions) {
|
|
7802
7836
|
throw new Error(`/compact ${compactMode.name} does not take focus instructions.`);
|
|
7803
7837
|
}
|
|
7804
|
-
this.#disconnectFromAgent();
|
|
7805
|
-
await this.abort({ goalReason: "internal" });
|
|
7806
7838
|
const compactionAbortController = new AbortController();
|
|
7807
7839
|
this.#compactionAbortController = compactionAbortController;
|
|
7808
7840
|
|
|
7809
7841
|
try {
|
|
7842
|
+
this.#disconnectFromAgent();
|
|
7843
|
+
await this.abort({ goalReason: "internal", preserveCompaction: true });
|
|
7810
7844
|
if (!this.model) {
|
|
7811
7845
|
throw new Error("No model selected");
|
|
7812
7846
|
}
|
|
@@ -8591,9 +8625,7 @@ export class AgentSession {
|
|
|
8591
8625
|
}
|
|
8592
8626
|
return COMPACTION_CHECK_NONE;
|
|
8593
8627
|
}
|
|
8594
|
-
#
|
|
8595
|
-
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
8596
|
-
if (!toolCallId) return false;
|
|
8628
|
+
#assistantMessageHasSuccessfulYieldToolCall(assistantMessage: AssistantMessage, toolCallId: string): boolean {
|
|
8597
8629
|
const lastToolCall = assistantMessage.content
|
|
8598
8630
|
.slice()
|
|
8599
8631
|
.reverse()
|
|
@@ -8601,6 +8633,22 @@ export class AgentSession {
|
|
|
8601
8633
|
return lastToolCall?.name === "yield" && lastToolCall.id === toolCallId;
|
|
8602
8634
|
}
|
|
8603
8635
|
|
|
8636
|
+
#assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
|
|
8637
|
+
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
8638
|
+
return toolCallId ? this.#assistantMessageHasSuccessfulYieldToolCall(assistantMessage, toolCallId) : false;
|
|
8639
|
+
}
|
|
8640
|
+
|
|
8641
|
+
#findSuccessfulYieldAssistantMessage(messages: readonly AgentMessage[]): AssistantMessage | undefined {
|
|
8642
|
+
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
8643
|
+
if (!toolCallId) return undefined;
|
|
8644
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
8645
|
+
const message = messages[i];
|
|
8646
|
+
if (message.role !== "assistant") continue;
|
|
8647
|
+
if (this.#assistantMessageHasSuccessfulYieldToolCall(message, toolCallId)) return message;
|
|
8648
|
+
}
|
|
8649
|
+
return undefined;
|
|
8650
|
+
}
|
|
8651
|
+
|
|
8604
8652
|
async #handleEmptyAssistantStop(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
8605
8653
|
if (!this.#isEmptyAssistantStop(assistantMessage)) {
|
|
8606
8654
|
this.#emptyStopRetryCount = 0;
|
|
@@ -10619,7 +10667,16 @@ export class AgentSession {
|
|
|
10619
10667
|
#getRetryFallbackChains(): RetryFallbackChains {
|
|
10620
10668
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
10621
10669
|
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
10622
|
-
|
|
10670
|
+
const chains: RetryFallbackChains = { ...(configuredChains as RetryFallbackChains) };
|
|
10671
|
+
const defaultChain = chains.default;
|
|
10672
|
+
if (Array.isArray(defaultChain)) {
|
|
10673
|
+
for (const role of Object.keys(this.settings.getModelRoles())) {
|
|
10674
|
+
if (role !== "default" && chains[role] === undefined) {
|
|
10675
|
+
chains[role] = defaultChain;
|
|
10676
|
+
}
|
|
10677
|
+
}
|
|
10678
|
+
}
|
|
10679
|
+
return chains;
|
|
10623
10680
|
}
|
|
10624
10681
|
|
|
10625
10682
|
#validateRetryFallbackChains(): void {
|
|
@@ -86,6 +86,14 @@ function lineCount(text: string): number {
|
|
|
86
86
|
return text.split("\n").length;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function primaryArgValue(value: unknown): string {
|
|
90
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
91
|
+
if (Array.isArray(value) && value.length > 0 && value.every(v => typeof v === "string")) {
|
|
92
|
+
return value.join(", ");
|
|
93
|
+
}
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
/** Pick the most informative scalar argument of a tool call. */
|
|
90
98
|
function primaryArg(name: string, args: Record<string, unknown> | undefined): string {
|
|
91
99
|
if (!args || typeof args !== "object") return "";
|
|
@@ -97,12 +105,21 @@ function primaryArg(name: string, args: Record<string, unknown> | undefined): st
|
|
|
97
105
|
if (note) return oneLine(note);
|
|
98
106
|
if (severity) return oneLine(severity);
|
|
99
107
|
}
|
|
108
|
+
if (name === "search") {
|
|
109
|
+
const pattern = primaryArgValue(args.pattern);
|
|
110
|
+
const paths = primaryArgValue(args.paths);
|
|
111
|
+
if (pattern && paths) return oneLine(`${pattern} @ ${paths}`);
|
|
112
|
+
if (pattern) return oneLine(pattern);
|
|
113
|
+
if (paths) return oneLine(paths);
|
|
114
|
+
}
|
|
115
|
+
if (name === "find") {
|
|
116
|
+
const paths = primaryArgValue(args.paths);
|
|
117
|
+
if (paths) return oneLine(paths);
|
|
118
|
+
}
|
|
100
119
|
for (const key of PRIMARY_ARG_KEYS) {
|
|
101
120
|
const value = args[key];
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
return oneLine(value.join(", "));
|
|
105
|
-
}
|
|
121
|
+
const summary = primaryArgValue(value);
|
|
122
|
+
if (summary) return oneLine(summary);
|
|
106
123
|
}
|
|
107
124
|
// Fallback: first non-intent string arg, then a compact JSON of the args.
|
|
108
125
|
const rest: Record<string, unknown> = {};
|