pi-harness-runtime 1.1.49 → 1.1.71-3
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.test.ts +0 -1
- 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 +62 -5
- package/harness/langchain/daemon.ts +231 -12
- package/harness/langchain/file-tools.d.ts +97 -0
- package/harness/langchain/file-tools.ts +29 -11
- package/harness/langchain/graph.d.ts +706 -0
- package/harness/langchain/graph.ts +184 -26
- 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 +28 -5
- 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 +38 -5
- package/packages/write-review/src/types.ts +13 -0
- package/skills/harness-runtime/SKILL.md +67 -4
|
@@ -20,13 +20,15 @@ import {
|
|
|
20
20
|
StateGraph,
|
|
21
21
|
} from "@langchain/langgraph";
|
|
22
22
|
import {
|
|
23
|
-
autonomyDirective,
|
|
24
23
|
isAutonomyRequest,
|
|
24
|
+
autonomyDirective,
|
|
25
25
|
lastMessage,
|
|
26
26
|
type ReviewVerdict,
|
|
27
27
|
ReviewVerdictSchema,
|
|
28
28
|
} from "./agents.js";
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
29
30
|
import { WriteReviewBlackboard } from "../../packages/write-review/src/blackboard.js";
|
|
31
|
+
import { getApprovedPatternStore } from "../../packages/trajectory/src/index.js";
|
|
30
32
|
import type { Verdict } from "../../packages/write-review/src/types.js";
|
|
31
33
|
import type { LoopWidget } from "./widget.js";
|
|
32
34
|
|
|
@@ -51,6 +53,17 @@ const LoopState = Annotation.Root({
|
|
|
51
53
|
reducer: (prev, next) => [...(prev ?? []), ...(next ?? [])],
|
|
52
54
|
default: () => [],
|
|
53
55
|
}),
|
|
56
|
+
/** Code from the previous iteration. Used for stuck detection (P0-2). */
|
|
57
|
+
prevCode: Annotation<string | undefined>({
|
|
58
|
+
reducer: (_prev, next) => next,
|
|
59
|
+
default: () => undefined,
|
|
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
|
+
}),
|
|
54
67
|
});
|
|
55
68
|
|
|
56
69
|
export type LoopState = typeof LoopState.State;
|
|
@@ -60,7 +73,22 @@ export type LoopState = typeof LoopState.State;
|
|
|
60
73
|
export interface LoopDeps {
|
|
61
74
|
plan: (request: string) => Promise<string>;
|
|
62
75
|
write: (plan: string, review: ReviewVerdict | null) => Promise<string>;
|
|
63
|
-
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>;
|
|
64
92
|
maxIterations: number;
|
|
65
93
|
onStep?: (step: string, state: LoopState) => void;
|
|
66
94
|
/** Optional widget for TUI / status-line display (mirrors pi-lens footer style). */
|
|
@@ -81,18 +109,60 @@ function planNode(deps: LoopDeps) {
|
|
|
81
109
|
};
|
|
82
110
|
}
|
|
83
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
|
+
|
|
84
139
|
function writeNode(deps: LoopDeps) {
|
|
85
140
|
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
86
141
|
const code = await deps.write(state.plan, state.review ?? null);
|
|
87
142
|
const iter = state.iteration + 1;
|
|
88
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
|
+
}
|
|
89
154
|
const reviewNote = state.review
|
|
90
155
|
? ` (addressing ${state.review.comments.length} review comment(s))`
|
|
91
156
|
: "";
|
|
92
|
-
// Note: previous `review` stays in state on purpose — the coder reads its comments.
|
|
93
157
|
return {
|
|
94
158
|
code,
|
|
159
|
+
writtenFiles,
|
|
95
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,
|
|
96
166
|
log: [`[write:${iter}] MiniMax wrote code${reviewNote}`],
|
|
97
167
|
};
|
|
98
168
|
};
|
|
@@ -100,7 +170,14 @@ function writeNode(deps: LoopDeps) {
|
|
|
100
170
|
|
|
101
171
|
function reviewNode(deps: LoopDeps) {
|
|
102
172
|
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
103
|
-
|
|
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
|
+
);
|
|
104
181
|
deps.onStep?.("review", state);
|
|
105
182
|
return {
|
|
106
183
|
review,
|
|
@@ -109,15 +186,25 @@ function reviewNode(deps: LoopDeps) {
|
|
|
109
186
|
};
|
|
110
187
|
}
|
|
111
188
|
|
|
112
|
-
function finishNode() {
|
|
189
|
+
function finishNode(maxIterations: number) {
|
|
113
190
|
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
114
191
|
const verdict = state.review?.verdict ?? "blocked";
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
192
|
+
const comments = state.review?.comments ?? [];
|
|
193
|
+
let reason: string;
|
|
194
|
+
if (verdict === "approved") {
|
|
195
|
+
reason = "reviewer approved";
|
|
196
|
+
} else if (verdict === "blocked") {
|
|
197
|
+
reason = "reviewer blocked the task";
|
|
198
|
+
} else if (state.iteration >= maxIterations) {
|
|
199
|
+
reason = `max iterations (${state.iteration}) reached with changes still requested`;
|
|
200
|
+
} else if (
|
|
201
|
+
comments.length > 0 &&
|
|
202
|
+
comments.every((c) => c.severity === "minor" || !c.severity)
|
|
203
|
+
) {
|
|
204
|
+
reason = `converged: only minor comments (${state.iteration})`;
|
|
205
|
+
} else {
|
|
206
|
+
reason = `stuck: changes still requested after ${state.iteration} iteration(s)`;
|
|
207
|
+
}
|
|
121
208
|
return {
|
|
122
209
|
log: [`[finish] ${state.iteration} iteration(s) — ${reason}`],
|
|
123
210
|
};
|
|
@@ -126,13 +213,28 @@ function finishNode() {
|
|
|
126
213
|
|
|
127
214
|
// ─── Routing ────────────────────────────────────────────────────────────────
|
|
128
215
|
|
|
216
|
+
/** Smart stop: loop terminates early when quality criteria are met. */
|
|
129
217
|
function routeAfterReview(
|
|
130
218
|
state: LoopState,
|
|
131
219
|
maxIterations: number,
|
|
132
220
|
): "write" | "finish" {
|
|
133
221
|
const verdict = state.review?.verdict;
|
|
222
|
+
const comments = state.review?.comments ?? [];
|
|
223
|
+
// 1. Always finish on approved or blocked
|
|
134
224
|
if (verdict === "approved" || verdict === "blocked") return "finish";
|
|
225
|
+
// 2. Hard cap
|
|
135
226
|
if (state.iteration >= maxIterations) return "finish";
|
|
227
|
+
// 3. SMART STOP: only minor comments (≤2) — converge fast
|
|
228
|
+
const hasOnlyMinor = comments.every(
|
|
229
|
+
(c) => c.severity === "minor" || !c.severity,
|
|
230
|
+
);
|
|
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
|
+
}
|
|
136
238
|
return "write";
|
|
137
239
|
}
|
|
138
240
|
|
|
@@ -148,7 +250,7 @@ export function buildWriteReviewLoop(
|
|
|
148
250
|
.addNode("planStep", planNode(deps))
|
|
149
251
|
.addNode("writeStep", writeNode(deps))
|
|
150
252
|
.addNode("reviewStep", reviewNode(deps))
|
|
151
|
-
.addNode("finishStep", finishNode())
|
|
253
|
+
.addNode("finishStep", finishNode(deps.maxIterations))
|
|
152
254
|
.addEdge(START, "planStep")
|
|
153
255
|
.addEdge("planStep", "writeStep")
|
|
154
256
|
.addEdge("writeStep", "reviewStep")
|
|
@@ -182,6 +284,15 @@ export interface RealLoopOptions {
|
|
|
182
284
|
onStep?: (step: string, state: LoopState) => void;
|
|
183
285
|
/** Directory for .write-review/blackboard. Defaults to process.cwd(). */
|
|
184
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;
|
|
185
296
|
importAgents?: () => Promise<{
|
|
186
297
|
createPlannerAgent: () => {
|
|
187
298
|
invoke: (input: { messages: unknown[] }) => Promise<{
|
|
@@ -214,9 +325,27 @@ export async function buildRealLoopDeps(
|
|
|
214
325
|
options: RealLoopOptions = {},
|
|
215
326
|
): Promise<LoopDeps> {
|
|
216
327
|
const mod = await import("./agents.js");
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
};
|
|
220
349
|
|
|
221
350
|
// Create and init the shared blackboard
|
|
222
351
|
const blackboard = new WriteReviewBlackboard(
|
|
@@ -228,9 +357,8 @@ export async function buildRealLoopDeps(
|
|
|
228
357
|
blackboard.init();
|
|
229
358
|
}
|
|
230
359
|
|
|
231
|
-
// Autonomy mode:
|
|
232
|
-
|
|
233
|
-
const _directive = "";
|
|
360
|
+
// Autonomy mode: set in plan() based on request content, reused by write()/review()
|
|
361
|
+
let _directive = "";
|
|
234
362
|
|
|
235
363
|
/** Inject scoreboard markdown into a user message. */
|
|
236
364
|
const withScoreboard = (msg: string) => {
|
|
@@ -238,6 +366,16 @@ export async function buildRealLoopDeps(
|
|
|
238
366
|
return msg + "\n\n## Current Progress\n" + scoreboard;
|
|
239
367
|
};
|
|
240
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
|
+
|
|
241
379
|
// Wrap onStep to also update the blackboard
|
|
242
380
|
const originalOnStep = options.onStep;
|
|
243
381
|
const onStep = (step: string, state: LoopState) => {
|
|
@@ -280,14 +418,17 @@ export async function buildRealLoopDeps(
|
|
|
280
418
|
return {
|
|
281
419
|
maxIterations: options.maxIterations ?? 3,
|
|
282
420
|
onStep,
|
|
283
|
-
plan: async (request) =>
|
|
284
|
-
|
|
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(
|
|
285
425
|
await planner.invoke({
|
|
286
426
|
messages: [
|
|
287
427
|
{ role: "user", content: _directive + withScoreboard(request) },
|
|
288
428
|
],
|
|
289
429
|
}),
|
|
290
|
-
)
|
|
430
|
+
);
|
|
431
|
+
},
|
|
291
432
|
write: async (plan, review) => {
|
|
292
433
|
const userMsg = review
|
|
293
434
|
? `## Plan\n${plan}\n\n## Review comments to address\n${review.comments
|
|
@@ -302,14 +443,20 @@ export async function buildRealLoopDeps(
|
|
|
302
443
|
}),
|
|
303
444
|
);
|
|
304
445
|
},
|
|
305
|
-
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\`\`\``;
|
|
306
454
|
const result = await reviewer.invoke({
|
|
307
455
|
messages: [
|
|
308
456
|
{
|
|
309
457
|
role: "user",
|
|
310
458
|
content:
|
|
311
|
-
_directive +
|
|
312
|
-
withScoreboard(`## Plan\n${plan}\n\n## Code to review\n${code}`),
|
|
459
|
+
_directive + withReviewContext(`## Plan\n${plan}\n\n${codeSection}`),
|
|
313
460
|
},
|
|
314
461
|
],
|
|
315
462
|
});
|
|
@@ -325,6 +472,9 @@ export async function buildRealLoopDeps(
|
|
|
325
472
|
};
|
|
326
473
|
}
|
|
327
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),
|
|
328
478
|
};
|
|
329
479
|
}
|
|
330
480
|
|
|
@@ -406,7 +556,7 @@ export function buildDryRunDeps(
|
|
|
406
556
|
`\`\`\`ts\n// stub code, iteration ${writeCount}\nexport const plan = ${JSON.stringify(plan.slice(0, 60))};${fixNote}\n\`\`\``,
|
|
407
557
|
);
|
|
408
558
|
},
|
|
409
|
-
review: async (_plan, _code) => {
|
|
559
|
+
review: async (_plan, _code, _writtenFiles = {}) => {
|
|
410
560
|
if (writeCount < 2) {
|
|
411
561
|
return {
|
|
412
562
|
verdict: "changes_requested",
|
|
@@ -415,7 +565,7 @@ export function buildDryRunDeps(
|
|
|
415
565
|
{
|
|
416
566
|
file: "index.ts",
|
|
417
567
|
comment: "dry-run: rename the exported constant",
|
|
418
|
-
severity: "
|
|
568
|
+
severity: "major",
|
|
419
569
|
},
|
|
420
570
|
],
|
|
421
571
|
};
|
|
@@ -426,5 +576,13 @@ export function buildDryRunDeps(
|
|
|
426
576
|
comments: [],
|
|
427
577
|
};
|
|
428
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
|
+
},
|
|
429
587
|
};
|
|
430
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
|
+
});
|