@poncho-ai/cli 0.13.0 → 0.14.1
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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +42 -0
- package/dist/{chunk-CUCEDHME.js → chunk-AIEVSNGF.js} +1994 -467
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-VZBOYJYS.js → run-interactive-ink-7ULE5JJI.js} +151 -118
- package/package.json +4 -4
- package/src/api-docs.ts +674 -0
- package/src/index.ts +632 -229
- package/src/init-onboarding.ts +14 -1
- package/src/run-interactive-ink.ts +171 -147
- package/src/web-ui.ts +760 -244
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
resolveStateConfig,
|
|
25
25
|
type CronJobConfig,
|
|
26
26
|
type PonchoConfig,
|
|
27
|
+
type Conversation,
|
|
27
28
|
type ConversationStore,
|
|
28
29
|
type UploadStore,
|
|
29
30
|
} from "@poncho-ai/harness";
|
|
@@ -31,6 +32,7 @@ import type { AgentEvent, FileInput, Message, RunInput } from "@poncho-ai/sdk";
|
|
|
31
32
|
import { getTextContent } from "@poncho-ai/sdk";
|
|
32
33
|
import {
|
|
33
34
|
AgentBridge,
|
|
35
|
+
ResendAdapter,
|
|
34
36
|
SlackAdapter,
|
|
35
37
|
type AgentRunner,
|
|
36
38
|
type MessagingAdapter,
|
|
@@ -53,6 +55,7 @@ import {
|
|
|
53
55
|
setCookie,
|
|
54
56
|
verifyPassphrase,
|
|
55
57
|
} from "./web-ui.js";
|
|
58
|
+
import { buildOpenApiSpec, renderApiDocsHtml } from "./api-docs.js";
|
|
56
59
|
import { createInterface } from "node:readline/promises";
|
|
57
60
|
import {
|
|
58
61
|
runInitOnboarding,
|
|
@@ -374,7 +377,7 @@ cp .env.example .env
|
|
|
374
377
|
poncho dev
|
|
375
378
|
\`\`\`
|
|
376
379
|
|
|
377
|
-
Open \`http://localhost:3000\` for the web UI.
|
|
380
|
+
Open \`http://localhost:3000\` for the web UI, or \`http://localhost:3000/api/docs\` for interactive API documentation.
|
|
378
381
|
|
|
379
382
|
On your first interactive session, the agent introduces its configurable capabilities.
|
|
380
383
|
While a response is streaming, you can stop it:
|
|
@@ -496,7 +499,7 @@ Core files:
|
|
|
496
499
|
|
|
497
500
|
- \`AGENT.md\`: behavior, model selection, runtime guidance
|
|
498
501
|
- \`poncho.config.js\`: runtime config (storage, auth, telemetry, MCP, tools)
|
|
499
|
-
- \`.env\`: secrets and environment variables
|
|
502
|
+
- \`.env\`: secrets and environment variables (loaded before the harness starts, so \`process.env\` is available in skill scripts)
|
|
500
503
|
|
|
501
504
|
Example \`poncho.config.js\`:
|
|
502
505
|
|
|
@@ -522,18 +525,20 @@ export default {
|
|
|
522
525
|
auth: { type: "bearer", tokenEnv: "GITHUB_TOKEN" },
|
|
523
526
|
},
|
|
524
527
|
],
|
|
528
|
+
// Tool access: true (available), false (disabled), 'approval' (requires human approval)
|
|
525
529
|
tools: {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
read_file: true,
|
|
529
|
-
write_file: true, // still gated by environment/policy
|
|
530
|
-
},
|
|
530
|
+
write_file: true, // gated by environment for writes
|
|
531
|
+
send_email: 'approval', // requires human approval
|
|
531
532
|
byEnvironment: {
|
|
532
533
|
production: {
|
|
533
|
-
|
|
534
|
+
write_file: false,
|
|
535
|
+
},
|
|
536
|
+
development: {
|
|
537
|
+
send_email: true, // skip approval in dev
|
|
534
538
|
},
|
|
535
539
|
},
|
|
536
540
|
},
|
|
541
|
+
// webUi: false, // Disable built-in UI for API-only deployments
|
|
537
542
|
};
|
|
538
543
|
\`\`\`
|
|
539
544
|
|
|
@@ -592,6 +597,26 @@ Connect your agent to Slack so it responds to @mentions:
|
|
|
592
597
|
messaging: [{ platform: 'slack' }]
|
|
593
598
|
\`\`\`
|
|
594
599
|
|
|
600
|
+
## Messaging (Email via Resend)
|
|
601
|
+
|
|
602
|
+
Connect your agent to email so users can interact by sending emails:
|
|
603
|
+
|
|
604
|
+
1. Set up a domain and enable Inbound at [resend.com](https://resend.com)
|
|
605
|
+
2. Create a webhook for \`email.received\` pointing to \`https://<your-url>/api/messaging/resend\`
|
|
606
|
+
3. Install the Resend SDK: \`npm install resend\`
|
|
607
|
+
4. Set env vars:
|
|
608
|
+
\`\`\`
|
|
609
|
+
RESEND_API_KEY=re_...
|
|
610
|
+
RESEND_WEBHOOK_SECRET=whsec_...
|
|
611
|
+
RESEND_FROM=Agent <agent@yourdomain.com>
|
|
612
|
+
\`\`\`
|
|
613
|
+
5. Add to \`poncho.config.js\`:
|
|
614
|
+
\`\`\`javascript
|
|
615
|
+
messaging: [{ platform: 'resend' }]
|
|
616
|
+
\`\`\`
|
|
617
|
+
|
|
618
|
+
For full control over outbound emails, use **tool mode** (\`mode: 'tool'\`) — the agent gets a \`send_email\` tool instead of auto-replying. See the repo README for details.
|
|
619
|
+
|
|
595
620
|
## Deployment
|
|
596
621
|
|
|
597
622
|
\`\`\`bash
|
|
@@ -1290,16 +1315,6 @@ export const createRequestHandler = async (options?: {
|
|
|
1290
1315
|
runId: string | null;
|
|
1291
1316
|
};
|
|
1292
1317
|
const activeConversationRuns = new Map<string, ActiveConversationRun>();
|
|
1293
|
-
type PendingApproval = {
|
|
1294
|
-
ownerId: string;
|
|
1295
|
-
runId: string;
|
|
1296
|
-
conversationId: string | null;
|
|
1297
|
-
tool: string;
|
|
1298
|
-
input: Record<string, unknown>;
|
|
1299
|
-
resolve: (approved: boolean) => void;
|
|
1300
|
-
};
|
|
1301
|
-
const pendingApprovals = new Map<string, PendingApproval>();
|
|
1302
|
-
|
|
1303
1318
|
// Per-conversation event streaming: buffer events and allow SSE subscribers
|
|
1304
1319
|
type ConversationEventStream = {
|
|
1305
1320
|
buffer: AgentEvent[];
|
|
@@ -1339,55 +1354,19 @@ export const createRequestHandler = async (options?: {
|
|
|
1339
1354
|
setTimeout(() => conversationEventStreams.delete(conversationId), 30_000);
|
|
1340
1355
|
}
|
|
1341
1356
|
};
|
|
1342
|
-
const persistConversationPendingApprovals = async (conversationId: string): Promise<void> => {
|
|
1343
|
-
const conversation = await conversationStore.get(conversationId);
|
|
1344
|
-
if (!conversation) {
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
conversation.pendingApprovals = Array.from(pendingApprovals.entries())
|
|
1348
|
-
.filter(
|
|
1349
|
-
([, pending]) =>
|
|
1350
|
-
pending.ownerId === conversation.ownerId && pending.conversationId === conversationId,
|
|
1351
|
-
)
|
|
1352
|
-
.map(([approvalId, pending]) => ({
|
|
1353
|
-
approvalId,
|
|
1354
|
-
runId: pending.runId,
|
|
1355
|
-
tool: pending.tool,
|
|
1356
|
-
input: pending.input,
|
|
1357
|
-
}));
|
|
1358
|
-
await conversationStore.update(conversation);
|
|
1359
|
-
};
|
|
1360
1357
|
const clearPendingApprovalsForConversation = async (conversationId: string): Promise<void> => {
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
pending.resolve(false);
|
|
1358
|
+
const conversation = await conversationStore.get(conversationId);
|
|
1359
|
+
if (!conversation) return;
|
|
1360
|
+
if (Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0) {
|
|
1361
|
+
conversation.pendingApprovals = [];
|
|
1362
|
+
await conversationStore.update(conversation);
|
|
1367
1363
|
}
|
|
1368
|
-
await persistConversationPendingApprovals(conversationId);
|
|
1369
1364
|
};
|
|
1370
1365
|
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
1371
1366
|
const harness = new AgentHarness({
|
|
1372
1367
|
workingDir,
|
|
1373
1368
|
environment: resolveHarnessEnvironment(),
|
|
1374
1369
|
uploadStore,
|
|
1375
|
-
approvalHandler: async (request) =>
|
|
1376
|
-
new Promise<boolean>((resolveApproval) => {
|
|
1377
|
-
const ownerIdForRun = runOwners.get(request.runId) ?? "local-owner";
|
|
1378
|
-
const conversationIdForRun = runConversations.get(request.runId) ?? null;
|
|
1379
|
-
pendingApprovals.set(request.approvalId, {
|
|
1380
|
-
ownerId: ownerIdForRun,
|
|
1381
|
-
runId: request.runId,
|
|
1382
|
-
conversationId: conversationIdForRun,
|
|
1383
|
-
tool: request.tool,
|
|
1384
|
-
input: request.input,
|
|
1385
|
-
resolve: resolveApproval,
|
|
1386
|
-
});
|
|
1387
|
-
if (conversationIdForRun) {
|
|
1388
|
-
void persistConversationPendingApprovals(conversationIdForRun);
|
|
1389
|
-
}
|
|
1390
|
-
}),
|
|
1391
1370
|
});
|
|
1392
1371
|
await harness.initialize();
|
|
1393
1372
|
const telemetry = new TelemetryEmitter(config?.telemetry);
|
|
@@ -1396,6 +1375,186 @@ export const createRequestHandler = async (options?: {
|
|
|
1396
1375
|
workingDir,
|
|
1397
1376
|
agentId: identity.id,
|
|
1398
1377
|
});
|
|
1378
|
+
// ---------------------------------------------------------------------------
|
|
1379
|
+
// Resume a run from a persisted checkpoint after approval.
|
|
1380
|
+
// Processes events the same way the interactive/messaging runners do.
|
|
1381
|
+
// ---------------------------------------------------------------------------
|
|
1382
|
+
const resumeRunFromCheckpoint = async (
|
|
1383
|
+
conversationId: string,
|
|
1384
|
+
conversation: Conversation,
|
|
1385
|
+
checkpoint: NonNullable<Conversation["pendingApprovals"]>[number],
|
|
1386
|
+
toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }>,
|
|
1387
|
+
): Promise<void> => {
|
|
1388
|
+
const abortController = new AbortController();
|
|
1389
|
+
activeConversationRuns.set(conversationId, {
|
|
1390
|
+
ownerId: conversation.ownerId,
|
|
1391
|
+
abortController,
|
|
1392
|
+
runId: null,
|
|
1393
|
+
});
|
|
1394
|
+
let latestRunId = conversation.runtimeRunId ?? "";
|
|
1395
|
+
let assistantResponse = "";
|
|
1396
|
+
const toolTimeline: string[] = [];
|
|
1397
|
+
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
1398
|
+
let currentText = "";
|
|
1399
|
+
let currentTools: string[] = [];
|
|
1400
|
+
let checkpointedRun = false;
|
|
1401
|
+
|
|
1402
|
+
const baseMessages = checkpoint.baseMessageCount != null
|
|
1403
|
+
? conversation.messages.slice(0, checkpoint.baseMessageCount)
|
|
1404
|
+
: [];
|
|
1405
|
+
const fullCheckpointMessages = [...baseMessages, ...checkpoint.checkpointMessages!];
|
|
1406
|
+
|
|
1407
|
+
try {
|
|
1408
|
+
for await (const event of harness.continueFromToolResult({
|
|
1409
|
+
messages: fullCheckpointMessages,
|
|
1410
|
+
toolResults,
|
|
1411
|
+
conversationId,
|
|
1412
|
+
abortSignal: abortController.signal,
|
|
1413
|
+
})) {
|
|
1414
|
+
if (event.type === "run:started") {
|
|
1415
|
+
latestRunId = event.runId;
|
|
1416
|
+
runOwners.set(event.runId, conversation.ownerId);
|
|
1417
|
+
runConversations.set(event.runId, conversationId);
|
|
1418
|
+
const active = activeConversationRuns.get(conversationId);
|
|
1419
|
+
if (active && active.abortController === abortController) {
|
|
1420
|
+
active.runId = event.runId;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (event.type === "model:chunk") {
|
|
1424
|
+
if (currentTools.length > 0) {
|
|
1425
|
+
sections.push({ type: "tools", content: currentTools });
|
|
1426
|
+
currentTools = [];
|
|
1427
|
+
}
|
|
1428
|
+
assistantResponse += event.content;
|
|
1429
|
+
currentText += event.content;
|
|
1430
|
+
}
|
|
1431
|
+
if (event.type === "tool:started") {
|
|
1432
|
+
if (currentText.length > 0) {
|
|
1433
|
+
sections.push({ type: "text", content: currentText });
|
|
1434
|
+
currentText = "";
|
|
1435
|
+
}
|
|
1436
|
+
const toolText = `- start \`${event.tool}\``;
|
|
1437
|
+
toolTimeline.push(toolText);
|
|
1438
|
+
currentTools.push(toolText);
|
|
1439
|
+
}
|
|
1440
|
+
if (event.type === "tool:completed") {
|
|
1441
|
+
const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
|
|
1442
|
+
toolTimeline.push(toolText);
|
|
1443
|
+
currentTools.push(toolText);
|
|
1444
|
+
}
|
|
1445
|
+
if (event.type === "tool:error") {
|
|
1446
|
+
const toolText = `- error \`${event.tool}\`: ${event.error}`;
|
|
1447
|
+
toolTimeline.push(toolText);
|
|
1448
|
+
currentTools.push(toolText);
|
|
1449
|
+
}
|
|
1450
|
+
if (event.type === "tool:approval:required") {
|
|
1451
|
+
const toolText = `- approval required \`${event.tool}\``;
|
|
1452
|
+
toolTimeline.push(toolText);
|
|
1453
|
+
currentTools.push(toolText);
|
|
1454
|
+
}
|
|
1455
|
+
if (event.type === "tool:approval:checkpoint") {
|
|
1456
|
+
const conv = await conversationStore.get(conversationId);
|
|
1457
|
+
if (conv) {
|
|
1458
|
+
conv.pendingApprovals = [{
|
|
1459
|
+
approvalId: event.approvalId,
|
|
1460
|
+
runId: latestRunId,
|
|
1461
|
+
tool: event.tool,
|
|
1462
|
+
toolCallId: event.toolCallId,
|
|
1463
|
+
input: event.input,
|
|
1464
|
+
checkpointMessages: [...fullCheckpointMessages, ...event.checkpointMessages],
|
|
1465
|
+
baseMessageCount: 0,
|
|
1466
|
+
pendingToolCalls: event.pendingToolCalls,
|
|
1467
|
+
}];
|
|
1468
|
+
conv.updatedAt = Date.now();
|
|
1469
|
+
await conversationStore.update(conv);
|
|
1470
|
+
}
|
|
1471
|
+
checkpointedRun = true;
|
|
1472
|
+
}
|
|
1473
|
+
if (
|
|
1474
|
+
event.type === "run:completed" &&
|
|
1475
|
+
assistantResponse.length === 0 &&
|
|
1476
|
+
event.result.response
|
|
1477
|
+
) {
|
|
1478
|
+
assistantResponse = event.result.response;
|
|
1479
|
+
}
|
|
1480
|
+
if (event.type === "run:error") {
|
|
1481
|
+
assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
|
|
1482
|
+
}
|
|
1483
|
+
await telemetry.emit(event);
|
|
1484
|
+
broadcastEvent(conversationId, event);
|
|
1485
|
+
}
|
|
1486
|
+
} catch (err) {
|
|
1487
|
+
console.error("[resume-run] error:", err instanceof Error ? err.message : err);
|
|
1488
|
+
assistantResponse = assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
if (currentTools.length > 0) {
|
|
1492
|
+
sections.push({ type: "tools", content: currentTools });
|
|
1493
|
+
}
|
|
1494
|
+
if (currentText.length > 0) {
|
|
1495
|
+
sections.push({ type: "text", content: currentText });
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
if (!checkpointedRun) {
|
|
1499
|
+
const conv = await conversationStore.get(conversationId);
|
|
1500
|
+
if (conv) {
|
|
1501
|
+
const prevMessages = conv.messages;
|
|
1502
|
+
const hasAssistantContent =
|
|
1503
|
+
assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
|
|
1504
|
+
if (hasAssistantContent) {
|
|
1505
|
+
const lastMsg = prevMessages[prevMessages.length - 1];
|
|
1506
|
+
if (lastMsg && lastMsg.role === "assistant" && lastMsg.metadata) {
|
|
1507
|
+
const existingToolActivity = (lastMsg.metadata as Record<string, unknown>).toolActivity;
|
|
1508
|
+
const existingSections = (lastMsg.metadata as Record<string, unknown>).sections;
|
|
1509
|
+
const mergedTimeline = [
|
|
1510
|
+
...(Array.isArray(existingToolActivity) ? existingToolActivity as string[] : []),
|
|
1511
|
+
...toolTimeline,
|
|
1512
|
+
];
|
|
1513
|
+
const mergedSections = [
|
|
1514
|
+
...(Array.isArray(existingSections) ? existingSections as Array<{ type: "text" | "tools"; content: string | string[] }> : []),
|
|
1515
|
+
...sections,
|
|
1516
|
+
];
|
|
1517
|
+
const mergedText = (typeof lastMsg.content === "string" ? lastMsg.content : "") + assistantResponse;
|
|
1518
|
+
conv.messages = [
|
|
1519
|
+
...prevMessages.slice(0, -1),
|
|
1520
|
+
{
|
|
1521
|
+
role: "assistant" as const,
|
|
1522
|
+
content: mergedText,
|
|
1523
|
+
metadata: {
|
|
1524
|
+
toolActivity: mergedTimeline,
|
|
1525
|
+
sections: mergedSections.length > 0 ? mergedSections : undefined,
|
|
1526
|
+
} as Message["metadata"],
|
|
1527
|
+
},
|
|
1528
|
+
];
|
|
1529
|
+
} else {
|
|
1530
|
+
conv.messages = [
|
|
1531
|
+
...prevMessages,
|
|
1532
|
+
{
|
|
1533
|
+
role: "assistant" as const,
|
|
1534
|
+
content: assistantResponse,
|
|
1535
|
+
metadata: (toolTimeline.length > 0 || sections.length > 0
|
|
1536
|
+
? { toolActivity: toolTimeline, sections: sections.length > 0 ? sections : undefined }
|
|
1537
|
+
: undefined) as Message["metadata"],
|
|
1538
|
+
},
|
|
1539
|
+
];
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
conv.runtimeRunId = latestRunId || conv.runtimeRunId;
|
|
1543
|
+
conv.pendingApprovals = [];
|
|
1544
|
+
conv.updatedAt = Date.now();
|
|
1545
|
+
await conversationStore.update(conv);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
finishConversationStream(conversationId);
|
|
1550
|
+
activeConversationRuns.delete(conversationId);
|
|
1551
|
+
if (latestRunId) {
|
|
1552
|
+
runOwners.delete(latestRunId);
|
|
1553
|
+
runConversations.delete(latestRunId);
|
|
1554
|
+
}
|
|
1555
|
+
console.log("[resume-run] complete for", conversationId);
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1399
1558
|
// ---------------------------------------------------------------------------
|
|
1400
1559
|
// Messaging adapters (Slack, etc.) — routes bypass Poncho auth; each
|
|
1401
1560
|
// adapter handles its own request verification (e.g. Slack signing secret).
|
|
@@ -1430,21 +1589,192 @@ export const createRequestHandler = async (options?: {
|
|
|
1430
1589
|
return { messages: [] };
|
|
1431
1590
|
},
|
|
1432
1591
|
async run(conversationId, input) {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1592
|
+
console.log("[messaging-runner] starting run for", conversationId, "task:", input.task.slice(0, 80));
|
|
1593
|
+
|
|
1594
|
+
const historyMessages = [...input.messages];
|
|
1595
|
+
const userContent = input.task;
|
|
1596
|
+
|
|
1597
|
+
// Read-modify-write helper: always fetches the latest version from
|
|
1598
|
+
// the store before writing, so concurrent writers don't get clobbered.
|
|
1599
|
+
const updateConversation = async (
|
|
1600
|
+
patch: (conv: Conversation) => void,
|
|
1601
|
+
): Promise<void> => {
|
|
1602
|
+
const fresh = await conversationStore.get(conversationId);
|
|
1603
|
+
if (!fresh) return;
|
|
1604
|
+
patch(fresh);
|
|
1605
|
+
fresh.updatedAt = Date.now();
|
|
1606
|
+
await conversationStore.update(fresh);
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
// Persist user turn immediately so the web UI shows it while the agent runs.
|
|
1610
|
+
await updateConversation((c) => {
|
|
1611
|
+
c.messages = [...historyMessages, { role: "user" as const, content: userContent }];
|
|
1436
1612
|
});
|
|
1437
|
-
const response = output.result.response ?? "";
|
|
1438
1613
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1614
|
+
let latestRunId = "";
|
|
1615
|
+
let assistantResponse = "";
|
|
1616
|
+
const toolTimeline: string[] = [];
|
|
1617
|
+
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
1618
|
+
let currentTools: string[] = [];
|
|
1619
|
+
let currentText = "";
|
|
1620
|
+
let checkpointedRun = false;
|
|
1621
|
+
|
|
1622
|
+
const buildMessages = (): Message[] => {
|
|
1623
|
+
const draftSections: Array<{ type: "text" | "tools"; content: string | string[] }> = [
|
|
1624
|
+
...sections.map((s) => ({
|
|
1625
|
+
type: s.type,
|
|
1626
|
+
content: Array.isArray(s.content) ? [...s.content] : s.content,
|
|
1627
|
+
})),
|
|
1445
1628
|
];
|
|
1446
|
-
|
|
1629
|
+
if (currentTools.length > 0) {
|
|
1630
|
+
draftSections.push({ type: "tools", content: [...currentTools] });
|
|
1631
|
+
}
|
|
1632
|
+
if (currentText.length > 0) {
|
|
1633
|
+
draftSections.push({ type: "text", content: currentText });
|
|
1634
|
+
}
|
|
1635
|
+
const hasDraftContent =
|
|
1636
|
+
assistantResponse.length > 0 || toolTimeline.length > 0 || draftSections.length > 0;
|
|
1637
|
+
if (!hasDraftContent) {
|
|
1638
|
+
return [...historyMessages, { role: "user" as const, content: userContent }];
|
|
1639
|
+
}
|
|
1640
|
+
return [
|
|
1641
|
+
...historyMessages,
|
|
1642
|
+
{ role: "user" as const, content: userContent },
|
|
1643
|
+
{
|
|
1644
|
+
role: "assistant" as const,
|
|
1645
|
+
content: assistantResponse,
|
|
1646
|
+
metadata:
|
|
1647
|
+
toolTimeline.length > 0 || draftSections.length > 0
|
|
1648
|
+
? ({
|
|
1649
|
+
toolActivity: [...toolTimeline],
|
|
1650
|
+
sections: draftSections.length > 0 ? draftSections : undefined,
|
|
1651
|
+
} as Message["metadata"])
|
|
1652
|
+
: undefined,
|
|
1653
|
+
},
|
|
1654
|
+
];
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
const persistDraftAssistantTurn = async (): Promise<void> => {
|
|
1658
|
+
if (assistantResponse.length === 0 && toolTimeline.length === 0) return;
|
|
1659
|
+
await updateConversation((c) => {
|
|
1660
|
+
c.messages = buildMessages();
|
|
1661
|
+
});
|
|
1662
|
+
};
|
|
1663
|
+
|
|
1664
|
+
const runInput = {
|
|
1665
|
+
task: input.task,
|
|
1666
|
+
conversationId,
|
|
1667
|
+
messages: input.messages,
|
|
1668
|
+
files: input.files,
|
|
1669
|
+
parameters: input.metadata ? {
|
|
1670
|
+
__messaging_platform: input.metadata.platform,
|
|
1671
|
+
__messaging_sender_id: input.metadata.sender.id,
|
|
1672
|
+
__messaging_sender_name: input.metadata.sender.name ?? "",
|
|
1673
|
+
__messaging_thread_id: input.metadata.threadId,
|
|
1674
|
+
} : undefined,
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
try {
|
|
1678
|
+
for await (const event of harness.runWithTelemetry(runInput)) {
|
|
1679
|
+
if (event.type === "run:started") {
|
|
1680
|
+
latestRunId = event.runId;
|
|
1681
|
+
runOwners.set(event.runId, "local-owner");
|
|
1682
|
+
runConversations.set(event.runId, conversationId);
|
|
1683
|
+
}
|
|
1684
|
+
if (event.type === "model:chunk") {
|
|
1685
|
+
if (currentTools.length > 0) {
|
|
1686
|
+
sections.push({ type: "tools", content: currentTools });
|
|
1687
|
+
currentTools = [];
|
|
1688
|
+
}
|
|
1689
|
+
assistantResponse += event.content;
|
|
1690
|
+
currentText += event.content;
|
|
1691
|
+
}
|
|
1692
|
+
if (event.type === "tool:started") {
|
|
1693
|
+
if (currentText.length > 0) {
|
|
1694
|
+
sections.push({ type: "text", content: currentText });
|
|
1695
|
+
currentText = "";
|
|
1696
|
+
}
|
|
1697
|
+
const toolText = `- start \`${event.tool}\``;
|
|
1698
|
+
toolTimeline.push(toolText);
|
|
1699
|
+
currentTools.push(toolText);
|
|
1700
|
+
}
|
|
1701
|
+
if (event.type === "tool:completed") {
|
|
1702
|
+
const toolText = `- done \`${event.tool}\` (${event.duration}ms)`;
|
|
1703
|
+
toolTimeline.push(toolText);
|
|
1704
|
+
currentTools.push(toolText);
|
|
1705
|
+
}
|
|
1706
|
+
if (event.type === "tool:error") {
|
|
1707
|
+
const toolText = `- error \`${event.tool}\`: ${event.error}`;
|
|
1708
|
+
toolTimeline.push(toolText);
|
|
1709
|
+
currentTools.push(toolText);
|
|
1710
|
+
}
|
|
1711
|
+
if (event.type === "step:completed") {
|
|
1712
|
+
await persistDraftAssistantTurn();
|
|
1713
|
+
}
|
|
1714
|
+
if (event.type === "tool:approval:required") {
|
|
1715
|
+
const toolText = `- approval required \`${event.tool}\``;
|
|
1716
|
+
toolTimeline.push(toolText);
|
|
1717
|
+
currentTools.push(toolText);
|
|
1718
|
+
await persistDraftAssistantTurn();
|
|
1719
|
+
}
|
|
1720
|
+
if (event.type === "tool:approval:checkpoint") {
|
|
1721
|
+
await updateConversation((c) => {
|
|
1722
|
+
c.messages = buildMessages();
|
|
1723
|
+
c.pendingApprovals = [{
|
|
1724
|
+
approvalId: event.approvalId,
|
|
1725
|
+
runId: latestRunId,
|
|
1726
|
+
tool: event.tool,
|
|
1727
|
+
toolCallId: event.toolCallId,
|
|
1728
|
+
input: event.input,
|
|
1729
|
+
checkpointMessages: event.checkpointMessages,
|
|
1730
|
+
baseMessageCount: historyMessages.length,
|
|
1731
|
+
pendingToolCalls: event.pendingToolCalls,
|
|
1732
|
+
}];
|
|
1733
|
+
});
|
|
1734
|
+
checkpointedRun = true;
|
|
1735
|
+
}
|
|
1736
|
+
if (
|
|
1737
|
+
event.type === "run:completed" &&
|
|
1738
|
+
assistantResponse.length === 0 &&
|
|
1739
|
+
event.result.response
|
|
1740
|
+
) {
|
|
1741
|
+
assistantResponse = event.result.response;
|
|
1742
|
+
}
|
|
1743
|
+
if (event.type === "run:error") {
|
|
1744
|
+
assistantResponse = assistantResponse || `[Error: ${event.error.message}]`;
|
|
1745
|
+
}
|
|
1746
|
+
broadcastEvent(conversationId, event);
|
|
1747
|
+
}
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
console.error("[messaging-runner] run failed:", err instanceof Error ? err.message : err);
|
|
1750
|
+
assistantResponse = assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Finalize sections (clear after pushing so buildMessages doesn't re-add)
|
|
1754
|
+
if (currentTools.length > 0) {
|
|
1755
|
+
sections.push({ type: "tools", content: currentTools });
|
|
1756
|
+
currentTools = [];
|
|
1447
1757
|
}
|
|
1758
|
+
if (currentText.length > 0) {
|
|
1759
|
+
sections.push({ type: "text", content: currentText });
|
|
1760
|
+
currentText = "";
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
if (!checkpointedRun) {
|
|
1764
|
+
await updateConversation((c) => {
|
|
1765
|
+
c.messages = buildMessages();
|
|
1766
|
+
c.runtimeRunId = latestRunId || c.runtimeRunId;
|
|
1767
|
+
c.pendingApprovals = [];
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
finishConversationStream(conversationId);
|
|
1771
|
+
if (latestRunId) {
|
|
1772
|
+
runOwners.delete(latestRunId);
|
|
1773
|
+
runConversations.delete(latestRunId);
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
console.log("[messaging-runner] run complete, response length:", assistantResponse.length);
|
|
1777
|
+
const response = assistantResponse;
|
|
1448
1778
|
|
|
1449
1779
|
return { response };
|
|
1450
1780
|
},
|
|
@@ -1475,6 +1805,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1475
1805
|
adapter,
|
|
1476
1806
|
runner: messagingRunner,
|
|
1477
1807
|
waitUntil: waitUntilHook,
|
|
1808
|
+
ownerId: "local-owner",
|
|
1478
1809
|
});
|
|
1479
1810
|
adapter.registerRoutes(messagingRouteRegistrar);
|
|
1480
1811
|
try {
|
|
@@ -1486,6 +1817,37 @@ export const createRequestHandler = async (options?: {
|
|
|
1486
1817
|
` Slack messaging disabled: ${err instanceof Error ? err.message : String(err)}`,
|
|
1487
1818
|
);
|
|
1488
1819
|
}
|
|
1820
|
+
} else if (channelConfig.platform === "resend") {
|
|
1821
|
+
const adapter = new ResendAdapter({
|
|
1822
|
+
apiKeyEnv: channelConfig.apiKeyEnv,
|
|
1823
|
+
webhookSecretEnv: channelConfig.webhookSecretEnv,
|
|
1824
|
+
fromEnv: channelConfig.fromEnv,
|
|
1825
|
+
allowedSenders: channelConfig.allowedSenders,
|
|
1826
|
+
mode: channelConfig.mode,
|
|
1827
|
+
allowedRecipients: channelConfig.allowedRecipients,
|
|
1828
|
+
maxSendsPerRun: channelConfig.maxSendsPerRun,
|
|
1829
|
+
});
|
|
1830
|
+
const bridge = new AgentBridge({
|
|
1831
|
+
adapter,
|
|
1832
|
+
runner: messagingRunner,
|
|
1833
|
+
waitUntil: waitUntilHook,
|
|
1834
|
+
ownerId: "local-owner",
|
|
1835
|
+
});
|
|
1836
|
+
adapter.registerRoutes(messagingRouteRegistrar);
|
|
1837
|
+
try {
|
|
1838
|
+
await bridge.start();
|
|
1839
|
+
messagingBridges.push(bridge);
|
|
1840
|
+
const adapterTools = adapter.getToolDefinitions?.() ?? [];
|
|
1841
|
+
if (adapterTools.length > 0) {
|
|
1842
|
+
harness.registerTools(adapterTools);
|
|
1843
|
+
}
|
|
1844
|
+
const modeLabel = channelConfig.mode === "tool" ? "tool" : "auto-reply";
|
|
1845
|
+
console.log(` Resend email messaging enabled at /api/messaging/resend (mode: ${modeLabel})`);
|
|
1846
|
+
} catch (err) {
|
|
1847
|
+
console.warn(
|
|
1848
|
+
` Resend email messaging disabled: ${err instanceof Error ? err.message : String(err)}`,
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1489
1851
|
}
|
|
1490
1852
|
}
|
|
1491
1853
|
}
|
|
@@ -1498,6 +1860,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1498
1860
|
const authRequired = config?.auth?.required ?? false;
|
|
1499
1861
|
const requireAuth = authRequired && authToken.length > 0;
|
|
1500
1862
|
|
|
1863
|
+
const webUiEnabled = config?.webUi !== false;
|
|
1501
1864
|
const isProduction = resolveHarnessEnvironment() === "production";
|
|
1502
1865
|
const secureCookies = isProduction;
|
|
1503
1866
|
|
|
@@ -1523,41 +1886,52 @@ export const createRequestHandler = async (options?: {
|
|
|
1523
1886
|
}
|
|
1524
1887
|
const [pathname] = request.url.split("?");
|
|
1525
1888
|
|
|
1526
|
-
if (
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1889
|
+
if (webUiEnabled) {
|
|
1890
|
+
if (request.method === "GET" && (pathname === "/" || pathname.startsWith("/c/"))) {
|
|
1891
|
+
writeHtml(response, 200, renderWebUiHtml({ agentName }));
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1530
1894
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1895
|
+
if (pathname === "/manifest.json" && request.method === "GET") {
|
|
1896
|
+
response.writeHead(200, { "Content-Type": "application/manifest+json" });
|
|
1897
|
+
response.end(renderManifest({ agentName }));
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1536
1900
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1901
|
+
if (pathname === "/sw.js" && request.method === "GET") {
|
|
1902
|
+
response.writeHead(200, {
|
|
1903
|
+
"Content-Type": "application/javascript",
|
|
1904
|
+
"Service-Worker-Allowed": "/",
|
|
1905
|
+
});
|
|
1906
|
+
response.end(renderServiceWorker());
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
if (pathname === "/icon.svg" && request.method === "GET") {
|
|
1911
|
+
response.writeHead(200, { "Content-Type": "image/svg+xml" });
|
|
1912
|
+
response.end(renderIconSvg({ agentName }));
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
if ((pathname === "/icon-192.png" || pathname === "/icon-512.png") && request.method === "GET") {
|
|
1917
|
+
response.writeHead(302, { Location: "/icon.svg" });
|
|
1918
|
+
response.end();
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1544
1921
|
}
|
|
1545
1922
|
|
|
1546
|
-
if (pathname === "/
|
|
1547
|
-
response
|
|
1548
|
-
response.end(renderIconSvg({ agentName }));
|
|
1923
|
+
if (pathname === "/health" && request.method === "GET") {
|
|
1924
|
+
writeJson(response, 200, { status: "ok" });
|
|
1549
1925
|
return;
|
|
1550
1926
|
}
|
|
1551
1927
|
|
|
1552
|
-
if (
|
|
1553
|
-
|
|
1554
|
-
response.writeHead(302, { Location: "/icon.svg" });
|
|
1555
|
-
response.end();
|
|
1928
|
+
if (pathname === "/api/openapi.json" && request.method === "GET") {
|
|
1929
|
+
writeJson(response, 200, buildOpenApiSpec({ agentName }));
|
|
1556
1930
|
return;
|
|
1557
1931
|
}
|
|
1558
1932
|
|
|
1559
|
-
if (pathname === "/
|
|
1560
|
-
|
|
1933
|
+
if (pathname === "/api/docs" && request.method === "GET") {
|
|
1934
|
+
writeHtml(response, 200, renderApiDocsHtml("/api/openapi.json"));
|
|
1561
1935
|
return;
|
|
1562
1936
|
}
|
|
1563
1937
|
|
|
@@ -1696,6 +2070,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1696
2070
|
createdAt: conversation.createdAt,
|
|
1697
2071
|
updatedAt: conversation.updatedAt,
|
|
1698
2072
|
messageCount: conversation.messages.length,
|
|
2073
|
+
hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
|
|
1699
2074
|
})),
|
|
1700
2075
|
});
|
|
1701
2076
|
return;
|
|
@@ -1721,40 +2096,93 @@ export const createRequestHandler = async (options?: {
|
|
|
1721
2096
|
const approvalMatch = pathname.match(/^\/api\/approvals\/([^/]+)$/);
|
|
1722
2097
|
if (approvalMatch && request.method === "POST") {
|
|
1723
2098
|
const approvalId = decodeURIComponent(approvalMatch[1] ?? "");
|
|
1724
|
-
const
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
conversation.pendingApprovals = next;
|
|
1739
|
-
await conversationStore.update(conversation);
|
|
1740
|
-
prunedStale = true;
|
|
1741
|
-
}
|
|
2099
|
+
const body = (await readRequestBody(request)) as { approved?: boolean };
|
|
2100
|
+
const approved = body.approved === true;
|
|
2101
|
+
|
|
2102
|
+
// Find the approval in the conversation store (checkpoint-based flow)
|
|
2103
|
+
const conversations = await conversationStore.list(ownerId);
|
|
2104
|
+
let foundConversation: Conversation | undefined;
|
|
2105
|
+
let foundApproval: NonNullable<Conversation["pendingApprovals"]>[number] | undefined;
|
|
2106
|
+
for (const conv of conversations) {
|
|
2107
|
+
if (!Array.isArray(conv.pendingApprovals)) continue;
|
|
2108
|
+
const match = conv.pendingApprovals.find(a => a.approvalId === approvalId);
|
|
2109
|
+
if (match) {
|
|
2110
|
+
foundConversation = conv;
|
|
2111
|
+
foundApproval = match;
|
|
2112
|
+
break;
|
|
1742
2113
|
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (!foundConversation || !foundApproval) {
|
|
1743
2117
|
writeJson(response, 404, {
|
|
1744
2118
|
code: "APPROVAL_NOT_FOUND",
|
|
1745
|
-
message:
|
|
1746
|
-
? "Approval request is no longer active"
|
|
1747
|
-
: "Approval request not found",
|
|
2119
|
+
message: "Approval request not found",
|
|
1748
2120
|
});
|
|
1749
2121
|
return;
|
|
1750
2122
|
}
|
|
1751
|
-
|
|
1752
|
-
const
|
|
1753
|
-
|
|
1754
|
-
if (
|
|
1755
|
-
|
|
2123
|
+
|
|
2124
|
+
const conversationId = foundConversation.conversationId;
|
|
2125
|
+
|
|
2126
|
+
if (!foundApproval.checkpointMessages || !foundApproval.toolCallId) {
|
|
2127
|
+
// Legacy approval without checkpoint data — cannot resume
|
|
2128
|
+
foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? [])
|
|
2129
|
+
.filter(a => a.approvalId !== approvalId);
|
|
2130
|
+
await conversationStore.update(foundConversation);
|
|
2131
|
+
writeJson(response, 404, {
|
|
2132
|
+
code: "APPROVAL_NOT_FOUND",
|
|
2133
|
+
message: "Approval request is no longer active (no checkpoint data)",
|
|
2134
|
+
});
|
|
2135
|
+
return;
|
|
1756
2136
|
}
|
|
1757
|
-
|
|
2137
|
+
|
|
2138
|
+
// Clear this approval from the conversation before resuming
|
|
2139
|
+
foundConversation.pendingApprovals = (foundConversation.pendingApprovals ?? [])
|
|
2140
|
+
.filter(a => a.approvalId !== approvalId);
|
|
2141
|
+
await conversationStore.update(foundConversation);
|
|
2142
|
+
|
|
2143
|
+
// Initialize the event stream so the web UI can connect immediately
|
|
2144
|
+
broadcastEvent(conversationId,
|
|
2145
|
+
approved
|
|
2146
|
+
? { type: "tool:approval:granted", approvalId }
|
|
2147
|
+
: { type: "tool:approval:denied", approvalId },
|
|
2148
|
+
);
|
|
2149
|
+
|
|
2150
|
+
// Resume the run asynchronously (tool execution + continuation)
|
|
2151
|
+
void (async () => {
|
|
2152
|
+
let toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }>;
|
|
2153
|
+
if (approved) {
|
|
2154
|
+
const toolContext = {
|
|
2155
|
+
runId: foundApproval.runId,
|
|
2156
|
+
agentId: identity.id,
|
|
2157
|
+
step: 0,
|
|
2158
|
+
workingDir,
|
|
2159
|
+
parameters: {},
|
|
2160
|
+
};
|
|
2161
|
+
const execResults = await harness.executeTools(
|
|
2162
|
+
[{ id: foundApproval.toolCallId!, name: foundApproval.tool, input: foundApproval.input }],
|
|
2163
|
+
toolContext,
|
|
2164
|
+
);
|
|
2165
|
+
toolResults = execResults.map(r => ({
|
|
2166
|
+
callId: r.callId,
|
|
2167
|
+
toolName: r.tool,
|
|
2168
|
+
result: r.output,
|
|
2169
|
+
error: r.error,
|
|
2170
|
+
}));
|
|
2171
|
+
} else {
|
|
2172
|
+
toolResults = [{
|
|
2173
|
+
callId: foundApproval.toolCallId!,
|
|
2174
|
+
toolName: foundApproval.tool,
|
|
2175
|
+
error: "Tool execution denied by user",
|
|
2176
|
+
}];
|
|
2177
|
+
}
|
|
2178
|
+
await resumeRunFromCheckpoint(
|
|
2179
|
+
conversationId,
|
|
2180
|
+
foundConversation!,
|
|
2181
|
+
foundApproval!,
|
|
2182
|
+
toolResults,
|
|
2183
|
+
);
|
|
2184
|
+
})();
|
|
2185
|
+
|
|
1758
2186
|
writeJson(response, 200, { ok: true, approvalId, approved });
|
|
1759
2187
|
return;
|
|
1760
2188
|
}
|
|
@@ -1779,18 +2207,19 @@ export const createRequestHandler = async (options?: {
|
|
|
1779
2207
|
});
|
|
1780
2208
|
const stream = conversationEventStreams.get(conversationId);
|
|
1781
2209
|
if (!stream) {
|
|
1782
|
-
// No active run — close immediately
|
|
1783
2210
|
response.write("event: stream:end\ndata: {}\n\n");
|
|
1784
2211
|
response.end();
|
|
1785
2212
|
return;
|
|
1786
2213
|
}
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2214
|
+
const liveOnly = (request.url ?? "").includes("live_only=true");
|
|
2215
|
+
if (!liveOnly) {
|
|
2216
|
+
for (const bufferedEvent of stream.buffer) {
|
|
2217
|
+
try {
|
|
2218
|
+
response.write(formatSseEvent(bufferedEvent));
|
|
2219
|
+
} catch {
|
|
2220
|
+
response.end();
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
1794
2223
|
}
|
|
1795
2224
|
}
|
|
1796
2225
|
if (stream.finished) {
|
|
@@ -1819,33 +2248,21 @@ export const createRequestHandler = async (options?: {
|
|
|
1819
2248
|
}
|
|
1820
2249
|
if (request.method === "GET") {
|
|
1821
2250
|
const storedPending = Array.isArray(conversation.pendingApprovals)
|
|
1822
|
-
? conversation.pendingApprovals
|
|
2251
|
+
? conversation.pendingApprovals.map(a => ({
|
|
2252
|
+
approvalId: a.approvalId,
|
|
2253
|
+
runId: a.runId,
|
|
2254
|
+
tool: a.tool,
|
|
2255
|
+
input: a.input,
|
|
2256
|
+
}))
|
|
1823
2257
|
: [];
|
|
1824
|
-
const
|
|
1825
|
-
|
|
1826
|
-
([, pending]) =>
|
|
1827
|
-
pending.ownerId === ownerId && pending.conversationId === conversationId,
|
|
1828
|
-
)
|
|
1829
|
-
.map(([approvalId, pending]) => ({
|
|
1830
|
-
approvalId,
|
|
1831
|
-
runId: pending.runId,
|
|
1832
|
-
tool: pending.tool,
|
|
1833
|
-
input: pending.input,
|
|
1834
|
-
}));
|
|
1835
|
-
const mergedPendingById = new Map<string, (typeof livePending)[number]>();
|
|
1836
|
-
for (const approval of storedPending) {
|
|
1837
|
-
if (approval && typeof approval.approvalId === "string") {
|
|
1838
|
-
mergedPendingById.set(approval.approvalId, approval);
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
for (const approval of livePending) {
|
|
1842
|
-
mergedPendingById.set(approval.approvalId, approval);
|
|
1843
|
-
}
|
|
2258
|
+
const activeStream = conversationEventStreams.get(conversationId);
|
|
2259
|
+
const hasActiveRun = !!activeStream && !activeStream.finished;
|
|
1844
2260
|
writeJson(response, 200, {
|
|
1845
2261
|
conversation: {
|
|
1846
2262
|
...conversation,
|
|
1847
|
-
pendingApprovals:
|
|
2263
|
+
pendingApprovals: storedPending,
|
|
1848
2264
|
},
|
|
2265
|
+
hasActiveRun,
|
|
1849
2266
|
});
|
|
1850
2267
|
return;
|
|
1851
2268
|
}
|
|
@@ -2029,6 +2446,7 @@ export const createRequestHandler = async (options?: {
|
|
|
2029
2446
|
let currentText = "";
|
|
2030
2447
|
let currentTools: string[] = [];
|
|
2031
2448
|
let runCancelled = false;
|
|
2449
|
+
let checkpointedRun = false;
|
|
2032
2450
|
let userContent: Message["content"] = messageText;
|
|
2033
2451
|
if (files.length > 0) {
|
|
2034
2452
|
try {
|
|
@@ -2124,6 +2542,7 @@ export const createRequestHandler = async (options?: {
|
|
|
2124
2542
|
|
|
2125
2543
|
for await (const event of harness.runWithTelemetry({
|
|
2126
2544
|
task: messageText,
|
|
2545
|
+
conversationId,
|
|
2127
2546
|
parameters: {
|
|
2128
2547
|
...(bodyParameters ?? {}),
|
|
2129
2548
|
__conversationRecallCorpus: recallCorpus,
|
|
@@ -2183,17 +2602,40 @@ export const createRequestHandler = async (options?: {
|
|
|
2183
2602
|
currentTools.push(toolText);
|
|
2184
2603
|
await persistDraftAssistantTurn();
|
|
2185
2604
|
}
|
|
2186
|
-
if (event.type === "tool:approval:
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2605
|
+
if (event.type === "tool:approval:checkpoint") {
|
|
2606
|
+
const checkpointSections = [...sections];
|
|
2607
|
+
if (currentTools.length > 0) {
|
|
2608
|
+
checkpointSections.push({ type: "tools", content: [...currentTools] });
|
|
2609
|
+
}
|
|
2610
|
+
if (currentText.length > 0) {
|
|
2611
|
+
checkpointSections.push({ type: "text", content: currentText });
|
|
2612
|
+
}
|
|
2613
|
+
conversation.messages = [
|
|
2614
|
+
...historyMessages,
|
|
2615
|
+
{ role: "user", content: userContent },
|
|
2616
|
+
...(assistantResponse.length > 0 || toolTimeline.length > 0 || checkpointSections.length > 0
|
|
2617
|
+
? [{
|
|
2618
|
+
role: "assistant" as const,
|
|
2619
|
+
content: assistantResponse,
|
|
2620
|
+
metadata: (toolTimeline.length > 0 || checkpointSections.length > 0
|
|
2621
|
+
? { toolActivity: [...toolTimeline], sections: checkpointSections.length > 0 ? checkpointSections : undefined }
|
|
2622
|
+
: undefined) as Message["metadata"],
|
|
2623
|
+
}]
|
|
2624
|
+
: []),
|
|
2625
|
+
];
|
|
2626
|
+
conversation.pendingApprovals = [{
|
|
2627
|
+
approvalId: event.approvalId,
|
|
2628
|
+
runId: latestRunId,
|
|
2629
|
+
tool: event.tool,
|
|
2630
|
+
toolCallId: event.toolCallId,
|
|
2631
|
+
input: event.input,
|
|
2632
|
+
checkpointMessages: event.checkpointMessages,
|
|
2633
|
+
baseMessageCount: historyMessages.length,
|
|
2634
|
+
pendingToolCalls: event.pendingToolCalls,
|
|
2635
|
+
}];
|
|
2636
|
+
conversation.updatedAt = Date.now();
|
|
2637
|
+
await conversationStore.update(conversation);
|
|
2638
|
+
checkpointedRun = true;
|
|
2197
2639
|
}
|
|
2198
2640
|
if (
|
|
2199
2641
|
event.type === "run:completed" &&
|
|
@@ -2218,29 +2660,31 @@ export const createRequestHandler = async (options?: {
|
|
|
2218
2660
|
if (currentText.length > 0) {
|
|
2219
2661
|
sections.push({ type: "text", content: currentText });
|
|
2220
2662
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2663
|
+
if (!checkpointedRun) {
|
|
2664
|
+
const hasAssistantContent =
|
|
2665
|
+
assistantResponse.length > 0 || toolTimeline.length > 0 || sections.length > 0;
|
|
2666
|
+
conversation.messages = hasAssistantContent
|
|
2667
|
+
? [
|
|
2668
|
+
...historyMessages,
|
|
2669
|
+
{ role: "user", content: userContent },
|
|
2670
|
+
{
|
|
2671
|
+
role: "assistant",
|
|
2672
|
+
content: assistantResponse,
|
|
2673
|
+
metadata:
|
|
2674
|
+
toolTimeline.length > 0 || sections.length > 0
|
|
2675
|
+
? ({
|
|
2676
|
+
toolActivity: toolTimeline,
|
|
2677
|
+
sections: sections.length > 0 ? sections : undefined,
|
|
2678
|
+
} as Message["metadata"])
|
|
2679
|
+
: undefined,
|
|
2680
|
+
},
|
|
2681
|
+
]
|
|
2682
|
+
: [...historyMessages, { role: "user", content: userContent }];
|
|
2683
|
+
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
2684
|
+
conversation.pendingApprovals = [];
|
|
2685
|
+
conversation.updatedAt = Date.now();
|
|
2686
|
+
await conversationStore.update(conversation);
|
|
2687
|
+
}
|
|
2244
2688
|
} catch (error) {
|
|
2245
2689
|
if (abortController.signal.aborted || runCancelled) {
|
|
2246
2690
|
const fallbackSections = [...sections];
|
|
@@ -2318,7 +2762,6 @@ export const createRequestHandler = async (options?: {
|
|
|
2318
2762
|
activeConversationRuns.delete(conversationId);
|
|
2319
2763
|
}
|
|
2320
2764
|
finishConversationStream(conversationId);
|
|
2321
|
-
await persistConversationPendingApprovals(conversationId);
|
|
2322
2765
|
if (latestRunId) {
|
|
2323
2766
|
runOwners.delete(latestRunId);
|
|
2324
2767
|
runConversations.delete(latestRunId);
|
|
@@ -2403,6 +2846,7 @@ export const createRequestHandler = async (options?: {
|
|
|
2403
2846
|
|
|
2404
2847
|
for await (const event of harness.runWithTelemetry({
|
|
2405
2848
|
task: cronJob.task,
|
|
2849
|
+
conversationId: conversation.conversationId,
|
|
2406
2850
|
parameters: { __activeConversationId: conversation.conversationId },
|
|
2407
2851
|
messages: historyMessages,
|
|
2408
2852
|
abortSignal: abortController.signal,
|
|
@@ -2589,6 +3033,7 @@ export const startDevServer = async (
|
|
|
2589
3033
|
let currentText = "";
|
|
2590
3034
|
for await (const event of harness.runWithTelemetry({
|
|
2591
3035
|
task: config.task,
|
|
3036
|
+
conversationId: conversation.conversationId,
|
|
2592
3037
|
parameters: { __activeConversationId: conversation.conversationId },
|
|
2593
3038
|
messages: [],
|
|
2594
3039
|
})) {
|
|
@@ -2775,44 +3220,10 @@ export const runInteractive = async (
|
|
|
2775
3220
|
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
2776
3221
|
const config = await loadPonchoConfig(workingDir);
|
|
2777
3222
|
|
|
2778
|
-
// Approval bridge: the harness calls this handler which creates a pending
|
|
2779
|
-
// promise. The Ink UI picks up the pending request and shows a Y/N prompt.
|
|
2780
|
-
// The user's response resolves the promise.
|
|
2781
|
-
type ApprovalRequest = {
|
|
2782
|
-
tool: string;
|
|
2783
|
-
input: Record<string, unknown>;
|
|
2784
|
-
approvalId: string;
|
|
2785
|
-
resolve: (approved: boolean) => void;
|
|
2786
|
-
};
|
|
2787
|
-
let pendingApproval: ApprovalRequest | null = null;
|
|
2788
|
-
let onApprovalRequest: ((req: ApprovalRequest) => void) | null = null;
|
|
2789
|
-
|
|
2790
|
-
const approvalHandler = async (request: {
|
|
2791
|
-
tool: string;
|
|
2792
|
-
input: Record<string, unknown>;
|
|
2793
|
-
runId: string;
|
|
2794
|
-
step: number;
|
|
2795
|
-
approvalId: string;
|
|
2796
|
-
}): Promise<boolean> => {
|
|
2797
|
-
return new Promise<boolean>((resolveApproval) => {
|
|
2798
|
-
const req: ApprovalRequest = {
|
|
2799
|
-
tool: request.tool,
|
|
2800
|
-
input: request.input,
|
|
2801
|
-
approvalId: request.approvalId,
|
|
2802
|
-
resolve: resolveApproval,
|
|
2803
|
-
};
|
|
2804
|
-
pendingApproval = req;
|
|
2805
|
-
if (onApprovalRequest) {
|
|
2806
|
-
onApprovalRequest(req);
|
|
2807
|
-
}
|
|
2808
|
-
});
|
|
2809
|
-
};
|
|
2810
|
-
|
|
2811
3223
|
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
2812
3224
|
const harness = new AgentHarness({
|
|
2813
3225
|
workingDir,
|
|
2814
3226
|
environment: resolveHarnessEnvironment(),
|
|
2815
|
-
approvalHandler,
|
|
2816
3227
|
uploadStore,
|
|
2817
3228
|
});
|
|
2818
3229
|
await harness.initialize();
|
|
@@ -2826,7 +3237,6 @@ export const runInteractive = async (
|
|
|
2826
3237
|
workingDir: string;
|
|
2827
3238
|
config?: PonchoConfig;
|
|
2828
3239
|
conversationStore: ConversationStore;
|
|
2829
|
-
onSetApprovalCallback?: (cb: (req: ApprovalRequest) => void) => void;
|
|
2830
3240
|
}) => Promise<void>
|
|
2831
3241
|
)({
|
|
2832
3242
|
harness,
|
|
@@ -2837,13 +3247,6 @@ export const runInteractive = async (
|
|
|
2837
3247
|
workingDir,
|
|
2838
3248
|
agentId: identity.id,
|
|
2839
3249
|
}),
|
|
2840
|
-
onSetApprovalCallback: (cb: (req: ApprovalRequest) => void) => {
|
|
2841
|
-
onApprovalRequest = cb;
|
|
2842
|
-
// If there's already a pending request, fire it immediately
|
|
2843
|
-
if (pendingApproval) {
|
|
2844
|
-
cb(pendingApproval);
|
|
2845
|
-
}
|
|
2846
|
-
},
|
|
2847
3250
|
});
|
|
2848
3251
|
} finally {
|
|
2849
3252
|
await harness.shutdown();
|