@truefoundry/assistant-ui-runtime 0.1.13 → 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/README.md +23 -0
- package/dist/{chunk-XYGEKRA2.js → chunk-S4SH2KT2.js} +1 -1
- package/dist/chunk-S4SH2KT2.js.map +1 -0
- package/dist/index.d.ts +23 -3
- package/dist/index.js +138 -11
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +2 -3
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/{types-BE-F3vCD.d.ts → types-VU3Fc17W.d.ts} +23 -5
- package/package.json +1 -1
- package/src/convertTurnMessages.test.ts +295 -1
- package/src/convertTurnMessages.ts +116 -13
- package/src/draft/useDraftAgentSpec.test.tsx +107 -0
- package/src/draft/useDraftAgentSpec.ts +38 -0
- package/src/hooks.ts +22 -0
- package/src/index.ts +6 -0
- package/src/plugins/truefoundry-agent-server-adapter/cp.ts +2 -1
- package/src/plugins/truefoundry-agent-server-adapter/types.ts +2 -2
- package/src/server/index.ts +3 -0
- package/src/server/types.ts +25 -3
- package/src/streamTurn.test.ts +18 -0
- package/src/streamTurn.ts +3 -3
- package/src/truefoundryExtras.ts +12 -0
- package/src/useTrueFoundryAgentMessages.test.tsx +115 -0
- package/src/useTrueFoundryAgentMessages.ts +38 -1
- package/src/useTrueFoundryAgentRuntime.ts +4 -0
- package/dist/chunk-XYGEKRA2.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AgentSessionClient } from 'truefoundry-gateway-sdk/agents';
|
|
2
2
|
import { PrivateAgentSessionClient } from 'truefoundry-gateway-sdk/agents/private';
|
|
3
|
-
import { A as AgentSpec,
|
|
3
|
+
import { A as AgentSpec, N as CreateSessionRequest, Y as ListSessionsParams, S as Session, d as Turn, aC as TurnState, a as AgentChatServer, aK as UpdateSessionRequest, k as AgentSelectorEntry, G as ConnectorSelectorEntry, a8 as ModelSelectorEntry, ax as SkillSelectorEntry, g as AgentBuilderServer } from '../../types-VU3Fc17W.js';
|
|
4
4
|
import { TruefoundryGatewayApi } from 'truefoundry-gateway-sdk';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -29,8 +29,7 @@ type RuntimeSkillMount = NonNullable<NonNullable<AgentSpec["skills"]>[number]>;
|
|
|
29
29
|
type RuntimeMcpServerMount = NonNullable<NonNullable<AgentSpec["mcpServers"]>[number]>;
|
|
30
30
|
type TfySkillMount = RuntimeSkillMount & TruefoundryGatewayApi.SkillMount;
|
|
31
31
|
type TfyMcpServerMount = RuntimeMcpServerMount & TruefoundryGatewayApi.McpServer;
|
|
32
|
-
interface TfyAgentSpec extends AgentSpec<TruefoundryGatewayApi.Model, TfySkillMount, TfyMcpServerMount> {
|
|
33
|
-
config?: TruefoundryGatewayApi.RuntimeConfig;
|
|
32
|
+
interface TfyAgentSpec extends AgentSpec<TruefoundryGatewayApi.Model, TfySkillMount, TfyMcpServerMount, TruefoundryGatewayApi.RuntimeConfig> {
|
|
34
33
|
responseFormat?: TruefoundryGatewayApi.ResponseFormat;
|
|
35
34
|
messages?: TruefoundryGatewayApi.AgentSpecUserMessage[];
|
|
36
35
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { aF as TurnStreamingEvent, aM as DeltaEvents, T as TurnEvent } from '../types-VU3Fc17W.js';
|
|
2
|
+
export { aN as ActionRequiredEvent, f as AgentBuilderCapabilitiesResponse, g as AgentBuilderServer, h as AgentCapabilityConfig, a as AgentChatServer, aO as AgentInfo, i as AgentLibraryEntry, aP as AgentParent, j as AgentRuntimeConfig, k as AgentSelectorEntry, l as AgentSkill, A as AgentSpec, m as AgentUIServer, n as AgentUIServerPort, o as ApprovalDecision, p as AuthenticateConnectorRequest, C as CatalogServer, aQ as ChunkDeltaToolCall, q as ConnectorAuth, r as ConnectorAuthApiKey, s as ConnectorAuthNone, t as ConnectorAuthOAuth, u as ConnectorAuthPublic, v as ConnectorAuthPublicApiKey, w as ConnectorAuthPublicNone, x as ConnectorAuthPublicOAuth, y as ConnectorAuthType, z as ConnectorAuthenticationResult, B as ConnectorBase, D as ConnectorCatalogEntry, E as ConnectorCatalogServer, F as ConnectorConfigBase, G as ConnectorSelectorEntry, H as ConnectorState, I as CreateConnectorRequest, J as CreateModelProviderRequest, K as CreateSandboxProviderRequest, L as CreateSandboxRequest, N as CreateSessionRequest, O as CreateSkillRequest, P as CreateSkillRequestBase, Q as DefinedSkill, R as GithubSkill, V as ImportGithubSkillRequest, W as ListResult, X as ListSessionsOrder, Y as ListSessionsParams, M as McpAuthRequiredEvent, aR as McpInitializeEvent, aS as McpServerAuthInfo, Z as McpServerMount, _ as Model, $ as ModelCatalogServer, a0 as ModelEntry, aT as ModelMessageContentPart, aU as ModelMessageDeltaEvent, a1 as ModelMessageEvent, a2 as ModelParams, a3 as ModelProperties, a4 as ModelProviderBase, a5 as ModelProviderCatalogEntry, a6 as ModelProviderConfigBase, a7 as ModelSelection, a8 as ModelSelectorEntry, a9 as PageParams, aa as PreviousTurnIdInput, ab as ProviderEntry, ac as ProviderType, ad as RegistrySkill, ae as SandboxBase, af as SandboxCatalogEntry, ag as SandboxCatalogServer, ah as SandboxConfig, ai as SandboxCreatedEvent, aj as SandboxProviderBase, ak as SandboxProviderCatalogEntry, al as SandboxProviderConfig, am as SaveAgentRequest, an as SaveAgentResult, ao as SearchAgentSelectorParams, ap as SearchAgentsParams, aq as SelectRegistrySkillRequest, S as Session, ar as SessionEventItem, as as SkillBase, at as SkillCatalogEntry, au as SkillCatalogServer, av as SkillConfigBase, aw as SkillMount, ax as SkillSelectorEntry, b as ThreadCreatedEvent, aV as ThreadDoneEvent, ay as ToolApprovalRequiredEvent, az as ToolBase, aA as ToolCall, aW as ToolCallFunction, aX as ToolCallRef, aY as ToolInfo, aZ as ToolResponseEvent, aB as ToolResponseRequiredEvent, d as Turn, a_ as TurnCreatedEvent, a$ as TurnDoneEvent, e as TurnInputItem, aC as TurnState, b0 as TurnStateCancelled, aD as TurnStateDone, b1 as TurnStateError, b2 as TurnStateRunning, aE as TurnStreamData, aG as UpdateConnectorRequest, aH as UpdateModelProviderRequest, aI as UpdateSandboxProviderRequest, aJ as UpdateSandboxRequest, aK as UpdateSessionRequest, aL as UserMessage, b3 as UserMessageContent, c as UserToolApprovalEvent, U as UserToolResponseEvent } from '../types-VU3Fc17W.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Local implementations of streaming delta helpers.
|
|
@@ -294,15 +294,24 @@ interface Model {
|
|
|
294
294
|
name: string;
|
|
295
295
|
params?: ModelParams;
|
|
296
296
|
}
|
|
297
|
+
interface AgentCapabilityConfig {
|
|
298
|
+
enabled?: boolean;
|
|
299
|
+
}
|
|
300
|
+
interface AgentRuntimeConfig {
|
|
301
|
+
generativeUi?: AgentCapabilityConfig;
|
|
302
|
+
dynamicSubAgents?: AgentCapabilityConfig;
|
|
303
|
+
askUserQuestions?: AgentCapabilityConfig;
|
|
304
|
+
}
|
|
297
305
|
/**
|
|
298
306
|
* SDK-owned agent definition — fields the FE reads/writes.
|
|
299
|
-
* Host widens `model` / `skills` / `mcpServers` via type params,
|
|
300
|
-
* extra fields via `TSpec extends AgentSpec<...>`.
|
|
307
|
+
* Host widens `model` / `skills` / `mcpServers` / `config` via type params,
|
|
308
|
+
* and adds extra fields via `TSpec extends AgentSpec<...>`.
|
|
301
309
|
*/
|
|
302
|
-
interface AgentSpec<TModel extends Model = Model, TSkill extends SkillMount = SkillMount, TMcp extends McpServerMount = McpServerMount> {
|
|
310
|
+
interface AgentSpec<TModel extends Model = Model, TSkill extends SkillMount = SkillMount, TMcp extends McpServerMount = McpServerMount, TConfig extends AgentRuntimeConfig = AgentRuntimeConfig> {
|
|
303
311
|
model: TModel;
|
|
304
312
|
skills?: TSkill[];
|
|
305
313
|
mcpServers?: TMcp[];
|
|
314
|
+
config?: TConfig;
|
|
306
315
|
instructions?: string;
|
|
307
316
|
variables?: Record<string, string>;
|
|
308
317
|
}
|
|
@@ -475,6 +484,15 @@ interface AgentChatServer<TSpec extends AgentSpec = AgentSpec, TSession extends
|
|
|
475
484
|
interface SaveAgentRequest<TSpec extends AgentSpec = AgentSpec> {
|
|
476
485
|
agentName: string;
|
|
477
486
|
agentSpec: TSpec;
|
|
487
|
+
intent: "create" | "update";
|
|
488
|
+
/** Current mutable session to update atomically with the named agent. */
|
|
489
|
+
sessionId?: string;
|
|
490
|
+
}
|
|
491
|
+
interface SaveAgentResult {
|
|
492
|
+
/** Immutable id allocated by the host registry. */
|
|
493
|
+
agentId?: string;
|
|
494
|
+
/** Timestamp returned when the active mutable session was updated. */
|
|
495
|
+
sessionUpdatedAt?: string;
|
|
478
496
|
}
|
|
479
497
|
/**
|
|
480
498
|
* Builder feature flags — atoms gate sandbox / skill / settings surfaces on these.
|
|
@@ -497,7 +515,7 @@ interface AgentBuilderCapabilitiesResponse {
|
|
|
497
515
|
* Builder catalog + persist port — atoms call these.
|
|
498
516
|
* Passed separately from the runtime's chat server.
|
|
499
517
|
*/
|
|
500
|
-
interface AgentBuilderServer<TSpec extends AgentSpec = AgentSpec, TModel extends ModelSelectorEntry = ModelSelectorEntry, TSkill extends SkillSelectorEntry = SkillSelectorEntry, TMcp extends ConnectorSelectorEntry = ConnectorSelectorEntry, TAgent extends AgentSelectorEntry = AgentSelectorEntry, TSave =
|
|
518
|
+
interface AgentBuilderServer<TSpec extends AgentSpec = AgentSpec, TModel extends ModelSelectorEntry = ModelSelectorEntry, TSkill extends SkillSelectorEntry = SkillSelectorEntry, TMcp extends ConnectorSelectorEntry = ConnectorSelectorEntry, TAgent extends AgentSelectorEntry = AgentSelectorEntry, TSave = SaveAgentResult, TCapabilities extends AgentBuilderCapabilitiesResponse = AgentBuilderCapabilitiesResponse> {
|
|
501
519
|
getCapabilities(): Promise<TCapabilities>;
|
|
502
520
|
getModels(): Promise<TModel[]>;
|
|
503
521
|
getSkills(): Promise<TSkill[]>;
|
|
@@ -815,4 +833,4 @@ type AgentUIServerPort<TChat extends AgentChatServer = AgentChatServer, TBuilder
|
|
|
815
833
|
/** Host-facing alias used by trueforge-ui. */
|
|
816
834
|
type AgentUIServer = AgentUIServerPort;
|
|
817
835
|
|
|
818
|
-
export type {
|
|
836
|
+
export type { ModelCatalogServer as $, AgentSpec as A, ConnectorBase as B, CatalogServer as C, ConnectorCatalogEntry as D, ConnectorCatalogServer as E, ConnectorConfigBase as F, ConnectorSelectorEntry as G, ConnectorState as H, CreateConnectorRequest as I, CreateModelProviderRequest as J, CreateSandboxProviderRequest as K, CreateSandboxRequest as L, McpAuthRequiredEvent as M, CreateSessionRequest as N, CreateSkillRequest as O, CreateSkillRequestBase as P, DefinedSkill as Q, GithubSkill as R, Session as S, TurnEvent as T, UserToolResponseEvent as U, ImportGithubSkillRequest as V, ListResult as W, ListSessionsOrder as X, ListSessionsParams as Y, McpServerMount as Z, Model as _, AgentChatServer as a, TurnDoneEvent as a$, ModelEntry as a0, ModelMessageEvent as a1, ModelParams as a2, ModelProperties as a3, ModelProviderBase as a4, ModelProviderCatalogEntry as a5, ModelProviderConfigBase as a6, ModelSelection as a7, ModelSelectorEntry as a8, PageParams as a9, ToolCall as aA, ToolResponseRequiredEvent as aB, TurnState as aC, TurnStateDone as aD, TurnStreamData as aE, TurnStreamingEvent as aF, UpdateConnectorRequest as aG, UpdateModelProviderRequest as aH, UpdateSandboxProviderRequest as aI, UpdateSandboxRequest as aJ, UpdateSessionRequest as aK, UserMessage as aL, DeltaEvents as aM, ActionRequiredEvent as aN, AgentInfo as aO, AgentParent as aP, ChunkDeltaToolCall as aQ, McpInitializeEvent as aR, McpServerAuthInfo as aS, ModelMessageContentPart as aT, ModelMessageDeltaEvent as aU, ThreadDoneEvent as aV, ToolCallFunction as aW, ToolCallRef as aX, ToolInfo as aY, ToolResponseEvent as aZ, TurnCreatedEvent as a_, PreviousTurnIdInput as aa, ProviderEntry as ab, ProviderType as ac, RegistrySkill as ad, SandboxBase as ae, SandboxCatalogEntry as af, SandboxCatalogServer as ag, SandboxConfig as ah, SandboxCreatedEvent as ai, SandboxProviderBase as aj, SandboxProviderCatalogEntry as ak, SandboxProviderConfig as al, SaveAgentRequest as am, SaveAgentResult as an, SearchAgentSelectorParams as ao, SearchAgentsParams as ap, SelectRegistrySkillRequest as aq, SessionEventItem as ar, SkillBase as as, SkillCatalogEntry as at, SkillCatalogServer as au, SkillConfigBase as av, SkillMount as aw, SkillSelectorEntry as ax, ToolApprovalRequiredEvent as ay, ToolBase as az, ThreadCreatedEvent as b, TurnStateCancelled as b0, TurnStateError as b1, TurnStateRunning as b2, UserMessageContent as b3, UserToolApprovalEvent as c, Turn as d, TurnInputItem as e, AgentBuilderCapabilitiesResponse as f, AgentBuilderServer as g, AgentCapabilityConfig as h, AgentLibraryEntry as i, AgentRuntimeConfig as j, AgentSelectorEntry as k, AgentSkill as l, AgentUIServer as m, AgentUIServerPort as n, ApprovalDecision as o, AuthenticateConnectorRequest as p, ConnectorAuth as q, ConnectorAuthApiKey as r, ConnectorAuthNone as s, ConnectorAuthOAuth as t, ConnectorAuthPublic as u, ConnectorAuthPublicApiKey as v, ConnectorAuthPublicNone as w, ConnectorAuthPublicOAuth as x, ConnectorAuthType as y, ConnectorAuthenticationResult as z };
|
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
|
/**
|