@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 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
- const data = await res.json();
2687
- const choice = data.choices?.[0];
2688
- if (!choice?.message) throw new Error("No response from GitHub Copilot");
2689
- const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
2690
- id: tc.id,
2691
- name: tc.function.name,
2692
- input: safeJsonParse(tc.function.arguments)
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: choice.message.content,
2763
+ text,
2696
2764
  toolCalls,
2697
- stopReason: toolCalls.length > 0 ? "tool_use" : choice.finish_reason === "length" ? "max_tokens" : "end",
2698
- usage: this.usage(data.usage?.prompt_tokens, data.usage?.completion_tokens)
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
- for (const tool of allTools) {
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
- if (cat === "mcp") continue;
3970
- if (relevantCategories.has(cat)) {
3971
- selected.set(tool.name, tool);
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
- const outputStr = typeof output === "string" ? output : JSON.stringify(output ?? { error: "No output" });
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, modelPicker, pager, picker, secret, skillsHub, sudo }) => Boolean(agents || approval || clarify || confirm || mcpHub || modelPicker || pager || picker || secret || skillsHub || sudo)
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({