@dv.nghiem/flowdeck 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.
package/dist/index.js CHANGED
@@ -1236,6 +1236,136 @@ function extractText2(parts) {
1236
1236
  return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
1237
1237
  `);
1238
1238
  }
1239
+ async function runWithStreaming(client, childId, agentName, fullPrompt, toolsConfig, directory, abort, onTitle) {
1240
+ const sseResult = await client.event.subscribe({ query: { directory } });
1241
+ const stream = sseResult.stream;
1242
+ const asyncRes = await client.session.promptAsync({
1243
+ path: { id: childId },
1244
+ query: { directory },
1245
+ body: {
1246
+ agent: agentName,
1247
+ tools: toolsConfig,
1248
+ parts: [{ type: "text", text: fullPrompt }]
1249
+ }
1250
+ });
1251
+ if (asyncRes.error) {
1252
+ return {
1253
+ output: "",
1254
+ error: `promptAsync failed: ${JSON.stringify(asyncRes.error)}`
1255
+ };
1256
+ }
1257
+ let streamedText = "";
1258
+ let currentTool = "";
1259
+ onTitle(`⏳ ${agentName} — starting…`);
1260
+ try {
1261
+ for await (const raw of stream) {
1262
+ if (abort.aborted)
1263
+ break;
1264
+ const event = typeof raw === "object" && raw !== null ? Object.values(raw)[0] ?? raw : raw;
1265
+ if (!event || typeof event !== "object")
1266
+ continue;
1267
+ const sid = event.properties?.sessionID;
1268
+ if (sid && sid !== childId)
1269
+ continue;
1270
+ switch (event.type) {
1271
+ case "session.next.step.started": {
1272
+ const model = event.properties?.model?.id ?? "";
1273
+ onTitle(`\uD83E\uDD14 ${agentName} — thinking${model ? ` (${model})` : ""}…`);
1274
+ break;
1275
+ }
1276
+ case "session.next.text.delta": {
1277
+ const delta = event.properties?.delta ?? "";
1278
+ streamedText += delta;
1279
+ const preview = streamedText.slice(-80).replace(/\n/g, " ").trim();
1280
+ onTitle(`✍️ ${agentName} — ${preview}`);
1281
+ break;
1282
+ }
1283
+ case "session.next.text.ended": {
1284
+ const text = event.properties?.text ?? streamedText;
1285
+ streamedText = text;
1286
+ break;
1287
+ }
1288
+ case "session.next.reasoning.delta": {
1289
+ const delta = event.properties?.delta ?? "";
1290
+ const preview = delta.slice(0, 60).replace(/\n/g, " ").trim();
1291
+ onTitle(`\uD83D\uDCAD ${agentName} — ${preview}`);
1292
+ break;
1293
+ }
1294
+ case "session.next.tool.called": {
1295
+ currentTool = event.properties?.tool ?? "tool";
1296
+ onTitle(`\uD83D\uDD27 ${agentName} → ${currentTool}…`);
1297
+ break;
1298
+ }
1299
+ case "session.next.tool.progress": {
1300
+ const content = event.properties?.content ?? [];
1301
+ const progressText = content.filter((c) => c.type === "text").map((c) => c.text).join(" ").slice(0, 80).replace(/\n/g, " ").trim();
1302
+ if (progressText) {
1303
+ onTitle(`\uD83D\uDD27 ${agentName} → ${currentTool}: ${progressText}`);
1304
+ }
1305
+ break;
1306
+ }
1307
+ case "session.next.tool.success": {
1308
+ onTitle(`✅ ${agentName} → ${currentTool} done`);
1309
+ currentTool = "";
1310
+ break;
1311
+ }
1312
+ case "session.next.tool.failed": {
1313
+ onTitle(`❌ ${agentName} → ${currentTool} failed`);
1314
+ currentTool = "";
1315
+ break;
1316
+ }
1317
+ case "session.next.retried": {
1318
+ onTitle(`↻ ${agentName} — retrying…`);
1319
+ break;
1320
+ }
1321
+ case "session.next.step.ended": {
1322
+ const cost = event.properties?.cost ?? 0;
1323
+ const finish = event.properties?.finish ?? "";
1324
+ if (cost > 0) {
1325
+ onTitle(`\uD83D\uDCCA ${agentName} — step done ($${cost.toFixed(4)}) [${finish}]`);
1326
+ } else {
1327
+ onTitle(`\uD83D\uDCCA ${agentName} — step done [${finish}]`);
1328
+ }
1329
+ break;
1330
+ }
1331
+ case "session.error": {
1332
+ const msg = event.properties?.error?.message ?? JSON.stringify(event.properties?.error);
1333
+ return { output: streamedText, error: `Session error: ${msg}` };
1334
+ }
1335
+ case "session.idle": {
1336
+ onTitle(`✓ ${agentName} — complete`);
1337
+ goto_done:
1338
+ break goto_done;
1339
+ }
1340
+ }
1341
+ if (event.type === "session.idle")
1342
+ break;
1343
+ }
1344
+ } catch (err) {
1345
+ if (!abort.aborted) {
1346
+ onTitle(`⚠️ ${agentName} — stream closed (${err?.message ?? err})`);
1347
+ }
1348
+ }
1349
+ if (streamedText) {
1350
+ return { output: streamedText };
1351
+ }
1352
+ try {
1353
+ const msgsRes = await client.session.messages({
1354
+ path: { id: childId },
1355
+ query: { directory }
1356
+ });
1357
+ const messages = msgsRes.data ?? [];
1358
+ for (let i = messages.length - 1;i >= 0; i--) {
1359
+ const msg = messages[i];
1360
+ if (msg.role === "assistant") {
1361
+ const text = extractText2(msg.parts ?? []);
1362
+ if (text)
1363
+ return { output: text };
1364
+ }
1365
+ }
1366
+ } catch {}
1367
+ return { output: "" };
1368
+ }
1239
1369
  function createDelegateTool(client) {
1240
1370
  return tool4({
1241
1371
  description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
@@ -1250,14 +1380,14 @@ function createDelegateTool(client) {
1250
1380
  workflow_id: tool4.schema.string().optional(),
1251
1381
  stage: tool4.schema.string().optional()
1252
1382
  },
1253
- async execute(args, context) {
1383
+ async execute(args, execContext) {
1254
1384
  const startTime = Date.now();
1255
1385
  const taskType = normalizeTaskType(args.task_type, args.agent);
1256
1386
  const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
1257
1387
  const maxRetries = Math.max(0, Math.floor(retryAttempts));
1258
1388
  let agentModel = "";
1259
1389
  try {
1260
- const cfg = loadFlowDeckConfig(context.directory);
1390
+ const cfg = loadFlowDeckConfig(execContext.directory);
1261
1391
  agentModel = cfg.agents?.[args.agent]?.model ?? "";
1262
1392
  } catch {}
1263
1393
  const metricsWorkflowId = args.workflow_id ?? "";
@@ -1271,16 +1401,16 @@ ${args.prompt}` : args.prompt;
1271
1401
  let stateVersion = 0;
1272
1402
  let indexVersion = 0;
1273
1403
  if (safe_to_cache) {
1274
- const index = readCodebaseIndex(context.directory);
1275
- const sp = statePath(context.directory);
1404
+ const index = readCodebaseIndex(execContext.directory);
1405
+ const sp = statePath(execContext.directory);
1276
1406
  const rawState = existsSync10(sp) ? readFileSync10(sp, "utf-8") : "";
1277
1407
  const state = rawState ? parseState(rawState) : {};
1278
1408
  stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
1279
1409
  indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
1280
- const cached = getCached(context.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, true);
1410
+ const cached = getCached(execContext.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, true);
1281
1411
  if (cached !== null) {
1282
1412
  if (metricsWorkflowId) {
1283
- recordCacheHit(context.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
1413
+ recordCacheHit(execContext.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
1284
1414
  }
1285
1415
  return JSON.stringify({
1286
1416
  agent: args.agent,
@@ -1295,8 +1425,8 @@ ${args.prompt}` : args.prompt;
1295
1425
  }
1296
1426
  }
1297
1427
  const createRes = await client.session.create({
1298
- body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
1299
- query: { directory: context.directory }
1428
+ body: { parentID: execContext.sessionID, title: `${args.agent}-delegate` },
1429
+ query: { directory: execContext.directory }
1300
1430
  });
1301
1431
  if (createRes.error || !createRes.data?.id) {
1302
1432
  return JSON.stringify({
@@ -1307,84 +1437,61 @@ ${args.prompt}` : args.prompt;
1307
1437
  });
1308
1438
  }
1309
1439
  const childId = createRes.data.id;
1310
- context.abort.addEventListener("abort", () => {
1440
+ execContext.abort.addEventListener("abort", () => {
1311
1441
  client.session.abort({
1312
1442
  path: { id: childId },
1313
- query: { directory: context.directory }
1443
+ query: { directory: execContext.directory }
1314
1444
  }).catch(() => {});
1315
1445
  });
1316
- const fullPromptForSession = args.context ? `${args.context}
1317
-
1318
- ---
1319
-
1320
- ${args.prompt}` : args.prompt;
1321
- let promptRes = null;
1446
+ let lastOutput = "";
1447
+ let lastError;
1322
1448
  let retriesUsed = 0;
1323
1449
  for (let attempt = 0;attempt <= maxRetries; attempt++) {
1324
1450
  const attemptStart = Date.now();
1325
- promptRes = await client.session.prompt({
1326
- path: { id: childId },
1327
- body: {
1328
- agent: args.agent,
1329
- parts: [{ type: "text", text: fullPromptForSession }],
1330
- tools: { question: false }
1331
- },
1332
- query: { directory: context.directory }
1333
- });
1334
- if (!shouldRetry(promptRes) || attempt === maxRetries)
1451
+ if (attempt > 0) {
1452
+ execContext.metadata({ title: `↻ ${args.agent} retry ${attempt}/${maxRetries}…` });
1453
+ }
1454
+ const result = await runWithStreaming(client, childId, args.agent, fullPrompt, { question: false }, execContext.directory, execContext.abort, (title) => execContext.metadata({ title }));
1455
+ lastOutput = result.output;
1456
+ lastError = result.error;
1457
+ const shouldRetryAttempt = !!(lastError || !lastOutput.trim());
1458
+ if (!shouldRetryAttempt || attempt === maxRetries)
1335
1459
  break;
1336
1460
  if (metricsWorkflowId) {
1337
- const retryInputTokens = estimateTokens(fullPromptForSession);
1461
+ const retryInputTokens = estimateTokens(fullPrompt);
1338
1462
  const retryCostUsd = agentModel ? estimateCostUSD(agentModel, retryInputTokens, 0) : undefined;
1339
- recordRetryCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
1463
+ recordRetryCall(execContext.directory, metricsWorkflowId, metricsStage, fullPrompt, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
1340
1464
  }
1341
1465
  retriesUsed++;
1342
1466
  }
1343
- if (!promptRes || promptRes.error) {
1344
- const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
1345
- recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
1346
- return JSON.stringify({
1347
- agent: args.agent,
1348
- session_id: childId,
1349
- success: false,
1350
- error: errMsg,
1351
- task_type: taskType,
1352
- model: "",
1353
- retries_used: retriesUsed,
1354
- duration_ms: Date.now() - startTime
1355
- });
1356
- }
1357
- const info = promptRes.data?.info;
1358
- if (info?.error) {
1359
- const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
1360
- recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
1467
+ if (lastError && !lastOutput.trim()) {
1468
+ recordRun(execContext.directory, args.agent, "", taskType, false, Date.now() - startTime);
1361
1469
  return JSON.stringify({
1362
1470
  agent: args.agent,
1363
1471
  session_id: childId,
1364
1472
  success: false,
1365
- error: errMsg,
1473
+ error: lastError,
1366
1474
  task_type: taskType,
1367
1475
  model: "",
1368
1476
  retries_used: retriesUsed,
1369
1477
  duration_ms: Date.now() - startTime
1370
1478
  });
1371
1479
  }
1372
- const output = extractText2(promptRes.data?.parts ?? []);
1373
- recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
1480
+ recordRun(execContext.directory, args.agent, "", taskType, true, Date.now() - startTime);
1374
1481
  if (metricsWorkflowId) {
1375
- const inputTokens = estimateTokens(fullPromptForSession);
1376
- const outputTokens = estimateTokens(output);
1482
+ const inputTokens = estimateTokens(fullPrompt);
1483
+ const outputTokens = estimateTokens(lastOutput);
1377
1484
  const costUsd = agentModel ? estimateCostUSD(agentModel, inputTokens, outputTokens) : undefined;
1378
- recordModelCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, output, args.agent, Date.now() - startTime, agentModel, costUsd);
1485
+ recordModelCall(execContext.directory, metricsWorkflowId, metricsStage, fullPrompt, lastOutput, args.agent, Date.now() - startTime, agentModel, costUsd);
1379
1486
  }
1380
- if (safe_to_cache && output) {
1381
- setCached(context.directory, args.agent, fullPromptForSession, args.context ?? "", stateVersion, indexVersion, output, true, args.cache_ttl_ms);
1487
+ if (safe_to_cache && lastOutput) {
1488
+ setCached(execContext.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, lastOutput, true, args.cache_ttl_ms);
1382
1489
  }
1383
1490
  return JSON.stringify({
1384
1491
  agent: args.agent,
1385
1492
  session_id: childId,
1386
1493
  success: true,
1387
- output: output || "(no text output)",
1494
+ output: lastOutput || "(no text output)",
1388
1495
  task_type: taskType,
1389
1496
  model: "",
1390
1497
  retries_used: retriesUsed,
@@ -1 +1 @@
1
- {"version":3,"file":"delegate.d.ts","sourceRoot":"","sources":["../../src/tools/delegate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAkBtD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CAqPzE"}
1
+ {"version":3,"file":"delegate.d.ts","sourceRoot":"","sources":["../../src/tools/delegate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAgOtD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,CA6NzE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dv.nghiem/flowdeck",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "FlowDeck — structured planning and execution workflows for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",