@truefoundry/assistant-ui-runtime 0.1.5 → 0.1.6

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.
Files changed (70) hide show
  1. package/README.md +198 -401
  2. package/dist/chunk-3A2EPLQG.js +93 -0
  3. package/dist/chunk-3A2EPLQG.js.map +1 -0
  4. package/dist/chunk-Q2SHKMLM.js +270 -0
  5. package/dist/chunk-Q2SHKMLM.js.map +1 -0
  6. package/dist/index.d.ts +24 -29
  7. package/dist/index.js +264 -237
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +162 -0
  10. package/dist/plugins/truefoundry-agent-server-adapter/index.js +19 -0
  11. package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -0
  12. package/dist/server/index.d.ts +17 -0
  13. package/dist/server/index.js +9 -0
  14. package/dist/server/index.js.map +1 -0
  15. package/dist/types-BfiFf8O1.d.ts +468 -0
  16. package/package.json +19 -6
  17. package/src/askUserQuestion.ts +3 -3
  18. package/src/buildEditedUserMessageContent.test.ts +2 -2
  19. package/src/collectPending.ts +1 -1
  20. package/src/convertTurnMessages.test.ts +141 -196
  21. package/src/convertTurnMessages.ts +130 -76
  22. package/src/createSubAgent.ts +1 -1
  23. package/src/draftAgentConfig.test.ts +26 -29
  24. package/src/extractTurnUserText.ts +1 -1
  25. package/src/foldPeerThreads.test.ts +1 -1
  26. package/src/foldPeerThreads.ts +3 -2
  27. package/src/index.ts +80 -4
  28. package/src/listPages.ts +21 -0
  29. package/src/loadSessionSnapshot.test.ts +9 -8
  30. package/src/loadSessionSnapshot.ts +9 -14
  31. package/src/mcpAuth.ts +6 -3
  32. package/src/messageCustomMetadata.ts +1 -1
  33. package/src/modelMessageContent.ts +1 -1
  34. package/src/modelMessageImageContent.test.ts +1 -1
  35. package/src/modelMessageImageContent.ts +7 -6
  36. package/src/plugins/truefoundry-agent-server-adapter/README.md +178 -0
  37. package/src/plugins/truefoundry-agent-server-adapter/guards.test.ts +113 -0
  38. package/src/plugins/truefoundry-agent-server-adapter/guards.ts +130 -0
  39. package/src/plugins/truefoundry-agent-server-adapter/index.ts +359 -0
  40. package/src/plugins/truefoundry-agent-server-adapter/types.ts +135 -0
  41. package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
  42. package/src/private/agentSpec.ts +8 -3
  43. package/src/private/draftSessionBridge.ts +14 -13
  44. package/src/private/truefoundryDraftThreadListAdapter.test.ts +44 -49
  45. package/src/private/truefoundryDraftThreadListAdapter.ts +22 -16
  46. package/src/requiredActionInputs.ts +1 -1
  47. package/src/requiredActionsFromActiveUpdate.test.ts +1 -1
  48. package/src/server/eventUtils.ts +120 -0
  49. package/src/server/events.ts +246 -0
  50. package/src/server/index.ts +66 -0
  51. package/src/server/types.ts +319 -0
  52. package/src/sessionSnapshot.ts +1 -1
  53. package/src/sessions.ts +5 -21
  54. package/src/streamTurn.test.ts +172 -155
  55. package/src/streamTurn.ts +51 -48
  56. package/src/toolApproval.ts +4 -4
  57. package/src/toolResponse.ts +4 -4
  58. package/src/truefoundryExtras.ts +1 -1
  59. package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +26 -29
  60. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +18 -23
  61. package/src/truefoundryThreadListAdapter.test.ts +16 -18
  62. package/src/truefoundryThreadListAdapter.ts +7 -7
  63. package/src/turnEventHelpers.ts +1 -1
  64. package/src/types.ts +2 -16
  65. package/src/useTrueFoundryAgentMessages.test.tsx +38 -70
  66. package/src/useTrueFoundryAgentMessages.ts +32 -44
  67. package/src/useTrueFoundryAgentRuntime.ts +11 -28
  68. package/src/private/bindDraftAgentSession.test.ts +0 -54
  69. package/src/private/bindDraftAgentSession.ts +0 -28
  70. package/src/private/getGatewayFromPrivateClient.ts +0 -13
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Compile-time assertions for the mount types in `types.ts`.
3
+ *
4
+ * TfySkillMount / TfyMcpServerMount are built by indexing into the runtime's
5
+ * exported AgentSpec. If that indexed access ever degrades to `unknown`, the
6
+ * intersection becomes a silent no-op and the gateway fields disappear without
7
+ * any error. These assertions fail `pnpm typecheck` if that happens.
8
+ *
9
+ * Not an entry point — tsup only bundles src/index.ts.
10
+ */
11
+
12
+ import type { AgentSpec } from "../../server/types.js";
13
+ import type { createTrueFoundryChatServer } from "./index.js";
14
+ import type {
15
+ TfyAgentSpec,
16
+ TfyFinishReason,
17
+ TfyMcpServerMount,
18
+ TfySkillMount,
19
+ TfySubject,
20
+ TfyTurn,
21
+ } from "./types.js";
22
+
23
+ // Both gateway source variants must survive the intersection with the base.
24
+ export const registrySkill: TfySkillMount = {
25
+ type: "truefoundry-skills-registry",
26
+ fqn: "tfy:skill:v1",
27
+ preload: true,
28
+ };
29
+
30
+ export const gitSkill: TfySkillMount = {
31
+ type: "git",
32
+ url: "https://github.com/acme/skills",
33
+ name: "reviewer",
34
+ ref: "main",
35
+ };
36
+
37
+ // @ts-expect-error the type discriminant must survive the intersection
38
+ export const skillWithoutType: TfySkillMount = { fqn: "a" };
39
+
40
+ // @ts-expect-error registry variant requires fqn
41
+ export const registrySkillWithoutFqn: TfySkillMount = {
42
+ type: "truefoundry-skills-registry",
43
+ };
44
+
45
+ export const registeredMcp: TfyMcpServerMount = {
46
+ name: "github",
47
+ type: "truefoundry-mcp-registry",
48
+ enableTools: ["@read-only"],
49
+ requireApprovalForTools: ["@destructive"],
50
+ };
51
+
52
+ export const inlineMcp: TfyMcpServerMount = {
53
+ name: "custom",
54
+ type: "inline",
55
+ url: "https://example.com/mcp",
56
+ };
57
+
58
+ // @ts-expect-error the type discriminant must survive the intersection
59
+ export const mcpWithoutType: TfyMcpServerMount = { name: "a" };
60
+
61
+ // @ts-expect-error inline variant requires url
62
+ export const inlineMcpWithoutUrl: TfyMcpServerMount = {
63
+ name: "a",
64
+ type: "inline",
65
+ };
66
+
67
+ // The gateway's own spec — a registry skill carries neither `id` nor `name` —
68
+ // must satisfy the runtime base. A base that rejects it makes every server
69
+ // response unassignable, which is the regression these mounts are guarding.
70
+ declare const gatewayShapedSpec: TfyAgentSpec;
71
+ export const asRuntimeSpec: AgentSpec = gatewayShapedSpec;
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Generic TSpec — a host extension must reach the returned server's types,
75
+ // and the bare (defaulted) form must keep working.
76
+ // ---------------------------------------------------------------------------
77
+
78
+ interface HostSpec extends TfyAgentSpec {
79
+ workspaceId: string;
80
+ }
81
+
82
+ type HostServer = ReturnType<typeof createTrueFoundryChatServer<HostSpec>>;
83
+ type DefaultServer = ReturnType<typeof createTrueFoundryChatServer>;
84
+
85
+ declare const hostSession: Awaited<ReturnType<HostServer["getSession"]>>;
86
+ declare const defaultSession: Awaited<ReturnType<DefaultServer["getSession"]>>;
87
+
88
+ // Host field is visible on the spec returned by the generic server.
89
+ export const workspaceId: string | undefined = hostSession.agentSpec?.workspaceId;
90
+
91
+ // Gateway fields still flow through alongside it.
92
+ export const hostSkills: TfySkillMount[] | undefined = hostSession.agentSpec?.skills;
93
+
94
+ // @ts-expect-error the default instantiation must NOT have the host's field
95
+ export const leakedWorkspaceId = defaultSession.agentSpec?.workspaceId;
96
+
97
+ // Host spec is accepted where the server expects one.
98
+ export declare function createHostSession(
99
+ server: HostServer,
100
+ spec: HostSpec,
101
+ ): ReturnType<HostServer["createSession"]>;
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Turn — the gateway narrowings must not collapse back into the runtime's
105
+ // `unknown` output / bare-`string` reason.
106
+ // ---------------------------------------------------------------------------
107
+
108
+ const subject: TfySubject = {
109
+ subjectId: "u-1",
110
+ subjectType: "user",
111
+ subjectSlug: "someone",
112
+ };
113
+
114
+ export const cancelledTurn: TfyTurn = {
115
+ id: "turn-1",
116
+ sessionId: "session-1",
117
+ state: {
118
+ status: "cancelled",
119
+ reason: "cancelled-for-next-turn",
120
+ completedAt: "2026-01-01T00:00:00Z",
121
+ },
122
+ createdBySubject: subject,
123
+ createdAt: "2026-01-01T00:00:00Z",
124
+ };
125
+
126
+ export const freeformCancelReason: TfyTurn = {
127
+ ...cancelledTurn,
128
+ // @ts-expect-error reason is one of four gateway values, not any string
129
+ state: { status: "cancelled", reason: "nope", completedAt: "2026-01-01T00:00:00Z" },
130
+ };
131
+
132
+ // @ts-expect-error createdBySubject is required — the gateway always sends it
133
+ export const turnWithoutSubject: TfyTurn = {
134
+ id: "turn-1",
135
+ sessionId: "session-1",
136
+ state: { status: "running" },
137
+ createdAt: "2026-01-01T00:00:00Z",
138
+ };
139
+
140
+ declare const doneTurn: Extract<TfyTurn["state"], { status: "done" }>;
141
+
142
+ // `output` is the gateway's model message; on the runtime base this is `unknown`
143
+ // and would not permit a property access at all.
144
+ export const doneFinishReason: TfyFinishReason | null | undefined =
145
+ doneTurn.output?.finishReason;
146
+ export const doneTokens: number | undefined = doneTurn.output?.usage?.inputTokens;
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Session / request params — gateway-only fields reach the server methods.
150
+ // ---------------------------------------------------------------------------
151
+
152
+ export const sessionSubject: TfySubject = hostSession.createdBySubject;
153
+
154
+ export const withMetadata: Parameters<DefaultServer["createSession"]>[0] = {
155
+ agentName: "agent",
156
+ tfyMetadata: JSON.stringify({ tenant: "acme" }),
157
+ };
158
+
159
+ export const withEndTimestamp: NonNullable<
160
+ Parameters<DefaultServer["listSessions"]>[0]
161
+ > = {
162
+ startTimestamp: "2026-01-01T00:00:00Z",
163
+ endTimestamp: "2026-02-01T00:00:00Z",
164
+ };
@@ -1,7 +1,12 @@
1
- import type { TruefoundryGatewayApi } from "truefoundry-gateway-sdk";
1
+ import type { AgentSpec } from "../server/types.js";
2
2
 
3
- export type AgentSpec = TruefoundryGatewayApi.AgentSpec;
4
- export type DraftSession = TruefoundryGatewayApi.DraftSession;
3
+ export type { AgentSpec } from "../server/types.js";
4
+
5
+ /** @deprecated Use Session with isMutable: true instead. Kept for title helper. */
6
+ export type DraftSession = {
7
+ title?: string | null;
8
+ agentSpec: AgentSpec;
9
+ };
5
10
 
6
11
  export type AgentSpecUpdate = {
7
12
  instructions?: string;
@@ -1,7 +1,5 @@
1
- import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
2
-
1
+ import type { AgentChatServer } from "../server/types.js";
3
2
  import type { AgentSpec } from "./agentSpec.js";
4
- import { getGatewayFromPrivateClient } from "./getGatewayFromPrivateClient.js";
5
3
 
6
4
  export const DRAFT_SESSION_LAST_UPDATED_AT_HEADER = "x-tfy-session-last-updated-at";
7
5
 
@@ -11,22 +9,25 @@ export type DraftSessionBridge = {
11
9
  };
12
10
 
13
11
  export function createDraftSessionBridge(
14
- privateClient: PrivateAgentSessionClient,
12
+ server: AgentChatServer,
15
13
  ): DraftSessionBridge {
16
14
  return {
17
15
  async getDraftAgentSpec(draftSessionId) {
18
- const draft = await privateClient.getDraftSession({ draftSessionId });
19
- return draft.agentSpec;
16
+ const session = await server.getSession({ sessionId: draftSessionId });
17
+ if (session.agentSpec == null) {
18
+ throw new Error(
19
+ `Session ${draftSessionId} has no agentSpec (isMutable=${session.isMutable}).`,
20
+ );
21
+ }
22
+ return session.agentSpec;
20
23
  },
21
24
 
22
25
  async syncAgentSpec(draftSessionId, agentSpec) {
23
- // PrivateAgentSessionClient does not wrap update yet — use the low-level client.
24
- const gateway = getGatewayFromPrivateClient(privateClient);
25
- const response = await gateway.agents.private.draftSessions.update(
26
- draftSessionId,
27
- { agentSpec },
28
- );
29
- return response.data.updatedAt;
26
+ const updated = await server.updateSession({
27
+ sessionId: draftSessionId,
28
+ agentSpec,
29
+ });
30
+ return updated.updatedAt;
30
31
  },
31
32
  };
32
33
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
3
+ import type { AgentChatServer, Session } from "../server/index.js";
4
4
 
5
5
  import { createTrueFoundryDraftThreadListAdapter } from "./truefoundryDraftThreadListAdapter.js";
6
6
  import type { AgentSpec } from "./agentSpec.js";
@@ -10,62 +10,57 @@ const defaultAgentSpec: AgentSpec = {
10
10
  instructions: "You are helpful.",
11
11
  };
12
12
 
13
- function mockDraft(id: string, title: string | undefined, updatedAt: string) {
13
+ function mockDraft(id: string, title: string | undefined, updatedAt: string): Session {
14
14
  return {
15
- type: "session/draft" as const,
16
15
  id,
17
16
  agentSpec: defaultAgentSpec,
18
17
  title,
19
- createdBySubject: { type: "user" as const, id: "u1" },
20
18
  createdAt: updatedAt,
21
19
  updatedAt,
20
+ isMutable: true,
22
21
  };
23
22
  }
24
23
 
25
- function mockDraftListPage(
26
- drafts: ReturnType<typeof mockDraft>[],
27
- nextPageToken?: string,
28
- ) {
24
+ function mockDraftListPage(drafts: Session[], nextPageToken?: string) {
29
25
  return {
30
26
  data: drafts,
31
- response: {
32
- pagination: {
33
- nextPageToken,
34
- limit: 20,
35
- },
36
- },
27
+ ...(nextPageToken != null ? { nextPageToken } : {}),
37
28
  };
38
29
  }
39
30
 
31
+ function mockServer(partial: Partial<AgentChatServer>): AgentChatServer {
32
+ return partial as AgentChatServer;
33
+ }
34
+
40
35
  describe("createTrueFoundryDraftThreadListAdapter", () => {
41
36
  it("lists draft sessions with pagination cursor", async () => {
42
- const listDraftSessions = vi.fn().mockResolvedValue(
37
+ const listSessions = vi.fn().mockResolvedValue(
43
38
  mockDraftListPage(
44
39
  [mockDraft("d1", "My draft", "2026-06-30T10:00:00.000Z")],
45
40
  "page-2",
46
41
  ),
47
42
  );
48
- const privateClient = {
49
- listDraftSessions,
50
- createDraftSession: vi.fn(),
51
- getDraftSession: vi.fn(),
52
- } as unknown as PrivateAgentSessionClient;
43
+ const server = mockServer({
44
+ listSessions,
45
+ createSession: vi.fn(),
46
+ getSession: vi.fn(),
47
+ });
53
48
 
54
49
  const adapter = createTrueFoundryDraftThreadListAdapter({
55
- privateClient,
50
+ server,
56
51
  defaultAgentSpec,
57
52
  });
58
53
 
59
54
  const result = await adapter.list();
60
55
 
61
- expect(listDraftSessions).toHaveBeenCalledWith(
56
+ expect(listSessions).toHaveBeenCalledWith(
62
57
  expect.objectContaining({
63
58
  limit: 20,
64
59
  pageToken: undefined,
65
60
  startTimestamp: expect.any(String),
66
61
  }),
67
62
  );
68
- expect(listDraftSessions).toHaveBeenCalledWith(
63
+ expect(listSessions).toHaveBeenCalledWith(
69
64
  expect.not.objectContaining({ agentName: expect.anything() }),
70
65
  );
71
66
  expect(result.threads).toEqual([
@@ -80,23 +75,23 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
80
75
  });
81
76
 
82
77
  it("creates a draft session on initialize", async () => {
83
- const createDraftSession = vi.fn().mockResolvedValue(
78
+ const createSession = vi.fn().mockResolvedValue(
84
79
  mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
85
80
  );
86
- const privateClient = {
87
- listDraftSessions: vi.fn(),
88
- createDraftSession,
89
- getDraftSession: vi.fn(),
90
- } as unknown as PrivateAgentSessionClient;
81
+ const server = mockServer({
82
+ listSessions: vi.fn(),
83
+ createSession,
84
+ getSession: vi.fn(),
85
+ });
91
86
 
92
87
  const adapter = createTrueFoundryDraftThreadListAdapter({
93
- privateClient,
88
+ server,
94
89
  defaultAgentSpec,
95
90
  });
96
91
 
97
92
  const result = await adapter.initialize("local-thread-id");
98
93
 
99
- expect(createDraftSession).toHaveBeenCalledWith({ agentSpec: defaultAgentSpec });
94
+ expect(createSession).toHaveBeenCalledWith({ agentSpec: defaultAgentSpec });
100
95
  expect(result).toEqual({ remoteId: "d-new", externalId: undefined });
101
96
  });
102
97
 
@@ -104,47 +99,47 @@ describe("createTrueFoundryDraftThreadListAdapter", () => {
104
99
  const liveAgentSpec: AgentSpec = {
105
100
  model: { name: "anthropic/claude-opus-4-8" },
106
101
  instructions: "You are helpful.",
107
- mcpServers: [{ type: "truefoundry-mcp-registry", name: "github", enableTools: ["@all"] }],
108
- skills: [{ fqn: "acme/skill-a:1", preload: false }],
102
+ mcpServers: [{ id: "github", name: "github" }],
103
+ skills: [{ id: "skill-a", name: "skill-a" }],
109
104
  };
110
- const createDraftSession = vi.fn().mockResolvedValue(
105
+ const createSession = vi.fn().mockResolvedValue(
111
106
  mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
112
107
  );
113
- const privateClient = {
114
- listDraftSessions: vi.fn(),
115
- createDraftSession,
116
- getDraftSession: vi.fn(),
117
- } as unknown as PrivateAgentSessionClient;
108
+ const server = mockServer({
109
+ listSessions: vi.fn(),
110
+ createSession,
111
+ getSession: vi.fn(),
112
+ });
118
113
 
119
114
  const adapter = createTrueFoundryDraftThreadListAdapter({
120
- privateClient,
115
+ server,
121
116
  defaultAgentSpec,
122
117
  getAgentSpec: () => liveAgentSpec,
123
118
  });
124
119
 
125
120
  await adapter.initialize("local-thread-id");
126
121
 
127
- expect(createDraftSession).toHaveBeenCalledWith({ agentSpec: liveAgentSpec });
122
+ expect(createSession).toHaveBeenCalledWith({ agentSpec: liveAgentSpec });
128
123
  });
129
124
 
130
125
  it("falls back to model name for title when draft has no title", async () => {
131
- const getDraftSession = vi.fn().mockResolvedValue(
126
+ const getSession = vi.fn().mockResolvedValue(
132
127
  mockDraft("d1", undefined, "2026-06-30T10:00:00.000Z"),
133
128
  );
134
- const privateClient = {
135
- listDraftSessions: vi.fn(),
136
- createDraftSession: vi.fn(),
137
- getDraftSession,
138
- } as unknown as PrivateAgentSessionClient;
129
+ const server = mockServer({
130
+ listSessions: vi.fn(),
131
+ createSession: vi.fn(),
132
+ getSession,
133
+ });
139
134
 
140
135
  const adapter = createTrueFoundryDraftThreadListAdapter({
141
- privateClient,
136
+ server,
142
137
  defaultAgentSpec,
143
138
  });
144
139
 
145
140
  const result = await adapter.fetch("d1");
146
141
 
147
- expect(getDraftSession).toHaveBeenCalledWith({ draftSessionId: "d1" });
142
+ expect(getSession).toHaveBeenCalledWith({ sessionId: "d1" });
148
143
  expect(result.title).toBe("anthropic/claude-sonnet-4-6");
149
144
  });
150
145
  });
@@ -1,52 +1,58 @@
1
1
  import type { RemoteThreadListAdapter } from "@assistant-ui/core";
2
- import type { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/private";
3
2
 
3
+ import type { AgentChatServer } from "../server/types.js";
4
4
  import { draftSessionTitle, type AgentSpec } from "./agentSpec.js";
5
5
  import { sessionListStartTimestamp } from "../sessionListStartTimestamp.js";
6
6
 
7
7
  const THREAD_LIST_PAGE_SIZE = 20;
8
8
 
9
9
  export function createTrueFoundryDraftThreadListAdapter(options: {
10
- privateClient: PrivateAgentSessionClient;
10
+ server: AgentChatServer;
11
11
  defaultAgentSpec: AgentSpec;
12
12
  getAgentSpec?: () => AgentSpec;
13
13
  }): RemoteThreadListAdapter {
14
- const { privateClient, defaultAgentSpec, getAgentSpec } = options;
14
+ const { server, defaultAgentSpec, getAgentSpec } = options;
15
15
 
16
16
  return {
17
17
  async list({ after } = {}) {
18
- const page = await privateClient.listDraftSessions({
18
+ const page = await server.listSessions({
19
19
  limit: THREAD_LIST_PAGE_SIZE,
20
20
  pageToken: after,
21
21
  startTimestamp: sessionListStartTimestamp(),
22
22
  });
23
- const threads = page.data.map((draft) => ({
24
- status: "regular" as const,
25
- remoteId: draft.id,
26
- title: draftSessionTitle(draft),
27
- lastMessageAt: new Date(draft.updatedAt),
28
- }));
23
+ const threads = page.data
24
+ .filter((session) => session.isMutable)
25
+ .map((draft) => ({
26
+ status: "regular" as const,
27
+ remoteId: draft.id,
28
+ title: draftSessionTitle({
29
+ title: draft.title,
30
+ agentSpec: draft.agentSpec ?? defaultAgentSpec,
31
+ }),
32
+ lastMessageAt: new Date(draft.updatedAt),
33
+ }));
29
34
  return {
30
35
  threads,
31
- nextCursor: page.response.pagination.nextPageToken ?? undefined,
36
+ nextCursor: page.nextPageToken ?? undefined,
32
37
  };
33
38
  },
34
39
 
35
40
  async initialize(_threadId: string) {
36
- const draft = await privateClient.createDraftSession({
41
+ const draft = await server.createSession({
37
42
  agentSpec: getAgentSpec?.() ?? defaultAgentSpec,
38
43
  });
39
44
  return { remoteId: draft.id, externalId: undefined };
40
45
  },
41
46
 
42
47
  async fetch(remoteId) {
43
- const draft = await privateClient.getDraftSession({
44
- draftSessionId: remoteId,
45
- });
48
+ const draft = await server.getSession({ sessionId: remoteId });
46
49
  return {
47
50
  status: "regular" as const,
48
51
  remoteId: draft.id,
49
- title: draftSessionTitle(draft),
52
+ title: draftSessionTitle({
53
+ title: draft.title,
54
+ agentSpec: draft.agentSpec ?? defaultAgentSpec,
55
+ }),
50
56
  lastMessageAt: new Date(draft.updatedAt),
51
57
  };
52
58
  },
@@ -3,7 +3,7 @@ import type {
3
3
  TurnInputItem,
4
4
  UserToolApprovalEvent,
5
5
  UserToolResponseEvent,
6
- } from "truefoundry-gateway-sdk/agents";
6
+ } from "./server/index.js";
7
7
 
8
8
  import { ROOT_THREAD_ID } from "./constants.js";
9
9
  import {
@@ -64,7 +64,7 @@ describe("requiredActionsFromActiveUpdate", () => {
64
64
  };
65
65
 
66
66
  const actions = requiredActionsFromActiveUpdate(update);
67
- expect(actions.map((action) => action.type)).toEqual([
67
+ expect(actions?.map((action) => action.type)).toEqual([
68
68
  "tool.approval_required",
69
69
  "tool.response_required",
70
70
  ]);
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Local implementations of streaming delta helpers.
3
+ * Formerly imported from truefoundry-gateway-sdk/agents.
4
+ */
5
+
6
+ import type {
7
+ DeltaEvents,
8
+ ModelMessageDeltaEvent,
9
+ ModelMessageEvent,
10
+ ToolCall,
11
+ ToolInfo,
12
+ TurnEvent,
13
+ TurnStreamingEvent,
14
+ } from "./events.js";
15
+
16
+ /** True for `.delta` streaming events. */
17
+ export function isEventDelta(event: TurnStreamingEvent): event is DeltaEvents {
18
+ return typeof event.type === "string" && event.type.endsWith(".delta");
19
+ }
20
+
21
+ /**
22
+ * Merge `delta` into `base` in place (same `id` required).
23
+ * Currently handles `model.message.delta` → `model.message`.
24
+ */
25
+ export function mergeEventDelta(base: TurnEvent, delta: DeltaEvents): void {
26
+ if (base.id !== delta.id) {
27
+ throw new Error(
28
+ `Cannot merge delta into a different event: base id "${base.id}" != delta id "${delta.id}".`,
29
+ );
30
+ }
31
+ if (delta.type === "model.message.delta" && base.type === "model.message") {
32
+ mergeModelMessageDelta(base, delta);
33
+ }
34
+ }
35
+
36
+ function asToolInfo(value: unknown): ToolInfo | undefined {
37
+ if (value == null || typeof value !== "object") {
38
+ return undefined;
39
+ }
40
+ return value as ToolInfo;
41
+ }
42
+
43
+ function mergeModelMessageDelta(
44
+ base: ModelMessageEvent,
45
+ delta: ModelMessageDeltaEvent,
46
+ ): void {
47
+ if (delta.content) {
48
+ if (
49
+ base.content === undefined ||
50
+ base.content === null ||
51
+ typeof base.content === "string"
52
+ ) {
53
+ base.content = (base.content ?? "") + delta.content;
54
+ } else {
55
+ const last = base.content[base.content.length - 1];
56
+ if (last && last.type === "text") {
57
+ last.text += delta.content;
58
+ } else {
59
+ base.content.push({ type: "text", text: delta.content });
60
+ }
61
+ }
62
+ }
63
+
64
+ if (delta.refusal) {
65
+ base.refusal = (base.refusal ?? "") + delta.refusal;
66
+ }
67
+
68
+ if (delta.toolCalls) {
69
+ base.toolCalls ??= [];
70
+ for (const d of delta.toolCalls) {
71
+ let tc: ToolCall | undefined = base.toolCalls[d.index];
72
+ if (tc === undefined) {
73
+ const toolInfo = asToolInfo(d.toolInfo);
74
+ tc = {
75
+ id: d.id ?? "",
76
+ type: d.type ?? "function",
77
+ function: {
78
+ name: d.function?.name ?? "",
79
+ arguments: "",
80
+ },
81
+ ...(toolInfo != null ? { toolInfo } : {}),
82
+ };
83
+ base.toolCalls[d.index] = tc;
84
+ }
85
+ if (d.id) {
86
+ tc.id = d.id;
87
+ }
88
+ if (d.type) {
89
+ tc.type = d.type;
90
+ }
91
+ if (d.function?.name) {
92
+ tc.function.name = d.function.name;
93
+ }
94
+ if (d.function?.arguments) {
95
+ tc.function.arguments += d.function.arguments;
96
+ }
97
+ const toolInfo = asToolInfo(d.toolInfo);
98
+ if (toolInfo != null) {
99
+ tc.toolInfo = toolInfo;
100
+ }
101
+ if (d.providerSpecificFields) {
102
+ tc.providerSpecificFields = {
103
+ ...(tc.providerSpecificFields ?? {}),
104
+ ...d.providerSpecificFields,
105
+ };
106
+ }
107
+ }
108
+ }
109
+
110
+ if (delta.finishReason) {
111
+ base.finishReason = delta.finishReason;
112
+ }
113
+ if (delta.reasoningContent) {
114
+ base.reasoningContent =
115
+ (base.reasoningContent ?? "") + delta.reasoningContent;
116
+ }
117
+ if (delta.usage) {
118
+ base.usage = delta.usage;
119
+ }
120
+ }