wave-agent-sdk 1.0.0 → 1.0.2
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/dist/agent.d.ts +9 -20
- package/dist/agent.js +35 -97
- package/dist/managers/aiManager.d.ts +63 -8
- package/dist/managers/aiManager.js +274 -80
- package/dist/managers/messageManager.d.ts +9 -5
- package/dist/managers/messageManager.js +36 -12
- package/dist/managers/permissionManager.js +13 -11
- package/dist/managers/subagentManager.d.ts +6 -0
- package/dist/managers/subagentManager.js +33 -22
- package/dist/prompts/index.d.ts +0 -2
- package/dist/prompts/index.js +71 -47
- package/dist/services/aiService.d.ts +1 -34
- package/dist/services/aiService.js +18 -130
- package/dist/services/autoMemoryService.d.ts +27 -2
- package/dist/services/autoMemoryService.js +124 -36
- package/dist/services/configurationService.js +14 -1
- package/dist/services/session.d.ts +13 -0
- package/dist/services/session.js +64 -0
- package/dist/types/agent.d.ts +0 -2
- package/dist/types/config.d.ts +7 -0
- package/dist/types/core.d.ts +1 -1
- package/dist/utils/containerSetup.js +12 -3
- package/package.json +1 -1
- package/src/agent.ts +50 -110
- package/src/managers/aiManager.ts +370 -105
- package/src/managers/messageManager.ts +51 -23
- package/src/managers/permissionManager.ts +15 -13
- package/src/managers/subagentManager.ts +36 -25
- package/src/prompts/index.ts +75 -56
- package/src/services/aiService.ts +25 -203
- package/src/services/autoMemoryService.ts +145 -39
- package/src/services/configurationService.ts +16 -1
- package/src/services/session.ts +68 -0
- package/src/types/agent.ts +0 -6
- package/src/types/config.ts +7 -0
- package/src/types/core.ts +1 -1
- package/src/utils/containerSetup.ts +12 -4
- package/dist/constants/goalPrompts.d.ts +0 -1
- package/dist/constants/goalPrompts.js +0 -10
- package/dist/managers/goalManager.d.ts +0 -42
- package/dist/managers/goalManager.js +0 -177
- package/src/constants/goalPrompts.ts +0 -10
- package/src/managers/goalManager.ts +0 -232
|
@@ -62,6 +62,22 @@ import {
|
|
|
62
62
|
import { logOTelEvent } from "../telemetry/events.js";
|
|
63
63
|
import type { BackgroundTask } from "../types/processes.js";
|
|
64
64
|
|
|
65
|
+
/** Result of a fork-path agent loop (compaction or auto-memory extraction). */
|
|
66
|
+
interface ForkLoopResult {
|
|
67
|
+
content?: string;
|
|
68
|
+
usage?: {
|
|
69
|
+
prompt_tokens: number;
|
|
70
|
+
completion_tokens: number;
|
|
71
|
+
total_tokens: number;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Max turns for the compaction fork: the model should summarize, not act. */
|
|
76
|
+
const MAX_FORK_TURNS = 3;
|
|
77
|
+
|
|
78
|
+
/** Max turns for the auto-memory extraction fork. */
|
|
79
|
+
const MAX_AUTO_MEMORY_FORK_TURNS = 5;
|
|
80
|
+
|
|
65
81
|
// Truncate text to `max` chars and append a "… [+N chars]" marker when exceeded.
|
|
66
82
|
// Used for background_tasks description/command fields (≤1000 chars per spec FR-063).
|
|
67
83
|
function truncateWithMarker(text: string, max: number): string {
|
|
@@ -694,8 +710,7 @@ export class AIManager {
|
|
|
694
710
|
} catch (compactError) {
|
|
695
711
|
this.consecutiveCompactionFailures++;
|
|
696
712
|
logger?.error(
|
|
697
|
-
`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive)
|
|
698
|
-
compactError,
|
|
713
|
+
`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive): ${compactError instanceof Error ? compactError.message : String(compactError)}`,
|
|
699
714
|
);
|
|
700
715
|
this.messageManager.addErrorBlock(
|
|
701
716
|
`Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`,
|
|
@@ -745,27 +760,26 @@ export class AIManager {
|
|
|
745
760
|
}
|
|
746
761
|
|
|
747
762
|
/**
|
|
748
|
-
* Fork-path
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
* another turn. Returns undefined content when the model never
|
|
754
|
-
* text; the caller treats that as a
|
|
763
|
+
* Fork-path loop: run a bounded agent loop over a copy of the conversation
|
|
764
|
+
* using the same system prompt, tools, model, and generation params as the
|
|
765
|
+
* main loop, so the forked request prefix matches exactly and the prompt
|
|
766
|
+
* cache is reused. A `canUseTool` gate decides whether each tool call
|
|
767
|
+
* executes locally (with a stripped context) or is denied and fed back to
|
|
768
|
+
* the model for another turn. Returns undefined content when the model never
|
|
769
|
+
* produces text; the caller treats that as a failure.
|
|
755
770
|
*/
|
|
756
|
-
private async
|
|
771
|
+
private async runForkLoop(
|
|
757
772
|
historyMessages: ChatCompletionMessageParam[],
|
|
758
|
-
|
|
773
|
+
prompt: string,
|
|
774
|
+
options: {
|
|
775
|
+
maxTurns: number;
|
|
776
|
+
/** Gate deciding which tool calls execute locally. When undefined, every tool call is denied. */
|
|
777
|
+
canUseTool?: (name: string, args: Record<string, unknown>) => boolean;
|
|
778
|
+
/** Message fed back to the model when a tool call is denied. */
|
|
779
|
+
deniedToolMessage?: string;
|
|
780
|
+
},
|
|
759
781
|
abortSignal?: AbortSignal,
|
|
760
|
-
): Promise<{
|
|
761
|
-
content?: string;
|
|
762
|
-
usage?: {
|
|
763
|
-
prompt_tokens: number;
|
|
764
|
-
completion_tokens: number;
|
|
765
|
-
total_tokens: number;
|
|
766
|
-
};
|
|
767
|
-
}> {
|
|
768
|
-
const MAX_FORK_TURNS = 3;
|
|
782
|
+
): Promise<ForkLoopResult> {
|
|
769
783
|
const modelConfig = this.getModelConfig();
|
|
770
784
|
const gatewayConfig = this.getGatewayConfig();
|
|
771
785
|
const sessionId = this.messageManager.getSessionId();
|
|
@@ -783,21 +797,22 @@ export class AIManager {
|
|
|
783
797
|
});
|
|
784
798
|
}
|
|
785
799
|
|
|
786
|
-
forkMessages.push({ role: "user", content:
|
|
800
|
+
forkMessages.push({ role: "user", content: prompt });
|
|
787
801
|
|
|
788
802
|
const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
|
|
789
803
|
const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
790
804
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
805
|
+
// Fresh read-state map so Read/Edit state built up inside the fork never
|
|
806
|
+
// leaks into the main session's dedup and staleness tracking.
|
|
807
|
+
const forkReadFileState = new Map<
|
|
808
|
+
string,
|
|
809
|
+
{ mtime: number; hash: string; offset?: number; limit?: number }
|
|
810
|
+
>();
|
|
811
|
+
|
|
812
|
+
let totalUsage: ForkLoopResult["usage"];
|
|
798
813
|
let content: string | undefined;
|
|
799
814
|
|
|
800
|
-
for (let turn = 0; turn <
|
|
815
|
+
for (let turn = 0; turn < options.maxTurns; turn++) {
|
|
801
816
|
const result = await aiService.callAgent({
|
|
802
817
|
gatewayConfig,
|
|
803
818
|
modelConfig,
|
|
@@ -810,7 +825,7 @@ export class AIManager {
|
|
|
810
825
|
toolChoice: this.toolChoiceOverride,
|
|
811
826
|
// Stream so a slow reasoning model emits first bytes before the
|
|
812
827
|
// gateway's idle timeout fires (non-streaming waits for the full
|
|
813
|
-
//
|
|
828
|
+
// response, which exceeds the timeout on large contexts).
|
|
814
829
|
stream: true,
|
|
815
830
|
});
|
|
816
831
|
|
|
@@ -832,31 +847,329 @@ export class AIManager {
|
|
|
832
847
|
}
|
|
833
848
|
|
|
834
849
|
if (result.tool_calls && result.tool_calls.length > 0) {
|
|
835
|
-
|
|
836
|
-
|
|
850
|
+
const functionCalls = result.tool_calls.filter(
|
|
851
|
+
(tc) => tc.type === "function",
|
|
852
|
+
);
|
|
853
|
+
if (functionCalls.length === 0) break;
|
|
854
|
+
|
|
837
855
|
forkMessages.push({
|
|
838
856
|
role: "assistant",
|
|
839
857
|
content: result.content ?? null,
|
|
840
|
-
tool_calls:
|
|
858
|
+
tool_calls: functionCalls,
|
|
841
859
|
});
|
|
842
|
-
|
|
860
|
+
|
|
861
|
+
for (const toolCall of functionCalls) {
|
|
862
|
+
const name = toolCall.function?.name || "";
|
|
863
|
+
const args = this.parseForkToolArgs(toolCall.function?.arguments);
|
|
864
|
+
let toolContent: string;
|
|
865
|
+
if (options.canUseTool && options.canUseTool(name, args)) {
|
|
866
|
+
toolContent = await this.executeForkTool(
|
|
867
|
+
name,
|
|
868
|
+
args,
|
|
869
|
+
workdir,
|
|
870
|
+
sessionId,
|
|
871
|
+
abortSignal,
|
|
872
|
+
forkReadFileState,
|
|
873
|
+
);
|
|
874
|
+
} else {
|
|
875
|
+
toolContent =
|
|
876
|
+
options.deniedToolMessage ??
|
|
877
|
+
"Tool use is not allowed in this context";
|
|
878
|
+
}
|
|
843
879
|
forkMessages.push({
|
|
844
880
|
role: "tool",
|
|
845
881
|
tool_call_id: toolCall.id,
|
|
846
|
-
content:
|
|
882
|
+
content: toolContent,
|
|
847
883
|
});
|
|
848
884
|
}
|
|
849
885
|
continue;
|
|
850
886
|
}
|
|
851
887
|
|
|
852
888
|
// Neither text nor tool calls: retrying the identical request is
|
|
853
|
-
// pointless, bail out and let the caller fail
|
|
889
|
+
// pointless, bail out and let the caller fail.
|
|
854
890
|
break;
|
|
855
891
|
}
|
|
856
892
|
|
|
857
893
|
return { content, usage: totalUsage };
|
|
858
894
|
}
|
|
859
895
|
|
|
896
|
+
/**
|
|
897
|
+
* Fork-path compaction: deny all tool calls locally (the model is told to
|
|
898
|
+
* summarize, not act) and feed the rejections back for another turn.
|
|
899
|
+
*/
|
|
900
|
+
private async runCompactFork(
|
|
901
|
+
historyMessages: ChatCompletionMessageParam[],
|
|
902
|
+
compactPrompt: string,
|
|
903
|
+
abortSignal?: AbortSignal,
|
|
904
|
+
): Promise<ForkLoopResult> {
|
|
905
|
+
return this.runForkLoop(
|
|
906
|
+
historyMessages,
|
|
907
|
+
compactPrompt,
|
|
908
|
+
{
|
|
909
|
+
maxTurns: MAX_FORK_TURNS,
|
|
910
|
+
deniedToolMessage: "Tool use is not allowed during compaction",
|
|
911
|
+
},
|
|
912
|
+
abortSignal,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Auto-memory extraction via the perfect fork: the extraction prompt is run
|
|
918
|
+
* against the same request prefix as the main conversation (same system
|
|
919
|
+
* prompt, tools, model, and message history) so the prompt cache is reused.
|
|
920
|
+
* Gate-approved tools execute locally in a stripped context; everything else
|
|
921
|
+
* is denied. Usage is reported with operation_type "agent" so extraction
|
|
922
|
+
* token costs stay visible in session accounting.
|
|
923
|
+
*/
|
|
924
|
+
public async runAutoMemoryFork(
|
|
925
|
+
messages: Message[],
|
|
926
|
+
prompt: string,
|
|
927
|
+
options: {
|
|
928
|
+
canUseTool: (name: string, args: Record<string, unknown>) => boolean;
|
|
929
|
+
deniedToolMessage?: string;
|
|
930
|
+
maxTurns?: number;
|
|
931
|
+
},
|
|
932
|
+
abortSignal?: AbortSignal,
|
|
933
|
+
): Promise<ForkLoopResult> {
|
|
934
|
+
const modelConfig = this.getModelConfig();
|
|
935
|
+
const historyMessages = convertMessagesForAPI(messages, {
|
|
936
|
+
supportsVision: supportsVision(modelConfig.capabilities),
|
|
937
|
+
});
|
|
938
|
+
// Give the fork a real signal even when the caller has none, so tools
|
|
939
|
+
// (e.g. Bash's foreground path) always receive a well-formed context.
|
|
940
|
+
const signal = abortSignal ?? new AbortController().signal;
|
|
941
|
+
const result = await this.runForkLoop(
|
|
942
|
+
historyMessages,
|
|
943
|
+
prompt,
|
|
944
|
+
{
|
|
945
|
+
maxTurns: options.maxTurns ?? MAX_AUTO_MEMORY_FORK_TURNS,
|
|
946
|
+
canUseTool: options.canUseTool,
|
|
947
|
+
deniedToolMessage: options.deniedToolMessage,
|
|
948
|
+
},
|
|
949
|
+
signal,
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
if (result.usage && this.callbacks?.onUsageAdded) {
|
|
953
|
+
this.callbacks.onUsageAdded({
|
|
954
|
+
...result.usage,
|
|
955
|
+
model: modelConfig.model,
|
|
956
|
+
operation_type: "agent",
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
return result;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Parse a fork tool call's JSON arguments, recovering truncated JSON the
|
|
964
|
+
* same way the main loop does. Unparseable arguments fall back to `{}` and
|
|
965
|
+
* are rejected by the gate or the tool's own parameter validation.
|
|
966
|
+
*/
|
|
967
|
+
private parseForkToolArgs(
|
|
968
|
+
argsString: string | undefined,
|
|
969
|
+
): Record<string, unknown> {
|
|
970
|
+
if (!argsString?.trim()) return {};
|
|
971
|
+
try {
|
|
972
|
+
return JSON.parse(argsString) as Record<string, unknown>;
|
|
973
|
+
} catch {
|
|
974
|
+
try {
|
|
975
|
+
return JSON.parse(recoverTruncatedJson(argsString)) as Record<
|
|
976
|
+
string,
|
|
977
|
+
unknown
|
|
978
|
+
>;
|
|
979
|
+
} catch {
|
|
980
|
+
return {};
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Execute a single tool call inside a fork with a stripped context: no
|
|
987
|
+
* permission manager (never prompts the user), no message manager (no
|
|
988
|
+
* conditional-rule triggering), no messageId (no file-history snapshots),
|
|
989
|
+
* and no background task manager (commands run in the foreground). Only
|
|
990
|
+
* gate-approved tool names reach this path.
|
|
991
|
+
*/
|
|
992
|
+
private async executeForkTool(
|
|
993
|
+
name: string,
|
|
994
|
+
args: Record<string, unknown>,
|
|
995
|
+
workdir: string,
|
|
996
|
+
sessionId: string | undefined,
|
|
997
|
+
abortSignal: AbortSignal | undefined,
|
|
998
|
+
readFileState: ToolContext["readFileState"],
|
|
999
|
+
): Promise<string> {
|
|
1000
|
+
const plugin = this.toolManager.getTools().find((t) => t.name === name);
|
|
1001
|
+
if (!plugin) {
|
|
1002
|
+
return `Tool '${name}' not found`;
|
|
1003
|
+
}
|
|
1004
|
+
const context: ToolContext = {
|
|
1005
|
+
abortSignal,
|
|
1006
|
+
workdir,
|
|
1007
|
+
originalWorkdir: this.originalWorkdir,
|
|
1008
|
+
sessionId,
|
|
1009
|
+
taskManager: this.taskManager,
|
|
1010
|
+
readFileState,
|
|
1011
|
+
onShortResultUpdate: () => {},
|
|
1012
|
+
onResultUpdate: () => {},
|
|
1013
|
+
onCwdChange: () => {},
|
|
1014
|
+
};
|
|
1015
|
+
try {
|
|
1016
|
+
const result = await plugin.execute(args, context);
|
|
1017
|
+
if (result.content) return result.content;
|
|
1018
|
+
if (result.error) return `Error: ${result.error}`;
|
|
1019
|
+
return "";
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1022
|
+
logger?.error(`Fork tool execution failed for ${name}:`, error);
|
|
1023
|
+
return `Tool execution failed: ${message}`;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Fork-path side question ("/btw"): run a single-turn fork of the
|
|
1029
|
+
* conversation using the same system prompt, tools, model, and generation
|
|
1030
|
+
* params as the main loop, so the forked request prefix matches exactly and
|
|
1031
|
+
* the prompt cache is reused. The in-progress assistant message (if any) is
|
|
1032
|
+
* stripped so the fork starts from the last completed request prefix. Tools
|
|
1033
|
+
* are never executed — the wrapped question instructs the model to answer
|
|
1034
|
+
* directly; an attempted tool call is surfaced as an error string.
|
|
1035
|
+
*/
|
|
1036
|
+
async runBtwFork(
|
|
1037
|
+
question: string,
|
|
1038
|
+
abortSignal?: AbortSignal,
|
|
1039
|
+
onContent?: (content: string) => void,
|
|
1040
|
+
onReasoning?: (content: string) => void,
|
|
1041
|
+
): Promise<{ content?: string; error?: string }> {
|
|
1042
|
+
const modelConfig = this.getModelConfig();
|
|
1043
|
+
const gatewayConfig = this.getGatewayConfig();
|
|
1044
|
+
const sessionId = this.messageManager.getSessionId();
|
|
1045
|
+
const workdir = this.getWorkdir();
|
|
1046
|
+
|
|
1047
|
+
const rawMessages = this.messageManager.getMessages();
|
|
1048
|
+
|
|
1049
|
+
// Strip the in-progress assistant message (a block still in "streaming"
|
|
1050
|
+
// stage) so the fork's request prefix matches the last completed
|
|
1051
|
+
// main-loop request and the prompt cache is reused.
|
|
1052
|
+
const lastMessage = rawMessages[rawMessages.length - 1];
|
|
1053
|
+
const hasInProgressMessage =
|
|
1054
|
+
lastMessage?.role === "assistant" &&
|
|
1055
|
+
lastMessage.blocks.some(
|
|
1056
|
+
(b) => "stage" in b && (b as { stage?: string }).stage === "streaming",
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
const forkMessages: ChatCompletionMessageParam[] = convertMessagesForAPI(
|
|
1060
|
+
hasInProgressMessage ? rawMessages.slice(0, -1) : rawMessages,
|
|
1061
|
+
{ supportsVision: supportsVision(modelConfig.capabilities) },
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
// Mirror the main loop's memory injection so the request prefix matches.
|
|
1065
|
+
const { prependContent } =
|
|
1066
|
+
await this.messageManager.getMemoryForInjection();
|
|
1067
|
+
if (prependContent.trim()) {
|
|
1068
|
+
forkMessages.unshift({
|
|
1069
|
+
role: "user",
|
|
1070
|
+
content: wrapInSystemReminder(prependContent),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Wrap the question with the side-question instructions (verbatim
|
|
1075
|
+
// Claude Code sideQuestion.ts) so the model answers directly.
|
|
1076
|
+
const wrappedQuestion = `<system-reminder>This is a side question from the user. You must answer this question directly in a single response.
|
|
1077
|
+
|
|
1078
|
+
IMPORTANT CONTEXT:
|
|
1079
|
+
- You are a separate, lightweight agent spawned to answer this one question
|
|
1080
|
+
- The main agent is NOT interrupted - it continues working independently in the background
|
|
1081
|
+
- You share the conversation context but are a completely separate instance
|
|
1082
|
+
- Do NOT reference being interrupted or what you were "previously doing" - that framing is incorrect
|
|
1083
|
+
|
|
1084
|
+
CRITICAL CONSTRAINTS:
|
|
1085
|
+
- You have NO tools available - you cannot read files, run commands, search, or take any actions
|
|
1086
|
+
- This is a one-off response - there will be no follow-up turns
|
|
1087
|
+
- You can ONLY provide information based on what you already know from the conversation context
|
|
1088
|
+
- NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action
|
|
1089
|
+
- If you don't know the answer, say so - do not offer to look it up or investigate
|
|
1090
|
+
|
|
1091
|
+
Simply answer the question with the information you have.</system-reminder>
|
|
1092
|
+
|
|
1093
|
+
${question}`;
|
|
1094
|
+
forkMessages.push({ role: "user", content: wrappedQuestion });
|
|
1095
|
+
|
|
1096
|
+
const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
|
|
1097
|
+
const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
1098
|
+
|
|
1099
|
+
try {
|
|
1100
|
+
// Surface partial output to the caller (e.g. the /btw overlay's
|
|
1101
|
+
// streaming display) as it arrives. Reasoning chunks from thinking
|
|
1102
|
+
// models stream through a separate channel when the caller supplies
|
|
1103
|
+
// one (webview hosts distinguish thinking from content so the panel
|
|
1104
|
+
// can drop thinking text once content starts); otherwise they fall
|
|
1105
|
+
// back to the content channel (CLI overlay mixes both).
|
|
1106
|
+
const streamToOverlay = (text: string) => {
|
|
1107
|
+
if (text.trim()) {
|
|
1108
|
+
onContent?.(text);
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
const streamReasoning = (text: string) => {
|
|
1112
|
+
if (text.trim()) {
|
|
1113
|
+
if (onReasoning) {
|
|
1114
|
+
onReasoning(text);
|
|
1115
|
+
} else {
|
|
1116
|
+
onContent?.(text);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
const result = await aiService.callAgent({
|
|
1121
|
+
gatewayConfig,
|
|
1122
|
+
modelConfig,
|
|
1123
|
+
messages: forkMessages,
|
|
1124
|
+
sessionId,
|
|
1125
|
+
abortSignal,
|
|
1126
|
+
workdir,
|
|
1127
|
+
tools: toolsConfig,
|
|
1128
|
+
systemPrompt,
|
|
1129
|
+
toolChoice: this.toolChoiceOverride,
|
|
1130
|
+
// Stream so a slow reasoning model emits first bytes before the
|
|
1131
|
+
// gateway's idle timeout fires (same rationale as runCompactFork).
|
|
1132
|
+
stream: true,
|
|
1133
|
+
onContentUpdate: streamToOverlay,
|
|
1134
|
+
onReasoningUpdate: streamReasoning,
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
if (result.content?.trim()) {
|
|
1138
|
+
return { content: result.content };
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// A thinking model may emit only reasoning content (e.g. the stream
|
|
1142
|
+
// is truncated before the final answer); surface that instead of
|
|
1143
|
+
// falling through to "No response received".
|
|
1144
|
+
if (result.reasoning_content?.trim()) {
|
|
1145
|
+
return { content: result.reasoning_content };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (result.tool_calls && result.tool_calls.length > 0) {
|
|
1149
|
+
const firstFunctionCall = result.tool_calls.find(
|
|
1150
|
+
(call) => call.type === "function",
|
|
1151
|
+
);
|
|
1152
|
+
const toolName = firstFunctionCall?.function?.name ?? "a tool";
|
|
1153
|
+
return {
|
|
1154
|
+
error: `(The model tried to call ${toolName} instead of answering directly. Try rephrasing or ask in the main conversation.)`,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Neither text nor tool calls.
|
|
1159
|
+
return { error: "No response received" };
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
// aiService.callAgent converts AbortError into a plain Error, so the
|
|
1162
|
+
// abort is detected via the signal itself; rethrow so the UI can
|
|
1163
|
+
// silently dismiss instead of showing an error.
|
|
1164
|
+
if (abortSignal?.aborted) {
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1167
|
+
return {
|
|
1168
|
+
error: `(API error: ${error instanceof Error ? error.message : String(error)})`,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
860
1173
|
/**
|
|
861
1174
|
* Build post-compact context restoration content.
|
|
862
1175
|
* Restores file reads, working directory, plan mode, skills, and background tasks.
|
|
@@ -1150,6 +1463,12 @@ export class AIManager {
|
|
|
1150
1463
|
systemPrompt: mainSystemPrompt, // Pass custom system prompt
|
|
1151
1464
|
maxTokens: maxTokens, // Pass max tokens override
|
|
1152
1465
|
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
1466
|
+
// Fast-model subagents send disable-thinking params only when
|
|
1467
|
+
// explicitly configured (never in the agent loop).
|
|
1468
|
+
disableThinkingOptions:
|
|
1469
|
+
this.modelOverride === "fastModel"
|
|
1470
|
+
? this.getModelConfig().disableThinkingOptions
|
|
1471
|
+
: undefined,
|
|
1153
1472
|
};
|
|
1154
1473
|
|
|
1155
1474
|
// Prepend: AGENTS.md + user memory + unconditional rules as system-reminder
|
|
@@ -1578,77 +1897,19 @@ export class AIManager {
|
|
|
1578
1897
|
}
|
|
1579
1898
|
}
|
|
1580
1899
|
|
|
1581
|
-
//
|
|
1582
|
-
const
|
|
1583
|
-
? this.container.get<import("./goalManager.js").GoalManager>(
|
|
1584
|
-
"GoalManager",
|
|
1585
|
-
)
|
|
1586
|
-
: undefined;
|
|
1587
|
-
|
|
1588
|
-
let goalContinuing = false;
|
|
1589
|
-
|
|
1590
|
-
if (goalManager?.isGoalActive() && !this.subagentType) {
|
|
1591
|
-
// 1. Increment turn count and check circuit breakers
|
|
1592
|
-
goalManager.incrementTurnCount();
|
|
1593
|
-
const circuitBreaker = goalManager.checkCircuitBreakers();
|
|
1594
|
-
|
|
1595
|
-
if (circuitBreaker) {
|
|
1596
|
-
goalManager.clearGoal();
|
|
1597
|
-
logger?.info(`[Goal] ${circuitBreaker}`);
|
|
1598
|
-
this.messageManager.addUserMessage({
|
|
1599
|
-
content: `<system-reminder>${circuitBreaker}</system-reminder>`,
|
|
1600
|
-
isMeta: true,
|
|
1601
|
-
});
|
|
1602
|
-
// Fall through to normal Stop hooks on the final turn
|
|
1603
|
-
} else {
|
|
1604
|
-
// 2. Evaluate goal
|
|
1605
|
-
const evaluation = await goalManager.evaluateGoal(
|
|
1606
|
-
abortController.signal,
|
|
1607
|
-
);
|
|
1900
|
+
// Execute Stop hooks
|
|
1901
|
+
const shouldContinue = await this.executeStopHooks();
|
|
1608
1902
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
});
|
|
1616
|
-
// Fall through to normal Stop hooks on the final turn
|
|
1617
|
-
} else {
|
|
1618
|
-
const goal = goalManager.getGoal()!;
|
|
1619
|
-
goal.lastReason = evaluation.reason;
|
|
1620
|
-
logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
|
|
1621
|
-
this.messageManager.addUserMessage({
|
|
1622
|
-
content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
|
|
1623
|
-
isMeta: true,
|
|
1624
|
-
});
|
|
1625
|
-
// Keep loading state active to prevent UI flicker
|
|
1626
|
-
this.setIsLoading(true);
|
|
1627
|
-
goalContinuing = true;
|
|
1628
|
-
// Restart outer loop to continue goal pursuit
|
|
1629
|
-
shouldRestart = true;
|
|
1630
|
-
turnOffset = 0;
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
// Skip Stop hooks when goal evaluator is continuing the conversation
|
|
1636
|
-
if (goalContinuing) {
|
|
1637
|
-
// Goal evaluator supersedes Stop hooks
|
|
1638
|
-
} else {
|
|
1639
|
-
const shouldContinue = await this.executeStopHooks();
|
|
1640
|
-
|
|
1641
|
-
// If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
|
|
1642
|
-
// restart the AI conversation cycle
|
|
1643
|
-
if (shouldContinue) {
|
|
1644
|
-
logger?.info(
|
|
1645
|
-
`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
|
|
1646
|
-
);
|
|
1903
|
+
// If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
|
|
1904
|
+
// restart the AI conversation cycle
|
|
1905
|
+
if (shouldContinue) {
|
|
1906
|
+
logger?.info(
|
|
1907
|
+
`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
|
|
1908
|
+
);
|
|
1647
1909
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
}
|
|
1910
|
+
// Restart the conversation to let AI fix the issues
|
|
1911
|
+
shouldRestart = true;
|
|
1912
|
+
turnOffset = 0;
|
|
1652
1913
|
}
|
|
1653
1914
|
}
|
|
1654
1915
|
|
|
@@ -1934,6 +2195,8 @@ export class AIManager {
|
|
|
1934
2195
|
id: toolId,
|
|
1935
2196
|
shortResult,
|
|
1936
2197
|
stage: "running",
|
|
2198
|
+
compactParams,
|
|
2199
|
+
name: toolName,
|
|
1937
2200
|
});
|
|
1938
2201
|
},
|
|
1939
2202
|
onResultUpdate: (result: string) => {
|
|
@@ -1941,6 +2204,8 @@ export class AIManager {
|
|
|
1941
2204
|
id: toolId,
|
|
1942
2205
|
result,
|
|
1943
2206
|
stage: "running",
|
|
2207
|
+
compactParams,
|
|
2208
|
+
name: toolName,
|
|
1944
2209
|
});
|
|
1945
2210
|
},
|
|
1946
2211
|
onCwdChange: async (newCwd: string) => {
|