@poncho-ai/cli 0.32.4 → 0.32.5
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 +7 -7
- package/CHANGELOG.md +6 -0
- package/dist/{chunk-F6NA3N2R.js → chunk-LVWNWMNE.js} +744 -510
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +81 -3
- package/dist/index.js +3 -1
- package/dist/{run-interactive-ink-GPCI4L6G.js → run-interactive-ink-VGKSZJDO.js} +1 -1
- package/package.json +3 -3
- package/src/index.ts +745 -464
- package/src/web-ui-client.ts +70 -8
- package/test/run-orchestration.test.ts +171 -0
package/src/web-ui-client.ts
CHANGED
|
@@ -32,6 +32,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
32
32
|
todoPanelCollapsed: false,
|
|
33
33
|
cronSectionCollapsed: true,
|
|
34
34
|
cronShowAll: false,
|
|
35
|
+
subagentPollInFlight: {},
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
const agentInitial = document.body.dataset.agentInitial || "A";
|
|
@@ -491,11 +492,20 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
491
492
|
return null;
|
|
492
493
|
}
|
|
493
494
|
const toolName = item && typeof item.tool === "string" ? item.tool : "tool";
|
|
495
|
+
const decision =
|
|
496
|
+
item && typeof item.decision === "string" ? item.decision : null;
|
|
497
|
+
const isResolved = decision === "approved" || decision === "denied";
|
|
494
498
|
return {
|
|
495
499
|
approvalId,
|
|
496
500
|
tool: toolName,
|
|
497
501
|
input: item?.input ?? {},
|
|
498
|
-
state: "pending",
|
|
502
|
+
state: isResolved ? "resolved" : "pending",
|
|
503
|
+
resolvedDecision:
|
|
504
|
+
decision === "approved"
|
|
505
|
+
? "approve"
|
|
506
|
+
: decision === "denied"
|
|
507
|
+
? "deny"
|
|
508
|
+
: null,
|
|
499
509
|
};
|
|
500
510
|
})
|
|
501
511
|
.filter(Boolean);
|
|
@@ -1545,8 +1555,8 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
1545
1555
|
updateContextRing();
|
|
1546
1556
|
renderMessages(state.activeMessages, payload.hasActiveRun);
|
|
1547
1557
|
}
|
|
1548
|
-
if (payload.hasActiveRun) {
|
|
1549
|
-
if (window._connectBrowserStream) window._connectBrowserStream();
|
|
1558
|
+
if (payload.hasActiveRun || payload.hasRunningSubagents) {
|
|
1559
|
+
if (payload.hasActiveRun && window._connectBrowserStream) window._connectBrowserStream();
|
|
1550
1560
|
setTimeout(poll, 2000);
|
|
1551
1561
|
} else {
|
|
1552
1562
|
setStreaming(false);
|
|
@@ -1561,9 +1571,15 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
1561
1571
|
};
|
|
1562
1572
|
|
|
1563
1573
|
const pollForSubagentResults = (conversationId) => {
|
|
1574
|
+
if (state.subagentPollInFlight[conversationId]) return;
|
|
1575
|
+
state.subagentPollInFlight[conversationId] = true;
|
|
1564
1576
|
let lastMessageCount = state.activeMessages ? state.activeMessages.length : 0;
|
|
1577
|
+
let lastUpdatedAt = 0;
|
|
1565
1578
|
const poll = async () => {
|
|
1566
|
-
if (state.activeConversationId !== conversationId)
|
|
1579
|
+
if (state.activeConversationId !== conversationId) {
|
|
1580
|
+
delete state.subagentPollInFlight[conversationId];
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1567
1583
|
try {
|
|
1568
1584
|
var payload = await api("/api/conversations/" + encodeURIComponent(conversationId));
|
|
1569
1585
|
if (state.activeConversationId !== conversationId) return;
|
|
@@ -1584,13 +1600,17 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
1584
1600
|
});
|
|
1585
1601
|
});
|
|
1586
1602
|
}
|
|
1587
|
-
|
|
1603
|
+
const conversationUpdatedAt =
|
|
1604
|
+
typeof payload.conversation.updatedAt === "number" ? payload.conversation.updatedAt : 0;
|
|
1605
|
+
if (messages.length > lastMessageCount || conversationUpdatedAt > lastUpdatedAt) {
|
|
1588
1606
|
lastMessageCount = messages.length;
|
|
1607
|
+
lastUpdatedAt = conversationUpdatedAt;
|
|
1589
1608
|
state.activeMessages = hydratePendingApprovals(messages, allPending);
|
|
1590
1609
|
renderMessages(state.activeMessages, payload.hasActiveRun || payload.hasRunningSubagents);
|
|
1591
1610
|
}
|
|
1592
1611
|
if (payload.hasActiveRun) {
|
|
1593
1612
|
// Parent agent started its continuation run — switch to live stream
|
|
1613
|
+
delete state.subagentPollInFlight[conversationId];
|
|
1594
1614
|
setStreaming(true);
|
|
1595
1615
|
state.activeMessages = hydratePendingApprovals(messages, allPending);
|
|
1596
1616
|
streamConversationEvents(conversationId, { liveOnly: true }).finally(() => {
|
|
@@ -1603,6 +1623,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
1603
1623
|
} else {
|
|
1604
1624
|
renderMessages(state.activeMessages, false);
|
|
1605
1625
|
await loadConversations();
|
|
1626
|
+
delete state.subagentPollInFlight[conversationId];
|
|
1606
1627
|
}
|
|
1607
1628
|
}
|
|
1608
1629
|
} catch {
|
|
@@ -2530,6 +2551,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2530
2551
|
let _maxSteps = 0;
|
|
2531
2552
|
let _receivedTerminalEvent = false;
|
|
2532
2553
|
let _shouldContinue = false;
|
|
2554
|
+
let _pendingSubagentsConversation = null;
|
|
2533
2555
|
|
|
2534
2556
|
// Helper to read an SSE stream from a fetch response
|
|
2535
2557
|
const readSseStream = async (response) => {
|
|
@@ -2847,7 +2869,7 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2847
2869
|
assistantMessage.content = String(payload.result?.response || "");
|
|
2848
2870
|
}
|
|
2849
2871
|
if (payload.pendingSubagents) {
|
|
2850
|
-
|
|
2872
|
+
_pendingSubagentsConversation = conversationId;
|
|
2851
2873
|
}
|
|
2852
2874
|
renderIfActiveConversation(false);
|
|
2853
2875
|
}
|
|
@@ -2975,6 +2997,19 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
2975
2997
|
}
|
|
2976
2998
|
elements.prompt.focus();
|
|
2977
2999
|
}
|
|
3000
|
+
|
|
3001
|
+
// Subagent callback: after sendMessage fully completes (including
|
|
3002
|
+
// finally cleanup), reload the conversation. loadConversation is
|
|
3003
|
+
// the exact same code path as a manual refresh — if the callback
|
|
3004
|
+
// is still running it connects to the event stream; if it already
|
|
3005
|
+
// finished it just renders the final persisted state.
|
|
3006
|
+
if (_pendingSubagentsConversation && state.activeConversationId === _pendingSubagentsConversation) {
|
|
3007
|
+
const cbConvId = _pendingSubagentsConversation;
|
|
3008
|
+
await new Promise(r => setTimeout(r, 1200));
|
|
3009
|
+
if (state.activeConversationId === cbConvId && !state.isStreaming) {
|
|
3010
|
+
await loadConversation(cbConvId);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
2978
3013
|
};
|
|
2979
3014
|
|
|
2980
3015
|
const requireAuth = async () => {
|
|
@@ -3208,9 +3243,22 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
3208
3243
|
}));
|
|
3209
3244
|
return api("/api/approvals/" + encodeURIComponent(approvalId), {
|
|
3210
3245
|
method: "POST",
|
|
3211
|
-
body: JSON.stringify({
|
|
3246
|
+
body: JSON.stringify({
|
|
3247
|
+
approved: decision === "approve",
|
|
3248
|
+
conversationId: state.activeConversationId || undefined,
|
|
3249
|
+
}),
|
|
3212
3250
|
}).catch((error) => {
|
|
3213
3251
|
const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
|
|
3252
|
+
const isNotReady = error && error.payload && error.payload.code === "APPROVAL_NOT_READY";
|
|
3253
|
+
if (isNotReady) {
|
|
3254
|
+
updatePendingApproval(approvalId, (request) => ({
|
|
3255
|
+
...request,
|
|
3256
|
+
state: "pending",
|
|
3257
|
+
pendingDecision: null,
|
|
3258
|
+
resolvedDecision: null,
|
|
3259
|
+
}));
|
|
3260
|
+
return;
|
|
3261
|
+
}
|
|
3214
3262
|
if (isStale) {
|
|
3215
3263
|
updatePendingApproval(approvalId, () => null);
|
|
3216
3264
|
} else {
|
|
@@ -3281,9 +3329,23 @@ export const getWebUiClientScript = (markedSource: string): string => `
|
|
|
3281
3329
|
for (const aid of pending) {
|
|
3282
3330
|
await api("/api/approvals/" + encodeURIComponent(aid), {
|
|
3283
3331
|
method: "POST",
|
|
3284
|
-
body: JSON.stringify({
|
|
3332
|
+
body: JSON.stringify({
|
|
3333
|
+
approved: decision === "approve",
|
|
3334
|
+
conversationId: state.activeConversationId || undefined,
|
|
3335
|
+
}),
|
|
3285
3336
|
}).catch((error) => {
|
|
3286
3337
|
const isStale = error && error.payload && error.payload.code === "APPROVAL_NOT_FOUND";
|
|
3338
|
+
const isNotReady = error && error.payload && error.payload.code === "APPROVAL_NOT_READY";
|
|
3339
|
+
if (isNotReady) {
|
|
3340
|
+
updatePendingApproval(aid, (request) => ({
|
|
3341
|
+
...request,
|
|
3342
|
+
state: "pending",
|
|
3343
|
+
pendingDecision: null,
|
|
3344
|
+
resolvedDecision: null,
|
|
3345
|
+
}));
|
|
3346
|
+
renderMessages(state.activeMessages, state.isStreaming);
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3287
3349
|
if (isStale) {
|
|
3288
3350
|
updatePendingApproval(aid, () => null);
|
|
3289
3351
|
} else {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { AgentEvent, Message } from "@poncho-ai/sdk";
|
|
3
|
+
import { __internalRunOrchestration } from "../src/index.js";
|
|
4
|
+
|
|
5
|
+
const baseMessages: Message[] = [{ role: "user", content: "hello" }];
|
|
6
|
+
|
|
7
|
+
describe("run orchestration helpers", () => {
|
|
8
|
+
it("falls back to user messages when harness history is unavailable", () => {
|
|
9
|
+
const conversation = {
|
|
10
|
+
messages: baseMessages,
|
|
11
|
+
_harnessMessages: "invalid",
|
|
12
|
+
};
|
|
13
|
+
const resolved = __internalRunOrchestration.loadRunHistory(
|
|
14
|
+
conversation as unknown as { messages: Message[]; _harnessMessages?: unknown; _continuationMessages?: unknown },
|
|
15
|
+
);
|
|
16
|
+
expect(resolved.source).toBe("messages");
|
|
17
|
+
expect(resolved.shouldRebuildCanonical).toBe(true);
|
|
18
|
+
expect(resolved.messages).toEqual(baseMessages);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("prefers continuation history when requested", () => {
|
|
22
|
+
const continuation: Message[] = [{ role: "assistant", content: "next" }];
|
|
23
|
+
const conversation = {
|
|
24
|
+
messages: baseMessages,
|
|
25
|
+
_harnessMessages: baseMessages,
|
|
26
|
+
_continuationMessages: continuation,
|
|
27
|
+
};
|
|
28
|
+
const resolved = __internalRunOrchestration.loadRunHistory(
|
|
29
|
+
conversation as unknown as { messages: Message[]; _harnessMessages?: unknown; _continuationMessages?: unknown },
|
|
30
|
+
{ preferContinuation: true },
|
|
31
|
+
);
|
|
32
|
+
expect(resolved.source).toBe("continuation");
|
|
33
|
+
expect(resolved.messages).toEqual(continuation);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("normalizes legacy approval checkpoint payloads", () => {
|
|
37
|
+
const normalized = __internalRunOrchestration.normalizeApprovalCheckpoint(
|
|
38
|
+
{
|
|
39
|
+
approvalId: "approval_1",
|
|
40
|
+
runId: "run_1",
|
|
41
|
+
tool: "read_file",
|
|
42
|
+
toolCallId: "tool_1",
|
|
43
|
+
input: {},
|
|
44
|
+
checkpointMessages: undefined,
|
|
45
|
+
baseMessageCount: -1,
|
|
46
|
+
pendingToolCalls: [{ id: "t2", name: "list_directory", input: {} }, { foo: "bar" }],
|
|
47
|
+
} as unknown as {
|
|
48
|
+
approvalId: string;
|
|
49
|
+
runId: string;
|
|
50
|
+
tool: string;
|
|
51
|
+
toolCallId?: string;
|
|
52
|
+
input: Record<string, unknown>;
|
|
53
|
+
checkpointMessages?: Message[];
|
|
54
|
+
baseMessageCount?: number;
|
|
55
|
+
pendingToolCalls?: Array<{ id: string; name: string; input: Record<string, unknown> }>;
|
|
56
|
+
decision?: "approved" | "denied";
|
|
57
|
+
},
|
|
58
|
+
baseMessages,
|
|
59
|
+
);
|
|
60
|
+
expect(normalized.checkpointMessages).toEqual(baseMessages);
|
|
61
|
+
expect(normalized.baseMessageCount).toBe(0);
|
|
62
|
+
expect(normalized.pendingToolCalls).toEqual([{ id: "t2", name: "list_directory", input: {} }]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("builds checkpoint approvals with a canonical shape", () => {
|
|
66
|
+
const checkpoints = __internalRunOrchestration.buildApprovalCheckpoints({
|
|
67
|
+
approvals: [
|
|
68
|
+
{
|
|
69
|
+
approvalId: "approval_1",
|
|
70
|
+
tool: "read_file",
|
|
71
|
+
toolCallId: "tool_1",
|
|
72
|
+
input: { path: "README.md" },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
runId: "run_1",
|
|
76
|
+
checkpointMessages: baseMessages,
|
|
77
|
+
baseMessageCount: 2,
|
|
78
|
+
pendingToolCalls: [{ id: "tool_2", name: "list_directory", input: {} }],
|
|
79
|
+
});
|
|
80
|
+
expect(checkpoints).toEqual([
|
|
81
|
+
{
|
|
82
|
+
approvalId: "approval_1",
|
|
83
|
+
runId: "run_1",
|
|
84
|
+
tool: "read_file",
|
|
85
|
+
toolCallId: "tool_1",
|
|
86
|
+
input: { path: "README.md" },
|
|
87
|
+
checkpointMessages: baseMessages,
|
|
88
|
+
baseMessageCount: 2,
|
|
89
|
+
pendingToolCalls: [{ id: "tool_2", name: "list_directory", input: {} }],
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("resolves run request by preferring continuation and preserves rebuild signal", () => {
|
|
95
|
+
const continuation: Message[] = [{ role: "assistant", content: "continuation" }];
|
|
96
|
+
const resolved = __internalRunOrchestration.resolveRunRequest(
|
|
97
|
+
{
|
|
98
|
+
messages: baseMessages,
|
|
99
|
+
_harnessMessages: "invalid",
|
|
100
|
+
_continuationMessages: continuation,
|
|
101
|
+
} as unknown as { messages: Message[]; _harnessMessages?: unknown; _continuationMessages?: unknown },
|
|
102
|
+
{
|
|
103
|
+
conversationId: "conv_1",
|
|
104
|
+
messages: baseMessages,
|
|
105
|
+
preferContinuation: true,
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
expect(resolved.source).toBe("continuation");
|
|
109
|
+
expect(resolved.messages).toEqual(continuation);
|
|
110
|
+
expect(resolved.shouldRebuildCanonical).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("records standard draft events for tool timeline and text", () => {
|
|
114
|
+
const draft = __internalRunOrchestration.createTurnDraftState();
|
|
115
|
+
__internalRunOrchestration.recordStandardTurnEvent(
|
|
116
|
+
draft,
|
|
117
|
+
{ type: "tool:started", runId: "run_1", step: 1, tool: "read_file", input: {} } as AgentEvent,
|
|
118
|
+
);
|
|
119
|
+
__internalRunOrchestration.recordStandardTurnEvent(
|
|
120
|
+
draft,
|
|
121
|
+
{ type: "tool:completed", runId: "run_1", step: 1, tool: "read_file", duration: 42, output: {} } as AgentEvent,
|
|
122
|
+
);
|
|
123
|
+
__internalRunOrchestration.recordStandardTurnEvent(
|
|
124
|
+
draft,
|
|
125
|
+
{ type: "model:chunk", runId: "run_1", step: 1, content: "done" } as AgentEvent,
|
|
126
|
+
);
|
|
127
|
+
expect(draft.toolTimeline).toEqual(["- start `read_file`", "- done `read_file` (42ms)"]);
|
|
128
|
+
expect(draft.assistantResponse).toBe("done");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("executes a conversation turn through the shared executor", async () => {
|
|
132
|
+
const events: AgentEvent[] = [
|
|
133
|
+
{ type: "run:started", runId: "run_1", startedAt: Date.now() } as AgentEvent,
|
|
134
|
+
{ type: "tool:started", runId: "run_1", step: 1, tool: "list_directory", input: {} } as AgentEvent,
|
|
135
|
+
{ type: "model:chunk", runId: "run_1", step: 1, content: "hello" } as AgentEvent,
|
|
136
|
+
{
|
|
137
|
+
type: "run:completed",
|
|
138
|
+
runId: "run_1",
|
|
139
|
+
result: {
|
|
140
|
+
status: "completed",
|
|
141
|
+
response: "hello",
|
|
142
|
+
steps: 2,
|
|
143
|
+
duration: 10,
|
|
144
|
+
continuation: false,
|
|
145
|
+
continuationMessages: baseMessages,
|
|
146
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
147
|
+
contextTokens: 321,
|
|
148
|
+
contextWindow: 1000,
|
|
149
|
+
},
|
|
150
|
+
} as AgentEvent,
|
|
151
|
+
];
|
|
152
|
+
const fakeHarness = {
|
|
153
|
+
async *runWithTelemetry() {
|
|
154
|
+
for (const event of events) yield event;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const seenTypes: string[] = [];
|
|
158
|
+
const result = await __internalRunOrchestration.executeConversationTurn({
|
|
159
|
+
harness: fakeHarness as never,
|
|
160
|
+
runInput: { task: "test", messages: baseMessages, conversationId: "conv_1" } as never,
|
|
161
|
+
onEvent: (event) => {
|
|
162
|
+
seenTypes.push(event.type);
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
expect(result.latestRunId).toBe("run_1");
|
|
166
|
+
expect(result.runSteps).toBe(2);
|
|
167
|
+
expect(result.runContextTokens).toBe(321);
|
|
168
|
+
expect(result.draft.assistantResponse).toBe("hello");
|
|
169
|
+
expect(seenTypes).toEqual(["run:started", "tool:started", "model:chunk", "run:completed"]);
|
|
170
|
+
});
|
|
171
|
+
});
|