github-router 0.3.211 → 0.3.219

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.
@@ -23,12 +23,13 @@ import { Worker } from "node:worker_threads";
23
23
  import { gunzipSync, inflateRawSync } from "node:zlib";
24
24
  import WebSocket from "ws";
25
25
  import { events } from "fetch-event-stream";
26
- import { Type } from "typebox";
26
+ import { Type, Type as Type$1 } from "typebox";
27
27
  import "partial-json";
28
28
  import { Compile } from "typebox/compile";
29
29
  import { Value } from "typebox/value";
30
30
  import "yaml";
31
31
  import "ignore";
32
+ import "diff";
32
33
 
33
34
  //#region rolldown:runtime
34
35
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -637,9 +638,9 @@ function resolveModel(modelId) {
637
638
  if (oneMMatch) {
638
639
  const stripped = oneMMatch[1];
639
640
  const resolved = resolveModel(stripped);
640
- if (!/-1m(?:$|-)/.test(resolved) && !warnedOneMDowngrade.has(modelId)) {
641
+ if (!(/-1m(?:$|-)/.test(resolved) || (models.find((m) => m.id === resolved)?.capabilities?.limits?.max_context_window_tokens ?? 0) >= 1e6) && !warnedOneMDowngrade.has(modelId)) {
641
642
  warnedOneMDowngrade.add(modelId);
642
- consola.warn(`Model "${modelId}" requested 1M context but no -1m backend is in Copilot's catalog for this tier/family; downgrading upstream to "${resolved}" (200K). Claude Code's local context accounting will still assume 1M — expect premature auto-compact. Drop the [1m] suffix (or unset CLAUDE_CODE_DISABLE_1M_CONTEXT if you set it) to silence.`);
643
+ consola.warn(`Model "${modelId}" requested 1M context but no 1M backend is in Copilot's catalog for this tier/family; downgrading upstream to "${resolved}" (200K). Claude Code's local context accounting will still assume 1M — expect premature auto-compact. Drop the [1m] suffix (or unset CLAUDE_CODE_DISABLE_1M_CONTEXT if you set it) to silence.`);
643
644
  }
644
645
  return resolved;
645
646
  }
@@ -649,8 +650,8 @@ function resolveModel(modelId) {
649
650
  if (ciMatch) return ciMatch.id;
650
651
  if (lower.includes("opus")) {
651
652
  const oneMs = models.filter((m) => m.id.includes("opus") && /-1m(?:$|-)/.test(m.id));
652
- const versionMatch = lower.match(/opus-(\d+)[.-](\d+)/);
653
- const requestedVersion = versionMatch ? `${versionMatch[1]}.${versionMatch[2]}` : void 0;
653
+ const versionMatch = lower.match(/opus-(\d+)(?:[.-](\d+))?/);
654
+ const requestedVersion = versionMatch ? versionMatch[2] ? `${versionMatch[1]}.${versionMatch[2]}` : versionMatch[1] : void 0;
654
655
  const oneM = (requestedVersion ? oneMs.find((m) => m.id.includes(`opus-${requestedVersion}-`)) : void 0) ?? (requestedVersion ? void 0 : oneMs[0]);
655
656
  if (oneM) return oneM.id;
656
657
  }
@@ -1005,15 +1006,15 @@ function getPackageVersion() {
1005
1006
  //#region src/lib/port.ts
1006
1007
  const DEFAULT_PORT = 8787;
1007
1008
  const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
1009
+ "claude-opus-4-8",
1008
1010
  "claude-opus-4-7",
1009
- "claude-opus-4-6",
1010
- "claude-opus-4-5"
1011
+ "claude-opus-4-6"
1011
1012
  ];
1012
1013
  /**
1013
1014
  * Cap-aware default picker for `ANTHROPIC_MODEL` on the implicit-default
1014
1015
  * path. Returns `claude-opus-${family}[1m]` when the live Copilot catalog
1015
1016
  * shows the family is 1M-capable, else the bare `claude-opus-${family}`
1016
- * slug. `family` defaults to `"4.8"` so the no-arg call selects the
1017
+ * slug. `family` defaults to `"5"` so the no-arg call selects the
1017
1018
  * current default; explicit values like `"4.7"` or `"4.6"` are used to
1018
1019
  * honor the `github-router claude -m <version>` family shorthand.
1019
1020
  *
@@ -1027,8 +1028,8 @@ const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
1027
1028
  * 2. **Base-slug capability signal** — the catalog entry whose id IS
1028
1029
  * the base `opus-${family}` slug advertises
1029
1030
  * `capabilities.limits.max_context_window_tokens >= 1_000_000`. This
1030
- * is how 4.8 ships — there is no `-1m` sibling; the single
1031
- * `claude-opus-4.8` id is the 1M variant.
1031
+ * is how 4.8 and 5 ship — there is no `-1m` sibling; the single
1032
+ * `claude-opus-4.8` / `claude-opus-5` id is itself the 1M variant.
1032
1033
  * Either signal flips on the `[1m]` decoration. Both signals together
1033
1034
  * also flip it on (no double-counting). The breadcrumb log names which
1034
1035
  * signal fired so users can spot catalog shape changes.
@@ -1058,7 +1059,7 @@ const DEFAULT_CLAUDE_MODEL_FALLBACKS = [
1058
1059
  * can't tell the difference between "no catalog yet" and "no 1M
1059
1060
  * variant" — defaulting safe-side preserves the pre-change behavior).
1060
1061
  */
1061
- const DEFAULT_OPUS_FAMILY = "4.8";
1062
+ const DEFAULT_OPUS_FAMILY = "5";
1062
1063
  const ONE_M_TOKENS = 1e6;
1063
1064
  function pickClaudeDefault(opusFamily = DEFAULT_OPUS_FAMILY) {
1064
1065
  const dotted = opusFamily.replace(/-/g, ".");
@@ -6187,19 +6188,19 @@ function resolveTierModel(tier$1) {
6187
6188
 
6188
6189
  //#endregion
6189
6190
  //#region src/lib/first-mate/classifier.ts
6190
- function isRecord$2(value) {
6191
+ function isRecord$1(value) {
6191
6192
  return typeof value === "object" && value !== null && !Array.isArray(value);
6192
6193
  }
6193
6194
  function firstMessageContent$1(value) {
6194
- if (!isRecord$2(value) || !Array.isArray(value.choices)) return null;
6195
+ if (!isRecord$1(value) || !Array.isArray(value.choices)) return null;
6195
6196
  const first = value.choices[0];
6196
- if (!isRecord$2(first) || !isRecord$2(first.message)) return null;
6197
+ if (!isRecord$1(first) || !isRecord$1(first.message)) return null;
6197
6198
  return typeof first.message.content === "string" ? first.message.content : null;
6198
6199
  }
6199
6200
  function parseJsonObject(value) {
6200
6201
  try {
6201
6202
  const parsed = JSON.parse(value);
6202
- return isRecord$2(parsed) ? parsed : null;
6203
+ return isRecord$1(parsed) ? parsed : null;
6203
6204
  } catch {
6204
6205
  return null;
6205
6206
  }
@@ -6269,7 +6270,7 @@ async function classifyPlanReady(logExcerpt) {
6269
6270
  schemaHint: "{\"planReady\":boolean,\"planExcerpt\":\"<=1200 chars from the completed plan, or empty\",\"confidence\":number}",
6270
6271
  maxTokens: 500,
6271
6272
  validate(value) {
6272
- if (!isRecord$2(value)) return null;
6273
+ if (!isRecord$1(value)) return null;
6273
6274
  const planReady = booleanValue$1(value.planReady);
6274
6275
  const planExcerpt = stringValue$1(value.planExcerpt);
6275
6276
  if (planReady === null || planExcerpt === null) return null;
@@ -6286,7 +6287,7 @@ async function classifyQuestionAnswerable(question, acceptanceCriteria) {
6286
6287
  user: `Acceptance criteria:\n${acceptanceCriteria}\n\nAgent question:\n${question}`,
6287
6288
  schemaHint: "{\"answerable\":boolean,\"answer\":\"present only when answerable\",\"confidence\":number}",
6288
6289
  validate(value) {
6289
- if (!isRecord$2(value)) return null;
6290
+ if (!isRecord$1(value)) return null;
6290
6291
  const answerable = booleanValue$1(value.answerable);
6291
6292
  if (answerable === null) return null;
6292
6293
  const answer = stringValue$1(value.answer);
@@ -6304,7 +6305,7 @@ async function classifyFixAddressed(failureSummary, latestLogExcerpt) {
6304
6305
  user: `Failure summary:\n${failureSummary}\n\nLatest log excerpt:\n${latestLogExcerpt}`,
6305
6306
  schemaHint: "{\"addressed\":boolean,\"confidence\":number}",
6306
6307
  validate(value) {
6307
- if (!isRecord$2(value)) return null;
6308
+ if (!isRecord$1(value)) return null;
6308
6309
  const addressed = booleanValue$1(value.addressed);
6309
6310
  return addressed === null ? null : { addressed };
6310
6311
  }
@@ -6316,7 +6317,7 @@ async function classifyStuck(logExcerpt) {
6316
6317
  user: `Log excerpt:\n${logExcerpt}`,
6317
6318
  schemaHint: "{\"stuck\":boolean,\"confidence\":number}",
6318
6319
  validate(value) {
6319
- if (!isRecord$2(value)) return null;
6320
+ if (!isRecord$1(value)) return null;
6320
6321
  const stuck = booleanValue$1(value.stuck);
6321
6322
  return stuck === null ? null : { stuck };
6322
6323
  }
@@ -12715,7 +12716,7 @@ function parsePackageJson(text) {
12715
12716
  if (text === void 0) return void 0;
12716
12717
  try {
12717
12718
  const parsed = JSON.parse(text);
12718
- if (!isRecord$1(parsed)) return void 0;
12719
+ if (!isRecord(parsed)) return void 0;
12719
12720
  return {
12720
12721
  scripts: stringRecord(parsed.scripts),
12721
12722
  dependencies: stringRecord(parsed.dependencies),
@@ -12727,9 +12728,9 @@ function parsePackageJson(text) {
12727
12728
  return;
12728
12729
  }
12729
12730
  }
12730
- function detectCommands(packageJson, overrides) {
12731
+ function detectCommands(packageJson, overrides$1) {
12731
12732
  const commands = {};
12732
- const pm = overrides?.package_manager ?? packageJson?.packageManager?.split("@")[0];
12733
+ const pm = overrides$1?.package_manager ?? packageJson?.packageManager?.split("@")[0];
12733
12734
  const runner = pm === "bun" || pm === "pnpm" || pm === "yarn" || pm === "npm" ? pm : "npm";
12734
12735
  const scripts = packageJson?.scripts ?? {};
12735
12736
  if (packageJson !== void 0) {
@@ -12742,11 +12743,11 @@ function detectCommands(packageJson, overrides) {
12742
12743
  "dev"
12743
12744
  ]) if (scripts[key] !== void 0) commands[key] = runScriptCommand(runner, key);
12744
12745
  }
12745
- if (overrides?.build_command !== void 0) commands.build = overrides.build_command;
12746
- if (overrides?.typecheck_command !== void 0) commands.typecheck = overrides.typecheck_command;
12747
- if (overrides?.lint_command !== void 0) commands.lint = overrides.lint_command;
12748
- if (overrides?.test_command !== void 0) commands.test = overrides.test_command;
12749
- if (overrides?.dev_command !== void 0) commands.dev = overrides.dev_command;
12746
+ if (overrides$1?.build_command !== void 0) commands.build = overrides$1.build_command;
12747
+ if (overrides$1?.typecheck_command !== void 0) commands.typecheck = overrides$1.typecheck_command;
12748
+ if (overrides$1?.lint_command !== void 0) commands.lint = overrides$1.lint_command;
12749
+ if (overrides$1?.test_command !== void 0) commands.test = overrides$1.test_command;
12750
+ if (overrides$1?.dev_command !== void 0) commands.dev = overrides$1.dev_command;
12750
12751
  return commands;
12751
12752
  }
12752
12753
  function detectPackageManager(rootNames, packageJsonText) {
@@ -12915,11 +12916,11 @@ function buildScaffoldPrBody(reports) {
12915
12916
  "No factory protocol files are seeded; orchestration remains external to the repository."
12916
12917
  ].join("\n");
12917
12918
  }
12918
- function isRecord$1(value) {
12919
+ function isRecord(value) {
12919
12920
  return typeof value === "object" && value !== null && !Array.isArray(value);
12920
12921
  }
12921
12922
  function stringRecord(value) {
12922
- if (!isRecord$1(value)) return {};
12923
+ if (!isRecord(value)) return {};
12923
12924
  const result = {};
12924
12925
  for (const [key, entry] of Object.entries(value)) if (typeof entry === "string") result[key] = entry;
12925
12926
  return result;
@@ -17432,7 +17433,7 @@ function hasSupportedBrowserInstalled() {
17432
17433
  * `src/lib/paths.ts`, which has no win32 branch). On Windows the
17433
17434
  * writer and reader never met, so the install-check returned
17434
17435
  * `bridge_not_running` even with a healthy bridge. Centralized here so
17435
- * the regression test in `tests/browser-bridge-discovery-path.test.ts`
17436
+ * the regression test in `tests/isolated/browser-bridge-discovery-path.test.ts`
17436
17437
  * can pin the round-trip.
17437
17438
  *
17438
17439
  * Mirrors `PATHS.APP_DIR` from `src/lib/paths.ts` (XDG-style on every
@@ -21340,58 +21341,6 @@ function registerExitHandlers$1() {
21340
21341
  }
21341
21342
  registerExitHandlers$1();
21342
21343
 
21343
- //#endregion
21344
- //#region src/vendor/pi/ai/api-registry.ts
21345
- const apiProviderRegistry = /* @__PURE__ */ new Map();
21346
- function getApiProvider(api) {
21347
- return apiProviderRegistry.get(api)?.provider;
21348
- }
21349
-
21350
- //#endregion
21351
- //#region src/vendor/pi/ai/env-api-keys.ts
21352
- let _existsSync = null;
21353
- let _homedir = null;
21354
- let _join = null;
21355
- const dynamicImport = (specifier) => import(specifier);
21356
- const NODE_FS_SPECIFIER = "node:fs";
21357
- const NODE_OS_SPECIFIER = "node:os";
21358
- const NODE_PATH_SPECIFIER = "node:path";
21359
- if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
21360
- dynamicImport(NODE_FS_SPECIFIER).then((m) => {
21361
- _existsSync = m.existsSync;
21362
- });
21363
- dynamicImport(NODE_OS_SPECIFIER).then((m) => {
21364
- _homedir = m.homedir;
21365
- });
21366
- dynamicImport(NODE_PATH_SPECIFIER).then((m) => {
21367
- _join = m.join;
21368
- });
21369
- }
21370
-
21371
- //#endregion
21372
- //#region src/vendor/pi/ai/models.generated.ts
21373
- const MODELS = {};
21374
-
21375
- //#endregion
21376
- //#region src/vendor/pi/ai/models.ts
21377
- const modelRegistry = /* @__PURE__ */ new Map();
21378
- for (const [provider, models] of Object.entries(MODELS)) {
21379
- const providerModels = /* @__PURE__ */ new Map();
21380
- for (const [id, model] of Object.entries(models)) providerModels.set(id, model);
21381
- modelRegistry.set(provider, providerModels);
21382
- }
21383
-
21384
- //#endregion
21385
- //#region src/vendor/pi/ai/stream.ts
21386
- function resolveApiProvider(api) {
21387
- const provider = getApiProvider(api);
21388
- if (!provider) throw new Error(`No API provider registered for api: ${api}`);
21389
- return provider;
21390
- }
21391
- function streamSimple(model, context, options) {
21392
- return resolveApiProvider(model.api).streamSimple(model, context, options);
21393
- }
21394
-
21395
21344
  //#endregion
21396
21345
  //#region src/vendor/pi/ai/utils/event-stream.ts
21397
21346
  var EventStream = class {
@@ -21453,19 +21402,89 @@ var AssistantMessageEventStream = class extends EventStream {
21453
21402
  }
21454
21403
  };
21455
21404
 
21405
+ //#endregion
21406
+ //#region src/vendor/pi/ai/env-api-keys.ts
21407
+ let _existsSync = null;
21408
+ let _homedir = null;
21409
+ let _join = null;
21410
+ const dynamicImport = (specifier) => import(specifier);
21411
+ const NODE_FS_SPECIFIER = "node:fs";
21412
+ const NODE_OS_SPECIFIER = "node:os";
21413
+ const NODE_PATH_SPECIFIER = "node:path";
21414
+ if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) {
21415
+ dynamicImport(NODE_FS_SPECIFIER).then((m) => {
21416
+ _existsSync = m.existsSync;
21417
+ });
21418
+ dynamicImport(NODE_OS_SPECIFIER).then((m) => {
21419
+ _homedir = m.homedir;
21420
+ });
21421
+ dynamicImport(NODE_PATH_SPECIFIER).then((m) => {
21422
+ _join = m.join;
21423
+ });
21424
+ }
21425
+
21426
+ //#endregion
21427
+ //#region src/vendor/pi/ai/utils/retry.ts
21428
+ function buildProviderErrorPattern(patterns) {
21429
+ return new RegExp(patterns.join("|"), "i");
21430
+ }
21431
+ const NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN = buildProviderErrorPattern([
21432
+ "GoUsageLimitError",
21433
+ "FreeUsageLimitError",
21434
+ "Monthly usage limit reached",
21435
+ "available balance",
21436
+ "insufficient_quota",
21437
+ "out of budget",
21438
+ "quota exceeded",
21439
+ "billing"
21440
+ ]);
21441
+ const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
21442
+ "overloaded",
21443
+ "rate.?limit",
21444
+ "too many requests",
21445
+ "429",
21446
+ "500",
21447
+ "502",
21448
+ "503",
21449
+ "504",
21450
+ "524",
21451
+ "service.?unavailable",
21452
+ "server.?error",
21453
+ "internal.?error",
21454
+ "provider.?returned.?error",
21455
+ "network.?error",
21456
+ "connection.?error",
21457
+ "connection.?refused",
21458
+ "connection.?lost",
21459
+ "other side closed",
21460
+ "fetch failed",
21461
+ "getaddrinfo",
21462
+ "ENOTFOUND",
21463
+ "EAI_AGAIN",
21464
+ "upstream.?connect",
21465
+ "reset before headers",
21466
+ "socket hang up",
21467
+ "socket connection was closed",
21468
+ "timed? out",
21469
+ "timeout",
21470
+ "terminated",
21471
+ "websocket.?closed",
21472
+ "websocket.?error",
21473
+ "ended without",
21474
+ "stream ended before message_stop",
21475
+ "stream ended before a terminal response event",
21476
+ "http2 request did not get a response",
21477
+ "retry delay",
21478
+ "you can retry your request",
21479
+ "try your request again",
21480
+ "please retry your request",
21481
+ "ResourceExhausted"
21482
+ ]);
21483
+
21456
21484
  //#endregion
21457
21485
  //#region src/vendor/pi/ai/utils/validation.ts
21458
21486
  const validatorCache = /* @__PURE__ */ new WeakMap();
21459
21487
  const TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
21460
- function isRecord(value) {
21461
- return typeof value === "object" && value !== null;
21462
- }
21463
- function isJsonSchemaObject(value) {
21464
- return isRecord(value);
21465
- }
21466
- function hasTypeBoxMetadata(schema) {
21467
- return isRecord(schema) && Object.getOwnPropertySymbols(schema).includes(TYPEBOX_KIND);
21468
- }
21469
21488
  function getSchemaTypes(schema) {
21470
21489
  if (typeof schema.type === "string") return [schema.type];
21471
21490
  if (Array.isArray(schema.type)) return schema.type.filter((type) => typeof type === "string");
@@ -21479,15 +21498,11 @@ function matchesJsonType(value, type) {
21479
21498
  case "string": return typeof value === "string";
21480
21499
  case "null": return value === null;
21481
21500
  case "array": return Array.isArray(value);
21482
- case "object": return isRecord(value) && !Array.isArray(value);
21501
+ case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
21483
21502
  default: return false;
21484
21503
  }
21485
21504
  }
21486
- function isValidatorSchema(value) {
21487
- return isRecord(value);
21488
- }
21489
21505
  function getSubSchemaValidator(schema) {
21490
- if (!isValidatorSchema(schema)) return;
21491
21506
  try {
21492
21507
  return getValidator(schema);
21493
21508
  } catch {
@@ -21540,7 +21555,7 @@ function applySchemaObjectCoercion(value, schema) {
21540
21555
  if (!(key in value)) continue;
21541
21556
  value[key] = coerceWithJsonSchema(value[key], propertySchema);
21542
21557
  }
21543
- if (schema.additionalProperties && isJsonSchemaObject(schema.additionalProperties)) for (const [key, propertyValue] of Object.entries(value)) {
21558
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") for (const [key, propertyValue] of Object.entries(value)) {
21544
21559
  if (definedKeys.has(key)) continue;
21545
21560
  value[key] = coerceWithJsonSchema(propertyValue, schema.additionalProperties);
21546
21561
  }
@@ -21554,7 +21569,7 @@ function applySchemaArrayCoercion(value, schema) {
21554
21569
  }
21555
21570
  return;
21556
21571
  }
21557
- if (isJsonSchemaObject(schema.items)) for (let index = 0; index < value.length; index++) value[index] = coerceWithJsonSchema(value[index], schema.items);
21572
+ if (schema.items && typeof schema.items === "object") for (let index = 0; index < value.length; index++) value[index] = coerceWithJsonSchema(value[index], schema.items);
21558
21573
  }
21559
21574
  function coerceWithUnionSchema(value, schemas) {
21560
21575
  for (const schema of schemas) {
@@ -21577,7 +21592,7 @@ function coerceWithJsonSchema(value, schema) {
21577
21592
  break;
21578
21593
  }
21579
21594
  }
21580
- if (schemaTypes.includes("object") && isRecord(nextValue) && !Array.isArray(nextValue)) applySchemaObjectCoercion(nextValue, schema);
21595
+ if (schemaTypes.includes("object") && typeof nextValue === "object" && nextValue !== null && !Array.isArray(nextValue)) applySchemaObjectCoercion(nextValue, schema);
21581
21596
  if (schemaTypes.includes("array") && Array.isArray(nextValue)) applySchemaArrayCoercion(nextValue, schema);
21582
21597
  return nextValue;
21583
21598
  }
@@ -21610,9 +21625,9 @@ function validateToolArguments(tool$1, toolCall) {
21610
21625
  const args = structuredClone(toolCall.arguments);
21611
21626
  Value.Convert(tool$1.parameters, args);
21612
21627
  const validator = getValidator(tool$1.parameters);
21613
- if (!hasTypeBoxMetadata(tool$1.parameters) && isJsonSchemaObject(tool$1.parameters)) {
21628
+ if (!Object.getOwnPropertySymbols(tool$1.parameters).includes(TYPEBOX_KIND)) {
21614
21629
  const coerced = coerceWithJsonSchema(args, tool$1.parameters);
21615
- if (coerced !== args) if (isRecord(args) && isRecord(coerced)) {
21630
+ if (coerced !== args) if (typeof args === "object" && args !== null && typeof coerced === "object" && coerced !== null) {
21616
21631
  for (const key of Object.keys(args)) delete args[key];
21617
21632
  Object.assign(args, coerced);
21618
21633
  } else return validator.Check(coerced) ? coerced : args;
@@ -21623,6 +21638,14 @@ function validateToolArguments(tool$1, toolCall) {
21623
21638
  throw new Error(errorMessage);
21624
21639
  }
21625
21640
 
21641
+ //#endregion
21642
+ //#region src/vendor/pi/agent/stream-fn.ts
21643
+ let defaultStreamFn;
21644
+ function getDefaultStreamFn() {
21645
+ if (!defaultStreamFn) throw new Error("No default stream function configured. Pass streamFn explicitly or call setDefaultStreamFn().");
21646
+ return defaultStreamFn;
21647
+ }
21648
+
21626
21649
  //#endregion
21627
21650
  //#region src/vendor/pi/agent/agent-loop.ts
21628
21651
  async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
@@ -21643,7 +21666,7 @@ async function runAgentLoop(prompts, context, config, emit, signal, streamFn) {
21643
21666
  message: prompt
21644
21667
  });
21645
21668
  }
21646
- await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
21669
+ await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
21647
21670
  return newMessages;
21648
21671
  }
21649
21672
  async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
@@ -21653,13 +21676,13 @@ async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
21653
21676
  const currentContext = { ...context };
21654
21677
  await emit({ type: "agent_start" });
21655
21678
  await emit({ type: "turn_start" });
21656
- await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
21679
+ await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
21657
21680
  return newMessages;
21658
21681
  }
21659
21682
  /**
21660
21683
  * Main loop logic shared by agentLoop and agentLoopContinue.
21661
21684
  */
21662
- async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFn) {
21685
+ async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFunction) {
21663
21686
  let currentContext = initialContext;
21664
21687
  let config = initialConfig;
21665
21688
  let firstTurn = true;
@@ -21684,7 +21707,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
21684
21707
  }
21685
21708
  pendingMessages = [];
21686
21709
  }
21687
- const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
21710
+ const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction);
21688
21711
  newMessages.push(message);
21689
21712
  if (message.stopReason === "error" || message.stopReason === "aborted") {
21690
21713
  await emit({
@@ -21702,7 +21725,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
21702
21725
  const toolResults = [];
21703
21726
  hasMoreToolCalls = false;
21704
21727
  if (toolCalls.length > 0) {
21705
- const executedToolBatch = await executeToolCalls(currentContext, message, config, signal, emit);
21728
+ const executedToolBatch = message.stopReason === "length" ? await failToolCallsFromTruncatedMessage(toolCalls, emit) : await executeToolCalls(currentContext, message, config, signal, emit);
21706
21729
  toolResults.push(...executedToolBatch.messages);
21707
21730
  hasMoreToolCalls = !executedToolBatch.terminate;
21708
21731
  for (const result of toolResults) {
@@ -21760,7 +21783,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
21760
21783
  * Stream an assistant response from the LLM.
21761
21784
  * This is where AgentMessage[] gets transformed to Message[] for the LLM.
21762
21785
  */
21763
- async function streamAssistantResponse(context, config, signal, emit, streamFn) {
21786
+ async function streamAssistantResponse(context, config, signal, emit, streamFunction) {
21764
21787
  let messages = context.messages;
21765
21788
  if (config.transformContext) messages = await config.transformContext(messages, signal);
21766
21789
  const llmMessages = await config.convertToLlm(messages);
@@ -21769,7 +21792,6 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
21769
21792
  messages: llmMessages,
21770
21793
  tools: context.tools
21771
21794
  };
21772
- const streamFunction = streamFn || streamSimple;
21773
21795
  const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : void 0) || config.apiKey;
21774
21796
  const response = await streamFunction(config.model, llmContext, {
21775
21797
  ...config,
@@ -21839,6 +21861,37 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
21839
21861
  return finalMessage;
21840
21862
  }
21841
21863
  /**
21864
+ * Fail all tool calls from an assistant message that was truncated by the
21865
+ * output token limit. Streamed tool-call arguments are finalized with a
21866
+ * best-effort JSON salvage parser, so a truncated message can yield tool calls
21867
+ * whose arguments parse and validate but are silently incomplete. None of them
21868
+ * are safe to execute; report each as an error so the model can re-issue them.
21869
+ */
21870
+ async function failToolCallsFromTruncatedMessage(toolCalls, emit) {
21871
+ const messages = [];
21872
+ for (const toolCall of toolCalls) {
21873
+ await emit({
21874
+ type: "tool_execution_start",
21875
+ toolCallId: toolCall.id,
21876
+ toolName: toolCall.name,
21877
+ args: toolCall.arguments
21878
+ });
21879
+ const finalized = {
21880
+ toolCall,
21881
+ result: createErrorToolResult(`Tool call "${toolCall.name}" was not executed: the response hit the output token limit, so its arguments may be truncated. Re-issue the tool call with complete arguments.`),
21882
+ isError: true
21883
+ };
21884
+ await emitToolExecutionEnd(finalized, emit);
21885
+ const toolResultMessage = createToolResultMessage(finalized);
21886
+ await emitToolResultMessage(toolResultMessage, emit);
21887
+ messages.push(toolResultMessage);
21888
+ }
21889
+ return {
21890
+ messages,
21891
+ terminate: false
21892
+ };
21893
+ }
21894
+ /**
21842
21895
  * Execute tool calls from an assistant message.
21843
21896
  */
21844
21897
  async function executeToolCalls(currentContext, assistantMessage, config, signal, emit) {
@@ -21977,8 +22030,10 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
21977
22030
  }
21978
22031
  async function executePreparedToolCall(prepared, signal, emit) {
21979
22032
  const updateEvents = [];
22033
+ let acceptingUpdates = true;
21980
22034
  try {
21981
22035
  const result = await prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, (partialResult) => {
22036
+ if (!acceptingUpdates) return;
21982
22037
  updateEvents.push(Promise.resolve(emit({
21983
22038
  type: "tool_execution_update",
21984
22039
  toolCallId: prepared.toolCall.id,
@@ -21987,17 +22042,21 @@ async function executePreparedToolCall(prepared, signal, emit) {
21987
22042
  partialResult
21988
22043
  })));
21989
22044
  });
22045
+ acceptingUpdates = false;
21990
22046
  await Promise.all(updateEvents);
21991
22047
  return {
21992
22048
  result,
21993
22049
  isError: false
21994
22050
  };
21995
22051
  } catch (error) {
22052
+ acceptingUpdates = false;
21996
22053
  await Promise.all(updateEvents);
21997
22054
  return {
21998
22055
  result: createErrorToolResult(error instanceof Error ? error.message : String(error)),
21999
22056
  isError: true
22000
22057
  };
22058
+ } finally {
22059
+ acceptingUpdates = false;
22001
22060
  }
22002
22061
  }
22003
22062
  async function finalizeExecutedToolCall(currentContext, assistantMessage, prepared, executed, config, signal) {
@@ -22014,8 +22073,10 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
22014
22073
  }, signal);
22015
22074
  if (afterResult) {
22016
22075
  result = {
22076
+ ...result,
22017
22077
  content: afterResult.content ?? result.content,
22018
22078
  details: afterResult.details ?? result.details,
22079
+ usage: afterResult.usage ?? result.usage,
22019
22080
  terminate: afterResult.terminate ?? result.terminate
22020
22081
  };
22021
22082
  isError = afterResult.isError ?? isError;
@@ -22053,8 +22114,10 @@ function createToolResultMessage(finalized) {
22053
22114
  role: "toolResult",
22054
22115
  toolCallId: finalized.toolCall.id,
22055
22116
  toolName: finalized.toolCall.name,
22056
- content: finalized.result.content,
22117
+ content: finalized.result.content ?? [],
22057
22118
  details: finalized.result.details,
22119
+ usage: finalized.result.usage,
22120
+ ...finalized.result.addedToolNames?.length ? { addedToolNames: finalized.result.addedToolNames } : {},
22058
22121
  isError: finalized.isError,
22059
22122
  timestamp: Date.now()
22060
22123
  };
@@ -22171,13 +22234,15 @@ var Agent$1 = class {
22171
22234
  followUpQueue;
22172
22235
  convertToLlm;
22173
22236
  transformContext;
22174
- streamFn;
22237
+ streamFunction;
22175
22238
  getApiKey;
22176
22239
  onPayload;
22177
22240
  onResponse;
22178
22241
  beforeToolCall;
22179
22242
  afterToolCall;
22243
+ shouldStopAfterTurn;
22180
22244
  prepareNextTurn;
22245
+ prepareNextTurnWithContext;
22181
22246
  activeRun;
22182
22247
  /** Session identifier forwarded to providers for cache-aware backends. */
22183
22248
  sessionId;
@@ -22189,24 +22254,27 @@ var Agent$1 = class {
22189
22254
  maxRetryDelayMs;
22190
22255
  /** Tool execution strategy for assistant messages that contain multiple tool calls. */
22191
22256
  toolExecution;
22192
- constructor(options = {}) {
22193
- this._state = createMutableAgentState(options.initialState);
22194
- this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
22195
- this.transformContext = options.transformContext;
22196
- this.streamFn = options.streamFn ?? streamSimple;
22197
- this.getApiKey = options.getApiKey;
22198
- this.onPayload = options.onPayload;
22199
- this.onResponse = options.onResponse;
22200
- this.beforeToolCall = options.beforeToolCall;
22201
- this.afterToolCall = options.afterToolCall;
22202
- this.prepareNextTurn = options.prepareNextTurn;
22203
- this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
22204
- this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
22205
- this.sessionId = options.sessionId;
22206
- this.thinkingBudgets = options.thinkingBudgets;
22207
- this.transport = options.transport ?? "auto";
22208
- this.maxRetryDelayMs = options.maxRetryDelayMs;
22209
- this.toolExecution = options.toolExecution ?? "parallel";
22257
+ constructor(options) {
22258
+ const runtimeOptions = options ?? {};
22259
+ this._state = createMutableAgentState(runtimeOptions.initialState);
22260
+ this.convertToLlm = runtimeOptions.convertToLlm ?? defaultConvertToLlm;
22261
+ this.transformContext = runtimeOptions.transformContext;
22262
+ this.streamFunction = runtimeOptions.streamFn ?? getDefaultStreamFn();
22263
+ this.getApiKey = runtimeOptions.getApiKey;
22264
+ this.onPayload = runtimeOptions.onPayload;
22265
+ this.onResponse = runtimeOptions.onResponse;
22266
+ this.beforeToolCall = runtimeOptions.beforeToolCall;
22267
+ this.afterToolCall = runtimeOptions.afterToolCall;
22268
+ this.shouldStopAfterTurn = runtimeOptions.shouldStopAfterTurn;
22269
+ this.prepareNextTurn = runtimeOptions.prepareNextTurn;
22270
+ this.prepareNextTurnWithContext = runtimeOptions.prepareNextTurnWithContext;
22271
+ this.steeringQueue = new PendingMessageQueue(runtimeOptions.steeringMode ?? "one-at-a-time");
22272
+ this.followUpQueue = new PendingMessageQueue(runtimeOptions.followUpMode ?? "one-at-a-time");
22273
+ this.sessionId = runtimeOptions.sessionId;
22274
+ this.thinkingBudgets = runtimeOptions.thinkingBudgets;
22275
+ this.transport = runtimeOptions.transport ?? "auto";
22276
+ this.maxRetryDelayMs = runtimeOptions.maxRetryDelayMs;
22277
+ this.toolExecution = runtimeOptions.toolExecution ?? "parallel";
22210
22278
  }
22211
22279
  /**
22212
22280
  * Subscribe to agent lifecycle events.
@@ -22336,12 +22404,12 @@ var Agent$1 = class {
22336
22404
  }
22337
22405
  async runPromptMessages(messages, options = {}) {
22338
22406
  await this.runWithLifecycle(async (signal) => {
22339
- await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFn);
22407
+ await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFunction);
22340
22408
  });
22341
22409
  }
22342
22410
  async runContinuation() {
22343
22411
  await this.runWithLifecycle(async (signal) => {
22344
- await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFn);
22412
+ await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFunction);
22345
22413
  });
22346
22414
  }
22347
22415
  createContextSnapshot() {
@@ -22365,7 +22433,11 @@ var Agent$1 = class {
22365
22433
  toolExecution: this.toolExecution,
22366
22434
  beforeToolCall: this.beforeToolCall,
22367
22435
  afterToolCall: this.afterToolCall,
22368
- prepareNextTurn: this.prepareNextTurn ? async () => await this.prepareNextTurn?.(this.signal) : void 0,
22436
+ shouldStopAfterTurn: this.shouldStopAfterTurn,
22437
+ prepareNextTurn: this.prepareNextTurnWithContext || this.prepareNextTurn ? async (context) => {
22438
+ if (this.prepareNextTurnWithContext) return await this.prepareNextTurnWithContext(context, this.signal);
22439
+ return await this.prepareNextTurn?.(this.signal);
22440
+ } : void 0,
22369
22441
  convertToLlm: this.convertToLlm,
22370
22442
  transformContext: this.transformContext,
22371
22443
  getApiKey: this.getApiKey,
@@ -22490,28 +22562,49 @@ var Agent$1 = class {
22490
22562
  const DEFAULT_MAX_BYTES = 50 * 1024;
22491
22563
  const runtimeBuffer = globalThis.Buffer;
22492
22564
 
22565
+ //#endregion
22566
+ //#region src/vendor/pi/agent/harness/tools/bash.ts
22567
+ const MAX_TIMEOUT_SECONDS = 2147483647 / 1e3;
22568
+ const bashSchema = Type.Object({
22569
+ command: Type.String({ description: "Bash command to execute" }),
22570
+ timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" }))
22571
+ });
22572
+
22573
+ //#endregion
22574
+ //#region src/vendor/pi/agent/harness/tools/edit.ts
22575
+ const replaceEditSchema = Type.Object({
22576
+ oldText: Type.String({ description: "Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call." }),
22577
+ newText: Type.String({ description: "Replacement text for this targeted edit." })
22578
+ }, {});
22579
+ const editSchema = Type.Object({
22580
+ path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
22581
+ edits: Type.Array(replaceEditSchema, { description: "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead." })
22582
+ }, {});
22583
+
22584
+ //#endregion
22585
+ //#region src/vendor/pi/agent/harness/tools/read.ts
22586
+ const readSchema = Type.Object({
22587
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
22588
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
22589
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" }))
22590
+ });
22591
+
22592
+ //#endregion
22593
+ //#region src/vendor/pi/agent/harness/tools/write.ts
22594
+ const writeSchema = Type.Object({
22595
+ path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
22596
+ content: Type.String({ description: "Content to write to the file" })
22597
+ });
22598
+
22493
22599
  //#endregion
22494
22600
  //#region src/lib/worker-agent/budget.ts
22495
22601
  const DEFAULT_MAX_TURNS = 500;
22496
22602
  const DEFAULT_MAX_WALLCLOCK_MS = 360 * 6e4;
22603
+ const DEFAULT_MODEL_CALL_TIMEOUT_MS = 15 * 6e4;
22497
22604
  const DEFAULT_MAX_TOOL_BYTES = 16 * 1024 * 1024;
22498
22605
  const DEFAULT_MAX_TOOL_CALLS = 250;
22499
22606
  const DEFAULT_MAX_REPEATED_CALLS = 3;
22500
22607
  /**
22501
- * Thrown when the wall-clock budget is exceeded. Engine catches this
22502
- * around `agent.prompt()` / `agent.continue()` and converts it to a
22503
- * terse `[halted: wallclock]` reply. Carries no extra metadata — by
22504
- * design (no advice).
22505
- */
22506
- var WorkerAbort = class extends Error {
22507
- reason;
22508
- constructor(reason) {
22509
- super(`[halted: ${reason}]`);
22510
- this.reason = reason;
22511
- this.name = "WorkerAbort";
22512
- }
22513
- };
22514
- /**
22515
22608
  * Read a positive-integer env override. Returns `undefined` if the
22516
22609
  * env var is unset, empty, or doesn't parse to a positive integer —
22517
22610
  * keeping the constructor defaults intact. We don't throw on bad input
@@ -22540,6 +22633,8 @@ const DEFAULT_MCP_TOOL_TIMEOUT_MS = 225e5;
22540
22633
  * hard-killed by the harness.
22541
22634
  */
22542
22635
  const MCP_TIMEOUT_HEADROOM_MS = 15 * 6e4;
22636
+ /** Smallest useful worker deadline when the MCP timeout is misconfigured below headroom. */
22637
+ const MIN_WORKER_WALLCLOCK_MS = 1e3;
22543
22638
  /**
22544
22639
  * The MCP per-tool-call timeout the proxy injects into the spawned CLI, in ms.
22545
22640
  * Positive-integer override via `GH_ROUTER_MCP_TOOL_TIMEOUT_MS`; falls back to
@@ -22551,6 +22646,10 @@ const MCP_TIMEOUT_HEADROOM_MS = 15 * 6e4;
22551
22646
  function resolveMcpToolTimeoutMs() {
22552
22647
  return envInt("GH_ROUTER_MCP_TOOL_TIMEOUT_MS") ?? DEFAULT_MCP_TOOL_TIMEOUT_MS;
22553
22648
  }
22649
+ /** Whole-call deadline for one worker model turn, including SSE consumption. */
22650
+ function resolveWorkerModelCallTimeoutMs() {
22651
+ return envInt("GH_ROUTER_WORKER_MODEL_CALL_TIMEOUT_MS") ?? DEFAULT_MODEL_CALL_TIMEOUT_MS;
22652
+ }
22554
22653
  /**
22555
22654
  * The maximum wall-clock a single worker call may be granted: the MCP
22556
22655
  * tool-call timeout minus the teardown headroom. A per-call `maxWallClockMs`
@@ -22560,7 +22659,7 @@ function resolveMcpToolTimeoutMs() {
22560
22659
  * the default MCP timeout (22_500_000 − 900_000 === 21_600_000).
22561
22660
  */
22562
22661
  function workerWallClockCeilingMs() {
22563
- return resolveMcpToolTimeoutMs() - MCP_TIMEOUT_HEADROOM_MS;
22662
+ return Math.max(MIN_WORKER_WALLCLOCK_MS, resolveMcpToolTimeoutMs() - MCP_TIMEOUT_HEADROOM_MS);
22564
22663
  }
22565
22664
  /**
22566
22665
  * Resolve a `BudgetConfig` from defaults + env overrides + caller-
@@ -22570,13 +22669,14 @@ function workerWallClockCeilingMs() {
22570
22669
  * can introspect the merged config without spinning up the `Budget`
22571
22670
  * class.
22572
22671
  */
22573
- function resolveBudgetConfig(overrides) {
22672
+ function resolveBudgetConfig(overrides$1) {
22673
+ const requestedWallClockMs = overrides$1?.maxWallClockMs ?? envInt("GH_ROUTER_WORKER_MAX_WALLCLOCK_MS") ?? DEFAULT_MAX_WALLCLOCK_MS;
22574
22674
  return {
22575
- maxTurns: overrides?.maxTurns ?? envInt("GH_ROUTER_WORKER_MAX_TURNS") ?? DEFAULT_MAX_TURNS,
22576
- maxWallClockMs: overrides?.maxWallClockMs ?? envInt("GH_ROUTER_WORKER_MAX_WALLCLOCK_MS") ?? DEFAULT_MAX_WALLCLOCK_MS,
22577
- maxToolBytes: overrides?.maxToolBytes ?? envInt("GH_ROUTER_WORKER_MAX_TOOL_BYTES") ?? DEFAULT_MAX_TOOL_BYTES,
22578
- maxToolCalls: overrides?.maxToolCalls ?? envInt("GH_ROUTER_WORKER_MAX_TOOL_CALLS") ?? DEFAULT_MAX_TOOL_CALLS,
22579
- maxRepeatedCalls: overrides?.maxRepeatedCalls ?? envInt("GH_ROUTER_WORKER_MAX_REPEATED_CALLS") ?? DEFAULT_MAX_REPEATED_CALLS
22675
+ maxTurns: overrides$1?.maxTurns ?? envInt("GH_ROUTER_WORKER_MAX_TURNS") ?? DEFAULT_MAX_TURNS,
22676
+ maxWallClockMs: Math.min(requestedWallClockMs, workerWallClockCeilingMs()),
22677
+ maxToolBytes: overrides$1?.maxToolBytes ?? envInt("GH_ROUTER_WORKER_MAX_TOOL_BYTES") ?? DEFAULT_MAX_TOOL_BYTES,
22678
+ maxToolCalls: overrides$1?.maxToolCalls ?? envInt("GH_ROUTER_WORKER_MAX_TOOL_CALLS") ?? DEFAULT_MAX_TOOL_CALLS,
22679
+ maxRepeatedCalls: overrides$1?.maxRepeatedCalls ?? envInt("GH_ROUTER_WORKER_MAX_REPEATED_CALLS") ?? DEFAULT_MAX_REPEATED_CALLS
22580
22680
  };
22581
22681
  }
22582
22682
  /**
@@ -22588,11 +22688,10 @@ function resolveBudgetConfig(overrides) {
22588
22688
  * - `checkBeforeCall(name, args)` is called from Pi's
22589
22689
  * `beforeToolCall` hook. Returns `{block: true, reason: "[halted:
22590
22690
  * turns]"}` etc. when a cap fires.
22591
- * - `recordToolBytes(result)` is called from Pi's `afterToolCall`
22592
- * hook.
22593
- * - `checkWallClock()` is called by the engine around blocking
22594
- * awaits and from `beforeToolCall` throws `WorkerAbort` when
22595
- * `Date.now() - startMs > maxWallClockMs`.
22691
+ * - `recordToolBytes(result)` is called from Pi's `afterToolCall` hook.
22692
+ * - A hard cap latches `hardStopReason`; the engine's
22693
+ * `shouldStopAfterTurn` hook reads it and ends the loop before another
22694
+ * provider request. The repeated-call guard remains a non-terminal block.
22596
22695
  */
22597
22696
  var Budget = class {
22598
22697
  config;
@@ -22600,10 +22699,11 @@ var Budget = class {
22600
22699
  turnCount = 0;
22601
22700
  toolBytes = 0;
22602
22701
  toolCallCount = 0;
22702
+ hardStopReasonValue = null;
22603
22703
  lastCallKey = null;
22604
22704
  consecutiveRepeats = 0;
22605
- constructor(overrides) {
22606
- this.config = resolveBudgetConfig(overrides);
22705
+ constructor(overrides$1) {
22706
+ this.config = resolveBudgetConfig(overrides$1);
22607
22707
  this.startMs = Date.now();
22608
22708
  }
22609
22709
  /** Record a turn. Does NOT throw — `checkBeforeCall` surfaces the cap. */
@@ -22622,16 +22722,16 @@ var Budget = class {
22622
22722
  get elapsedMs() {
22623
22723
  return Date.now() - this.startMs;
22624
22724
  }
22625
- /**
22626
- * Throw `WorkerAbort("wallclock")` if elapsed time exceeds
22627
- * `maxWallClockMs`. Engine wraps long awaits in `await
22628
- * Promise.race([..., wallClockTimer])` for prompt cancellation; this
22629
- * is the fallback for cases where the timer hasn't fired yet but a
22630
- * call site wants to be sure (e.g. before sending the next LLM
22631
- * request).
22632
- */
22633
- checkWallClock() {
22634
- if (this.elapsedMs > this.config.maxWallClockMs) throw new WorkerAbort("wallclock");
22725
+ /** First hard cap reached during this run, or null while work may continue. */
22726
+ get hardStopReason() {
22727
+ return this.hardStopReasonValue;
22728
+ }
22729
+ halt(reason) {
22730
+ this.hardStopReasonValue ??= reason;
22731
+ return {
22732
+ block: true,
22733
+ reason: `[halted: ${reason}]`
22734
+ };
22635
22735
  }
22636
22736
  /**
22637
22737
  * Pi `beforeToolCall` integration. Returns `{block: true, reason}`
@@ -22650,23 +22750,12 @@ var Budget = class {
22650
22750
  * signature in Pi without forcing the engine into a wrapper.
22651
22751
  */
22652
22752
  checkBeforeCall(toolName, args) {
22653
- if (this.turnCount > this.config.maxTurns) return {
22654
- block: true,
22655
- reason: "[halted: turns]"
22656
- };
22657
- if (this.elapsedMs > this.config.maxWallClockMs) return {
22658
- block: true,
22659
- reason: "[halted: wallclock]"
22660
- };
22661
- if (this.toolBytes > this.config.maxToolBytes) return {
22662
- block: true,
22663
- reason: "[halted: tool-bytes]"
22664
- };
22753
+ if (this.hardStopReasonValue) return this.halt(this.hardStopReasonValue);
22754
+ if (this.turnCount > this.config.maxTurns) return this.halt("turns");
22755
+ if (this.elapsedMs > this.config.maxWallClockMs) return this.halt("wallclock");
22756
+ if (this.toolBytes > this.config.maxToolBytes) return this.halt("tool-bytes");
22665
22757
  this.toolCallCount += 1;
22666
- if (this.toolCallCount > this.config.maxToolCalls) return {
22667
- block: true,
22668
- reason: "[halted: tool-calls]"
22669
- };
22758
+ if (this.toolCallCount > this.config.maxToolCalls) return this.halt("tool-calls");
22670
22759
  const key = `${toolName}:${stableArgs(args)}`;
22671
22760
  if (key === this.lastCallKey) this.consecutiveRepeats += 1;
22672
22761
  else {
@@ -22735,14 +22824,15 @@ function extractTextByteLength(result) {
22735
22824
  * the clamp logic. Lower index = less thinking. `"off"` is below
22736
22825
  * everything; `"xhigh"` is the cap.
22737
22826
  */
22738
- const THINKING_ORDER = [
22827
+ const WORKER_THINKING_LEVELS = Object.freeze([
22739
22828
  "off",
22740
22829
  "minimal",
22741
22830
  "low",
22742
22831
  "medium",
22743
22832
  "high",
22744
22833
  "xhigh"
22745
- ];
22834
+ ]);
22835
+ const THINKING_ORDER = WORKER_THINKING_LEVELS;
22746
22836
  function tier(level) {
22747
22837
  const i = THINKING_ORDER.indexOf(level);
22748
22838
  return i < 0 ? THINKING_ORDER.indexOf("high") : i;
@@ -22784,13 +22874,7 @@ function resolveModelAndThinking(opts) {
22784
22874
  });
22785
22875
  const allowedRaw = found.capabilities?.supports?.reasoning_effort;
22786
22876
  if (!allowedRaw || allowedRaw.length === 0) return mkOk("off");
22787
- const allowed = allowedRaw.filter((l) => [
22788
- "minimal",
22789
- "low",
22790
- "medium",
22791
- "high",
22792
- "xhigh"
22793
- ].includes(l)).sort((a, b) => tier(a) - tier(b));
22877
+ const allowed = allowedRaw.filter((l) => WORKER_THINKING_LEVELS.includes(l) && l !== "off").sort((a, b) => tier(a) - tier(b));
22794
22878
  if (allowed.length === 0) return mkOk("off");
22795
22879
  if (opts.thinking === "off") return mkOk("off");
22796
22880
  if (allowed.includes(opts.thinking)) return mkOk(opts.thinking);
@@ -22804,6 +22888,40 @@ function resolveModelAndThinking(opts) {
22804
22888
  return mkOk(clamp$1);
22805
22889
  }
22806
22890
 
22891
+ //#endregion
22892
+ //#region src/lib/worker-agent/session-defaults.ts
22893
+ const MODES = Object.freeze([
22894
+ "explore",
22895
+ "review",
22896
+ "plan",
22897
+ "implement",
22898
+ "test",
22899
+ "browse"
22900
+ ]);
22901
+ /**
22902
+ * Process-global, in-memory overrides. They are intentionally not persisted.
22903
+ * In `serve` mode one process may serve multiple client sessions, so these
22904
+ * values are process-wide rather than isolated to an individual client.
22905
+ */
22906
+ const overrides = {};
22907
+ function getWorkerSessionDefault(mode) {
22908
+ return { ...overrides[mode] };
22909
+ }
22910
+ function setWorkerSessionDefault(mode, value) {
22911
+ const next = { ...overrides[mode] };
22912
+ if (value.model !== void 0) next.model = value.model;
22913
+ if (value.thinking !== void 0) next.thinking = value.thinking;
22914
+ overrides[mode] = next;
22915
+ return { ...next };
22916
+ }
22917
+ function resetWorkerSessionDefault(mode) {
22918
+ delete overrides[mode];
22919
+ }
22920
+ function resetAllWorkerSessionDefaults() {
22921
+ for (const mode of MODES) delete overrides[mode];
22922
+ }
22923
+ const WORKER_MODES = MODES;
22924
+
22807
22925
  //#endregion
22808
22926
  //#region src/lib/worker-agent/prompts.ts
22809
22927
  /**
@@ -22855,7 +22973,7 @@ const READ_TOOL_NOTES = [
22855
22973
  const WRITE_TOOL_NOTES = [
22856
22974
  "`edit` — exact-string replacement in a file.",
22857
22975
  "`write` — overwrite or create a file.",
22858
- "`bash` — run a shell command in the workspace.",
22976
+ "`bash` — run builds, tests, git, package managers, and programs; reading, searching, and editing have dedicated tools (`read`/`glob`/`grep`/`code_search`/`edit`/`write`/`toolbelt`), so a `python`/`node`/`powershell` one-off script for those jobs is unnecessary.",
22859
22977
  "`codex_review` — code review by `codex-reviewer` (gpt-5.3-codex, code-specialist critic). Returns line-level findings on a diff or single file."
22860
22978
  ];
22861
22979
  function buildToolBlock(tools) {
@@ -23220,6 +23338,13 @@ function assembleResponsesPayload(opts) {
23220
23338
  */
23221
23339
  /** Conservative bytes/token for dense DOM-JSON; over-counts tokens by design. */
23222
23340
  const BYTES_PER_TOKEN = 3;
23341
+ /**
23342
+ * Conservative context floor when the live catalog omits or reports an invalid window.
23343
+ * This is deliberately not a hardcoded per-model window table: duplicating the
23344
+ * live catalog would go stale on every model launch and violate the rule to
23345
+ * gate on catalog capabilities rather than model slugs.
23346
+ */
23347
+ const FALLBACK_WINDOW_TOKENS = 128e3;
23223
23348
  const OUTPUT_RESERVE_TOKENS = 12e3;
23224
23349
  const TOOL_SCHEMA_RESERVE_TOKENS = 6e3;
23225
23350
  const SYSTEM_RESERVE_TOKENS = 2e3;
@@ -23253,17 +23378,19 @@ function tokensFromBytes(bytes) {
23253
23378
  /**
23254
23379
  * Build a per-run budget from the model's catalog context window (tokens).
23255
23380
  *
23256
- * Returns `undefined` when the window is unknown / non-positive — callers
23257
- * MUST no-op (no compaction, no dynamic cap) rather than prune blindly
23258
- * against a guessed window. This is the safe degradation on a catalog that
23259
- * doesn't report `max_context_window_tokens`.
23381
+ * Unknown, non-finite, and non-positive windows use the conservative fallback
23382
+ * floor so compaction and the dynamic result cap remain engaged. The returned
23383
+ * `windowKnown` flag keeps the request backstop advisory in that case: upstream
23384
+ * remains authoritative when the model's real window is unknown.
23260
23385
  */
23261
23386
  function makeContextBudget(windowTokens) {
23262
- if (windowTokens === void 0 || !Number.isFinite(windowTokens) || windowTokens <= 0) return;
23263
- const inputHardLimitTokens = Math.max(0, Math.floor(windowTokens * (1 - ASSEMBLY_MARGIN_FRACTION)) - OUTPUT_RESERVE_TOKENS);
23387
+ const windowKnown = windowTokens !== void 0 && Number.isFinite(windowTokens) && windowTokens > 0;
23388
+ const effectiveWindowTokens = windowKnown ? windowTokens : FALLBACK_WINDOW_TOKENS;
23389
+ const inputHardLimitTokens = Math.max(0, Math.floor(effectiveWindowTokens * (1 - ASSEMBLY_MARGIN_FRACTION)) - OUTPUT_RESERVE_TOKENS);
23264
23390
  const promptBudgetTokens = Math.max(0, inputHardLimitTokens - TOOL_SCHEMA_RESERVE_TOKENS - SYSTEM_RESERVE_TOKENS);
23265
23391
  return {
23266
- windowTokens,
23392
+ windowKnown,
23393
+ windowTokens: effectiveWindowTokens,
23267
23394
  inputHardLimitTokens,
23268
23395
  promptBudgetTokens,
23269
23396
  compactTriggerTokens: Math.floor(promptBudgetTokens * COMPACT_TRIGGER_FRACTION),
@@ -23271,7 +23398,7 @@ function makeContextBudget(windowTokens) {
23271
23398
  hardLimitTokens: Math.floor(promptBudgetTokens * HARD_LIMIT_FRACTION),
23272
23399
  keepRecentTokens: Math.max(KEEP_RECENT_FLOOR_TOKENS, Math.floor(promptBudgetTokens * KEEP_RECENT_FRACTION)),
23273
23400
  maxProtectedTokens: Math.max(Math.max(KEEP_RECENT_FLOOR_TOKENS, Math.floor(promptBudgetTokens * KEEP_RECENT_FRACTION)), Math.floor(promptBudgetTokens * MAX_PROTECTED_FRACTION)),
23274
- perResultCapBytes: clamp(Math.round(windowTokens * PER_RESULT_CAP_FRACTION * BYTES_PER_TOKEN), PER_RESULT_CAP_MIN_BYTES, PER_RESULT_CAP_MAX_BYTES)
23401
+ perResultCapBytes: clamp(Math.round(effectiveWindowTokens * PER_RESULT_CAP_FRACTION * BYTES_PER_TOKEN), PER_RESULT_CAP_MIN_BYTES, PER_RESULT_CAP_MAX_BYTES)
23275
23402
  };
23276
23403
  }
23277
23404
 
@@ -23294,11 +23421,52 @@ function createCopilotStreamFn(opts) {
23294
23421
  return stream;
23295
23422
  };
23296
23423
  }
23424
+ async function runModelCallAttempts(stream, resolved, options, timeoutMs, runAttempt) {
23425
+ for (let attempt = 0; attempt < 2; attempt += 1) {
23426
+ const deadline = new AbortController();
23427
+ let localDeadlineFired = false;
23428
+ let rejectDeadline = () => {};
23429
+ const timeoutPromise = new Promise((_resolve, reject) => {
23430
+ rejectDeadline = reject;
23431
+ });
23432
+ timeoutPromise.catch(() => {});
23433
+ const timer = setTimeout(() => {
23434
+ localDeadlineFired = true;
23435
+ const error = new DOMException("Worker model call timed out", "TimeoutError");
23436
+ deadline.abort(error);
23437
+ rejectDeadline(error);
23438
+ }, timeoutMs);
23439
+ const signal = options?.signal ? AbortSignal.any([options.signal, deadline.signal]) : deadline.signal;
23440
+ const state$1 = {
23441
+ emitted: false,
23442
+ active: true
23443
+ };
23444
+ const attemptPromise = runAttempt(signal, state$1);
23445
+ attemptPromise.catch(() => {});
23446
+ try {
23447
+ await Promise.race([attemptPromise, timeoutPromise]);
23448
+ return;
23449
+ } catch (err) {
23450
+ if (options?.signal?.aborted) {
23451
+ pushTerminalError(stream, resolved, options.signal.reason ?? err);
23452
+ return;
23453
+ }
23454
+ if (!state$1.emitted && attempt === 0) continue;
23455
+ if (localDeadlineFired) pushModelCallTimeoutDiagnostic(stream, resolved, timeoutMs, state$1.emitted);
23456
+ else pushTerminalError(stream, resolved, err);
23457
+ return;
23458
+ } finally {
23459
+ state$1.active = false;
23460
+ clearTimeout(timer);
23461
+ }
23462
+ }
23463
+ }
23297
23464
  async function runStreamLoop(stream, context, opts, options) {
23298
23465
  const { resolved } = opts;
23299
23466
  if (opts.contextBudget) {
23300
23467
  const assembledTokens = tokensFromBytes(estimateContextBytes(context));
23301
- if (assembledTokens > opts.contextBudget.inputHardLimitTokens) {
23468
+ if (assembledTokens > opts.contextBudget.inputHardLimitTokens) if (!opts.contextBudget.windowKnown) consola.warn(`[worker] assembled request estimate ${assembledTokens} tokens exceeds fallback input limit ${opts.contextBudget.inputHardLimitTokens}; proceeding because the catalog window is unknown`);
23469
+ else {
23302
23470
  pushBackstopDiagnostic(stream, resolved, assembledTokens, opts.contextBudget.inputHardLimitTokens);
23303
23471
  return;
23304
23472
  }
@@ -23314,15 +23482,13 @@ async function runStreamLoop(stream, context, opts, options) {
23314
23482
  pushTerminalError(stream, resolved, err);
23315
23483
  return;
23316
23484
  }
23317
- let sseStream;
23318
- try {
23319
- const result = await createChatCompletions(payload, void 0, options?.signal, true);
23320
- if (result == null || typeof result[Symbol.asyncIterator] !== "function") throw new Error("Upstream did not return an SSE stream (stream: true expected)");
23321
- sseStream = result;
23322
- } catch (err) {
23323
- pushTerminalError(stream, resolved, err);
23324
- return;
23325
- }
23485
+ await runModelCallAttempts(stream, resolved, options, opts.modelCallTimeoutMs ?? 15 * 6e4, (signal, state$1) => runChatAttempt(stream, payload, opts, signal, state$1));
23486
+ }
23487
+ async function runChatAttempt(stream, payload, opts, signal, state$1) {
23488
+ const { resolved } = opts;
23489
+ const result = await createChatCompletions(payload, void 0, signal, false);
23490
+ if (result == null || typeof result[Symbol.asyncIterator] !== "function") throw new Error("Upstream did not return an SSE stream (stream: true expected)");
23491
+ const sseStream = result;
23326
23492
  const accum = {
23327
23493
  blocks: [],
23328
23494
  textChunksByIndex: /* @__PURE__ */ new Map(),
@@ -23331,100 +23497,102 @@ async function runStreamLoop(stream, context, opts, options) {
23331
23497
  let nextContentIndex = 0;
23332
23498
  let activeTextIndex = null;
23333
23499
  const toolPiIndexByOAI = /* @__PURE__ */ new Map();
23334
- try {
23335
- for await (const evt of sseStream) {
23336
- const data = evt?.data;
23337
- if (data == null) continue;
23338
- if (data === "[DONE]") break;
23339
- let chunk;
23340
- try {
23341
- chunk = JSON.parse(data);
23342
- } catch {
23343
- continue;
23500
+ for await (const evt of sseStream) {
23501
+ const data = evt?.data;
23502
+ if (data == null) continue;
23503
+ if (data === "[DONE]") break;
23504
+ let chunk;
23505
+ try {
23506
+ chunk = JSON.parse(data);
23507
+ } catch {
23508
+ continue;
23509
+ }
23510
+ try {
23511
+ opts.onChunk?.(chunk);
23512
+ } catch {}
23513
+ if (chunk.usage) accum.usage = chunk.usage;
23514
+ const choice = chunk.choices?.[0];
23515
+ if (!choice) continue;
23516
+ const delta = choice.delta ?? {};
23517
+ if (typeof delta.content === "string" && delta.content.length > 0) {
23518
+ if (activeTextIndex == null) {
23519
+ state$1.emitted = true;
23520
+ activeTextIndex = nextContentIndex++;
23521
+ accum.blocks.push({
23522
+ kind: "text",
23523
+ contentIndex: activeTextIndex
23524
+ });
23525
+ accum.textChunksByIndex.set(activeTextIndex, []);
23526
+ if (!state$1.active) return;
23527
+ stream.push({
23528
+ type: "text_start",
23529
+ contentIndex: activeTextIndex,
23530
+ partial: buildPartial(resolved, accum)
23531
+ });
23344
23532
  }
23345
- try {
23346
- opts.onChunk?.(chunk);
23347
- } catch {}
23348
- if (chunk.usage) accum.usage = chunk.usage;
23349
- const choice = chunk.choices?.[0];
23350
- if (!choice) continue;
23351
- const delta = choice.delta ?? {};
23352
- if (typeof delta.content === "string" && delta.content.length > 0) {
23353
- if (activeTextIndex == null) {
23354
- activeTextIndex = nextContentIndex++;
23355
- accum.blocks.push({
23356
- kind: "text",
23357
- contentIndex: activeTextIndex
23358
- });
23359
- accum.textChunksByIndex.set(activeTextIndex, []);
23360
- stream.push({
23361
- type: "text_start",
23362
- contentIndex: activeTextIndex,
23363
- partial: buildPartial(resolved, accum)
23364
- });
23365
- }
23366
- accum.textChunksByIndex.get(activeTextIndex).push(delta.content);
23533
+ accum.textChunksByIndex.get(activeTextIndex).push(delta.content);
23534
+ stream.push({
23535
+ type: "text_delta",
23536
+ contentIndex: activeTextIndex,
23537
+ delta: delta.content,
23538
+ partial: buildPartial(resolved, accum)
23539
+ });
23540
+ }
23541
+ if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
23542
+ if (activeTextIndex != null) {
23543
+ if (!state$1.active) return;
23367
23544
  stream.push({
23368
- type: "text_delta",
23545
+ type: "text_end",
23369
23546
  contentIndex: activeTextIndex,
23370
- delta: delta.content,
23547
+ content: joinTextChunks(accum, activeTextIndex),
23371
23548
  partial: buildPartial(resolved, accum)
23372
23549
  });
23550
+ activeTextIndex = null;
23373
23551
  }
23374
- if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
23375
- if (activeTextIndex != null) {
23552
+ for (const tcd of delta.tool_calls) {
23553
+ if (tcd == null || tcd.index == null) continue;
23554
+ let piIdx = toolPiIndexByOAI.get(tcd.index);
23555
+ if (piIdx == null) {
23556
+ state$1.emitted = true;
23557
+ piIdx = nextContentIndex++;
23558
+ toolPiIndexByOAI.set(tcd.index, piIdx);
23559
+ accum.blocks.push({
23560
+ kind: "tool",
23561
+ contentIndex: piIdx,
23562
+ openaiIndex: tcd.index
23563
+ });
23564
+ accum.toolByIndex.set(piIdx, {
23565
+ id: "",
23566
+ name: "",
23567
+ argumentChunks: []
23568
+ });
23569
+ if (!state$1.active) return;
23376
23570
  stream.push({
23377
- type: "text_end",
23378
- contentIndex: activeTextIndex,
23379
- content: joinTextChunks(accum, activeTextIndex),
23571
+ type: "toolcall_start",
23572
+ contentIndex: piIdx,
23380
23573
  partial: buildPartial(resolved, accum)
23381
23574
  });
23382
- activeTextIndex = null;
23383
23575
  }
23384
- for (const tcd of delta.tool_calls) {
23385
- if (tcd == null || tcd.index == null) continue;
23386
- let piIdx = toolPiIndexByOAI.get(tcd.index);
23387
- if (piIdx == null) {
23388
- piIdx = nextContentIndex++;
23389
- toolPiIndexByOAI.set(tcd.index, piIdx);
23390
- accum.blocks.push({
23391
- kind: "tool",
23392
- contentIndex: piIdx,
23393
- openaiIndex: tcd.index
23394
- });
23395
- accum.toolByIndex.set(piIdx, {
23396
- id: "",
23397
- name: "",
23398
- argumentChunks: []
23399
- });
23400
- stream.push({
23401
- type: "toolcall_start",
23402
- contentIndex: piIdx,
23403
- partial: buildPartial(resolved, accum)
23404
- });
23405
- }
23406
- const entry = accum.toolByIndex.get(piIdx);
23407
- if (!entry) continue;
23408
- if (tcd.id) entry.id = tcd.id;
23409
- if (tcd.function?.name) entry.name = tcd.function.name;
23410
- const argDelta = tcd.function?.arguments;
23411
- if (typeof argDelta === "string" && argDelta.length > 0) {
23412
- entry.argumentChunks.push(argDelta);
23413
- stream.push({
23414
- type: "toolcall_delta",
23415
- contentIndex: piIdx,
23416
- delta: argDelta,
23417
- partial: buildPartial(resolved, accum)
23418
- });
23419
- }
23576
+ const entry = accum.toolByIndex.get(piIdx);
23577
+ if (!entry) continue;
23578
+ if (tcd.id) entry.id = tcd.id;
23579
+ if (tcd.function?.name) entry.name = tcd.function.name;
23580
+ const argDelta = tcd.function?.arguments;
23581
+ if (typeof argDelta === "string" && argDelta.length > 0) {
23582
+ entry.argumentChunks.push(argDelta);
23583
+ if (!state$1.active) return;
23584
+ stream.push({
23585
+ type: "toolcall_delta",
23586
+ contentIndex: piIdx,
23587
+ delta: argDelta,
23588
+ partial: buildPartial(resolved, accum)
23589
+ });
23420
23590
  }
23421
23591
  }
23422
- if (choice.finish_reason) accum.finishReason = choice.finish_reason;
23423
23592
  }
23424
- } catch (err) {
23425
- pushTerminalError(stream, resolved, err);
23426
- return;
23593
+ if (choice.finish_reason) accum.finishReason = choice.finish_reason;
23427
23594
  }
23595
+ if (!state$1.active) return;
23428
23596
  if (activeTextIndex != null) {
23429
23597
  stream.push({
23430
23598
  type: "text_end",
@@ -23587,15 +23755,13 @@ async function runResponsesStreamLoop(stream, context, opts, options) {
23587
23755
  pushTerminalError(stream, resolved, err);
23588
23756
  return;
23589
23757
  }
23590
- let sseStream;
23591
- try {
23592
- const result = await createResponses(payload, void 0, options?.signal, true);
23593
- if (result == null || typeof result[Symbol.asyncIterator] !== "function") throw new Error("Upstream did not return an SSE stream (stream: true expected)");
23594
- sseStream = result;
23595
- } catch (err) {
23596
- pushTerminalError(stream, resolved, err);
23597
- return;
23598
- }
23758
+ await runModelCallAttempts(stream, resolved, options, opts.modelCallTimeoutMs ?? 15 * 6e4, (signal, state$1) => runResponsesAttempt(stream, payload, opts, signal, state$1));
23759
+ }
23760
+ async function runResponsesAttempt(stream, payload, opts, signal, state$1) {
23761
+ const { resolved } = opts;
23762
+ const result = await createResponses(payload, void 0, signal, false);
23763
+ if (result == null || typeof result[Symbol.asyncIterator] !== "function") throw new Error("Upstream did not return an SSE stream (stream: true expected)");
23764
+ const sseStream = result;
23599
23765
  const accum = {
23600
23766
  blocks: [],
23601
23767
  textChunksByIndex: /* @__PURE__ */ new Map(),
@@ -23615,166 +23781,173 @@ async function runResponsesStreamLoop(stream, context, opts, options) {
23615
23781
  });
23616
23782
  activeTextIndex = null;
23617
23783
  };
23618
- try {
23619
- for await (const evt of sseStream) {
23620
- const data = evt?.data;
23621
- if (data == null) continue;
23622
- if (data === "[DONE]") break;
23623
- let ev;
23624
- try {
23625
- ev = JSON.parse(data);
23626
- } catch {
23627
- continue;
23628
- }
23629
- switch (ev.type) {
23630
- case "response.output_text.delta": {
23631
- const delta = ev.delta;
23632
- if (typeof delta !== "string" || delta.length === 0) break;
23633
- if (activeTextIndex == null) {
23634
- activeTextIndex = nextContentIndex++;
23635
- accum.blocks.push({
23636
- kind: "text",
23637
- contentIndex: activeTextIndex
23638
- });
23639
- accum.textChunksByIndex.set(activeTextIndex, []);
23640
- stream.push({
23641
- type: "text_start",
23642
- contentIndex: activeTextIndex,
23643
- partial: buildPartial(resolved, accum)
23644
- });
23645
- }
23646
- accum.textChunksByIndex.get(activeTextIndex).push(delta);
23784
+ for await (const evt of sseStream) {
23785
+ const data = evt?.data;
23786
+ if (data == null) continue;
23787
+ if (data === "[DONE]") break;
23788
+ let ev;
23789
+ try {
23790
+ ev = JSON.parse(data);
23791
+ } catch {
23792
+ continue;
23793
+ }
23794
+ switch (ev.type) {
23795
+ case "response.output_text.delta": {
23796
+ const delta = ev.delta;
23797
+ if (typeof delta !== "string" || delta.length === 0) break;
23798
+ if (activeTextIndex == null) {
23799
+ state$1.emitted = true;
23800
+ activeTextIndex = nextContentIndex++;
23801
+ accum.blocks.push({
23802
+ kind: "text",
23803
+ contentIndex: activeTextIndex
23804
+ });
23805
+ accum.textChunksByIndex.set(activeTextIndex, []);
23806
+ if (!state$1.active) return;
23647
23807
  stream.push({
23648
- type: "text_delta",
23808
+ type: "text_start",
23649
23809
  contentIndex: activeTextIndex,
23650
- delta,
23651
23810
  partial: buildPartial(resolved, accum)
23652
23811
  });
23653
- break;
23654
23812
  }
23655
- case "response.output_text.done":
23656
- if (activeTextIndex == null && typeof ev.text === "string" && ev.text.length > 0) {
23657
- activeTextIndex = nextContentIndex++;
23658
- accum.blocks.push({
23659
- kind: "text",
23660
- contentIndex: activeTextIndex
23661
- });
23662
- accum.textChunksByIndex.set(activeTextIndex, []);
23663
- stream.push({
23664
- type: "text_start",
23665
- contentIndex: activeTextIndex,
23666
- partial: buildPartial(resolved, accum)
23667
- });
23668
- accum.textChunksByIndex.get(activeTextIndex).push(ev.text);
23669
- stream.push({
23670
- type: "text_delta",
23671
- contentIndex: activeTextIndex,
23672
- delta: ev.text,
23673
- partial: buildPartial(resolved, accum)
23674
- });
23675
- }
23676
- closeActiveText();
23677
- break;
23678
- case "response.output_item.added": {
23679
- const item = ev.item;
23680
- if (item?.type !== "function_call") break;
23681
- const key = responsesToolKey(ev.output_index, item.id);
23682
- if (key == null) break;
23683
- if (toolPiIndexByKey.has(key)) break;
23684
- closeActiveText();
23685
- const piIdx = nextContentIndex++;
23686
- toolPiIndexByKey.set(key, piIdx);
23813
+ accum.textChunksByIndex.get(activeTextIndex).push(delta);
23814
+ if (!state$1.active) return;
23815
+ stream.push({
23816
+ type: "text_delta",
23817
+ contentIndex: activeTextIndex,
23818
+ delta,
23819
+ partial: buildPartial(resolved, accum)
23820
+ });
23821
+ break;
23822
+ }
23823
+ case "response.output_text.done":
23824
+ if (activeTextIndex == null && typeof ev.text === "string" && ev.text.length > 0) {
23825
+ state$1.emitted = true;
23826
+ activeTextIndex = nextContentIndex++;
23687
23827
  accum.blocks.push({
23688
- kind: "tool",
23689
- contentIndex: piIdx,
23690
- openaiIndex: piIdx
23691
- });
23692
- accum.toolByIndex.set(piIdx, {
23693
- id: item.call_id ?? item.id ?? key,
23694
- name: item.name ?? "",
23695
- argumentChunks: []
23696
- });
23697
- stream.push({
23698
- type: "toolcall_start",
23699
- contentIndex: piIdx,
23700
- partial: buildPartial(resolved, accum)
23828
+ kind: "text",
23829
+ contentIndex: activeTextIndex
23701
23830
  });
23702
- break;
23703
- }
23704
- case "response.function_call_arguments.delta": {
23705
- const key = responsesToolKey(ev.output_index, ev.item_id);
23706
- if (key == null) break;
23707
- const piIdx = toolPiIndexByKey.get(key);
23708
- if (piIdx == null) break;
23709
- const entry = accum.toolByIndex.get(piIdx);
23710
- if (!entry) break;
23711
- const delta = ev.delta;
23712
- if (typeof delta !== "string" || delta.length === 0) break;
23713
- entry.argumentChunks.push(delta);
23831
+ accum.textChunksByIndex.set(activeTextIndex, []);
23832
+ if (!state$1.active) return;
23714
23833
  stream.push({
23715
- type: "toolcall_delta",
23716
- contentIndex: piIdx,
23717
- delta,
23834
+ type: "text_start",
23835
+ contentIndex: activeTextIndex,
23718
23836
  partial: buildPartial(resolved, accum)
23719
23837
  });
23720
- break;
23721
- }
23722
- case "response.function_call_arguments.done": {
23723
- const key = responsesToolKey(ev.output_index, ev.item_id);
23724
- if (key == null) break;
23725
- const piIdx = toolPiIndexByKey.get(key);
23726
- if (piIdx == null) break;
23727
- const entry = accum.toolByIndex.get(piIdx);
23728
- if (entry && typeof ev.arguments === "string") entry.argumentChunks = [ev.arguments];
23729
- break;
23730
- }
23731
- case "response.output_item.done": {
23732
- const item = ev.item;
23733
- if (item?.type !== "function_call") break;
23734
- const key = responsesToolKey(ev.output_index, item.id);
23735
- if (key == null) break;
23736
- const piIdx = toolPiIndexByKey.get(key);
23737
- if (piIdx == null) break;
23738
- const entry = accum.toolByIndex.get(piIdx);
23739
- if (!entry) break;
23740
- if (item.call_id) entry.id = item.call_id;
23741
- if (item.name) entry.name = item.name;
23742
- if (typeof item.arguments === "string") entry.argumentChunks = [item.arguments];
23838
+ accum.textChunksByIndex.get(activeTextIndex).push(ev.text);
23839
+ if (!state$1.active) return;
23743
23840
  stream.push({
23744
- type: "toolcall_end",
23745
- contentIndex: piIdx,
23746
- toolCall: makePiToolCall(entry),
23841
+ type: "text_delta",
23842
+ contentIndex: activeTextIndex,
23843
+ delta: ev.text,
23747
23844
  partial: buildPartial(resolved, accum)
23748
23845
  });
23749
- closedToolItems.add(piIdx);
23750
- break;
23751
23846
  }
23752
- case "response.completed":
23753
- case "response.incomplete":
23754
- accum.usage = mapResponsesUsage(ev.response?.usage);
23755
- if (ev.type === "response.incomplete" && ev.response?.incomplete_details?.reason === "max_output_tokens") accum.finishReason = "length";
23756
- if (opts.onChunk && accum.usage) try {
23757
- opts.onChunk({
23758
- id: "",
23759
- object: "chat.completion.chunk",
23760
- created: 0,
23761
- model: resolved.modelId,
23762
- choices: [],
23763
- usage: accum.usage
23764
- });
23765
- } catch {}
23766
- break;
23767
- case "response.failed":
23768
- closeActiveText();
23769
- pushTerminalError(stream, resolved, new Error(ev.response?.error?.message ?? "response.failed"));
23770
- return;
23771
- default: break;
23847
+ closeActiveText();
23848
+ break;
23849
+ case "response.output_item.added": {
23850
+ const item = ev.item;
23851
+ if (item?.type !== "function_call") break;
23852
+ const key = responsesToolKey(ev.output_index, item.id);
23853
+ if (key == null) break;
23854
+ if (toolPiIndexByKey.has(key)) break;
23855
+ closeActiveText();
23856
+ state$1.emitted = true;
23857
+ const piIdx = nextContentIndex++;
23858
+ toolPiIndexByKey.set(key, piIdx);
23859
+ accum.blocks.push({
23860
+ kind: "tool",
23861
+ contentIndex: piIdx,
23862
+ openaiIndex: piIdx
23863
+ });
23864
+ accum.toolByIndex.set(piIdx, {
23865
+ id: item.call_id ?? item.id ?? key,
23866
+ name: item.name ?? "",
23867
+ argumentChunks: []
23868
+ });
23869
+ if (!state$1.active) return;
23870
+ stream.push({
23871
+ type: "toolcall_start",
23872
+ contentIndex: piIdx,
23873
+ partial: buildPartial(resolved, accum)
23874
+ });
23875
+ break;
23876
+ }
23877
+ case "response.function_call_arguments.delta": {
23878
+ const key = responsesToolKey(ev.output_index, ev.item_id);
23879
+ if (key == null) break;
23880
+ const piIdx = toolPiIndexByKey.get(key);
23881
+ if (piIdx == null) break;
23882
+ const entry = accum.toolByIndex.get(piIdx);
23883
+ if (!entry) break;
23884
+ const delta = ev.delta;
23885
+ if (typeof delta !== "string" || delta.length === 0) break;
23886
+ entry.argumentChunks.push(delta);
23887
+ if (!state$1.active) return;
23888
+ stream.push({
23889
+ type: "toolcall_delta",
23890
+ contentIndex: piIdx,
23891
+ delta,
23892
+ partial: buildPartial(resolved, accum)
23893
+ });
23894
+ break;
23895
+ }
23896
+ case "response.function_call_arguments.done": {
23897
+ const key = responsesToolKey(ev.output_index, ev.item_id);
23898
+ if (key == null) break;
23899
+ const piIdx = toolPiIndexByKey.get(key);
23900
+ if (piIdx == null) break;
23901
+ const entry = accum.toolByIndex.get(piIdx);
23902
+ if (entry && typeof ev.arguments === "string") entry.argumentChunks = [ev.arguments];
23903
+ break;
23904
+ }
23905
+ case "response.output_item.done": {
23906
+ const item = ev.item;
23907
+ if (item?.type !== "function_call") break;
23908
+ const key = responsesToolKey(ev.output_index, item.id);
23909
+ if (key == null) break;
23910
+ const piIdx = toolPiIndexByKey.get(key);
23911
+ if (piIdx == null) break;
23912
+ const entry = accum.toolByIndex.get(piIdx);
23913
+ if (!entry) break;
23914
+ if (item.call_id) entry.id = item.call_id;
23915
+ if (item.name) entry.name = item.name;
23916
+ if (typeof item.arguments === "string") entry.argumentChunks = [item.arguments];
23917
+ if (!state$1.active) return;
23918
+ stream.push({
23919
+ type: "toolcall_end",
23920
+ contentIndex: piIdx,
23921
+ toolCall: makePiToolCall(entry),
23922
+ partial: buildPartial(resolved, accum)
23923
+ });
23924
+ closedToolItems.add(piIdx);
23925
+ break;
23772
23926
  }
23927
+ case "response.completed":
23928
+ case "response.incomplete":
23929
+ accum.usage = mapResponsesUsage(ev.response?.usage);
23930
+ if (ev.type === "response.incomplete" && ev.response?.incomplete_details?.reason === "max_output_tokens") accum.finishReason = "length";
23931
+ if (opts.onChunk && accum.usage) try {
23932
+ opts.onChunk({
23933
+ id: "",
23934
+ object: "chat.completion.chunk",
23935
+ created: 0,
23936
+ model: resolved.modelId,
23937
+ choices: [],
23938
+ usage: accum.usage
23939
+ });
23940
+ } catch {}
23941
+ break;
23942
+ case "response.failed":
23943
+ if (!state$1.active) return;
23944
+ closeActiveText();
23945
+ pushTerminalError(stream, resolved, new Error(ev.response?.error?.message ?? "response.failed"));
23946
+ return;
23947
+ default: break;
23773
23948
  }
23774
- } catch (err) {
23775
- pushTerminalError(stream, resolved, err);
23776
- return;
23777
23949
  }
23950
+ if (!state$1.active) return;
23778
23951
  closeActiveText();
23779
23952
  for (const block of accum.blocks) {
23780
23953
  if (block.kind !== "tool") continue;
@@ -24092,6 +24265,23 @@ function fieldBytes(v) {
24092
24265
  * the engine marks the result isError). No upstream call is made — this
24093
24266
  * replaces an opaque upstream 4xx with an actionable, sanitized message.
24094
24267
  */
24268
+ function pushModelCallTimeoutDiagnostic(stream, resolved, timeoutMs, emitted) {
24269
+ const text = `Worker model call timed out after ${timeoutMs}ms${emitted ? " after emitting partial output" : " after two attempts with no output"}. The upstream stream stopped making progress. Retry the worker call; if it repeats, choose a different model.`;
24270
+ const final = {
24271
+ ...makeBaseMessage(resolved),
24272
+ content: [{
24273
+ type: "text",
24274
+ text
24275
+ }],
24276
+ stopReason: "error",
24277
+ errorMessage: "worker model-call deadline exceeded"
24278
+ };
24279
+ stream.push({
24280
+ type: "error",
24281
+ reason: "error",
24282
+ error: final
24283
+ });
24284
+ }
24095
24285
  function pushBackstopDiagnostic(stream, resolved, assembledTokens, limitTokens) {
24096
24286
  const text = `Request too large: the assembled input is ~${assembledTokens} tokens, over the ~${limitTokens}-token budget for ${resolved.modelId}. The run was stopped before an overflow error. Retry with a narrower task — target a specific section / file / element rather than reading everything at once.`;
24097
24287
  const final = {
@@ -25143,7 +25333,7 @@ function shimDefaultsToXhigh(id) {
25143
25333
  * ALL THREE peer models the consensus protocol needs:
25144
25334
  * - an OpenAI frontier model (`gpt-5.6-sol`, else `gpt-5.5` — see
25145
25335
  * `resolveOpenAiFrontier`)
25146
- * - `claude-opus-4-7` (opus_critic's model)
25336
+ * - `claude-opus-5` (stand_in's Anthropic slot)
25147
25337
  * - any `gemini-3.X.*pro` (gemini_critic's model family — matches the
25148
25338
  * same regex `geminiAvailable()` uses, so the gate stays in sync if
25149
25339
  * the GA slug renames `gemini-3.1-pro-preview` → `gemini-3.1-pro`)
@@ -25152,11 +25342,8 @@ function shimDefaultsToXhigh(id) {
25152
25342
  * fails `tools/call` with -32601 (mirroring the `worker` capability's
25153
25343
  * defense-in-depth pattern — the gated tool is functionally invisible).
25154
25344
  *
25155
- * Tier-mismatch on `claude-opus-4-7`: the proxy's `resolveModel` will
25156
- * fuzzy-match `claude-opus-4-7` to `claude-opus-4.7` (Copilot's dotted
25157
- * slug). For the catalog probe we use the Anthropic-published dashed
25158
- * slug too — `state.models?.data` mirrors Copilot's catalog where these
25159
- * land under the dotted slug, so we match by Copilot's actual id shape.
25345
+ * `claude-opus-5` is a single-segment slug (dotted == dashed), so the
25346
+ * catalog probe matches Copilot's actual id shape directly.
25160
25347
  */
25161
25348
  function geminiAvailable(source = state) {
25162
25349
  const models = source.models?.data;
@@ -25183,7 +25370,7 @@ function standInToolEnabled() {
25183
25370
  const models = state.models?.data;
25184
25371
  if (!models) return false;
25185
25372
  const hasOpenAi = resolveOpenAiFrontier() != null;
25186
- const hasOpus = models.some((m) => m.id === "claude-opus-4-7" || m.id === "claude-opus-4.7");
25373
+ const hasOpus = models.some((m) => m.id === "claude-opus-5");
25187
25374
  const hasGeminiPro = geminiAvailable();
25188
25375
  return hasOpenAi && hasOpus && hasGeminiPro;
25189
25376
  }
@@ -25461,27 +25648,42 @@ function checkAuth(c) {
25461
25648
  return { ok: true };
25462
25649
  }
25463
25650
  /**
25464
- * The 1M-context Opus 4.6 variant (`claude-opus-4.6-1m`, `max_prompt_tokens`
25465
- * 936K). opus_critic prefers it so it can take large artifacts in one shot
25466
- * (the whole point of pairing it with gpt-5.6-sol as the big-window peers);
25467
- * falls back to the 200K `claude-opus-4-6` when the catalog doesn't carry
25468
- * a 1M 4.6 slug. The regex is version-anchored to 4.6 AND requires a
25469
- * `-1m` suffix boundary (not a permissive `.*1m`), so it does NOT
25470
- * false-positive on `claude-opus-4.7-1m-internal` (stand_in's pinned
25471
- * 4.7 row), `claude-opus-4.6-1max` (hypothetical), or `claude-opus-4.8`
25472
- * (1M-without-sibling). Tolerates dotted (`opus-4.6-1m`) and dashed
25473
- * (`opus-4-6-1m`) catalog separators.
25651
+ * opus_critic's effective model, resolved against the live catalog.
25652
+ *
25653
+ * Prefers `claude-opus-5` a single-segment slug that is natively 1M
25654
+ * (no `-1m` sibling), so it takes large artifacts in one shot (the whole
25655
+ * point of pairing it with gpt-5.6-sol as the big-window peers). When opus-5
25656
+ * isn't in the catalog (e.g. a lesser tier), falls back to the older
25657
+ * 1M-context Opus 4.6 variant (`claude-opus-4.6-1m`, `max_prompt_tokens`
25658
+ * 936K), then to the 200K `claude-opus-4-6`. The 4.6 regex is
25659
+ * version-anchored AND requires a `-1m` suffix boundary (not a permissive
25660
+ * `.*1m`), so it does NOT false-positive on `claude-opus-4.7-1m-internal`,
25661
+ * `claude-opus-4.6-1max`, or a 1M-without-sibling base slug. Tolerates
25662
+ * dotted (`opus-4.6-1m`) and dashed (`opus-4-6-1m`) catalog separators.
25474
25663
  */
25475
25664
  const OPUS_1M_RE = /opus-4[.-]6-1m(?:$|-)/i;
25476
25665
  function resolveOpusCriticModel() {
25477
- const oneM = state.models?.data?.find((m) => OPUS_1M_RE.test(m.id));
25666
+ const models = state.models?.data;
25667
+ if (models?.some((m) => m.id === "claude-opus-5")) return "claude-opus-5";
25668
+ const oneM = models?.find((m) => OPUS_1M_RE.test(m.id));
25478
25669
  return oneM ? oneM.id : "claude-opus-4-6";
25479
25670
  }
25480
25671
  function activePersonas() {
25481
- return PERSONAS_READ.filter((p) => !p.requiresGeminiCatalog || geminiAvailable()).map((p) => p.toolNameHttp === "opus_critic" ? {
25482
- ...p,
25483
- model: resolveOpusCriticModel()
25484
- } : p);
25672
+ return PERSONAS_READ.filter((p) => !p.requiresGeminiCatalog || geminiAvailable()).map((p) => {
25673
+ if (p.toolNameHttp !== "opus_critic") return p;
25674
+ const model = resolveOpusCriticModel();
25675
+ const allowedEfforts = model === "claude-opus-5" ? [
25676
+ "low",
25677
+ "medium",
25678
+ "high",
25679
+ "xhigh"
25680
+ ] : p.allowedEfforts;
25681
+ return {
25682
+ ...p,
25683
+ model,
25684
+ allowedEfforts
25685
+ };
25686
+ });
25485
25687
  }
25486
25688
  function toolEntries(scope) {
25487
25689
  const personaEntries = scope === "all" || scope === "peers" ? activePersonas().map((p) => ({
@@ -25672,7 +25874,9 @@ async function predictedWindowOverflow(persona, prompt, context) {
25672
25874
  return;
25673
25875
  }
25674
25876
  if (tokens <= budget) return void 0;
25675
- const opusHint = OPUS_1M_RE.test(id) ? "" : " / `opus_critic` (Opus-4.7 1M ≈ 936K tokens, when the enterprise catalog carries it)";
25877
+ const criticModel = resolveOpusCriticModel();
25878
+ const criticIs1M = criticModel === "claude-opus-5" || OPUS_1M_RE.test(criticModel);
25879
+ const opusHint = id === "claude-opus-5" || OPUS_1M_RE.test(id) || !criticIs1M ? "" : " / `opus_critic` (Opus 5, 1M context ≈ 1M tokens)";
25676
25880
  return `pre-flight rejected: this ${persona.toolNameHttp} brief is ≈${tokens} tokens, over the ${budget}-token budget for ${persona.model} (its ${maxPromptTokens}-token prompt window minus a ${PEER_PROMPT_TOKEN_RESERVE}-token framing reserve). Do NOT summarize or truncate the artifact to fit. Route the full artifact to a larger-window peer — \`codex_critic\` (gpt-5.6-sol ≈ 1M tokens)${opusHint} — or split it into focused sub-calls BY CONCERN and call them in parallel, then aggregate.`;
25677
25881
  }
25678
25882
  /**
@@ -25732,7 +25936,7 @@ function jsonPathPreflightCap(body, scope) {
25732
25936
  * the `stand_in` orchestrator in `src/lib/stand-in.ts` — can reuse the
25733
25937
  * same per-endpoint request shaping without re-implementing it. The
25734
25938
  * stand_in tool needs to drive its own per-round system prompts across
25735
- * three concrete models (gpt-5.6-sol, claude-opus-4-7, gemini-3.1-pro-preview),
25939
+ * three concrete models (gpt-5.6-sol, claude-opus-5, gemini-3.1-pro-preview),
25736
25940
  * each on a different endpoint; doing that with a `PersonaSpec` would
25737
25941
  * require either inventing throwaway personas per round or duplicating
25738
25942
  * the dispatch switch.
@@ -28033,13 +28237,13 @@ function atomicWriteSync(absPath, contents) {
28033
28237
  throw err;
28034
28238
  }
28035
28239
  }
28036
- const READ_PARAMS = Type.Object({
28037
- path: Type.String({ description: "Workspace-relative or absolute path." }),
28038
- offset: Type.Optional(Type.Integer({
28240
+ const READ_PARAMS = Type$1.Object({
28241
+ path: Type$1.String({ description: "Workspace-relative or absolute path." }),
28242
+ offset: Type$1.Optional(Type$1.Integer({
28039
28243
  minimum: 0,
28040
28244
  description: "Line offset (0-indexed)."
28041
28245
  })),
28042
- limit: Type.Optional(Type.Integer({
28246
+ limit: Type$1.Optional(Type$1.Integer({
28043
28247
  minimum: 1,
28044
28248
  description: "Max lines to return."
28045
28249
  }))
@@ -28064,9 +28268,9 @@ function readTool(workspace) {
28064
28268
  }
28065
28269
  };
28066
28270
  }
28067
- const GLOB_PARAMS = Type.Object({
28068
- pattern: Type.String({ description: "ripgrep glob pattern, e.g. `src/**/*.ts`." }),
28069
- limit: Type.Optional(Type.Integer({
28271
+ const GLOB_PARAMS = Type$1.Object({
28272
+ pattern: Type$1.String({ description: "ripgrep glob pattern, e.g. `src/**/*.ts`." }),
28273
+ limit: Type$1.Optional(Type$1.Integer({
28070
28274
  minimum: 1,
28071
28275
  maximum: SEARCH_HARD_MAX
28072
28276
  }))
@@ -28093,11 +28297,11 @@ function globTool(workspace) {
28093
28297
  }
28094
28298
  };
28095
28299
  }
28096
- const GREP_PARAMS = Type.Object({
28097
- query: Type.String({ description: "Pattern to search for." }),
28098
- mode: Type.Optional(Type.Union([Type.Literal("literal"), Type.Literal("regex")], { description: "`literal` (default) = fixed-string; `regex` = PCRE2." })),
28099
- file_glob: Type.Optional(Type.String({ description: "ripgrep glob filter, e.g. `*.ts`." })),
28100
- limit: Type.Optional(Type.Integer({
28300
+ const GREP_PARAMS = Type$1.Object({
28301
+ query: Type$1.String({ description: "Pattern to search for." }),
28302
+ mode: Type$1.Optional(Type$1.Union([Type$1.Literal("literal"), Type$1.Literal("regex")], { description: "`literal` (default) = fixed-string; `regex` = PCRE2." })),
28303
+ file_glob: Type$1.Optional(Type$1.String({ description: "ripgrep glob filter, e.g. `*.ts`." })),
28304
+ limit: Type$1.Optional(Type$1.Integer({
28101
28305
  minimum: 1,
28102
28306
  maximum: SEARCH_HARD_MAX
28103
28307
  }))
@@ -28133,10 +28337,10 @@ function grepTool(workspace) {
28133
28337
  }
28134
28338
  };
28135
28339
  }
28136
- const EDIT_PARAMS = Type.Object({
28137
- path: Type.String({ description: "Workspace-relative or absolute path." }),
28138
- old_string: Type.String({ description: "Exact text to find. Must match exactly once; tool returns `not found` (0 matches) or `matches N times` (>1) without editing." }),
28139
- new_string: Type.String({ description: "Replacement text. May be empty (deletes old_string)." })
28340
+ const EDIT_PARAMS = Type$1.Object({
28341
+ path: Type$1.String({ description: "Workspace-relative or absolute path." }),
28342
+ old_string: Type$1.String({ description: "Exact text to find. Must match exactly once; tool returns `not found` (0 matches) or `matches N times` (>1) without editing." }),
28343
+ new_string: Type$1.String({ description: "Replacement text. May be empty (deletes old_string)." })
28140
28344
  });
28141
28345
  function editTool(workspace) {
28142
28346
  return {
@@ -28164,9 +28368,9 @@ function editTool(workspace) {
28164
28368
  }
28165
28369
  };
28166
28370
  }
28167
- const WRITE_PARAMS = Type.Object({
28168
- path: Type.String({ description: "Workspace-relative or absolute path." }),
28169
- contents: Type.String({ description: "Full file contents. Refused if >10 MiB." })
28371
+ const WRITE_PARAMS = Type$1.Object({
28372
+ path: Type$1.String({ description: "Workspace-relative or absolute path." }),
28373
+ contents: Type$1.String({ description: "Full file contents. Refused if >10 MiB." })
28170
28374
  });
28171
28375
  function writeTool(workspace) {
28172
28376
  return {
@@ -28182,9 +28386,9 @@ function writeTool(workspace) {
28182
28386
  }
28183
28387
  };
28184
28388
  }
28185
- const BASH_PARAMS = Type.Object({
28186
- cmd: Type.String({ description: "Shell command line." }),
28187
- timeout_ms: Type.Optional(Type.Integer({
28389
+ const BASH_PARAMS = Type$1.Object({
28390
+ cmd: Type$1.String({ description: "Shell command line." }),
28391
+ timeout_ms: Type$1.Optional(Type$1.Integer({
28188
28392
  minimum: 100,
28189
28393
  maximum: BASH_MAX_TIMEOUT_MS,
28190
28394
  description: `Per-call timeout (default ${BASH_DEFAULT_TIMEOUT_MS} ms).`
@@ -28194,7 +28398,7 @@ function bashTool(workspace) {
28194
28398
  return {
28195
28399
  name: "bash",
28196
28400
  label: "Run bash",
28197
- description: "Run a shell command in the worker's workspace under a strict env allowlist (credentials stripped) with a bounded timeout. Non-zero exit returns `<stdout>\\n<stderr>\\nexit=N` as text, not an error.",
28401
+ description: "Run a shell command in the worker's workspace under a strict env allowlist (credentials stripped) with a bounded timeout. Non-zero exit returns `<stdout>\\n<stderr>\\nexit=N` as text, not an error. Prefer the dedicated tools (read/glob/grep/code_search/edit/write/toolbelt) for reading, searching, and editing; reserve bash for builds, tests, git, package managers, and running programs — don't reimplement a file read/search/edit as a python/node/powershell one-off script.",
28198
28402
  parameters: BASH_PARAMS,
28199
28403
  executionMode: "sequential",
28200
28404
  async execute(_toolCallId, params, signal) {
@@ -28217,7 +28421,7 @@ function bashTool(workspace) {
28217
28421
  }
28218
28422
  };
28219
28423
  }
28220
- const WEB_SEARCH_PARAMS = Type.Object({ query: Type.String({ description: "Natural-language search query." }) });
28424
+ const WEB_SEARCH_PARAMS = Type$1.Object({ query: Type$1.String({ description: "Natural-language search query." }) });
28221
28425
  function webSearchTool() {
28222
28426
  return {
28223
28427
  name: "web_search",
@@ -28233,12 +28437,12 @@ function webSearchTool() {
28233
28437
  }
28234
28438
  };
28235
28439
  }
28236
- const FETCH_URL_PARAMS = Type.Object({ url: Type.String({ description: "Absolute URL (http/https only)." }) });
28440
+ const FETCH_URL_PARAMS = Type$1.Object({ url: Type$1.String({ description: "Absolute URL (http/https only)." }) });
28237
28441
  function fetchUrlTool() {
28238
28442
  return {
28239
28443
  name: "fetch_url",
28240
28444
  label: "Fetch URL",
28241
- description: "Fetch a URL (HTTP/HTTPS only) and return the response body as text. Bounded to 1 MiB and 30 s. No HTML→markdown conversion — callers that need it should ask `peer_review` to parse.",
28445
+ description: "Fetch a URL (HTTP/HTTPS only) and return the response body as text. Bounded to 1 MiB and 30 s. No HTML→markdown conversion.",
28242
28446
  parameters: FETCH_URL_PARAMS,
28243
28447
  async execute(_toolCallId, params, signal) {
28244
28448
  if (networkDisabled()) throw new Error("rejected: network disabled");
@@ -28287,26 +28491,26 @@ function fetchUrlTool() {
28287
28491
  }
28288
28492
  };
28289
28493
  }
28290
- const CODE_SEARCH_PARAMS = Type.Object({
28291
- query: Type.String({ description: "Search text. Natural-language intent in the default `semantic` mode; a literal string in `lexical`/`exact`; a PCRE2 regex in `regex`." }),
28292
- mode: Type.Optional(Type.Union([
28293
- Type.Literal("semantic"),
28294
- Type.Literal("lexical"),
28295
- Type.Literal("exact"),
28296
- Type.Literal("regex"),
28297
- Type.Literal("ast")
28494
+ const CODE_SEARCH_PARAMS = Type$1.Object({
28495
+ query: Type$1.String({ description: "Search text. Natural-language intent in the default `semantic` mode; a literal string in `lexical`/`exact`; a PCRE2 regex in `regex`." }),
28496
+ mode: Type$1.Optional(Type$1.Union([
28497
+ Type$1.Literal("semantic"),
28498
+ Type$1.Literal("lexical"),
28499
+ Type$1.Literal("exact"),
28500
+ Type$1.Literal("regex"),
28501
+ Type$1.Literal("ast")
28298
28502
  ], { description: "Search mode. `semantic` (DEFAULT): ColBERT meaning-based ranking, falls back to lexical when the index isn't ready (response `source` says which engine ran). `lexical`: BM25F + tree-sitter (best for exact symbols). `exact`: fixed-string. `regex`: PCRE2. `ast`: ast-grep structural (needs `ast_pattern` + `ast_lang`)." })),
28299
- pattern: Type.Optional(Type.String({ description: "Semantic mode only: regex pre-filter (colgrep -e) — grep first, then rank semantically. Ignored in lexical modes." })),
28300
- file_glob: Type.Optional(Type.String({ description: "ripgrep glob filter." })),
28301
- limit: Type.Optional(Type.Integer({
28503
+ pattern: Type$1.Optional(Type$1.String({ description: "Semantic mode only: regex pre-filter (colgrep -e) — grep first, then rank semantically. Ignored in lexical modes." })),
28504
+ file_glob: Type$1.Optional(Type$1.String({ description: "ripgrep glob filter." })),
28505
+ limit: Type$1.Optional(Type$1.Integer({
28302
28506
  minimum: 1,
28303
28507
  description: "Max hits to return."
28304
28508
  })),
28305
- structural: Type.Optional(Type.Union([Type.Literal("full"), Type.Literal("topN")], { description: "Structural-ranking depth (lexical mode only)." })),
28306
- complete: Type.Optional(Type.Boolean({ description: "Lexical mode: when true, return the COMPLETE match set (every line ripgrep would find, capped only by `limit`) — disables the default precision shoulder cut + per-file cap. Use it when you must not miss any occurrence (every caller of X, a rename, an audit). The default response `notice` says when matches were hidden." })),
28307
- multiline: Type.Optional(Type.Boolean({ description: "Set true with mode:'regex' to let a pattern span newlines (ripgrep -U), e.g. 'foo[\\s\\S]*?bar' across lines. (literal/lexical queries can't contain a newline.)" })),
28308
- ast_pattern: Type.Optional(Type.String({ description: "mode:'ast' structural pattern (e.g. 'function $F($$$) { $$$ }'). Matches come from ast-grep instead of ripgrep — for multi-line AST shapes the regex modes can't express. Takes precedence over `query`. REQUIRES `ast_lang`. If ast-grep isn't installed you get a `notice`; it never falls back to regex." })),
28309
- ast_lang: Type.Optional(Type.String({ description: "Language grammar for `ast_pattern` (REQUIRED with it): 'ts' | 'tsx' | 'js' | 'py' | 'rust' | 'go' | … Without it ast-grep cross-matches every language and returns garbage." }))
28509
+ structural: Type$1.Optional(Type$1.Union([Type$1.Literal("full"), Type$1.Literal("topN")], { description: "Structural-ranking depth (lexical mode only)." })),
28510
+ complete: Type$1.Optional(Type$1.Boolean({ description: "Lexical mode: when true, return the COMPLETE match set (every line ripgrep would find, capped only by `limit`) — disables the default precision shoulder cut + per-file cap. Use it when you must not miss any occurrence (every caller of X, a rename, an audit). The default response `notice` says when matches were hidden." })),
28511
+ multiline: Type$1.Optional(Type$1.Boolean({ description: "Set true with mode:'regex' to let a pattern span newlines (ripgrep -U), e.g. 'foo[\\s\\S]*?bar' across lines. (literal/lexical queries can't contain a newline.)" })),
28512
+ ast_pattern: Type$1.Optional(Type$1.String({ description: "mode:'ast' structural pattern (e.g. 'function $F($$$) { $$$ }'). Matches come from ast-grep instead of ripgrep — for multi-line AST shapes the regex modes can't express. Takes precedence over `query`. REQUIRES `ast_lang`. If ast-grep isn't installed you get a `notice`; it never falls back to regex." })),
28513
+ ast_lang: Type$1.Optional(Type$1.String({ description: "Language grammar for `ast_pattern` (REQUIRED with it): 'ts' | 'tsx' | 'js' | 'py' | 'rust' | 'go' | … Without it ast-grep cross-matches every language and returns garbage." }))
28310
28514
  });
28311
28515
  function codeSearchTool(workspace) {
28312
28516
  return {
@@ -28465,9 +28669,9 @@ const GIT_DIFF_PRODUCING = new Set([
28465
28669
  "show",
28466
28670
  "diff"
28467
28671
  ]);
28468
- const TOOLBELT_PARAMS = Type.Object({
28469
- tool: Type.Union(TOOLBELT_TOOLS.map((t) => Type.Literal(t)), { description: "Which read-only analysis CLI to run: rg (ripgrep search), fd (file find), sg (ast-grep structural search), jq (JSON), yq (YAML/TOML/XML), gron (flatten JSON to greppable lines), scc (code stats: LOC + complexity), tokei (code stats), difft (difftastic structural diff), git (read-only subcommands only)." }),
28470
- args: Type.Optional(Type.Array(Type.String(), { description: "Arguments passed LITERALLY to the tool (no shell: no pipes, redirects, chaining, or glob expansion). For git, args[0] must be a read-only subcommand (log/show/diff/blame/ls-files/…)." }))
28672
+ const TOOLBELT_PARAMS = Type$1.Object({
28673
+ tool: Type$1.Union(TOOLBELT_TOOLS.map((t) => Type$1.Literal(t)), { description: "Which read-only analysis CLI to run: rg (ripgrep search), fd (file find), sg (ast-grep structural search), jq (JSON), yq (YAML/TOML/XML), gron (flatten JSON to greppable lines), scc (code stats: LOC + complexity), tokei (code stats), difft (difftastic structural diff), git (read-only subcommands only)." }),
28674
+ args: Type$1.Optional(Type$1.Array(Type$1.String(), { description: "Arguments passed LITERALLY to the tool (no shell: no pipes, redirects, chaining, or glob expansion). For git, args[0] must be a read-only subcommand (log/show/diff/blame/ls-files/…)." }))
28471
28675
  });
28472
28676
  /**
28473
28677
  * True iff `arg` triggers a denied flag. Long flags (`--foo`) match on the
@@ -28576,10 +28780,10 @@ function toolbeltTool(workspace) {
28576
28780
  };
28577
28781
  }
28578
28782
  const PEER_CRITIC_TUPLE = [
28579
- Type.Literal("codex_critic"),
28580
- Type.Literal("gemini_critic"),
28581
- Type.Literal("codex_reviewer"),
28582
- Type.Literal("opus_critic")
28783
+ Type$1.Literal("codex_critic"),
28784
+ Type$1.Literal("gemini_critic"),
28785
+ Type$1.Literal("codex_reviewer"),
28786
+ Type$1.Literal("opus_critic")
28583
28787
  ];
28584
28788
  /**
28585
28789
  * Critic names accepted by `peer_review.critic`. Exported for the
@@ -28587,17 +28791,17 @@ const PEER_CRITIC_TUPLE = [
28587
28791
  * against `PERSONAS_READ` from `~/lib/peer-mcp-personas`.
28588
28792
  */
28589
28793
  const PEER_CRITIC_NAMES = PEER_CRITIC_TUPLE.map((l) => l.const);
28590
- const PEER_EFFORT_UNION = Type.Union([
28591
- Type.Literal("low"),
28592
- Type.Literal("medium"),
28593
- Type.Literal("high"),
28594
- Type.Literal("xhigh")
28794
+ const PEER_EFFORT_UNION = Type$1.Union([
28795
+ Type$1.Literal("low"),
28796
+ Type$1.Literal("medium"),
28797
+ Type$1.Literal("high"),
28798
+ Type$1.Literal("xhigh")
28595
28799
  ], { description: "Reasoning depth. Per-critic allowedEfforts gate; out-of-band values are clamped to the critic's default." });
28596
- const PEER_REVIEW_PARAMS = Type.Object({
28597
- critic: Type.Union([...PEER_CRITIC_TUPLE], { description: "Critic tool name. One of " + PEER_CRITIC_NAMES.map((n) => `\`${n}\``).join(", ") + ". `gemini_critic` is only valid when gemini-3.x is in the Copilot catalog; otherwise the call is refused." }),
28598
- prompt: Type.String({ description: "The brief — artifact under review plus constraints. Pasted verbatim into the critic's user message." }),
28599
- context: Type.Optional(Type.String({ description: "Optional extra context concatenated to the brief." })),
28600
- effort: Type.Optional(PEER_EFFORT_UNION)
28800
+ const PEER_REVIEW_PARAMS = Type$1.Object({
28801
+ critic: Type$1.Union([...PEER_CRITIC_TUPLE], { description: "Critic tool name. One of " + PEER_CRITIC_NAMES.map((n) => `\`${n}\``).join(", ") + ". `gemini_critic` is only valid when gemini-3.x is in the Copilot catalog; otherwise the call is refused." }),
28802
+ prompt: Type$1.String({ description: "The brief — artifact under review plus constraints. Pasted verbatim into the critic's user message." }),
28803
+ context: Type$1.Optional(Type$1.String({ description: "Optional extra context concatenated to the brief." })),
28804
+ effort: Type$1.Optional(PEER_EFFORT_UNION)
28601
28805
  });
28602
28806
  function lookupPersona(critic) {
28603
28807
  const persona = PERSONAS_READ.find((p) => p.toolNameHttp === critic);
@@ -28618,10 +28822,10 @@ function lookupPersona(critic) {
28618
28822
  * `acquireInFlightSlot`, and `callPersona` keeps the slot accounting,
28619
28823
  * effort clamping, and isError-promotion semantics identical.
28620
28824
  */
28621
- const CODEX_REVIEW_PARAMS = Type.Object({
28622
- prompt: Type.String({ description: "The code-review brief — diff or single file under review plus constraints. Pasted verbatim into codex-reviewer's user message." }),
28623
- context: Type.Optional(Type.String({ description: "Optional extra context concatenated to the brief." })),
28624
- effort: Type.Optional(PEER_EFFORT_UNION)
28825
+ const CODEX_REVIEW_PARAMS = Type$1.Object({
28826
+ prompt: Type$1.String({ description: "The code-review brief — diff or single file under review plus constraints. Pasted verbatim into codex-reviewer's user message." }),
28827
+ context: Type$1.Optional(Type$1.String({ description: "Optional extra context concatenated to the brief." })),
28828
+ effort: Type$1.Optional(PEER_EFFORT_UNION)
28625
28829
  });
28626
28830
  function codexReviewTool() {
28627
28831
  return {
@@ -28632,7 +28836,7 @@ function codexReviewTool() {
28632
28836
  executionMode: "sequential",
28633
28837
  async execute(_toolCallId, params, signal) {
28634
28838
  if (networkDisabled()) throw new Error("rejected: network disabled");
28635
- const persona = lookupPersona("codex-reviewer");
28839
+ const persona = lookupPersona("codex_reviewer");
28636
28840
  const requested = params.effort;
28637
28841
  const effort = requested && persona.allowedEfforts.includes(requested) ? requested : persona.defaultEffort;
28638
28842
  const release = acquireInFlightSlot();
@@ -28655,7 +28859,7 @@ function geminiInCatalog() {
28655
28859
  if (!models) return false;
28656
28860
  return models.some((m) => /^gemini-3\..*pro/i.test(m.id));
28657
28861
  }
28658
- const ADVISOR_PARAMS = Type.Object({ concern: Type.String({
28862
+ const ADVISOR_PARAMS = Type$1.Object({ concern: Type$1.String({
28659
28863
  description: "What you want a second pair of eyes on — your current approach, the blocker you're stuck on, or the decision you're about to commit. Required: the advisor needs a focal point.",
28660
28864
  minLength: 1
28661
28865
  }) });
@@ -28770,22 +28974,22 @@ function advisorTool(getMessages) {
28770
28974
  }
28771
28975
  };
28772
28976
  }
28773
- const UPDATE_PLAN_PARAMS = Type.Object({
28774
- steps: Type.Array(Type.Object({
28775
- title: Type.String({
28977
+ const UPDATE_PLAN_PARAMS = Type$1.Object({
28978
+ steps: Type$1.Array(Type$1.Object({
28979
+ title: Type$1.String({
28776
28980
  minLength: 1,
28777
28981
  description: "Short imperative description of the step."
28778
28982
  }),
28779
- status: Type.Union([
28780
- Type.Literal("pending"),
28781
- Type.Literal("in_progress"),
28782
- Type.Literal("completed")
28983
+ status: Type$1.Union([
28984
+ Type$1.Literal("pending"),
28985
+ Type$1.Literal("in_progress"),
28986
+ Type$1.Literal("completed")
28783
28987
  ], { description: "Current status of this step." })
28784
28988
  }), {
28785
28989
  minItems: 1,
28786
28990
  description: "The FULL ordered plan. Each call replaces the previous plan, so always send every step (not just the changed one)."
28787
28991
  }),
28788
- explanation: Type.Optional(Type.String({ description: "Optional one-line note on what changed this update." }))
28992
+ explanation: Type$1.Optional(Type$1.String({ description: "Optional one-line note on what changed this update." }))
28789
28993
  });
28790
28994
  function createPlanState() {
28791
28995
  return { current: [] };
@@ -29380,15 +29584,21 @@ registerExitHandlers(WORKTREE_REGISTRY);
29380
29584
  * mode. */
29381
29585
  const DEFAULT_MODEL = "gpt-5.4-mini";
29382
29586
  const DEFAULT_THINKING = "xhigh";
29383
- /** Default model for the READ-ONLY `explore` mode. `claude-sonnet-5` at `xhigh`
29384
- * (via `DEFAULT_THINKING`) — a strong, NATIVE (no-shim) tool-caller for repo
29385
- * research. Native Claude models run as workers over `/chat/completions`, the
29386
- * same path proven by `PLAN_DEFAULT_MODEL` (claude-opus-4.8). Like `implement`'s
29387
- * gpt-5.6-sol this is NOT a `workerToolsEnabled` gate input — if absent (e.g. a
29388
- * non-enterprise tier) `explore` errors helpfully at call time rather than
29389
- * vanishing the whole worker surface. The caller (the main model) overrides
29390
- * BOTH the model and the reasoning per call via the `model` / `thinking` args. */
29391
- const EXPLORE_DEFAULT_MODEL = "claude-sonnet-5";
29587
+ /** Default model for the READ-ONLY `explore` mode. `gemini-3.6-flash` at `high`
29588
+ * (via `EXPLORE_DEFAULT_THINKING`; flash advertises no xhigh) — a fast, cheap,
29589
+ * 1M-context tool-caller for read-only repo research. Routes over
29590
+ * `/chat/completions` via the translation shim (the same proven path the
29591
+ * `review` worker uses for gemini). Like `implement`'s gpt-5.6-sol this is NOT a
29592
+ * `workerToolsEnabled` gate input if absent (e.g. a non-enterprise tier)
29593
+ * `explore` errors helpfully at call time rather than vanishing the whole worker
29594
+ * surface. The caller (the main model) overrides BOTH the model and the reasoning
29595
+ * per call via the `model` / `thinking` args — see the tier ladder (gpt-5.6-sol
29596
+ * heavy / gpt-5.6-terra moderate / gemini-3.6-flash light) in the MCP tool desc. */
29597
+ const EXPLORE_DEFAULT_MODEL = "gemini-3.6-flash";
29598
+ /** Default thinking for `explore`. `high` (flash has no xhigh); explicit rather
29599
+ * than inherited from `DEFAULT_THINKING` so the explore effort can't drift if the
29600
+ * shared fallback changes. */
29601
+ const EXPLORE_DEFAULT_THINKING = "high";
29392
29602
  /** Default model + thinking for the READ-ONLY `review` mode.
29393
29603
  * `gemini-3.1-pro-preview` at `xhigh` (clamped to `high` at call time — gemini
29394
29604
  * advertises no xhigh). DELIBERATELY DECORRELATED FROM THE IMPLEMENTER: bounded
@@ -29410,6 +29620,10 @@ const REVIEW_DEFAULT_THINKING = "xhigh";
29410
29620
  * autonomous implementation. An explicit `opts.model` still wins. */
29411
29621
  const IMPLEMENT_DEFAULT_MODEL = "gpt-5.6-sol";
29412
29622
  const IMPLEMENT_DEFAULT_THINKING = "xhigh";
29623
+ /** `test` starts with the same built-in pair as `implement`, but remains an
29624
+ * independent mode so either can be overridden without affecting the other. */
29625
+ const TEST_DEFAULT_MODEL = "gpt-5.6-sol";
29626
+ const TEST_DEFAULT_THINKING = "xhigh";
29413
29627
  /** Default model for `browse` mode. `gpt-5.4-mini` — the Gate-B-winning
29414
29628
  * browse model (small + fast enough to drive a tab at human pace, with
29415
29629
  * enough tool-calling discipline to terminate). This is DISTINCT from the
@@ -29429,14 +29643,53 @@ const BROWSE_DEFAULT_THINKING = "high";
29429
29643
  /** Default model + thinking for the read-only `plan` mode. `claude-opus-4.8`
29430
29644
  * at `xhigh` — planning is the highest-leverage read-only step (the plan
29431
29645
  * shapes everything downstream), so it gets the strongest reasoning model
29432
- * rather than the cheap `gemini-3.5-flash` explore default. Uses the DOTTED
29646
+ * rather than the lightweight `gemini-3.6-flash` explore default. Uses the DOTTED
29433
29647
  * Copilot catalog id (the worker resolver exact-matches `catalog.id`, it does
29434
- * NOT translate the Anthropic dashed slug). Falls back to a helpful
29435
- * unknown-model error at call time if opus-4.8 isn't in the catalog (e.g. a
29436
- * non-enterprise tier), exactly like `implement`'s `gpt-5.6-sol`. Caller's `model`
29437
- * arg still wins. */
29438
- const PLAN_DEFAULT_MODEL = "claude-opus-4.8";
29439
- const PLAN_DEFAULT_THINKING = "xhigh";
29648
+ * NOT translate the Anthropic dashed slug; `claude-opus-5` is a single-segment
29649
+ * slug so dotted == dashed). Falls back to a helpful unknown-model error at call
29650
+ * time if opus-5 isn't in the catalog (e.g. a non-enterprise tier), exactly like
29651
+ * `implement`'s `gpt-5.6-sol`. Caller's `model` arg still wins. */
29652
+ const PLAN_DEFAULT_MODEL = "claude-opus-5";
29653
+ const BUILT_IN_MODE_DEFAULTS = Object.freeze({
29654
+ explore: {
29655
+ model: EXPLORE_DEFAULT_MODEL,
29656
+ thinking: EXPLORE_DEFAULT_THINKING
29657
+ },
29658
+ review: {
29659
+ model: REVIEW_DEFAULT_MODEL,
29660
+ thinking: REVIEW_DEFAULT_THINKING
29661
+ },
29662
+ plan: {
29663
+ model: PLAN_DEFAULT_MODEL,
29664
+ thinking: "xhigh"
29665
+ },
29666
+ implement: {
29667
+ model: IMPLEMENT_DEFAULT_MODEL,
29668
+ thinking: IMPLEMENT_DEFAULT_THINKING
29669
+ },
29670
+ test: {
29671
+ model: TEST_DEFAULT_MODEL,
29672
+ thinking: TEST_DEFAULT_THINKING
29673
+ },
29674
+ browse: {
29675
+ model: BROWSE_DEFAULT_MODEL,
29676
+ thinking: BROWSE_DEFAULT_THINKING
29677
+ }
29678
+ });
29679
+ /** Resolve the effective mode ladder without changing the gate sentinel. */
29680
+ function resolveModeDefaults(mode, ignoreSessionDefaults = false) {
29681
+ const builtIn = BUILT_IN_MODE_DEFAULTS[mode] ?? {
29682
+ model: DEFAULT_MODEL,
29683
+ thinking: DEFAULT_THINKING
29684
+ };
29685
+ const override = ignoreSessionDefaults ? {} : getWorkerSessionDefault(mode);
29686
+ return {
29687
+ model: override.model ?? builtIn.model,
29688
+ thinking: override.thinking ?? builtIn.thinking,
29689
+ modelSource: override.model === void 0 ? "built-in" : "override",
29690
+ thinkingSource: override.thinking === void 0 ? "built-in" : "override"
29691
+ };
29692
+ }
29440
29693
  /**
29441
29694
  * `Model<any>` shim used to satisfy `Agent.initialState.model` typing.
29442
29695
  *
@@ -29450,6 +29703,7 @@ const PLAN_DEFAULT_THINKING = "xhigh";
29450
29703
  * diagnostics (e.g. error-message AssistantMessage's `model` field
29451
29704
  * if Pi ever inspects it) faithful to what the caller asked for.
29452
29705
  */
29706
+ let agentOptionsObserverForTests;
29453
29707
  function makeModelShim(modelId) {
29454
29708
  return {
29455
29709
  id: modelId,
@@ -29487,6 +29741,34 @@ function extractAssistantText(content) {
29487
29741
  for (const part of content) if (part.type === "text") out += part.text;
29488
29742
  return out;
29489
29743
  }
29744
+ const MAX_EMPTY_OUTPUT_NUDGES = 3;
29745
+ const EMPTY_OUTPUT_NUDGES = [
29746
+ "Summarize your findings so far.",
29747
+ "Your previous reply was empty. Provide the answer now in plain text.",
29748
+ "Reply with plain text only. Do not call any tool."
29749
+ ];
29750
+ /**
29751
+ * Resolve the per-run nudge cap. Zero explicitly disables nudging; malformed,
29752
+ * negative, and fractional values fall back to the default.
29753
+ */
29754
+ function resolveMaxEmptyOutputNudges() {
29755
+ const raw = process$1.env.GH_ROUTER_WORKER_MAX_NUDGES;
29756
+ if (raw === void 0 || raw === "") return MAX_EMPTY_OUTPUT_NUDGES;
29757
+ const parsed = Number(raw);
29758
+ if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) return MAX_EMPTY_OUTPUT_NUDGES;
29759
+ return parsed;
29760
+ }
29761
+ function emptyOutputNudge(attempt) {
29762
+ return EMPTY_OUTPUT_NUDGES[Math.min(attempt, EMPTY_OUTPUT_NUDGES.length) - 1];
29763
+ }
29764
+ /** True only for a clean, empty assistant stop with no pending tool calls. */
29765
+ function shouldNudgeForEmptyOutput(message) {
29766
+ if (message.role !== "assistant") return false;
29767
+ const assistant = message;
29768
+ if (assistant.stopReason !== "stop" || !Array.isArray(assistant.content)) return false;
29769
+ if (assistant.content.some((part) => part.type === "toolCall")) return false;
29770
+ return extractAssistantText(assistant.content).trim() === "";
29771
+ }
29490
29772
  /**
29491
29773
  * Trivial stub for the no-worktree path. `dir` is the workspace
29492
29774
  * itself; `finalize` returns an empty diff (the response text won't
@@ -29527,21 +29809,15 @@ async function runWorkerAgentOnce(opts) {
29527
29809
  isError: true
29528
29810
  };
29529
29811
  try {
29530
- const isBrowse = opts.mode === "browse";
29531
- const isPlan = opts.mode === "plan";
29532
- const isReview = opts.mode === "review";
29533
- const isWriteCapable = opts.mode === "implement" || opts.mode === "test";
29534
- const isExplore = opts.mode === "explore";
29535
- const defaultModel = isBrowse ? BROWSE_DEFAULT_MODEL : isPlan ? PLAN_DEFAULT_MODEL : isReview ? REVIEW_DEFAULT_MODEL : isWriteCapable ? IMPLEMENT_DEFAULT_MODEL : isExplore ? EXPLORE_DEFAULT_MODEL : DEFAULT_MODEL;
29536
- const defaultThinking = isBrowse ? BROWSE_DEFAULT_THINKING : isPlan ? PLAN_DEFAULT_THINKING : isReview ? REVIEW_DEFAULT_THINKING : isWriteCapable ? IMPLEMENT_DEFAULT_THINKING : DEFAULT_THINKING;
29537
29812
  const resolved = resolveModelAndThinking({
29538
- model: opts.model ?? defaultModel,
29539
- thinking: opts.thinking ?? defaultThinking
29813
+ model: opts.model,
29814
+ thinking: opts.thinking
29540
29815
  });
29541
29816
  if (!resolved.ok) return {
29542
29817
  text: resolved.error,
29543
29818
  isError: true
29544
29819
  };
29820
+ const isBrowse = opts.mode === "browse";
29545
29821
  const ctxBudget = makeContextBudget(resolved.contextWindow);
29546
29822
  const workspaceInput = opts.workspace ?? (isBrowse ? process$1.cwd() : void 0);
29547
29823
  if (workspaceInput === void 0) return {
@@ -29581,7 +29857,7 @@ async function runWorkerAgentOnce(opts) {
29581
29857
  getMessages,
29582
29858
  planState
29583
29859
  });
29584
- const agent = new Agent$1({
29860
+ const agentOptions = {
29585
29861
  initialState: {
29586
29862
  systemPrompt: systemPromptFor(opts.mode),
29587
29863
  model: makeModelShim(resolved.modelId),
@@ -29590,7 +29866,8 @@ async function runWorkerAgentOnce(opts) {
29590
29866
  },
29591
29867
  streamFn: createCopilotStreamFn({
29592
29868
  resolved,
29593
- contextBudget: ctxBudget
29869
+ contextBudget: ctxBudget,
29870
+ modelCallTimeoutMs: resolveWorkerModelCallTimeoutMs()
29594
29871
  }),
29595
29872
  toolExecution: "parallel",
29596
29873
  transformContext: async (messages) => {
@@ -29623,6 +29900,7 @@ async function runWorkerAgentOnce(opts) {
29623
29900
  if (a.trim()) terminalText = a;
29624
29901
  }
29625
29902
  },
29903
+ shouldStopAfterTurn: () => budget.hardStopReason !== null,
29626
29904
  afterToolCall: async (ctx) => {
29627
29905
  budget.recordToolBytes(ctx.result);
29628
29906
  if (ctxBudget) {
@@ -29633,15 +29911,32 @@ async function runWorkerAgentOnce(opts) {
29633
29911
  prepareNextTurn: async () => {
29634
29912
  budget.addTurn();
29635
29913
  }
29636
- });
29914
+ };
29915
+ agentOptionsObserverForTests?.(agentOptions);
29916
+ const agent = new Agent$1(agentOptions);
29637
29917
  agentHolder.agent = agent;
29638
- const abortHandler = () => agent?.abort();
29639
- if (opts.signal) if (opts.signal.aborted) agent.abort();
29640
- else opts.signal.addEventListener("abort", abortHandler, { once: true });
29918
+ const abortHandler = () => agent.abort();
29919
+ if (opts.signal) opts.signal.addEventListener("abort", abortHandler, { once: true });
29641
29920
  let finalText = "";
29642
29921
  let lastStopReason = null;
29922
+ let nudgeCount = 0;
29923
+ const maxEmptyOutputNudges = resolveMaxEmptyOutputNudges();
29643
29924
  let terminalText = null;
29644
29925
  const unsubscribe = agent.subscribe((event) => {
29926
+ if (event.type === "turn_end") {
29927
+ if (nudgeCount < maxEmptyOutputNudges && shouldNudgeForEmptyOutput(event.message)) {
29928
+ nudgeCount += 1;
29929
+ agent.followUp({
29930
+ role: "user",
29931
+ content: [{
29932
+ type: "text",
29933
+ text: emptyOutputNudge(nudgeCount)
29934
+ }],
29935
+ timestamp: Date.now()
29936
+ });
29937
+ }
29938
+ return;
29939
+ }
29645
29940
  if (event.type !== "message_end") return;
29646
29941
  const msg = event.message;
29647
29942
  if (typeof msg !== "object" || msg === null) return;
@@ -29652,11 +29947,14 @@ async function runWorkerAgentOnce(opts) {
29652
29947
  const sr = msg.stopReason;
29653
29948
  if (typeof sr === "string") lastStopReason = sr;
29654
29949
  });
29950
+ let wallClockExpired = false;
29655
29951
  const wallClockTimer = setTimeout(() => {
29656
- agent?.abort();
29952
+ wallClockExpired = true;
29953
+ agent.abort();
29657
29954
  }, budget.config.maxWallClockMs);
29658
29955
  wallClockTimer.unref?.();
29659
29956
  try {
29957
+ if (opts.signal?.aborted) throw new Error("[halted: cancelled]");
29660
29958
  await agent.prompt(opts.prompt);
29661
29959
  await agent.waitForIdle();
29662
29960
  let diff = "";
@@ -29669,12 +29967,26 @@ async function runWorkerAgentOnce(opts) {
29669
29967
  await ws.remove();
29670
29968
  } catch {}
29671
29969
  const text = isBrowse ? terminalText ?? finalText : diff ? `${finalText}\n\n${diff}` : finalText;
29672
- if (lastStopReason === "error") return {
29673
- text: [(terminalText ?? finalText).trim() || "Worker run failed before producing an answer — the model's input likely overflowed (a large tool result), or the upstream errored. Retry with a narrower task: target a specific section / file / element rather than reading everything at once.", diff].filter(Boolean).join("\n\n"),
29970
+ if (lastStopReason === "error" || lastStopReason === "aborted") {
29971
+ const diag = (terminalText ?? finalText).trim();
29972
+ let diagnostic;
29973
+ if (lastStopReason === "aborted") diagnostic = wallClockExpired ? "[halted: wallclock]" : "[halted: cancelled]";
29974
+ else diagnostic = diag || "Worker run failed before producing an answer — the model's input likely overflowed (a large tool result), or the upstream errored. Retry with a narrower task: target a specific section / file / element rather than reading everything at once.";
29975
+ return {
29976
+ text: lastStopReason === "aborted" ? [
29977
+ diag,
29978
+ diff,
29979
+ diagnostic
29980
+ ].filter(Boolean).join("\n\n") : [diagnostic, diff].filter(Boolean).join("\n\n"),
29981
+ isError: true
29982
+ };
29983
+ }
29984
+ if (budget.hardStopReason) return {
29985
+ text: [text, `[halted: ${budget.hardStopReason}]`].filter(Boolean).join("\n\n"),
29674
29986
  isError: true
29675
29987
  };
29676
29988
  if (!text.trim()) return {
29677
- text: `${NO_OUTPUT_PREFIX} (stopReason=${lastStopReason ?? "unknown"}, turns=${budget.turns}, elapsed=${budget.elapsedMs}ms)]`,
29989
+ text: `${NO_OUTPUT_PREFIX} after ${nudgeCount} nudges (stopReason=${lastStopReason ?? "unknown"}, turns=${budget.turns}, elapsed=${budget.elapsedMs}ms)]; retry with a different model via worker_defaults, or narrow/split the task.`,
29678
29990
  isError: true
29679
29991
  };
29680
29992
  return { text };
@@ -29708,42 +30020,21 @@ async function runWorkerAgentOnce(opts) {
29708
30020
  }
29709
30021
  /**
29710
30022
  * Prefix of the sentinel `runWorkerAgentOnce` returns when a worker stops
29711
- * CLEANLY but emits no usable text the model occasionally ends a turn right
29712
- * after a tool call without summarizing. Stable so the retry wrapper can detect
29713
- * exactly this case. Distinct from a budget cap (`WorkerAbort` → halt message),
29714
- * a stream error (`stopReason="error"` → overflow/upstream diagnostic), and a
29715
- * real failure — none of which carry this prefix, so none are retried.
30023
+ * cleanly but emits no usable text even after its bounded in-run nudges. Kept
30024
+ * stable for callers that recognize the existing sentinel shape.
29716
30025
  */
29717
30026
  const NO_OUTPUT_PREFIX = "[worker exited with no output";
29718
- /** True iff `r` is the transient no-output sentinel (a clean stop with empty
29719
- * text), the one case worth a fresh retry. Keyed on the specific sentinel
29720
- * PREFIX, not on `isError` so the retry can't be silently decoupled if the
29721
- * sentinel's error flag ever changes, and a real worker answer never begins
29722
- * with this string. */
29723
- function isTransientNoOutput(r) {
29724
- return typeof r.text === "string" && r.text.startsWith(NO_OUTPUT_PREFIX);
29725
- }
29726
- /**
29727
- * Run `runOnce`, and on the transient no-output sentinel retry EXACTLY ONCE with
29728
- * a fresh run before surfacing it. Real errors, budget caps, and stream errors
29729
- * are returned as-is (they have distinct, actionable messages and a retry would
29730
- * not help). A consumed abort signal short-circuits the retry. If the retry also
29731
- * produces no output, the ORIGINAL is returned (one is enough signal; the
29732
- * failure isn't hidden). Extracted + injected for unit-testability.
29733
- */
29734
- async function withNoOutputRetry(runOnce, opts) {
29735
- const first = await runOnce(opts);
29736
- if (!isTransientNoOutput(first) || opts.signal?.aborted) return first;
29737
- const second = await runOnce(opts);
29738
- return isTransientNoOutput(second) ? first : second;
30027
+ /** Public entry. Resolve model/thinking once, then run under one transcript and Budget. */
30028
+ function resolveWorkerRunOpts(opts) {
30029
+ const defaults = resolveModeDefaults(opts.mode, opts.ignoreSessionDefaults === true);
30030
+ return {
30031
+ ...opts,
30032
+ model: opts.model ?? defaults.model,
30033
+ thinking: opts.thinking ?? defaults.thinking
30034
+ };
29739
30035
  }
29740
- /**
29741
- * Public entry: a worker run with a single transient-no-output retry. Wraps the
29742
- * implementation (`runWorkerAgentOnce`); the signature is unchanged so every
29743
- * caller (MCP dispatch, the orchestration runner) gets the retry for free.
29744
- */
29745
30036
  async function runWorkerAgent(opts) {
29746
- return withNoOutputRetry(runWorkerAgentOnce, opts);
30037
+ return runWorkerAgentOnce(resolveWorkerRunOpts(opts));
29747
30038
  }
29748
30039
  /**
29749
30040
  * Test-only exports. The public surface of the engine is
@@ -29794,8 +30085,8 @@ const STAND_IN_MODELS = Object.freeze([
29794
30085
  effort: "xhigh"
29795
30086
  },
29796
30087
  {
29797
- key: "claude-opus-4-7",
29798
- model: "claude-opus-4-7",
30088
+ key: "claude-opus-5",
30089
+ model: "claude-opus-5",
29799
30090
  endpoint: "/v1/messages",
29800
30091
  effort: "xhigh"
29801
30092
  },
@@ -31891,7 +32182,8 @@ async function runWorkflowLive(opts) {
31891
32182
  mode,
31892
32183
  prompt,
31893
32184
  workspace,
31894
- signal: opts.signal
32185
+ signal: opts.signal,
32186
+ ignoreSessionDefaults: true
31895
32187
  });
31896
32188
  return {
31897
32189
  text: r.text,
@@ -32011,10 +32303,11 @@ function isMcpGroup(s) {
32011
32303
  * (handler.ts:handleToolsCallSSE). Claude Code's MCP HTTP client honors
32012
32304
  * `text/event-stream` responses without applying the ~60s per-tool-call
32013
32305
  * timer that previously broke xhigh on gpt-5.5 (~56s wall) and on
32014
- * Anthropic Opus families (high+ thinking budgets). opus-critic itself
32015
- * now runs on claude-opus-4-6 which doesn't advertise xhigh, so the
32016
- * SSE long-tail concern there is moot; the SSE machinery still applies
32017
- * to the other personas that do expose xhigh.
32306
+ * Anthropic Opus families (high+ thinking budgets). opus-critic caps its
32307
+ * exposed effort at `high` (its effective model can fall back to
32308
+ * claude-opus-4-6, which doesn't advertise xhigh), so the SSE long-tail
32309
+ * concern there is moot; the SSE machinery still applies to the other
32310
+ * personas that do expose xhigh.
32018
32311
  */
32019
32312
  const EFFORT_LEVELS = [
32020
32313
  "low",
@@ -32147,7 +32440,7 @@ Reply format (markdown):
32147
32440
 
32148
32441
  Resilience reminder:
32149
32442
  If your session terminates abnormally before "Status: complete", the lead will retry once. On recovery, ask the lead to confirm what's already been done before re-applying changes — duplicate edits are worse than a slow restart.`;
32150
- const OPUS_CRITIC_BASE = `You are opus-critic, a fresh-context same-lab adversarial reviewer running on Opus 4.6. The lead orchestrator that just delegated to you runs newer Opus-family context, but you are NOT the lead. You did not see the lead's reasoning trace. You only see the brief.
32443
+ const OPUS_CRITIC_BASE = `You are opus-critic, a fresh-context same-lab adversarial reviewer running on Opus 5. The lead orchestrator that just delegated to you runs Opus-family context too, but you are NOT the lead. You did not see the lead's reasoning trace. You only see the brief.
32151
32444
 
32152
32445
  Your job is to spot what the lead missed because of cognitive momentum, sunk-cost on a plan, or motivated reasoning toward a particular fix. Your blind-spot diversification is LIMITED compared to codex-critic (gpt-5.6-sol) and gemini-critic (gemini-3.1-pro), same lab, adjacent model family, related priors. Use that honestly: don't pretend to find a different perspective when the obvious read is "the lead got it right." Silence on good work is a valid and welcome answer.
32153
32446
 
@@ -32232,9 +32525,9 @@ const PERSONAS_READ = Object.freeze([
32232
32525
  {
32233
32526
  agentName: "opus-critic",
32234
32527
  toolNameHttp: "opus_critic",
32235
- model: "claude-opus-4-6",
32528
+ model: "claude-opus-5",
32236
32529
  endpoint: "/v1/messages",
32237
- description: "Adversarial same-lab critic backed by fresh-context Opus 4.6, with limited blind-spot diversity compared with cross-lab critics. It reviews plans, designs, or code tradeoffs for cognitive momentum, sunk-cost reasoning, and confabulated assumptions, then returns a calibrated objection or no material objection. Use when a same-family sanity check can catch lead-context drift or when comparing against codex_critic / gemini_critic findings. Not a substitute for cross-lab review on security-sensitive or high-risk changes; use codex_critic or gemini_critic for stronger diversity. On enterprise catalogs that carry Opus-4.6-1M it runs with ≈936K input tokens; otherwise ≈168K. Pinned two minors behind the default Opus so the panel spans more of the version curve. Pass artifact verbatim.",
32530
+ description: "Adversarial same-lab critic backed by fresh-context Opus 5, with limited blind-spot diversity compared with cross-lab critics. It reviews plans, designs, or code tradeoffs for cognitive momentum, sunk-cost reasoning, and confabulated assumptions, then returns a calibrated objection or no material objection. Use when a same-family sanity check can catch lead-context drift or when comparing against codex_critic / gemini_critic findings. Not a substitute for cross-lab review on security-sensitive or high-risk changes; use codex_critic or gemini_critic for stronger diversity. Runs with the full 1M-context Opus 5 window (native, no -1m sibling needed). Pass artifact verbatim.",
32238
32531
  baseInstructions: OPUS_CRITIC_BASE,
32239
32532
  agentPrompt: "",
32240
32533
  writeCapable: false,
@@ -32383,7 +32676,7 @@ function buildPeerAwarenessSnippet(opts) {
32383
32676
  criticList.push("`gemini_reviewer` (gemini-3.1-pro, line-level code review)");
32384
32677
  criticList.push("`gemini_critic` (gemini-3.1-pro)");
32385
32678
  }
32386
- criticList.push("`opus_critic` (Opus 4.6)");
32679
+ criticList.push("`opus_critic` (Opus 5)");
32387
32680
  const codexCliClause = opts.codexCli ? " `mcp__codex-cli__codex` dispatches to `codex-implementer` (gpt-5.3-codex with workspace-write) for end-to-end coding tasks." : "";
32388
32681
  const para2Parts = [`\`mcp__${searchKey}__code\` is the one-stop code search (no extra model call). Its DEFAULT mode (or \`mode:"semantic"\`) ranks by MEANING via ColBERT over a per-workspace index, the first thing to reach for on intent/concept questions ("where is retry/backoff handled", "how does auth work"); when that index isn't ready it transparently falls back to lexical (the response \`source\` says which engine ran). Forced modes cover the rest: \`lexical\` (BM25F-ranked + tree-sitter, best for exact symbols), \`exact\`, \`regex\`, \`complete\` (exhaustive set), \`ast_pattern\`+\`ast_lang\` for multi-line AST shapes, \`scan\` for a whole-workspace symbol outline, \`multiline\` for cross-line regex. Multiple queries can run in a single turn. The index covers code-shaped files; for unstructured files (logs, \`.csv\`, \`.env*\`, config-only wiring), \`grep\`/\`glob\` still apply.`];
32389
32682
  if (opts.workerToolsAvailable) para2Parts.push(`\`worker-*\` are background Agent subagents (subagent_type) that run the matching worker in its own context and deliver the result as a completion notification, so a long run never blocks the turn: \`worker-explore\` (read-only research), \`worker-review\` (reads the code to verify a change or claim), \`worker-plan\` (ordered implementation plan), \`worker-implement\` (edit/write/bash; ALWAYS runs in an isolated git worktree and returns the diff via a saved patch file; for in-place edits use the \`implementer\` subagent), \`worker-test\` (independent test author; also always worktree-isolated). The raw \`mcp__${workersKey}__*\` tools they call are guarded (a direct main-thread call is redirected to the matching agent); Workers themselves have \`code_search\`.`);
@@ -32460,6 +32753,15 @@ function formatWebSearchResult(results) {
32460
32753
  const refsLine = results.references.map((r) => `- [${r.title}](${r.url})`).join("\n");
32461
32754
  return `${results.content}\n\n## References\n${refsLine}`;
32462
32755
  }
32756
+ /**
32757
+ * Model-override tier ladder surfaced on the read-heavy workers
32758
+ * (explore / implement / review). The caller picks a model by task weight;
32759
+ * all three tiers are 1M-context, and `high` is the recommended reasoning
32760
+ * depth for the ladder (flash tops out at high; sol/terra go higher if the
32761
+ * caller wants). Appended to those tools' `model` param description so the
32762
+ * lead has actionable override guidance instead of a bare free string.
32763
+ */
32764
+ const WORKER_TIER_GUIDANCE = " Override by task weight: `gpt-5.6-sol` (heavy/deep), `gpt-5.6-terra` (moderate), `gemini-3.6-flash` (light/cheap) — all 1M context; pair with thinking:'high'.";
32463
32765
  const NON_PERSONA_MCP_TOOLS = Object.freeze([
32464
32766
  {
32465
32767
  toolNameHttp: "web",
@@ -32647,11 +32949,84 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32647
32949
  }
32648
32950
  }
32649
32951
  },
32952
+ {
32953
+ toolNameHttp: "worker_defaults",
32954
+ group: "workers",
32955
+ capability: "worker",
32956
+ description: "Sets or clears process-wide worker model/reasoning defaults and returns the full effective table. Omit arguments to inspect current values. Per-call worker arguments still take precedence; values are in-memory and apply to every client served by this process.",
32957
+ inputSchema: {
32958
+ type: "object",
32959
+ additionalProperties: false,
32960
+ properties: {
32961
+ mode: {
32962
+ type: "string",
32963
+ enum: WORKER_MODES,
32964
+ description: "Worker mode to set or clear."
32965
+ },
32966
+ model: {
32967
+ type: "string",
32968
+ description: "Copilot catalog model id to use by default for the mode."
32969
+ },
32970
+ thinking: {
32971
+ type: "string",
32972
+ enum: WORKER_THINKING_LEVELS,
32973
+ description: "Requested default reasoning level; clamped per run for the selected model."
32974
+ },
32975
+ clear: {
32976
+ type: "boolean",
32977
+ description: "When true, clears both overrides for the selected mode."
32978
+ },
32979
+ clearAll: {
32980
+ type: "boolean",
32981
+ description: "When true, clears overrides for every worker mode."
32982
+ }
32983
+ }
32984
+ },
32985
+ async handler(args) {
32986
+ const mode = typeof args.mode === "string" && WORKER_MODES.includes(args.mode) ? args.mode : void 0;
32987
+ const model = typeof args.model === "string" ? args.model : void 0;
32988
+ const thinking = typeof args.thinking === "string" && WORKER_THINKING_LEVELS.includes(args.thinking) ? args.thinking : void 0;
32989
+ const clear = args.clear === true;
32990
+ const clearAll = args.clearAll === true;
32991
+ if (args.mode !== void 0 && mode === void 0 || args.model !== void 0 && model === void 0 || args.thinking !== void 0 && thinking === void 0 || args.clear !== void 0 && typeof args.clear !== "boolean" || args.clearAll !== void 0 && typeof args.clearAll !== "boolean" || clearAll && (mode !== void 0 || model !== void 0 || thinking !== void 0 || args.clear !== void 0) || clear && (mode === void 0 || model !== void 0 || thinking !== void 0) || !clearAll && !clear && (model !== void 0 || thinking !== void 0) && mode === void 0) return {
32992
+ content: [{
32993
+ type: "text",
32994
+ text: "worker_defaults: use mode with model/thinking or clear:true; clearAll:true must stand alone"
32995
+ }],
32996
+ isError: true
32997
+ };
32998
+ if (clearAll) resetAllWorkerSessionDefaults();
32999
+ else if (clear && mode) resetWorkerSessionDefault(mode);
33000
+ else if (mode && (model !== void 0 || thinking !== void 0)) {
33001
+ const current = resolveModeDefaults(mode);
33002
+ const validation = resolveModelAndThinking({
33003
+ model: model ?? current.model,
33004
+ thinking: thinking ?? current.thinking
33005
+ });
33006
+ if (!validation.ok) return {
33007
+ content: [{
33008
+ type: "text",
33009
+ text: validation.error
33010
+ }],
33011
+ isError: true
33012
+ };
33013
+ setWorkerSessionDefault(mode, {
33014
+ model,
33015
+ thinking
33016
+ });
33017
+ }
33018
+ const table = Object.fromEntries(WORKER_MODES.map((workerMode) => [workerMode, resolveModeDefaults(workerMode)]));
33019
+ return { content: [{
33020
+ type: "text",
33021
+ text: JSON.stringify(table)
33022
+ }] };
33023
+ }
33024
+ },
32650
33025
  {
32651
33026
  toolNameHttp: "explore",
32652
33027
  group: "workers",
32653
33028
  capability: "worker",
32654
- description: "Runs as the background `worker-explore` agent. Dispatch via the Agent tool (subagent_type: worker-explore) so the turn is never blocked; the result arrives as a completion notification. Read-only investigation by an autonomous worker (Pi runtime; default model `claude-sonnet-5` at xhigh reasoning, override via the `model` arg with any Copilot-catalog model that advertises `tool_calls`). It has read, glob, grep, semantic-first code search, web search, fetch_url, advisor, update_plan, and read-only toolbelt tools, and it returns a single text answer. Use for bounded research, repo discovery, dependency investigation, or multi-file reading that would otherwise consume the lead context window. Not for implementation, test authoring, or verification of a concrete diff; use implement, test, or review for those scopes. Brief the investigation goal and constraints, not step-by-step tool semantics.",
33029
+ description: "Runs as the background `worker-explore` agent. Dispatch via the Agent tool (subagent_type: worker-explore) so the turn is never blocked; the result arrives as a completion notification. Read-only investigation by an autonomous worker (Pi runtime; default model `gemini-3.6-flash` at high reasoning, override via the `model` arg with any Copilot-catalog model that advertises `tool_calls`). It has read, glob, grep, semantic-first code search, web search, fetch_url, advisor, update_plan, and read-only toolbelt tools, and it returns a single text answer. Use for bounded research, repo discovery, dependency investigation, or multi-file reading that would otherwise consume the lead context window. Not for implementation, test authoring, or verification of a concrete diff; use implement, test, or review for those scopes. Brief the investigation goal and constraints, not step-by-step tool semantics.",
32655
33030
  inputSchema: {
32656
33031
  type: "object",
32657
33032
  required: ["prompt"],
@@ -32663,19 +33038,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32663
33038
  },
32664
33039
  model: {
32665
33040
  type: "string",
32666
- description: "Optional Copilot catalog model id (defaults to claude-sonnet-5). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
33041
+ description: "Optional Copilot catalog model id (defaults to gemini-3.6-flash). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch." + WORKER_TIER_GUIDANCE
32667
33042
  },
32668
33043
  thinking: {
32669
33044
  type: "string",
32670
- enum: [
32671
- "off",
32672
- "minimal",
32673
- "low",
32674
- "medium",
32675
- "high",
32676
- "xhigh"
32677
- ],
32678
- description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
33045
+ enum: WORKER_THINKING_LEVELS,
33046
+ description: "Optional reasoning depth. Use worker_defaults to inspect the effective value. Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32679
33047
  },
32680
33048
  workspace: {
32681
33049
  type: "string",
@@ -32715,19 +33083,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32715
33083
  },
32716
33084
  model: {
32717
33085
  type: "string",
32718
- description: "Optional Copilot catalog model id (defaults to gpt-5.6-sol). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
33086
+ description: "Optional Copilot catalog model id (defaults to gpt-5.6-sol). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch." + WORKER_TIER_GUIDANCE
32719
33087
  },
32720
33088
  thinking: {
32721
33089
  type: "string",
32722
- enum: [
32723
- "off",
32724
- "minimal",
32725
- "low",
32726
- "medium",
32727
- "high",
32728
- "xhigh"
32729
- ],
32730
- description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
33090
+ enum: WORKER_THINKING_LEVELS,
33091
+ description: "Optional reasoning depth. Use worker_defaults to inspect the effective value. Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32731
33092
  },
32732
33093
  workspace: {
32733
33094
  type: "string",
@@ -32763,18 +33124,11 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32763
33124
  },
32764
33125
  model: {
32765
33126
  type: "string",
32766
- description: "Optional Copilot catalog model id (defaults to gemini-3.1-pro-preview). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
33127
+ description: "Optional Copilot catalog model id (defaults to gemini-3.1-pro-preview). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch." + WORKER_TIER_GUIDANCE
32767
33128
  },
32768
33129
  thinking: {
32769
33130
  type: "string",
32770
- enum: [
32771
- "off",
32772
- "minimal",
32773
- "low",
32774
- "medium",
32775
- "high",
32776
- "xhigh"
32777
- ],
33131
+ enum: WORKER_THINKING_LEVELS,
32778
33132
  description: "Optional reasoning depth (defaults to xhigh, clamped to high for the default review model). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32779
33133
  },
32780
33134
  workspace: {
@@ -32799,7 +33153,7 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32799
33153
  toolNameHttp: "plan",
32800
33154
  group: "workers",
32801
33155
  capability: "worker",
32802
- description: "Runs as the background `worker-plan` agent. Dispatch via the Agent tool (subagent_type: worker-plan) so the turn is never blocked; the result arrives as a completion notification. Read-only implementation planning by an autonomous worker (Pi runtime; default model `claude-opus-4.8` at xhigh reasoning, override via `model` with any Copilot-catalog model that advertises `tool_calls`). It has the same read-only toolset as explore and returns a concrete, ordered implementation plan covering files, approach, risks, and how acceptance criteria will be verified. Use before coding when the task needs repo-grounded sequencing or acceptance criteria translated into implementation steps. Not for editing files, running an implementation, writing tests, or adversarial review; use implement, test, or review for those scopes.",
33156
+ description: "Runs as the background `worker-plan` agent. Dispatch via the Agent tool (subagent_type: worker-plan) so the turn is never blocked; the result arrives as a completion notification. Read-only implementation planning by an autonomous worker (Pi runtime; default model `claude-opus-5` at xhigh reasoning, override via `model` with any Copilot-catalog model that advertises `tool_calls`). It has the same read-only toolset as explore and returns a concrete, ordered implementation plan covering files, approach, risks, and how acceptance criteria will be verified. Use before coding when the task needs repo-grounded sequencing or acceptance criteria translated into implementation steps. Not for editing files, running an implementation, writing tests, or adversarial review; use implement, test, or review for those scopes.",
32803
33157
  inputSchema: {
32804
33158
  type: "object",
32805
33159
  required: ["prompt"],
@@ -32811,19 +33165,12 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32811
33165
  },
32812
33166
  model: {
32813
33167
  type: "string",
32814
- description: "Optional Copilot catalog model id (defaults to claude-opus-4.8). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
33168
+ description: "Optional Copilot catalog model id (defaults to claude-opus-5). Must advertise tool_calls support; the engine emits an isError envelope listing the eligible catalog models on mismatch."
32815
33169
  },
32816
33170
  thinking: {
32817
33171
  type: "string",
32818
- enum: [
32819
- "off",
32820
- "minimal",
32821
- "low",
32822
- "medium",
32823
- "high",
32824
- "xhigh"
32825
- ],
32826
- description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
33172
+ enum: WORKER_THINKING_LEVELS,
33173
+ description: "Optional reasoning depth. Use worker_defaults to inspect the effective value. Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32827
33174
  },
32828
33175
  workspace: {
32829
33176
  type: "string",
@@ -32867,15 +33214,8 @@ const NON_PERSONA_MCP_TOOLS = Object.freeze([
32867
33214
  },
32868
33215
  thinking: {
32869
33216
  type: "string",
32870
- enum: [
32871
- "off",
32872
- "minimal",
32873
- "low",
32874
- "medium",
32875
- "high",
32876
- "xhigh"
32877
- ],
32878
- description: "Optional reasoning depth (default xhigh). Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
33217
+ enum: WORKER_THINKING_LEVELS,
33218
+ description: "Optional reasoning depth. Use worker_defaults to inspect the effective value. Silently clamped to the model's allowed range; \"off\" drops the parameter entirely."
32879
33219
  },
32880
33220
  workspace: {
32881
33221
  type: "string",
@@ -33248,14 +33588,7 @@ async function runWorkerToolCall(call) {
33248
33588
  isError: true
33249
33589
  };
33250
33590
  const thinkingRaw = args.thinking;
33251
- const ALLOWED_THINKING = [
33252
- "off",
33253
- "minimal",
33254
- "low",
33255
- "medium",
33256
- "high",
33257
- "xhigh"
33258
- ];
33591
+ const ALLOWED_THINKING = WORKER_THINKING_LEVELS;
33259
33592
  let thinking;
33260
33593
  if (thinkingRaw !== void 0) {
33261
33594
  if (typeof thinkingRaw !== "string" || !ALLOWED_THINKING.includes(thinkingRaw)) return {
@@ -33568,5 +33901,5 @@ function enumerateInjectedMcpToolNames(groupKeys, opts = {}) {
33568
33901
  }
33569
33902
 
33570
33903
  //#endregion
33571
- export { buildAdvisorStream as $, getPackageVersion as $t, trustRepo as A, readResponseBodyCapped as At, runWorkerAgent as B, ArtifactClient as Bt, fileLastPromptStore as C, getTokenCount as Ct, repoRoot as D, createResponses as Dt, repoFingerprint as E, pickEndpoint as Et, EXPLORE_DEFAULT_MODEL as F, extractTarGzMember as Ft, toolbeltEnabled as G, DEFAULT_CLAUDE_MODEL_FALLBACKS as Gt, buildEnv as H, buildWorkspaceHeaderJson as Ht, IMPLEMENT_DEFAULT_MODEL as I, extractZipMember as It, TOOLBELT_TOOLS$1 as J, DEFAULT_PORT as Jt, toolbeltSkipSet as K, DEFAULT_CODEX_MODEL as Kt, PLAN_DEFAULT_MODEL as L, CONDENSED_OPERATING_SEQUENCE as Lt, resolveSealedGate as M, provisionBrowserAssets as Mt, BROWSE_DEFAULT_MODEL as N, hasSupportedBrowserInstalled as Nt, stopGateEnabledForRepo as O, createChatCompletions as Ot, DEFAULT_MODEL as P, provisionAndIndexColbert as Pt, ADVISOR_TOOL_INSTRUCTIONS as Q, pickClaudeDefault as Qt, REVIEW_DEFAULT_MODEL as R, DEFINITION_OF_GREATNESS as Rt, fileFindingsStore as S, state as Sn, createMessages as St, isSubagentContext as T, resolveMcpToolTimeoutMs as Tt, availableToolCommands as U, collapsePathKeys as Ut, withNoOutputRetry as V, buildWorkspaceHeaderHelperCommand as Vt, buildToolbeltAwareness as W, toolbeltPathOverride as Wt, searchWeb as X, UPSTREAM_INACTIVITY_TIMEOUT_MS as Xt, assetFor as Y, UPSTREAM_FETCH_TIMEOUT_MS as Yt, ADVISOR_INTERNAL_TOOL_NAME as Z, generateRandomPort as Zt, stopGateDisabled as _, forwardError as _n, nativeSubagentModel as _t, buildPeerAwarenessSnippet as a, cacheCopilotVersion as an, logStreamError as at, stopReviewEnabled as b, copilotHeaders as bn, shimDefaultsToXhigh as bt, personasFor as c, filterBetaHeader as cn, handleMcpDelete as ct, buildStopHookCommand as d, resolveModel as dn, artifactToolsEnabled as dt, withInstallLock as en, injectAdvisorTool as et, captureLaunchBaseline as f, sleep as fn, browseAgentEnabled as ft, launchBaselineKey as g, HTTPError as gn, geminiAvailable as gt, injectStopHookIntoSettingsFile as h, fetchWithTransientRetry as hn, fleetToolsEnabled as ht, buildAgentPrompt as i, tryRefreshAndRetry as in, isControllerClosedError as it, liveExec as j, parseJsonOrDiagnose as jt, stopReviewStateDir as k, MAX_RESPONSE_BODY_BYTES as kt, buildArtifactOpenHookCommand as l, isNullish as ln, handleMcpPost as lt, fileBlockBudget as m, getGitHubUser as mn, browserToolsEnabled as mt, MCP_GROUPS as n, setupGitHubAgentToken as nn, buildAnthropicErrorEvent as nt, buildPeerAwarenessSummary as o, cacheModels as on, readIteratorWithTimeout as ot, decideStopHook as p, getModels as pn, browserCompoundToolsEnabled as pt, vscodeRipgrepPath as q, DEFAULT_CODEX_MODEL_FALLBACKS as qt, assertMcpToolSurfaceConsistent as r, setupGitHubToken as rn, buildOpenAIErrorEvent as rt, enumerateInjectedMcpToolNames as s, cacheVSCodeVersion as sn, relayAnthropicStream as st, GROUP_META as t, setupCopilotToken as tn, isAdvisorRequested as tt, buildSessionBindHookCommand as u, resolveCodexModel as un, agentToolsEnabled as ut, stopGateId as v, GITHUB_API_BASE_URL as vn, standInToolEnabled as vt, fileReviewDebounce as w, assembleResponsesPayload as wt, fileBaselineStore as x, githubHeaders as xn, countTokens as xt, stopGatePlanMode as y, copilotBaseUrl as yn, workerToolsEnabled as yt, appendPlanReminder as z, shouldUseInsecureTls as zt };
33572
- //# sourceMappingURL=peer-mcp-personas-CxpFD-rW.js.map
33904
+ export { searchWeb as $, UPSTREAM_INACTIVITY_TIMEOUT_MS as $t, trustRepo as A, createResponses as At, TEST_DEFAULT_MODEL as B, CONDENSED_OPERATING_SEQUENCE as Bt, fileLastPromptStore as C, copilotHeaders as Cn, shimDefaultsToXhigh as Ct, repoRoot as D, assembleResponsesPayload as Dt, repoFingerprint as E, getTokenCount as Et, EXPLORE_DEFAULT_MODEL as F, provisionBrowserAssets as Ft, buildEnv as G, buildWorkspaceHeaderJson as Gt, resolveModeDefaults as H, shouldUseInsecureTls as Ht, EXPLORE_DEFAULT_THINKING as I, hasSupportedBrowserInstalled as It, toolbeltEnabled as J, DEFAULT_CLAUDE_MODEL_FALLBACKS as Jt, availableToolCommands as K, collapsePathKeys as Kt, IMPLEMENT_DEFAULT_MODEL as L, provisionAndIndexColbert as Lt, resolveSealedGate as M, MAX_RESPONSE_BODY_BYTES as Mt, BROWSE_DEFAULT_MODEL as N, readResponseBodyCapped as Nt, stopGateEnabledForRepo as O, resolveMcpToolTimeoutMs as Ot, DEFAULT_MODEL as P, parseJsonOrDiagnose as Pt, assetFor as Q, UPSTREAM_FETCH_TIMEOUT_MS as Qt, PLAN_DEFAULT_MODEL as R, extractTarGzMember as Rt, fileFindingsStore as S, copilotBaseUrl as Sn, workerToolsEnabled as St, isSubagentContext as T, state as Tn, createMessages as Tt, resolveWorkerRunOpts as U, ArtifactClient as Ut, appendPlanReminder as V, DEFINITION_OF_GREATNESS as Vt, runWorkerAgent as W, buildWorkspaceHeaderHelperCommand as Wt, vscodeRipgrepPath as X, DEFAULT_CODEX_MODEL_FALLBACKS as Xt, toolbeltSkipSet as Y, DEFAULT_CODEX_MODEL as Yt, TOOLBELT_TOOLS$1 as Z, DEFAULT_PORT as Zt, stopGateDisabled as _, getGitHubUser as _n, browserToolsEnabled as _t, buildPeerAwarenessSnippet as a, setupGitHubAgentToken as an, buildAnthropicErrorEvent as at, stopReviewEnabled as b, forwardError as bn, nativeSubagentModel as bt, personasFor as c, cacheCopilotVersion as cn, logStreamError as ct, buildStopHookCommand as d, filterBetaHeader as dn, handleMcpDelete as dt, generateRandomPort as en, ADVISOR_INTERNAL_TOOL_NAME as et, captureLaunchBaseline as f, isNullish as fn, handleMcpPost as ft, launchBaselineKey as g, getModels as gn, browserCompoundToolsEnabled as gt, injectStopHookIntoSettingsFile as h, sleep as hn, browseAgentEnabled as ht, buildAgentPrompt as i, setupCopilotToken as in, isAdvisorRequested as it, liveExec as j, createChatCompletions as jt, stopReviewStateDir as k, pickEndpoint as kt, buildArtifactOpenHookCommand as l, cacheModels as ln, readIteratorWithTimeout as lt, fileBlockBudget as m, resolveModel as mn, artifactToolsEnabled as mt, MCP_GROUPS as n, getPackageVersion as nn, buildAdvisorStream as nt, buildPeerAwarenessSummary as o, setupGitHubToken as on, buildOpenAIErrorEvent as ot, decideStopHook as p, resolveCodexModel as pn, agentToolsEnabled as pt, buildToolbeltAwareness as q, toolbeltPathOverride as qt, assertMcpToolSurfaceConsistent as r, withInstallLock as rn, injectAdvisorTool as rt, enumerateInjectedMcpToolNames as s, tryRefreshAndRetry as sn, isControllerClosedError as st, GROUP_META as t, pickClaudeDefault as tn, ADVISOR_TOOL_INSTRUCTIONS as tt, buildSessionBindHookCommand as u, cacheVSCodeVersion as un, relayAnthropicStream as ut, stopGateId as v, fetchWithTransientRetry as vn, fleetToolsEnabled as vt, fileReviewDebounce as w, githubHeaders as wn, countTokens as wt, fileBaselineStore as x, GITHUB_API_BASE_URL as xn, standInToolEnabled as xt, stopGatePlanMode as y, HTTPError as yn, geminiAvailable as yt, REVIEW_DEFAULT_MODEL as z, extractZipMember as zt };
33905
+ //# sourceMappingURL=peer-mcp-personas-A6PytLD6.js.map