@gajae-code/coding-agent 0.11.8 → 0.11.10
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 +35 -1
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/model-profiles.d.ts +2 -3
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +12 -5
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +41 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +32 -17
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +17 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +135 -4
- package/dist/types/gjc-runtime/state-writer.d.ts +45 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +5 -3
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +16 -8
- package/dist/types/runtime-mcp/redaction.d.ts +2 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +8 -1
- package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +14 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +74 -5
- package/dist/types/sdk/session.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +13 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -3
- package/dist/types/session/session-manager.d.ts +5 -1
- package/dist/types/session/session-storage.d.ts +13 -0
- package/dist/types/skill-state/active-state.d.ts +4 -0
- package/dist/types/task/provider-retry-status.d.ts +13 -0
- package/dist/types/task/types.d.ts +3 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +1 -0
- package/src/cli/mcp-cli.ts +4 -21
- package/src/cli/notify-cli.ts +1 -0
- package/src/cli.ts +32 -0
- package/src/commands/deep-interview.ts +9 -4
- package/src/commit/agentic/index.ts +3 -3
- package/src/commit/map-reduce/index.ts +2 -2
- package/src/config/model-profiles.ts +9 -10
- package/src/config/model-registry.ts +4 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +78 -163
- package/src/eval/js/tool-bridge.ts +1 -1
- package/src/gjc-runtime/deep-interview-ambiguity.ts +68 -47
- package/src/gjc-runtime/deep-interview-draft.ts +843 -0
- package/src/gjc-runtime/deep-interview-payload.ts +262 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +310 -227
- package/src/gjc-runtime/deep-interview-repair.ts +1674 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +264 -103
- package/src/gjc-runtime/deep-interview-state.ts +916 -4
- package/src/gjc-runtime/ralplan-runtime.ts +3 -1
- package/src/gjc-runtime/state-writer.ts +166 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +386 -1
- package/src/gjc-runtime/workflow-manifest.ts +191 -5
- package/src/hooks/skill-state.ts +23 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/modes/components/welcome.ts +10 -2
- package/src/modes/controllers/selector-controller.ts +38 -8
- package/src/modes/interactive-mode.ts +29 -9
- package/src/prompts/agent-fragments/restricted-bash.md +1 -1
- package/src/prompts/agents/architect.md +8 -1
- package/src/prompts/agents/critic.md +8 -1
- package/src/prompts/agents/planner.md +8 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/runtime-mcp/json-rpc.ts +7 -4
- package/src/runtime-mcp/redaction.ts +30 -0
- package/src/sdk/broker/identity.ts +22 -12
- package/src/sdk/broker/session-index.ts +11 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/index.ts +60 -5
- package/src/sdk/bus/notification-service.ts +19 -5
- package/src/sdk/bus/operator-runtime.ts +31 -5
- package/src/sdk/bus/telegram-cli.ts +6 -13
- package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +15 -7
- package/src/sdk/bus/telegram-daemon-control.ts +14 -5
- package/src/sdk/bus/telegram-daemon.ts +685 -120
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +12 -0
- package/src/session/agent-session.ts +140 -10
- package/src/session/internal/managed-session-scope.ts +339 -228
- package/src/session/internal/managed-session-storage.ts +17 -4
- package/src/session/session-manager.ts +96 -33
- package/src/session/session-storage.ts +23 -0
- package/src/skill-state/active-state.ts +10 -0
- package/src/skill-state/workflow-mutation-guard.ts +41 -7
- package/src/task/executor.ts +154 -44
- package/src/task/provider-retry-status.ts +88 -0
- package/src/task/render.ts +45 -26
- package/src/task/types.ts +3 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/bash-allowed-prefixes.ts +45 -1
- package/src/tools/index.ts +3 -1
- package/src/tools/subagent-render.ts +74 -16
- package/src/tools/subagent.ts +2 -0
- package/vendor/insane-search/MANIFEST.json +4 -1
- package/vendor/insane-search/engine/bias_check.py +5 -0
- package/vendor/insane-search/engine/fetch_chain.py +67 -1
- package/vendor/insane-search/engine/transport.py +13 -2
- package/vendor/insane-search/engine/validators.py +4 -1
package/src/task/executor.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
} from "@gajae-code/agent-core";
|
|
16
16
|
import { recordHandoff, resolveTelemetry } from "@gajae-code/agent-core";
|
|
17
17
|
import { estimateMessageTokensHeuristic } from "@gajae-code/agent-core/compaction";
|
|
18
|
-
import type { Message, Model, ServiceTier } from "@gajae-code/ai";
|
|
18
|
+
import type { AssistantMessage, Message, Model, ServiceTier } from "@gajae-code/ai";
|
|
19
19
|
import { type JsonSchemaValidationIssue, validateJsonSchemaValue } from "@gajae-code/ai/utils/schema";
|
|
20
20
|
import { logger, prompt, untilAborted } from "@gajae-code/utils";
|
|
21
21
|
import { AsyncJobManager } from "../async";
|
|
@@ -50,6 +50,7 @@ import type { EventBus } from "../utils/event-bus";
|
|
|
50
50
|
import { buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
51
51
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
52
52
|
import { validateAllocatedTaskId } from "./id";
|
|
53
|
+
import { classifyProviderRetry, providerNameFromModel } from "./provider-retry-status";
|
|
53
54
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
54
55
|
import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
|
|
55
56
|
import {
|
|
@@ -81,6 +82,21 @@ const agentEventTypes = new Set<AgentEvent["type"]>([
|
|
|
81
82
|
"tool_execution_end",
|
|
82
83
|
]);
|
|
83
84
|
|
|
85
|
+
const providerStreamingUpdateTypes = new Set<string>([
|
|
86
|
+
"text_start",
|
|
87
|
+
"text_delta",
|
|
88
|
+
"text_end",
|
|
89
|
+
"thinking_start",
|
|
90
|
+
"thinking_delta",
|
|
91
|
+
"thinking_end",
|
|
92
|
+
"reasoning_summary_start",
|
|
93
|
+
"reasoning_summary_delta",
|
|
94
|
+
"reasoning_summary_end",
|
|
95
|
+
"toolcall_start",
|
|
96
|
+
"toolcall_delta",
|
|
97
|
+
"toolcall_end",
|
|
98
|
+
]);
|
|
99
|
+
|
|
84
100
|
const isAgentEvent = (event: AgentSessionEvent): event is AgentEvent =>
|
|
85
101
|
agentEventTypes.has(event.type as AgentEvent["type"]);
|
|
86
102
|
|
|
@@ -486,11 +502,12 @@ function buildSchemaViolationOutcome(
|
|
|
486
502
|
missing.length > 0
|
|
487
503
|
? `schema_violation: missing required fields: ${missing.join(", ")}`
|
|
488
504
|
: `schema_violation: ${failure.message}`;
|
|
505
|
+
const dataPreview = previewOffendingData(data);
|
|
489
506
|
const payload = {
|
|
490
507
|
error: "schema_violation",
|
|
491
508
|
message: failure.message,
|
|
492
509
|
missingRequired: missing,
|
|
493
|
-
data
|
|
510
|
+
data,
|
|
494
511
|
};
|
|
495
512
|
let rawOutput: string;
|
|
496
513
|
try {
|
|
@@ -498,7 +515,7 @@ function buildSchemaViolationOutcome(
|
|
|
498
515
|
} catch {
|
|
499
516
|
rawOutput = `{"error":"schema_violation","message":${JSON.stringify(headline)}}`;
|
|
500
517
|
}
|
|
501
|
-
return { rawOutput, stderr: headline
|
|
518
|
+
return { rawOutput, stderr: `${headline}. Offending data preview: ${dataPreview}`, exitCode: 1 };
|
|
502
519
|
}
|
|
503
520
|
|
|
504
521
|
function buildPlaceholderYieldOutcome(
|
|
@@ -541,9 +558,16 @@ export function finalizeSubprocessOutput(args: FinalizeSubprocessOutputArgs): Fi
|
|
|
541
558
|
const completeData = normalizeCompleteData(submitData, reportFindings);
|
|
542
559
|
const { validator, error: schemaError } = buildOutputValidator(outputSchema);
|
|
543
560
|
if (schemaError) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
561
|
+
const outcome = buildSchemaViolationOutcome(
|
|
562
|
+
{
|
|
563
|
+
message: `invalid output schema: ${schemaError}`,
|
|
564
|
+
missingRequired: [],
|
|
565
|
+
},
|
|
566
|
+
completeData,
|
|
567
|
+
);
|
|
568
|
+
rawOutput = outcome.rawOutput;
|
|
569
|
+
stderr = outcome.stderr;
|
|
570
|
+
exitCode = outcome.exitCode;
|
|
547
571
|
} else {
|
|
548
572
|
const placeholderPath = findPlaceholderYieldPath(completeData);
|
|
549
573
|
if (placeholderPath) {
|
|
@@ -837,7 +861,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
837
861
|
let modelSubstitutionWarning: ModelSubstitutionWarning | undefined;
|
|
838
862
|
let resolvedModelString: string | undefined;
|
|
839
863
|
let lastAssistantModelString: string | undefined;
|
|
864
|
+
let activeProviderModelString: string | undefined;
|
|
840
865
|
let effectiveThinkingLevelForWarning: ThinkingLevel | undefined;
|
|
866
|
+
let lastProviderProgressAtMs: number | undefined;
|
|
867
|
+
let sessionEventOrdinal = 0;
|
|
868
|
+
let retryStartOrdinal = 0;
|
|
869
|
+
const seenAssistantMessages = new WeakSet<AgentMessage>();
|
|
870
|
+
const seenAssistantMessageIdentities = new Set<string>();
|
|
841
871
|
|
|
842
872
|
// Accumulate usage incrementally from message_end events (no memory for streaming events)
|
|
843
873
|
const accumulatedUsage = {
|
|
@@ -919,7 +949,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
919
949
|
|
|
920
950
|
const emitProgressNow = () => {
|
|
921
951
|
progress.durationMs = Date.now() - startTime;
|
|
922
|
-
|
|
952
|
+
const progressSnapshot = structuredClone(progress);
|
|
953
|
+
onProgress?.(progressSnapshot);
|
|
923
954
|
if (options.eventBus) {
|
|
924
955
|
options.eventBus.emit(TASK_SUBAGENT_PROGRESS_CHANNEL, {
|
|
925
956
|
index,
|
|
@@ -927,7 +958,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
927
958
|
agentSource: agent.source,
|
|
928
959
|
task,
|
|
929
960
|
assignment,
|
|
930
|
-
progress:
|
|
961
|
+
progress: progressSnapshot,
|
|
931
962
|
sessionFile: null,
|
|
932
963
|
});
|
|
933
964
|
}
|
|
@@ -982,6 +1013,79 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
982
1013
|
: undefined;
|
|
983
1014
|
};
|
|
984
1015
|
|
|
1016
|
+
const getAssistantMessageIdentity = (message: AgentMessage): string | undefined => {
|
|
1017
|
+
if (message.role !== "assistant") return undefined;
|
|
1018
|
+
const model = getMessageModelString(message);
|
|
1019
|
+
const timestamp = "timestamp" in message ? message.timestamp : undefined;
|
|
1020
|
+
if (!model || typeof timestamp !== "number") return undefined;
|
|
1021
|
+
const responseId = "responseId" in message && typeof message.responseId === "string" ? message.responseId : "";
|
|
1022
|
+
return JSON.stringify([model, timestamp, responseId]);
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
const rememberAssistantMessage = (message: AgentMessage): void => {
|
|
1026
|
+
seenAssistantMessages.add(message);
|
|
1027
|
+
const identity = getAssistantMessageIdentity(message);
|
|
1028
|
+
if (identity) seenAssistantMessageIdentities.add(identity);
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const isProviderAssistantMessage = (message: AgentMessage): message is AssistantMessage =>
|
|
1032
|
+
message.role === "assistant" &&
|
|
1033
|
+
"stopReason" in message &&
|
|
1034
|
+
Array.isArray(message.content) &&
|
|
1035
|
+
getMessageModelString(message) !== undefined;
|
|
1036
|
+
|
|
1037
|
+
const isSuccessfulProviderAssistantMessage = (message: AgentMessage): message is AssistantMessage =>
|
|
1038
|
+
isProviderAssistantMessage(message) &&
|
|
1039
|
+
message.stopReason !== "error" &&
|
|
1040
|
+
message.stopReason !== "aborted" &&
|
|
1041
|
+
message.errorMessage === undefined &&
|
|
1042
|
+
message.errorKind === undefined &&
|
|
1043
|
+
message.transportFailure === undefined;
|
|
1044
|
+
|
|
1045
|
+
const hasMatchingActiveProviderModel = (message: AgentMessage): message is AssistantMessage =>
|
|
1046
|
+
isProviderAssistantMessage(message) &&
|
|
1047
|
+
activeProviderModelString !== undefined &&
|
|
1048
|
+
getMessageModelString(message) === activeProviderModelString;
|
|
1049
|
+
|
|
1050
|
+
const isProviderStreamingUpdate = (event: Extract<AgentEvent, { type: "message_update" }>): boolean => {
|
|
1051
|
+
const update = event.assistantMessageEvent;
|
|
1052
|
+
if (!update || typeof update !== "object" || !providerStreamingUpdateTypes.has(update.type)) return false;
|
|
1053
|
+
if (!("partial" in update) || getMessageModelString(update.partial) !== activeProviderModelString) return false;
|
|
1054
|
+
switch (update.type) {
|
|
1055
|
+
case "text_delta":
|
|
1056
|
+
case "thinking_delta":
|
|
1057
|
+
case "reasoning_summary_delta":
|
|
1058
|
+
case "toolcall_delta":
|
|
1059
|
+
return typeof update.delta === "string";
|
|
1060
|
+
default:
|
|
1061
|
+
return typeof update.contentIndex === "number";
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
const getProviderTextDelta = (event: Extract<AgentEvent, { type: "message_update" }>): string | undefined => {
|
|
1066
|
+
const update = (event as { assistantMessageEvent?: unknown }).assistantMessageEvent;
|
|
1067
|
+
if (!update || typeof update !== "object") return undefined;
|
|
1068
|
+
const record = update as { type?: unknown; delta?: unknown };
|
|
1069
|
+
return record.type === "text_delta" && typeof record.delta === "string" ? record.delta : undefined;
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
const isProviderBackedRecoveryEvent = (
|
|
1073
|
+
event: Extract<AgentEvent, { type: "message_start" | "message_update" }>,
|
|
1074
|
+
eventOrdinal: number,
|
|
1075
|
+
): boolean => {
|
|
1076
|
+
const messageIdentity = getAssistantMessageIdentity(event.message);
|
|
1077
|
+
if (
|
|
1078
|
+
!progress.retryState ||
|
|
1079
|
+
eventOrdinal <= retryStartOrdinal ||
|
|
1080
|
+
seenAssistantMessages.has(event.message) ||
|
|
1081
|
+
(messageIdentity !== undefined && seenAssistantMessageIdentities.has(messageIdentity))
|
|
1082
|
+
)
|
|
1083
|
+
return false;
|
|
1084
|
+
if (!hasMatchingActiveProviderModel(event.message)) return false;
|
|
1085
|
+
if (event.type === "message_start") return isSuccessfulProviderAssistantMessage(event.message);
|
|
1086
|
+
return isProviderStreamingUpdate(event);
|
|
1087
|
+
};
|
|
1088
|
+
|
|
985
1089
|
const updateRecentOutputLines = () => {
|
|
986
1090
|
const lines = recentOutputTail.split("\n").filter(line => line.trim());
|
|
987
1091
|
progress.recentOutput = lines.slice(-8).reverse();
|
|
@@ -996,21 +1100,6 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
996
1100
|
updateRecentOutputLines();
|
|
997
1101
|
};
|
|
998
1102
|
|
|
999
|
-
const replaceRecentOutputFromContent = (content: unknown[]) => {
|
|
1000
|
-
recentOutputTail = "";
|
|
1001
|
-
for (const block of content) {
|
|
1002
|
-
if (!block || typeof block !== "object") continue;
|
|
1003
|
-
const record = block as { type?: unknown; text?: unknown };
|
|
1004
|
-
if (record.type !== "text" || typeof record.text !== "string") continue;
|
|
1005
|
-
if (!record.text) continue;
|
|
1006
|
-
recentOutputTail += record.text;
|
|
1007
|
-
if (recentOutputTail.length > RECENT_OUTPUT_TAIL_BYTES) {
|
|
1008
|
-
recentOutputTail = recentOutputTail.slice(-RECENT_OUTPUT_TAIL_BYTES);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
updateRecentOutputLines();
|
|
1012
|
-
};
|
|
1013
|
-
|
|
1014
1103
|
const resetRecentOutput = () => {
|
|
1015
1104
|
recentOutputTail = "";
|
|
1016
1105
|
progress.recentOutput = [];
|
|
@@ -1030,20 +1119,32 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1030
1119
|
});
|
|
1031
1120
|
};
|
|
1032
1121
|
|
|
1033
|
-
const processEvent = (event: AgentEvent) => {
|
|
1122
|
+
const processEvent = (event: AgentEvent, eventOrdinal: number) => {
|
|
1034
1123
|
if (resolved) return;
|
|
1035
1124
|
|
|
1036
1125
|
forwardSubagentEvent(event);
|
|
1037
1126
|
|
|
1038
1127
|
const now = Date.now();
|
|
1039
1128
|
let flushProgress = false;
|
|
1129
|
+
// A retry is recovered at the first assistant provider event, before the
|
|
1130
|
+
// session emits auto_retry_end after message completion.
|
|
1040
1131
|
|
|
1041
1132
|
switch (event.type) {
|
|
1042
|
-
case "message_start":
|
|
1043
|
-
if (event.message
|
|
1133
|
+
case "message_start": {
|
|
1134
|
+
if (event.message.role !== "assistant") break;
|
|
1135
|
+
const retryWasActive = Boolean(progress.retryState);
|
|
1136
|
+
const recoversRetry = isProviderBackedRecoveryEvent(event, eventOrdinal);
|
|
1137
|
+
rememberAssistantMessage(event.message);
|
|
1138
|
+
if (recoversRetry || !retryWasActive) {
|
|
1139
|
+
lastProviderProgressAtMs = now;
|
|
1044
1140
|
resetRecentOutput();
|
|
1045
1141
|
}
|
|
1142
|
+
if (recoversRetry) {
|
|
1143
|
+
progress.retryState = undefined;
|
|
1144
|
+
flushProgress = true;
|
|
1145
|
+
}
|
|
1046
1146
|
break;
|
|
1147
|
+
}
|
|
1047
1148
|
|
|
1048
1149
|
case "tool_execution_start": {
|
|
1049
1150
|
progress.toolCount++;
|
|
@@ -1155,24 +1256,17 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1155
1256
|
}
|
|
1156
1257
|
|
|
1157
1258
|
case "message_update": {
|
|
1158
|
-
if (event.message
|
|
1159
|
-
const
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
)
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
break;
|
|
1167
|
-
}
|
|
1168
|
-
if (assistantEvent && assistantEvent.type !== "text_delta") {
|
|
1169
|
-
break;
|
|
1170
|
-
}
|
|
1171
|
-
const updateContent =
|
|
1172
|
-
getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
|
|
1173
|
-
if (updateContent && Array.isArray(updateContent)) {
|
|
1174
|
-
replaceRecentOutputFromContent(updateContent);
|
|
1259
|
+
if (event.message.role !== "assistant") break;
|
|
1260
|
+
const retryWasActive = Boolean(progress.retryState);
|
|
1261
|
+
const recoversRetry = isProviderBackedRecoveryEvent(event, eventOrdinal);
|
|
1262
|
+
if (!retryWasActive) rememberAssistantMessage(event.message);
|
|
1263
|
+
if (recoversRetry || !retryWasActive) lastProviderProgressAtMs = now;
|
|
1264
|
+
if (recoversRetry) {
|
|
1265
|
+
progress.retryState = undefined;
|
|
1266
|
+
flushProgress = true;
|
|
1175
1267
|
}
|
|
1268
|
+
const textDelta = getProviderTextDelta(event);
|
|
1269
|
+
if (textDelta !== undefined) appendRecentOutputTail(textDelta);
|
|
1176
1270
|
break;
|
|
1177
1271
|
}
|
|
1178
1272
|
|
|
@@ -1180,6 +1274,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1180
1274
|
// Extract text from assistant and toolResult messages (not user prompts)
|
|
1181
1275
|
const role = event.message?.role;
|
|
1182
1276
|
if (role === "assistant") {
|
|
1277
|
+
if (isSuccessfulProviderAssistantMessage(event.message)) lastProviderProgressAtMs = now;
|
|
1183
1278
|
const messageContent =
|
|
1184
1279
|
getMessageContent(event.message) || (event as AgentEvent & { content?: unknown }).content;
|
|
1185
1280
|
if (messageContent && Array.isArray(messageContent)) {
|
|
@@ -1192,6 +1287,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1192
1287
|
const assistantModel = getMessageModelString(event.message);
|
|
1193
1288
|
if (assistantModel) {
|
|
1194
1289
|
lastAssistantModelString = assistantModel;
|
|
1290
|
+
activeProviderModelString = assistantModel;
|
|
1195
1291
|
if (resolvedModelString && assistantModel !== resolvedModelString && !modelSubstitutionWarning) {
|
|
1196
1292
|
modelSubstitutionWarning = {
|
|
1197
1293
|
requested: resolvedModelString,
|
|
@@ -1359,6 +1455,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1359
1455
|
);
|
|
1360
1456
|
if (model) {
|
|
1361
1457
|
resolvedModelString = formatModelString(model);
|
|
1458
|
+
activeProviderModelString = resolvedModelString;
|
|
1362
1459
|
}
|
|
1363
1460
|
if (authFallbackUsed && model && requestedModel) {
|
|
1364
1461
|
modelSubstitutionWarning = {
|
|
@@ -1496,6 +1593,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1496
1593
|
? { requestedModel, reason: modelSubstitutionWarning.reason }
|
|
1497
1594
|
: undefined,
|
|
1498
1595
|
toolNames,
|
|
1596
|
+
alwaysActiveToolNames: ircEnabled ? ["irc"] : undefined,
|
|
1499
1597
|
outputSchema,
|
|
1500
1598
|
requireYieldTool: true,
|
|
1501
1599
|
contextFiles: options.contextFiles,
|
|
@@ -1729,11 +1827,22 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1729
1827
|
|
|
1730
1828
|
const MAX_YIELD_RETRIES = 3;
|
|
1731
1829
|
unsubscribe = session.subscribe(event => {
|
|
1830
|
+
sessionEventOrdinal += 1;
|
|
1831
|
+
const eventOrdinal = sessionEventOrdinal;
|
|
1732
1832
|
if (event.type === "auto_retry_start") {
|
|
1833
|
+
retryStartOrdinal = eventOrdinal;
|
|
1834
|
+
for (const message of session.messages) {
|
|
1835
|
+
if (message.role === "assistant") rememberAssistantMessage(message);
|
|
1836
|
+
}
|
|
1733
1837
|
progress.retryState = {
|
|
1734
1838
|
attempt: event.attempt,
|
|
1735
1839
|
maxAttempts: event.maxAttempts,
|
|
1736
1840
|
unbounded: event.unbounded,
|
|
1841
|
+
kind: classifyProviderRetry(event.errorMessage),
|
|
1842
|
+
provider: providerNameFromModel(
|
|
1843
|
+
activeProviderModelString ?? lastAssistantModelString ?? resolvedModelString,
|
|
1844
|
+
),
|
|
1845
|
+
lastProviderProgressAtMs,
|
|
1737
1846
|
delayMs: event.delayMs,
|
|
1738
1847
|
errorMessage: event.errorMessage,
|
|
1739
1848
|
startedAtMs: Date.now(),
|
|
@@ -1755,12 +1864,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1755
1864
|
return;
|
|
1756
1865
|
}
|
|
1757
1866
|
if (event.type === "model_fallback_switched") {
|
|
1867
|
+
activeProviderModelString = event.to;
|
|
1758
1868
|
forwardSubagentEvent(event);
|
|
1759
1869
|
return;
|
|
1760
1870
|
}
|
|
1761
1871
|
if (isAgentEvent(event)) {
|
|
1762
1872
|
try {
|
|
1763
|
-
processEvent(event);
|
|
1873
|
+
processEvent(event, eventOrdinal);
|
|
1764
1874
|
} catch (err) {
|
|
1765
1875
|
logger.error("Subagent event processing failed", {
|
|
1766
1876
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { AgentProgress, TaskToolDetails } from "./types";
|
|
2
|
+
|
|
3
|
+
export type ProviderRetryKind = NonNullable<AgentProgress["retryState"]>["kind"];
|
|
4
|
+
|
|
5
|
+
const FIRST_EVENT_TIMEOUT_PATTERN = /stream timed out while waiting for the first event/i;
|
|
6
|
+
const IDLE_STREAM_STALL_PATTERN = /stream stalled while waiting for the next event/i;
|
|
7
|
+
|
|
8
|
+
export function classifyProviderRetry(errorMessage: string): ProviderRetryKind {
|
|
9
|
+
if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage)) return "first_event_timeout";
|
|
10
|
+
if (IDLE_STREAM_STALL_PATTERN.test(errorMessage)) return "idle_stream_stall";
|
|
11
|
+
return "provider_error";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function providerNameFromModel(model: string | undefined): string | undefined {
|
|
15
|
+
const provider = model?.split("/", 1)[0]?.trim();
|
|
16
|
+
return provider || undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function providerRetryPhaseLabel(kind: ProviderRetryKind): string {
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case "first_event_timeout":
|
|
22
|
+
return "first event timeout";
|
|
23
|
+
case "idle_stream_stall":
|
|
24
|
+
return "stream stalled";
|
|
25
|
+
case "provider_error":
|
|
26
|
+
return "provider error";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function providerProgressAgeLabel(retryState: NonNullable<AgentProgress["retryState"]>, nowMs: number): string {
|
|
31
|
+
if (retryState.lastProviderProgressAtMs === undefined) return "no provider events yet";
|
|
32
|
+
const ageSeconds = Math.max(0, Math.floor((nowMs - retryState.lastProviderProgressAtMs) / 1000));
|
|
33
|
+
return `last provider progress ${ageSeconds}s ago`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
37
|
+
return Boolean(value) && typeof value === "object";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTaskToolDetails(value: unknown): value is TaskToolDetails {
|
|
41
|
+
return (
|
|
42
|
+
isRecord(value) && Array.isArray(value.results) && (value.progress === undefined || Array.isArray(value.progress))
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isAgentProgress(value: unknown): value is AgentProgress {
|
|
47
|
+
return isRecord(value) && typeof value.status === "string";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hasActiveProviderRetryInTaskDetails(details: TaskToolDetails, seen = new WeakSet<object>()): boolean {
|
|
51
|
+
if (!isTaskToolDetails(details) || seen.has(details)) return false;
|
|
52
|
+
seen.add(details);
|
|
53
|
+
for (const progress of details.progress ?? []) {
|
|
54
|
+
if (isAgentProgress(progress) && hasActiveProviderRetryInProgress(progress, seen)) return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function hasActiveProviderRetryInProgress(progress: AgentProgress, seen = new WeakSet<object>()): boolean {
|
|
60
|
+
if (!isAgentProgress(progress) || seen.has(progress)) return false;
|
|
61
|
+
seen.add(progress);
|
|
62
|
+
if (progress.status === "running" && progress.retryState) return true;
|
|
63
|
+
|
|
64
|
+
const nestedTaskData = isRecord(progress.extractedToolData) ? progress.extractedToolData.task : undefined;
|
|
65
|
+
if (Array.isArray(nestedTaskData)) {
|
|
66
|
+
for (const nestedDetails of nestedTaskData) {
|
|
67
|
+
if (hasActiveProviderRetryInTaskDetails(nestedDetails as TaskToolDetails, seen)) return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return hasActiveProviderRetryInTaskDetails(progress.inflightTaskDetails as TaskToolDetails, seen);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ProviderDegradationGroup {
|
|
74
|
+
provider: string;
|
|
75
|
+
count: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function collectProviderDegradationGroups(progress: readonly AgentProgress[]): ProviderDegradationGroup[] {
|
|
79
|
+
const counts = new Map<string, number>();
|
|
80
|
+
for (const item of progress) {
|
|
81
|
+
if (!isAgentProgress(item) || item.status !== "running" || !item.retryState) continue;
|
|
82
|
+
const provider = item.retryState.provider ?? "provider";
|
|
83
|
+
counts.set(provider, (counts.get(provider) ?? 0) + 1);
|
|
84
|
+
}
|
|
85
|
+
return Array.from(counts, ([provider, count]) => ({ provider, count }))
|
|
86
|
+
.filter(group => group.count > 1)
|
|
87
|
+
.sort((a, b) => b.count - a.count || a.provider.localeCompare(b.provider));
|
|
88
|
+
}
|
package/src/task/render.ts
CHANGED
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
type SubmitReviewDetails,
|
|
29
29
|
} from "../tools/review";
|
|
30
30
|
import { Ellipsis, Hasher, type RenderCache, renderStatusLine } from "../tui";
|
|
31
|
+
import {
|
|
32
|
+
collectProviderDegradationGroups,
|
|
33
|
+
hasActiveProviderRetryInTaskDetails,
|
|
34
|
+
providerProgressAgeLabel,
|
|
35
|
+
providerRetryPhaseLabel,
|
|
36
|
+
} from "./provider-retry-status";
|
|
31
37
|
import type { TaskResultReceipt } from "./receipt";
|
|
32
38
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
33
39
|
import type { AgentProgress, TaskParams, TaskToolDetails } from "./types";
|
|
@@ -123,6 +129,15 @@ function formatModelSubstitutionWarning(warning: { requested: string; effective:
|
|
|
123
129
|
return `Requested model substituted: ${warning.requested} -> ${warning.effective}`;
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
function renderProviderDegradationNotices(progress: readonly AgentProgress[], theme: Theme): string[] {
|
|
133
|
+
return collectProviderDegradationGroups(progress).map(group =>
|
|
134
|
+
theme.fg(
|
|
135
|
+
"warning",
|
|
136
|
+
truncateToWidth(replaceTabs(`provider degraded: ${group.count} subagents retrying on ${group.provider}`), 100),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
126
141
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
127
142
|
if (value === null) return "null";
|
|
128
143
|
if (typeof value === "string") {
|
|
@@ -548,14 +563,11 @@ function renderAgentProgress(
|
|
|
548
563
|
const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
|
|
549
564
|
let statusLine = `${prefix} ${theme.fg(iconColor, icon)} ${theme.fg("accent", titlePart)}`;
|
|
550
565
|
|
|
551
|
-
//
|
|
552
|
-
// is sleeping on a provider 429, not silently progressing. Wins over the
|
|
553
|
-
// generic running spinner because "we're waiting on a quota window" is
|
|
554
|
-
// the operationally meaningful state.
|
|
566
|
+
// A provider recovery loop is operationally distinct from normal agent work.
|
|
555
567
|
if (progress.retryState && progress.status === "running") {
|
|
556
|
-
statusLine += ` ${formatBadge("
|
|
568
|
+
statusLine += ` ${formatBadge("provider degraded", "warning", theme)}`;
|
|
557
569
|
} else if (progress.retryFailure && (progress.status === "failed" || progress.status === "aborted")) {
|
|
558
|
-
statusLine += ` ${formatBadge("
|
|
570
|
+
statusLine += ` ${formatBadge("provider failed", "error", theme)}`;
|
|
559
571
|
} else if (progress.status === "failed" || progress.status === "aborted") {
|
|
560
572
|
const statusLabel = progress.status === "failed" ? "failed" : "aborted";
|
|
561
573
|
statusLine += ` ${formatBadge(statusLabel, iconColor, theme)}`;
|
|
@@ -615,8 +627,10 @@ function renderAgentProgress(
|
|
|
615
627
|
// keep spinning while a child sleeps on a 3-hour provider rate-limit.
|
|
616
628
|
if (progress.retryState && progress.status === "running") {
|
|
617
629
|
const attemptLabel = progress.retryState.unbounded
|
|
618
|
-
? `attempt ${progress.retryState.attempt}`
|
|
619
|
-
:
|
|
630
|
+
? `attempt ${progress.retryState.attempt}, unbounded`
|
|
631
|
+
: `attempt ${progress.retryState.attempt} of ${progress.retryState.maxAttempts}, bounded`;
|
|
632
|
+
const phaseLabel = providerRetryPhaseLabel(progress.retryState.kind);
|
|
633
|
+
const progressAge = staticTime ? "" : ` · ${providerProgressAgeLabel(progress.retryState, Date.now())}`;
|
|
620
634
|
// `staticTime` omits the wall-clock countdown so a cached await body stays a
|
|
621
635
|
// pure function of its key (the producer already drops time-only churn).
|
|
622
636
|
let waitLabel = "";
|
|
@@ -624,7 +638,7 @@ function renderAgentProgress(
|
|
|
624
638
|
const remainingMs = Math.max(0, progress.retryState.startedAtMs + progress.retryState.delayMs - Date.now());
|
|
625
639
|
waitLabel = remainingMs > 0 ? ` in ${formatDuration(remainingMs)}` : " now";
|
|
626
640
|
}
|
|
627
|
-
const summary =
|
|
641
|
+
const summary = `${phaseLabel} · retrying ${attemptLabel}${waitLabel}${progressAge}: ${truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60)}`;
|
|
628
642
|
lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
|
|
629
643
|
} else if (progress.retryFailure && progress.status !== "running") {
|
|
630
644
|
const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
|
|
@@ -953,23 +967,28 @@ export function renderResult(
|
|
|
953
967
|
.u32(spinnerFrame ?? 0)
|
|
954
968
|
.u32(width)
|
|
955
969
|
.digest();
|
|
956
|
-
|
|
970
|
+
const hasDynamicRetry = hasActiveProviderRetryInTaskDetails(details);
|
|
971
|
+
if (!hasDynamicRetry && cached?.key === key) return cached.lines;
|
|
957
972
|
|
|
958
973
|
const lines: string[] = [];
|
|
959
974
|
|
|
960
|
-
const
|
|
961
|
-
|
|
975
|
+
const hasProgress = Boolean(details.progress && details.progress.length > 0);
|
|
976
|
+
const shouldRenderProgress = hasProgress && (isPartial || details.results.length === 0);
|
|
977
|
+
const hasResults = details.results.length > 0;
|
|
978
|
+
if (hasResults) {
|
|
979
|
+
details.results.forEach((res, i) => {
|
|
980
|
+
const isLast = !shouldRenderProgress && i === details.results.length - 1;
|
|
981
|
+
lines.push(...renderAgentResult(res, isLast, expanded, theme));
|
|
982
|
+
});
|
|
983
|
+
}
|
|
962
984
|
if (shouldRenderProgress && details.progress) {
|
|
985
|
+
lines.push(...renderProviderDegradationNotices(details.progress, theme));
|
|
963
986
|
details.progress.forEach((progress, i) => {
|
|
964
987
|
const isLast = i === details.progress!.length - 1;
|
|
965
988
|
lines.push(...renderAgentProgress(progress, isLast, expanded, theme, spinnerFrame));
|
|
966
989
|
});
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
const isLast = i === details.results.length - 1;
|
|
970
|
-
lines.push(...renderAgentResult(res, isLast, expanded, theme));
|
|
971
|
-
});
|
|
972
|
-
|
|
990
|
+
}
|
|
991
|
+
if (hasResults && !shouldRenderProgress) {
|
|
973
992
|
const abortedCount = details.results.filter(r => r.status === "aborted").length;
|
|
974
993
|
const mergeFailedCount = details.results.filter(r => r.status === "merge_failed").length;
|
|
975
994
|
const successCount = details.results.filter(r => r.status === "completed").length;
|
|
@@ -997,7 +1016,7 @@ export function renderResult(
|
|
|
997
1016
|
if (lines.length === 0) {
|
|
998
1017
|
const text = fallbackText.trim() ? fallbackText : "No results";
|
|
999
1018
|
const result = [theme.fg("dim", truncateToWidth(text, width))];
|
|
1000
|
-
cached = { key, lines: result };
|
|
1019
|
+
if (!hasDynamicRetry) cached = { key, lines: result };
|
|
1001
1020
|
return result;
|
|
1002
1021
|
}
|
|
1003
1022
|
|
|
@@ -1019,9 +1038,9 @@ export function renderResult(
|
|
|
1019
1038
|
}
|
|
1020
1039
|
|
|
1021
1040
|
const indented = lines.map(line =>
|
|
1022
|
-
line.length > 0 ? truncateToWidth(` ${line}`, width, Ellipsis.Omit) : "",
|
|
1041
|
+
line.length > 0 ? truncateToWidth(` ${replaceTabs(line)}`, width, Ellipsis.Omit) : "",
|
|
1023
1042
|
);
|
|
1024
|
-
cached = { key, lines: indented };
|
|
1043
|
+
if (!hasDynamicRetry) cached = { key, lines: indented };
|
|
1025
1044
|
return indented;
|
|
1026
1045
|
},
|
|
1027
1046
|
invalidate() {
|
|
@@ -1065,16 +1084,16 @@ function renderNestedTaskTree(
|
|
|
1065
1084
|
): string[] {
|
|
1066
1085
|
const lines: string[] = [];
|
|
1067
1086
|
for (const details of detailsList) {
|
|
1068
|
-
const
|
|
1069
|
-
|
|
1087
|
+
const inflight = details.progress;
|
|
1088
|
+
const hasInflight = Boolean(inflight && inflight.length > 0);
|
|
1089
|
+
if (details.results && details.results.length > 0) {
|
|
1070
1090
|
details.results.forEach((result, index) => {
|
|
1071
|
-
const isLast = index === details.results.length - 1;
|
|
1091
|
+
const isLast = !hasInflight && index === details.results.length - 1;
|
|
1072
1092
|
lines.push(...renderAgentResult(result, isLast, expanded, theme));
|
|
1073
1093
|
});
|
|
1074
|
-
continue;
|
|
1075
1094
|
}
|
|
1076
|
-
const inflight = details.progress;
|
|
1077
1095
|
if (inflight && inflight.length > 0) {
|
|
1096
|
+
lines.push(...renderProviderDegradationNotices(inflight, theme));
|
|
1078
1097
|
inflight.forEach((prog, index) => {
|
|
1079
1098
|
const isLast = index === inflight.length - 1;
|
|
1080
1099
|
lines.push(...renderAgentProgress(prog, isLast, expanded, theme, spinnerFrame, staticTime));
|
package/src/task/types.ts
CHANGED
|
@@ -285,6 +285,9 @@ export interface AgentProgress {
|
|
|
285
285
|
attempt: number;
|
|
286
286
|
maxAttempts: number;
|
|
287
287
|
unbounded?: boolean;
|
|
288
|
+
kind: "first_event_timeout" | "idle_stream_stall" | "provider_error";
|
|
289
|
+
provider?: string;
|
|
290
|
+
lastProviderProgressAtMs?: number;
|
|
288
291
|
delayMs: number;
|
|
289
292
|
errorMessage: string;
|
|
290
293
|
startedAtMs: number;
|
package/src/tools/ask.ts
CHANGED
|
@@ -461,6 +461,7 @@ function recoverRoundZeroIntentContract(
|
|
|
461
461
|
const deepInterview = question.deepInterview;
|
|
462
462
|
const hasIntentContract = Object.hasOwn(deepInterview, "intent_contract");
|
|
463
463
|
const hasIntentReview = Object.hasOwn(deepInterview, "intent_review");
|
|
464
|
+
if (hasIntentContract && hasIntentReview && stage !== "topology") return { outcome: "reject" };
|
|
464
465
|
if (hasIntentContract !== hasIntentReview && askSchema.safeParse(normalizedArguments).success)
|
|
465
466
|
return { outcome: "passthrough" };
|
|
466
467
|
|