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.
Files changed (104) hide show
  1. package/harness/langchain/agents.ts +198 -0
  2. package/harness/langchain/checkpointer.test.ts +269 -0
  3. package/harness/langchain/checkpointer.ts +343 -0
  4. package/harness/langchain/daemon.test.ts +398 -0
  5. package/harness/langchain/daemon.ts +921 -0
  6. package/harness/langchain/graph.ts +291 -0
  7. package/harness/langchain/models.ts +95 -0
  8. package/harness/langchain/run.ts +207 -0
  9. package/harness/langchain/surge.test.ts +214 -0
  10. package/harness/langchain/surge.ts +217 -0
  11. package/package.json +8 -2
  12. package/packages/a2a-adapter/package.json +1 -1
  13. package/packages/architecture-generator/package.json +1 -1
  14. package/packages/auth/package.json +1 -1
  15. package/packages/autonomous-refactor/package.json +1 -1
  16. package/packages/autonomous-runtime/package.json +1 -1
  17. package/packages/cache-strategy/package.json +1 -1
  18. package/packages/capability-registry/package.json +1 -1
  19. package/packages/checkpoint/package.json +1 -1
  20. package/packages/cli-plugin-sdk/package.json +1 -1
  21. package/packages/clipboard/package.json +1 -1
  22. package/packages/clipboard-plugin/package.json +1 -1
  23. package/packages/code-generation/package.json +1 -1
  24. package/packages/code-review/package.json +1 -1
  25. package/packages/codex-adapter/package.json +1 -1
  26. package/packages/config-capture/package.json +1 -1
  27. package/packages/context-compiler/package.json +1 -1
  28. package/packages/context-discovery/package.json +1 -1
  29. package/packages/context-manager/package.json +1 -1
  30. package/packages/cookie-sanitizer/package.json +1 -1
  31. package/packages/cost-optimizer/package.json +1 -1
  32. package/packages/dependency-analyzer/package.json +1 -1
  33. package/packages/django-plugin/package.json +1 -1
  34. package/packages/doc-generator/package.json +1 -1
  35. package/packages/evaluation-engine/package.json +1 -1
  36. package/packages/evaluation-runner/package.json +1 -1
  37. package/packages/event-bus/dist/src/types.d.ts +1 -1
  38. package/packages/event-bus/dist/src/types.d.ts.map +1 -1
  39. package/packages/event-bus/dist/src/types.js.map +1 -1
  40. package/packages/event-bus/package.json +1 -1
  41. package/packages/event-bus/src/types.ts +1 -0
  42. package/packages/event-store/package.json +1 -1
  43. package/packages/experience-replay/package.json +1 -1
  44. package/packages/feedback-collector/package.json +1 -1
  45. package/packages/file-copy-helper/package.json +1 -1
  46. package/packages/framework-detector/package.json +1 -1
  47. package/packages/framework-plugin-sdk/package.json +1 -1
  48. package/packages/frappe-plugin/package.json +1 -1
  49. package/packages/generic-web-plugin/package.json +1 -1
  50. package/packages/health-monitor/package.json +1 -1
  51. package/packages/intent-analyzer/package.json +1 -1
  52. package/packages/knowledge-graph/package.json +1 -1
  53. package/packages/knowledge-retrieval/package.json +1 -1
  54. package/packages/laravel-plugin/package.json +1 -1
  55. package/packages/learning-engine/package.json +1 -1
  56. package/packages/mcp-adapter/package.json +1 -1
  57. package/packages/memory-engine/package.json +1 -1
  58. package/packages/milestone-manager/package.json +1 -1
  59. package/packages/model-registry/package.json +1 -1
  60. package/packages/nextjs-plugin/package.json +1 -1
  61. package/packages/notification/package.json +1 -1
  62. package/packages/observability/package.json +1 -1
  63. package/packages/okf-indexer/package.json +1 -1
  64. package/packages/performance-optimizer/package.json +1 -1
  65. package/packages/privilege-broker/package.json +1 -1
  66. package/packages/project-analyzer/package.json +1 -1
  67. package/packages/project-bootstrap/package.json +1 -1
  68. package/packages/projection-engine/package.json +1 -1
  69. package/packages/prompt-compiler/package.json +1 -1
  70. package/packages/prompt-versioning/package.json +1 -1
  71. package/packages/provider-adapter-sdk/package.json +1 -1
  72. package/packages/provider-router/package.json +1 -1
  73. package/packages/provider-selector/package.json +1 -1
  74. package/packages/providers/package.json +1 -1
  75. package/packages/quota-manager/package.json +1 -1
  76. package/packages/rate-limiter/package.json +1 -1
  77. package/packages/react-vite-plugin/package.json +1 -1
  78. package/packages/release-manager/package.json +1 -1
  79. package/packages/requirement-compiler/package.json +1 -1
  80. package/packages/runtime/package.json +1 -1
  81. package/packages/scheduler/package.json +1 -1
  82. package/packages/scheduler-adapter/package.json +1 -1
  83. package/packages/session/package.json +1 -1
  84. package/packages/session-api/package.json +1 -1
  85. package/packages/session-export/package.json +1 -1
  86. package/packages/shared-context/package.json +1 -1
  87. package/packages/skill-mcp-client/package.json +1 -1
  88. package/packages/skill-registry/package.json +1 -1
  89. package/packages/sprint-planner/package.json +1 -1
  90. package/packages/subscription-engine/package.json +1 -1
  91. package/packages/task-compiler/package.json +1 -1
  92. package/packages/tencentdb-memory/package.json +1 -1
  93. package/packages/tencentdb-sync/package.json +1 -1
  94. package/packages/test-data-generator/package.json +1 -1
  95. package/packages/test-generator/package.json +1 -1
  96. package/packages/todo-bd-sync/package.json +1 -1
  97. package/packages/token-estimation/package.json +1 -1
  98. package/packages/token-optimizer/package.json +1 -1
  99. package/packages/tui/package.json +1 -1
  100. package/packages/types/package.json +1 -1
  101. package/packages/workflow-events/package.json +1 -1
  102. package/packages/workspace-scanner/package.json +1 -1
  103. package/packages/worktree/package.json +1 -1
  104. 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
+ });