pi-harness-runtime 1.1.50 → 1.1.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/CHANGELOG.md +30 -9
  2. package/README.md +119 -0
  3. package/harness/langchain/agents.d.ts +143 -0
  4. package/harness/langchain/agents.test.d.ts +2 -0
  5. package/harness/langchain/agents.ts +196 -13
  6. package/harness/langchain/checkpointer.d.ts +48 -0
  7. package/harness/langchain/checkpointer.test.d.ts +8 -0
  8. package/harness/langchain/cron-cli.d.ts +12 -0
  9. package/harness/langchain/cron-cli.ts +169 -0
  10. package/harness/langchain/daemon.d.ts +161 -0
  11. package/harness/langchain/daemon.test.d.ts +11 -0
  12. package/harness/langchain/daemon.test.ts +3 -1
  13. package/harness/langchain/daemon.ts +230 -12
  14. package/harness/langchain/file-tools.d.ts +97 -0
  15. package/harness/langchain/file-tools.ts +14 -6
  16. package/harness/langchain/graph.d.ts +706 -0
  17. package/harness/langchain/graph.ts +158 -25
  18. package/harness/langchain/model-router.d.ts +63 -0
  19. package/harness/langchain/model-router.test.d.ts +2 -0
  20. package/harness/langchain/model-router.test.ts +191 -0
  21. package/harness/langchain/model-router.ts +188 -0
  22. package/harness/langchain/models.d.ts +72 -0
  23. package/harness/langchain/models.ts +111 -49
  24. package/harness/langchain/ping-pong-middleware.d.ts +28 -0
  25. package/harness/langchain/ping-pong-middleware.test.d.ts +2 -0
  26. package/harness/langchain/ping-pong-middleware.test.ts +69 -0
  27. package/harness/langchain/ping-pong-middleware.ts +204 -0
  28. package/harness/langchain/run.d.ts +15 -0
  29. package/harness/langchain/run.ts +15 -3
  30. package/harness/langchain/scoreboard.test.d.ts +13 -0
  31. package/harness/langchain/scoreboard.test.ts +23 -1
  32. package/harness/langchain/status-line.d.ts +91 -0
  33. package/harness/langchain/status-line.test.d.ts +8 -0
  34. package/harness/langchain/surge.d.ts +103 -0
  35. package/harness/langchain/surge.test.d.ts +10 -0
  36. package/harness/langchain/surge.test.ts +179 -0
  37. package/harness/langchain/surge.ts +188 -19
  38. package/harness/langchain/widget.d.ts +105 -0
  39. package/harness/langchain/widget.test.d.ts +8 -0
  40. package/harness/ssh-detach-interceptor.test.ts +263 -0
  41. package/harness/ssh-detach-interceptor.ts +438 -0
  42. package/index.ts +212 -47
  43. package/package.json +4 -2
  44. package/packages/a2a-adapter/package.json +1 -1
  45. package/packages/architecture-generator/package.json +1 -1
  46. package/packages/auth/package.json +1 -1
  47. package/packages/autonomous-refactor/package.json +1 -1
  48. package/packages/autonomous-runtime/package.json +1 -1
  49. package/packages/cache-strategy/package.json +1 -1
  50. package/packages/capability-registry/package.json +1 -1
  51. package/packages/checkpoint/package.json +1 -1
  52. package/packages/cli-plugin-sdk/package.json +1 -1
  53. package/packages/clipboard/package.json +1 -1
  54. package/packages/clipboard-plugin/package.json +1 -1
  55. package/packages/code-generation/package.json +1 -1
  56. package/packages/code-review/package.json +1 -1
  57. package/packages/codex-adapter/package.json +1 -1
  58. package/packages/config-capture/package.json +1 -1
  59. package/packages/context-compiler/package.json +1 -1
  60. package/packages/context-discovery/package.json +1 -1
  61. package/packages/context-manager/package.json +1 -1
  62. package/packages/cookie-sanitizer/package.json +1 -1
  63. package/packages/cost-optimizer/package.json +1 -1
  64. package/packages/dependency-analyzer/package.json +1 -1
  65. package/packages/django-plugin/package.json +1 -1
  66. package/packages/doc-generator/package.json +1 -1
  67. package/packages/evaluation-engine/package.json +1 -1
  68. package/packages/evaluation-runner/package.json +1 -1
  69. package/packages/event-bus/package.json +1 -1
  70. package/packages/event-store/package.json +1 -1
  71. package/packages/experience-replay/package.json +1 -1
  72. package/packages/feedback-collector/package.json +1 -1
  73. package/packages/file-copy-helper/package.json +1 -1
  74. package/packages/framework-detector/package.json +1 -1
  75. package/packages/framework-plugin-sdk/package.json +1 -1
  76. package/packages/frappe-plugin/package.json +1 -1
  77. package/packages/generic-web-plugin/package.json +1 -1
  78. package/packages/health-monitor/package.json +1 -1
  79. package/packages/intent-analyzer/dist/src/index.d.ts +2 -0
  80. package/packages/intent-analyzer/dist/src/index.d.ts.map +1 -1
  81. package/packages/intent-analyzer/dist/src/index.js +1 -0
  82. package/packages/intent-analyzer/dist/src/index.js.map +1 -1
  83. package/packages/intent-analyzer/dist/src/ping-pong.d.ts +56 -0
  84. package/packages/intent-analyzer/dist/src/ping-pong.d.ts.map +1 -0
  85. package/packages/intent-analyzer/dist/src/ping-pong.js +286 -0
  86. package/packages/intent-analyzer/dist/src/ping-pong.js.map +1 -0
  87. package/packages/intent-analyzer/package.json +1 -1
  88. package/packages/intent-analyzer/src/index.ts +13 -0
  89. package/packages/intent-analyzer/src/ping-pong.ts +387 -0
  90. package/packages/intent-analyzer/test/ping-pong.test.ts +96 -0
  91. package/packages/jev-judge/README.md +234 -0
  92. package/packages/jev-judge/dist/decision-engine.d.ts +100 -0
  93. package/packages/jev-judge/dist/decision-engine.d.ts.map +1 -0
  94. package/packages/jev-judge/dist/decision-engine.js +151 -0
  95. package/packages/jev-judge/dist/decision-engine.js.map +1 -0
  96. package/packages/jev-judge/dist/e2e-judge.d.ts +61 -0
  97. package/packages/jev-judge/dist/e2e-judge.d.ts.map +1 -0
  98. package/packages/jev-judge/dist/e2e-judge.js +268 -0
  99. package/packages/jev-judge/dist/e2e-judge.js.map +1 -0
  100. package/packages/jev-judge/dist/flaky-detector.d.ts +34 -0
  101. package/packages/jev-judge/dist/flaky-detector.d.ts.map +1 -0
  102. package/packages/jev-judge/dist/flaky-detector.js +154 -0
  103. package/packages/jev-judge/dist/flaky-detector.js.map +1 -0
  104. package/packages/jev-judge/dist/index.d.ts +77 -0
  105. package/packages/jev-judge/dist/index.d.ts.map +1 -0
  106. package/packages/jev-judge/dist/index.js +271 -0
  107. package/packages/jev-judge/dist/index.js.map +1 -0
  108. package/packages/jev-judge/dist/types.d.ts +97 -0
  109. package/packages/jev-judge/dist/types.d.ts.map +1 -0
  110. package/packages/jev-judge/dist/types.js +8 -0
  111. package/packages/jev-judge/dist/types.js.map +1 -0
  112. package/packages/jev-judge/package.json +46 -0
  113. package/packages/jev-judge/src/decision-engine.ts +231 -0
  114. package/packages/jev-judge/src/e2e-judge.ts +364 -0
  115. package/packages/jev-judge/src/flaky-detector.ts +190 -0
  116. package/packages/jev-judge/src/index.ts +345 -0
  117. package/packages/jev-judge/src/types.ts +131 -0
  118. package/packages/knowledge-graph/package.json +1 -1
  119. package/packages/knowledge-retrieval/package.json +1 -1
  120. package/packages/laravel-plugin/package.json +1 -1
  121. package/packages/learning-engine/package.json +1 -1
  122. package/packages/mcp-adapter/package.json +1 -1
  123. package/packages/memory-engine/package.json +1 -1
  124. package/packages/milestone-manager/package.json +1 -1
  125. package/packages/model-registry/package.json +1 -1
  126. package/packages/nextjs-plugin/package.json +1 -1
  127. package/packages/notification/package.json +1 -1
  128. package/packages/observability/package.json +1 -1
  129. package/packages/okf-indexer/package.json +1 -1
  130. package/packages/performance-optimizer/package.json +1 -1
  131. package/packages/privilege-broker/package.json +1 -1
  132. package/packages/project-analyzer/package.json +1 -1
  133. package/packages/project-bootstrap/package.json +1 -1
  134. package/packages/projection-engine/package.json +1 -1
  135. package/packages/prompt-compiler/package.json +1 -1
  136. package/packages/prompt-versioning/package.json +1 -1
  137. package/packages/provider-adapter-sdk/package.json +1 -1
  138. package/packages/provider-router/package.json +1 -1
  139. package/packages/provider-selector/package.json +1 -1
  140. package/packages/providers/package.json +1 -1
  141. package/packages/quota-manager/package.json +1 -1
  142. package/packages/rate-limiter/package.json +1 -1
  143. package/packages/react-vite-plugin/package.json +1 -1
  144. package/packages/release-manager/package.json +1 -1
  145. package/packages/requirement-compiler/package.json +1 -1
  146. package/packages/runtime/package.json +1 -1
  147. package/packages/scheduler/package.json +1 -1
  148. package/packages/scheduler-adapter/package.json +1 -1
  149. package/packages/session/package.json +1 -1
  150. package/packages/session-api/package.json +1 -1
  151. package/packages/session-export/package.json +1 -1
  152. package/packages/shared-context/package.json +1 -1
  153. package/packages/skill-mcp-client/package.json +1 -1
  154. package/packages/skill-registry/package.json +1 -1
  155. package/packages/sprint-planner/package.json +1 -1
  156. package/packages/subscription-engine/package.json +1 -1
  157. package/packages/task-compiler/package.json +1 -1
  158. package/packages/tencentdb-memory/package.json +1 -1
  159. package/packages/tencentdb-sync/package.json +1 -1
  160. package/packages/test-data-generator/package.json +1 -1
  161. package/packages/test-generator/package.json +1 -1
  162. package/packages/todo-bd-sync/README.md +71 -0
  163. package/packages/todo-bd-sync/package.json +1 -1
  164. package/packages/todo-bd-sync/src/extension.ts +16 -1
  165. package/packages/token-estimation/package.json +1 -1
  166. package/packages/token-optimizer/package.json +1 -1
  167. package/packages/trajectory/dist/src/approved-patterns.d.ts +51 -0
  168. package/packages/trajectory/dist/src/approved-patterns.d.ts.map +1 -0
  169. package/packages/trajectory/dist/src/approved-patterns.js +134 -0
  170. package/packages/trajectory/dist/src/approved-patterns.js.map +1 -0
  171. package/packages/trajectory/dist/src/index.d.ts +13 -0
  172. package/packages/trajectory/dist/src/index.d.ts.map +1 -0
  173. package/packages/trajectory/dist/src/index.js +12 -0
  174. package/packages/trajectory/dist/src/index.js.map +1 -0
  175. package/packages/trajectory/dist/src/store.d.ts +69 -0
  176. package/packages/trajectory/dist/src/store.d.ts.map +1 -0
  177. package/packages/trajectory/dist/src/store.js +253 -0
  178. package/packages/trajectory/dist/src/store.js.map +1 -0
  179. package/packages/trajectory/dist/src/types.d.ts +73 -0
  180. package/packages/trajectory/dist/src/types.d.ts.map +1 -0
  181. package/packages/trajectory/dist/src/types.js +9 -0
  182. package/packages/trajectory/dist/src/types.js.map +1 -0
  183. package/packages/trajectory/dist/test/store.test.d.ts +2 -0
  184. package/packages/trajectory/dist/test/store.test.d.ts.map +1 -0
  185. package/packages/trajectory/dist/test/store.test.js +228 -0
  186. package/packages/trajectory/dist/test/store.test.js.map +1 -0
  187. package/packages/trajectory/package.json +29 -0
  188. package/packages/trajectory/src/approved-patterns.ts +180 -0
  189. package/packages/trajectory/src/index.ts +26 -0
  190. package/packages/trajectory/src/store.ts +285 -0
  191. package/packages/trajectory/src/types.ts +84 -0
  192. package/packages/trajectory/test/store.test.d.ts +2 -0
  193. package/packages/trajectory/test/store.test.ts +250 -0
  194. package/packages/tui/package.json +1 -1
  195. package/packages/types/package.json +1 -1
  196. package/packages/workflow-events/package.json +1 -1
  197. package/packages/workspace-scanner/package.json +1 -1
  198. package/packages/worktree/package.json +1 -1
  199. package/packages/write-review/package.json +1 -1
  200. package/packages/write-review/src/blackboard.ts +24 -1
  201. package/packages/write-review/src/types.ts +13 -0
  202. package/skills/harness-runtime/SKILL.md +67 -4
@@ -0,0 +1,188 @@
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
+
30
+ /** Matches [role: modelFamily] directives, case-insensitive. */
31
+ const DIRECTIVE_RE = /\[(\w+)\s*:\s*(\w[\w-]*)\]/gi;
32
+
33
+ /** Map from directive family name → env prefix. */
34
+ const FAMILY_MAP: Record<string, string> = {
35
+ gpt: "PLANNER",
36
+ glm: "GLM",
37
+ minimax: "MINIMAX",
38
+ };
39
+
40
+ /** Env vars that exist for each prefix. */
41
+ const ENVVARS = {
42
+ PLANNER: ["PLANNER_MODEL", "PLANNER_BASE_URL", "PLANNER_API_KEY"],
43
+ GLM: ["GLM_MODEL", "GLM_BASE_URL", "GLM_API_KEY"],
44
+ MINIMAX: ["MINIMAX_MODEL", "MINIMAX_BASE_URL", "MINIMAX_API_KEY"],
45
+ };
46
+
47
+ /** Model options derived from a resolved env prefix. */
48
+ export interface ResolvedModel {
49
+ model: string;
50
+ baseURL: string;
51
+ apiKey: string;
52
+ }
53
+
54
+ /** Result of routing a request to models. */
55
+ export interface RouteResult {
56
+ planner: ResolvedModel;
57
+ reviewer: ResolvedModel; // master reviewer
58
+ coder: ResolvedModel;
59
+ /** Text of the request with directive comments stripped (safe for agent prompts). */
60
+ cleanRequest: string;
61
+ }
62
+
63
+ /**
64
+ * Resolve a model family to actual env-backed model options.
65
+ * family: "GLM" | "PLANNER" | "MINIMAX" (case-insensitive input)
66
+ */
67
+ export function resolveModel(family: string): ResolvedModel {
68
+ const key = FAMILY_MAP[family.toLowerCase()] ?? family.toUpperCase();
69
+ const env = process.env;
70
+
71
+ const modelKey = `${key}_MODEL`;
72
+ const baseURLKey = `${key}_BASE_URL`;
73
+ const apiKeyKey = `${key}_API_KEY`;
74
+
75
+ const model = env[modelKey];
76
+ const baseURL = env[baseURLKey];
77
+ const apiKey = env[apiKeyKey];
78
+
79
+ if (!apiKey) {
80
+ throw new Error(
81
+ `${apiKeyKey} is not set. Set it in .env (see .env.example).`,
82
+ );
83
+ }
84
+ if (!model) {
85
+ throw new Error(
86
+ `${modelKey} is not set. Set it in .env (e.g. ${modelKey}="GLM-5.2").`,
87
+ );
88
+ }
89
+ if (!baseURL) {
90
+ throw new Error(
91
+ `${baseURLKey} is not set. Set it in .env (e.g. ${baseURLKey}="https://api.z.ai/api/v1").`,
92
+ );
93
+ }
94
+
95
+ return { model, baseURL, apiKey };
96
+ }
97
+
98
+ /**
99
+ * Route a request prompt to the correct model configuration.
100
+ *
101
+ * Algorithm:
102
+ * 1. Extract [role: family] directives from the prompt
103
+ * 2. Resolve each family to actual env-backed model options
104
+ * 3. Apply defaults (planner→PLANNER, reviewer→GLM, coder→MINIMAX)
105
+ * 4. Strip directive comments from the request before passing to agents
106
+ */
107
+ export function routeRequest(request: string): RouteResult {
108
+ const directives = new Map<string, string>();
109
+ let clean = request;
110
+
111
+ // Extract and strip directives
112
+ let match: RegExpExecArray | null;
113
+ const re = new RegExp(DIRECTIVE_RE.source, "gi");
114
+ while ((match = re.exec(request)) !== null) {
115
+ const [, role, family] = match;
116
+ directives.set(role.toLowerCase(), family.toLowerCase());
117
+ }
118
+ // Remove directive lines/comments from the prompt
119
+ clean = request.replace(DIRECTIVE_RE, "").trim();
120
+
121
+ // Resolve each role
122
+ const plannerFamily = directives.get("planner") ?? "gpt";
123
+ const reviewerFamily = directives.get("reviewer") ?? "glm";
124
+ const coderFamily = directives.get("coder") ?? "minimax";
125
+
126
+ return {
127
+ planner: resolveModel(plannerFamily),
128
+ reviewer: resolveModel(reviewerFamily),
129
+ coder: resolveModel(coderFamily),
130
+ cleanRequest: clean,
131
+ };
132
+ }
133
+
134
+ /** Also match natural-language hints in the request. */
135
+ const NATURAL_HINTS: Array<[RegExp, string, string]> = [
136
+ // Reviewer hints
137
+ [/\bGLM\s+as\s+(master\s+)?reviewer\b/gi, "reviewer", "glm"],
138
+ [/\bGPT\s+as\s+(master\s+)?reviewer\b/gi, "reviewer", "gpt"],
139
+ [/\[reviewer:\s*GLM\]/gi, "reviewer", "glm"],
140
+ [/\[reviewer:\s*GPT\]/gi, "reviewer", "gpt"],
141
+ // Planner hints
142
+ [/\bGPT\s+as\s+planner\b/gi, "planner", "gpt"],
143
+ [/\bGLM\s+as\s+planner\b/gi, "planner", "glm"],
144
+ [/\[planner:\s*GLM\]/gi, "planner", "glm"],
145
+ [/\[planner:\s*GPT\]/gi, "planner", "gpt"],
146
+ // Coder hints
147
+ [/\bMiniMax\s+as\s+coder\b/gi, "coder", "minimax"],
148
+ [/\bGPT\s+as\s+coder\b/gi, "coder", "gpt"],
149
+ [/\[coder:\s*MiniMax\]/gi, "coder", "minimax"],
150
+ [/\[coder:\s*GPT\]/gi, "coder", "gpt"],
151
+ ];
152
+
153
+ /**
154
+ * Route a request, also scanning natural-language hints.
155
+ * Directives ([role: family]) take precedence over natural hints.
156
+ */
157
+ export function routeRequestSmart(request: string): RouteResult {
158
+ const directives = new Map<string, string>();
159
+ const hints = new Map<string, string>();
160
+ let clean = request;
161
+
162
+ // 1. Extract [role: family] directives
163
+ let match: RegExpExecArray | null;
164
+ const re = new RegExp(DIRECTIVE_RE.source, "gi");
165
+ while ((match = re.exec(request)) !== null) {
166
+ const [, role, family] = match;
167
+ directives.set(role.toLowerCase(), family.toLowerCase());
168
+ }
169
+ clean = request.replace(DIRECTIVE_RE, "").trim();
170
+
171
+ // 2. Scan for natural-language hints
172
+ for (const [pattern, role, family] of NATURAL_HINTS) {
173
+ if (!hints.has(role) && pattern.test(request)) {
174
+ hints.set(role, family);
175
+ }
176
+ }
177
+
178
+ // 3. Merge: directives override hints, hints fill gaps
179
+ const getRole = (role: string, def: string) =>
180
+ directives.get(role) ?? hints.get(role) ?? def;
181
+
182
+ return {
183
+ planner: resolveModel(getRole("planner", "gpt")),
184
+ reviewer: resolveModel(getRole("reviewer", "glm")),
185
+ coder: resolveModel(getRole("coder", "minimax")),
186
+ cleanRequest: clean,
187
+ };
188
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Model factories — all models fully env-driven, no hardcoded defaults.
3
+ *
4
+ * Agent roles (configurable via env):
5
+ * planner → GPT (PLANNER_API_KEY, PLANNER_MODEL, PLANNER_BASE_URL)
6
+ * reviewer → GLM (GLM_API_KEY, GLM_MODEL, GLM_BASE_URL)
7
+ * coder → MiniMax (MINIMAX_API_KEY, MINIMAX_MODEL, MINIMAX_BASE_URL)
8
+ *
9
+ * Wiki: wiki/multi-agent-langchain.md
10
+ */
11
+ import { ChatOpenAI } from "@langchain/openai";
12
+ export interface ModelOptions {
13
+ /** Override model id (e.g. "gpt-5.6", "GLM-5.2", "MiniMax-M2") */
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
+ * Pre-resolved model config from model-router.ts.
24
+ * Pass this to skip env-lookup and use the routed model directly.
25
+ */
26
+ export interface ResolvedModelOptions {
27
+ model: string;
28
+ baseURL: string;
29
+ apiKey: string;
30
+ /** Sampling temperature */
31
+ temperature?: number;
32
+ }
33
+ /**
34
+ * Planner model — GPT family.
35
+ *
36
+ * Can be called with:
37
+ * - ModelOptions (override fields, rest from env)
38
+ * - ResolvedModelOptions (fully resolved from model-router, no env lookup)
39
+ *
40
+ * Env vars (when using ModelOptions):
41
+ * PLANNER_API_KEY (required)
42
+ * PLANNER_MODEL (required)
43
+ * PLANNER_BASE_URL (required)
44
+ */
45
+ export declare function createPlannerModel(opts?: ModelOptions | ResolvedModelOptions): ChatOpenAI;
46
+ /**
47
+ * Reviewer model — GLM family (master reviewer per design).
48
+ *
49
+ * Can be called with:
50
+ * - ModelOptions (override fields, rest from env)
51
+ * - ResolvedModelOptions (fully resolved from model-router, no env lookup)
52
+ *
53
+ * Env vars (when using ModelOptions):
54
+ * GLM_API_KEY (required)
55
+ * GLM_MODEL (required)
56
+ * GLM_BASE_URL (required)
57
+ */
58
+ export declare function createReviewerModel(opts?: ModelOptions | ResolvedModelOptions): ChatOpenAI;
59
+ /**
60
+ * Coder model — MiniMax family.
61
+ *
62
+ * Can be called with:
63
+ * - ModelOptions (override fields, rest from env)
64
+ * - ResolvedModelOptions (fully resolved from model-router, no env lookup)
65
+ *
66
+ * Env vars (when using ModelOptions):
67
+ * MINIMAX_API_KEY (required)
68
+ * MINIMAX_MODEL (required)
69
+ * MINIMAX_BASE_URL (required)
70
+ */
71
+ export declare function createCoderModel(opts?: ModelOptions | ResolvedModelOptions): ChatOpenAI;
72
+ //# sourceMappingURL=models.d.ts.map
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Model factories — GPT (planner/reviewer) + MiniMax (coder).
2
+ * Model factories — all models fully env-driven, no hardcoded defaults.
3
3
  *
4
- * MiniMax exposes an OpenAI-compatible chat completions API, so we reuse
5
- * ChatOpenAI with a baseURL override. All values are env-configurable.
4
+ * Agent roles (configurable via env):
5
+ * planner → GPT (PLANNER_API_KEY, PLANNER_MODEL, PLANNER_BASE_URL)
6
+ * reviewer → GLM (GLM_API_KEY, GLM_MODEL, GLM_BASE_URL)
7
+ * coder → MiniMax (MINIMAX_API_KEY, MINIMAX_MODEL, MINIMAX_BASE_URL)
6
8
  *
7
9
  * Wiki: wiki/multi-agent-langchain.md
8
10
  */
@@ -10,7 +12,7 @@
10
12
  import { ChatOpenAI } from "@langchain/openai";
11
13
 
12
14
  export interface ModelOptions {
13
- /** Override model id (e.g. "gpt-4o", "MiniMax-M2.1") */
15
+ /** Override model id (e.g. "gpt-5.6", "GLM-5.2", "MiniMax-M2") */
14
16
  model?: string;
15
17
  /** Override API base URL */
16
18
  baseURL?: string;
@@ -20,76 +22,136 @@ export interface ModelOptions {
20
22
  temperature?: number;
21
23
  }
22
24
 
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
- };
25
+ /**
26
+ * Pre-resolved model config from model-router.ts.
27
+ * Pass this to skip env-lookup and use the routed model directly.
28
+ */
29
+ export interface ResolvedModelOptions {
30
+ model: string;
31
+ baseURL: string;
32
+ apiKey: string;
33
+ /** Sampling temperature */
34
+ temperature?: number;
35
+ }
36
+
37
+ // ─── Shared helpers ──────────────────────────────────────────────────────────
38
+
39
+ /** Build the `configuration.baseURL` override for ChatOpenAI. */
40
+ function baseURLConfig(baseURL: string): {
41
+ configuration: { baseURL: string };
42
+ } {
43
+ return { configuration: { baseURL } };
36
44
  }
37
45
 
38
- function minimaxDefaults(): {
46
+ /** Env-var tuple for a single model family. */
47
+ interface ModelEnv {
39
48
  model: string;
40
49
  baseURL: string;
41
50
  apiKey: string;
42
- } {
43
- const apiKey = process.env.MINIMAX_API_KEY ?? "";
51
+ }
52
+
53
+ /** Read env vars for a model family. Throws if the API key is absent. */
54
+ function readEnv(prefix: string, env: typeof process.env): ModelEnv {
55
+ const apiKey = env[`${prefix}_API_KEY`] ?? "";
44
56
  if (!apiKey) {
45
57
  throw new Error(
46
- "MINIMAX_API_KEY is not set. Add it to .env (see .env.example).",
58
+ `${prefix}_API_KEY is not set. Add it to .env (see .env.example).\n` +
59
+ ` Example: ${prefix}_MODEL="GLM-5.2" ${prefix}_BASE_URL="https://api.z.ai/api/v1"`,
47
60
  );
48
61
  }
49
62
  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
63
  apiKey,
64
+ model:
65
+ env[`${prefix}_MODEL`] ??
66
+ (() => {
67
+ throw new Error(
68
+ `${prefix}_MODEL is not set. Set it in .env (e.g. ${prefix}_MODEL="GLM-5.2").`,
69
+ );
70
+ })(),
71
+ baseURL:
72
+ env[`${prefix}_BASE_URL`] ??
73
+ (() => {
74
+ throw new Error(
75
+ `${prefix}_BASE_URL is not set. Set it in .env (e.g. ${prefix}_BASE_URL="https://api.openai.com/v1").`,
76
+ );
77
+ })(),
54
78
  };
55
79
  }
56
80
 
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
- }
81
+ // ─── Model factories ─────────────────────────────────────────────────────────
63
82
 
64
- /** GPT model — used for planning and code review. */
65
- export function createPlannerModel(opts: ModelOptions = {}): ChatOpenAI {
66
- const d = gptDefaults();
83
+ /**
84
+ * Planner model — GPT family.
85
+ *
86
+ * Can be called with:
87
+ * - ModelOptions (override fields, rest from env)
88
+ * - ResolvedModelOptions (fully resolved from model-router, no env lookup)
89
+ *
90
+ * Env vars (when using ModelOptions):
91
+ * PLANNER_API_KEY (required)
92
+ * PLANNER_MODEL (required)
93
+ * PLANNER_BASE_URL (required)
94
+ */
95
+ export function createPlannerModel(
96
+ opts: ModelOptions | ResolvedModelOptions = {},
97
+ ): ChatOpenAI {
98
+ const resolved = "baseURL" in opts ? opts : readEnv("PLANNER", process.env);
99
+ // resolved.baseURL is always a string (readEnv throws if absent); cast is safe
100
+ const baseURL = (opts.baseURL ?? resolved.baseURL) as string;
67
101
  return new ChatOpenAI({
68
- model: opts.model ?? d.model,
69
- apiKey: opts.apiKey ?? d.apiKey,
102
+ model: opts.model ?? resolved.model,
103
+ apiKey: opts.apiKey ?? resolved.apiKey,
70
104
  temperature: opts.temperature ?? 0.2,
71
- ...baseURLConfig(opts.baseURL ?? d.baseURL),
105
+ ...baseURLConfig(baseURL),
72
106
  });
73
107
  }
74
108
 
75
- /** Reviewer shares the GPT configuration (can be overridden). */
76
- export function createReviewerModel(opts: ModelOptions = {}): ChatOpenAI {
77
- const d = gptDefaults();
109
+ /**
110
+ * Reviewer model — GLM family (master reviewer per design).
111
+ *
112
+ * Can be called with:
113
+ * - ModelOptions (override fields, rest from env)
114
+ * - ResolvedModelOptions (fully resolved from model-router, no env lookup)
115
+ *
116
+ * Env vars (when using ModelOptions):
117
+ * GLM_API_KEY (required)
118
+ * GLM_MODEL (required)
119
+ * GLM_BASE_URL (required)
120
+ */
121
+ export function createReviewerModel(
122
+ opts: ModelOptions | ResolvedModelOptions = {},
123
+ ): ChatOpenAI {
124
+ const resolved = "baseURL" in opts ? opts : readEnv("GLM", process.env);
125
+ const baseURL = (opts.baseURL ?? resolved.baseURL) as string;
78
126
  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),
127
+ model: opts.model ?? resolved.model,
128
+ apiKey: opts.apiKey ?? resolved.apiKey,
129
+ temperature: opts.temperature ?? 0.1,
130
+ ...baseURLConfig(baseURL),
83
131
  });
84
132
  }
85
133
 
86
- /** MiniMax model — used for code generation (OpenAI-compatible endpoint). */
87
- export function createCoderModel(opts: ModelOptions = {}): ChatOpenAI {
88
- const d = minimaxDefaults();
134
+ /**
135
+ * Coder model — MiniMax family.
136
+ *
137
+ * Can be called with:
138
+ * - ModelOptions (override fields, rest from env)
139
+ * - ResolvedModelOptions (fully resolved from model-router, no env lookup)
140
+ *
141
+ * Env vars (when using ModelOptions):
142
+ * MINIMAX_API_KEY (required)
143
+ * MINIMAX_MODEL (required)
144
+ * MINIMAX_BASE_URL (required)
145
+ */
146
+ export function createCoderModel(
147
+ opts: ModelOptions | ResolvedModelOptions = {},
148
+ ): ChatOpenAI {
149
+ const resolved = "baseURL" in opts ? opts : readEnv("MINIMAX", process.env);
150
+ const baseURL = (opts.baseURL ?? resolved.baseURL) as string;
89
151
  return new ChatOpenAI({
90
- model: opts.model ?? d.model,
91
- apiKey: opts.apiKey ?? d.apiKey,
152
+ model: opts.model ?? resolved.model,
153
+ apiKey: opts.apiKey ?? resolved.apiKey,
92
154
  temperature: opts.temperature ?? 0.3,
93
- ...baseURLConfig(opts.baseURL ?? d.baseURL),
155
+ ...baseURLConfig(baseURL),
94
156
  });
95
157
  }
@@ -0,0 +1,28 @@
1
+ import { PingPongDecisionEngine, type PingPongDecisionOptions, type PingPongDecisionResult } from "../../packages/intent-analyzer/src/index.js";
2
+ export interface PingPongMiddlewareOptions extends PingPongDecisionOptions {
3
+ /** Shared directory watched by LoopDaemon. Defaults to /tmp/herdr-workspace. */
4
+ workspace?: string;
5
+ /** Repository root and blackboard owner. Defaults to process.cwd(). */
6
+ projectRoot?: string;
7
+ /** Agent or terminal identifier for audit records. */
8
+ actorId?: string;
9
+ /** Also enqueue suggest_ping_pong decisions. Default: false. */
10
+ enqueueSuggestions?: boolean;
11
+ /** Test hook for stable timestamps. */
12
+ now?: () => Date;
13
+ }
14
+ export interface PingPongMiddlewareResult {
15
+ decision: PingPongDecisionResult;
16
+ enqueued: boolean;
17
+ taskId?: string;
18
+ taskFile?: string;
19
+ auditFile: string;
20
+ blackboardFile: string;
21
+ }
22
+ export declare class PingPongMiddleware {
23
+ private readonly engine;
24
+ constructor(engine?: PingPongDecisionEngine);
25
+ submit(request: string, options?: PingPongMiddlewareOptions): PingPongMiddlewareResult;
26
+ }
27
+ export declare function submitPingPongIntent(request: string, options?: PingPongMiddlewareOptions): PingPongMiddlewareResult;
28
+ //# sourceMappingURL=ping-pong-middleware.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ping-pong-middleware.test.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { describe, it } from "node:test";
2
+ import { equal, ok } from "node:assert";
3
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { PingPongMiddleware } from "./ping-pong-middleware.js";
7
+
8
+ describe("PingPongMiddleware", () => {
9
+ it("records explicit PING-PONG decisions in blackboard and inbox", () => {
10
+ const projectRoot = mkdtempSync(join(tmpdir(), "ping-pong-project-"));
11
+ const workspace = mkdtempSync(join(tmpdir(), "ping-pong-workspace-"));
12
+ try {
13
+ const result = new PingPongMiddleware().submit(
14
+ "Good plan, then write down plan to wiki/ then I will ask Minimax to write code then you review",
15
+ {
16
+ projectRoot,
17
+ workspace,
18
+ actorId: "test-agent",
19
+ now: () => new Date("2026-09-10T00:00:00.000Z"),
20
+ },
21
+ );
22
+
23
+ equal(result.enqueued, true);
24
+ ok(result.taskFile);
25
+ ok(result.taskId?.endsWith(".md"));
26
+ const taskBody = readFileSync(result.taskFile!, "utf8");
27
+ ok(taskBody.includes("kind: ping-pong"));
28
+ ok(taskBody.includes("ask Minimax to write code"));
29
+
30
+ const blackboard = JSON.parse(
31
+ readFileSync(join(projectRoot, ".write-review", "status.json"), "utf8"),
32
+ );
33
+ equal(blackboard.mode, "ping_pong");
34
+ equal(blackboard.phase, "writing");
35
+ equal(blackboard.pingPong.decision, "run_ping_pong");
36
+ equal(blackboard.pingPong.taskId, result.taskId);
37
+
38
+ const audit = readFileSync(result.auditFile, "utf8");
39
+ ok(audit.includes("run_ping_pong"));
40
+ ok(audit.includes("test-agent"));
41
+ } finally {
42
+ rmSync(projectRoot, { recursive: true, force: true });
43
+ rmSync(workspace, { recursive: true, force: true });
44
+ }
45
+ });
46
+
47
+ it("records inline decisions without enqueueing", () => {
48
+ const projectRoot = mkdtempSync(join(tmpdir(), "ping-pong-project-"));
49
+ const workspace = mkdtempSync(join(tmpdir(), "ping-pong-workspace-"));
50
+ try {
51
+ const result = new PingPongMiddleware().submit(
52
+ "analyze why the loop is quiet, do not edit code",
53
+ { projectRoot, workspace, actorId: "test-agent" },
54
+ );
55
+
56
+ equal(result.enqueued, false);
57
+ equal(result.taskFile, undefined);
58
+ const blackboard = JSON.parse(
59
+ readFileSync(join(projectRoot, ".write-review", "status.json"), "utf8"),
60
+ );
61
+ equal(blackboard.mode, "ping_pong");
62
+ equal(blackboard.phase, "idle");
63
+ equal(blackboard.pingPong.decision, "handle_inline");
64
+ } finally {
65
+ rmSync(projectRoot, { recursive: true, force: true });
66
+ rmSync(workspace, { recursive: true, force: true });
67
+ }
68
+ });
69
+ });