agent-mp 0.4.6 → 0.4.7

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.
@@ -88,7 +88,7 @@ async function ask(prompt, rl, fi) {
88
88
  const tempRl = readline.createInterface({ input: process.stdin, output: process.stdout });
89
89
  return new Promise((resolve) => tempRl.question(prompt, (a) => { tempRl.close(); resolve(a); }));
90
90
  }
91
- function runCli(cmd, prompt, timeoutMs = 600000, envOverride) {
91
+ function runCli(cmd, prompt, timeoutMs = 600000, envOverride, onData) {
92
92
  // Si el comando es "qwen-direct", usar la API directa sin CLI
93
93
  if (cmd.startsWith('qwen-direct')) {
94
94
  const model = cmd.includes('-m') ? cmd.split('-m')[1]?.trim().split(/\s/)[0] : 'coder-model';
@@ -143,7 +143,7 @@ function runCli(cmd, prompt, timeoutMs = 600000, envOverride) {
143
143
  });
144
144
  let output = '';
145
145
  let stderr = '';
146
- child.stdout?.on('data', (d) => { output += d.toString(); });
146
+ child.stdout?.on('data', (d) => { const s = d.toString(); output += s; onData?.(s); });
147
147
  child.stderr?.on('data', (d) => { stderr += d.toString(); });
148
148
  child.on('close', (code) => { resolve({ output: output + stderr, exitCode: code || 0 }); });
149
149
  child.on('error', reject);
@@ -423,7 +423,10 @@ INSTRUCCIONES:
423
423
  const tryWithAutoRepair = async (cliName, model, currentCmd) => {
424
424
  const sp = this._startSpinner(`${cliName} ${model}`);
425
425
  try {
426
- const result = await runCli(currentCmd, rolePrompt);
426
+ const result = await runCli(currentCmd, rolePrompt, 600000, undefined, (chunk) => {
427
+ this._parseChunk(chunk).forEach(l => { if (l.trim())
428
+ sp.push(l); });
429
+ });
427
430
  if (result.exitCode !== 0) {
428
431
  sp.stop();
429
432
  const detail = result.output.trim().slice(0, 500);
@@ -487,7 +490,10 @@ INSTRUCCIONES:
487
490
  // Retry with new command
488
491
  const sp2 = this._startSpinner(`${cliName} ${model} (retry)`);
489
492
  try {
490
- const result = await runCli(newCmd, rolePrompt);
493
+ const result = await runCli(newCmd, rolePrompt, 600000, undefined, (chunk) => {
494
+ this._parseChunk(chunk).forEach(l => { if (l.trim())
495
+ sp2.push(l); });
496
+ });
491
497
  sp2.stop();
492
498
  if (result.exitCode !== 0) {
493
499
  const detail = result.output.trim().slice(0, 500);
@@ -585,10 +591,10 @@ INSTRUCCIONES:
585
591
  const preambles = {
586
592
  orchestrator: `ROL: ORCHESTRATOR — solo planifica, nunca implementa.
587
593
  REGLA: Tu unica salida debe ser el JSON del plan. Sin explicaciones, sin texto extra.`,
588
- implementor: `ROL: IMPLEMENTOR — solo ejecuta los steps del plan usando tus herramientas.
589
- REGLA: Usa tus herramientas (read_file, write_file, run_shell_command, etc.) para ejecutar cada step.
590
- REGLA: NO tomes decisiones de arquitectura. NO modifiques el plan.
591
- REGLA: Al terminar, responde SOLO con el JSON de progreso indicado.`,
594
+ implementor: `ROL: IMPLEMENTOR — genera el contenido completo de los archivos indicados.
595
+ REGLA: Para cada archivo, usa el bloque === FILE: ruta === ... === END FILE === exactamente.
596
+ REGLA: NO tomes decisiones de arquitectura. Sigue los steps del plan.
597
+ REGLA: Genera el contenido COMPLETO de cada archivo, no fragmentos ni pseudocodigo.`,
592
598
  reviewer: `ROL: REVIEWER — solo valida, nunca modifica codigo.
593
599
  REGLA: Usa tus herramientas (read_file, grep_search, etc.) para leer los archivos y verificar criterios.
594
600
  REGLA: NO modifiques archivos. NO implementes correcciones.
@@ -739,41 +745,73 @@ INSTRUCCIONES:
739
745
  if (await fileExists(structurePath)) {
740
746
  structureRules = await readFile(structurePath);
741
747
  }
742
- // Build steps list implementor gets ONLY steps and structure rules (no acceptance_criteria, no deliberation)
748
+ // Read architecture context for reference
749
+ const context = await this.buildOrchestratorContext();
750
+ let archContext = '';
751
+ const archPath = path.join(this.projectDir, '.agent', 'context', 'architecture.md');
752
+ if (await fileExists(archPath)) {
753
+ archContext = (await readFile(archPath)).slice(0, 3000);
754
+ }
755
+ const allTargetFiles = [...new Set(plan.steps.flatMap((s) => s.files || []))];
743
756
  const stepsText = plan.steps.map((s) => `Step ${s.num}: ${s.description}${s.files?.length ? `\n Archivos: ${s.files.join(', ')}` : ''}`).join('\n');
757
+ // The engine writes files itself — ask the LLM to generate content in parseable blocks.
758
+ // This avoids depending on CLI tool-use capabilities which are unavailable in API mode.
744
759
  const prompt = `TAREA: ${plan.description}
745
760
  DIRECTORIO_TRABAJO: ${this.projectDir}
746
761
 
747
- STEPS A EJECUTAR EN ORDEN:
762
+ STEPS A IMPLEMENTAR EN ORDEN:
748
763
  ${stepsText}
749
764
 
765
+ ${allTargetFiles.length ? `ARCHIVOS A CREAR/MODIFICAR:\n${allTargetFiles.map(f => `- ${f}`).join('\n')}` : ''}
766
+
767
+ ${context ? `CONTEXTO DEL PROYECTO:\n${context.slice(0, 2000)}\n` : ''}
768
+ ${archContext ? `ARQUITECTURA EXISTENTE:\n${archContext}\n` : ''}
750
769
  ${structureRules ? `REGLAS DE ESTRUCTURA:\n${structureRules}\n` : ''}
751
- INSTRUCCIONES:
752
- 1. Ejecuta cada step EN ORDEN usando tus herramientas (read_file, write_file, run_shell_command, etc.)
753
- 2. Para cada step: lee lo necesario, luego crea/modifica los archivos indicados.
754
- 3. NO tomes decisiones de arquitectura fuera del plan.
755
- 4. Al terminar todos los steps, responde SOLO con este JSON:
756
770
 
757
- {"status":"completed","steps_done":[1,2,3],"notes":"resumen breve de lo que se hizo"}`;
771
+ INSTRUCCIONES CRITICAS:
772
+ 1. Para CADA archivo que debes crear/modificar, usa este formato EXACTO:
773
+
774
+ === FILE: ruta/relativa/del/archivo ===
775
+ contenido completo del archivo aqui
776
+ === END FILE ===
777
+
778
+ 2. Las rutas son RELATIVAS al DIRECTORIO_TRABAJO.
779
+ 3. Genera TODOS los archivos necesarios — uno por bloque FILE.
780
+ 4. Incluye el contenido completo de cada archivo (no fragmentos).
781
+ 5. Si un step modifica un archivo existente, incluye el archivo completo con los cambios.
782
+ 6. NO incluyas explicaciones fuera de los bloques FILE.`;
758
783
  const res = await this.runWithFallback('implementor', prompt, 'Implementacion');
759
- // Extract text from streaming CLI output (qwen/claude return event arrays)
760
784
  const text = extractCliText(res);
761
- let progress;
762
- try {
763
- const json = extractJson(text);
764
- progress = (json.progress || json);
785
+ // Parse === FILE: path === ... === END FILE === blocks written by the LLM
786
+ const fileBlocks = [];
787
+ const fileBlockRegex = /^=== FILE: (.+?) ===\n([\s\S]*?)\n=== END FILE ===/gm;
788
+ let match;
789
+ while ((match = fileBlockRegex.exec(text)) !== null) {
790
+ fileBlocks.push({ relPath: match[1].trim(), content: match[2] });
765
791
  }
766
- catch {
767
- // CLI executed the task but didn't return structured JSON — build synthetic progress
768
- log.warn('Implementor did not return JSON progress building from plan');
769
- const now = new Date().toISOString();
770
- progress = {
771
- task_id: taskId,
772
- created_at: now,
773
- status: 'completed',
774
- steps: plan.steps.map((s) => ({ ...s, status: 'completed', completed_at: now })),
775
- };
792
+ const now = new Date().toISOString();
793
+ if (fileBlocks.length > 0) {
794
+ for (const { relPath, content } of fileBlocks) {
795
+ const absPath = path.join(this.projectDir, relPath);
796
+ await fs.mkdir(path.dirname(absPath), { recursive: true });
797
+ await writeFile(absPath, content);
798
+ log.ok(` Creado: ${relPath}`);
799
+ }
800
+ log.ok(`${fileBlocks.length} archivo(s) implementado(s)`);
801
+ }
802
+ else {
803
+ log.warn('Implementor did not return FILE blocks — no files written');
776
804
  }
805
+ const progress = {
806
+ task_id: taskId,
807
+ created_at: now,
808
+ status: fileBlocks.length > 0 ? 'completed' : 'failed',
809
+ steps: plan.steps.map((s) => ({
810
+ ...s,
811
+ status: fileBlocks.length > 0 ? 'completed' : 'failed',
812
+ completed_at: now,
813
+ })),
814
+ };
777
815
  await writeJson(path.join(taskDir, 'progress.json'), progress);
778
816
  log.ok('progress.json updated');
779
817
  return progress;
@@ -11,16 +11,12 @@ export declare function qwenAuthStatus(): Promise<{
11
11
  export declare function fetchQwenModels(): Promise<string[]>;
12
12
  export declare function getQwenAccessToken(): Promise<string | null>;
13
13
  /**
14
- * Call Qwen by spawning the `qwen` CLI with piped stdin.
15
- * The qwen CLI manages its own token refresh and uses the correct API format.
16
- * Falls back to direct HTTP call if the qwen CLI is not available.
14
+ * Call Qwen REST API directly using OAuth credentials stored locally.
15
+ * No dependency on any qwen CLI binary.
17
16
  */
18
17
  export declare function callQwenAPI(prompt: string, model?: string, onData?: (chunk: string) => void): Promise<string>;
19
18
  /**
20
- * Call Qwen API using credentials from a specific file path (for role binaries).
21
- * The role binary CLI (e.g. agent-explorer) manages its own qwen auth via the
22
- * shared ~/.qwen/oauth_creds.json — we spawn it with piped stdin so it runs
23
- * in non-interactive mode without TTY issues.
24
- * Falls back to direct HTTP if the role binary is not found.
19
+ * Call Qwen API using credentials stored at a specific path (for role binaries).
20
+ * Calls the Qwen REST API directly no dependency on any qwen CLI binary.
25
21
  */
26
22
  export declare function callQwenAPIFromCreds(prompt: string, model: string, credsPath: string, onData?: (chunk: string) => void): Promise<string>;
@@ -1,25 +1,6 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import * as path from 'path';
3
3
  import * as crypto from 'crypto';
4
- import { spawn } from 'child_process';
5
- /** Async alternative to spawnSync — keeps the event loop free so UI can update. */
6
- function spawnAsync(bin, args, input, timeout, onData) {
7
- return new Promise((resolve, reject) => {
8
- const child = spawn(bin, args, { stdio: ['pipe', 'pipe', 'pipe'] });
9
- let stdout = '';
10
- child.stdout?.on('data', (d) => {
11
- const s = d.toString();
12
- stdout += s;
13
- onData?.(s);
14
- });
15
- child.stderr?.on('data', (d) => { stdout += d.toString(); });
16
- const timer = setTimeout(() => { child.kill(); reject(new Error('qwen timeout')); }, timeout);
17
- child.on('close', (code) => { clearTimeout(timer); resolve({ stdout, status: code ?? 0 }); });
18
- child.on('error', (err) => { clearTimeout(timer); reject(err); });
19
- child.stdin?.write(input, 'utf-8');
20
- child.stdin?.end();
21
- });
22
- }
23
4
  import open from 'open';
24
5
  import { AGENT_HOME } from './config.js';
25
6
  const QWEN_OAUTH_BASE_URL = 'https://chat.qwen.ai';
@@ -267,8 +248,9 @@ export async function getQwenAccessToken() {
267
248
  const token = await loadToken();
268
249
  return token?.accessToken || null;
269
250
  }
270
- async function callQwenAPIWithToken(token, prompt, model) {
251
+ async function callQwenAPIWithToken(token, prompt, model, onData) {
271
252
  const baseUrl = 'https://chat.qwen.ai/api/v1';
253
+ const useStream = !!onData;
272
254
  const response = await fetch(`${baseUrl}/chat/completions`, {
273
255
  method: 'POST',
274
256
  headers: {
@@ -277,46 +259,65 @@ async function callQwenAPIWithToken(token, prompt, model) {
277
259
  },
278
260
  body: JSON.stringify({
279
261
  model: model || 'coder-model',
280
- messages: [
281
- { role: 'user', content: prompt },
282
- ],
262
+ messages: [{ role: 'user', content: prompt }],
263
+ stream: useStream,
283
264
  }),
284
265
  });
285
266
  if (!response.ok) {
286
267
  const errorText = await response.text();
287
- if (response.status === 401) {
268
+ if (response.status === 401)
288
269
  throw new Error(`QWEN_AUTH_EXPIRED: ${errorText}`);
289
- }
290
270
  throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
291
271
  }
292
- const data = await response.json();
293
- return data.choices?.[0]?.message?.content || '';
272
+ if (!useStream) {
273
+ const data = await response.json();
274
+ return data.choices?.[0]?.message?.content || '';
275
+ }
276
+ // Streaming — parse SSE chunks
277
+ const reader = response.body?.getReader();
278
+ if (!reader)
279
+ throw new Error('No response body for streaming');
280
+ const decoder = new TextDecoder();
281
+ let fullText = '';
282
+ let buffer = '';
283
+ while (true) {
284
+ const { done, value } = await reader.read();
285
+ if (done)
286
+ break;
287
+ buffer += decoder.decode(value, { stream: true });
288
+ const lines = buffer.split('\n');
289
+ buffer = lines.pop() || '';
290
+ for (const line of lines) {
291
+ const trimmed = line.trim();
292
+ if (!trimmed.startsWith('data: '))
293
+ continue;
294
+ const data = trimmed.slice(6);
295
+ if (data === '[DONE]')
296
+ continue;
297
+ try {
298
+ const parsed = JSON.parse(data);
299
+ const delta = parsed.choices?.[0]?.delta?.content || '';
300
+ if (delta) {
301
+ fullText += delta;
302
+ onData(delta);
303
+ }
304
+ }
305
+ catch { /* skip malformed chunks */ }
306
+ }
307
+ }
308
+ return fullText;
294
309
  }
295
310
  /**
296
- * Call Qwen by spawning the `qwen` CLI with piped stdin.
297
- * The qwen CLI manages its own token refresh and uses the correct API format.
298
- * Falls back to direct HTTP call if the qwen CLI is not available.
311
+ * Call Qwen REST API directly using OAuth credentials stored locally.
312
+ * No dependency on any qwen CLI binary.
299
313
  */
300
314
  export async function callQwenAPI(prompt, model = 'coder-model', onData) {
301
- // Try using the qwen CLI subprocess first — it handles auth/refresh/format automatically
302
- const qwenBin = process.env.QWEN_BIN || 'qwen';
303
- try {
304
- const result = await spawnAsync(qwenBin, [], prompt, 300000, onData);
305
- if (result.status === 0 && result.stdout.trim()) {
306
- return result.stdout.trim();
307
- }
308
- // qwen not available or failed — fall through to direct API
309
- }
310
- catch {
311
- // qwen not installed — fall through
312
- }
313
- // Fallback: direct API call (requires valid token in AGENT_HOME)
314
315
  let token = await loadToken();
315
316
  if (!token) {
316
317
  throw new Error('QWEN_AUTH_EXPIRED: No hay token de Qwen. Ejecutá --login primero.');
317
318
  }
318
319
  try {
319
- return await callQwenAPIWithToken(token, prompt, model);
320
+ return await callQwenAPIWithToken(token, prompt, model, onData);
320
321
  }
321
322
  catch (err) {
322
323
  if (!err.message?.startsWith('QWEN_AUTH_EXPIRED'))
@@ -325,34 +326,18 @@ export async function callQwenAPI(prompt, model = 'coder-model', onData) {
325
326
  const refreshed = await doRefreshToken(token.refreshToken);
326
327
  if (refreshed) {
327
328
  await saveToken(refreshed);
328
- return callQwenAPIWithToken(refreshed, prompt, model);
329
+ return callQwenAPIWithToken(refreshed, prompt, model, onData);
329
330
  }
330
331
  }
331
332
  throw new Error('QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: agent-mp --login');
332
333
  }
333
334
  }
334
335
  /**
335
- * Call Qwen API using credentials from a specific file path (for role binaries).
336
- * The role binary CLI (e.g. agent-explorer) manages its own qwen auth via the
337
- * shared ~/.qwen/oauth_creds.json — we spawn it with piped stdin so it runs
338
- * in non-interactive mode without TTY issues.
339
- * Falls back to direct HTTP if the role binary is not found.
336
+ * Call Qwen API using credentials stored at a specific path (for role binaries).
337
+ * Calls the Qwen REST API directly no dependency on any qwen CLI binary.
340
338
  */
341
339
  export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
342
- // Derive the role binary name from the creds path (e.g. ~/.agent-explorer/ → agent-explorer)
343
340
  const cliName = path.basename(path.dirname(credsPath)).replace(/^\./, '');
344
- // Try spawning the qwen CLI with piped stdin (async — keeps event loop free)
345
- const qwenBin = process.env.QWEN_BIN || 'qwen';
346
- try {
347
- const result = await spawnAsync(qwenBin, [], prompt, 300000, onData);
348
- if (result.status === 0 && result.stdout.trim()) {
349
- return result.stdout.trim();
350
- }
351
- }
352
- catch {
353
- // qwen not available
354
- }
355
- // Fallback: direct HTTP with stored creds
356
341
  let raw;
357
342
  try {
358
343
  raw = JSON.parse(await fs.readFile(credsPath, 'utf-8'));
@@ -370,7 +355,6 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
370
355
  if (!token.accessToken) {
371
356
  throw new Error(`Invalid credentials at ${credsPath}. Run: ${cliName} --login`);
372
357
  }
373
- // Refresh proactivo: si vence en menos de 2 minutos (o ya venció)
374
358
  const TWO_MIN = 2 * 60 * 1000;
375
359
  if (token.expiresAt - Date.now() < TWO_MIN && token.refreshToken) {
376
360
  const refreshed = await doRefreshToken(token.refreshToken);
@@ -380,17 +364,16 @@ export async function callQwenAPIFromCreds(prompt, model, credsPath, onData) {
380
364
  }
381
365
  }
382
366
  try {
383
- return await callQwenAPIWithToken(token, prompt, model);
367
+ return await callQwenAPIWithToken(token, prompt, model, onData);
384
368
  }
385
369
  catch (err) {
386
370
  if (!err.message?.startsWith('QWEN_AUTH_EXPIRED'))
387
371
  throw err;
388
- // 401 server-side — intentar refresh aunque expiresAt no haya vencido
389
372
  if (token.refreshToken) {
390
373
  const refreshed = await doRefreshToken(token.refreshToken);
391
374
  if (refreshed) {
392
375
  await fs.writeFile(credsPath, JSON.stringify(refreshed, null, 2), 'utf-8');
393
- return callQwenAPIWithToken(refreshed, prompt, model);
376
+ return callQwenAPIWithToken(refreshed, prompt, model, onData);
394
377
  }
395
378
  }
396
379
  throw new Error(`QWEN_AUTH_EXPIRED: Sesión expirada. Ejecutá: ${cliName} --login`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-mp",
3
- "version": "0.4.6",
4
- "description": "Deterministic multi-agent CLI orchestrator \u2014 plan, code, review",
3
+ "version": "0.4.7",
4
+ "description": "Deterministic multi-agent CLI orchestrator plan, code, review",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "files": [
@@ -41,4 +41,4 @@
41
41
  "engines": {
42
42
  "node": ">=18.0.0"
43
43
  }
44
- }
44
+ }