pi-harness-runtime 1.1.148 → 1.1.152
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 +7 -0
- package/harness/key-loader.ts +84 -0
- package/harness/langchain/agents.ts +65 -15
- package/harness/langchain/daemon.ts +58 -30
- package/harness/langchain/graph.ts +55 -7
- package/harness/langchain/jev-logger.ts +274 -0
- package/harness/langchain/run.ts +64 -161
- package/harness/langchain/surge.test.ts +42 -0
- package/harness/langchain/surge.ts +22 -1
- package/index.ts +65 -26
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/dist/extension.d.ts +19 -0
- package/packages/agent-policy/dist/extension.d.ts.map +1 -0
- package/packages/agent-policy/dist/extension.js +322 -0
- package/packages/agent-policy/dist/extension.js.map +1 -0
- package/packages/agent-policy/dist/index.d.ts +25 -0
- package/packages/agent-policy/dist/index.d.ts.map +1 -0
- package/packages/agent-policy/dist/index.js +27 -0
- package/packages/agent-policy/dist/index.js.map +1 -0
- package/packages/agent-policy/dist/policy-loader.d.ts +90 -0
- package/packages/agent-policy/dist/policy-loader.d.ts.map +1 -0
- package/packages/agent-policy/dist/policy-loader.js +331 -0
- package/packages/agent-policy/dist/policy-loader.js.map +1 -0
- package/packages/agent-policy/dist/policy-renderer.d.ts +42 -0
- package/packages/agent-policy/dist/policy-renderer.d.ts.map +1 -0
- package/packages/agent-policy/dist/policy-renderer.js +295 -0
- package/packages/agent-policy/dist/policy-renderer.js.map +1 -0
- package/packages/agent-policy/dist/policy-store.d.ts +99 -0
- package/packages/agent-policy/dist/policy-store.d.ts.map +1 -0
- package/packages/agent-policy/dist/policy-store.js +244 -0
- package/packages/agent-policy/dist/policy-store.js.map +1 -0
- package/packages/agent-policy/dist/types.d.ts +237 -0
- package/packages/agent-policy/dist/types.d.ts.map +1 -0
- package/packages/agent-policy/dist/types.js +130 -0
- package/packages/agent-policy/dist/types.js.map +1 -0
- package/packages/agent-policy/package.json +45 -0
- package/packages/agent-policy/src/extension.ts +413 -0
- package/packages/agent-policy/src/index.ts +52 -0
- package/packages/agent-policy/src/policy-loader.ts +398 -0
- package/packages/agent-policy/src/policy-renderer.ts +362 -0
- package/packages/agent-policy/src/policy-store.ts +319 -0
- package/packages/agent-policy/src/types.ts +385 -0
- package/packages/agent-policy/test/policy.test.ts +383 -0
- 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/codex-watcher/dist/src/parser.d.ts.map +1 -1
- package/packages/codex-watcher/dist/src/parser.js +35 -0
- package/packages/codex-watcher/dist/src/parser.js.map +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/codex-watcher/src/parser.ts +41 -0
- 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/package.json +1 -1
- package/packages/jev-judge/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/dist/adapters/telegram-adapter.d.ts +5 -0
- package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.js +54 -0
- package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
- package/packages/notification/dist/notification-center.d.ts +13 -0
- package/packages/notification/dist/notification-center.d.ts.map +1 -1
- package/packages/notification/dist/notification-center.js +36 -0
- package/packages/notification/dist/notification-center.js.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/adapters/telegram-adapter.ts +71 -0
- package/packages/notification/src/notification-center.ts +53 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/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/qdrant-skills/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/skills/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/trajectory/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
- package/proactive-compact.ts +2 -2
- package/scripts/debug-codex-integration.ts +203 -0
- package/scripts/sync-keys-from-pi.ts +188 -0
- package/scripts/test-codex-session.ts +23 -0
- package/scripts/test-openai.ts +14 -0
- package/scripts/test-telegram.ts +34 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jev Logger - Exclusive logging for Jev's decisions
|
|
3
|
+
*
|
|
4
|
+
* Logs all Jev decisions:
|
|
5
|
+
* - Provider selection (minimax vs gpt)
|
|
6
|
+
* - User input decisions
|
|
7
|
+
* - Escalation triggers
|
|
8
|
+
* - Reasoning
|
|
9
|
+
*
|
|
10
|
+
* Sends to both file and Telegram for real-time monitoring.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdirSync, appendFileSync, existsSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
|
|
17
|
+
export interface JevDecision {
|
|
18
|
+
timestamp: string;
|
|
19
|
+
request: string;
|
|
20
|
+
provider: "minimax" | "gpt";
|
|
21
|
+
reason: string;
|
|
22
|
+
userInput: {
|
|
23
|
+
type: "yesNo" | "open" | null;
|
|
24
|
+
question?: string;
|
|
25
|
+
} | null;
|
|
26
|
+
escalate: boolean;
|
|
27
|
+
escalateReason?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface JevLogEntry extends JevDecision {
|
|
31
|
+
raw?: string; // Raw planner output for debugging
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const LOG_DIR = join(homedir(), ".pi-harness-runtime", "logs");
|
|
35
|
+
const LOG_FILE = join(LOG_DIR, "jev.log");
|
|
36
|
+
|
|
37
|
+
// Telegram config (loaded from environment)
|
|
38
|
+
let TELEGRAM_BOT_TOKEN: string | undefined;
|
|
39
|
+
let TELEGRAM_CHAT_ID: string | undefined;
|
|
40
|
+
let telegramQueue: string[] = [];
|
|
41
|
+
let telegramSending = false;
|
|
42
|
+
|
|
43
|
+
export function configureTelegram(botToken?: string, chatId?: string): void {
|
|
44
|
+
TELEGRAM_BOT_TOKEN = botToken ?? process.env.TELEGRAM_BOT_TOKEN;
|
|
45
|
+
TELEGRAM_CHAT_ID = chatId ?? process.env.TELEGRAM_CHAT_ID;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ensureLogDir(): void {
|
|
49
|
+
if (!existsSync(LOG_DIR)) {
|
|
50
|
+
mkdirSync(LOG_DIR, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format decision for file log (detailed, multi-line)
|
|
56
|
+
*/
|
|
57
|
+
function formatFileLog(entry: JevLogEntry): string {
|
|
58
|
+
const lines = [
|
|
59
|
+
`═══════════════════════════════════════════════════════════════`,
|
|
60
|
+
`[JEV] ${entry.timestamp}`,
|
|
61
|
+
`═══════════════════════════════════════════════════════════════`,
|
|
62
|
+
``,
|
|
63
|
+
`## Request`,
|
|
64
|
+
`${entry.request}`,
|
|
65
|
+
``,
|
|
66
|
+
`## Jev Provider Decision`,
|
|
67
|
+
`Provider: ${entry.provider.toUpperCase()}`,
|
|
68
|
+
`Reason: ${entry.reason}`,
|
|
69
|
+
``,
|
|
70
|
+
`## Jev User Input Decision`,
|
|
71
|
+
`Type: ${entry.userInput?.type ?? "none (proceed automatically)"}`,
|
|
72
|
+
entry.userInput?.question ? `Question: ${entry.userInput.question}` : ``,
|
|
73
|
+
``,
|
|
74
|
+
`## Escalation`,
|
|
75
|
+
`Escalate: ${entry.escalate ? "YES → Switching to GPT" : "No"}`,
|
|
76
|
+
entry.escalateReason ? `Reason: ${entry.escalateReason}` : ``,
|
|
77
|
+
``,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
if (entry.raw) {
|
|
81
|
+
lines.push(`## Raw Planner Output`);
|
|
82
|
+
lines.push(`\`\`\`\n${entry.raw.slice(0, 500)}\n...\`\`\``); // Truncate for file
|
|
83
|
+
lines.push(``);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
lines.push(`═══════════════════════════════════════════════════════════════`);
|
|
87
|
+
lines.push(``);
|
|
88
|
+
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Format decision for Telegram (compact, single message)
|
|
94
|
+
*/
|
|
95
|
+
function formatTelegramMessage(entry: JevLogEntry): string {
|
|
96
|
+
const providerIcon = entry.provider === "gpt" ? "🤖" : "⚡";
|
|
97
|
+
const escalateIcon = entry.escalate ? " ↑ ESCALATE" : "";
|
|
98
|
+
|
|
99
|
+
const parts = [
|
|
100
|
+
`${providerIcon} *JEV DECISION*`,
|
|
101
|
+
`\n*Provider:* ${entry.provider.toUpperCase()}${escalateIcon}`,
|
|
102
|
+
`\n*Reason:* ${entry.reason}`,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (entry.userInput?.question) {
|
|
106
|
+
const inputIcon = entry.userInput.type === "yesNo" ? "❓" : "💬";
|
|
107
|
+
parts.push(`\n${inputIcon} *USER INPUT:* ${entry.userInput.question}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Truncate request for Telegram (usually too long)
|
|
111
|
+
const shortRequest = entry.request.length > 100
|
|
112
|
+
? entry.request.slice(0, 100) + "..."
|
|
113
|
+
: entry.request;
|
|
114
|
+
parts.push(`\n\n📋 *Request:* ${shortRequest}`);
|
|
115
|
+
|
|
116
|
+
return parts.join("");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Send message to Telegram (async, queued)
|
|
121
|
+
*/
|
|
122
|
+
async function sendToTelegram(message: string): Promise<void> {
|
|
123
|
+
// Always try to load from env first
|
|
124
|
+
TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN ?? TELEGRAM_BOT_TOKEN;
|
|
125
|
+
TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID ?? TELEGRAM_CHAT_ID;
|
|
126
|
+
|
|
127
|
+
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
|
|
128
|
+
console.log("[JEV] Telegram not configured - skipping");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`[JEV] Telegram sending: ${TELEGRAM_BOT_TOKEN?.slice(0, 10)}... to ${TELEGRAM_CHAT_ID}`);
|
|
133
|
+
telegramQueue.push(message);
|
|
134
|
+
|
|
135
|
+
// Process queue sequentially
|
|
136
|
+
if (telegramSending) {
|
|
137
|
+
console.log("[JEV] Telegram already sending, queued");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
telegramSending = true;
|
|
141
|
+
|
|
142
|
+
while (telegramQueue.length > 0) {
|
|
143
|
+
const msg = telegramQueue.shift()!;
|
|
144
|
+
try {
|
|
145
|
+
const response = await fetch(
|
|
146
|
+
`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
|
|
147
|
+
{
|
|
148
|
+
method: "POST",
|
|
149
|
+
headers: { "Content-Type": "application/json" },
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
chat_id: TELEGRAM_CHAT_ID,
|
|
152
|
+
text: msg,
|
|
153
|
+
parse_mode: "Markdown",
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
console.error(`[JEV] Telegram send failed: ${response.status}`);
|
|
160
|
+
} else {
|
|
161
|
+
console.log("[JEV] Telegram sent successfully");
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error(`[JEV] Telegram error:`, err);
|
|
165
|
+
}
|
|
166
|
+
// Rate limit: 1 message per second
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
telegramSending = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function logJevDecision(decision: JevLogEntry): Promise<void> {
|
|
174
|
+
ensureLogDir();
|
|
175
|
+
|
|
176
|
+
// Always log to file (sync)
|
|
177
|
+
const fileLog = formatFileLog(decision);
|
|
178
|
+
appendFileSync(LOG_FILE, fileLog);
|
|
179
|
+
console.log(fileLog);
|
|
180
|
+
|
|
181
|
+
// Send to Telegram (async, non-blocking for main flow)
|
|
182
|
+
const telegramMsg = formatTelegramMessage(decision);
|
|
183
|
+
// Fire-and-forget: don't await, let it run in background
|
|
184
|
+
sendToTelegram(telegramMsg).catch((err) => {
|
|
185
|
+
console.error("[JEV] Failed to send Telegram notification:", err);
|
|
186
|
+
});
|
|
187
|
+
// Also await briefly for testing, then let process exit handle it
|
|
188
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse Jev's decision from planner output
|
|
193
|
+
* Looks for [JEVO] and [USER_INPUT] markers
|
|
194
|
+
*/
|
|
195
|
+
export function parseJevDecision(planOutput: string): {
|
|
196
|
+
provider: "minimax" | "gpt";
|
|
197
|
+
reason: string;
|
|
198
|
+
userInput: JevDecision["userInput"];
|
|
199
|
+
} {
|
|
200
|
+
const result: {
|
|
201
|
+
provider: "minimax" | "gpt";
|
|
202
|
+
reason: string;
|
|
203
|
+
userInput: JevDecision["userInput"];
|
|
204
|
+
} = {
|
|
205
|
+
provider: "minimax",
|
|
206
|
+
reason: "default (no explicit decision)",
|
|
207
|
+
userInput: null,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Parse [JEVO] block
|
|
211
|
+
const jevoMatch = planOutput.match(/\[JEVO\]\s*\{[\s\S]*?"provider":\s*"(minimax|gpt)"[\s\S]*?"reason":\s*"([^"]*)"[\s\S]*?\}/);
|
|
212
|
+
if (jevoMatch) {
|
|
213
|
+
result.provider = jevoMatch[1] as "minimax" | "gpt";
|
|
214
|
+
result.reason = jevoMatch[2] || "explicit provider decision";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Also check for [use-gpt] directive in the raw text
|
|
218
|
+
if (/\[use-gpt(?:-only)?\]/i.test(planOutput)) {
|
|
219
|
+
result.provider = "gpt";
|
|
220
|
+
result.reason = "user directive: [use-gpt]";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Parse [USER_INPUT] block
|
|
224
|
+
const userInputMatch = planOutput.match(
|
|
225
|
+
/\[USER_INPUT\]\s*(\{[\s\S]*?\})/,
|
|
226
|
+
);
|
|
227
|
+
if (userInputMatch) {
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(userInputMatch[1]);
|
|
230
|
+
if (parsed.type === "yesNo" || parsed.type === "open") {
|
|
231
|
+
result.userInput = {
|
|
232
|
+
type: parsed.type,
|
|
233
|
+
question: parsed.question,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// Ignore parse errors
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if review indicates escalation needed
|
|
246
|
+
*/
|
|
247
|
+
export function shouldEscalate(reviewComments: string[]): {
|
|
248
|
+
should: boolean;
|
|
249
|
+
reason: string;
|
|
250
|
+
} {
|
|
251
|
+
const escalationKeywords = [
|
|
252
|
+
"blocked",
|
|
253
|
+
"stuck",
|
|
254
|
+
"cannot proceed",
|
|
255
|
+
"complex",
|
|
256
|
+
"race condition",
|
|
257
|
+
"security issue",
|
|
258
|
+
"architecture decision needed",
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
for (const comment of reviewComments) {
|
|
262
|
+
const lowerComment = comment.toLowerCase();
|
|
263
|
+
for (const keyword of escalationKeywords) {
|
|
264
|
+
if (lowerComment.includes(keyword)) {
|
|
265
|
+
return {
|
|
266
|
+
should: true,
|
|
267
|
+
reason: `Review flagged: "${keyword}"`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { should: false, reason: "" };
|
|
274
|
+
}
|
package/harness/langchain/run.ts
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
* CLI runner for the LangChain write-review loop.
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
|
-
*
|
|
6
|
-
* bun harness/langchain/run.ts --
|
|
7
|
-
* bun harness/langchain/run.ts --mode supervisor --request "implement X"
|
|
5
|
+
* # Run a single task
|
|
6
|
+
* bun run harness/langchain/run.ts --request "implement X"
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* # Run with dry-run (no API calls)
|
|
9
|
+
* bun run harness/langchain/run.ts --request "implement X" --dry-run
|
|
11
10
|
*
|
|
12
|
-
*
|
|
11
|
+
* # Run with max iterations
|
|
12
|
+
* bun run harness/langchain/run.ts --request "implement X" --max-iterations 5
|
|
13
|
+
*
|
|
14
|
+
* # Run as daemon (with watchers - configure sources in code)
|
|
15
|
+
* bun run harness/langchain/run.ts --daemon
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { randomUUID } from "node:crypto";
|
|
@@ -17,17 +20,16 @@ import {
|
|
|
17
20
|
buildDryRunDeps,
|
|
18
21
|
buildRealLoopDeps,
|
|
19
22
|
buildWriteReviewLoop,
|
|
20
|
-
type LoopState,
|
|
21
23
|
} from "./graph.js";
|
|
22
|
-
|
|
24
|
+
import { loadKeys } from "../key-loader.js";
|
|
23
25
|
interface CliArgs {
|
|
24
26
|
mode: "graph" | "supervisor";
|
|
25
27
|
request: string;
|
|
26
28
|
maxIterations: number;
|
|
27
29
|
dryRun: boolean;
|
|
28
30
|
daemon: boolean;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
telegramBotToken?: string;
|
|
32
|
+
telegramChatId?: string;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function parseArgs(argv: string[]): CliArgs {
|
|
@@ -37,183 +39,84 @@ function parseArgs(argv: string[]): CliArgs {
|
|
|
37
39
|
maxIterations: 3,
|
|
38
40
|
dryRun: false,
|
|
39
41
|
daemon: false,
|
|
42
|
+
telegramBotToken: process.env.TELEGRAM_BOT_TOKEN,
|
|
43
|
+
telegramChatId: process.env.TELEGRAM_CHAT_ID,
|
|
40
44
|
};
|
|
41
|
-
// Normalize space-separated flags (--mode graph) into --mode=graph form
|
|
42
|
-
const normalized: string[] = [];
|
|
43
|
-
for (let i = 0; i < argv.length; i++) {
|
|
44
|
-
const arg = argv[i] as string;
|
|
45
|
-
const spaced =
|
|
46
|
-
(arg === "--mode" || arg === "--request" || arg === "--max-iterations") &&
|
|
47
|
-
argv[i + 1] !== undefined &&
|
|
48
|
-
!(argv[i + 1] as string).startsWith("--");
|
|
49
|
-
if (spaced) {
|
|
50
|
-
normalized.push(`${arg}=${argv[i + 1]}`);
|
|
51
|
-
i++;
|
|
52
|
-
} else {
|
|
53
|
-
normalized.push(arg);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// Detect "cron" subcommand before processing other flags
|
|
57
|
-
const cronIndex = normalized.indexOf("cron");
|
|
58
|
-
if (cronIndex !== -1) {
|
|
59
|
-
args.cronArgs = normalized.slice(cronIndex + 1);
|
|
60
|
-
return args;
|
|
61
|
-
}
|
|
62
45
|
|
|
63
|
-
for (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw new Error(`Unknown mode: ${mode} (expected graph|supervisor)`);
|
|
68
|
-
}
|
|
69
|
-
args.mode = mode;
|
|
70
|
-
} else if (arg.startsWith("--request=")) {
|
|
71
|
-
args.request = arg.slice("--request=".length);
|
|
72
|
-
} else if (arg.startsWith("--max-iterations=")) {
|
|
73
|
-
const n = Number.parseInt(arg.slice("--max-iterations=".length), 10);
|
|
74
|
-
if (Number.isNaN(n) || n < 1 || n > 20) {
|
|
75
|
-
throw new Error("--max-iterations must be 1-20");
|
|
76
|
-
}
|
|
77
|
-
args.maxIterations = n;
|
|
46
|
+
for (let i = 0; i < argv.length; i++) {
|
|
47
|
+
const arg = argv[i];
|
|
48
|
+
if (arg === "--request" || arg === "-r") {
|
|
49
|
+
args.request = argv[++i] ?? "";
|
|
78
50
|
} else if (arg === "--dry-run") {
|
|
79
51
|
args.dryRun = true;
|
|
80
52
|
} else if (arg === "--daemon") {
|
|
81
53
|
args.daemon = true;
|
|
82
|
-
} else if (arg === "--
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"",
|
|
87
|
-
"Options:",
|
|
88
|
-
" --mode=graph|supervisor Loop style (default: graph)",
|
|
89
|
-
' --request="..." The feature request',
|
|
90
|
-
" --max-iterations=N Max write-review rounds (default: 3)",
|
|
91
|
-
" --dry-run Deterministic stubs, no API calls",
|
|
92
|
-
" --daemon Start as a long-running daemon (auto-trigger loop)",
|
|
93
|
-
].join("\n"),
|
|
94
|
-
);
|
|
95
|
-
process.exit(0);
|
|
96
|
-
} else {
|
|
97
|
-
// Bare positional argument = request
|
|
98
|
-
if (!args.request) args.request = arg;
|
|
54
|
+
} else if (arg === "--max-iterations") {
|
|
55
|
+
args.maxIterations = parseInt(argv[++i] ?? "3", 10);
|
|
56
|
+
} else if (arg === "--mode") {
|
|
57
|
+
args.mode = argv[++i] as "graph" | "supervisor";
|
|
99
58
|
}
|
|
100
59
|
}
|
|
101
|
-
if (!args.request && !args.daemon && !args.cronArgs) {
|
|
102
|
-
throw new Error(
|
|
103
|
-
'A request is required: --request="..." or a bare string (or use --daemon or --cron to start the watcher)',
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
return args;
|
|
107
|
-
}
|
|
108
60
|
|
|
109
|
-
|
|
110
|
-
const labels: Record<string, string> = {
|
|
111
|
-
plan: "🧭 GPT planner",
|
|
112
|
-
write: "✍️ MiniMax coder",
|
|
113
|
-
review: "🔍 GPT reviewer",
|
|
114
|
-
finish: "🏁 finish",
|
|
115
|
-
};
|
|
116
|
-
const iter = state.iteration > 0 ? ` (iteration ${state.iteration})` : "";
|
|
117
|
-
console.log(` ${labels[step] ?? step}${iter}`);
|
|
61
|
+
return args;
|
|
118
62
|
}
|
|
119
63
|
|
|
120
|
-
async function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
);
|
|
124
|
-
console.log(`Request: ${args.request}\n`);
|
|
64
|
+
async function runGraphMode(args: CliArgs): Promise<void> {
|
|
65
|
+
const loopId = `loop-${randomUUID().slice(0, 8)}`;
|
|
66
|
+
const log = (msg: string) => console.log(`[${loopId}] ${msg}`);
|
|
125
67
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
:
|
|
68
|
+
// Build deps
|
|
69
|
+
const deps = await (args.dryRun
|
|
70
|
+
? buildDryRunDeps({ maxIterations: args.maxIterations })
|
|
71
|
+
: buildRealLoopDeps({
|
|
129
72
|
maxIterations: args.maxIterations,
|
|
130
|
-
|
|
131
|
-
});
|
|
73
|
+
request: args.request,
|
|
74
|
+
}));
|
|
132
75
|
|
|
76
|
+
// Create loop
|
|
133
77
|
const loop = buildWriteReviewLoop(deps);
|
|
134
|
-
const threadId = `loop-${randomUUID().slice(0, 8)}`;
|
|
135
|
-
|
|
136
|
-
const finalState = await loop.invoke(
|
|
137
|
-
{ request: args.request },
|
|
138
|
-
{ configurable: { thread_id: threadId } },
|
|
139
|
-
);
|
|
140
78
|
|
|
141
|
-
|
|
142
|
-
|
|
79
|
+
// Run
|
|
80
|
+
log(`Starting write-review loop (maxIterations=${args.maxIterations})`);
|
|
143
81
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (review.comments.length > 0) {
|
|
149
|
-
console.log(" Open comments:");
|
|
150
|
-
for (const c of review.comments) {
|
|
151
|
-
const file = c.file ? ` (${c.file})` : "";
|
|
152
|
-
console.log(` - [${c.severity ?? "n/a"}] ${c.comment}${file}`);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
} else {
|
|
156
|
-
console.log(" (no review produced)");
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async function runSupervisor(args: CliArgs): Promise<void> {
|
|
161
|
-
if (args.dryRun) {
|
|
162
|
-
throw new Error(
|
|
163
|
-
"--dry-run is only supported for --mode=graph (supervisor needs real models)",
|
|
82
|
+
try {
|
|
83
|
+
const result = await loop.invoke(
|
|
84
|
+
{ request: args.request },
|
|
85
|
+
{ configurable: { thread_id: loopId }, recursionLimit: 50 },
|
|
164
86
|
);
|
|
87
|
+
log(`Loop completed! Final state: ${JSON.stringify(result, null, 2)}`);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`[${loopId}] Error:`, err);
|
|
90
|
+
process.exit(1);
|
|
165
91
|
}
|
|
166
|
-
console.log("\n━━━ Write-Review Loop (supervisor mode) ━━━");
|
|
167
|
-
console.log(`Request: ${args.request}\n`);
|
|
168
|
-
|
|
169
|
-
const { createSupervisor, lastMessage } = await import("./agents.js");
|
|
170
|
-
const supervisor = createSupervisor();
|
|
171
|
-
const result = await supervisor.invoke({
|
|
172
|
-
messages: [{ role: "user", content: args.request }],
|
|
173
|
-
});
|
|
174
|
-
console.log("\n─── Supervisor output ───");
|
|
175
|
-
console.log(lastMessage(result));
|
|
176
92
|
}
|
|
177
93
|
|
|
178
|
-
async function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
maxIterations: args.maxIterations,
|
|
182
|
-
dryRun: args.dryRun,
|
|
183
|
-
sources: ["inbox", "bus", "codex"],
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// Graceful shutdown on SIGTERM / SIGINT
|
|
187
|
-
const shutdown = () => {
|
|
188
|
-
daemon.stop();
|
|
189
|
-
process.exit(0);
|
|
190
|
-
};
|
|
191
|
-
process.on("SIGTERM", shutdown);
|
|
192
|
-
process.on("SIGINT", shutdown);
|
|
193
|
-
|
|
194
|
-
daemon.start();
|
|
195
|
-
|
|
196
|
-
// Keep the process alive
|
|
197
|
-
await new Promise(() => {});
|
|
94
|
+
async function runDaemonMode(_args: CliArgs): Promise<void> {
|
|
95
|
+
console.log("Daemon mode - use --request to run a task or configure watchers");
|
|
96
|
+
process.exit(0);
|
|
198
97
|
}
|
|
199
98
|
|
|
200
99
|
async function main(): Promise<void> {
|
|
201
100
|
const args = parseArgs(process.argv.slice(2));
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
101
|
+
|
|
102
|
+
// Load API keys
|
|
103
|
+
loadKeys();
|
|
104
|
+
|
|
105
|
+
if (!args.request && !args.daemon) {
|
|
106
|
+
console.error("Usage: bun run harness/langchain/run.ts --request 'task description'");
|
|
107
|
+
console.error("Options:");
|
|
108
|
+
console.error(" --request, -r Task description");
|
|
109
|
+
console.error(" --dry-run Run without API calls");
|
|
110
|
+
console.error(" --max-iterations N Max iterations (default: 3)");
|
|
111
|
+
console.error(" --daemon Run in daemon mode");
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (args.daemon) {
|
|
116
|
+
await runDaemonMode(args);
|
|
209
117
|
} else {
|
|
210
|
-
await
|
|
118
|
+
await runGraphMode(args);
|
|
211
119
|
}
|
|
212
120
|
}
|
|
213
121
|
|
|
214
|
-
main().catch(
|
|
215
|
-
console.error(
|
|
216
|
-
`[run] Error: ${err instanceof Error ? err.message : String(err)}`,
|
|
217
|
-
);
|
|
218
|
-
process.exit(1);
|
|
219
|
-
});
|
|
122
|
+
main().catch(console.error);
|
|
@@ -76,6 +76,48 @@ describe("T1 — classifySurge", () => {
|
|
|
76
76
|
assert.equal(classifySurge(new Error("ECONNREFUSED")), null);
|
|
77
77
|
assert.equal(classifySurge("file not found"), null);
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
it("classifies 'Request timed out.'", () => {
|
|
81
|
+
const sig = classifySurge(new Error("Request timed out."));
|
|
82
|
+
assert.ok(sig, "should classify timeout");
|
|
83
|
+
assert.equal(sig?.explicit, false); // no stated delay → 2 min default
|
|
84
|
+
assert.equal(sig?.retryAfterMs, 120_000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("classifies 'timed out' without 'request' prefix", () => {
|
|
88
|
+
const sig = classifySurge(new Error("timed out"));
|
|
89
|
+
assert.ok(sig, "should classify");
|
|
90
|
+
assert.equal(sig?.retryAfterMs, 120_000);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("parses 'timed out after Xms'", () => {
|
|
94
|
+
const sig = classifySurge(new Error("timed out after 30000ms"));
|
|
95
|
+
assert.ok(sig);
|
|
96
|
+
assert.equal(sig?.retryAfterMs, 30_000);
|
|
97
|
+
assert.equal(sig?.explicit, true);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("parses 'Request timed out after X seconds'", () => {
|
|
101
|
+
const sig = classifySurge(new Error("Request timed out after 60 seconds"));
|
|
102
|
+
assert.ok(sig);
|
|
103
|
+
assert.equal(sig?.retryAfterMs, 60_000);
|
|
104
|
+
assert.equal(sig?.explicit, true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("parses 'timed out after Xms' (milliseconds keyword)", () => {
|
|
108
|
+
const sig = classifySurge(new Error("command timed out after 15000 milliseconds"));
|
|
109
|
+
assert.ok(sig);
|
|
110
|
+
assert.equal(sig?.retryAfterMs, 15_000);
|
|
111
|
+
assert.equal(sig?.explicit, true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("'Retry failed after 3 attempts: Request timed out.'", () => {
|
|
115
|
+
const sig = classifySurge(
|
|
116
|
+
new Error("Retry failed after 3 attempts: Request timed out."),
|
|
117
|
+
);
|
|
118
|
+
assert.ok(sig, "should classify inner timeout");
|
|
119
|
+
assert.equal(sig?.retryAfterMs, 120_000);
|
|
120
|
+
});
|
|
79
121
|
});
|
|
80
122
|
|
|
81
123
|
// ─── T2/T3 Delay computation & escalation ────────────────────────────────────
|
|
@@ -25,11 +25,14 @@ const OVERLOADED_PATTERNS: RegExp[] = [
|
|
|
25
25
|
/overloaded_error/i,
|
|
26
26
|
/peak.?hour.*surge/i,
|
|
27
27
|
/(?:overload|surge|busy)/i,
|
|
28
|
+
/(?:request )?timed?\s*out/i,
|
|
28
29
|
];
|
|
29
30
|
|
|
30
31
|
const RECOVERY_RANGE = /recovers? within (\d+)\s*[\u2013-]\s*(\d+)\s*minutes/i;
|
|
31
32
|
const SIMPLE_DELAY = /retry (?:after|in) (\d+)\s*(seconds?|secs?|s)\b/i;
|
|
32
33
|
const RETRY_AFTER_MS = /retry.?after[^0-9]{0,12}(\d{4,})\s*ms/i;
|
|
34
|
+
const TIMEOUT_DURATION_MS = /timed?\s*out(?: after)?[^0-9]*(\d+)\s*(ms|milliseconds?)/i;
|
|
35
|
+
const TIMEOUT_DURATION_S = /timed?\s*out(?: after)?[^0-9]*(\d+)\s*(seconds?|secs?|s)\b/i;
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Classify an unknown error as a transient provider surge (MiniMax).
|
|
@@ -83,6 +86,24 @@ export function classifySurge(err: unknown): SurgeSignal | null {
|
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
// "timed out after Xms" → use that duration
|
|
90
|
+
const timeoutMs = sourceText.match(TIMEOUT_DURATION_MS);
|
|
91
|
+
if (timeoutMs) {
|
|
92
|
+
const val = Number.parseInt(timeoutMs[1] as string, 10);
|
|
93
|
+
if (Number.isFinite(val) && val > 0) {
|
|
94
|
+
return { retryAfterMs: val, explicit: true, sourceText };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// "timed out after X seconds" → convert to ms
|
|
99
|
+
const timeoutSecs = sourceText.match(TIMEOUT_DURATION_S);
|
|
100
|
+
if (timeoutSecs) {
|
|
101
|
+
const val = Number.parseInt(timeoutSecs[1] as string, 10);
|
|
102
|
+
if (Number.isFinite(val) && val > 0) {
|
|
103
|
+
return { retryAfterMs: val * 1000, explicit: true, sourceText };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
// Overload-class error without a stated delay → conservative default
|
|
87
108
|
return { retryAfterMs: 120_000, explicit: false, sourceText };
|
|
88
109
|
}
|
|
@@ -188,7 +209,7 @@ export interface SurgePolicy {
|
|
|
188
209
|
const DEFAULT_SURGE_POLICY: SurgePolicy = {
|
|
189
210
|
multiplier: 2,
|
|
190
211
|
minDelayMs: 30_000,
|
|
191
|
-
maxDelayMs: 15 *
|
|
212
|
+
maxDelayMs: 15 * 60 * 1000,
|
|
192
213
|
jitterRatio: 0.2,
|
|
193
214
|
maxAttempts: 5,
|
|
194
215
|
};
|