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.
- package/CHANGELOG.md +30 -9
- package/README.md +119 -0
- package/harness/langchain/agents.d.ts +143 -0
- package/harness/langchain/agents.test.d.ts +2 -0
- package/harness/langchain/agents.ts +196 -13
- package/harness/langchain/checkpointer.d.ts +48 -0
- package/harness/langchain/checkpointer.test.d.ts +8 -0
- package/harness/langchain/cron-cli.d.ts +12 -0
- package/harness/langchain/cron-cli.ts +169 -0
- package/harness/langchain/daemon.d.ts +161 -0
- package/harness/langchain/daemon.test.d.ts +11 -0
- package/harness/langchain/daemon.test.ts +3 -1
- package/harness/langchain/daemon.ts +230 -12
- package/harness/langchain/file-tools.d.ts +97 -0
- package/harness/langchain/file-tools.ts +14 -6
- package/harness/langchain/graph.d.ts +706 -0
- package/harness/langchain/graph.ts +158 -25
- package/harness/langchain/model-router.d.ts +63 -0
- package/harness/langchain/model-router.test.d.ts +2 -0
- package/harness/langchain/model-router.test.ts +191 -0
- package/harness/langchain/model-router.ts +188 -0
- package/harness/langchain/models.d.ts +72 -0
- package/harness/langchain/models.ts +111 -49
- package/harness/langchain/ping-pong-middleware.d.ts +28 -0
- package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
- package/harness/langchain/ping-pong-middleware.test.ts +69 -0
- package/harness/langchain/ping-pong-middleware.ts +204 -0
- package/harness/langchain/run.d.ts +15 -0
- package/harness/langchain/run.ts +15 -3
- package/harness/langchain/scoreboard.test.d.ts +13 -0
- package/harness/langchain/scoreboard.test.ts +23 -1
- package/harness/langchain/status-line.d.ts +91 -0
- package/harness/langchain/status-line.test.d.ts +8 -0
- package/harness/langchain/surge.d.ts +103 -0
- package/harness/langchain/surge.test.d.ts +10 -0
- package/harness/langchain/surge.test.ts +179 -0
- package/harness/langchain/surge.ts +188 -19
- package/harness/langchain/widget.d.ts +105 -0
- package/harness/langchain/widget.test.d.ts +8 -0
- package/harness/ssh-detach-interceptor.test.ts +263 -0
- package/harness/ssh-detach-interceptor.ts +438 -0
- package/index.ts +212 -47
- package/package.json +4 -2
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
- package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
- package/packages/intent-analyzer/dist/src/index.js +1 -0
- package/packages/intent-analyzer/dist/src/index.js.map +1 -1
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
- package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
- package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/intent-analyzer/src/index.ts +13 -0
- package/packages/intent-analyzer/src/ping-pong.ts +387 -0
- package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
- package/packages/jev-judge/README.md +234 -0
- package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
- package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
- package/packages/jev-judge/dist/decision-engine.js +151 -0
- package/packages/jev-judge/dist/decision-engine.js.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
- package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
- package/packages/jev-judge/dist/e2e-judge.js +268 -0
- package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
- package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
- package/packages/jev-judge/dist/flaky-detector.js +154 -0
- package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
- package/packages/jev-judge/dist/index.d.ts +77 -0
- package/packages/jev-judge/dist/index.d.ts.map +1 -0
- package/packages/jev-judge/dist/index.js +271 -0
- package/packages/jev-judge/dist/index.js.map +1 -0
- package/packages/jev-judge/dist/types.d.ts +97 -0
- package/packages/jev-judge/dist/types.d.ts.map +1 -0
- package/packages/jev-judge/dist/types.js +8 -0
- package/packages/jev-judge/dist/types.js.map +1 -0
- package/packages/jev-judge/package.json +46 -0
- package/packages/jev-judge/src/decision-engine.ts +231 -0
- package/packages/jev-judge/src/e2e-judge.ts +364 -0
- package/packages/jev-judge/src/flaky-detector.ts +190 -0
- package/packages/jev-judge/src/index.ts +345 -0
- package/packages/jev-judge/src/types.ts +131 -0
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/README.md +71 -0
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/todo-bd-sync/src/extension.ts +16 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
- package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
- package/packages/trajectory/dist/src/approved-patterns.js +134 -0
- package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
- package/packages/trajectory/dist/src/index.d.ts +13 -0
- package/packages/trajectory/dist/src/index.d.ts.map +1 -0
- package/packages/trajectory/dist/src/index.js +12 -0
- package/packages/trajectory/dist/src/index.js.map +1 -0
- package/packages/trajectory/dist/src/store.d.ts +69 -0
- package/packages/trajectory/dist/src/store.d.ts.map +1 -0
- package/packages/trajectory/dist/src/store.js +253 -0
- package/packages/trajectory/dist/src/store.js.map +1 -0
- package/packages/trajectory/dist/src/types.d.ts +73 -0
- package/packages/trajectory/dist/src/types.d.ts.map +1 -0
- package/packages/trajectory/dist/src/types.js +9 -0
- package/packages/trajectory/dist/src/types.js.map +1 -0
- package/packages/trajectory/dist/test/store.test.d.ts +2 -0
- package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
- package/packages/trajectory/dist/test/store.test.js +228 -0
- package/packages/trajectory/dist/test/store.test.js.map +1 -0
- package/packages/trajectory/package.json +29 -0
- package/packages/trajectory/src/approved-patterns.ts +180 -0
- package/packages/trajectory/src/index.ts +26 -0
- package/packages/trajectory/src/store.ts +285 -0
- package/packages/trajectory/src/types.ts +84 -0
- package/packages/trajectory/test/store.test.d.ts +2 -0
- package/packages/trajectory/test/store.test.ts +250 -0
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/packages/write-review/src/blackboard.ts +24 -1
- package/packages/write-review/src/types.ts +13 -0
- 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
|
-
/**
|
|
53
|
-
|
|
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: (
|
|
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
|
-
|
|
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 >=
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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:
|
|
258
|
-
|
|
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
|
-
|
|
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,
|
|
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,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
|
+
});
|