@poncho-ai/cli 0.14.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 +19 -0
- package/dist/{chunk-A32BXZKP.js → chunk-AIEVSNGF.js} +523 -255
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/{run-interactive-ink-SLWDVTDX.js → run-interactive-ink-7ULE5JJI.js} +151 -118
- package/package.json +4 -4
- package/src/index.ts +357 -197
- package/src/run-interactive-ink.ts +171 -147
- package/src/web-ui.ts +213 -101
package/src/index.ts
CHANGED
|
@@ -1315,16 +1315,6 @@ export const createRequestHandler = async (options?: {
|
|
|
1315
1315
|
runId: string | null;
|
|
1316
1316
|
};
|
|
1317
1317
|
const activeConversationRuns = new Map<string, ActiveConversationRun>();
|
|
1318
|
-
type PendingApproval = {
|
|
1319
|
-
ownerId: string;
|
|
1320
|
-
runId: string;
|
|
1321
|
-
conversationId: string | null;
|
|
1322
|
-
tool: string;
|
|
1323
|
-
input: Record<string, unknown>;
|
|
1324
|
-
resolve: (approved: boolean) => void;
|
|
1325
|
-
};
|
|
1326
|
-
const pendingApprovals = new Map<string, PendingApproval>();
|
|
1327
|
-
|
|
1328
1318
|
// Per-conversation event streaming: buffer events and allow SSE subscribers
|
|
1329
1319
|
type ConversationEventStream = {
|
|
1330
1320
|
buffer: AgentEvent[];
|
|
@@ -1364,55 +1354,19 @@ export const createRequestHandler = async (options?: {
|
|
|
1364
1354
|
setTimeout(() => conversationEventStreams.delete(conversationId), 30_000);
|
|
1365
1355
|
}
|
|
1366
1356
|
};
|
|
1367
|
-
const persistConversationPendingApprovals = async (conversationId: string): Promise<void> => {
|
|
1368
|
-
const conversation = await conversationStore.get(conversationId);
|
|
1369
|
-
if (!conversation) {
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
|
-
conversation.pendingApprovals = Array.from(pendingApprovals.entries())
|
|
1373
|
-
.filter(
|
|
1374
|
-
([, pending]) =>
|
|
1375
|
-
pending.ownerId === conversation.ownerId && pending.conversationId === conversationId,
|
|
1376
|
-
)
|
|
1377
|
-
.map(([approvalId, pending]) => ({
|
|
1378
|
-
approvalId,
|
|
1379
|
-
runId: pending.runId,
|
|
1380
|
-
tool: pending.tool,
|
|
1381
|
-
input: pending.input,
|
|
1382
|
-
}));
|
|
1383
|
-
await conversationStore.update(conversation);
|
|
1384
|
-
};
|
|
1385
1357
|
const clearPendingApprovalsForConversation = async (conversationId: string): Promise<void> => {
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
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);
|
|
1392
1363
|
}
|
|
1393
|
-
await persistConversationPendingApprovals(conversationId);
|
|
1394
1364
|
};
|
|
1395
1365
|
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
1396
1366
|
const harness = new AgentHarness({
|
|
1397
1367
|
workingDir,
|
|
1398
1368
|
environment: resolveHarnessEnvironment(),
|
|
1399
1369
|
uploadStore,
|
|
1400
|
-
approvalHandler: async (request) =>
|
|
1401
|
-
new Promise<boolean>((resolveApproval) => {
|
|
1402
|
-
const ownerIdForRun = runOwners.get(request.runId) ?? "local-owner";
|
|
1403
|
-
const conversationIdForRun = runConversations.get(request.runId) ?? null;
|
|
1404
|
-
pendingApprovals.set(request.approvalId, {
|
|
1405
|
-
ownerId: ownerIdForRun,
|
|
1406
|
-
runId: request.runId,
|
|
1407
|
-
conversationId: conversationIdForRun,
|
|
1408
|
-
tool: request.tool,
|
|
1409
|
-
input: request.input,
|
|
1410
|
-
resolve: resolveApproval,
|
|
1411
|
-
});
|
|
1412
|
-
if (conversationIdForRun) {
|
|
1413
|
-
void persistConversationPendingApprovals(conversationIdForRun);
|
|
1414
|
-
}
|
|
1415
|
-
}),
|
|
1416
1370
|
});
|
|
1417
1371
|
await harness.initialize();
|
|
1418
1372
|
const telemetry = new TelemetryEmitter(config?.telemetry);
|
|
@@ -1421,6 +1375,186 @@ export const createRequestHandler = async (options?: {
|
|
|
1421
1375
|
workingDir,
|
|
1422
1376
|
agentId: identity.id,
|
|
1423
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
|
+
|
|
1424
1558
|
// ---------------------------------------------------------------------------
|
|
1425
1559
|
// Messaging adapters (Slack, etc.) — routes bypass Poncho auth; each
|
|
1426
1560
|
// adapter handles its own request verification (e.g. Slack signing secret).
|
|
@@ -1461,8 +1595,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1461
1595
|
const userContent = input.task;
|
|
1462
1596
|
|
|
1463
1597
|
// Read-modify-write helper: always fetches the latest version from
|
|
1464
|
-
// the store before writing, so concurrent writers
|
|
1465
|
-
// handler's persistConversationPendingApprovals) don't get clobbered.
|
|
1598
|
+
// the store before writing, so concurrent writers don't get clobbered.
|
|
1466
1599
|
const updateConversation = async (
|
|
1467
1600
|
patch: (conv: Conversation) => void,
|
|
1468
1601
|
): Promise<void> => {
|
|
@@ -1484,6 +1617,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1484
1617
|
const sections: Array<{ type: "text" | "tools"; content: string | string[] }> = [];
|
|
1485
1618
|
let currentTools: string[] = [];
|
|
1486
1619
|
let currentText = "";
|
|
1620
|
+
let checkpointedRun = false;
|
|
1487
1621
|
|
|
1488
1622
|
const buildMessages = (): Message[] => {
|
|
1489
1623
|
const draftSections: Array<{ type: "text" | "tools"; content: string | string[] }> = [
|
|
@@ -1582,19 +1716,22 @@ export const createRequestHandler = async (options?: {
|
|
|
1582
1716
|
toolTimeline.push(toolText);
|
|
1583
1717
|
currentTools.push(toolText);
|
|
1584
1718
|
await persistDraftAssistantTurn();
|
|
1585
|
-
await persistConversationPendingApprovals(conversationId);
|
|
1586
|
-
}
|
|
1587
|
-
if (event.type === "tool:approval:granted") {
|
|
1588
|
-
const toolText = `- approval granted (${event.approvalId})`;
|
|
1589
|
-
toolTimeline.push(toolText);
|
|
1590
|
-
currentTools.push(toolText);
|
|
1591
|
-
await persistDraftAssistantTurn();
|
|
1592
1719
|
}
|
|
1593
|
-
if (event.type === "tool:approval:
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
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;
|
|
1598
1735
|
}
|
|
1599
1736
|
if (
|
|
1600
1737
|
event.type === "run:completed" &&
|
|
@@ -1623,13 +1760,14 @@ export const createRequestHandler = async (options?: {
|
|
|
1623
1760
|
currentText = "";
|
|
1624
1761
|
}
|
|
1625
1762
|
|
|
1626
|
-
|
|
1627
|
-
c
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1763
|
+
if (!checkpointedRun) {
|
|
1764
|
+
await updateConversation((c) => {
|
|
1765
|
+
c.messages = buildMessages();
|
|
1766
|
+
c.runtimeRunId = latestRunId || c.runtimeRunId;
|
|
1767
|
+
c.pendingApprovals = [];
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1631
1770
|
finishConversationStream(conversationId);
|
|
1632
|
-
await persistConversationPendingApprovals(conversationId);
|
|
1633
1771
|
if (latestRunId) {
|
|
1634
1772
|
runOwners.delete(latestRunId);
|
|
1635
1773
|
runConversations.delete(latestRunId);
|
|
@@ -1932,6 +2070,7 @@ export const createRequestHandler = async (options?: {
|
|
|
1932
2070
|
createdAt: conversation.createdAt,
|
|
1933
2071
|
updatedAt: conversation.updatedAt,
|
|
1934
2072
|
messageCount: conversation.messages.length,
|
|
2073
|
+
hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
|
|
1935
2074
|
})),
|
|
1936
2075
|
});
|
|
1937
2076
|
return;
|
|
@@ -1957,40 +2096,93 @@ export const createRequestHandler = async (options?: {
|
|
|
1957
2096
|
const approvalMatch = pathname.match(/^\/api\/approvals\/([^/]+)$/);
|
|
1958
2097
|
if (approvalMatch && request.method === "POST") {
|
|
1959
2098
|
const approvalId = decodeURIComponent(approvalMatch[1] ?? "");
|
|
1960
|
-
const
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
conversation.pendingApprovals = next;
|
|
1975
|
-
await conversationStore.update(conversation);
|
|
1976
|
-
prunedStale = true;
|
|
1977
|
-
}
|
|
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;
|
|
1978
2113
|
}
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
if (!foundConversation || !foundApproval) {
|
|
1979
2117
|
writeJson(response, 404, {
|
|
1980
2118
|
code: "APPROVAL_NOT_FOUND",
|
|
1981
|
-
message:
|
|
1982
|
-
? "Approval request is no longer active"
|
|
1983
|
-
: "Approval request not found",
|
|
2119
|
+
message: "Approval request not found",
|
|
1984
2120
|
});
|
|
1985
2121
|
return;
|
|
1986
2122
|
}
|
|
1987
|
-
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
if (
|
|
1991
|
-
|
|
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;
|
|
1992
2136
|
}
|
|
1993
|
-
|
|
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
|
+
|
|
1994
2186
|
writeJson(response, 200, { ok: true, approvalId, approved });
|
|
1995
2187
|
return;
|
|
1996
2188
|
}
|
|
@@ -2056,34 +2248,19 @@ export const createRequestHandler = async (options?: {
|
|
|
2056
2248
|
}
|
|
2057
2249
|
if (request.method === "GET") {
|
|
2058
2250
|
const storedPending = Array.isArray(conversation.pendingApprovals)
|
|
2059
|
-
? conversation.pendingApprovals
|
|
2251
|
+
? conversation.pendingApprovals.map(a => ({
|
|
2252
|
+
approvalId: a.approvalId,
|
|
2253
|
+
runId: a.runId,
|
|
2254
|
+
tool: a.tool,
|
|
2255
|
+
input: a.input,
|
|
2256
|
+
}))
|
|
2060
2257
|
: [];
|
|
2061
|
-
const livePending = Array.from(pendingApprovals.entries())
|
|
2062
|
-
.filter(
|
|
2063
|
-
([, pending]) =>
|
|
2064
|
-
pending.ownerId === ownerId && pending.conversationId === conversationId,
|
|
2065
|
-
)
|
|
2066
|
-
.map(([approvalId, pending]) => ({
|
|
2067
|
-
approvalId,
|
|
2068
|
-
runId: pending.runId,
|
|
2069
|
-
tool: pending.tool,
|
|
2070
|
-
input: pending.input,
|
|
2071
|
-
}));
|
|
2072
|
-
const mergedPendingById = new Map<string, (typeof livePending)[number]>();
|
|
2073
|
-
for (const approval of storedPending) {
|
|
2074
|
-
if (approval && typeof approval.approvalId === "string") {
|
|
2075
|
-
mergedPendingById.set(approval.approvalId, approval);
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
for (const approval of livePending) {
|
|
2079
|
-
mergedPendingById.set(approval.approvalId, approval);
|
|
2080
|
-
}
|
|
2081
2258
|
const activeStream = conversationEventStreams.get(conversationId);
|
|
2082
2259
|
const hasActiveRun = !!activeStream && !activeStream.finished;
|
|
2083
2260
|
writeJson(response, 200, {
|
|
2084
2261
|
conversation: {
|
|
2085
2262
|
...conversation,
|
|
2086
|
-
pendingApprovals:
|
|
2263
|
+
pendingApprovals: storedPending,
|
|
2087
2264
|
},
|
|
2088
2265
|
hasActiveRun,
|
|
2089
2266
|
});
|
|
@@ -2269,6 +2446,7 @@ export const createRequestHandler = async (options?: {
|
|
|
2269
2446
|
let currentText = "";
|
|
2270
2447
|
let currentTools: string[] = [];
|
|
2271
2448
|
let runCancelled = false;
|
|
2449
|
+
let checkpointedRun = false;
|
|
2272
2450
|
let userContent: Message["content"] = messageText;
|
|
2273
2451
|
if (files.length > 0) {
|
|
2274
2452
|
try {
|
|
@@ -2424,17 +2602,40 @@ export const createRequestHandler = async (options?: {
|
|
|
2424
2602
|
currentTools.push(toolText);
|
|
2425
2603
|
await persistDraftAssistantTurn();
|
|
2426
2604
|
}
|
|
2427
|
-
if (event.type === "tool:approval:
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
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;
|
|
2438
2639
|
}
|
|
2439
2640
|
if (
|
|
2440
2641
|
event.type === "run:completed" &&
|
|
@@ -2459,29 +2660,31 @@ export const createRequestHandler = async (options?: {
|
|
|
2459
2660
|
if (currentText.length > 0) {
|
|
2460
2661
|
sections.push({ type: "text", content: currentText });
|
|
2461
2662
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
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
|
+
}
|
|
2485
2688
|
} catch (error) {
|
|
2486
2689
|
if (abortController.signal.aborted || runCancelled) {
|
|
2487
2690
|
const fallbackSections = [...sections];
|
|
@@ -2559,7 +2762,6 @@ export const createRequestHandler = async (options?: {
|
|
|
2559
2762
|
activeConversationRuns.delete(conversationId);
|
|
2560
2763
|
}
|
|
2561
2764
|
finishConversationStream(conversationId);
|
|
2562
|
-
await persistConversationPendingApprovals(conversationId);
|
|
2563
2765
|
if (latestRunId) {
|
|
2564
2766
|
runOwners.delete(latestRunId);
|
|
2565
2767
|
runConversations.delete(latestRunId);
|
|
@@ -3018,44 +3220,10 @@ export const runInteractive = async (
|
|
|
3018
3220
|
dotenv.config({ path: resolve(workingDir, ".env") });
|
|
3019
3221
|
const config = await loadPonchoConfig(workingDir);
|
|
3020
3222
|
|
|
3021
|
-
// Approval bridge: the harness calls this handler which creates a pending
|
|
3022
|
-
// promise. The Ink UI picks up the pending request and shows a Y/N prompt.
|
|
3023
|
-
// The user's response resolves the promise.
|
|
3024
|
-
type ApprovalRequest = {
|
|
3025
|
-
tool: string;
|
|
3026
|
-
input: Record<string, unknown>;
|
|
3027
|
-
approvalId: string;
|
|
3028
|
-
resolve: (approved: boolean) => void;
|
|
3029
|
-
};
|
|
3030
|
-
let pendingApproval: ApprovalRequest | null = null;
|
|
3031
|
-
let onApprovalRequest: ((req: ApprovalRequest) => void) | null = null;
|
|
3032
|
-
|
|
3033
|
-
const approvalHandler = async (request: {
|
|
3034
|
-
tool: string;
|
|
3035
|
-
input: Record<string, unknown>;
|
|
3036
|
-
runId: string;
|
|
3037
|
-
step: number;
|
|
3038
|
-
approvalId: string;
|
|
3039
|
-
}): Promise<boolean> => {
|
|
3040
|
-
return new Promise<boolean>((resolveApproval) => {
|
|
3041
|
-
const req: ApprovalRequest = {
|
|
3042
|
-
tool: request.tool,
|
|
3043
|
-
input: request.input,
|
|
3044
|
-
approvalId: request.approvalId,
|
|
3045
|
-
resolve: resolveApproval,
|
|
3046
|
-
};
|
|
3047
|
-
pendingApproval = req;
|
|
3048
|
-
if (onApprovalRequest) {
|
|
3049
|
-
onApprovalRequest(req);
|
|
3050
|
-
}
|
|
3051
|
-
});
|
|
3052
|
-
};
|
|
3053
|
-
|
|
3054
3223
|
const uploadStore = await createUploadStore(config?.uploads, workingDir);
|
|
3055
3224
|
const harness = new AgentHarness({
|
|
3056
3225
|
workingDir,
|
|
3057
3226
|
environment: resolveHarnessEnvironment(),
|
|
3058
|
-
approvalHandler,
|
|
3059
3227
|
uploadStore,
|
|
3060
3228
|
});
|
|
3061
3229
|
await harness.initialize();
|
|
@@ -3069,7 +3237,6 @@ export const runInteractive = async (
|
|
|
3069
3237
|
workingDir: string;
|
|
3070
3238
|
config?: PonchoConfig;
|
|
3071
3239
|
conversationStore: ConversationStore;
|
|
3072
|
-
onSetApprovalCallback?: (cb: (req: ApprovalRequest) => void) => void;
|
|
3073
3240
|
}) => Promise<void>
|
|
3074
3241
|
)({
|
|
3075
3242
|
harness,
|
|
@@ -3080,13 +3247,6 @@ export const runInteractive = async (
|
|
|
3080
3247
|
workingDir,
|
|
3081
3248
|
agentId: identity.id,
|
|
3082
3249
|
}),
|
|
3083
|
-
onSetApprovalCallback: (cb: (req: ApprovalRequest) => void) => {
|
|
3084
|
-
onApprovalRequest = cb;
|
|
3085
|
-
// If there's already a pending request, fire it immediately
|
|
3086
|
-
if (pendingApproval) {
|
|
3087
|
-
cb(pendingApproval);
|
|
3088
|
-
}
|
|
3089
|
-
},
|
|
3090
3250
|
});
|
|
3091
3251
|
} finally {
|
|
3092
3252
|
await harness.shutdown();
|