@charzhu/openjaw-agent 0.3.1 → 0.3.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/main.js +403 -30
- package/dist/main.js.map +4 -4
- package/package.json +1 -1
- package/prompts/IDENTITY.md +2 -2
- package/prompts/REASONING.md +17 -1
package/dist/main.js
CHANGED
|
@@ -131,7 +131,8 @@ function loadAgentConfig() {
|
|
|
131
131
|
copilot_oauth_client_id: parsedLlm?.copilot_oauth_client_id ?? DEFAULT_CONFIG.llm.copilot_oauth_client_id,
|
|
132
132
|
context_compression: parsedLlm?.context_compression,
|
|
133
133
|
compression_threshold: parsedLlm?.compression_threshold,
|
|
134
|
-
compression_model: parsedLlm?.compression_model
|
|
134
|
+
compression_model: parsedLlm?.compression_model,
|
|
135
|
+
max_tool_rounds: parsedLlm?.max_tool_rounds
|
|
135
136
|
},
|
|
136
137
|
telegram: parsed?.telegram ?? void 0,
|
|
137
138
|
feishu: parsed?.feishu ?? void 0,
|
|
@@ -2641,7 +2642,8 @@ var init_copilot = __esm({
|
|
|
2641
2642
|
messages.push({
|
|
2642
2643
|
role: "assistant",
|
|
2643
2644
|
content: msg.content,
|
|
2644
|
-
...toolCalls?.length ? { tool_calls: toolCalls } : {}
|
|
2645
|
+
...toolCalls?.length ? { tool_calls: toolCalls } : {},
|
|
2646
|
+
...msg.reasoningOpaque ? { reasoning_opaque: msg.reasoningOpaque } : {}
|
|
2645
2647
|
});
|
|
2646
2648
|
} else {
|
|
2647
2649
|
for (const result of msg.results) {
|
|
@@ -2671,11 +2673,12 @@ var init_copilot = __esm({
|
|
|
2671
2673
|
messages: this.buildChatMessages(options),
|
|
2672
2674
|
tools: options.tools.length > 0 ? options.tools.map(toChatTool) : void 0,
|
|
2673
2675
|
tool_choice: options.tools.length > 0 ? "auto" : void 0,
|
|
2674
|
-
temperature: this.config.temperature
|
|
2676
|
+
temperature: this.config.temperature,
|
|
2677
|
+
stream: true
|
|
2675
2678
|
};
|
|
2676
2679
|
const res = await fetch(`${await this.baseUrl(options.signal)}/chat/completions`, {
|
|
2677
2680
|
method: "POST",
|
|
2678
|
-
headers: await this.headers(options),
|
|
2681
|
+
headers: { ...await this.headers(options), Accept: "text/event-stream" },
|
|
2679
2682
|
body: JSON.stringify(requestBody),
|
|
2680
2683
|
signal: options.signal
|
|
2681
2684
|
});
|
|
@@ -2683,19 +2686,85 @@ var init_copilot = __esm({
|
|
|
2683
2686
|
const detail = await res.text();
|
|
2684
2687
|
throw new Error(`GitHub Copilot chat error: ${res.status} ${detail}`);
|
|
2685
2688
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2689
|
+
if (!res.body) throw new Error("GitHub Copilot chat: no response body for stream");
|
|
2690
|
+
let text = null;
|
|
2691
|
+
let finishReason;
|
|
2692
|
+
let reasoningOpaque;
|
|
2693
|
+
let promptTokens;
|
|
2694
|
+
let completionTokens;
|
|
2695
|
+
const toolSlots = [];
|
|
2696
|
+
const lastSlotByIndex = /* @__PURE__ */ new Map();
|
|
2697
|
+
const reader = res.body.getReader();
|
|
2698
|
+
const decoder = new TextDecoder();
|
|
2699
|
+
let buf = "";
|
|
2700
|
+
for (; ; ) {
|
|
2701
|
+
const { done, value } = await reader.read();
|
|
2702
|
+
if (done) break;
|
|
2703
|
+
buf += decoder.decode(value, { stream: true });
|
|
2704
|
+
let nl;
|
|
2705
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
2706
|
+
const rawLine = buf.slice(0, nl).trim();
|
|
2707
|
+
buf = buf.slice(nl + 1);
|
|
2708
|
+
if (!rawLine.startsWith("data:")) continue;
|
|
2709
|
+
const payload = rawLine.slice(5).trim();
|
|
2710
|
+
if (payload === "[DONE]") continue;
|
|
2711
|
+
let evt;
|
|
2712
|
+
try {
|
|
2713
|
+
evt = JSON.parse(payload);
|
|
2714
|
+
} catch {
|
|
2715
|
+
continue;
|
|
2716
|
+
}
|
|
2717
|
+
const choice = evt.choices?.[0];
|
|
2718
|
+
if (choice) {
|
|
2719
|
+
const delta = choice.delta;
|
|
2720
|
+
if (delta) {
|
|
2721
|
+
if (typeof delta.content === "string") text = (text ?? "") + delta.content;
|
|
2722
|
+
if (typeof delta.reasoning_opaque === "string") {
|
|
2723
|
+
reasoningOpaque = (reasoningOpaque ?? "") + delta.reasoning_opaque;
|
|
2724
|
+
}
|
|
2725
|
+
const deltaToolCalls = delta.tool_calls;
|
|
2726
|
+
for (const tc of deltaToolCalls ?? []) {
|
|
2727
|
+
const idx = typeof tc.index === "number" ? tc.index : 0;
|
|
2728
|
+
const fn = tc.function;
|
|
2729
|
+
const startsNewCall = typeof tc.id === "string" || typeof fn?.name === "string";
|
|
2730
|
+
let slot;
|
|
2731
|
+
if (startsNewCall) {
|
|
2732
|
+
slot = { args: "" };
|
|
2733
|
+
if (typeof tc.id === "string") slot.id = tc.id;
|
|
2734
|
+
if (fn?.name) slot.name = fn.name;
|
|
2735
|
+
toolSlots.push(slot);
|
|
2736
|
+
lastSlotByIndex.set(idx, slot);
|
|
2737
|
+
} else {
|
|
2738
|
+
slot = lastSlotByIndex.get(idx) ?? { args: "" };
|
|
2739
|
+
if (!lastSlotByIndex.has(idx)) {
|
|
2740
|
+
toolSlots.push(slot);
|
|
2741
|
+
lastSlotByIndex.set(idx, slot);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
if (typeof fn?.arguments === "string") slot.args += fn.arguments;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
if (typeof choice.finish_reason === "string") finishReason = choice.finish_reason;
|
|
2748
|
+
if (typeof choice.reasoning_opaque === "string") {
|
|
2749
|
+
reasoningOpaque = choice.reasoning_opaque;
|
|
2750
|
+
}
|
|
2751
|
+
const msg = choice.message;
|
|
2752
|
+
if (typeof msg?.reasoning_opaque === "string") reasoningOpaque = msg.reasoning_opaque;
|
|
2753
|
+
}
|
|
2754
|
+
const usage2 = evt.usage;
|
|
2755
|
+
if (usage2) {
|
|
2756
|
+
promptTokens = usage2.prompt_tokens;
|
|
2757
|
+
completionTokens = usage2.completion_tokens;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
const toolCalls = toolSlots.filter((slot) => slot.id && slot.name).map((slot) => ({ id: slot.id, name: slot.name, input: safeJsonParse(slot.args) }));
|
|
2694
2762
|
return {
|
|
2695
|
-
text
|
|
2763
|
+
text,
|
|
2696
2764
|
toolCalls,
|
|
2697
|
-
stopReason: toolCalls.length > 0 ? "tool_use" :
|
|
2698
|
-
usage: this.usage(
|
|
2765
|
+
stopReason: toolCalls.length > 0 ? "tool_use" : finishReason === "length" ? "max_tokens" : "end",
|
|
2766
|
+
usage: this.usage(promptTokens, completionTokens),
|
|
2767
|
+
reasoningOpaque
|
|
2699
2768
|
};
|
|
2700
2769
|
}
|
|
2701
2770
|
buildResponsesInput(options) {
|
|
@@ -3963,13 +4032,13 @@ function selectToolsForRequest(params) {
|
|
|
3963
4032
|
addByName(name);
|
|
3964
4033
|
}
|
|
3965
4034
|
const relevantCategories = categoriesForMessage(userMessage);
|
|
3966
|
-
|
|
3967
|
-
if (selected.size >= maxTools) break;
|
|
4035
|
+
const relevantTools = allTools.map((tool, index) => ({ index, score: toolRelevanceScore(tool.name, userMessage), tool })).filter(({ tool }) => {
|
|
3968
4036
|
const cat = categoryForTool(tool.name);
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
4037
|
+
return cat !== "mcp" && relevantCategories.has(cat) && !selected.has(tool.name);
|
|
4038
|
+
}).sort((a, b) => b.score - a.score || a.index - b.index);
|
|
4039
|
+
for (const { tool } of relevantTools) {
|
|
4040
|
+
if (selected.size >= maxTools) break;
|
|
4041
|
+
selected.set(tool.name, tool);
|
|
3973
4042
|
}
|
|
3974
4043
|
if (selected.size < maxTools && relevantCategories.size === 0) {
|
|
3975
4044
|
for (const tool of allTools) {
|
|
@@ -4000,6 +4069,20 @@ function categoriesForMessage(message) {
|
|
|
4000
4069
|
}
|
|
4001
4070
|
return categories;
|
|
4002
4071
|
}
|
|
4072
|
+
function preloadRelevantCategoriesForRequest(loader, message) {
|
|
4073
|
+
if (typeof loader.loadCategory !== "function") return [];
|
|
4074
|
+
const loaded = [];
|
|
4075
|
+
for (const category of categoriesForMessage(message)) {
|
|
4076
|
+
if (category === "mcp" || category === "meta" || category === "skill") continue;
|
|
4077
|
+
const added = loader.loadCategory(category);
|
|
4078
|
+
if (added > 0) loaded.push(category);
|
|
4079
|
+
}
|
|
4080
|
+
return loaded;
|
|
4081
|
+
}
|
|
4082
|
+
function toolOutputLooksFailed(output) {
|
|
4083
|
+
if (!output) return false;
|
|
4084
|
+
return /^\s*\{?"?(error|success)"?\s*:\s*("|false)/i.test(output) || /\b(error|not found|enoent|failed)\b/i.test(output.slice(0, 300));
|
|
4085
|
+
}
|
|
4003
4086
|
function normalizeCategory(value) {
|
|
4004
4087
|
const normalized = value.toLowerCase();
|
|
4005
4088
|
if (["browser", "email", "teams", "office", "wechat", "memory", "files", "system"].includes(normalized)) {
|
|
@@ -4020,6 +4103,20 @@ function categoryForTool(toolName) {
|
|
|
4020
4103
|
if (toolName.startsWith("system_") || toolName.startsWith("clipboard_") || ["code_execute", "web_fetch", "web_search", "web_extract", "notify", "sleep", "ask_user", "config"].includes(toolName)) return "system";
|
|
4021
4104
|
return "mcp";
|
|
4022
4105
|
}
|
|
4106
|
+
function toolRelevanceScore(toolName, message) {
|
|
4107
|
+
const lower = message.toLowerCase();
|
|
4108
|
+
const name = toolName.toLowerCase();
|
|
4109
|
+
if (/https?:\/\//i.test(message) && ["web_extract", "web_fetch", "web_search"].includes(name)) return 150;
|
|
4110
|
+
if (/(?:^|[\s"'`])(?:~|\.\.?|[A-Za-z]:)?[\\/][^\s"'`]+|\b[\w.-]+\.(html?|md|txt|json|ya?ml|ts|tsx|js|jsx|py|csv|xlsx?|pptx?|pdf)\b/i.test(message) && ["file_read", "file_info", "file_list"].includes(name)) return 145;
|
|
4111
|
+
if (/\b(repo|repository|source code|github|codebase)\b/.test(lower) && ["system_run", "code_execute", "grep", "glob", "web_extract", "web_fetch", "file_read"].includes(name)) return 140;
|
|
4112
|
+
if (/https?:\/\//i.test(message) && ["browser_navigate", "browser_extract", "browser_snapshot"].includes(name)) return 135;
|
|
4113
|
+
if (/\b(powerpoint|pptx?|presentation|slides?|deck)\b/.test(lower) && ["powerpoint_focus", "powerpoint_new_presentation", "powerpoint_new_slide", "powerpoint_read_content", "powerpoint_send_keys", "powerpoint_screenshot"].includes(name)) return 130;
|
|
4114
|
+
if (/\b(powerpoint|pptx?|presentation|slides?|deck)\b/.test(lower) && name.startsWith("powerpoint_")) return 85;
|
|
4115
|
+
if (/\b(excel|spreadsheet|xlsx?|csv|data analysis)\b/.test(lower) && ["excel_focus", "excel_new_workbook", "excel_read_content", "excel_enter_value", "excel_enter_formula"].includes(name)) return 95;
|
|
4116
|
+
if (/\b(word|docx?|document|report)\b/.test(lower) && ["word_focus", "word_new_document", "word_read_content", "word_insert_text", "word_save"].includes(name)) return 95;
|
|
4117
|
+
if (name === "openjaw_load_tools" || name === "invoke_skill") return 90;
|
|
4118
|
+
return 0;
|
|
4119
|
+
}
|
|
4023
4120
|
var DEFAULT_OPENAI_MAX_TOOLS, MCP_AUTO_GROW_HARD_CAP, BUILTIN_HEADROOM, FOUNDATION_TOOL_NAMES, PROFILE_CATEGORIES, CATEGORY_KEYWORDS;
|
|
4024
4121
|
var init_tool_exposure = __esm({
|
|
4025
4122
|
"src/tool-exposure.ts"() {
|
|
@@ -4043,10 +4140,10 @@ var init_tool_exposure = __esm({
|
|
|
4043
4140
|
CATEGORY_KEYWORDS = [
|
|
4044
4141
|
{ category: "email", patterns: [/\b(email|mail|outlook|inbox|calendar|schedule|meeting|invite|today|tomorrow)\b/i] },
|
|
4045
4142
|
{ category: "teams", patterns: [/\b(teams|chat|channel|message|dm|meeting|standup|today|mention)\b/i] },
|
|
4046
|
-
{ category: "browser", patterns: [/\b(browser|page|website|web|navigate|click|screenshot|snapshot|console|image|search online)\b/i] },
|
|
4047
|
-
{ category: "files", patterns: [/\b(file|folder|directory|read|write|edit|grep|glob|find in repo|codebase)\b/i] },
|
|
4143
|
+
{ category: "browser", patterns: [/\b(browser|page|website|web|navigate|click|screenshot|snapshot|console|image|search online)\b/i, /https?:\/\//i] },
|
|
4144
|
+
{ category: "files", patterns: [/\b(file|folder|directory|read|write|edit|grep|glob|find in repo|codebase|source code|repo|repository|downloads?)\b/i, /(?:^|[\s"'`])(?:~|\.\.?|[A-Za-z]:)?[\\/][^\s"'`]+/i, /\b[\w.-]+\.(html?|md|txt|json|ya?ml|ts|tsx|js|jsx|py|csv|xlsx?|pptx?|pdf)\b/i] },
|
|
4048
4145
|
{ category: "system", patterns: [/\b(shell|command|terminal|run|execute|clipboard|notify|sleep|web search|fetch url|extract url|read url|article|docs?|paper|source page|news|latest|headlines|current events|breaking news)\b/i] },
|
|
4049
|
-
{ category: "office", patterns: [/\b(word|excel|powerpoint|spreadsheet|document|presentation|slide)\b/i] },
|
|
4146
|
+
{ category: "office", patterns: [/\b(word|excel|powerpoint|pptx?|spreadsheet|document|presentation|slide|deck)\b/i] },
|
|
4050
4147
|
{ category: "wechat", patterns: [/\b(wechat|weixin)\b/i] },
|
|
4051
4148
|
{ category: "memory", patterns: [/\b(memory|remember|recall|todo|preference)\b/i] }
|
|
4052
4149
|
];
|
|
@@ -4058,8 +4155,245 @@ var init_tool_exposure = __esm({
|
|
|
4058
4155
|
__name(rememberLoadedToolExposure, "rememberLoadedToolExposure");
|
|
4059
4156
|
__name(selectToolsForRequest, "selectToolsForRequest");
|
|
4060
4157
|
__name(categoriesForMessage, "categoriesForMessage");
|
|
4158
|
+
__name(preloadRelevantCategoriesForRequest, "preloadRelevantCategoriesForRequest");
|
|
4159
|
+
__name(toolOutputLooksFailed, "toolOutputLooksFailed");
|
|
4061
4160
|
__name(normalizeCategory, "normalizeCategory");
|
|
4062
4161
|
__name(categoryForTool, "categoryForTool");
|
|
4162
|
+
__name(toolRelevanceScore, "toolRelevanceScore");
|
|
4163
|
+
}
|
|
4164
|
+
});
|
|
4165
|
+
|
|
4166
|
+
// src/turn-control.ts
|
|
4167
|
+
var DEFAULT_MAX_TOOL_ROUNDS, IterationBudget;
|
|
4168
|
+
var init_turn_control = __esm({
|
|
4169
|
+
"src/turn-control.ts"() {
|
|
4170
|
+
"use strict";
|
|
4171
|
+
DEFAULT_MAX_TOOL_ROUNDS = 100;
|
|
4172
|
+
IterationBudget = class {
|
|
4173
|
+
static {
|
|
4174
|
+
__name(this, "IterationBudget");
|
|
4175
|
+
}
|
|
4176
|
+
used = 0;
|
|
4177
|
+
graceUsed = false;
|
|
4178
|
+
max;
|
|
4179
|
+
constructor(max = DEFAULT_MAX_TOOL_ROUNDS) {
|
|
4180
|
+
this.max = Math.max(1, Math.floor(max));
|
|
4181
|
+
}
|
|
4182
|
+
get remaining() {
|
|
4183
|
+
return Math.max(0, this.max - this.used);
|
|
4184
|
+
}
|
|
4185
|
+
get consumed() {
|
|
4186
|
+
return this.used;
|
|
4187
|
+
}
|
|
4188
|
+
/** True while there is budget (or the one-time grace round) left to run. */
|
|
4189
|
+
canContinue() {
|
|
4190
|
+
return this.remaining > 0 || !this.graceUsed;
|
|
4191
|
+
}
|
|
4192
|
+
/** True only when the budget is spent and we are on the grace round. */
|
|
4193
|
+
isGraceRound() {
|
|
4194
|
+
return this.remaining === 0 && !this.graceUsed;
|
|
4195
|
+
}
|
|
4196
|
+
/** Consume one round. Returns false if nothing (not even grace) is left. */
|
|
4197
|
+
consume() {
|
|
4198
|
+
if (this.remaining > 0) {
|
|
4199
|
+
this.used += 1;
|
|
4200
|
+
return true;
|
|
4201
|
+
}
|
|
4202
|
+
if (!this.graceUsed) {
|
|
4203
|
+
this.graceUsed = true;
|
|
4204
|
+
return true;
|
|
4205
|
+
}
|
|
4206
|
+
return false;
|
|
4207
|
+
}
|
|
4208
|
+
/** Give a round back (e.g. a round that made no model-visible progress). */
|
|
4209
|
+
refund() {
|
|
4210
|
+
if (this.used > 0) this.used -= 1;
|
|
4211
|
+
}
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
});
|
|
4215
|
+
|
|
4216
|
+
// src/tool-guardrails.ts
|
|
4217
|
+
function isNoProgressTracked(toolName) {
|
|
4218
|
+
if (MUTATING_TOOLS.has(toolName)) return false;
|
|
4219
|
+
if (toolName.startsWith("powerpoint_") || toolName.startsWith("word_") || toolName.startsWith("excel_")) {
|
|
4220
|
+
return false;
|
|
4221
|
+
}
|
|
4222
|
+
return true;
|
|
4223
|
+
}
|
|
4224
|
+
function signatureOf(toolName, args) {
|
|
4225
|
+
return `${toolName}\0${canonicalJson(args ?? {})}`;
|
|
4226
|
+
}
|
|
4227
|
+
function canonicalJson(value) {
|
|
4228
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
4229
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
4230
|
+
const obj = value;
|
|
4231
|
+
const keys = Object.keys(obj).sort();
|
|
4232
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
|
|
4233
|
+
}
|
|
4234
|
+
function hashOutput(output) {
|
|
4235
|
+
let h = 2166136261;
|
|
4236
|
+
const s = output ?? "";
|
|
4237
|
+
for (let i = 0; i < s.length; i++) {
|
|
4238
|
+
h ^= s.charCodeAt(i);
|
|
4239
|
+
h = Math.imul(h, 16777619);
|
|
4240
|
+
}
|
|
4241
|
+
return (h >>> 0).toString(16);
|
|
4242
|
+
}
|
|
4243
|
+
function appendGuardrailGuidance(result, decision) {
|
|
4244
|
+
if (decision.action !== "warn" && decision.action !== "halt" || !decision.message) return result;
|
|
4245
|
+
const label = decision.action === "halt" ? "Tool loop hard stop" : "Tool loop warning";
|
|
4246
|
+
return `${result || ""}
|
|
4247
|
+
|
|
4248
|
+
[${label}: ${decision.code}; count=${decision.count}; ${decision.message}]`;
|
|
4249
|
+
}
|
|
4250
|
+
function blockedToolResult(decision) {
|
|
4251
|
+
return JSON.stringify({ error: decision.message, guardrail: { code: decision.code, action: decision.action } });
|
|
4252
|
+
}
|
|
4253
|
+
var DEFAULT_GUARDRAIL_THRESHOLDS, MUTATING_TOOLS, ToolLoopGuardrails;
|
|
4254
|
+
var init_tool_guardrails = __esm({
|
|
4255
|
+
"src/tool-guardrails.ts"() {
|
|
4256
|
+
"use strict";
|
|
4257
|
+
DEFAULT_GUARDRAIL_THRESHOLDS = {
|
|
4258
|
+
hardStopEnabled: true,
|
|
4259
|
+
exactFailureWarnAfter: 2,
|
|
4260
|
+
exactFailureBlockAfter: 5,
|
|
4261
|
+
sameToolFailureWarnAfter: 3,
|
|
4262
|
+
sameToolFailureHaltAfter: 8,
|
|
4263
|
+
noProgressWarnAfter: 2,
|
|
4264
|
+
noProgressBlockAfter: 5
|
|
4265
|
+
};
|
|
4266
|
+
MUTATING_TOOLS = /* @__PURE__ */ new Set([
|
|
4267
|
+
"file_write",
|
|
4268
|
+
"file_edit",
|
|
4269
|
+
"file_delete",
|
|
4270
|
+
"notify",
|
|
4271
|
+
"ask_user",
|
|
4272
|
+
"memory_append",
|
|
4273
|
+
"memory_save"
|
|
4274
|
+
]);
|
|
4275
|
+
__name(isNoProgressTracked, "isNoProgressTracked");
|
|
4276
|
+
__name(signatureOf, "signatureOf");
|
|
4277
|
+
__name(canonicalJson, "canonicalJson");
|
|
4278
|
+
__name(hashOutput, "hashOutput");
|
|
4279
|
+
ToolLoopGuardrails = class {
|
|
4280
|
+
static {
|
|
4281
|
+
__name(this, "ToolLoopGuardrails");
|
|
4282
|
+
}
|
|
4283
|
+
t;
|
|
4284
|
+
exactFailure = /* @__PURE__ */ new Map();
|
|
4285
|
+
sameToolFailure = /* @__PURE__ */ new Map();
|
|
4286
|
+
noProgress = /* @__PURE__ */ new Map();
|
|
4287
|
+
haltDecision = null;
|
|
4288
|
+
constructor(thresholds = {}) {
|
|
4289
|
+
this.t = { ...DEFAULT_GUARDRAIL_THRESHOLDS, ...thresholds };
|
|
4290
|
+
}
|
|
4291
|
+
/** Set once a block/halt has fired; the loop reads this to stop the turn. */
|
|
4292
|
+
get halted() {
|
|
4293
|
+
return this.haltDecision;
|
|
4294
|
+
}
|
|
4295
|
+
/**
|
|
4296
|
+
* Consult before (re-)executing a tool call. Returns `block` if this exact
|
|
4297
|
+
* call has already failed/no-progressed past the hard-stop threshold, so the
|
|
4298
|
+
* loop can skip execution and tell the model to change strategy.
|
|
4299
|
+
*/
|
|
4300
|
+
before(toolName, args) {
|
|
4301
|
+
if (!this.t.hardStopEnabled) return { action: "allow", toolName };
|
|
4302
|
+
const sig = signatureOf(toolName, args);
|
|
4303
|
+
const exact = this.exactFailure.get(sig) ?? 0;
|
|
4304
|
+
if (exact >= this.t.exactFailureBlockAfter) {
|
|
4305
|
+
return this.recordHalt({
|
|
4306
|
+
action: "block",
|
|
4307
|
+
code: "repeated_exact_failure_block",
|
|
4308
|
+
message: `Blocked ${toolName}: the same tool call failed ${exact} times with identical arguments. Stop retrying it unchanged; change strategy or explain the blocker.`,
|
|
4309
|
+
toolName,
|
|
4310
|
+
count: exact
|
|
4311
|
+
});
|
|
4312
|
+
}
|
|
4313
|
+
if (isNoProgressTracked(toolName)) {
|
|
4314
|
+
const rec = this.noProgress.get(sig);
|
|
4315
|
+
if (rec && rec.count >= this.t.noProgressBlockAfter) {
|
|
4316
|
+
return this.recordHalt({
|
|
4317
|
+
action: "block",
|
|
4318
|
+
code: "idempotent_no_progress_block",
|
|
4319
|
+
message: `Blocked ${toolName}: this call returned the same result ${rec.count} times. Stop repeating it unchanged; use the result already provided or try a different approach.`,
|
|
4320
|
+
toolName,
|
|
4321
|
+
count: rec.count
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
return { action: "allow", toolName };
|
|
4326
|
+
}
|
|
4327
|
+
/**
|
|
4328
|
+
* Record the outcome after a tool call. Returns a `warn` to append to the
|
|
4329
|
+
* tool result, or a `halt` to stop the turn. `failed` is derived by the
|
|
4330
|
+
* caller from the output shape (reuse tool-exposure's toolOutputLooksFailed).
|
|
4331
|
+
*/
|
|
4332
|
+
after(toolName, args, output, failed) {
|
|
4333
|
+
const sig = signatureOf(toolName, args);
|
|
4334
|
+
if (failed) {
|
|
4335
|
+
const exact = (this.exactFailure.get(sig) ?? 0) + 1;
|
|
4336
|
+
this.exactFailure.set(sig, exact);
|
|
4337
|
+
this.noProgress.delete(sig);
|
|
4338
|
+
const same = (this.sameToolFailure.get(toolName) ?? 0) + 1;
|
|
4339
|
+
this.sameToolFailure.set(toolName, same);
|
|
4340
|
+
if (this.t.hardStopEnabled && same >= this.t.sameToolFailureHaltAfter) {
|
|
4341
|
+
return this.recordHalt({
|
|
4342
|
+
action: "halt",
|
|
4343
|
+
code: "same_tool_failure_halt",
|
|
4344
|
+
message: `Stopped ${toolName}: it failed ${same} times this turn. Stop retrying the same failing tool path and choose a different approach.`,
|
|
4345
|
+
toolName,
|
|
4346
|
+
count: same
|
|
4347
|
+
});
|
|
4348
|
+
}
|
|
4349
|
+
if (exact >= this.t.exactFailureWarnAfter) {
|
|
4350
|
+
return {
|
|
4351
|
+
action: "warn",
|
|
4352
|
+
code: "repeated_exact_failure_warning",
|
|
4353
|
+
message: `${toolName} has failed ${exact} times with identical arguments. This looks like a loop; inspect the error and change strategy instead of retrying it unchanged.`,
|
|
4354
|
+
toolName,
|
|
4355
|
+
count: exact
|
|
4356
|
+
};
|
|
4357
|
+
}
|
|
4358
|
+
if (same >= this.t.sameToolFailureWarnAfter) {
|
|
4359
|
+
return {
|
|
4360
|
+
action: "warn",
|
|
4361
|
+
code: "same_tool_failure_warning",
|
|
4362
|
+
message: `${toolName} has failed ${same} times this turn. This looks like a loop. Do not switch to text-only replies; keep using tools, but inspect the latest error and verify your assumptions before retrying.`,
|
|
4363
|
+
toolName,
|
|
4364
|
+
count: same
|
|
4365
|
+
};
|
|
4366
|
+
}
|
|
4367
|
+
return { action: "allow", toolName, count: exact };
|
|
4368
|
+
}
|
|
4369
|
+
this.exactFailure.delete(sig);
|
|
4370
|
+
this.sameToolFailure.delete(toolName);
|
|
4371
|
+
if (!isNoProgressTracked(toolName)) {
|
|
4372
|
+
this.noProgress.delete(sig);
|
|
4373
|
+
return { action: "allow", toolName };
|
|
4374
|
+
}
|
|
4375
|
+
const hash3 = hashOutput(output);
|
|
4376
|
+
const prev = this.noProgress.get(sig);
|
|
4377
|
+
const count = prev && prev.hash === hash3 ? prev.count + 1 : 1;
|
|
4378
|
+
this.noProgress.set(sig, { hash: hash3, count });
|
|
4379
|
+
if (count >= this.t.noProgressWarnAfter) {
|
|
4380
|
+
return {
|
|
4381
|
+
action: "warn",
|
|
4382
|
+
code: "idempotent_no_progress_warning",
|
|
4383
|
+
message: `${toolName} returned the same result ${count} times. Use the result already provided or change the approach instead of repeating it unchanged.`,
|
|
4384
|
+
toolName,
|
|
4385
|
+
count
|
|
4386
|
+
};
|
|
4387
|
+
}
|
|
4388
|
+
return { action: "allow", toolName, count };
|
|
4389
|
+
}
|
|
4390
|
+
recordHalt(decision) {
|
|
4391
|
+
this.haltDecision = decision;
|
|
4392
|
+
return decision;
|
|
4393
|
+
}
|
|
4394
|
+
};
|
|
4395
|
+
__name(appendGuardrailGuidance, "appendGuardrailGuidance");
|
|
4396
|
+
__name(blockedToolResult, "blockedToolResult");
|
|
4063
4397
|
}
|
|
4064
4398
|
});
|
|
4065
4399
|
|
|
@@ -4612,6 +4946,8 @@ var init_agent_loop = __esm({
|
|
|
4612
4946
|
init_telemetry();
|
|
4613
4947
|
init_context_compressor();
|
|
4614
4948
|
init_tool_exposure();
|
|
4949
|
+
init_turn_control();
|
|
4950
|
+
init_tool_guardrails();
|
|
4615
4951
|
init_provider_auth();
|
|
4616
4952
|
init_connect();
|
|
4617
4953
|
AgentLoop = class {
|
|
@@ -5132,15 +5468,25 @@ ${summary}
|
|
|
5132
5468
|
const MAX_SAME_ACTIONS = 3;
|
|
5133
5469
|
let previousCacheReadTokens = 0;
|
|
5134
5470
|
let maxTokensContinuations = 0;
|
|
5471
|
+
const budget = new IterationBudget(this.config.llm.max_tool_rounds ?? DEFAULT_MAX_TOOL_ROUNDS);
|
|
5472
|
+
const guardrails = new ToolLoopGuardrails();
|
|
5135
5473
|
for (let step = 0; ; step++) {
|
|
5136
5474
|
if (signal.aborted) {
|
|
5137
5475
|
yield { type: "answer", content: "[Interrupted by user]" };
|
|
5138
5476
|
return;
|
|
5139
5477
|
}
|
|
5478
|
+
const forceFinalSummary = budget.isGraceRound();
|
|
5479
|
+
if (!budget.consume()) {
|
|
5480
|
+
const reason = "max_iterations";
|
|
5481
|
+
yield { type: "answer", content: `[Reached the maximum of ${budget.max} tool rounds. Stopping.]`, exitReason: reason };
|
|
5482
|
+
return;
|
|
5483
|
+
}
|
|
5140
5484
|
let responseText = null;
|
|
5141
5485
|
const responseToolCalls = [];
|
|
5142
5486
|
let responseStopReason = "end";
|
|
5487
|
+
let responseReasoningOpaque;
|
|
5143
5488
|
let responseUsage;
|
|
5489
|
+
preloadRelevantCategoriesForRequest(this.toolRegistry, userMessage);
|
|
5144
5490
|
const allTools = this.toolRegistry.listTools();
|
|
5145
5491
|
const exposure = selectToolsForRequest({
|
|
5146
5492
|
config: this.config,
|
|
@@ -5148,7 +5494,13 @@ ${summary}
|
|
|
5148
5494
|
userMessage,
|
|
5149
5495
|
state: this._toolExposureState
|
|
5150
5496
|
});
|
|
5151
|
-
const tools = exposure.tools;
|
|
5497
|
+
const tools = forceFinalSummary ? [] : exposure.tools;
|
|
5498
|
+
if (forceFinalSummary) {
|
|
5499
|
+
messages.push({
|
|
5500
|
+
role: "user",
|
|
5501
|
+
content: "[System: You've reached the maximum number of tool-calling rounds. Do not call any more tools. Summarize what you accomplished, what is still incomplete, and any blocker, as your final answer.]"
|
|
5502
|
+
});
|
|
5503
|
+
}
|
|
5152
5504
|
const chatOptions = {
|
|
5153
5505
|
systemPrompt,
|
|
5154
5506
|
messages,
|
|
@@ -5231,6 +5583,7 @@ ${summary}
|
|
|
5231
5583
|
responseToolCalls.push(...response.toolCalls);
|
|
5232
5584
|
responseStopReason = response.stopReason;
|
|
5233
5585
|
responseUsage = response.usage;
|
|
5586
|
+
responseReasoningOpaque = response.reasoningOpaque;
|
|
5234
5587
|
}
|
|
5235
5588
|
} catch (apiError) {
|
|
5236
5589
|
const errMsg = apiError instanceof Error ? apiError.message : String(apiError);
|
|
@@ -5357,10 +5710,11 @@ ${summary}
|
|
|
5357
5710
|
this.conversationHistory.push({ role: "assistant", content: responseText });
|
|
5358
5711
|
this.session.messages = this.conversationHistory;
|
|
5359
5712
|
saveSession(this.session);
|
|
5713
|
+
const exitReason = forceFinalSummary ? "max_iterations" : "completed";
|
|
5360
5714
|
if (!this.provider.chatStream) {
|
|
5361
|
-
yield { type: "answer", content: responseText ?? "" };
|
|
5715
|
+
yield { type: "answer", content: responseText ?? "", exitReason };
|
|
5362
5716
|
} else {
|
|
5363
|
-
yield { type: "answer", content: "" };
|
|
5717
|
+
yield { type: "answer", content: "", exitReason };
|
|
5364
5718
|
}
|
|
5365
5719
|
if (this._toolRoundsInRun >= 1 || responseText && responseText.length > 200) {
|
|
5366
5720
|
void this.postTurnMemorySave(systemPrompt, messages, tools, signal);
|
|
@@ -5392,7 +5746,8 @@ ${summary}
|
|
|
5392
5746
|
messages.push({
|
|
5393
5747
|
role: "assistant",
|
|
5394
5748
|
content: responseText,
|
|
5395
|
-
toolCalls: validToolCalls
|
|
5749
|
+
toolCalls: validToolCalls,
|
|
5750
|
+
reasoningOpaque: responseReasoningOpaque
|
|
5396
5751
|
});
|
|
5397
5752
|
const computerCalls = validToolCalls.filter((tc) => tc.name === "computer");
|
|
5398
5753
|
const otherCalls = validToolCalls.filter((tc) => tc.name !== "computer");
|
|
@@ -5400,6 +5755,10 @@ ${summary}
|
|
|
5400
5755
|
if (signal.aborted) {
|
|
5401
5756
|
return { id: tc.id, name: tc.name, output: "[Interrupted]", imageData: void 0 };
|
|
5402
5757
|
}
|
|
5758
|
+
const pre = guardrails.before(tc.name, tc.input);
|
|
5759
|
+
if (pre.action === "block") {
|
|
5760
|
+
return { id: tc.id, name: tc.name, output: blockedToolResult(pre), imageData: void 0 };
|
|
5761
|
+
}
|
|
5403
5762
|
let output;
|
|
5404
5763
|
let imageData2;
|
|
5405
5764
|
try {
|
|
@@ -5456,7 +5815,12 @@ ${summary}
|
|
|
5456
5815
|
}
|
|
5457
5816
|
} catch {
|
|
5458
5817
|
}
|
|
5459
|
-
|
|
5818
|
+
let outputStr = typeof output === "string" ? output : JSON.stringify(output ?? { error: "No output" });
|
|
5819
|
+
const failed = toolOutputLooksFailed(outputStr);
|
|
5820
|
+
const verdict = guardrails.after(tc.name, tc.input, outputStr, failed);
|
|
5821
|
+
if (verdict.action === "warn" || verdict.action === "halt") {
|
|
5822
|
+
outputStr = appendGuardrailGuidance(outputStr, verdict);
|
|
5823
|
+
}
|
|
5460
5824
|
return { id: tc.id, name: tc.name, output: outputStr, imageData: imageData2 };
|
|
5461
5825
|
}, "executeOne");
|
|
5462
5826
|
const results = [];
|
|
@@ -5516,6 +5880,15 @@ ${summary}
|
|
|
5516
5880
|
this.conversationHistory = [...messages];
|
|
5517
5881
|
this.session.messages = this.conversationHistory;
|
|
5518
5882
|
saveSession(this.session);
|
|
5883
|
+
if (guardrails.halted) {
|
|
5884
|
+
const reason = "guardrail_halt";
|
|
5885
|
+
yield {
|
|
5886
|
+
type: "answer",
|
|
5887
|
+
content: `[Stopped: ${guardrails.halted.message ?? "tool loop detected"}]`,
|
|
5888
|
+
exitReason: reason
|
|
5889
|
+
};
|
|
5890
|
+
return;
|
|
5891
|
+
}
|
|
5519
5892
|
if (this._toolRoundsInRun > 0 && this._toolRoundsInRun % 5 === 0) {
|
|
5520
5893
|
messages.push({
|
|
5521
5894
|
role: "user",
|
|
@@ -46378,7 +46751,7 @@ var init_overlayStore = __esm({
|
|
|
46378
46751
|
$overlayState = atom2(buildOverlayState());
|
|
46379
46752
|
$isBlocked = computed2(
|
|
46380
46753
|
$overlayState,
|
|
46381
|
-
({ agents, approval, clarify, confirm, mcpHub,
|
|
46754
|
+
({ agents, approval, clarify, confirm, mcpHub, pager, picker, secret, skillsHub, sudo }) => Boolean(agents || approval || clarify || confirm || mcpHub || pager || picker || secret || skillsHub || sudo)
|
|
46382
46755
|
);
|
|
46383
46756
|
patchOverlayState = /* @__PURE__ */ __name((next) => $overlayState.set(typeof next === "function" ? next($overlayState.get()) : { ...$overlayState.get(), ...next }), "patchOverlayState");
|
|
46384
46757
|
resetFlowOverlays = /* @__PURE__ */ __name(() => $overlayState.set({
|