@truefoundry/assistant-ui-runtime 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +63 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/convertTurnMessages.test.ts +295 -1
- package/src/convertTurnMessages.ts +116 -13
package/package.json
CHANGED
|
@@ -2434,7 +2434,7 @@ describe("buildSnapshotFromSessionEvents", () => {
|
|
|
2434
2434
|
expect(progressSnapshots).toEqual([1, 2]);
|
|
2435
2435
|
});
|
|
2436
2436
|
|
|
2437
|
-
it("
|
|
2437
|
+
it("falls back to listTurns({ limit: 1 }) when listEvents omits the running tip", async () => {
|
|
2438
2438
|
const listTurns = vi.fn(async () => ({
|
|
2439
2439
|
data: [
|
|
2440
2440
|
{
|
|
@@ -2460,6 +2460,300 @@ describe("buildSnapshotFromSessionEvents", () => {
|
|
|
2460
2460
|
expect(snapshot.pendingUser?.content).toBe("now");
|
|
2461
2461
|
});
|
|
2462
2462
|
|
|
2463
|
+
it("attaches an open tip from events when listTurns is oldest-first and clears answered ask-user", async () => {
|
|
2464
|
+
// Trueforge listTurns is ASC: limit:1 returns the oldest done turn even
|
|
2465
|
+
// while a later tip is still running. Tip detection must use the open
|
|
2466
|
+
// turn.created in listEvents (which includes running turns).
|
|
2467
|
+
const oldestDoneTurn = {
|
|
2468
|
+
id: "t-pause",
|
|
2469
|
+
sessionId: SESSION_ID,
|
|
2470
|
+
state: {
|
|
2471
|
+
status: "done",
|
|
2472
|
+
requiredActions: [
|
|
2473
|
+
{
|
|
2474
|
+
id: "resp-req-1",
|
|
2475
|
+
type: "tool.response_required",
|
|
2476
|
+
threadId: ROOT_THREAD_ID,
|
|
2477
|
+
createdAt,
|
|
2478
|
+
toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
|
|
2479
|
+
},
|
|
2480
|
+
],
|
|
2481
|
+
completedAt: createdAt,
|
|
2482
|
+
},
|
|
2483
|
+
input: [{ type: "user.message", content: "please research" }],
|
|
2484
|
+
createdAt,
|
|
2485
|
+
} as unknown as Turn;
|
|
2486
|
+
|
|
2487
|
+
const runningTip = {
|
|
2488
|
+
id: "t-running",
|
|
2489
|
+
sessionId: SESSION_ID,
|
|
2490
|
+
state: { status: "running" },
|
|
2491
|
+
input: [
|
|
2492
|
+
{
|
|
2493
|
+
type: "user.tool_response",
|
|
2494
|
+
threadId: ROOT_THREAD_ID,
|
|
2495
|
+
toolCallId: "question-1",
|
|
2496
|
+
content: "AI-native / LLM-powered SaaS",
|
|
2497
|
+
},
|
|
2498
|
+
],
|
|
2499
|
+
createdAt,
|
|
2500
|
+
} as unknown as Turn;
|
|
2501
|
+
|
|
2502
|
+
const items: SessionEventItem[] = [
|
|
2503
|
+
{
|
|
2504
|
+
turnId: "t-pause",
|
|
2505
|
+
event: {
|
|
2506
|
+
type: "turn.created",
|
|
2507
|
+
id: "evt-c-pause",
|
|
2508
|
+
turnId: "t-pause",
|
|
2509
|
+
input: oldestDoneTurn.input,
|
|
2510
|
+
state: { status: "running" },
|
|
2511
|
+
createdAt,
|
|
2512
|
+
},
|
|
2513
|
+
},
|
|
2514
|
+
{
|
|
2515
|
+
turnId: "t-pause",
|
|
2516
|
+
event: modelMessage({
|
|
2517
|
+
id: "model-1",
|
|
2518
|
+
threadId: ROOT_THREAD_ID,
|
|
2519
|
+
content: "One calibration question:",
|
|
2520
|
+
toolCalls: [
|
|
2521
|
+
{
|
|
2522
|
+
id: "question-1",
|
|
2523
|
+
type: "function",
|
|
2524
|
+
function: {
|
|
2525
|
+
name: "ask_user_question",
|
|
2526
|
+
arguments: JSON.stringify({
|
|
2527
|
+
question: "Which direction should I focus on?",
|
|
2528
|
+
options: [
|
|
2529
|
+
"General B2B SaaS",
|
|
2530
|
+
"AI-native / LLM-powered SaaS",
|
|
2531
|
+
],
|
|
2532
|
+
}),
|
|
2533
|
+
},
|
|
2534
|
+
toolInfo: {
|
|
2535
|
+
type: "truefoundry-system",
|
|
2536
|
+
name: "ask_user_question",
|
|
2537
|
+
},
|
|
2538
|
+
},
|
|
2539
|
+
],
|
|
2540
|
+
}),
|
|
2541
|
+
},
|
|
2542
|
+
{
|
|
2543
|
+
turnId: "t-pause",
|
|
2544
|
+
event: responseRequired({
|
|
2545
|
+
id: "resp-req-1",
|
|
2546
|
+
threadId: ROOT_THREAD_ID,
|
|
2547
|
+
toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
|
|
2548
|
+
}),
|
|
2549
|
+
},
|
|
2550
|
+
{
|
|
2551
|
+
turnId: "t-pause",
|
|
2552
|
+
event: {
|
|
2553
|
+
type: "turn.done",
|
|
2554
|
+
id: "evt-d-pause",
|
|
2555
|
+
state: oldestDoneTurn.state as TurnDoneEvent["state"],
|
|
2556
|
+
createdAt,
|
|
2557
|
+
} as TurnDoneEvent,
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
turnId: "t-running",
|
|
2561
|
+
event: {
|
|
2562
|
+
type: "turn.created",
|
|
2563
|
+
id: "evt-c-running",
|
|
2564
|
+
turnId: "t-running",
|
|
2565
|
+
previousTurnId: "t-pause",
|
|
2566
|
+
input: runningTip.input,
|
|
2567
|
+
state: { status: "running" },
|
|
2568
|
+
createdAt,
|
|
2569
|
+
},
|
|
2570
|
+
},
|
|
2571
|
+
{
|
|
2572
|
+
turnId: "t-running",
|
|
2573
|
+
event: modelMessage({
|
|
2574
|
+
id: "model-2",
|
|
2575
|
+
threadId: ROOT_THREAD_ID,
|
|
2576
|
+
content: "Locked in — starting deep research.",
|
|
2577
|
+
}),
|
|
2578
|
+
},
|
|
2579
|
+
];
|
|
2580
|
+
|
|
2581
|
+
const listTurns = vi.fn(async () => ({
|
|
2582
|
+
data: [oldestDoneTurn],
|
|
2583
|
+
nextPageToken: "eyJvZmZzZXQiOjF9",
|
|
2584
|
+
}));
|
|
2585
|
+
const getTurn = vi.fn(async () => runningTip);
|
|
2586
|
+
const server = {
|
|
2587
|
+
listTurns,
|
|
2588
|
+
getTurn,
|
|
2589
|
+
listEvents: sessionEventsPage(items),
|
|
2590
|
+
listTurnEvents: async () => ({ data: [] }),
|
|
2591
|
+
} as unknown as AgentChatServer;
|
|
2592
|
+
|
|
2593
|
+
const snapshot = await buildSnapshotFromSessionEvents(server, SESSION_ID);
|
|
2594
|
+
|
|
2595
|
+
expect(getTurn).toHaveBeenCalledWith({
|
|
2596
|
+
sessionId: SESSION_ID,
|
|
2597
|
+
turnId: "t-running",
|
|
2598
|
+
});
|
|
2599
|
+
expect(listTurns).not.toHaveBeenCalled();
|
|
2600
|
+
expect(snapshot.runningTurn?.id).toBe("t-running");
|
|
2601
|
+
expect(snapshot.unstable_resume).toBe(true);
|
|
2602
|
+
|
|
2603
|
+
const messages = projectSessionMessages(snapshot);
|
|
2604
|
+
expect(collectPendingToolResponses(messages)).toHaveLength(0);
|
|
2605
|
+
|
|
2606
|
+
const assistant = messages.find((message) => message.role === "assistant");
|
|
2607
|
+
expect(assistant).toBeDefined();
|
|
2608
|
+
const toolCall = assistant?.content.find(
|
|
2609
|
+
(part) => part.type === "tool-call" && part.toolCallId === "question-1",
|
|
2610
|
+
);
|
|
2611
|
+
expect(toolCall).toMatchObject({
|
|
2612
|
+
type: "tool-call",
|
|
2613
|
+
toolCallId: "question-1",
|
|
2614
|
+
result: "AI-native / LLM-powered SaaS",
|
|
2615
|
+
});
|
|
2616
|
+
if (toolCall?.type === "tool-call") {
|
|
2617
|
+
expect(toolCall.interrupt).toBeUndefined();
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
|
|
2621
|
+
it("keeps an answered ask-user answered when the open tip finished before getTurn", async () => {
|
|
2622
|
+
// The tip can complete between listEvents and getTurn. There is nothing
|
|
2623
|
+
// left to resume, but its user.tool_response input never reached the
|
|
2624
|
+
// fold via ingestion (no turn.done in the window), so it must still be
|
|
2625
|
+
// applied — otherwise the answered prompt resurfaces as pending.
|
|
2626
|
+
const finishedTip = {
|
|
2627
|
+
id: "t-tip",
|
|
2628
|
+
sessionId: SESSION_ID,
|
|
2629
|
+
state: { status: "done", requiredActions: [], completedAt: createdAt },
|
|
2630
|
+
input: [
|
|
2631
|
+
{
|
|
2632
|
+
type: "user.tool_response",
|
|
2633
|
+
threadId: ROOT_THREAD_ID,
|
|
2634
|
+
toolCallId: "question-1",
|
|
2635
|
+
content: "AI-native / LLM-powered SaaS",
|
|
2636
|
+
},
|
|
2637
|
+
],
|
|
2638
|
+
createdAt,
|
|
2639
|
+
} as unknown as Turn;
|
|
2640
|
+
|
|
2641
|
+
const items: SessionEventItem[] = [
|
|
2642
|
+
{
|
|
2643
|
+
turnId: "t-pause",
|
|
2644
|
+
event: {
|
|
2645
|
+
type: "turn.created",
|
|
2646
|
+
id: "evt-c-pause",
|
|
2647
|
+
turnId: "t-pause",
|
|
2648
|
+
input: [{ type: "user.message", content: "please research" }],
|
|
2649
|
+
state: { status: "running" },
|
|
2650
|
+
createdAt,
|
|
2651
|
+
},
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
turnId: "t-pause",
|
|
2655
|
+
event: modelMessage({
|
|
2656
|
+
id: "model-1",
|
|
2657
|
+
threadId: ROOT_THREAD_ID,
|
|
2658
|
+
content: "One calibration question:",
|
|
2659
|
+
toolCalls: [
|
|
2660
|
+
{
|
|
2661
|
+
id: "question-1",
|
|
2662
|
+
type: "function",
|
|
2663
|
+
function: {
|
|
2664
|
+
name: "ask_user_question",
|
|
2665
|
+
arguments: JSON.stringify({
|
|
2666
|
+
question: "Which direction should I focus on?",
|
|
2667
|
+
options: [
|
|
2668
|
+
"General B2B SaaS",
|
|
2669
|
+
"AI-native / LLM-powered SaaS",
|
|
2670
|
+
],
|
|
2671
|
+
}),
|
|
2672
|
+
},
|
|
2673
|
+
toolInfo: {
|
|
2674
|
+
type: "truefoundry-system",
|
|
2675
|
+
name: "ask_user_question",
|
|
2676
|
+
},
|
|
2677
|
+
},
|
|
2678
|
+
],
|
|
2679
|
+
}),
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
turnId: "t-pause",
|
|
2683
|
+
event: responseRequired({
|
|
2684
|
+
id: "resp-req-1",
|
|
2685
|
+
threadId: ROOT_THREAD_ID,
|
|
2686
|
+
toolCalls: [{ id: "question-1", sourceEventId: "model-1" }],
|
|
2687
|
+
}),
|
|
2688
|
+
},
|
|
2689
|
+
{
|
|
2690
|
+
turnId: "t-pause",
|
|
2691
|
+
event: {
|
|
2692
|
+
type: "turn.done",
|
|
2693
|
+
id: "evt-d-pause",
|
|
2694
|
+
state: {
|
|
2695
|
+
status: "done",
|
|
2696
|
+
requiredActions: [
|
|
2697
|
+
{
|
|
2698
|
+
id: "resp-req-1",
|
|
2699
|
+
type: "tool.response_required",
|
|
2700
|
+
threadId: ROOT_THREAD_ID,
|
|
2701
|
+
createdAt,
|
|
2702
|
+
toolCalls: [
|
|
2703
|
+
{ id: "question-1", sourceEventId: "model-1" },
|
|
2704
|
+
],
|
|
2705
|
+
},
|
|
2706
|
+
],
|
|
2707
|
+
completedAt: createdAt,
|
|
2708
|
+
},
|
|
2709
|
+
createdAt,
|
|
2710
|
+
} as unknown as TurnDoneEvent,
|
|
2711
|
+
},
|
|
2712
|
+
{
|
|
2713
|
+
turnId: "t-tip",
|
|
2714
|
+
event: {
|
|
2715
|
+
type: "turn.created",
|
|
2716
|
+
id: "evt-c-tip",
|
|
2717
|
+
turnId: "t-tip",
|
|
2718
|
+
previousTurnId: "t-pause",
|
|
2719
|
+
input: finishedTip.input,
|
|
2720
|
+
state: { status: "running" },
|
|
2721
|
+
createdAt,
|
|
2722
|
+
},
|
|
2723
|
+
},
|
|
2724
|
+
];
|
|
2725
|
+
|
|
2726
|
+
const listTurns = vi.fn(async () => ({ data: [], nextPageToken: undefined }));
|
|
2727
|
+
const getTurn = vi.fn(async () => finishedTip);
|
|
2728
|
+
const server = {
|
|
2729
|
+
listTurns,
|
|
2730
|
+
getTurn,
|
|
2731
|
+
listEvents: sessionEventsPage(items),
|
|
2732
|
+
listTurnEvents: async () => ({ data: [] }),
|
|
2733
|
+
} as unknown as AgentChatServer;
|
|
2734
|
+
|
|
2735
|
+
const snapshot = await buildSnapshotFromSessionEvents(server, SESSION_ID);
|
|
2736
|
+
|
|
2737
|
+
expect(snapshot.runningTurn).toBeUndefined();
|
|
2738
|
+
expect(snapshot.unstable_resume).toBeFalsy();
|
|
2739
|
+
|
|
2740
|
+
const messages = projectSessionMessages(snapshot);
|
|
2741
|
+
expect(collectPendingToolResponses(messages)).toHaveLength(0);
|
|
2742
|
+
|
|
2743
|
+
const assistant = messages.find((message) => message.role === "assistant");
|
|
2744
|
+
const toolCall = assistant?.content.find(
|
|
2745
|
+
(part) => part.type === "tool-call" && part.toolCallId === "question-1",
|
|
2746
|
+
);
|
|
2747
|
+
expect(toolCall).toMatchObject({
|
|
2748
|
+
type: "tool-call",
|
|
2749
|
+
toolCallId: "question-1",
|
|
2750
|
+
result: "AI-native / LLM-powered SaaS",
|
|
2751
|
+
});
|
|
2752
|
+
if (toolCall?.type === "tool-call") {
|
|
2753
|
+
expect(toolCall.interrupt).toBeUndefined();
|
|
2754
|
+
}
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2463
2757
|
it("loads only the newest event page and exposes an older-history cursor", async () => {
|
|
2464
2758
|
const makeTurnItems = (id: string, text: string): SessionEventItem[] => [
|
|
2465
2759
|
{
|
|
@@ -516,10 +516,6 @@ function attachRunningTurn(
|
|
|
516
516
|
if (runningTurn == null) {
|
|
517
517
|
return snapshot;
|
|
518
518
|
}
|
|
519
|
-
// Session-level history excludes the running turn. Apply continuation inputs
|
|
520
|
-
// from the turn listing so answered approvals / ask-user prompts are not
|
|
521
|
-
// restored as pending while reconnecting to that turn after a refresh.
|
|
522
|
-
applyUserToolResponsesToFold(snapshot.fold, runningTurn.input ?? []);
|
|
523
519
|
const pendingUserText = extractTurnUserText(runningTurn.input);
|
|
524
520
|
return replaceSessionSnapshot(snapshot, {
|
|
525
521
|
runningTurn,
|
|
@@ -537,13 +533,117 @@ function attachRunningTurn(
|
|
|
537
533
|
});
|
|
538
534
|
}
|
|
539
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Last `turn.created` in ASC event order with no following `turn.done` — the
|
|
538
|
+
* open tip of the active branch in this window.
|
|
539
|
+
*/
|
|
540
|
+
function findOpenTurnCreated(
|
|
541
|
+
itemsAsc: readonly GatewaySessionEventItem[],
|
|
542
|
+
): { turnId: string; event: TurnCreatedEvent } | undefined {
|
|
543
|
+
let open: { turnId: string; event: TurnCreatedEvent } | undefined;
|
|
544
|
+
for (const item of itemsAsc) {
|
|
545
|
+
if (item.event.type === "turn.created") {
|
|
546
|
+
open = { turnId: item.turnId, event: item.event };
|
|
547
|
+
} else if (item.event.type === "turn.done") {
|
|
548
|
+
open = undefined;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return open;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function turnFromCreatedEvent(options: {
|
|
555
|
+
sessionId: string;
|
|
556
|
+
turnId: string;
|
|
557
|
+
event: TurnCreatedEvent;
|
|
558
|
+
}): Turn {
|
|
559
|
+
const { sessionId, turnId, event } = options;
|
|
560
|
+
return {
|
|
561
|
+
id: turnId,
|
|
562
|
+
sessionId,
|
|
563
|
+
state: { status: "running" },
|
|
564
|
+
createdAt: event.createdAt,
|
|
565
|
+
...(event.input != null ? { input: event.input } : {}),
|
|
566
|
+
...(event.previousTurnId === undefined
|
|
567
|
+
? {}
|
|
568
|
+
: { previousTurnId: event.previousTurnId }),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
type SessionTip = {
|
|
573
|
+
/** Turn to resume and subscribe to; absent once the tip has finished. */
|
|
574
|
+
runningTurn?: Turn;
|
|
575
|
+
/**
|
|
576
|
+
* Tip input that event ingestion could not apply, because it only folds a
|
|
577
|
+
* turn's input once that turn's `turn.done` arrives. Answered ask-user
|
|
578
|
+
* prompts and approvals live here, so this must be folded even when the tip
|
|
579
|
+
* is no longer running.
|
|
580
|
+
*/
|
|
581
|
+
continuationInput: readonly TurnInputItem[];
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Resolves the tip turn of the active branch for resume/subscribe.
|
|
586
|
+
*
|
|
587
|
+
* Prefer an open tip from the events window (works when listTurns is
|
|
588
|
+
* oldest-first and when listEvents includes the running turn). Fall back to
|
|
589
|
+
* `listTurns({ limit: 1 })` for hosts that omit the running turn from
|
|
590
|
+
* listEvents and put the tip first.
|
|
591
|
+
*/
|
|
592
|
+
async function resolveSessionTip(options: {
|
|
593
|
+
server: AgentChatServer;
|
|
594
|
+
sessionId: string;
|
|
595
|
+
itemsAsc: readonly GatewaySessionEventItem[];
|
|
596
|
+
}): Promise<SessionTip> {
|
|
597
|
+
const { server, sessionId, itemsAsc } = options;
|
|
598
|
+
const open = findOpenTurnCreated(itemsAsc);
|
|
599
|
+
if (open != null) {
|
|
600
|
+
const continuationInput = open.event.input ?? [];
|
|
601
|
+
if (typeof server.getTurn === "function") {
|
|
602
|
+
try {
|
|
603
|
+
const turn = await server.getTurn({
|
|
604
|
+
sessionId,
|
|
605
|
+
turnId: open.turnId,
|
|
606
|
+
});
|
|
607
|
+
if (turn.state.status === "running") {
|
|
608
|
+
return {
|
|
609
|
+
runningTurn: turn,
|
|
610
|
+
continuationInput: turn.input ?? continuationInput,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
// Tip finished between listEvents and getTurn: nothing to
|
|
614
|
+
// resume, but its answers still belong in the fold.
|
|
615
|
+
return { continuationInput: turn.input ?? continuationInput };
|
|
616
|
+
} catch {
|
|
617
|
+
// getTurn failed; synthesize from the open turn.created below.
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
runningTurn: turnFromCreatedEvent({
|
|
622
|
+
sessionId,
|
|
623
|
+
turnId: open.turnId,
|
|
624
|
+
event: open.event,
|
|
625
|
+
}),
|
|
626
|
+
continuationInput,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Hosts that exclude the running turn from listEvents still surface it as
|
|
631
|
+
// the first row of listTurns when that API is tip-first.
|
|
632
|
+
const turnsPage = await server.listTurns({ sessionId, limit: 1 });
|
|
633
|
+
const tip = turnsPage.data[0] as Turn | undefined;
|
|
634
|
+
return tip?.state?.status === "running"
|
|
635
|
+
? { runningTurn: tip, continuationInput: tip.input ?? [] }
|
|
636
|
+
: { continuationInput: [] };
|
|
637
|
+
}
|
|
638
|
+
|
|
540
639
|
/**
|
|
541
640
|
* Builds a session snapshot using the session-level `listEvents` API.
|
|
542
641
|
*
|
|
543
642
|
* Loads the newest event page (extending only when a page boundary splits a
|
|
544
643
|
* turn group), then leaves older pages for `prependOlderSessionHistory`.
|
|
545
|
-
*
|
|
546
|
-
*
|
|
644
|
+
* Detects a currently-running tip from an open `turn.created` in that window
|
|
645
|
+
* when present; otherwise falls back to `listTurns({ limit: 1 })` for hosts
|
|
646
|
+
* that omit the running turn from listEvents.
|
|
547
647
|
*
|
|
548
648
|
* `onProgress` is called after each complete turn is ingested so callers can
|
|
549
649
|
* update the UI progressively while the processing loop runs.
|
|
@@ -553,12 +653,6 @@ export async function buildSnapshotFromSessionEvents(
|
|
|
553
653
|
sessionId: string,
|
|
554
654
|
onProgress?: (snap: SessionSnapshot) => void,
|
|
555
655
|
): Promise<SessionSnapshot> {
|
|
556
|
-
// Detect a running turn with a single listTurns page — do not drain pagination.
|
|
557
|
-
const turnsPage = await server.listTurns({ sessionId, limit: 1 });
|
|
558
|
-
const newestTurn = turnsPage.data[0] as Turn | undefined;
|
|
559
|
-
const runningTurn =
|
|
560
|
-
newestTurn?.state?.status === "running" ? newestTurn : undefined;
|
|
561
|
-
|
|
562
656
|
const window = await fetchSessionEventsWindow(server, sessionId);
|
|
563
657
|
const historyPagination: SessionHistoryPagination = {
|
|
564
658
|
hasOlder: window.hasOlder,
|
|
@@ -575,7 +669,16 @@ export async function buildSnapshotFromSessionEvents(
|
|
|
575
669
|
historyPagination,
|
|
576
670
|
});
|
|
577
671
|
|
|
578
|
-
|
|
672
|
+
const tip = await resolveSessionTip({
|
|
673
|
+
server,
|
|
674
|
+
sessionId,
|
|
675
|
+
itemsAsc: window.itemsAsc,
|
|
676
|
+
});
|
|
677
|
+
// The tip has no turn.done in this window, so ingestion never folded its
|
|
678
|
+
// input. Apply it here so answered approvals / ask-user prompts are not
|
|
679
|
+
// restored as pending after a refresh.
|
|
680
|
+
applyUserToolResponsesToFold(withHistory.fold, tip.continuationInput);
|
|
681
|
+
return attachRunningTurn(withHistory, tip.runningTurn);
|
|
579
682
|
}
|
|
580
683
|
|
|
581
684
|
/**
|