@caupulican/pi-adaptative 0.80.97 → 0.80.99

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/core/agent-session.d.ts +46 -5
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +385 -17
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/autonomy/envelope-enforcement.d.ts +17 -0
  7. package/dist/core/autonomy/envelope-enforcement.d.ts.map +1 -0
  8. package/dist/core/autonomy/envelope-enforcement.js +80 -0
  9. package/dist/core/autonomy/envelope-enforcement.js.map +1 -0
  10. package/dist/core/autonomy/foreground-envelope.d.ts +22 -0
  11. package/dist/core/autonomy/foreground-envelope.d.ts.map +1 -0
  12. package/dist/core/autonomy/foreground-envelope.js +65 -0
  13. package/dist/core/autonomy/foreground-envelope.js.map +1 -0
  14. package/dist/core/autonomy/status.d.ts +11 -0
  15. package/dist/core/autonomy/status.d.ts.map +1 -1
  16. package/dist/core/autonomy/status.js.map +1 -1
  17. package/dist/core/context/brain-curator.d.ts +7 -0
  18. package/dist/core/context/brain-curator.d.ts.map +1 -1
  19. package/dist/core/context/brain-curator.js +6 -0
  20. package/dist/core/context/brain-curator.js.map +1 -1
  21. package/dist/core/context/context-composition.d.ts.map +1 -1
  22. package/dist/core/context/context-composition.js +1 -1
  23. package/dist/core/context/context-composition.js.map +1 -1
  24. package/dist/core/delegation/session-worker-result.d.ts +8 -2
  25. package/dist/core/delegation/session-worker-result.d.ts.map +1 -1
  26. package/dist/core/delegation/session-worker-result.js +18 -1
  27. package/dist/core/delegation/session-worker-result.js.map +1 -1
  28. package/dist/core/delegation/worker-actions.d.ts +50 -0
  29. package/dist/core/delegation/worker-actions.d.ts.map +1 -0
  30. package/dist/core/delegation/worker-actions.js +70 -0
  31. package/dist/core/delegation/worker-actions.js.map +1 -0
  32. package/dist/core/delegation/worker-runner.d.ts +9 -0
  33. package/dist/core/delegation/worker-runner.d.ts.map +1 -1
  34. package/dist/core/delegation/worker-runner.js +38 -4
  35. package/dist/core/delegation/worker-runner.js.map +1 -1
  36. package/dist/core/learning/observation-store.d.ts +20 -0
  37. package/dist/core/learning/observation-store.d.ts.map +1 -0
  38. package/dist/core/learning/observation-store.js +101 -0
  39. package/dist/core/learning/observation-store.js.map +1 -0
  40. package/dist/core/model-capability.d.ts +19 -0
  41. package/dist/core/model-capability.d.ts.map +1 -1
  42. package/dist/core/model-capability.js +19 -0
  43. package/dist/core/model-capability.js.map +1 -1
  44. package/dist/core/model-router/executor-route.d.ts +8 -0
  45. package/dist/core/model-router/executor-route.d.ts.map +1 -0
  46. package/dist/core/model-router/executor-route.js +33 -0
  47. package/dist/core/model-router/executor-route.js.map +1 -0
  48. package/dist/core/model-router/tool-escalation.d.ts +2 -0
  49. package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
  50. package/dist/core/model-router/tool-escalation.js +6 -0
  51. package/dist/core/model-router/tool-escalation.js.map +1 -1
  52. package/dist/core/research/research-runner.d.ts +8 -1
  53. package/dist/core/research/research-runner.d.ts.map +1 -1
  54. package/dist/core/research/research-runner.js +13 -1
  55. package/dist/core/research/research-runner.js.map +1 -1
  56. package/dist/core/research/workspace-collector.d.ts +25 -0
  57. package/dist/core/research/workspace-collector.d.ts.map +1 -0
  58. package/dist/core/research/workspace-collector.js +286 -0
  59. package/dist/core/research/workspace-collector.js.map +1 -0
  60. package/dist/core/settings-manager.d.ts +5 -0
  61. package/dist/core/settings-manager.d.ts.map +1 -1
  62. package/dist/core/settings-manager.js +8 -0
  63. package/dist/core/settings-manager.js.map +1 -1
  64. package/dist/modes/interactive/components/fitness-role-selector.d.ts +1 -1
  65. package/dist/modes/interactive/components/fitness-role-selector.d.ts.map +1 -1
  66. package/dist/modes/interactive/components/fitness-role-selector.js +5 -0
  67. package/dist/modes/interactive/components/fitness-role-selector.js.map +1 -1
  68. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/settings-selector.js +20 -0
  70. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  71. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  72. package/dist/modes/interactive/interactive-mode.js +9 -0
  73. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  74. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  75. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  76. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  77. package/examples/extensions/sandbox/package-lock.json +2 -2
  78. package/examples/extensions/sandbox/package.json +1 -1
  79. package/examples/extensions/with-deps/package-lock.json +2 -2
  80. package/examples/extensions/with-deps/package.json +1 -1
  81. package/npm-shrinkwrap.json +12 -12
  82. package/package.json +4 -4
@@ -1,5 +1,6 @@
1
1
  import { runBoundedCompletion } from "../autonomy/bounded-completion.js";
2
2
  import { createEvidenceBundle } from "../research/evidence-bundle.js";
3
+ import { parseWorkerActions } from "./worker-actions.js";
3
4
  import { validateWorkerResult } from "./worker-result.js";
4
5
  /**
5
6
  * Pure orchestration for one bounded scout-worker delegation: bounded isolated completion ->
@@ -18,6 +19,17 @@ export const WORKER_LANE_SYSTEM_PROMPT = [
18
19
  'Use status "blocked" with blockers only when the task cannot be answered from the provided context.',
19
20
  "Never invent file paths, APIs, or facts.",
20
21
  ].join("\n");
22
+ /** Write-capable variant (G2): same contract plus a structured actions array — the model never
23
+ * touches the filesystem; the runner applies actions through the envelope's path scope. */
24
+ export const WORKER_WRITE_LANE_SYSTEM_PROMPT = [
25
+ "You are a bounded code-writing worker delegated one task by a coding agent.",
26
+ "You cannot run tools; you CHANGE FILES only by listing actions the runner applies for you.",
27
+ "Respond with STRICT JSON only - no prose, no markdown fences:",
28
+ '{"summary":"<what you did>","status":"completed"|"blocked","blockers":[],"findings":[{"summary":"<finding>","confidence":<0..1>}],"actions":[{"op":"write","path":"<relative path>","content":"<full file content>"},{"op":"edit","path":"<relative path>","old":"<exact text>","new":"<replacement>"}]}',
29
+ "Only touch paths inside your delegated scope. Keep edits minimal and exact.",
30
+ 'Use status "blocked" with blockers when the task cannot be done from the provided context.',
31
+ "Never invent file paths, APIs, or facts.",
32
+ ].join("\n");
21
33
  export function buildWorkerUserPrompt(request) {
22
34
  return `Delegated task: ${request.instructions}`;
23
35
  }
@@ -64,7 +76,7 @@ export function parseWorkerOutput(text) {
64
76
  findings.push({ summary: findingSummary.trim(), confidence });
65
77
  }
66
78
  }
67
- return { summary: summary.trim(), status, blockers, findings };
79
+ return { summary: summary.trim(), status, blockers, findings, actions: parseWorkerActions(record.actions) };
68
80
  }
69
81
  return undefined;
70
82
  }
@@ -115,11 +127,14 @@ export async function runWorker(options) {
115
127
  usageReportId: options.usageReportId,
116
128
  createdAt: now(),
117
129
  };
130
+ // The WRITE lane requires BOTH the envelope grant and a caller-supplied applier — either
131
+ // alone keeps the read-only scout contract byte-for-byte.
132
+ const writeCapable = options.request.envelope.capabilities.includes("write_files") && options.applyActions !== undefined;
118
133
  const bounded = await runBoundedCompletion({
119
134
  maxWallClockMs: options.maxWallClockMs,
120
135
  signal: options.signal,
121
136
  execute: (signal) => options.complete({
122
- systemPrompt: WORKER_LANE_SYSTEM_PROMPT,
137
+ systemPrompt: writeCapable ? WORKER_WRITE_LANE_SYSTEM_PROMPT : WORKER_LANE_SYSTEM_PROMPT,
123
138
  userPrompt: buildWorkerUserPrompt(options.request),
124
139
  signal,
125
140
  }),
@@ -160,11 +175,30 @@ export async function runWorker(options) {
160
175
  });
161
176
  }
162
177
  const evidence = buildWorkerEvidence(options.request, parsed.findings);
178
+ let changedFiles = [];
179
+ const actionBlockers = [];
180
+ if (writeCapable && parsed.status !== "blocked" && parsed.actions.length > 0 && options.applyActions) {
181
+ // Runner-side application through the envelope path scope: refusals and failures are
182
+ // surfaced as blockers so a partially-applied change can never look like clean success.
183
+ const applied = options.applyActions(parsed.actions);
184
+ changedFiles = applied.changedFiles;
185
+ for (const refusal of applied.refused) {
186
+ actionBlockers.push(`action refused (${refusal.path}): ${refusal.reason}`);
187
+ }
188
+ for (const failure of applied.failed) {
189
+ actionBlockers.push(`action failed (${failure.path}): ${failure.reason}`);
190
+ }
191
+ }
192
+ else if (!writeCapable && parsed.actions.length > 0) {
193
+ actionBlockers.push("worker emitted file actions without a write_files envelope grant; nothing was applied");
194
+ }
195
+ const allBlockers = [...parsed.blockers, ...actionBlockers];
163
196
  const result = {
164
197
  ...baseResult,
165
- status: parsed.status === "blocked" || parsed.blockers.length > 0 ? "blocked" : "completed",
198
+ changedFiles,
199
+ status: parsed.status === "blocked" || allBlockers.length > 0 ? "blocked" : "completed",
166
200
  summary: parsed.summary,
167
- ...(parsed.blockers.length > 0 ? { blockers: parsed.blockers } : {}),
201
+ ...(allBlockers.length > 0 ? { blockers: allBlockers } : {}),
168
202
  ...(evidence ? { evidence } : {}),
169
203
  };
170
204
  if (result.status === "blocked") {
@@ -1 +1 @@
1
- {"version":3,"file":"worker-runner.js","sourceRoot":"","sources":["../../../src/core/delegation/worker-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAGzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D;;;;;;;GAOG;AAEH,wEAAwE;AACxE,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACxC,gFAAgF;IAChF,yFAAyF;IACzF,+DAA+D;IAC/D,4KAA4K;IAC5K,qGAAqG;IACrG,0CAA0C;CAC1C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAkCb,MAAM,UAAU,qBAAqB,CAAC,OAAsB,EAAU;IACrE,OAAO,mBAAmB,OAAO,CAAC,YAAY,EAAE,CAAC;AAAA,CACjD;AASD,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAkC;IAC/E,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,MAAM,UAAU,GAAa,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,8BAA8B,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5D,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC;QAAE,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,KAAK,IAAI,CAAC,IAAI,GAAG,GAAG,KAAK;QAAE,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IAE9E,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACJ,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;QAAC,MAAM,CAAC;YACR,SAAS;QACV,CAAC;QACD,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,SAAS;QAC7E,MAAM,MAAM,GAAG,MAAiC,CAAC;QACjD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC/B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAEzE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC;QACrE,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;YAC9C,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAqB,EAAE,CAAC,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;YAC3G,CAAC,CAAC,EAAE,CAAC;QACN,MAAM,QAAQ,GAAoD,EAAE,CAAC;QACrE,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACpC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACpC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;oBAAE,SAAS;gBACvE,MAAM,cAAc,GAAI,IAA8B,CAAC,OAAO,CAAC;gBAC/D,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;oBAAE,SAAS;gBACvF,MAAM,aAAa,GAAI,IAAiC,CAAC,UAAU,CAAC;gBACpE,MAAM,UAAU,GACf,OAAO,aAAa,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;oBAClE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;oBACzC,CAAC,CAAC,SAAS,CAAC;gBACd,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,cAAc,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;YAC/D,CAAC;QACF,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;IAChE,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,SAAS,mBAAmB,CAAC,OAAsB,EAAE,QAAwC,EAAE;IAC9F,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC5C,MAAM,eAAe,GAAgB;QACpC,EAAE,EAAE,kBAAkB;QACtB,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,6BAA6B;QACpC,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC;KAC5C,CAAC;IACF,MAAM,YAAY,GAAgB;QACjC,EAAE,EAAE,YAAY;QAChB,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,wBAAwB;QAC/B,OAAO,EAAE,KAAK;KACd,CAAC;IACF,MAAM,cAAc,GAAc,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QACnE,EAAE,EAAE,WAAW,KAAK,GAAG,CAAC,EAAE;QAC1B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,WAAW,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9B,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC/E,CAAC,CAAC,CAAC;IACJ,OAAO,oBAAoB,CAAC;QAC3B,KAAK,EAAE,UAAU,OAAO,CAAC,EAAE,EAAE;QAC7B,OAAO,EAAE,CAAC,eAAe,EAAE,YAAY,CAAC;QACxC,QAAQ,EAAE,cAAc;KACxB,CAAC,CAAC;AAAA,CACH;AAED,SAAS,aAAa,CAAC,IAMtB,EAAoB;IACpB,MAAM,UAAU,GAAG,oBAAoB,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IACxF,OAAO;QACN,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,UAAU;QACV,QAAQ,EAAE,UAAU,CAAC,OAAO,KAAK,OAAO;QACxC,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;KACrB,CAAC;AAAA,CACF;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAA4B,EAA6B;IACxF,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5D,MAAM,UAAU,GAAG;QAClB,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE;QAC7B,YAAY,EAAE,EAAc;QAC5B,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,SAAS,EAAE,GAAG,EAAE;KAChB,CAAC;IAEF,MAAM,OAAO,GAAG,MAAM,oBAAoB,CAAC;QAC1C,cAAc,EAAE,OAAO,CAAC,cAAc;QACtC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CACnB,OAAO,CAAC,QAAQ,CAAC;YAChB,YAAY,EAAE,yBAAyB;YACvC,UAAU,EAAE,qBAAqB,CAAC,OAAO,CAAC,OAAO,CAAC;YAClD,MAAM;SACN,CAAC;KACH,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,EAAE,OAAO,IAAI,CAAC,CAAC;IAEjD,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC;QAChG,OAAO,aAAa,CAAC;YACpB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE;gBACP,GAAG,UAAU;gBACb,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ;gBAC1C,OAAO,EAAE,4BAA4B,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE;aACjE;YACD,UAAU,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM;YAClC,UAAU,EAAE,OAAO,CAAC,OAAO,CAAC,UAAU;YACtC,OAAO;SACP,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IACtC,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,UAAU,KAAK,OAAO,IAAI,UAAU,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7F,OAAO,aAAa,CAAC;YACpB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,EAAE,GAAG,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,EAAE;YACjF,UAAU,EAAE,QAAQ;YACpB,UAAU,EAAE,aAAa;YACzB,OAAO;SACP,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IAClD,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO,aAAa,CAAC;YACpB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM,EAAE,EAAE,GAAG,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,8CAA8C,EAAE;YACpG,UAAU,EAAE,QAAQ;YACpB,UAAU,EAAE,oBAAoB;YAChC,OAAO;SACP,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,mBAAmB,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACvE,MAAM,MAAM,GAAiB;QAC5B,GAAG,UAAU;QACb,MAAM,EAAE,MAAM,CAAC,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW;QAC3F,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACjC,CAAC;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QACjC,OAAO,aAAa,CAAC;YACpB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,MAAM;YACN,UAAU,EAAE,QAAQ;YACpB,UAAU,EAAE,gBAAgB;YAC5B,OAAO;SACP,CAAC,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IAClE,OAAO,aAAa,CAAC;QACpB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,MAAM;QACN,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,WAAW;QACzD,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,kBAAkB;QACpE,OAAO;KACP,CAAC,CAAC;AAAA,CACH","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { EvidenceRef, Finding, GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport type { LaneTerminalStatus } from \"../autonomy/lane-tracker.ts\";\nimport { createEvidenceBundle } from \"../research/evidence-bundle.ts\";\nimport { validateWorkerResult } from \"./worker-result.ts\";\n\n/**\n * Pure orchestration for one bounded scout-worker delegation: bounded isolated completion ->\n * parse -> `WorkerResult` -> parent validation via {@link validateWorkerResult}.\n *\n * Slice scope: scout (read-only) workers only — the completion receives text prompts, no tools, so\n * `changedFiles` is always empty. Code-writing workers stay out until a real execution envelope\n * enforces path scope at tool level. Worker output is untrusted until the parent verifies it.\n */\n\n/** Static across calls so callers can use `cacheRetention: \"short\"`. */\nexport const WORKER_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded read-only scout worker delegated one task by a coding agent.\",\n\t\"You cannot run tools or change files; produce your best analysis of the delegated task.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you concluded>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[\"<why you are stuck>\"],\"findings\":[{\"summary\":\"<one concrete finding>\",\"confidence\":<0..1>}]}',\n\t'Use status \"blocked\" with blockers only when the task cannot be answered from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\nexport interface WorkerCompletion {\n\ttext: string;\n\tcostUsd: number;\n\tstopReason: string;\n}\n\nexport interface WorkerRunnerOptions {\n\trequest: WorkerRequest;\n\t/** Budget for this delegation; a post-hoc breach marks the lane budget_exhausted. */\n\tmaxUsd: number;\n\t/** Wall-clock budget in milliseconds; 0 disables. */\n\tmaxWallClockMs: number;\n\t/**\n\t * Pre-allocated spawned-usage report id. Always stamped on the result so parent validation can\n\t * enforce the cost-visibility invariant (a completed result without a usage report is blocked).\n\t */\n\tusageReportId: string;\n\tcomplete: (args: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<WorkerCompletion>;\n\tsignal?: AbortSignal;\n\tnow?: () => string;\n}\n\nexport interface WorkerRunOutcome {\n\tresult: WorkerResult;\n\t/** Parent-review verdict from {@link validateWorkerResult}; worker output stays untrusted. */\n\tacceptance: GateOutcome;\n\taccepted: boolean;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}\n\nexport function buildWorkerUserPrompt(request: WorkerRequest): string {\n\treturn `Delegated task: ${request.instructions}`;\n}\n\nexport interface ParsedWorkerOutput {\n\tsummary: string;\n\tstatus: \"completed\" | \"blocked\";\n\tblockers: string[];\n\tfindings: Array<{ summary: string; confidence?: number }>;\n}\n\nexport function parseWorkerOutput(text: string): ParsedWorkerOutput | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\tconst summary = record.summary;\n\t\tif (typeof summary !== \"string\" || summary.trim().length === 0) continue;\n\n\t\tconst status = record.status === \"blocked\" ? \"blocked\" : \"completed\";\n\t\tconst blockers = Array.isArray(record.blockers)\n\t\t\t? record.blockers.filter((blocker): blocker is string => typeof blocker === \"string\" && blocker.length > 0)\n\t\t\t: [];\n\t\tconst findings: Array<{ summary: string; confidence?: number }> = [];\n\t\tif (Array.isArray(record.findings)) {\n\t\t\tfor (const item of record.findings) {\n\t\t\t\tif (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n\t\t\t\tconst findingSummary = (item as { summary?: unknown }).summary;\n\t\t\t\tif (typeof findingSummary !== \"string\" || findingSummary.trim().length === 0) continue;\n\t\t\t\tconst confidenceRaw = (item as { confidence?: unknown }).confidence;\n\t\t\t\tconst confidence =\n\t\t\t\t\ttypeof confidenceRaw === \"number\" && Number.isFinite(confidenceRaw)\n\t\t\t\t\t\t? Math.min(Math.max(confidenceRaw, 0), 1)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tfindings.push({ summary: findingSummary.trim(), confidence });\n\t\t\t}\n\t\t}\n\t\treturn { summary: summary.trim(), status, blockers, findings };\n\t}\n\treturn undefined;\n}\n\nfunction buildWorkerEvidence(request: WorkerRequest, findings: ParsedWorkerOutput[\"findings\"]) {\n\tif (findings.length === 0) return undefined;\n\tconst instructionsRef: EvidenceRef = {\n\t\tid: \"src-instructions\",\n\t\tkind: \"user\",\n\t\ttitle: \"Delegated task instructions\",\n\t\ttrusted: true,\n\t\texcerpt: request.instructions.slice(0, 2000),\n\t};\n\tconst synthesisRef: EvidenceRef = {\n\t\tid: \"src-worker\",\n\t\tkind: \"tool\",\n\t\ttitle: \"Scout-worker synthesis\",\n\t\ttrusted: false,\n\t};\n\tconst bundleFindings: Finding[] = findings.map((finding, index) => ({\n\t\tid: `finding-${index + 1}`,\n\t\tsummary: finding.summary,\n\t\tevidenceIds: [synthesisRef.id],\n\t\t...(finding.confidence !== undefined ? { confidence: finding.confidence } : {}),\n\t}));\n\treturn createEvidenceBundle({\n\t\tquery: `worker:${request.id}`,\n\t\tsources: [instructionsRef, synthesisRef],\n\t\tfindings: bundleFindings,\n\t});\n}\n\nfunction finishOutcome(args: {\n\trequest: WorkerRequest;\n\tresult: WorkerResult;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}): WorkerRunOutcome {\n\tconst acceptance = validateWorkerResult({ request: args.request, result: args.result });\n\treturn {\n\t\tresult: args.result,\n\t\tacceptance,\n\t\taccepted: acceptance.outcome === \"allow\",\n\t\tlaneStatus: args.laneStatus,\n\t\treasonCode: args.reasonCode,\n\t\tcostUsd: args.costUsd,\n\t};\n}\n\nexport async function runWorker(options: WorkerRunnerOptions): Promise<WorkerRunOutcome> {\n\tconst now = options.now ?? (() => new Date().toISOString());\n\tconst baseResult = {\n\t\trequestId: options.request.id,\n\t\tchangedFiles: [] as string[],\n\t\tusageReportId: options.usageReportId,\n\t\tcreatedAt: now(),\n\t};\n\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: options.maxWallClockMs,\n\t\tsignal: options.signal,\n\t\texecute: (signal) =>\n\t\t\toptions.complete({\n\t\t\t\tsystemPrompt: WORKER_LANE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildWorkerUserPrompt(options.request),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure) {\n\t\tconst cancelled = bounded.failure.status === \"canceled\" || bounded.failure.status === \"timeout\";\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: {\n\t\t\t\t...baseResult,\n\t\t\t\tstatus: cancelled ? \"cancelled\" : \"failed\",\n\t\t\t\tsummary: `Worker did not complete: ${bounded.failure.reasonCode}`,\n\t\t\t},\n\t\t\tlaneStatus: bounded.failure.status,\n\t\t\treasonCode: bounded.failure.reasonCode,\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst completion = bounded.completion;\n\tif (!completion || completion.stopReason === \"error\" || completion.stopReason === \"aborted\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker model call failed.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"model_error\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst parsed = parseWorkerOutput(completion.text);\n\tif (!parsed) {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker output was not valid structured JSON.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"unparseable_output\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst evidence = buildWorkerEvidence(options.request, parsed.findings);\n\tconst result: WorkerResult = {\n\t\t...baseResult,\n\t\tstatus: parsed.status === \"blocked\" || parsed.blockers.length > 0 ? \"blocked\" : \"completed\",\n\t\tsummary: parsed.summary,\n\t\t...(parsed.blockers.length > 0 ? { blockers: parsed.blockers } : {}),\n\t\t...(evidence ? { evidence } : {}),\n\t};\n\n\tif (result.status === \"blocked\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult,\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"worker_blocked\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst overBudget = options.maxUsd > 0 && costUsd > options.maxUsd;\n\treturn finishOutcome({\n\t\trequest: options.request,\n\t\tresult,\n\t\tlaneStatus: overBudget ? \"budget_exhausted\" : \"succeeded\",\n\t\treasonCode: overBudget ? \"cost_budget_exceeded\" : \"worker_completed\",\n\t\tcostUsd,\n\t});\n}\n"]}
1
+ {"version":3,"file":"worker-runner.js","sourceRoot":"","sources":["../../../src/core/delegation/worker-runner.ts"],"names":[],"mappings":"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","sourcesContent":["import { runBoundedCompletion } from \"../autonomy/bounded-completion.ts\";\nimport type { EvidenceRef, Finding, GateOutcome, WorkerRequest, WorkerResult } from \"../autonomy/contracts.ts\";\nimport type { LaneTerminalStatus } from \"../autonomy/lane-tracker.ts\";\nimport { createEvidenceBundle } from \"../research/evidence-bundle.ts\";\nimport { type AppliedActionsReport, parseWorkerActions, type WorkerAction } from \"./worker-actions.ts\";\nimport { validateWorkerResult } from \"./worker-result.ts\";\n\n/**\n * Pure orchestration for one bounded scout-worker delegation: bounded isolated completion ->\n * parse -> `WorkerResult` -> parent validation via {@link validateWorkerResult}.\n *\n * Slice scope: scout (read-only) workers only — the completion receives text prompts, no tools, so\n * `changedFiles` is always empty. Code-writing workers stay out until a real execution envelope\n * enforces path scope at tool level. Worker output is untrusted until the parent verifies it.\n */\n\n/** Static across calls so callers can use `cacheRetention: \"short\"`. */\nexport const WORKER_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded read-only scout worker delegated one task by a coding agent.\",\n\t\"You cannot run tools or change files; produce your best analysis of the delegated task.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you concluded>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[\"<why you are stuck>\"],\"findings\":[{\"summary\":\"<one concrete finding>\",\"confidence\":<0..1>}]}',\n\t'Use status \"blocked\" with blockers only when the task cannot be answered from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\n/** Write-capable variant (G2): same contract plus a structured actions array — the model never\n * touches the filesystem; the runner applies actions through the envelope's path scope. */\nexport const WORKER_WRITE_LANE_SYSTEM_PROMPT = [\n\t\"You are a bounded code-writing worker delegated one task by a coding agent.\",\n\t\"You cannot run tools; you CHANGE FILES only by listing actions the runner applies for you.\",\n\t\"Respond with STRICT JSON only - no prose, no markdown fences:\",\n\t'{\"summary\":\"<what you did>\",\"status\":\"completed\"|\"blocked\",\"blockers\":[],\"findings\":[{\"summary\":\"<finding>\",\"confidence\":<0..1>}],\"actions\":[{\"op\":\"write\",\"path\":\"<relative path>\",\"content\":\"<full file content>\"},{\"op\":\"edit\",\"path\":\"<relative path>\",\"old\":\"<exact text>\",\"new\":\"<replacement>\"}]}',\n\t\"Only touch paths inside your delegated scope. Keep edits minimal and exact.\",\n\t'Use status \"blocked\" with blockers when the task cannot be done from the provided context.',\n\t\"Never invent file paths, APIs, or facts.\",\n].join(\"\\n\");\n\nexport interface WorkerCompletion {\n\ttext: string;\n\tcostUsd: number;\n\tstopReason: string;\n}\n\nexport interface WorkerRunnerOptions {\n\trequest: WorkerRequest;\n\t/** Budget for this delegation; a post-hoc breach marks the lane budget_exhausted. */\n\tmaxUsd: number;\n\t/** Wall-clock budget in milliseconds; 0 disables. */\n\tmaxWallClockMs: number;\n\t/**\n\t * Pre-allocated spawned-usage report id. Always stamped on the result so parent validation can\n\t * enforce the cost-visibility invariant (a completed result without a usage report is blocked).\n\t */\n\tusageReportId: string;\n\tcomplete: (args: { systemPrompt: string; userPrompt: string; signal?: AbortSignal }) => Promise<WorkerCompletion>;\n\tsignal?: AbortSignal;\n\tnow?: () => string;\n\t/** Enables the WRITE lane: only honored when the request envelope grants \"write_files\". The\n\t * runner applies the worker's structured actions through the envelope path scope; refusals\n\t * and failures become blockers, never silent drops. */\n\tapplyActions?: (actions: readonly WorkerAction[]) => AppliedActionsReport;\n}\n\nexport interface WorkerRunOutcome {\n\tresult: WorkerResult;\n\t/** Parent-review verdict from {@link validateWorkerResult}; worker output stays untrusted. */\n\tacceptance: GateOutcome;\n\taccepted: boolean;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}\n\nexport function buildWorkerUserPrompt(request: WorkerRequest): string {\n\treturn `Delegated task: ${request.instructions}`;\n}\n\nexport interface ParsedWorkerOutput {\n\tsummary: string;\n\tstatus: \"completed\" | \"blocked\";\n\tblockers: string[];\n\tfindings: Array<{ summary: string; confidence?: number }>;\n\tactions: WorkerAction[];\n}\n\nexport function parseWorkerOutput(text: string): ParsedWorkerOutput | undefined {\n\tconst trimmed = text.trim();\n\tconst candidates: string[] = [trimmed];\n\tconst fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(trimmed);\n\tif (fenced?.[1]) candidates.push(fenced[1].trim());\n\tconst start = trimmed.indexOf(\"{\");\n\tconst end = trimmed.lastIndexOf(\"}\");\n\tif (start >= 0 && end > start) candidates.push(trimmed.slice(start, end + 1));\n\n\tfor (const candidate of candidates) {\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(candidate);\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tif (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) continue;\n\t\tconst record = parsed as Record<string, unknown>;\n\t\tconst summary = record.summary;\n\t\tif (typeof summary !== \"string\" || summary.trim().length === 0) continue;\n\n\t\tconst status = record.status === \"blocked\" ? \"blocked\" : \"completed\";\n\t\tconst blockers = Array.isArray(record.blockers)\n\t\t\t? record.blockers.filter((blocker): blocker is string => typeof blocker === \"string\" && blocker.length > 0)\n\t\t\t: [];\n\t\tconst findings: Array<{ summary: string; confidence?: number }> = [];\n\t\tif (Array.isArray(record.findings)) {\n\t\t\tfor (const item of record.findings) {\n\t\t\t\tif (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n\t\t\t\tconst findingSummary = (item as { summary?: unknown }).summary;\n\t\t\t\tif (typeof findingSummary !== \"string\" || findingSummary.trim().length === 0) continue;\n\t\t\t\tconst confidenceRaw = (item as { confidence?: unknown }).confidence;\n\t\t\t\tconst confidence =\n\t\t\t\t\ttypeof confidenceRaw === \"number\" && Number.isFinite(confidenceRaw)\n\t\t\t\t\t\t? Math.min(Math.max(confidenceRaw, 0), 1)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tfindings.push({ summary: findingSummary.trim(), confidence });\n\t\t\t}\n\t\t}\n\t\treturn { summary: summary.trim(), status, blockers, findings, actions: parseWorkerActions(record.actions) };\n\t}\n\treturn undefined;\n}\n\nfunction buildWorkerEvidence(request: WorkerRequest, findings: ParsedWorkerOutput[\"findings\"]) {\n\tif (findings.length === 0) return undefined;\n\tconst instructionsRef: EvidenceRef = {\n\t\tid: \"src-instructions\",\n\t\tkind: \"user\",\n\t\ttitle: \"Delegated task instructions\",\n\t\ttrusted: true,\n\t\texcerpt: request.instructions.slice(0, 2000),\n\t};\n\tconst synthesisRef: EvidenceRef = {\n\t\tid: \"src-worker\",\n\t\tkind: \"tool\",\n\t\ttitle: \"Scout-worker synthesis\",\n\t\ttrusted: false,\n\t};\n\tconst bundleFindings: Finding[] = findings.map((finding, index) => ({\n\t\tid: `finding-${index + 1}`,\n\t\tsummary: finding.summary,\n\t\tevidenceIds: [synthesisRef.id],\n\t\t...(finding.confidence !== undefined ? { confidence: finding.confidence } : {}),\n\t}));\n\treturn createEvidenceBundle({\n\t\tquery: `worker:${request.id}`,\n\t\tsources: [instructionsRef, synthesisRef],\n\t\tfindings: bundleFindings,\n\t});\n}\n\nfunction finishOutcome(args: {\n\trequest: WorkerRequest;\n\tresult: WorkerResult;\n\tlaneStatus: LaneTerminalStatus;\n\treasonCode: string;\n\tcostUsd: number;\n}): WorkerRunOutcome {\n\tconst acceptance = validateWorkerResult({ request: args.request, result: args.result });\n\treturn {\n\t\tresult: args.result,\n\t\tacceptance,\n\t\taccepted: acceptance.outcome === \"allow\",\n\t\tlaneStatus: args.laneStatus,\n\t\treasonCode: args.reasonCode,\n\t\tcostUsd: args.costUsd,\n\t};\n}\n\nexport async function runWorker(options: WorkerRunnerOptions): Promise<WorkerRunOutcome> {\n\tconst now = options.now ?? (() => new Date().toISOString());\n\tconst baseResult = {\n\t\trequestId: options.request.id,\n\t\tchangedFiles: [] as string[],\n\t\tusageReportId: options.usageReportId,\n\t\tcreatedAt: now(),\n\t};\n\n\t// The WRITE lane requires BOTH the envelope grant and a caller-supplied applier — either\n\t// alone keeps the read-only scout contract byte-for-byte.\n\tconst writeCapable =\n\t\toptions.request.envelope.capabilities.includes(\"write_files\") && options.applyActions !== undefined;\n\n\tconst bounded = await runBoundedCompletion({\n\t\tmaxWallClockMs: options.maxWallClockMs,\n\t\tsignal: options.signal,\n\t\texecute: (signal) =>\n\t\t\toptions.complete({\n\t\t\t\tsystemPrompt: writeCapable ? WORKER_WRITE_LANE_SYSTEM_PROMPT : WORKER_LANE_SYSTEM_PROMPT,\n\t\t\t\tuserPrompt: buildWorkerUserPrompt(options.request),\n\t\t\t\tsignal,\n\t\t\t}),\n\t});\n\tconst costUsd = bounded.completion?.costUsd ?? 0;\n\n\tif (bounded.failure) {\n\t\tconst cancelled = bounded.failure.status === \"canceled\" || bounded.failure.status === \"timeout\";\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: {\n\t\t\t\t...baseResult,\n\t\t\t\tstatus: cancelled ? \"cancelled\" : \"failed\",\n\t\t\t\tsummary: `Worker did not complete: ${bounded.failure.reasonCode}`,\n\t\t\t},\n\t\t\tlaneStatus: bounded.failure.status,\n\t\t\treasonCode: bounded.failure.reasonCode,\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst completion = bounded.completion;\n\tif (!completion || completion.stopReason === \"error\" || completion.stopReason === \"aborted\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker model call failed.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"model_error\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst parsed = parseWorkerOutput(completion.text);\n\tif (!parsed) {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult: { ...baseResult, status: \"failed\", summary: \"Worker output was not valid structured JSON.\" },\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"unparseable_output\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst evidence = buildWorkerEvidence(options.request, parsed.findings);\n\tlet changedFiles: string[] = [];\n\tconst actionBlockers: string[] = [];\n\tif (writeCapable && parsed.status !== \"blocked\" && parsed.actions.length > 0 && options.applyActions) {\n\t\t// Runner-side application through the envelope path scope: refusals and failures are\n\t\t// surfaced as blockers so a partially-applied change can never look like clean success.\n\t\tconst applied = options.applyActions(parsed.actions);\n\t\tchangedFiles = applied.changedFiles;\n\t\tfor (const refusal of applied.refused) {\n\t\t\tactionBlockers.push(`action refused (${refusal.path}): ${refusal.reason}`);\n\t\t}\n\t\tfor (const failure of applied.failed) {\n\t\t\tactionBlockers.push(`action failed (${failure.path}): ${failure.reason}`);\n\t\t}\n\t} else if (!writeCapable && parsed.actions.length > 0) {\n\t\tactionBlockers.push(\"worker emitted file actions without a write_files envelope grant; nothing was applied\");\n\t}\n\tconst allBlockers = [...parsed.blockers, ...actionBlockers];\n\tconst result: WorkerResult = {\n\t\t...baseResult,\n\t\tchangedFiles,\n\t\tstatus: parsed.status === \"blocked\" || allBlockers.length > 0 ? \"blocked\" : \"completed\",\n\t\tsummary: parsed.summary,\n\t\t...(allBlockers.length > 0 ? { blockers: allBlockers } : {}),\n\t\t...(evidence ? { evidence } : {}),\n\t};\n\n\tif (result.status === \"blocked\") {\n\t\treturn finishOutcome({\n\t\t\trequest: options.request,\n\t\t\tresult,\n\t\t\tlaneStatus: \"failed\",\n\t\t\treasonCode: \"worker_blocked\",\n\t\t\tcostUsd,\n\t\t});\n\t}\n\n\tconst overBudget = options.maxUsd > 0 && costUsd > options.maxUsd;\n\treturn finishOutcome({\n\t\trequest: options.request,\n\t\tresult,\n\t\tlaneStatus: overBudget ? \"budget_exhausted\" : \"succeeded\",\n\t\treasonCode: overBudget ? \"cost_budget_exceeded\" : \"worker_completed\",\n\t\tcostUsd,\n\t});\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Stable evidence key for a durable-change proposal: sha256 of the layer plus its summary,
3
+ * lowercased and whitespace-collapsed, truncated to 24 hex chars. Normalization means the same
4
+ * lesson re-observed with reworded whitespace/casing accumulates onto one key instead of scattering.
5
+ */
6
+ export declare function observationKey(layer: string, summary: string): string;
7
+ export declare class ObservationStore {
8
+ private readonly filePath;
9
+ constructor(filePath: string);
10
+ static forAgentDir(agentDir: string): ObservationStore;
11
+ private load;
12
+ private save;
13
+ /** Evict least-recently-incremented keys until the store is back within {@link MAX_KEYS}. */
14
+ private evict;
15
+ /** Record one more observation of `key` and return the new (capped) count. */
16
+ increment(key: string, at?: string): number;
17
+ /** Current observation count for `key` (0 if never observed). */
18
+ get(key: string): number;
19
+ }
20
+ //# sourceMappingURL=observation-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation-store.d.ts","sourceRoot":"","sources":["../../../src/core/learning/observation-store.ts"],"names":[],"mappings":"AAgCA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAGrE;AAED,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,YAAY,QAAQ,EAAE,MAAM,EAE3B;IAED,MAAM,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAErD;IAED,OAAO,CAAC,IAAI;IAoCZ,OAAO,CAAC,IAAI;IAKZ,6FAA6F;IAC7F,OAAO,CAAC,KAAK;IAab,8EAA8E;IAC9E,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ1C;IAED,iEAAiE;IACjE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvB;CACD","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Durable, BOUNDED evidence-strength store for the learning gate (G6). The gate auto-applies a\n * durable change only once it has been *observed* enough times; a single reflection pass sees a\n * lesson once, so without persistence every proposal would look brand-new and never accumulate the\n * repeated evidence the gate requires. This store counts how many times the SAME lesson (keyed by\n * its durable layer + normalized summary) has been proposed across passes and sessions.\n *\n * File layout mirrors {@link ../models/fitness-store.ts}: a versioned JSON object under\n * `<agentDir>/state/`, best-effort writes, and a corrupt/missing file recovers as a fresh store.\n */\n\n/** Cap per-key counts so a hot lesson can't grow unbounded (the gate only needs a small threshold). */\nconst MAX_COUNT = 100;\n/** Cap the number of tracked keys; least-recently-incremented keys are evicted past this bound. */\nconst MAX_KEYS = 500;\n\ninterface ObservationEntry {\n\tcount: number;\n\t/** ISO timestamp of the most recent increment; drives least-recently-incremented eviction. */\n\tlastAt: string;\n}\n\ninterface ObservationStoreFile {\n\tversion: 1;\n\t/** observationKey -> accumulated evidence for that lesson. */\n\tobservations: Record<string, ObservationEntry>;\n}\n\n/**\n * Stable evidence key for a durable-change proposal: sha256 of the layer plus its summary,\n * lowercased and whitespace-collapsed, truncated to 24 hex chars. Normalization means the same\n * lesson re-observed with reworded whitespace/casing accumulates onto one key instead of scattering.\n */\nexport function observationKey(layer: string, summary: string): string {\n\tconst normalized = summary.toLowerCase().replace(/\\s+/g, \" \").trim();\n\treturn createHash(\"sha256\").update(`${layer}\\n${normalized}`).digest(\"hex\").slice(0, 24);\n}\n\nexport class ObservationStore {\n\tprivate readonly filePath: string;\n\n\tconstructor(filePath: string) {\n\t\tthis.filePath = filePath;\n\t}\n\n\tstatic forAgentDir(agentDir: string): ObservationStore {\n\t\treturn new ObservationStore(join(agentDir, \"state\", \"learning-observations.json\"));\n\t}\n\n\tprivate load(): ObservationStoreFile {\n\t\ttry {\n\t\t\tif (!existsSync(this.filePath)) return { version: 1, observations: {} };\n\t\t\tconst parsed = JSON.parse(readFileSync(this.filePath, \"utf-8\")) as ObservationStoreFile;\n\t\t\tif (\n\t\t\t\tparsed &&\n\t\t\t\tparsed.version === 1 &&\n\t\t\t\tparsed.observations &&\n\t\t\t\ttypeof parsed.observations === \"object\" &&\n\t\t\t\t!Array.isArray(parsed.observations)\n\t\t\t) {\n\t\t\t\t// Sanitize per-entry so a partially-mangled file still yields a usable store rather than\n\t\t\t\t// leaking NaN/undefined counts into the gate.\n\t\t\t\tconst clean: ObservationStoreFile = { version: 1, observations: {} };\n\t\t\t\tfor (const [key, value] of Object.entries(parsed.observations)) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tvalue &&\n\t\t\t\t\t\ttypeof value === \"object\" &&\n\t\t\t\t\t\ttypeof (value as ObservationEntry).count === \"number\" &&\n\t\t\t\t\t\tNumber.isFinite((value as ObservationEntry).count) &&\n\t\t\t\t\t\ttypeof (value as ObservationEntry).lastAt === \"string\"\n\t\t\t\t\t) {\n\t\t\t\t\t\tclean.observations[key] = {\n\t\t\t\t\t\t\tcount: Math.min(Math.max(0, Math.floor((value as ObservationEntry).count)), MAX_COUNT),\n\t\t\t\t\t\t\tlastAt: (value as ObservationEntry).lastAt,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn clean;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Unreadable/corrupt store: start fresh in memory; the next increment rewrites the file.\n\t\t}\n\t\treturn { version: 1, observations: {} };\n\t}\n\n\tprivate save(file: ObservationStoreFile): void {\n\t\tmkdirSync(dirname(this.filePath), { recursive: true });\n\t\twriteFileSync(this.filePath, `${JSON.stringify(file, null, \"\\t\")}\\n`, \"utf-8\");\n\t}\n\n\t/** Evict least-recently-incremented keys until the store is back within {@link MAX_KEYS}. */\n\tprivate evict(file: ObservationStoreFile): void {\n\t\tconst keys = Object.keys(file.observations);\n\t\tif (keys.length <= MAX_KEYS) return;\n\t\tkeys.sort((a, b) => {\n\t\t\tconst la = file.observations[a]!.lastAt;\n\t\t\tconst lb = file.observations[b]!.lastAt;\n\t\t\treturn la < lb ? -1 : la > lb ? 1 : 0;\n\t\t});\n\t\tfor (const key of keys.slice(0, keys.length - MAX_KEYS)) {\n\t\t\tdelete file.observations[key];\n\t\t}\n\t}\n\n\t/** Record one more observation of `key` and return the new (capped) count. */\n\tincrement(key: string, at?: string): number {\n\t\tconst file = this.load();\n\t\tconst now = at ?? new Date().toISOString();\n\t\tconst count = Math.min((file.observations[key]?.count ?? 0) + 1, MAX_COUNT);\n\t\tfile.observations[key] = { count, lastAt: now };\n\t\tthis.evict(file);\n\t\tthis.save(file);\n\t\treturn count;\n\t}\n\n\t/** Current observation count for `key` (0 if never observed). */\n\tget(key: string): number {\n\t\treturn this.load().observations[key]?.count ?? 0;\n\t}\n}\n"]}
@@ -0,0 +1,101 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ /**
5
+ * Durable, BOUNDED evidence-strength store for the learning gate (G6). The gate auto-applies a
6
+ * durable change only once it has been *observed* enough times; a single reflection pass sees a
7
+ * lesson once, so without persistence every proposal would look brand-new and never accumulate the
8
+ * repeated evidence the gate requires. This store counts how many times the SAME lesson (keyed by
9
+ * its durable layer + normalized summary) has been proposed across passes and sessions.
10
+ *
11
+ * File layout mirrors {@link ../models/fitness-store.ts}: a versioned JSON object under
12
+ * `<agentDir>/state/`, best-effort writes, and a corrupt/missing file recovers as a fresh store.
13
+ */
14
+ /** Cap per-key counts so a hot lesson can't grow unbounded (the gate only needs a small threshold). */
15
+ const MAX_COUNT = 100;
16
+ /** Cap the number of tracked keys; least-recently-incremented keys are evicted past this bound. */
17
+ const MAX_KEYS = 500;
18
+ /**
19
+ * Stable evidence key for a durable-change proposal: sha256 of the layer plus its summary,
20
+ * lowercased and whitespace-collapsed, truncated to 24 hex chars. Normalization means the same
21
+ * lesson re-observed with reworded whitespace/casing accumulates onto one key instead of scattering.
22
+ */
23
+ export function observationKey(layer, summary) {
24
+ const normalized = summary.toLowerCase().replace(/\s+/g, " ").trim();
25
+ return createHash("sha256").update(`${layer}\n${normalized}`).digest("hex").slice(0, 24);
26
+ }
27
+ export class ObservationStore {
28
+ filePath;
29
+ constructor(filePath) {
30
+ this.filePath = filePath;
31
+ }
32
+ static forAgentDir(agentDir) {
33
+ return new ObservationStore(join(agentDir, "state", "learning-observations.json"));
34
+ }
35
+ load() {
36
+ try {
37
+ if (!existsSync(this.filePath))
38
+ return { version: 1, observations: {} };
39
+ const parsed = JSON.parse(readFileSync(this.filePath, "utf-8"));
40
+ if (parsed &&
41
+ parsed.version === 1 &&
42
+ parsed.observations &&
43
+ typeof parsed.observations === "object" &&
44
+ !Array.isArray(parsed.observations)) {
45
+ // Sanitize per-entry so a partially-mangled file still yields a usable store rather than
46
+ // leaking NaN/undefined counts into the gate.
47
+ const clean = { version: 1, observations: {} };
48
+ for (const [key, value] of Object.entries(parsed.observations)) {
49
+ if (value &&
50
+ typeof value === "object" &&
51
+ typeof value.count === "number" &&
52
+ Number.isFinite(value.count) &&
53
+ typeof value.lastAt === "string") {
54
+ clean.observations[key] = {
55
+ count: Math.min(Math.max(0, Math.floor(value.count)), MAX_COUNT),
56
+ lastAt: value.lastAt,
57
+ };
58
+ }
59
+ }
60
+ return clean;
61
+ }
62
+ }
63
+ catch {
64
+ // Unreadable/corrupt store: start fresh in memory; the next increment rewrites the file.
65
+ }
66
+ return { version: 1, observations: {} };
67
+ }
68
+ save(file) {
69
+ mkdirSync(dirname(this.filePath), { recursive: true });
70
+ writeFileSync(this.filePath, `${JSON.stringify(file, null, "\t")}\n`, "utf-8");
71
+ }
72
+ /** Evict least-recently-incremented keys until the store is back within {@link MAX_KEYS}. */
73
+ evict(file) {
74
+ const keys = Object.keys(file.observations);
75
+ if (keys.length <= MAX_KEYS)
76
+ return;
77
+ keys.sort((a, b) => {
78
+ const la = file.observations[a].lastAt;
79
+ const lb = file.observations[b].lastAt;
80
+ return la < lb ? -1 : la > lb ? 1 : 0;
81
+ });
82
+ for (const key of keys.slice(0, keys.length - MAX_KEYS)) {
83
+ delete file.observations[key];
84
+ }
85
+ }
86
+ /** Record one more observation of `key` and return the new (capped) count. */
87
+ increment(key, at) {
88
+ const file = this.load();
89
+ const now = at ?? new Date().toISOString();
90
+ const count = Math.min((file.observations[key]?.count ?? 0) + 1, MAX_COUNT);
91
+ file.observations[key] = { count, lastAt: now };
92
+ this.evict(file);
93
+ this.save(file);
94
+ return count;
95
+ }
96
+ /** Current observation count for `key` (0 if never observed). */
97
+ get(key) {
98
+ return this.load().observations[key]?.count ?? 0;
99
+ }
100
+ }
101
+ //# sourceMappingURL=observation-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation-store.js","sourceRoot":"","sources":["../../../src/core/learning/observation-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Durable, BOUNDED evidence-strength store for the learning gate (G6). The gate auto-applies a\n * durable change only once it has been *observed* enough times; a single reflection pass sees a\n * lesson once, so without persistence every proposal would look brand-new and never accumulate the\n * repeated evidence the gate requires. This store counts how many times the SAME lesson (keyed by\n * its durable layer + normalized summary) has been proposed across passes and sessions.\n *\n * File layout mirrors {@link ../models/fitness-store.ts}: a versioned JSON object under\n * `<agentDir>/state/`, best-effort writes, and a corrupt/missing file recovers as a fresh store.\n */\n\n/** Cap per-key counts so a hot lesson can't grow unbounded (the gate only needs a small threshold). */\nconst MAX_COUNT = 100;\n/** Cap the number of tracked keys; least-recently-incremented keys are evicted past this bound. */\nconst MAX_KEYS = 500;\n\ninterface ObservationEntry {\n\tcount: number;\n\t/** ISO timestamp of the most recent increment; drives least-recently-incremented eviction. */\n\tlastAt: string;\n}\n\ninterface ObservationStoreFile {\n\tversion: 1;\n\t/** observationKey -> accumulated evidence for that lesson. */\n\tobservations: Record<string, ObservationEntry>;\n}\n\n/**\n * Stable evidence key for a durable-change proposal: sha256 of the layer plus its summary,\n * lowercased and whitespace-collapsed, truncated to 24 hex chars. Normalization means the same\n * lesson re-observed with reworded whitespace/casing accumulates onto one key instead of scattering.\n */\nexport function observationKey(layer: string, summary: string): string {\n\tconst normalized = summary.toLowerCase().replace(/\\s+/g, \" \").trim();\n\treturn createHash(\"sha256\").update(`${layer}\\n${normalized}`).digest(\"hex\").slice(0, 24);\n}\n\nexport class ObservationStore {\n\tprivate readonly filePath: string;\n\n\tconstructor(filePath: string) {\n\t\tthis.filePath = filePath;\n\t}\n\n\tstatic forAgentDir(agentDir: string): ObservationStore {\n\t\treturn new ObservationStore(join(agentDir, \"state\", \"learning-observations.json\"));\n\t}\n\n\tprivate load(): ObservationStoreFile {\n\t\ttry {\n\t\t\tif (!existsSync(this.filePath)) return { version: 1, observations: {} };\n\t\t\tconst parsed = JSON.parse(readFileSync(this.filePath, \"utf-8\")) as ObservationStoreFile;\n\t\t\tif (\n\t\t\t\tparsed &&\n\t\t\t\tparsed.version === 1 &&\n\t\t\t\tparsed.observations &&\n\t\t\t\ttypeof parsed.observations === \"object\" &&\n\t\t\t\t!Array.isArray(parsed.observations)\n\t\t\t) {\n\t\t\t\t// Sanitize per-entry so a partially-mangled file still yields a usable store rather than\n\t\t\t\t// leaking NaN/undefined counts into the gate.\n\t\t\t\tconst clean: ObservationStoreFile = { version: 1, observations: {} };\n\t\t\t\tfor (const [key, value] of Object.entries(parsed.observations)) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tvalue &&\n\t\t\t\t\t\ttypeof value === \"object\" &&\n\t\t\t\t\t\ttypeof (value as ObservationEntry).count === \"number\" &&\n\t\t\t\t\t\tNumber.isFinite((value as ObservationEntry).count) &&\n\t\t\t\t\t\ttypeof (value as ObservationEntry).lastAt === \"string\"\n\t\t\t\t\t) {\n\t\t\t\t\t\tclean.observations[key] = {\n\t\t\t\t\t\t\tcount: Math.min(Math.max(0, Math.floor((value as ObservationEntry).count)), MAX_COUNT),\n\t\t\t\t\t\t\tlastAt: (value as ObservationEntry).lastAt,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn clean;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Unreadable/corrupt store: start fresh in memory; the next increment rewrites the file.\n\t\t}\n\t\treturn { version: 1, observations: {} };\n\t}\n\n\tprivate save(file: ObservationStoreFile): void {\n\t\tmkdirSync(dirname(this.filePath), { recursive: true });\n\t\twriteFileSync(this.filePath, `${JSON.stringify(file, null, \"\\t\")}\\n`, \"utf-8\");\n\t}\n\n\t/** Evict least-recently-incremented keys until the store is back within {@link MAX_KEYS}. */\n\tprivate evict(file: ObservationStoreFile): void {\n\t\tconst keys = Object.keys(file.observations);\n\t\tif (keys.length <= MAX_KEYS) return;\n\t\tkeys.sort((a, b) => {\n\t\t\tconst la = file.observations[a]!.lastAt;\n\t\t\tconst lb = file.observations[b]!.lastAt;\n\t\t\treturn la < lb ? -1 : la > lb ? 1 : 0;\n\t\t});\n\t\tfor (const key of keys.slice(0, keys.length - MAX_KEYS)) {\n\t\t\tdelete file.observations[key];\n\t\t}\n\t}\n\n\t/** Record one more observation of `key` and return the new (capped) count. */\n\tincrement(key: string, at?: string): number {\n\t\tconst file = this.load();\n\t\tconst now = at ?? new Date().toISOString();\n\t\tconst count = Math.min((file.observations[key]?.count ?? 0) + 1, MAX_COUNT);\n\t\tfile.observations[key] = { count, lastAt: now };\n\t\tthis.evict(file);\n\t\tthis.save(file);\n\t\treturn count;\n\t}\n\n\t/** Current observation count for `key` (0 if never observed). */\n\tget(key: string): number {\n\t\treturn this.load().observations[key]?.count ?? 0;\n\t}\n}\n"]}
@@ -36,6 +36,25 @@ export declare function deriveModelCapabilityProfile(args: {
36
36
  contextWindow?: number;
37
37
  mode?: ModelCapabilityMode;
38
38
  }): ModelCapabilityProfile;
39
+ /** Goal-continuation (autosteer) budgets, scaled to the session's capability class. */
40
+ export interface ContinuationBudgets {
41
+ /** Maximum continuation prompts per idle goal loop. */
42
+ maxTurns: number;
43
+ /** Wall-clock budget in minutes; 0 means "disabled" (upstream convention). */
44
+ maxWallClockMinutes: number;
45
+ }
46
+ /** Lean-class continuation caps: a 16-32k window cannot afford the full autosteer budget. */
47
+ export declare const MODEL_CAPABILITY_LEAN_MAX_CONTINUE_TURNS = 2;
48
+ export declare const MODEL_CAPABILITY_LEAN_MAX_CONTINUE_WALL_CLOCK_MINUTES = 5;
49
+ /**
50
+ * Scale goal-continuation budgets to the model's capability class. Lean-window models (16-32k) keep
51
+ * autonomy but at a reduced budget; every other class passes the configured budget through unchanged
52
+ * (full stays full; minimal/chat never reach here — their background lanes are disabled upstream).
53
+ *
54
+ * Both dimensions are a straight `min(configured, cap)`: a disabled wall-clock budget (0) stays
55
+ * disabled because `min(0, cap) === 0`, so the cap only ever tightens an already-positive budget.
56
+ */
57
+ export declare function scaleContinuationBudgetsForCapability(profile: ModelCapabilityProfile, budgets: ContinuationBudgets): ContinuationBudgets;
39
58
  /** Apply the profile's allow/block lists to a requested tool-name list, preserving order. */
40
59
  export declare function filterToolNamesForCapability(toolNames: readonly string[], profile: ModelCapabilityProfile): string[];
41
60
  //# sourceMappingURL=model-capability.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-capability.d.ts","sourceRoot":"","sources":["../../src/core/model-capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAExE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,KAAK,GAAG,oBAAoB,CAAC;AAExE,MAAM,WAAW,sBAAsB;IACtC,KAAK,EAAE,oBAAoB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,0FAA0F;IAC1F,sBAAsB,EAAE,OAAO,CAAC;IAChC,4EAA4E;IAC5E,mBAAmB,EAAE,MAAM,CAAC;CAC5B;AAED,8DAA8D;AAC9D,eAAO,MAAM,iCAAiC,QAAS,CAAC;AACxD,oFAAoF;AACpF,eAAO,MAAM,iCAAiC,QAAS,CAAC;AACxD,+EAA+E;AAC/E,eAAO,MAAM,oCAAoC,OAAQ,CAAC;AAE1D,eAAO,MAAM,mCAAmC,EAAE,SAAS,MAAM,EAAkC,CAAC;AACpG,eAAO,MAAM,sCAAsC,EAAE,SAAS,MAAM,EAOnE,CAAC;AACF,eAAO,MAAM,mCAAmC,EAAE,SAAS,MAAM,EAAO,CAAC;AAEzE,eAAO,MAAM,8BAA8B,OAAO,CAAC;AA0CnD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,mBAAmB,CAAC;CAC3B,GAAG,sBAAsB,CA2BzB;AAED,6FAA6F;AAC7F,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,sBAAsB,GAAG,MAAM,EAAE,CAWpH","sourcesContent":["/**\n * Model capability auto-detection: derive what the harness may load onto a model FROM the model's\n * own metadata (`Model.contextWindow`), so small open models (4k/8k/16k windows, sub-1B params)\n * can still hold a usable chat instead of drowning in tool schemas and background-lane prompts.\n *\n * Derivation is metadata-first; defaults apply only when the metadata is missing (unknown/zero\n * window keeps today's full behavior rather than guessing). Detection can be disabled or forced\n * per class via the `modelCapability.mode` setting.\n */\n\nexport type ModelCapabilityClass = \"full\" | \"lean\" | \"minimal\" | \"chat\";\n\nexport type ModelCapabilityMode = \"auto\" | \"off\" | ModelCapabilityClass;\n\nexport interface ModelCapabilityProfile {\n\tclass: ModelCapabilityClass;\n\tcontextWindow?: number;\n\treasonCode: string;\n\t/** Allow-list; undefined = no allow-list restriction. */\n\tallowedToolNames?: readonly string[];\n\t/** Block-list applied after the allow-list; undefined = nothing blocked. */\n\tblockedToolNames?: readonly string[];\n\t/** Whether idle background lanes (goal auto-continue, research) may run on this model. */\n\tbackgroundLanesEnabled: boolean;\n\t/** Output-token cap for lane isolated completions, scaled to the window. */\n\tlaneMaxOutputTokens: number;\n}\n\n/** Windows at or above this keep the full harness surface. */\nexport const MODEL_CAPABILITY_FULL_MIN_CONTEXT = 32_768;\n/** Windows at or above this keep core tools but shed background-autonomy extras. */\nexport const MODEL_CAPABILITY_LEAN_MIN_CONTEXT = 16_384;\n/** Windows at or above this get the minimal coding set; below is chat-only. */\nexport const MODEL_CAPABILITY_MINIMAL_MIN_CONTEXT = 8_192;\n\nexport const MODEL_CAPABILITY_LEAN_BLOCKED_TOOLS: readonly string[] = [\"delegate\", \"context_audit\"];\nexport const MODEL_CAPABILITY_MINIMAL_ALLOWED_TOOLS: readonly string[] = [\n\t\"read\",\n\t\"bash\",\n\t\"edit\",\n\t\"write\",\n\t// The executor tool: minimal-class models ARE the daily-ops executors, and its schema is tiny.\n\t\"run_toolkit_script\",\n];\nexport const MODEL_CAPABILITY_CHAT_ALLOWED_TOOLS: readonly string[] = [];\n\nexport const DEFAULT_LANE_MAX_OUTPUT_TOKENS = 2048;\nconst MIN_LANE_MAX_OUTPUT_TOKENS = 256;\n\nfunction laneOutputTokensForWindow(contextWindow: number | undefined): number {\n\tif (contextWindow === undefined || contextWindow <= 0) return DEFAULT_LANE_MAX_OUTPUT_TOKENS;\n\t// A lane completion may use at most an eighth of the window for output, floored so tiny\n\t// windows still produce something parseable.\n\treturn Math.min(DEFAULT_LANE_MAX_OUTPUT_TOKENS, Math.max(MIN_LANE_MAX_OUTPUT_TOKENS, Math.floor(contextWindow / 8)));\n}\n\nfunction profileForClass(\n\tcapabilityClass: ModelCapabilityClass,\n\treasonCode: string,\n\tcontextWindow: number | undefined,\n): ModelCapabilityProfile {\n\tconst base = {\n\t\tclass: capabilityClass,\n\t\treasonCode,\n\t\tbackgroundLanesEnabled: true,\n\t\tlaneMaxOutputTokens: laneOutputTokensForWindow(contextWindow),\n\t\t...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),\n\t};\n\tswitch (capabilityClass) {\n\t\tcase \"full\":\n\t\t\treturn base;\n\t\tcase \"lean\":\n\t\t\treturn { ...base, blockedToolNames: MODEL_CAPABILITY_LEAN_BLOCKED_TOOLS };\n\t\tcase \"minimal\":\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tallowedToolNames: MODEL_CAPABILITY_MINIMAL_ALLOWED_TOOLS,\n\t\t\t\tbackgroundLanesEnabled: false,\n\t\t\t};\n\t\tcase \"chat\":\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tallowedToolNames: MODEL_CAPABILITY_CHAT_ALLOWED_TOOLS,\n\t\t\t\tbackgroundLanesEnabled: false,\n\t\t\t};\n\t}\n}\n\nexport function deriveModelCapabilityProfile(args: {\n\tcontextWindow?: number;\n\tmode?: ModelCapabilityMode;\n}): ModelCapabilityProfile {\n\tconst mode = args.mode ?? \"auto\";\n\tconst contextWindow =\n\t\targs.contextWindow !== undefined && Number.isFinite(args.contextWindow) && args.contextWindow > 0\n\t\t\t? args.contextWindow\n\t\t\t: undefined;\n\tif (mode === \"off\") {\n\t\treturn profileForClass(\"full\", \"detection_disabled\", contextWindow);\n\t}\n\tif (mode !== \"auto\") {\n\t\treturn profileForClass(mode, \"forced_by_setting\", contextWindow);\n\t}\n\n\tif (contextWindow === undefined) {\n\t\t// Metadata missing: defaults, never guesses.\n\t\treturn profileForClass(\"full\", \"unknown_context_window_defaults\", undefined);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_FULL_MIN_CONTEXT) {\n\t\treturn profileForClass(\"full\", \"large_context_window\", contextWindow);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_LEAN_MIN_CONTEXT) {\n\t\treturn profileForClass(\"lean\", \"lean_context_window\", contextWindow);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_MINIMAL_MIN_CONTEXT) {\n\t\treturn profileForClass(\"minimal\", \"minimal_context_window\", contextWindow);\n\t}\n\treturn profileForClass(\"chat\", \"chat_only_context_window\", contextWindow);\n}\n\n/** Apply the profile's allow/block lists to a requested tool-name list, preserving order. */\nexport function filterToolNamesForCapability(toolNames: readonly string[], profile: ModelCapabilityProfile): string[] {\n\tlet filtered = [...toolNames];\n\tif (profile.allowedToolNames !== undefined) {\n\t\tconst allowed = new Set(profile.allowedToolNames);\n\t\tfiltered = filtered.filter((name) => allowed.has(name));\n\t}\n\tif (profile.blockedToolNames !== undefined) {\n\t\tconst blocked = new Set(profile.blockedToolNames);\n\t\tfiltered = filtered.filter((name) => !blocked.has(name));\n\t}\n\treturn filtered;\n}\n"]}
1
+ {"version":3,"file":"model-capability.d.ts","sourceRoot":"","sources":["../../src/core/model-capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAExE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,KAAK,GAAG,oBAAoB,CAAC;AAExE,MAAM,WAAW,sBAAsB;IACtC,KAAK,EAAE,oBAAoB,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,0FAA0F;IAC1F,sBAAsB,EAAE,OAAO,CAAC;IAChC,4EAA4E;IAC5E,mBAAmB,EAAE,MAAM,CAAC;CAC5B;AAED,8DAA8D;AAC9D,eAAO,MAAM,iCAAiC,QAAS,CAAC;AACxD,oFAAoF;AACpF,eAAO,MAAM,iCAAiC,QAAS,CAAC;AACxD,+EAA+E;AAC/E,eAAO,MAAM,oCAAoC,OAAQ,CAAC;AAE1D,eAAO,MAAM,mCAAmC,EAAE,SAAS,MAAM,EAAkC,CAAC;AACpG,eAAO,MAAM,sCAAsC,EAAE,SAAS,MAAM,EAOnE,CAAC;AACF,eAAO,MAAM,mCAAmC,EAAE,SAAS,MAAM,EAAO,CAAC;AAEzE,eAAO,MAAM,8BAA8B,OAAO,CAAC;AA0CnD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE;IAClD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,mBAAmB,CAAC;CAC3B,GAAG,sBAAsB,CA2BzB;AAED,uFAAuF;AACvF,MAAM,WAAW,mBAAmB;IACnC,uDAAuD;IACvD,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,mBAAmB,EAAE,MAAM,CAAC;CAC5B;AAED,6FAA6F;AAC7F,eAAO,MAAM,wCAAwC,IAAI,CAAC;AAC1D,eAAO,MAAM,qDAAqD,IAAI,CAAC;AAEvE;;;;;;;GAOG;AACH,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,sBAAsB,EAC/B,OAAO,EAAE,mBAAmB,GAC1B,mBAAmB,CAMrB;AAED,6FAA6F;AAC7F,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,sBAAsB,GAAG,MAAM,EAAE,CAWpH","sourcesContent":["/**\n * Model capability auto-detection: derive what the harness may load onto a model FROM the model's\n * own metadata (`Model.contextWindow`), so small open models (4k/8k/16k windows, sub-1B params)\n * can still hold a usable chat instead of drowning in tool schemas and background-lane prompts.\n *\n * Derivation is metadata-first; defaults apply only when the metadata is missing (unknown/zero\n * window keeps today's full behavior rather than guessing). Detection can be disabled or forced\n * per class via the `modelCapability.mode` setting.\n */\n\nexport type ModelCapabilityClass = \"full\" | \"lean\" | \"minimal\" | \"chat\";\n\nexport type ModelCapabilityMode = \"auto\" | \"off\" | ModelCapabilityClass;\n\nexport interface ModelCapabilityProfile {\n\tclass: ModelCapabilityClass;\n\tcontextWindow?: number;\n\treasonCode: string;\n\t/** Allow-list; undefined = no allow-list restriction. */\n\tallowedToolNames?: readonly string[];\n\t/** Block-list applied after the allow-list; undefined = nothing blocked. */\n\tblockedToolNames?: readonly string[];\n\t/** Whether idle background lanes (goal auto-continue, research) may run on this model. */\n\tbackgroundLanesEnabled: boolean;\n\t/** Output-token cap for lane isolated completions, scaled to the window. */\n\tlaneMaxOutputTokens: number;\n}\n\n/** Windows at or above this keep the full harness surface. */\nexport const MODEL_CAPABILITY_FULL_MIN_CONTEXT = 32_768;\n/** Windows at or above this keep core tools but shed background-autonomy extras. */\nexport const MODEL_CAPABILITY_LEAN_MIN_CONTEXT = 16_384;\n/** Windows at or above this get the minimal coding set; below is chat-only. */\nexport const MODEL_CAPABILITY_MINIMAL_MIN_CONTEXT = 8_192;\n\nexport const MODEL_CAPABILITY_LEAN_BLOCKED_TOOLS: readonly string[] = [\"delegate\", \"context_audit\"];\nexport const MODEL_CAPABILITY_MINIMAL_ALLOWED_TOOLS: readonly string[] = [\n\t\"read\",\n\t\"bash\",\n\t\"edit\",\n\t\"write\",\n\t// The executor tool: minimal-class models ARE the daily-ops executors, and its schema is tiny.\n\t\"run_toolkit_script\",\n];\nexport const MODEL_CAPABILITY_CHAT_ALLOWED_TOOLS: readonly string[] = [];\n\nexport const DEFAULT_LANE_MAX_OUTPUT_TOKENS = 2048;\nconst MIN_LANE_MAX_OUTPUT_TOKENS = 256;\n\nfunction laneOutputTokensForWindow(contextWindow: number | undefined): number {\n\tif (contextWindow === undefined || contextWindow <= 0) return DEFAULT_LANE_MAX_OUTPUT_TOKENS;\n\t// A lane completion may use at most an eighth of the window for output, floored so tiny\n\t// windows still produce something parseable.\n\treturn Math.min(DEFAULT_LANE_MAX_OUTPUT_TOKENS, Math.max(MIN_LANE_MAX_OUTPUT_TOKENS, Math.floor(contextWindow / 8)));\n}\n\nfunction profileForClass(\n\tcapabilityClass: ModelCapabilityClass,\n\treasonCode: string,\n\tcontextWindow: number | undefined,\n): ModelCapabilityProfile {\n\tconst base = {\n\t\tclass: capabilityClass,\n\t\treasonCode,\n\t\tbackgroundLanesEnabled: true,\n\t\tlaneMaxOutputTokens: laneOutputTokensForWindow(contextWindow),\n\t\t...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),\n\t};\n\tswitch (capabilityClass) {\n\t\tcase \"full\":\n\t\t\treturn base;\n\t\tcase \"lean\":\n\t\t\treturn { ...base, blockedToolNames: MODEL_CAPABILITY_LEAN_BLOCKED_TOOLS };\n\t\tcase \"minimal\":\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tallowedToolNames: MODEL_CAPABILITY_MINIMAL_ALLOWED_TOOLS,\n\t\t\t\tbackgroundLanesEnabled: false,\n\t\t\t};\n\t\tcase \"chat\":\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tallowedToolNames: MODEL_CAPABILITY_CHAT_ALLOWED_TOOLS,\n\t\t\t\tbackgroundLanesEnabled: false,\n\t\t\t};\n\t}\n}\n\nexport function deriveModelCapabilityProfile(args: {\n\tcontextWindow?: number;\n\tmode?: ModelCapabilityMode;\n}): ModelCapabilityProfile {\n\tconst mode = args.mode ?? \"auto\";\n\tconst contextWindow =\n\t\targs.contextWindow !== undefined && Number.isFinite(args.contextWindow) && args.contextWindow > 0\n\t\t\t? args.contextWindow\n\t\t\t: undefined;\n\tif (mode === \"off\") {\n\t\treturn profileForClass(\"full\", \"detection_disabled\", contextWindow);\n\t}\n\tif (mode !== \"auto\") {\n\t\treturn profileForClass(mode, \"forced_by_setting\", contextWindow);\n\t}\n\n\tif (contextWindow === undefined) {\n\t\t// Metadata missing: defaults, never guesses.\n\t\treturn profileForClass(\"full\", \"unknown_context_window_defaults\", undefined);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_FULL_MIN_CONTEXT) {\n\t\treturn profileForClass(\"full\", \"large_context_window\", contextWindow);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_LEAN_MIN_CONTEXT) {\n\t\treturn profileForClass(\"lean\", \"lean_context_window\", contextWindow);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_MINIMAL_MIN_CONTEXT) {\n\t\treturn profileForClass(\"minimal\", \"minimal_context_window\", contextWindow);\n\t}\n\treturn profileForClass(\"chat\", \"chat_only_context_window\", contextWindow);\n}\n\n/** Goal-continuation (autosteer) budgets, scaled to the session's capability class. */\nexport interface ContinuationBudgets {\n\t/** Maximum continuation prompts per idle goal loop. */\n\tmaxTurns: number;\n\t/** Wall-clock budget in minutes; 0 means \"disabled\" (upstream convention). */\n\tmaxWallClockMinutes: number;\n}\n\n/** Lean-class continuation caps: a 16-32k window cannot afford the full autosteer budget. */\nexport const MODEL_CAPABILITY_LEAN_MAX_CONTINUE_TURNS = 2;\nexport const MODEL_CAPABILITY_LEAN_MAX_CONTINUE_WALL_CLOCK_MINUTES = 5;\n\n/**\n * Scale goal-continuation budgets to the model's capability class. Lean-window models (16-32k) keep\n * autonomy but at a reduced budget; every other class passes the configured budget through unchanged\n * (full stays full; minimal/chat never reach here — their background lanes are disabled upstream).\n *\n * Both dimensions are a straight `min(configured, cap)`: a disabled wall-clock budget (0) stays\n * disabled because `min(0, cap) === 0`, so the cap only ever tightens an already-positive budget.\n */\nexport function scaleContinuationBudgetsForCapability(\n\tprofile: ModelCapabilityProfile,\n\tbudgets: ContinuationBudgets,\n): ContinuationBudgets {\n\tif (profile.class !== \"lean\") return budgets;\n\treturn {\n\t\tmaxTurns: Math.min(budgets.maxTurns, MODEL_CAPABILITY_LEAN_MAX_CONTINUE_TURNS),\n\t\tmaxWallClockMinutes: Math.min(budgets.maxWallClockMinutes, MODEL_CAPABILITY_LEAN_MAX_CONTINUE_WALL_CLOCK_MINUTES),\n\t};\n}\n\n/** Apply the profile's allow/block lists to a requested tool-name list, preserving order. */\nexport function filterToolNamesForCapability(toolNames: readonly string[], profile: ModelCapabilityProfile): string[] {\n\tlet filtered = [...toolNames];\n\tif (profile.allowedToolNames !== undefined) {\n\t\tconst allowed = new Set(profile.allowedToolNames);\n\t\tfiltered = filtered.filter((name) => allowed.has(name));\n\t}\n\tif (profile.blockedToolNames !== undefined) {\n\t\tconst blocked = new Set(profile.blockedToolNames);\n\t\tfiltered = filtered.filter((name) => !blocked.has(name));\n\t}\n\treturn filtered;\n}\n"]}
@@ -85,6 +85,25 @@ export function deriveModelCapabilityProfile(args) {
85
85
  }
86
86
  return profileForClass("chat", "chat_only_context_window", contextWindow);
87
87
  }
88
+ /** Lean-class continuation caps: a 16-32k window cannot afford the full autosteer budget. */
89
+ export const MODEL_CAPABILITY_LEAN_MAX_CONTINUE_TURNS = 2;
90
+ export const MODEL_CAPABILITY_LEAN_MAX_CONTINUE_WALL_CLOCK_MINUTES = 5;
91
+ /**
92
+ * Scale goal-continuation budgets to the model's capability class. Lean-window models (16-32k) keep
93
+ * autonomy but at a reduced budget; every other class passes the configured budget through unchanged
94
+ * (full stays full; minimal/chat never reach here — their background lanes are disabled upstream).
95
+ *
96
+ * Both dimensions are a straight `min(configured, cap)`: a disabled wall-clock budget (0) stays
97
+ * disabled because `min(0, cap) === 0`, so the cap only ever tightens an already-positive budget.
98
+ */
99
+ export function scaleContinuationBudgetsForCapability(profile, budgets) {
100
+ if (profile.class !== "lean")
101
+ return budgets;
102
+ return {
103
+ maxTurns: Math.min(budgets.maxTurns, MODEL_CAPABILITY_LEAN_MAX_CONTINUE_TURNS),
104
+ maxWallClockMinutes: Math.min(budgets.maxWallClockMinutes, MODEL_CAPABILITY_LEAN_MAX_CONTINUE_WALL_CLOCK_MINUTES),
105
+ };
106
+ }
88
107
  /** Apply the profile's allow/block lists to a requested tool-name list, preserving order. */
89
108
  export function filterToolNamesForCapability(toolNames, profile) {
90
109
  let filtered = [...toolNames];
@@ -1 +1 @@
1
- {"version":3,"file":"model-capability.js","sourceRoot":"","sources":["../../src/core/model-capability.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model capability auto-detection: derive what the harness may load onto a model FROM the model's\n * own metadata (`Model.contextWindow`), so small open models (4k/8k/16k windows, sub-1B params)\n * can still hold a usable chat instead of drowning in tool schemas and background-lane prompts.\n *\n * Derivation is metadata-first; defaults apply only when the metadata is missing (unknown/zero\n * window keeps today's full behavior rather than guessing). Detection can be disabled or forced\n * per class via the `modelCapability.mode` setting.\n */\n\nexport type ModelCapabilityClass = \"full\" | \"lean\" | \"minimal\" | \"chat\";\n\nexport type ModelCapabilityMode = \"auto\" | \"off\" | ModelCapabilityClass;\n\nexport interface ModelCapabilityProfile {\n\tclass: ModelCapabilityClass;\n\tcontextWindow?: number;\n\treasonCode: string;\n\t/** Allow-list; undefined = no allow-list restriction. */\n\tallowedToolNames?: readonly string[];\n\t/** Block-list applied after the allow-list; undefined = nothing blocked. */\n\tblockedToolNames?: readonly string[];\n\t/** Whether idle background lanes (goal auto-continue, research) may run on this model. */\n\tbackgroundLanesEnabled: boolean;\n\t/** Output-token cap for lane isolated completions, scaled to the window. */\n\tlaneMaxOutputTokens: number;\n}\n\n/** Windows at or above this keep the full harness surface. */\nexport const MODEL_CAPABILITY_FULL_MIN_CONTEXT = 32_768;\n/** Windows at or above this keep core tools but shed background-autonomy extras. */\nexport const MODEL_CAPABILITY_LEAN_MIN_CONTEXT = 16_384;\n/** Windows at or above this get the minimal coding set; below is chat-only. */\nexport const MODEL_CAPABILITY_MINIMAL_MIN_CONTEXT = 8_192;\n\nexport const MODEL_CAPABILITY_LEAN_BLOCKED_TOOLS: readonly string[] = [\"delegate\", \"context_audit\"];\nexport const MODEL_CAPABILITY_MINIMAL_ALLOWED_TOOLS: readonly string[] = [\n\t\"read\",\n\t\"bash\",\n\t\"edit\",\n\t\"write\",\n\t// The executor tool: minimal-class models ARE the daily-ops executors, and its schema is tiny.\n\t\"run_toolkit_script\",\n];\nexport const MODEL_CAPABILITY_CHAT_ALLOWED_TOOLS: readonly string[] = [];\n\nexport const DEFAULT_LANE_MAX_OUTPUT_TOKENS = 2048;\nconst MIN_LANE_MAX_OUTPUT_TOKENS = 256;\n\nfunction laneOutputTokensForWindow(contextWindow: number | undefined): number {\n\tif (contextWindow === undefined || contextWindow <= 0) return DEFAULT_LANE_MAX_OUTPUT_TOKENS;\n\t// A lane completion may use at most an eighth of the window for output, floored so tiny\n\t// windows still produce something parseable.\n\treturn Math.min(DEFAULT_LANE_MAX_OUTPUT_TOKENS, Math.max(MIN_LANE_MAX_OUTPUT_TOKENS, Math.floor(contextWindow / 8)));\n}\n\nfunction profileForClass(\n\tcapabilityClass: ModelCapabilityClass,\n\treasonCode: string,\n\tcontextWindow: number | undefined,\n): ModelCapabilityProfile {\n\tconst base = {\n\t\tclass: capabilityClass,\n\t\treasonCode,\n\t\tbackgroundLanesEnabled: true,\n\t\tlaneMaxOutputTokens: laneOutputTokensForWindow(contextWindow),\n\t\t...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),\n\t};\n\tswitch (capabilityClass) {\n\t\tcase \"full\":\n\t\t\treturn base;\n\t\tcase \"lean\":\n\t\t\treturn { ...base, blockedToolNames: MODEL_CAPABILITY_LEAN_BLOCKED_TOOLS };\n\t\tcase \"minimal\":\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tallowedToolNames: MODEL_CAPABILITY_MINIMAL_ALLOWED_TOOLS,\n\t\t\t\tbackgroundLanesEnabled: false,\n\t\t\t};\n\t\tcase \"chat\":\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tallowedToolNames: MODEL_CAPABILITY_CHAT_ALLOWED_TOOLS,\n\t\t\t\tbackgroundLanesEnabled: false,\n\t\t\t};\n\t}\n}\n\nexport function deriveModelCapabilityProfile(args: {\n\tcontextWindow?: number;\n\tmode?: ModelCapabilityMode;\n}): ModelCapabilityProfile {\n\tconst mode = args.mode ?? \"auto\";\n\tconst contextWindow =\n\t\targs.contextWindow !== undefined && Number.isFinite(args.contextWindow) && args.contextWindow > 0\n\t\t\t? args.contextWindow\n\t\t\t: undefined;\n\tif (mode === \"off\") {\n\t\treturn profileForClass(\"full\", \"detection_disabled\", contextWindow);\n\t}\n\tif (mode !== \"auto\") {\n\t\treturn profileForClass(mode, \"forced_by_setting\", contextWindow);\n\t}\n\n\tif (contextWindow === undefined) {\n\t\t// Metadata missing: defaults, never guesses.\n\t\treturn profileForClass(\"full\", \"unknown_context_window_defaults\", undefined);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_FULL_MIN_CONTEXT) {\n\t\treturn profileForClass(\"full\", \"large_context_window\", contextWindow);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_LEAN_MIN_CONTEXT) {\n\t\treturn profileForClass(\"lean\", \"lean_context_window\", contextWindow);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_MINIMAL_MIN_CONTEXT) {\n\t\treturn profileForClass(\"minimal\", \"minimal_context_window\", contextWindow);\n\t}\n\treturn profileForClass(\"chat\", \"chat_only_context_window\", contextWindow);\n}\n\n/** Apply the profile's allow/block lists to a requested tool-name list, preserving order. */\nexport function filterToolNamesForCapability(toolNames: readonly string[], profile: ModelCapabilityProfile): string[] {\n\tlet filtered = [...toolNames];\n\tif (profile.allowedToolNames !== undefined) {\n\t\tconst allowed = new Set(profile.allowedToolNames);\n\t\tfiltered = filtered.filter((name) => allowed.has(name));\n\t}\n\tif (profile.blockedToolNames !== undefined) {\n\t\tconst blocked = new Set(profile.blockedToolNames);\n\t\tfiltered = filtered.filter((name) => !blocked.has(name));\n\t}\n\treturn filtered;\n}\n"]}
1
+ {"version":3,"file":"model-capability.js","sourceRoot":"","sources":["../../src/core/model-capability.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model capability auto-detection: derive what the harness may load onto a model FROM the model's\n * own metadata (`Model.contextWindow`), so small open models (4k/8k/16k windows, sub-1B params)\n * can still hold a usable chat instead of drowning in tool schemas and background-lane prompts.\n *\n * Derivation is metadata-first; defaults apply only when the metadata is missing (unknown/zero\n * window keeps today's full behavior rather than guessing). Detection can be disabled or forced\n * per class via the `modelCapability.mode` setting.\n */\n\nexport type ModelCapabilityClass = \"full\" | \"lean\" | \"minimal\" | \"chat\";\n\nexport type ModelCapabilityMode = \"auto\" | \"off\" | ModelCapabilityClass;\n\nexport interface ModelCapabilityProfile {\n\tclass: ModelCapabilityClass;\n\tcontextWindow?: number;\n\treasonCode: string;\n\t/** Allow-list; undefined = no allow-list restriction. */\n\tallowedToolNames?: readonly string[];\n\t/** Block-list applied after the allow-list; undefined = nothing blocked. */\n\tblockedToolNames?: readonly string[];\n\t/** Whether idle background lanes (goal auto-continue, research) may run on this model. */\n\tbackgroundLanesEnabled: boolean;\n\t/** Output-token cap for lane isolated completions, scaled to the window. */\n\tlaneMaxOutputTokens: number;\n}\n\n/** Windows at or above this keep the full harness surface. */\nexport const MODEL_CAPABILITY_FULL_MIN_CONTEXT = 32_768;\n/** Windows at or above this keep core tools but shed background-autonomy extras. */\nexport const MODEL_CAPABILITY_LEAN_MIN_CONTEXT = 16_384;\n/** Windows at or above this get the minimal coding set; below is chat-only. */\nexport const MODEL_CAPABILITY_MINIMAL_MIN_CONTEXT = 8_192;\n\nexport const MODEL_CAPABILITY_LEAN_BLOCKED_TOOLS: readonly string[] = [\"delegate\", \"context_audit\"];\nexport const MODEL_CAPABILITY_MINIMAL_ALLOWED_TOOLS: readonly string[] = [\n\t\"read\",\n\t\"bash\",\n\t\"edit\",\n\t\"write\",\n\t// The executor tool: minimal-class models ARE the daily-ops executors, and its schema is tiny.\n\t\"run_toolkit_script\",\n];\nexport const MODEL_CAPABILITY_CHAT_ALLOWED_TOOLS: readonly string[] = [];\n\nexport const DEFAULT_LANE_MAX_OUTPUT_TOKENS = 2048;\nconst MIN_LANE_MAX_OUTPUT_TOKENS = 256;\n\nfunction laneOutputTokensForWindow(contextWindow: number | undefined): number {\n\tif (contextWindow === undefined || contextWindow <= 0) return DEFAULT_LANE_MAX_OUTPUT_TOKENS;\n\t// A lane completion may use at most an eighth of the window for output, floored so tiny\n\t// windows still produce something parseable.\n\treturn Math.min(DEFAULT_LANE_MAX_OUTPUT_TOKENS, Math.max(MIN_LANE_MAX_OUTPUT_TOKENS, Math.floor(contextWindow / 8)));\n}\n\nfunction profileForClass(\n\tcapabilityClass: ModelCapabilityClass,\n\treasonCode: string,\n\tcontextWindow: number | undefined,\n): ModelCapabilityProfile {\n\tconst base = {\n\t\tclass: capabilityClass,\n\t\treasonCode,\n\t\tbackgroundLanesEnabled: true,\n\t\tlaneMaxOutputTokens: laneOutputTokensForWindow(contextWindow),\n\t\t...(contextWindow !== undefined && contextWindow > 0 ? { contextWindow } : {}),\n\t};\n\tswitch (capabilityClass) {\n\t\tcase \"full\":\n\t\t\treturn base;\n\t\tcase \"lean\":\n\t\t\treturn { ...base, blockedToolNames: MODEL_CAPABILITY_LEAN_BLOCKED_TOOLS };\n\t\tcase \"minimal\":\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tallowedToolNames: MODEL_CAPABILITY_MINIMAL_ALLOWED_TOOLS,\n\t\t\t\tbackgroundLanesEnabled: false,\n\t\t\t};\n\t\tcase \"chat\":\n\t\t\treturn {\n\t\t\t\t...base,\n\t\t\t\tallowedToolNames: MODEL_CAPABILITY_CHAT_ALLOWED_TOOLS,\n\t\t\t\tbackgroundLanesEnabled: false,\n\t\t\t};\n\t}\n}\n\nexport function deriveModelCapabilityProfile(args: {\n\tcontextWindow?: number;\n\tmode?: ModelCapabilityMode;\n}): ModelCapabilityProfile {\n\tconst mode = args.mode ?? \"auto\";\n\tconst contextWindow =\n\t\targs.contextWindow !== undefined && Number.isFinite(args.contextWindow) && args.contextWindow > 0\n\t\t\t? args.contextWindow\n\t\t\t: undefined;\n\tif (mode === \"off\") {\n\t\treturn profileForClass(\"full\", \"detection_disabled\", contextWindow);\n\t}\n\tif (mode !== \"auto\") {\n\t\treturn profileForClass(mode, \"forced_by_setting\", contextWindow);\n\t}\n\n\tif (contextWindow === undefined) {\n\t\t// Metadata missing: defaults, never guesses.\n\t\treturn profileForClass(\"full\", \"unknown_context_window_defaults\", undefined);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_FULL_MIN_CONTEXT) {\n\t\treturn profileForClass(\"full\", \"large_context_window\", contextWindow);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_LEAN_MIN_CONTEXT) {\n\t\treturn profileForClass(\"lean\", \"lean_context_window\", contextWindow);\n\t}\n\tif (contextWindow >= MODEL_CAPABILITY_MINIMAL_MIN_CONTEXT) {\n\t\treturn profileForClass(\"minimal\", \"minimal_context_window\", contextWindow);\n\t}\n\treturn profileForClass(\"chat\", \"chat_only_context_window\", contextWindow);\n}\n\n/** Goal-continuation (autosteer) budgets, scaled to the session's capability class. */\nexport interface ContinuationBudgets {\n\t/** Maximum continuation prompts per idle goal loop. */\n\tmaxTurns: number;\n\t/** Wall-clock budget in minutes; 0 means \"disabled\" (upstream convention). */\n\tmaxWallClockMinutes: number;\n}\n\n/** Lean-class continuation caps: a 16-32k window cannot afford the full autosteer budget. */\nexport const MODEL_CAPABILITY_LEAN_MAX_CONTINUE_TURNS = 2;\nexport const MODEL_CAPABILITY_LEAN_MAX_CONTINUE_WALL_CLOCK_MINUTES = 5;\n\n/**\n * Scale goal-continuation budgets to the model's capability class. Lean-window models (16-32k) keep\n * autonomy but at a reduced budget; every other class passes the configured budget through unchanged\n * (full stays full; minimal/chat never reach here — their background lanes are disabled upstream).\n *\n * Both dimensions are a straight `min(configured, cap)`: a disabled wall-clock budget (0) stays\n * disabled because `min(0, cap) === 0`, so the cap only ever tightens an already-positive budget.\n */\nexport function scaleContinuationBudgetsForCapability(\n\tprofile: ModelCapabilityProfile,\n\tbudgets: ContinuationBudgets,\n): ContinuationBudgets {\n\tif (profile.class !== \"lean\") return budgets;\n\treturn {\n\t\tmaxTurns: Math.min(budgets.maxTurns, MODEL_CAPABILITY_LEAN_MAX_CONTINUE_TURNS),\n\t\tmaxWallClockMinutes: Math.min(budgets.maxWallClockMinutes, MODEL_CAPABILITY_LEAN_MAX_CONTINUE_WALL_CLOCK_MINUTES),\n\t};\n}\n\n/** Apply the profile's allow/block lists to a requested tool-name list, preserving order. */\nexport function filterToolNamesForCapability(toolNames: readonly string[], profile: ModelCapabilityProfile): string[] {\n\tlet filtered = [...toolNames];\n\tif (profile.allowedToolNames !== undefined) {\n\t\tconst allowed = new Set(profile.allowedToolNames);\n\t\tfiltered = filtered.filter((name) => allowed.has(name));\n\t}\n\tif (profile.blockedToolNames !== undefined) {\n\t\tconst blocked = new Set(profile.blockedToolNames);\n\t\tfiltered = filtered.filter((name) => !blocked.has(name));\n\t}\n\treturn filtered;\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import { type ToolkitScript } from "../toolkit/script-registry.ts";
2
+ export interface ExecutorRouteVerdict {
3
+ execute: boolean;
4
+ scriptName?: string;
5
+ reason: string;
6
+ }
7
+ export declare function classifyExecutorTurn(prompt: string, scripts: readonly ToolkitScript[]): ExecutorRouteVerdict;
8
+ //# sourceMappingURL=executor-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor-route.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/executor-route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAmBvF,MAAM,WAAW,oBAAoB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,oBAAoB,CAc5G","sourcesContent":["import { matchToolkitScript, type ToolkitScript } from \"../toolkit/script-registry.ts\";\n\n/**\n * Executor-lane classifier (G16): decides when a USER turn is a direct toolkit command that a\n * small local executor model can own end-to-end (\"restore db staging\", \"run the status report\"),\n * instead of spending the frontier model on a one-tool reflex.\n *\n * Deliberately conservative — ALL of:\n * - the deterministic Level-0 matcher scores an EXACT/direct hit (the same margin rule that\n * gates tool-side matching; ambiguity never routes to the executor, it stays with the big\n * model + reflex brain), and\n * - the prompt LOOKS like a command: single line, short, no code fences/paths of substance.\n * Everything else falls through to normal routing. The executor model itself is gated by the\n * caller (configured + resolved + tool-call fitness), and failures escalate via the existing\n * cheap-tier escalation path.\n */\n\nconst EXECUTOR_MAX_PROMPT_CHARS = 120;\n\nexport interface ExecutorRouteVerdict {\n\texecute: boolean;\n\tscriptName?: string;\n\treason: string;\n}\n\nexport function classifyExecutorTurn(prompt: string, scripts: readonly ToolkitScript[]): ExecutorRouteVerdict {\n\tconst trimmed = prompt.trim();\n\tif (scripts.length === 0) return { execute: false, reason: \"no_toolkit_scripts\" };\n\tif (trimmed.length === 0 || trimmed.length > EXECUTOR_MAX_PROMPT_CHARS) {\n\t\treturn { execute: false, reason: \"not_command_shaped\" };\n\t}\n\tif (trimmed.includes(\"\\n\") || trimmed.includes(\"```\")) {\n\t\treturn { execute: false, reason: \"not_command_shaped\" };\n\t}\n\tconst match = matchToolkitScript(trimmed, scripts);\n\tif (match.kind !== \"exact\") {\n\t\treturn { execute: false, reason: match.kind === \"ambiguous\" ? \"ambiguous_match\" : \"no_match\" };\n\t}\n\treturn { execute: true, scriptName: match.script.name, reason: \"level0_direct_hit\" };\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { matchToolkitScript } from "../toolkit/script-registry.js";
2
+ /**
3
+ * Executor-lane classifier (G16): decides when a USER turn is a direct toolkit command that a
4
+ * small local executor model can own end-to-end ("restore db staging", "run the status report"),
5
+ * instead of spending the frontier model on a one-tool reflex.
6
+ *
7
+ * Deliberately conservative — ALL of:
8
+ * - the deterministic Level-0 matcher scores an EXACT/direct hit (the same margin rule that
9
+ * gates tool-side matching; ambiguity never routes to the executor, it stays with the big
10
+ * model + reflex brain), and
11
+ * - the prompt LOOKS like a command: single line, short, no code fences/paths of substance.
12
+ * Everything else falls through to normal routing. The executor model itself is gated by the
13
+ * caller (configured + resolved + tool-call fitness), and failures escalate via the existing
14
+ * cheap-tier escalation path.
15
+ */
16
+ const EXECUTOR_MAX_PROMPT_CHARS = 120;
17
+ export function classifyExecutorTurn(prompt, scripts) {
18
+ const trimmed = prompt.trim();
19
+ if (scripts.length === 0)
20
+ return { execute: false, reason: "no_toolkit_scripts" };
21
+ if (trimmed.length === 0 || trimmed.length > EXECUTOR_MAX_PROMPT_CHARS) {
22
+ return { execute: false, reason: "not_command_shaped" };
23
+ }
24
+ if (trimmed.includes("\n") || trimmed.includes("```")) {
25
+ return { execute: false, reason: "not_command_shaped" };
26
+ }
27
+ const match = matchToolkitScript(trimmed, scripts);
28
+ if (match.kind !== "exact") {
29
+ return { execute: false, reason: match.kind === "ambiguous" ? "ambiguous_match" : "no_match" };
30
+ }
31
+ return { execute: true, scriptName: match.script.name, reason: "level0_direct_hit" };
32
+ }
33
+ //# sourceMappingURL=executor-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor-route.js","sourceRoot":"","sources":["../../../src/core/model-router/executor-route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAsB,MAAM,+BAA+B,CAAC;AAEvF;;;;;;;;;;;;;GAaG;AAEH,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAQtC,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,OAAiC,EAAwB;IAC7G,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAClF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,yBAAyB,EAAE,CAAC;QACxE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IACzD,CAAC;IACD,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACvD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;IACzD,CAAC;IACD,MAAM,KAAK,GAAG,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC5B,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC;IAChG,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;AAAA,CACrF","sourcesContent":["import { matchToolkitScript, type ToolkitScript } from \"../toolkit/script-registry.ts\";\n\n/**\n * Executor-lane classifier (G16): decides when a USER turn is a direct toolkit command that a\n * small local executor model can own end-to-end (\"restore db staging\", \"run the status report\"),\n * instead of spending the frontier model on a one-tool reflex.\n *\n * Deliberately conservative — ALL of:\n * - the deterministic Level-0 matcher scores an EXACT/direct hit (the same margin rule that\n * gates tool-side matching; ambiguity never routes to the executor, it stays with the big\n * model + reflex brain), and\n * - the prompt LOOKS like a command: single line, short, no code fences/paths of substance.\n * Everything else falls through to normal routing. The executor model itself is gated by the\n * caller (configured + resolved + tool-call fitness), and failures escalate via the existing\n * cheap-tier escalation path.\n */\n\nconst EXECUTOR_MAX_PROMPT_CHARS = 120;\n\nexport interface ExecutorRouteVerdict {\n\texecute: boolean;\n\tscriptName?: string;\n\treason: string;\n}\n\nexport function classifyExecutorTurn(prompt: string, scripts: readonly ToolkitScript[]): ExecutorRouteVerdict {\n\tconst trimmed = prompt.trim();\n\tif (scripts.length === 0) return { execute: false, reason: \"no_toolkit_scripts\" };\n\tif (trimmed.length === 0 || trimmed.length > EXECUTOR_MAX_PROMPT_CHARS) {\n\t\treturn { execute: false, reason: \"not_command_shaped\" };\n\t}\n\tif (trimmed.includes(\"\\n\") || trimmed.includes(\"```\")) {\n\t\treturn { execute: false, reason: \"not_command_shaped\" };\n\t}\n\tconst match = matchToolkitScript(trimmed, scripts);\n\tif (match.kind !== \"exact\") {\n\t\treturn { execute: false, reason: match.kind === \"ambiguous\" ? \"ambiguous_match\" : \"no_match\" };\n\t}\n\treturn { execute: true, scriptName: match.script.name, reason: \"level0_direct_hit\" };\n}\n"]}
@@ -3,5 +3,7 @@ export declare function shouldEscalateModelRouterTool(options: {
3
3
  tier: ModelTier;
4
4
  toolName: string;
5
5
  args?: unknown;
6
+ /** The route's reasonCode; executor-lane turns carry "executor_direct". */
7
+ reasonCode?: string;
6
8
  }): boolean;
7
9
  //# sourceMappingURL=tool-escalation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tool-escalation.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/tool-escalation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AA2F1D,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAUrH","sourcesContent":["import type { ModelTier } from \"../autonomy/contracts.ts\";\n\nconst READ_ONLY_TOOL_NAMES = new Set([\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"list\",\n\t\"search\",\n\t\"glob\",\n\t\"view_file\",\n\t\"list_dir\",\n\t\"grep_search\",\n\t\"search_web\",\n\t\"read_url_content\",\n\t\"read_browser_page\",\n]);\n\nconst SHELL_TOOL_NAMES = new Set([\"bash\", \"exec\", \"execute\", \"run\", \"run_command\", \"shell\"]);\n\nconst READ_ONLY_COMMANDS = new Set([\n\t\"awk\",\n\t\"cat\",\n\t\"date\",\n\t\"df\",\n\t\"du\",\n\t\"env\",\n\t\"git\",\n\t\"grep\",\n\t\"head\",\n\t\"jq\",\n\t\"ls\",\n\t\"node\",\n\t\"npm\",\n\t\"pnpm\",\n\t\"pwd\",\n\t\"rg\",\n\t\"sed\",\n\t\"tail\",\n\t\"test\",\n\t\"tsc\",\n\t\"wc\",\n\t\"which\",\n\t\"yarn\",\n]);\n\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\"branch\", \"diff\", \"log\", \"rev-parse\", \"show\", \"status\", \"tag\"]);\nconst READ_ONLY_NPM_SUBCOMMANDS = new Set([\"info\", \"list\", \"ls\", \"outdated\", \"view\", \"whoami\"]);\nconst MUTATING_SHELL_TOKEN_RE =\n\t/(^|\\s)(>|>>|2>|&>|tee\\b|rm\\b|mv\\b|cp\\b|mkdir\\b|touch\\b|chmod\\b|chown\\b|install\\b|commit\\b|push\\b|publish\\b|deploy\\b|apply\\b|add\\b|checkout\\b|switch\\b|reset\\b|clean\\b|stash\\b|merge\\b|rebase\\b|npm\\s+(?:i|install|ci|update|publish|run)\\b|pnpm\\s+(?:i|install|update|publish|run)\\b|yarn\\s+(?:add|install|upgrade|publish|run)\\b)/i;\nconst MUTATING_TOOL_NAME_RE =\n\t/(bash|exec|execute|run|shell|write|edit|patch|replace|delete|remove|move|rename|create|mkdir|touch|install|commit|push|publish|deploy|apply)/i;\n\nfunction getShellCommand(args: unknown): string | undefined {\n\tif (!args || typeof args !== \"object\") return undefined;\n\tconst record = args as Record<string, unknown>;\n\tconst command = record.command ?? record.cmd ?? record.shellCommand;\n\treturn typeof command === \"string\" ? command.trim() : undefined;\n}\n\nfunction commandName(segment: string): string | undefined {\n\tconst first = segment.trim().match(/^[A-Za-z0-9_./-]+/)?.[0];\n\tif (!first) return undefined;\n\tconst parts = first.split(\"/\");\n\treturn parts[parts.length - 1]?.toLowerCase();\n}\n\nfunction commandArg(segment: string, index: number): string | undefined {\n\treturn segment.trim().split(/\\s+/)[index]?.toLowerCase();\n}\n\nfunction isReadOnlyShellSegment(segment: string): boolean {\n\tconst name = commandName(segment);\n\tif (!name || !READ_ONLY_COMMANDS.has(name)) return false;\n\tif (name === \"git\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_GIT_SUBCOMMANDS.has(subcommand));\n\t}\n\tif (name === \"npm\" || name === \"pnpm\" || name === \"yarn\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_NPM_SUBCOMMANDS.has(subcommand));\n\t}\n\treturn true;\n}\n\nfunction isReadOnlyShellCommand(command: string): boolean {\n\tif (!command || MUTATING_SHELL_TOKEN_RE.test(command)) return false;\n\tconst segments = command.split(/\\s*&&\\s*/).map((segment) => segment.trim());\n\treturn segments.length > 0 && segments.every(isReadOnlyShellSegment);\n}\n\nexport function shouldEscalateModelRouterTool(options: { tier: ModelTier; toolName: string; args?: unknown }): boolean {\n\tif (options.tier !== \"cheap\") return false;\n\tconst toolName = options.toolName.trim().toLowerCase();\n\tif (!toolName) return true;\n\tif (READ_ONLY_TOOL_NAMES.has(toolName)) return false;\n\tif (SHELL_TOOL_NAMES.has(toolName)) {\n\t\tconst command = getShellCommand(options.args);\n\t\treturn command ? !isReadOnlyShellCommand(command) : true;\n\t}\n\treturn MUTATING_TOOL_NAME_RE.test(toolName) || !toolName.startsWith(\"read_\");\n}\n"]}
1
+ {"version":3,"file":"tool-escalation.d.ts","sourceRoot":"","sources":["../../../src/core/model-router/tool-escalation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AA2F1D,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACtD,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAeV","sourcesContent":["import type { ModelTier } from \"../autonomy/contracts.ts\";\n\nconst READ_ONLY_TOOL_NAMES = new Set([\n\t\"read\",\n\t\"grep\",\n\t\"find\",\n\t\"ls\",\n\t\"list\",\n\t\"search\",\n\t\"glob\",\n\t\"view_file\",\n\t\"list_dir\",\n\t\"grep_search\",\n\t\"search_web\",\n\t\"read_url_content\",\n\t\"read_browser_page\",\n]);\n\nconst SHELL_TOOL_NAMES = new Set([\"bash\", \"exec\", \"execute\", \"run\", \"run_command\", \"shell\"]);\n\nconst READ_ONLY_COMMANDS = new Set([\n\t\"awk\",\n\t\"cat\",\n\t\"date\",\n\t\"df\",\n\t\"du\",\n\t\"env\",\n\t\"git\",\n\t\"grep\",\n\t\"head\",\n\t\"jq\",\n\t\"ls\",\n\t\"node\",\n\t\"npm\",\n\t\"pnpm\",\n\t\"pwd\",\n\t\"rg\",\n\t\"sed\",\n\t\"tail\",\n\t\"test\",\n\t\"tsc\",\n\t\"wc\",\n\t\"which\",\n\t\"yarn\",\n]);\n\nconst READ_ONLY_GIT_SUBCOMMANDS = new Set([\"branch\", \"diff\", \"log\", \"rev-parse\", \"show\", \"status\", \"tag\"]);\nconst READ_ONLY_NPM_SUBCOMMANDS = new Set([\"info\", \"list\", \"ls\", \"outdated\", \"view\", \"whoami\"]);\nconst MUTATING_SHELL_TOKEN_RE =\n\t/(^|\\s)(>|>>|2>|&>|tee\\b|rm\\b|mv\\b|cp\\b|mkdir\\b|touch\\b|chmod\\b|chown\\b|install\\b|commit\\b|push\\b|publish\\b|deploy\\b|apply\\b|add\\b|checkout\\b|switch\\b|reset\\b|clean\\b|stash\\b|merge\\b|rebase\\b|npm\\s+(?:i|install|ci|update|publish|run)\\b|pnpm\\s+(?:i|install|update|publish|run)\\b|yarn\\s+(?:add|install|upgrade|publish|run)\\b)/i;\nconst MUTATING_TOOL_NAME_RE =\n\t/(bash|exec|execute|run|shell|write|edit|patch|replace|delete|remove|move|rename|create|mkdir|touch|install|commit|push|publish|deploy|apply)/i;\n\nfunction getShellCommand(args: unknown): string | undefined {\n\tif (!args || typeof args !== \"object\") return undefined;\n\tconst record = args as Record<string, unknown>;\n\tconst command = record.command ?? record.cmd ?? record.shellCommand;\n\treturn typeof command === \"string\" ? command.trim() : undefined;\n}\n\nfunction commandName(segment: string): string | undefined {\n\tconst first = segment.trim().match(/^[A-Za-z0-9_./-]+/)?.[0];\n\tif (!first) return undefined;\n\tconst parts = first.split(\"/\");\n\treturn parts[parts.length - 1]?.toLowerCase();\n}\n\nfunction commandArg(segment: string, index: number): string | undefined {\n\treturn segment.trim().split(/\\s+/)[index]?.toLowerCase();\n}\n\nfunction isReadOnlyShellSegment(segment: string): boolean {\n\tconst name = commandName(segment);\n\tif (!name || !READ_ONLY_COMMANDS.has(name)) return false;\n\tif (name === \"git\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_GIT_SUBCOMMANDS.has(subcommand));\n\t}\n\tif (name === \"npm\" || name === \"pnpm\" || name === \"yarn\") {\n\t\tconst subcommand = commandArg(segment, 1);\n\t\treturn Boolean(subcommand && READ_ONLY_NPM_SUBCOMMANDS.has(subcommand));\n\t}\n\treturn true;\n}\n\nfunction isReadOnlyShellCommand(command: string): boolean {\n\tif (!command || MUTATING_SHELL_TOKEN_RE.test(command)) return false;\n\tconst segments = command.split(/\\s*&&\\s*/).map((segment) => segment.trim());\n\treturn segments.length > 0 && segments.every(isReadOnlyShellSegment);\n}\n\nexport function shouldEscalateModelRouterTool(options: {\n\ttier: ModelTier;\n\ttoolName: string;\n\targs?: unknown;\n\t/** The route's reasonCode; executor-lane turns carry \"executor_direct\". */\n\treasonCode?: string;\n}): boolean {\n\tif (options.tier !== \"cheap\") return false;\n\tconst toolName = options.toolName.trim().toLowerCase();\n\t// Executor-lane turns (G16) exist to run exactly one tool: run_toolkit_script, which enforces\n\t// its own safety (danger confirmation, structural exit-code contract). Escalating on it would\n\t// abort every executor turn at the moment it does its job. Any OTHER mutating tool still\n\t// escalates to the expensive model as usual.\n\tif (options.reasonCode === \"executor_direct\" && toolName === \"run_toolkit_script\") return false;\n\tif (!toolName) return true;\n\tif (READ_ONLY_TOOL_NAMES.has(toolName)) return false;\n\tif (SHELL_TOOL_NAMES.has(toolName)) {\n\t\tconst command = getShellCommand(options.args);\n\t\treturn command ? !isReadOnlyShellCommand(command) : true;\n\t}\n\treturn MUTATING_TOOL_NAME_RE.test(toolName) || !toolName.startsWith(\"read_\");\n}\n"]}
@@ -84,6 +84,12 @@ export function shouldEscalateModelRouterTool(options) {
84
84
  if (options.tier !== "cheap")
85
85
  return false;
86
86
  const toolName = options.toolName.trim().toLowerCase();
87
+ // Executor-lane turns (G16) exist to run exactly one tool: run_toolkit_script, which enforces
88
+ // its own safety (danger confirmation, structural exit-code contract). Escalating on it would
89
+ // abort every executor turn at the moment it does its job. Any OTHER mutating tool still
90
+ // escalates to the expensive model as usual.
91
+ if (options.reasonCode === "executor_direct" && toolName === "run_toolkit_script")
92
+ return false;
87
93
  if (!toolName)
88
94
  return true;
89
95
  if (READ_ONLY_TOOL_NAMES.has(toolName))