pi-harness-runtime 1.1.30 ā 1.1.32
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/harness/langchain/agents.ts +198 -0
- package/harness/langchain/checkpointer.test.ts +269 -0
- package/harness/langchain/checkpointer.ts +343 -0
- package/harness/langchain/daemon.test.ts +398 -0
- package/harness/langchain/daemon.ts +921 -0
- package/harness/langchain/graph.ts +291 -0
- package/harness/langchain/models.ts +95 -0
- package/harness/langchain/run.ts +207 -0
- package/harness/langchain/surge.test.ts +214 -0
- package/harness/langchain/surge.ts +217 -0
- package/package.json +8 -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/dist/src/types.d.ts +1 -1
- package/packages/event-bus/dist/src/types.d.ts.map +1 -1
- package/packages/event-bus/dist/src/types.js.map +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-bus/src/types.ts +1 -0
- 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/package.json +1 -1
- 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/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- 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
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic write-review loop as a LangGraph StateGraph.
|
|
3
|
+
*
|
|
4
|
+
* START ā plan (GPT) ā write (MiniMax) ā review (GPT)
|
|
5
|
+
* āā approved / blocked / max-iter ā finish
|
|
6
|
+
* āā changes_requested ā write (with comments)
|
|
7
|
+
*
|
|
8
|
+
* Unlike the supervisor variant (agents.ts, agents.py style) where an LLM
|
|
9
|
+
* decides routing, here the structured review verdict drives the conditional
|
|
10
|
+
* edge ā the loop is guaranteed to terminate at maxIterations.
|
|
11
|
+
*
|
|
12
|
+
* Wiki: wiki/multi-agent-langchain.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
Annotation,
|
|
17
|
+
END,
|
|
18
|
+
MemorySaver,
|
|
19
|
+
START,
|
|
20
|
+
StateGraph,
|
|
21
|
+
} from "@langchain/langgraph";
|
|
22
|
+
import {
|
|
23
|
+
lastMessage,
|
|
24
|
+
type ReviewVerdict,
|
|
25
|
+
ReviewVerdictSchema,
|
|
26
|
+
} from "./agents.js";
|
|
27
|
+
|
|
28
|
+
// āāā State āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
29
|
+
|
|
30
|
+
const LoopState = Annotation.Root({
|
|
31
|
+
/** Original user request */
|
|
32
|
+
request: Annotation<string>,
|
|
33
|
+
/** Plan produced by the GPT planner */
|
|
34
|
+
plan: Annotation<string>,
|
|
35
|
+
/** Current iteration (0 = first pass) */
|
|
36
|
+
iteration: Annotation<number>({
|
|
37
|
+
reducer: (_prev, next) => next,
|
|
38
|
+
default: () => 0,
|
|
39
|
+
}),
|
|
40
|
+
/** Latest code output from the MiniMax coder */
|
|
41
|
+
code: Annotation<string>,
|
|
42
|
+
/** Latest structured review from the GPT reviewer */
|
|
43
|
+
review: Annotation<ReviewVerdict>,
|
|
44
|
+
/** Human-readable step log (reducer appends) */
|
|
45
|
+
log: Annotation<string[]>({
|
|
46
|
+
reducer: (prev, next) => [...(prev ?? []), ...(next ?? [])],
|
|
47
|
+
default: () => [],
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type LoopState = typeof LoopState.State;
|
|
52
|
+
|
|
53
|
+
// āāā Dependencies (inject real agents or dry-run stubs) āāāāāāāāāāāāāāāāāāāāā
|
|
54
|
+
|
|
55
|
+
export interface LoopDeps {
|
|
56
|
+
plan: (request: string) => Promise<string>;
|
|
57
|
+
write: (plan: string, review: ReviewVerdict | null) => Promise<string>;
|
|
58
|
+
review: (plan: string, code: string) => Promise<ReviewVerdict>;
|
|
59
|
+
maxIterations: number;
|
|
60
|
+
onStep?: (step: string, state: LoopState) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// āāā Node implementations āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
64
|
+
|
|
65
|
+
function planNode(deps: LoopDeps) {
|
|
66
|
+
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
67
|
+
const plan = await deps.plan(state.request);
|
|
68
|
+
deps.onStep?.("plan", state);
|
|
69
|
+
return {
|
|
70
|
+
plan,
|
|
71
|
+
iteration: 0,
|
|
72
|
+
log: [`[plan] GPT produced plan (${plan.length} chars)`],
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeNode(deps: LoopDeps) {
|
|
78
|
+
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
79
|
+
const code = await deps.write(state.plan, state.review ?? null);
|
|
80
|
+
const iter = state.iteration + 1;
|
|
81
|
+
deps.onStep?.("write", { ...state, iteration: iter });
|
|
82
|
+
const reviewNote = state.review
|
|
83
|
+
? ` (addressing ${state.review.comments.length} review comment(s))`
|
|
84
|
+
: "";
|
|
85
|
+
// Note: previous `review` stays in state on purpose ā the coder reads its comments.
|
|
86
|
+
return {
|
|
87
|
+
code,
|
|
88
|
+
iteration: iter,
|
|
89
|
+
log: [`[write:${iter}] MiniMax wrote code${reviewNote}`],
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function reviewNode(deps: LoopDeps) {
|
|
95
|
+
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
96
|
+
const review = await deps.review(state.plan, state.code);
|
|
97
|
+
deps.onStep?.("review", state);
|
|
98
|
+
return {
|
|
99
|
+
review,
|
|
100
|
+
log: [`[review:${state.iteration}] GPT verdict: ${review.verdict}`],
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function finishNode() {
|
|
106
|
+
return async (state: LoopState): Promise<Partial<LoopState>> => {
|
|
107
|
+
const verdict = state.review?.verdict ?? "blocked";
|
|
108
|
+
const reason =
|
|
109
|
+
verdict === "approved"
|
|
110
|
+
? "reviewer approved"
|
|
111
|
+
: verdict === "changes_requested"
|
|
112
|
+
? `max iterations (${state.iteration}) reached with changes still requested`
|
|
113
|
+
: "reviewer blocked the task";
|
|
114
|
+
return {
|
|
115
|
+
log: [`[finish] ${state.iteration} iteration(s) ā ${reason}`],
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// āāā Routing āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
121
|
+
|
|
122
|
+
function routeAfterReview(
|
|
123
|
+
state: LoopState,
|
|
124
|
+
maxIterations: number,
|
|
125
|
+
): "write" | "finish" {
|
|
126
|
+
const verdict = state.review?.verdict;
|
|
127
|
+
if (verdict === "approved" || verdict === "blocked") return "finish";
|
|
128
|
+
if (state.iteration >= maxIterations) return "finish";
|
|
129
|
+
return "write";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// āāā Graph builder āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
133
|
+
|
|
134
|
+
export function buildWriteReviewLoop(
|
|
135
|
+
deps: LoopDeps,
|
|
136
|
+
opts: { checkpointer?: boolean | unknown } = {},
|
|
137
|
+
) {
|
|
138
|
+
// Node names must not collide with state channel names (LangGraph rule),
|
|
139
|
+
// hence the "*Step" suffixes.
|
|
140
|
+
const builder = new StateGraph(LoopState)
|
|
141
|
+
.addNode("planStep", planNode(deps))
|
|
142
|
+
.addNode("writeStep", writeNode(deps))
|
|
143
|
+
.addNode("reviewStep", reviewNode(deps))
|
|
144
|
+
.addNode("finishStep", finishNode())
|
|
145
|
+
.addEdge(START, "planStep")
|
|
146
|
+
.addEdge("planStep", "writeStep")
|
|
147
|
+
.addEdge("writeStep", "reviewStep")
|
|
148
|
+
.addConditionalEdges(
|
|
149
|
+
"reviewStep",
|
|
150
|
+
(state) => routeAfterReview(state, deps.maxIterations),
|
|
151
|
+
// Router returns logical names; map them to the physical node names
|
|
152
|
+
{ write: "writeStep", finish: "finishStep" },
|
|
153
|
+
)
|
|
154
|
+
.addEdge("finishStep", END);
|
|
155
|
+
|
|
156
|
+
// Resolve checkpointer: false=disabled, object=use it, true/undefined=default MemorySaver
|
|
157
|
+
const cp = opts.checkpointer;
|
|
158
|
+
const checkpointerToUse =
|
|
159
|
+
cp === false
|
|
160
|
+
? undefined
|
|
161
|
+
: cp != null && cp !== true
|
|
162
|
+
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
|
+
(cp as any)
|
|
164
|
+
: new MemorySaver();
|
|
165
|
+
return builder.compile({ checkpointer: checkpointerToUse });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Inferred compiled-graph type (do not hand-roll langgraph generics). */
|
|
169
|
+
export type WriteReviewLoop = ReturnType<typeof buildWriteReviewLoop>;
|
|
170
|
+
|
|
171
|
+
// āāā Real-model dependency wiring āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
172
|
+
|
|
173
|
+
export interface RealLoopOptions {
|
|
174
|
+
maxIterations?: number;
|
|
175
|
+
onStep?: (step: string, state: LoopState) => void;
|
|
176
|
+
importAgents?: () => Promise<{
|
|
177
|
+
createPlannerAgent: () => {
|
|
178
|
+
invoke: (input: { messages: unknown[] }) => Promise<{
|
|
179
|
+
messages: Array<{ content: unknown }>;
|
|
180
|
+
}>;
|
|
181
|
+
};
|
|
182
|
+
createCoderAgent: () => {
|
|
183
|
+
invoke: (input: { messages: unknown[] }) => Promise<{
|
|
184
|
+
messages: Array<{ content: unknown }>;
|
|
185
|
+
}>;
|
|
186
|
+
};
|
|
187
|
+
createReviewerAgent: () => {
|
|
188
|
+
invoke: (input: { messages: unknown[] }) => Promise<{
|
|
189
|
+
messages: Array<{ content: unknown }>;
|
|
190
|
+
structuredResponse?: ReviewVerdict;
|
|
191
|
+
}>;
|
|
192
|
+
};
|
|
193
|
+
}>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build LoopDeps backed by the real GPT/MiniMax agents from agents.ts.
|
|
198
|
+
* Kept lazy (dynamic import) so dry-run mode never touches API keys.
|
|
199
|
+
*/
|
|
200
|
+
export async function buildRealLoopDeps(
|
|
201
|
+
options: RealLoopOptions = {},
|
|
202
|
+
): Promise<LoopDeps> {
|
|
203
|
+
const mod = await import("./agents.js");
|
|
204
|
+
const planner = mod.createPlannerAgent();
|
|
205
|
+
const coder = mod.createCoderAgent();
|
|
206
|
+
const reviewer = mod.createReviewerAgent();
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
maxIterations: options.maxIterations ?? 3,
|
|
210
|
+
onStep: options.onStep,
|
|
211
|
+
plan: async (request) =>
|
|
212
|
+
lastMessage(
|
|
213
|
+
await planner.invoke({
|
|
214
|
+
messages: [{ role: "user", content: request }],
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
write: async (plan, review) => {
|
|
218
|
+
const userMsg = review
|
|
219
|
+
? `## Plan\n${plan}\n\n## Review comments to address\n${review.comments
|
|
220
|
+
.map((c, i) => `${i + 1}. ${c.comment}`)
|
|
221
|
+
.join("\n")}`
|
|
222
|
+
: `## Plan\n${plan}`;
|
|
223
|
+
return lastMessage(
|
|
224
|
+
await coder.invoke({ messages: [{ role: "user", content: userMsg }] }),
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
review: async (plan, code) => {
|
|
228
|
+
const result = await reviewer.invoke({
|
|
229
|
+
messages: [
|
|
230
|
+
{
|
|
231
|
+
role: "user",
|
|
232
|
+
content: `## Plan\n${plan}\n\n## Code to review\n${code}`,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
if (result.structuredResponse) return result.structuredResponse;
|
|
237
|
+
// Fallback: try to parse the last message as JSON
|
|
238
|
+
try {
|
|
239
|
+
return ReviewVerdictSchema.parse(JSON.parse(lastMessage(result)));
|
|
240
|
+
} catch {
|
|
241
|
+
return {
|
|
242
|
+
verdict: "blocked",
|
|
243
|
+
summary: "Reviewer returned unparseable output",
|
|
244
|
+
comments: [],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// āāā Dry-run dependencies (no API keys needed) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
252
|
+
|
|
253
|
+
/** Deterministic stubs: iteration 1 requests changes, iteration 2 approves. */
|
|
254
|
+
export function buildDryRunDeps(
|
|
255
|
+
options: { maxIterations?: number; onStep?: LoopDeps["onStep"] } = {},
|
|
256
|
+
): LoopDeps {
|
|
257
|
+
let writeCount = 0;
|
|
258
|
+
return {
|
|
259
|
+
maxIterations: options.maxIterations ?? 3,
|
|
260
|
+
onStep: options.onStep,
|
|
261
|
+
plan: async (request) =>
|
|
262
|
+
`# Plan (dry-run)\n\nRequest: ${request}\n\n1. Stub step one\n2. Stub step two`,
|
|
263
|
+
write: async (plan, review) => {
|
|
264
|
+
writeCount += 1;
|
|
265
|
+
const fixNote = review
|
|
266
|
+
? `\n// addressing: ${review.comments.map((c) => c.comment).join("; ")}`
|
|
267
|
+
: "";
|
|
268
|
+
return `\`\`\`ts\n// stub code, iteration ${writeCount}\nexport const plan = ${JSON.stringify(plan.slice(0, 60))};${fixNote}\n\`\`\``;
|
|
269
|
+
},
|
|
270
|
+
review: async (_plan, _code) => {
|
|
271
|
+
if (writeCount < 2) {
|
|
272
|
+
return {
|
|
273
|
+
verdict: "changes_requested",
|
|
274
|
+
summary: "Dry-run: requesting one round of changes",
|
|
275
|
+
comments: [
|
|
276
|
+
{
|
|
277
|
+
file: "index.ts",
|
|
278
|
+
comment: "dry-run: rename the exported constant",
|
|
279
|
+
severity: "minor",
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
verdict: "approved",
|
|
286
|
+
summary: "Dry-run: looks good",
|
|
287
|
+
comments: [],
|
|
288
|
+
};
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model factories ā GPT (planner/reviewer) + MiniMax (coder).
|
|
3
|
+
*
|
|
4
|
+
* MiniMax exposes an OpenAI-compatible chat completions API, so we reuse
|
|
5
|
+
* ChatOpenAI with a baseURL override. All values are env-configurable.
|
|
6
|
+
*
|
|
7
|
+
* Wiki: wiki/multi-agent-langchain.md
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
11
|
+
|
|
12
|
+
export interface ModelOptions {
|
|
13
|
+
/** Override model id (e.g. "gpt-4o", "MiniMax-M2.1") */
|
|
14
|
+
model?: string;
|
|
15
|
+
/** Override API base URL */
|
|
16
|
+
baseURL?: string;
|
|
17
|
+
/** Override API key */
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
/** Sampling temperature */
|
|
20
|
+
temperature?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function gptDefaults(): { model: string; baseURL: string; apiKey: string } {
|
|
24
|
+
const apiKey = process.env.OPENAI_API_KEY ?? "";
|
|
25
|
+
if (!apiKey) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"OPENAI_API_KEY is not set. Add it to .env (see .env.example).",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
model: process.env.OPENAI_MODEL ?? "gpt-4o",
|
|
32
|
+
// Optional override (proxies / gateways); defaults to the public endpoint
|
|
33
|
+
baseURL: process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
|
|
34
|
+
apiKey,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function minimaxDefaults(): {
|
|
39
|
+
model: string;
|
|
40
|
+
baseURL: string;
|
|
41
|
+
apiKey: string;
|
|
42
|
+
} {
|
|
43
|
+
const apiKey = process.env.MINIMAX_API_KEY ?? "";
|
|
44
|
+
if (!apiKey) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
"MINIMAX_API_KEY is not set. Add it to .env (see .env.example).",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
model: process.env.MINIMAX_MODEL ?? "MiniMax-M2",
|
|
51
|
+
// International endpoint by default; China mainland: https://api.minimax.chat/v1
|
|
52
|
+
baseURL: process.env.MINIMAX_BASE_URL ?? "https://api.minimaxi.com/v1",
|
|
53
|
+
apiKey,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Build the `configuration.baseURL` override for ChatOpenAI (always set). */
|
|
58
|
+
function baseURLConfig(baseURL: string): {
|
|
59
|
+
configuration: { baseURL: string };
|
|
60
|
+
} {
|
|
61
|
+
return { configuration: { baseURL } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** GPT model ā used for planning and code review. */
|
|
65
|
+
export function createPlannerModel(opts: ModelOptions = {}): ChatOpenAI {
|
|
66
|
+
const d = gptDefaults();
|
|
67
|
+
return new ChatOpenAI({
|
|
68
|
+
model: opts.model ?? d.model,
|
|
69
|
+
apiKey: opts.apiKey ?? d.apiKey,
|
|
70
|
+
temperature: opts.temperature ?? 0.2,
|
|
71
|
+
...baseURLConfig(opts.baseURL ?? d.baseURL),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Reviewer shares the GPT configuration (can be overridden). */
|
|
76
|
+
export function createReviewerModel(opts: ModelOptions = {}): ChatOpenAI {
|
|
77
|
+
const d = gptDefaults();
|
|
78
|
+
return new ChatOpenAI({
|
|
79
|
+
model: opts.model ?? d.model,
|
|
80
|
+
apiKey: opts.apiKey ?? d.apiKey,
|
|
81
|
+
temperature: opts.temperature ?? 0.1, // reviews want determinism
|
|
82
|
+
...baseURLConfig(opts.baseURL ?? d.baseURL),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** MiniMax model ā used for code generation (OpenAI-compatible endpoint). */
|
|
87
|
+
export function createCoderModel(opts: ModelOptions = {}): ChatOpenAI {
|
|
88
|
+
const d = minimaxDefaults();
|
|
89
|
+
return new ChatOpenAI({
|
|
90
|
+
model: opts.model ?? d.model,
|
|
91
|
+
apiKey: opts.apiKey ?? d.apiKey,
|
|
92
|
+
temperature: opts.temperature ?? 0.3,
|
|
93
|
+
...baseURLConfig(opts.baseURL ?? d.baseURL),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI runner for the LangChain write-review loop.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* bun harness/langchain/run.ts --mode graph --request "implement X" [--max-iterations 3]
|
|
6
|
+
* bun harness/langchain/run.ts --mode graph --request "implement X" --dry-run
|
|
7
|
+
* bun harness/langchain/run.ts --mode supervisor --request "implement X"
|
|
8
|
+
*
|
|
9
|
+
* --dry-run uses deterministic stubs ā no API keys needed. Use it to verify
|
|
10
|
+
* the whole loop machinery (plan ā write ā review ā fix ā approve).
|
|
11
|
+
*
|
|
12
|
+
* Wiki: wiki/multi-agent-langchain.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import {
|
|
17
|
+
buildDryRunDeps,
|
|
18
|
+
buildRealLoopDeps,
|
|
19
|
+
buildWriteReviewLoop,
|
|
20
|
+
type LoopState,
|
|
21
|
+
} from "./graph.js";
|
|
22
|
+
|
|
23
|
+
interface CliArgs {
|
|
24
|
+
mode: "graph" | "supervisor";
|
|
25
|
+
request: string;
|
|
26
|
+
maxIterations: number;
|
|
27
|
+
dryRun: boolean;
|
|
28
|
+
daemon: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
32
|
+
const args: CliArgs = {
|
|
33
|
+
mode: "graph",
|
|
34
|
+
request: "",
|
|
35
|
+
maxIterations: 3,
|
|
36
|
+
dryRun: false,
|
|
37
|
+
daemon: false,
|
|
38
|
+
};
|
|
39
|
+
// Normalize space-separated flags (--mode graph) into --mode=graph form
|
|
40
|
+
const normalized: string[] = [];
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const arg = argv[i] as string;
|
|
43
|
+
const spaced =
|
|
44
|
+
(arg === "--mode" || arg === "--request" || arg === "--max-iterations") &&
|
|
45
|
+
argv[i + 1] !== undefined &&
|
|
46
|
+
!(argv[i + 1] as string).startsWith("--");
|
|
47
|
+
if (spaced) {
|
|
48
|
+
normalized.push(`${arg}=${argv[i + 1]}`);
|
|
49
|
+
i++;
|
|
50
|
+
} else {
|
|
51
|
+
normalized.push(arg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const arg of normalized) {
|
|
55
|
+
if (arg.startsWith("--mode=")) {
|
|
56
|
+
const mode = arg.slice("--mode=".length);
|
|
57
|
+
if (mode !== "graph" && mode !== "supervisor") {
|
|
58
|
+
throw new Error(`Unknown mode: ${mode} (expected graph|supervisor)`);
|
|
59
|
+
}
|
|
60
|
+
args.mode = mode;
|
|
61
|
+
} else if (arg.startsWith("--request=")) {
|
|
62
|
+
args.request = arg.slice("--request=".length);
|
|
63
|
+
} else if (arg.startsWith("--max-iterations=")) {
|
|
64
|
+
const n = Number.parseInt(arg.slice("--max-iterations=".length), 10);
|
|
65
|
+
if (Number.isNaN(n) || n < 1 || n > 20) {
|
|
66
|
+
throw new Error("--max-iterations must be 1-20");
|
|
67
|
+
}
|
|
68
|
+
args.maxIterations = n;
|
|
69
|
+
} else if (arg === "--dry-run") {
|
|
70
|
+
args.dryRun = true;
|
|
71
|
+
} else if (arg === "--daemon") {
|
|
72
|
+
args.daemon = true;
|
|
73
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
74
|
+
console.log(
|
|
75
|
+
[
|
|
76
|
+
"Usage: bun harness/langchain/run.ts [options]",
|
|
77
|
+
"",
|
|
78
|
+
"Options:",
|
|
79
|
+
" --mode=graph|supervisor Loop style (default: graph)",
|
|
80
|
+
' --request="..." The feature request',
|
|
81
|
+
" --max-iterations=N Max write-review rounds (default: 3)",
|
|
82
|
+
" --dry-run Deterministic stubs, no API calls",
|
|
83
|
+
" --daemon Start as a long-running daemon (auto-trigger loop)",
|
|
84
|
+
].join("\n"),
|
|
85
|
+
);
|
|
86
|
+
process.exit(0);
|
|
87
|
+
} else {
|
|
88
|
+
// Bare positional argument = request
|
|
89
|
+
if (!args.request) args.request = arg;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!args.request && !args.daemon) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
'A request is required: --request="..." or a bare string (or use --daemon to start the watcher)',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return args;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function printStep(step: string, state: LoopState): void {
|
|
101
|
+
const labels: Record<string, string> = {
|
|
102
|
+
plan: "š§ GPT planner",
|
|
103
|
+
write: "āļø MiniMax coder",
|
|
104
|
+
review: "š GPT reviewer",
|
|
105
|
+
finish: "š finish",
|
|
106
|
+
};
|
|
107
|
+
const iter = state.iteration > 0 ? ` (iteration ${state.iteration})` : "";
|
|
108
|
+
console.log(` ${labels[step] ?? step}${iter}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function runGraphLoop(args: CliArgs): Promise<void> {
|
|
112
|
+
console.log(
|
|
113
|
+
`\nāāā Write-Review Loop (graph mode)${args.dryRun ? " ā DRY RUN" : ""} āāā`,
|
|
114
|
+
);
|
|
115
|
+
console.log(`Request: ${args.request}\n`);
|
|
116
|
+
|
|
117
|
+
const deps = args.dryRun
|
|
118
|
+
? buildDryRunDeps({ maxIterations: args.maxIterations, onStep: printStep })
|
|
119
|
+
: await buildRealLoopDeps({
|
|
120
|
+
maxIterations: args.maxIterations,
|
|
121
|
+
onStep: printStep,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const loop = buildWriteReviewLoop(deps);
|
|
125
|
+
const threadId = `loop-${randomUUID().slice(0, 8)}`;
|
|
126
|
+
|
|
127
|
+
const finalState = await loop.invoke(
|
|
128
|
+
{ request: args.request },
|
|
129
|
+
{ configurable: { thread_id: threadId } },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
console.log("\nāāā Step log āāā");
|
|
133
|
+
for (const line of finalState.log) console.log(` ${line}`);
|
|
134
|
+
|
|
135
|
+
console.log("\nāāā Verdict āāā");
|
|
136
|
+
const review = finalState.review;
|
|
137
|
+
if (review) {
|
|
138
|
+
console.log(` ${review.verdict.toUpperCase()} ā ${review.summary}`);
|
|
139
|
+
if (review.comments.length > 0) {
|
|
140
|
+
console.log(" Open comments:");
|
|
141
|
+
for (const c of review.comments) {
|
|
142
|
+
const file = c.file ? ` (${c.file})` : "";
|
|
143
|
+
console.log(` - [${c.severity ?? "n/a"}] ${c.comment}${file}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
console.log(" (no review produced)");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function runSupervisor(args: CliArgs): Promise<void> {
|
|
152
|
+
if (args.dryRun) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
"--dry-run is only supported for --mode=graph (supervisor needs real models)",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
console.log("\nāāā Write-Review Loop (supervisor mode) āāā");
|
|
158
|
+
console.log(`Request: ${args.request}\n`);
|
|
159
|
+
|
|
160
|
+
const { createSupervisor, lastMessage } = await import("./agents.js");
|
|
161
|
+
const supervisor = createSupervisor();
|
|
162
|
+
const result = await supervisor.invoke({
|
|
163
|
+
messages: [{ role: "user", content: args.request }],
|
|
164
|
+
});
|
|
165
|
+
console.log("\nāāā Supervisor output āāā");
|
|
166
|
+
console.log(lastMessage(result));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function runDaemon(args: CliArgs): Promise<void> {
|
|
170
|
+
const { LoopDaemon } = await import("./daemon.js");
|
|
171
|
+
const daemon = new LoopDaemon({
|
|
172
|
+
maxIterations: args.maxIterations,
|
|
173
|
+
dryRun: args.dryRun,
|
|
174
|
+
sources: ["inbox", "bus"],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Graceful shutdown on SIGTERM / SIGINT
|
|
178
|
+
const shutdown = () => {
|
|
179
|
+
daemon.stop();
|
|
180
|
+
process.exit(0);
|
|
181
|
+
};
|
|
182
|
+
process.on("SIGTERM", shutdown);
|
|
183
|
+
process.on("SIGINT", shutdown);
|
|
184
|
+
|
|
185
|
+
daemon.start();
|
|
186
|
+
|
|
187
|
+
// Keep the process alive
|
|
188
|
+
await new Promise(() => {});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function main(): Promise<void> {
|
|
192
|
+
const args = parseArgs(process.argv.slice(2));
|
|
193
|
+
if (args.daemon) {
|
|
194
|
+
await runDaemon(args);
|
|
195
|
+
} else if (args.mode === "supervisor") {
|
|
196
|
+
await runSupervisor(args);
|
|
197
|
+
} else {
|
|
198
|
+
await runGraphLoop(args);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
main().catch((err: unknown) => {
|
|
203
|
+
console.error(
|
|
204
|
+
`[run] Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
205
|
+
);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
});
|