pi-harness-runtime 1.1.50 → 1.1.77

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 (202) hide show
  1. package/CHANGELOG.md +30 -9
  2. package/README.md +119 -0
  3. package/harness/langchain/agents.d.ts +143 -0
  4. package/harness/langchain/agents.test.d.ts +2 -0
  5. package/harness/langchain/agents.ts +196 -13
  6. package/harness/langchain/checkpointer.d.ts +48 -0
  7. package/harness/langchain/checkpointer.test.d.ts +8 -0
  8. package/harness/langchain/cron-cli.d.ts +12 -0
  9. package/harness/langchain/cron-cli.ts +169 -0
  10. package/harness/langchain/daemon.d.ts +161 -0
  11. package/harness/langchain/daemon.test.d.ts +11 -0
  12. package/harness/langchain/daemon.test.ts +3 -1
  13. package/harness/langchain/daemon.ts +230 -12
  14. package/harness/langchain/file-tools.d.ts +97 -0
  15. package/harness/langchain/file-tools.ts +14 -6
  16. package/harness/langchain/graph.d.ts +706 -0
  17. package/harness/langchain/graph.ts +158 -25
  18. package/harness/langchain/model-router.d.ts +63 -0
  19. package/harness/langchain/model-router.test.d.ts +2 -0
  20. package/harness/langchain/model-router.test.ts +191 -0
  21. package/harness/langchain/model-router.ts +188 -0
  22. package/harness/langchain/models.d.ts +72 -0
  23. package/harness/langchain/models.ts +111 -49
  24. package/harness/langchain/ping-pong-middleware.d.ts +28 -0
  25. package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
  26. package/harness/langchain/ping-pong-middleware.test.ts +69 -0
  27. package/harness/langchain/ping-pong-middleware.ts +204 -0
  28. package/harness/langchain/run.d.ts +15 -0
  29. package/harness/langchain/run.ts +15 -3
  30. package/harness/langchain/scoreboard.test.d.ts +13 -0
  31. package/harness/langchain/scoreboard.test.ts +23 -1
  32. package/harness/langchain/status-line.d.ts +91 -0
  33. package/harness/langchain/status-line.test.d.ts +8 -0
  34. package/harness/langchain/surge.d.ts +103 -0
  35. package/harness/langchain/surge.test.d.ts +10 -0
  36. package/harness/langchain/surge.test.ts +179 -0
  37. package/harness/langchain/surge.ts +188 -19
  38. package/harness/langchain/widget.d.ts +105 -0
  39. package/harness/langchain/widget.test.d.ts +8 -0
  40. package/harness/ssh-detach-interceptor.test.ts +263 -0
  41. package/harness/ssh-detach-interceptor.ts +438 -0
  42. package/index.ts +212 -47
  43. package/package.json +4 -2
  44. package/packages/a2a-adapter/package.json +1 -1
  45. package/packages/architecture-generator/package.json +1 -1
  46. package/packages/auth/package.json +1 -1
  47. package/packages/autonomous-refactor/package.json +1 -1
  48. package/packages/autonomous-runtime/package.json +1 -1
  49. package/packages/cache-strategy/package.json +1 -1
  50. package/packages/capability-registry/package.json +1 -1
  51. package/packages/checkpoint/package.json +1 -1
  52. package/packages/cli-plugin-sdk/package.json +1 -1
  53. package/packages/clipboard/package.json +1 -1
  54. package/packages/clipboard-plugin/package.json +1 -1
  55. package/packages/code-generation/package.json +1 -1
  56. package/packages/code-review/package.json +1 -1
  57. package/packages/codex-adapter/package.json +1 -1
  58. package/packages/config-capture/package.json +1 -1
  59. package/packages/context-compiler/package.json +1 -1
  60. package/packages/context-discovery/package.json +1 -1
  61. package/packages/context-manager/package.json +1 -1
  62. package/packages/cookie-sanitizer/package.json +1 -1
  63. package/packages/cost-optimizer/package.json +1 -1
  64. package/packages/dependency-analyzer/package.json +1 -1
  65. package/packages/django-plugin/package.json +1 -1
  66. package/packages/doc-generator/package.json +1 -1
  67. package/packages/evaluation-engine/package.json +1 -1
  68. package/packages/evaluation-runner/package.json +1 -1
  69. package/packages/event-bus/package.json +1 -1
  70. package/packages/event-store/package.json +1 -1
  71. package/packages/experience-replay/package.json +1 -1
  72. package/packages/feedback-collector/package.json +1 -1
  73. package/packages/file-copy-helper/package.json +1 -1
  74. package/packages/framework-detector/package.json +1 -1
  75. package/packages/framework-plugin-sdk/package.json +1 -1
  76. package/packages/frappe-plugin/package.json +1 -1
  77. package/packages/generic-web-plugin/package.json +1 -1
  78. package/packages/health-monitor/package.json +1 -1
  79. package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
  80. package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
  81. package/packages/intent-analyzer/dist/src/index.js +1 -0
  82. package/packages/intent-analyzer/dist/src/index.js.map +1 -1
  83. package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
  84. package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
  85. package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
  86. package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
  87. package/packages/intent-analyzer/package.json +1 -1
  88. package/packages/intent-analyzer/src/index.ts +13 -0
  89. package/packages/intent-analyzer/src/ping-pong.ts +387 -0
  90. package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
  91. package/packages/jev-judge/README.md +234 -0
  92. package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
  93. package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
  94. package/packages/jev-judge/dist/decision-engine.js +151 -0
  95. package/packages/jev-judge/dist/decision-engine.js.map +1 -0
  96. package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
  97. package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
  98. package/packages/jev-judge/dist/e2e-judge.js +268 -0
  99. package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
  100. package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
  101. package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
  102. package/packages/jev-judge/dist/flaky-detector.js +154 -0
  103. package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
  104. package/packages/jev-judge/dist/index.d.ts +77 -0
  105. package/packages/jev-judge/dist/index.d.ts.map +1 -0
  106. package/packages/jev-judge/dist/index.js +271 -0
  107. package/packages/jev-judge/dist/index.js.map +1 -0
  108. package/packages/jev-judge/dist/types.d.ts +97 -0
  109. package/packages/jev-judge/dist/types.d.ts.map +1 -0
  110. package/packages/jev-judge/dist/types.js +8 -0
  111. package/packages/jev-judge/dist/types.js.map +1 -0
  112. package/packages/jev-judge/package.json +46 -0
  113. package/packages/jev-judge/src/decision-engine.ts +231 -0
  114. package/packages/jev-judge/src/e2e-judge.ts +364 -0
  115. package/packages/jev-judge/src/flaky-detector.ts +190 -0
  116. package/packages/jev-judge/src/index.ts +345 -0
  117. package/packages/jev-judge/src/types.ts +131 -0
  118. package/packages/knowledge-graph/package.json +1 -1
  119. package/packages/knowledge-retrieval/package.json +1 -1
  120. package/packages/laravel-plugin/package.json +1 -1
  121. package/packages/learning-engine/package.json +1 -1
  122. package/packages/mcp-adapter/package.json +1 -1
  123. package/packages/memory-engine/package.json +1 -1
  124. package/packages/milestone-manager/package.json +1 -1
  125. package/packages/model-registry/package.json +1 -1
  126. package/packages/nextjs-plugin/package.json +1 -1
  127. package/packages/notification/package.json +1 -1
  128. package/packages/observability/package.json +1 -1
  129. package/packages/okf-indexer/package.json +1 -1
  130. package/packages/performance-optimizer/package.json +1 -1
  131. package/packages/privilege-broker/package.json +1 -1
  132. package/packages/project-analyzer/package.json +1 -1
  133. package/packages/project-bootstrap/package.json +1 -1
  134. package/packages/projection-engine/package.json +1 -1
  135. package/packages/prompt-compiler/package.json +1 -1
  136. package/packages/prompt-versioning/package.json +1 -1
  137. package/packages/provider-adapter-sdk/package.json +1 -1
  138. package/packages/provider-router/package.json +1 -1
  139. package/packages/provider-selector/package.json +1 -1
  140. package/packages/providers/package.json +1 -1
  141. package/packages/quota-manager/package.json +1 -1
  142. package/packages/rate-limiter/package.json +1 -1
  143. package/packages/react-vite-plugin/package.json +1 -1
  144. package/packages/release-manager/package.json +1 -1
  145. package/packages/requirement-compiler/package.json +1 -1
  146. package/packages/runtime/package.json +1 -1
  147. package/packages/scheduler/package.json +1 -1
  148. package/packages/scheduler-adapter/package.json +1 -1
  149. package/packages/session/package.json +1 -1
  150. package/packages/session-api/package.json +1 -1
  151. package/packages/session-export/package.json +1 -1
  152. package/packages/shared-context/package.json +1 -1
  153. package/packages/skill-mcp-client/package.json +1 -1
  154. package/packages/skill-registry/package.json +1 -1
  155. package/packages/sprint-planner/package.json +1 -1
  156. package/packages/subscription-engine/package.json +1 -1
  157. package/packages/task-compiler/package.json +1 -1
  158. package/packages/tencentdb-memory/package.json +1 -1
  159. package/packages/tencentdb-sync/package.json +1 -1
  160. package/packages/test-data-generator/package.json +1 -1
  161. package/packages/test-generator/package.json +1 -1
  162. package/packages/todo-bd-sync/README.md +71 -0
  163. package/packages/todo-bd-sync/package.json +1 -1
  164. package/packages/todo-bd-sync/src/extension.ts +16 -1
  165. package/packages/token-estimation/package.json +1 -1
  166. package/packages/token-optimizer/package.json +1 -1
  167. package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
  168. package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
  169. package/packages/trajectory/dist/src/approved-patterns.js +134 -0
  170. package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
  171. package/packages/trajectory/dist/src/index.d.ts +13 -0
  172. package/packages/trajectory/dist/src/index.d.ts.map +1 -0
  173. package/packages/trajectory/dist/src/index.js +12 -0
  174. package/packages/trajectory/dist/src/index.js.map +1 -0
  175. package/packages/trajectory/dist/src/store.d.ts +69 -0
  176. package/packages/trajectory/dist/src/store.d.ts.map +1 -0
  177. package/packages/trajectory/dist/src/store.js +253 -0
  178. package/packages/trajectory/dist/src/store.js.map +1 -0
  179. package/packages/trajectory/dist/src/types.d.ts +73 -0
  180. package/packages/trajectory/dist/src/types.d.ts.map +1 -0
  181. package/packages/trajectory/dist/src/types.js +9 -0
  182. package/packages/trajectory/dist/src/types.js.map +1 -0
  183. package/packages/trajectory/dist/test/store.test.d.ts +2 -0
  184. package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
  185. package/packages/trajectory/dist/test/store.test.js +228 -0
  186. package/packages/trajectory/dist/test/store.test.js.map +1 -0
  187. package/packages/trajectory/package.json +29 -0
  188. package/packages/trajectory/src/approved-patterns.ts +180 -0
  189. package/packages/trajectory/src/index.ts +26 -0
  190. package/packages/trajectory/src/store.ts +285 -0
  191. package/packages/trajectory/src/types.ts +84 -0
  192. package/packages/trajectory/test/store.test.d.ts +2 -0
  193. package/packages/trajectory/test/store.test.ts +250 -0
  194. package/packages/tui/package.json +1 -1
  195. package/packages/types/package.json +1 -1
  196. package/packages/workflow-events/package.json +1 -1
  197. package/packages/workspace-scanner/package.json +1 -1
  198. package/packages/worktree/package.json +1 -1
  199. package/packages/write-review/package.json +1 -1
  200. package/packages/write-review/src/blackboard.ts +24 -1
  201. package/packages/write-review/src/types.ts +13 -0
  202. package/skills/harness-runtime/SKILL.md +67 -4
@@ -20,11 +20,15 @@ import {
20
20
  StateGraph,
21
21
  } from "@langchain/langgraph";
22
22
  import {
23
+ isAutonomyRequest,
24
+ autonomyDirective,
23
25
  lastMessage,
24
26
  type ReviewVerdict,
25
27
  ReviewVerdictSchema,
26
28
  } from "./agents.js";
29
+ import { readFileSync } from "node:fs";
27
30
  import { WriteReviewBlackboard } from "../../packages/write-review/src/blackboard.js";
31
+ import { getApprovedPatternStore } from "../../packages/trajectory/src/index.js";
28
32
  import type { Verdict } from "../../packages/write-review/src/types.js";
29
33
  import type { LoopWidget } from "./widget.js";
30
34
 
@@ -49,11 +53,17 @@ const LoopState = Annotation.Root({
49
53
  reducer: (prev, next) => [...(prev ?? []), ...(next ?? [])],
50
54
  default: () => [],
51
55
  }),
52
- /** File that received comments in the previous review. Used for smart-stop. */
53
- lastCommentedFile: Annotation<string | undefined>({
56
+ /** Code from the previous iteration. Used for stuck detection (P0-2). */
57
+ prevCode: Annotation<string | undefined>({
54
58
  reducer: (_prev, next) => next,
55
59
  default: () => undefined,
56
60
  }),
61
+ /** Actual file contents written in the most recent write step.
62
+ * P1-1: used to pass real file contents to the reviewer instead of a summary. */
63
+ writtenFiles: Annotation<Record<string, string>>({
64
+ reducer: (_prev, next) => next,
65
+ default: () => ({}),
66
+ }),
57
67
  });
58
68
 
59
69
  export type LoopState = typeof LoopState.State;
@@ -63,7 +73,22 @@ export type LoopState = typeof LoopState.State;
63
73
  export interface LoopDeps {
64
74
  plan: (request: string) => Promise<string>;
65
75
  write: (plan: string, review: ReviewVerdict | null) => Promise<string>;
66
- review: (plan: string, code: string) => Promise<ReviewVerdict>;
76
+ review: (
77
+ plan: string,
78
+ code: string,
79
+ writtenFiles?: Record<string, string>,
80
+ ) => Promise<ReviewVerdict>;
81
+ /**
82
+ * ADK ParallelAgent equivalent: run 3 specialist reviews in parallel.
83
+ * If provided, reviewNode uses this instead of the monolithic review().
84
+ * Specialists: security auditor + style auditor + performance analyst.
85
+ * Takes ~1/3 the time of sequential reviews.
86
+ */
87
+ parallelReview?: (
88
+ plan: string,
89
+ code: string,
90
+ writtenFiles?: Record<string, string>,
91
+ ) => Promise<ReviewVerdict>;
67
92
  maxIterations: number;
68
93
  onStep?: (step: string, state: LoopState) => void;
69
94
  /** Optional widget for TUI / status-line display (mirrors pi-lens footer style). */
@@ -84,18 +109,60 @@ function planNode(deps: LoopDeps) {
84
109
  };
85
110
  }
86
111
 
112
+ /** Extract file paths from a code string (mirrors blackboard.extractFilePaths). */
113
+ function extractFilePaths(code: string): string[] {
114
+ const files = new Set<string>();
115
+ const lines = code.split("\n");
116
+ for (const line of lines) {
117
+ const match = line.match(
118
+ /^```(?:typescript|ts|javascript|js|tsx|jsx|text)?\s*\/([^\s`]+)\s*$/,
119
+ );
120
+ if (match) {
121
+ const path = match[1].trim();
122
+ if (path && !path.includes(" ")) files.add(path);
123
+ }
124
+ const mdMatch = line.match(/^\*\*([^:*]+):\*\*/);
125
+ if (mdMatch) {
126
+ const path = mdMatch[1].trim();
127
+ if (
128
+ path &&
129
+ !path.includes(" ") &&
130
+ (path.includes("/") || path.endsWith(".ts") || path.endsWith(".js"))
131
+ ) {
132
+ files.add(path);
133
+ }
134
+ }
135
+ }
136
+ return [...files];
137
+ }
138
+
87
139
  function writeNode(deps: LoopDeps) {
88
140
  return async (state: LoopState): Promise<Partial<LoopState>> => {
89
141
  const code = await deps.write(state.plan, state.review ?? null);
90
142
  const iter = state.iteration + 1;
91
143
  deps.onStep?.("write", { ...state, iteration: iter });
144
+ // P1-1: read actual file contents so the reviewer sees real code, not a summary
145
+ const paths = extractFilePaths(code);
146
+ const writtenFiles: Record<string, string> = {};
147
+ for (const p of paths) {
148
+ try {
149
+ writtenFiles[p] = readFileSync(p, "utf8");
150
+ } catch {
151
+ // File may not exist yet (mkdir was pending); skip
152
+ }
153
+ }
92
154
  const reviewNote = state.review
93
155
  ? ` (addressing ${state.review.comments.length} review comment(s))`
94
156
  : "";
95
- // Note: previous `review` stays in state on purpose — the coder reads its comments.
96
157
  return {
97
158
  code,
159
+ writtenFiles,
98
160
  iteration: iter,
161
+ // Snapshot the code from the previous iteration (before it was
162
+ // overwritten). routeAfterReview uses this to detect stuck: if the
163
+ // coder outputs identical content in two consecutive iterations, the
164
+ // loop terminates early instead of wasting another review cycle.
165
+ prevCode: state.code,
99
166
  log: [`[write:${iter}] MiniMax wrote code${reviewNote}`],
100
167
  };
101
168
  };
@@ -103,19 +170,23 @@ function writeNode(deps: LoopDeps) {
103
170
 
104
171
  function reviewNode(deps: LoopDeps) {
105
172
  return async (state: LoopState): Promise<Partial<LoopState>> => {
106
- const review = await deps.review(state.plan, state.code);
173
+ // ADK ParallelAgent: use parallel review if available (3x faster)
174
+ // Falls back to sequential review for dry-run or when parallelReview is not wired.
175
+ const doReview = deps.parallelReview ?? deps.review;
176
+ const review = await doReview(
177
+ state.plan,
178
+ state.code,
179
+ state.writtenFiles ?? {},
180
+ );
107
181
  deps.onStep?.("review", state);
108
- const commentedFile =
109
- review.comments.length > 0 ? (review.comments[0].file ?? "") : undefined;
110
182
  return {
111
183
  review,
112
- lastCommentedFile: commentedFile,
113
184
  log: [`[review:${state.iteration}] GPT verdict: ${review.verdict}`],
114
185
  };
115
186
  };
116
187
  }
117
188
 
118
- function finishNode() {
189
+ function finishNode(maxIterations: number) {
119
190
  return async (state: LoopState): Promise<Partial<LoopState>> => {
120
191
  const verdict = state.review?.verdict ?? "blocked";
121
192
  const comments = state.review?.comments ?? [];
@@ -124,7 +195,7 @@ function finishNode() {
124
195
  reason = "reviewer approved";
125
196
  } else if (verdict === "blocked") {
126
197
  reason = "reviewer blocked the task";
127
- } else if (state.iteration >= 3) {
198
+ } else if (state.iteration >= maxIterations) {
128
199
  reason = `max iterations (${state.iteration}) reached with changes still requested`;
129
200
  } else if (
130
201
  comments.length > 0 &&
@@ -132,8 +203,7 @@ function finishNode() {
132
203
  ) {
133
204
  reason = `converged: only minor comments (${state.iteration})`;
134
205
  } else {
135
- const file = state.lastCommentedFile ?? "unknown";
136
- reason = `stuck: same file (${file}) flagged for ${comments.length} comment(s) across iterations`;
206
+ reason = `stuck: changes still requested after ${state.iteration} iteration(s)`;
137
207
  }
138
208
  return {
139
209
  log: [`[finish] ${state.iteration} iteration(s) — ${reason}`],
@@ -159,6 +229,12 @@ function routeAfterReview(
159
229
  (c) => c.severity === "minor" || !c.severity,
160
230
  );
161
231
  if (hasOnlyMinor && comments.length <= 2) return "finish";
232
+ // 4. P0-2 STUCK DETECTION: if the coder produced identical output in consecutive
233
+ // iterations, no progress is being made — terminate early instead of wasting
234
+ // another review cycle
235
+ if (state.prevCode !== undefined && state.code === state.prevCode) {
236
+ return "finish";
237
+ }
162
238
  return "write";
163
239
  }
164
240
 
@@ -174,7 +250,7 @@ export function buildWriteReviewLoop(
174
250
  .addNode("planStep", planNode(deps))
175
251
  .addNode("writeStep", writeNode(deps))
176
252
  .addNode("reviewStep", reviewNode(deps))
177
- .addNode("finishStep", finishNode())
253
+ .addNode("finishStep", finishNode(deps.maxIterations))
178
254
  .addEdge(START, "planStep")
179
255
  .addEdge("planStep", "writeStep")
180
256
  .addEdge("writeStep", "reviewStep")
@@ -208,6 +284,15 @@ export interface RealLoopOptions {
208
284
  onStep?: (step: string, state: LoopState) => void;
209
285
  /** Directory for .write-review/blackboard. Defaults to process.cwd(). */
210
286
  blackboardDir?: string;
287
+ /**
288
+ * The request string. Used by the smart model router to determine which
289
+ * models to use for planner / reviewer / coder roles. Supports per-request
290
+ * directives in the prompt:
291
+ * [planner: gpt] [reviewer: GLM] [coder: MiniMax]
292
+ * "Use GPT as master reviewer" is also recognised.
293
+ * If omitted, defaults to env vars (PLANNER_*, GLM_*, MINIMAX_*).
294
+ */
295
+ request?: string;
211
296
  importAgents?: () => Promise<{
212
297
  createPlannerAgent: () => {
213
298
  invoke: (input: { messages: unknown[] }) => Promise<{
@@ -240,9 +325,27 @@ export async function buildRealLoopDeps(
240
325
  options: RealLoopOptions = {},
241
326
  ): Promise<LoopDeps> {
242
327
  const mod = await import("./agents.js");
243
- const planner = mod.createPlannerAgent();
244
- const coder = mod.createCoderAgent();
245
- const reviewer = mod.createReviewerAgent();
328
+
329
+ // Smart model routing — parses [role: family] directives in the request.
330
+ // Falls back to env defaults if no directives present.
331
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
332
+ const { routeRequestSmart } = require("./model-router.js");
333
+ const route = options.request ? routeRequestSmart(options.request) : undefined;
334
+
335
+ const plannerModelOpts = route?.planner;
336
+ const reviewerModelOpts = route?.reviewer;
337
+ const coderModelOpts = route?.coder;
338
+
339
+ const planner = mod.createPlannerAgent(plannerModelOpts);
340
+ const coder = mod.createCoderAgent(coderModelOpts);
341
+ const reviewer = mod.createReviewerAgent(reviewerModelOpts);
342
+
343
+ // ADK ParallelAgent: create 3 specialist reviewers for parallel execution
344
+ const specialists = {
345
+ security: mod.createSpecialistReviewer("security", reviewerModelOpts),
346
+ style: mod.createSpecialistReviewer("style", reviewerModelOpts),
347
+ performance: mod.createSpecialistReviewer("performance", reviewerModelOpts),
348
+ };
246
349
 
247
350
  // Create and init the shared blackboard
248
351
  const blackboard = new WriteReviewBlackboard(
@@ -254,8 +357,8 @@ export async function buildRealLoopDeps(
254
357
  blackboard.init();
255
358
  }
256
359
 
257
- // Autonomy mode: computed inside plan(), stored for write()/review()
258
- const _directive = "";
360
+ // Autonomy mode: set in plan() based on request content, reused by write()/review()
361
+ let _directive = "";
259
362
 
260
363
  /** Inject scoreboard markdown into a user message. */
261
364
  const withScoreboard = (msg: string) => {
@@ -263,6 +366,16 @@ export async function buildRealLoopDeps(
263
366
  return msg + "\n\n## Current Progress\n" + scoreboard;
264
367
  };
265
368
 
369
+ /** Inject scoreboard + approved patterns into a user message (for the reviewer). */
370
+ const withReviewContext = (msg: string) => {
371
+ const scoreboard = blackboard.toMarkdown();
372
+ const patterns = getApprovedPatternStore().toMarkdown();
373
+ const parts = [msg];
374
+ if (scoreboard) parts.push("\n\n## Current Progress\n" + scoreboard);
375
+ if (patterns) parts.push("\n\n" + patterns);
376
+ return parts.join("");
377
+ };
378
+
266
379
  // Wrap onStep to also update the blackboard
267
380
  const originalOnStep = options.onStep;
268
381
  const onStep = (step: string, state: LoopState) => {
@@ -305,14 +418,17 @@ export async function buildRealLoopDeps(
305
418
  return {
306
419
  maxIterations: options.maxIterations ?? 3,
307
420
  onStep,
308
- plan: async (request) =>
309
- lastMessage(
421
+ plan: async (request) => {
422
+ // P1-2: detect autonomy signal in the request and unlock full autonomous mode
423
+ _directive = isAutonomyRequest(request) ? autonomyDirective() : "";
424
+ return lastMessage(
310
425
  await planner.invoke({
311
426
  messages: [
312
427
  { role: "user", content: _directive + withScoreboard(request) },
313
428
  ],
314
429
  }),
315
- ),
430
+ );
431
+ },
316
432
  write: async (plan, review) => {
317
433
  const userMsg = review
318
434
  ? `## Plan\n${plan}\n\n## Review comments to address\n${review.comments
@@ -327,14 +443,20 @@ export async function buildRealLoopDeps(
327
443
  }),
328
444
  );
329
445
  },
330
- review: async (plan, code) => {
446
+ review: async (plan, _code, writtenFiles = {}) => {
447
+ // P1-1: build a code-to-review section from actual file contents
448
+ const codeSection =
449
+ Object.keys(writtenFiles).length > 0
450
+ ? Object.entries(writtenFiles)
451
+ .map(([path, content]) => `## ${path}\n\n\`\`\`\n${content}\n\`\`\``)
452
+ .join("\n\n")
453
+ : `## Code\n\n\`\`\`\n${_code}\n\`\`\``;
331
454
  const result = await reviewer.invoke({
332
455
  messages: [
333
456
  {
334
457
  role: "user",
335
458
  content:
336
- _directive +
337
- withScoreboard(`## Plan\n${plan}\n\n## Code to review\n${code}`),
459
+ _directive + withReviewContext(`## Plan\n${plan}\n\n${codeSection}`),
338
460
  },
339
461
  ],
340
462
  });
@@ -350,6 +472,9 @@ export async function buildRealLoopDeps(
350
472
  };
351
473
  }
352
474
  },
475
+ // ADK ParallelAgent: 3 specialists run in parallel via Promise.all
476
+ parallelReview: async (plan, _code, writtenFiles = {}) =>
477
+ mod.parallelReview(specialists, plan, _code, writtenFiles),
353
478
  };
354
479
  }
355
480
 
@@ -431,7 +556,7 @@ export function buildDryRunDeps(
431
556
  `\`\`\`ts\n// stub code, iteration ${writeCount}\nexport const plan = ${JSON.stringify(plan.slice(0, 60))};${fixNote}\n\`\`\``,
432
557
  );
433
558
  },
434
- review: async (_plan, _code) => {
559
+ review: async (_plan, _code, _writtenFiles = {}) => {
435
560
  if (writeCount < 2) {
436
561
  return {
437
562
  verdict: "changes_requested",
@@ -451,5 +576,13 @@ export function buildDryRunDeps(
451
576
  comments: [],
452
577
  };
453
578
  },
579
+ // Dry-run parallel review: same deterministic stub (no LLM calls)
580
+ parallelReview: async (_plan, _code, _writtenFiles = {}) => {
581
+ return buildDryRunDeps({ maxIterations: options.maxIterations }).review(
582
+ "",
583
+ "",
584
+ {},
585
+ );
586
+ },
454
587
  };
455
588
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Smart model router — reads the request prompt and decides which model to use
3
+ * for each agent role at runtime. No hardcoded model IDs; all choices are driven
4
+ * by env vars + optional per-request directives in the prompt.
5
+ *
6
+ * Per-request directives (optional — fallback to env defaults if absent):
7
+ *
8
+ * Planner directive:
9
+ * [planner: gpt-5.6] or [planner: GLM-5.2] or [planner: MiniMax-M2]
10
+ *
11
+ * Reviewer directive (master reviewer — GLM is the design default):
12
+ * [reviewer: GLM-5.2] or [reviewer: GPT-5.6]
13
+ *
14
+ * Coder directive:
15
+ * [coder: MiniMax-M2] or [coder: gpt-5.6]
16
+ *
17
+ * Examples in a prompt:
18
+ * "Implement X [reviewer: GPT-5.6] [planner: GLM-5.2]"
19
+ * "Use GLM as master reviewer for this task"
20
+ * "GPT as reviewer: please review this code"
21
+ *
22
+ * The parser is forgiving — these all work:
23
+ * [reviewer: GLM] → uses GLM_MODEL from env
24
+ * [reviewer: gpt] → uses PLANNER_MODEL from env (same family)
25
+ * [reviewer: MiniMax] → uses MINIMAX_MODEL from env
26
+ *
27
+ * Wiki: wiki/multi-agent-langchain.md
28
+ */
29
+ /** Model options derived from a resolved env prefix. */
30
+ export interface ResolvedModel {
31
+ model: string;
32
+ baseURL: string;
33
+ apiKey: string;
34
+ }
35
+ /** Result of routing a request to models. */
36
+ export interface RouteResult {
37
+ planner: ResolvedModel;
38
+ reviewer: ResolvedModel;
39
+ coder: ResolvedModel;
40
+ /** Text of the request with directive comments stripped (safe for agent prompts). */
41
+ cleanRequest: string;
42
+ }
43
+ /**
44
+ * Resolve a model family to actual env-backed model options.
45
+ * family: "GLM" | "PLANNER" | "MINIMAX" (case-insensitive input)
46
+ */
47
+ export declare function resolveModel(family: string): ResolvedModel;
48
+ /**
49
+ * Route a request prompt to the correct model configuration.
50
+ *
51
+ * Algorithm:
52
+ * 1. Extract [role: family] directives from the prompt
53
+ * 2. Resolve each family to actual env-backed model options
54
+ * 3. Apply defaults (planner→PLANNER, reviewer→GLM, coder→MINIMAX)
55
+ * 4. Strip directive comments from the request before passing to agents
56
+ */
57
+ export declare function routeRequest(request: string): RouteResult;
58
+ /**
59
+ * Route a request, also scanning natural-language hints.
60
+ * Directives ([role: family]) take precedence over natural hints.
61
+ */
62
+ export declare function routeRequestSmart(request: string): RouteResult;
63
+ //# sourceMappingURL=model-router.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model-router.test.d.ts.map
@@ -0,0 +1,191 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import {
4
+ routeRequest,
5
+ routeRequestSmart,
6
+ resolveModel,
7
+ } from "./model-router.js";
8
+
9
+ const TEST_ENV = {
10
+ PLANNER_API_KEY: "pk-test-planner",
11
+ PLANNER_MODEL: "gpt-5.6",
12
+ PLANNER_BASE_URL: "https://api.openai.com/v1",
13
+ GLM_API_KEY: "pk-test-glm",
14
+ GLM_MODEL: "GLM-5.2",
15
+ GLM_BASE_URL: "https://api.z.ai/api/v1",
16
+ MINIMAX_API_KEY: "pk-test-minimax",
17
+ MINIMAX_MODEL: "MiniMax-M2",
18
+ MINIMAX_BASE_URL: "https://api.minimaxi.com/v1",
19
+ };
20
+
21
+ describe("resolveModel", () => {
22
+ const original = process.env;
23
+
24
+ beforeEach(() => {
25
+ process.env = { ...original, ...TEST_ENV };
26
+ });
27
+
28
+ afterEach(() => {
29
+ process.env = original;
30
+ });
31
+
32
+ it("resolves GLM family from env", () => {
33
+ const r = resolveModel("glm");
34
+ assert.equal(r.model, "GLM-5.2");
35
+ assert.equal(r.baseURL, "https://api.z.ai/api/v1");
36
+ assert.equal(r.apiKey, "pk-test-glm");
37
+ });
38
+
39
+ it("resolves PLANNER family from env", () => {
40
+ const r = resolveModel("planner");
41
+ assert.equal(r.model, "gpt-5.6");
42
+ assert.equal(r.apiKey, "pk-test-planner");
43
+ });
44
+
45
+ it("resolves MINIMAX family from env", () => {
46
+ const r = resolveModel("minimax");
47
+ assert.equal(r.model, "MiniMax-M2");
48
+ assert.equal(r.apiKey, "pk-test-minimax");
49
+ });
50
+
51
+ it("is case-insensitive", () => {
52
+ const r = resolveModel("GLM");
53
+ assert.equal(r.model, "GLM-5.2");
54
+ });
55
+
56
+ it("throws if API key is missing", () => {
57
+ delete process.env.GLM_API_KEY;
58
+ assert.throws(() => resolveModel("glm"), /GLM_API_KEY is not set/);
59
+ });
60
+
61
+ it("throws if MODEL is missing", () => {
62
+ delete process.env.GLM_MODEL;
63
+ assert.throws(() => resolveModel("glm"), /GLM_MODEL is not set/);
64
+ });
65
+
66
+ it("throws if BASE_URL is missing", () => {
67
+ delete process.env.GLM_BASE_URL;
68
+ assert.throws(() => resolveModel("glm"), /GLM_BASE_URL is not set/);
69
+ });
70
+ });
71
+
72
+ describe("routeRequest", () => {
73
+ const original = process.env;
74
+
75
+ beforeEach(() => {
76
+ process.env = { ...original, ...TEST_ENV };
77
+ });
78
+
79
+ afterEach(() => {
80
+ process.env = original;
81
+ });
82
+
83
+ it("default: planner→PLANNER, reviewer→GLM, coder→MINIMAX", () => {
84
+ const r = routeRequest("Implement a rate limiter");
85
+ assert.equal(r.planner.model, "gpt-5.6");
86
+ assert.equal(r.reviewer.model, "GLM-5.2");
87
+ assert.equal(r.coder.model, "MiniMax-M2");
88
+ assert.equal(r.cleanRequest, "Implement a rate limiter");
89
+ });
90
+
91
+ it("strips [role: family] directives from cleanRequest", () => {
92
+ const r = routeRequest(
93
+ "Fix the bug [planner: gpt] [reviewer: GLM] [coder: MiniMax]",
94
+ );
95
+ assert.equal(r.cleanRequest, "Fix the bug");
96
+ });
97
+
98
+ it("override reviewer with GPT directive → PLANNER env", () => {
99
+ const r = routeRequest("Implement feature X [reviewer: gpt]");
100
+ assert.equal(r.reviewer.model, "gpt-5.6");
101
+ assert.equal(r.cleanRequest, "Implement feature X");
102
+ });
103
+
104
+ it("override planner with GLM directive", () => {
105
+ const r = routeRequest("Plan this task [planner: glm]");
106
+ assert.equal(r.planner.model, "GLM-5.2");
107
+ });
108
+
109
+ it("override coder with GPT directive → PLANNER env", () => {
110
+ const r = routeRequest("Write the code [coder: gpt]");
111
+ assert.equal(r.coder.model, "gpt-5.6");
112
+ });
113
+
114
+ it("multiple directives together", () => {
115
+ const r = routeRequest(
116
+ "Implement X [planner: gpt] [reviewer: GLM] [coder: MiniMax]",
117
+ );
118
+ assert.equal(r.planner.model, "gpt-5.6");
119
+ assert.equal(r.reviewer.model, "GLM-5.2");
120
+ assert.equal(r.coder.model, "MiniMax-M2");
121
+ });
122
+
123
+ it("partial directive — only reviewer overridden", () => {
124
+ const r = routeRequest("Do it [reviewer: gpt]");
125
+ assert.equal(r.reviewer.model, "gpt-5.6");
126
+ assert.equal(r.planner.model, "gpt-5.6");
127
+ assert.equal(r.coder.model, "MiniMax-M2");
128
+ });
129
+
130
+ it("unknown family throws — must be a recognised family name", () => {
131
+ // FAMILY_MAP only knows: gpt, glm, minimax
132
+ // unknown families are passed through as-is, causing readEnv to throw
133
+ assert.throws(
134
+ () => routeRequest("Do it [reviewer: unknown-provider]"),
135
+ /UNKNOWN-PROVIDER_API_KEY is not set/,
136
+ );
137
+ });
138
+ });
139
+
140
+ describe("routeRequestSmart — natural language hints", () => {
141
+ const original = process.env;
142
+
143
+ beforeEach(() => {
144
+ process.env = { ...original, ...TEST_ENV };
145
+ });
146
+
147
+ afterEach(() => {
148
+ process.env = original;
149
+ });
150
+
151
+ it("'use GLM as master reviewer' → reviewer→GLM", () => {
152
+ const r = routeRequestSmart(
153
+ "Implement a rate limiter. Use GLM as master reviewer.",
154
+ );
155
+ assert.equal(r.reviewer.model, "GLM-5.2");
156
+ });
157
+
158
+ it("'use GPT as reviewer' → reviewer→PLANNER", () => {
159
+ const r = routeRequestSmart("Implement a rate limiter. Use GPT as reviewer.");
160
+ assert.equal(r.reviewer.model, "gpt-5.6");
161
+ });
162
+
163
+ it("'[reviewer: GLM]' directive beats natural hint", () => {
164
+ const r = routeRequestSmart(
165
+ "Implement X. Use GPT as master reviewer. [reviewer: GLM]",
166
+ );
167
+ // directive wins
168
+ assert.equal(r.reviewer.model, "GLM-5.2");
169
+ assert.equal(r.cleanRequest, "Implement X. Use GPT as master reviewer.");
170
+ });
171
+
172
+ it("'GPT as planner' → planner→PLANNER", () => {
173
+ const r = routeRequestSmart("Plan the implementation. GPT as planner.");
174
+ assert.equal(r.planner.model, "gpt-5.6");
175
+ });
176
+
177
+ it("strips directive comments but keeps natural hints", () => {
178
+ const r = routeRequestSmart(
179
+ "Fix the bug. Use GLM as master reviewer. [reviewer: gpt]",
180
+ );
181
+ assert.equal(r.cleanRequest, "Fix the bug. Use GLM as master reviewer.");
182
+ assert.equal(r.reviewer.model, "gpt-5.6"); // directive wins
183
+ });
184
+
185
+ it("defaults all three when no hints or directives", () => {
186
+ const r = routeRequestSmart("Just do it.");
187
+ assert.equal(r.planner.model, "gpt-5.6");
188
+ assert.equal(r.reviewer.model, "GLM-5.2");
189
+ assert.equal(r.coder.model, "MiniMax-M2");
190
+ });
191
+ });