@truefoundry/assistant-ui-runtime 0.1.6-rc.0 → 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.
@@ -0,0 +1,178 @@
1
+ # truefoundry-agent-server-adapter
2
+
3
+ A gateway plugin that wraps [`truefoundry-gateway-sdk`](https://www.npmjs.com/package/truefoundry-gateway-sdk) into an [`AgentChatServer`](../../README.md#server-port-agentchatserver) for `@truefoundry/assistant-ui-runtime`.
4
+
5
+ Named vs draft session routing is internal — you pass `apiKey` / `baseUrl` (or pre-built clients) and get a flat server the runtime can call.
6
+
7
+ ### Used by the [runtime Quick start](../../README.md#quick-start)
8
+
9
+ ---
10
+
11
+ ## Table of contents
12
+
13
+ - [Installation](#installation)
14
+ - [Quick start](#quick-start)
15
+ - [`createTrueFoundryChatServer` options](#createtruefoundrychatserver-options)
16
+ - [Named vs draft sessions](#named-vs-draft-sessions)
17
+ - [Types & guards](#types--guards)
18
+ - [Extending `TfyAgentSpec`](#extending-tfyagentspec)
19
+ - [Exports](#exports)
20
+ - [License](#license)
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ Shipped as a subpath of the runtime package (also re-exported from the main entry):
27
+
28
+ ```bash
29
+ npm install @truefoundry/assistant-ui-runtime truefoundry-gateway-sdk
30
+ # or
31
+ pnpm add @truefoundry/assistant-ui-runtime truefoundry-gateway-sdk
32
+ # or
33
+ yarn add @truefoundry/assistant-ui-runtime truefoundry-gateway-sdk
34
+ ```
35
+
36
+ `truefoundry-gateway-sdk` is an optional peer of the runtime — required only when using this plugin.
37
+
38
+ ---
39
+
40
+ ## Quick start
41
+
42
+ ```tsx
43
+ import { createTrueFoundryChatServer } from "@truefoundry/assistant-ui-runtime";
44
+ // Isolated import (no React):
45
+ // import { createTrueFoundryChatServer } from "@truefoundry/assistant-ui-runtime/plugins/truefoundry-agent-server-adapter";
46
+
47
+ const server = createTrueFoundryChatServer({
48
+ apiKey: process.env.TFY_API_KEY!,
49
+ baseUrl: process.env.TFY_GATEWAY_URL!,
50
+ });
51
+
52
+ // Pass `server` to useTrueFoundryAgentRuntime({ server, agentName })
53
+ ```
54
+
55
+ That returns a `TrueFoundryChatServer` implementing `AgentChatServer` with concrete `TfySession` / `TfyTurn` types.
56
+
57
+ ---
58
+
59
+ ## `createTrueFoundryChatServer` options
60
+
61
+ | Option | Type | Required | Description |
62
+ | ------ | ---- | -------- | ----------- |
63
+ | `apiKey` | `string` | ✅ | TrueFoundry API key |
64
+ | `baseUrl` | `string` | ✅ | Gateway base URL |
65
+ | `client` | `AgentSessionClient` | — | Override the named-session client (otherwise built from `apiKey` / `baseUrl`) |
66
+ | `privateClient` | `PrivateAgentSessionClient` | — | Override the draft/private client |
67
+ | `deleteSession` | `(req: { sessionId: string }) => Promise<void>` | — | Optional delete hook — not on the gateway SDK today; pass your own if needed |
68
+
69
+ ```tsx
70
+ const server = createTrueFoundryChatServer({
71
+ apiKey,
72
+ baseUrl,
73
+ // client, privateClient, deleteSession — optional overrides
74
+ });
75
+ ```
76
+
77
+ Escape hatch for hosts that still need raw gateway clients:
78
+
79
+ ```tsx
80
+ const { client, privateClient } = server.getGatewayClients();
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Named vs draft sessions
86
+
87
+ Routing is fully internal via an in-memory session-type cache populated by `createSession` / `listSessions`:
88
+
89
+ | Create with | Session kind | Client used |
90
+ | ----------- | ------------ | ----------- |
91
+ | `agentName` | Named (immutable) | `AgentSessionClient` |
92
+ | `agentSpec` | Draft (mutable) | `PrivateAgentSessionClient` |
93
+
94
+ `updateSession` is only allowed when `session.isMutable === true` (draft). Calling it on a named session throws.
95
+
96
+ > Ensure `createSession` or `listSessions` ran before `getSession` / turn methods for a given id — the adapter must have cached the session type.
97
+
98
+ ---
99
+
100
+ ## Types & guards
101
+
102
+ The plugin surfaces concrete gateway types for hosts that need them:
103
+
104
+ ```tsx
105
+ import type {
106
+ TfyAgentSpec,
107
+ TfySkillMount,
108
+ TfyMcpServerMount,
109
+ TfySession,
110
+ TfyTurn,
111
+ TfyTurnState,
112
+ TfyToolInfo,
113
+ } from "@truefoundry/assistant-ui-runtime";
114
+
115
+ import {
116
+ isTfyToolInfo,
117
+ isTfySystemToolInfo,
118
+ isTfyMcpToolInfo,
119
+ getTfyUsage,
120
+ getTfyThreadState,
121
+ getTfyMcpInitServers,
122
+ } from "@truefoundry/assistant-ui-runtime";
123
+ ```
124
+
125
+ Use the type guards to narrow event fields typed as `unknown` by the runtime.
126
+
127
+ ---
128
+
129
+ ## Extending `TfyAgentSpec`
130
+
131
+ Only the **spec** is generic. Session / turn / list-params stay as concrete `Tfy*` types. Host-added spec fields survive the round trip because the gateway SDK serializes with `unrecognizedObjectKeys: "passthrough"`:
132
+
133
+ ```tsx
134
+ import {
135
+ createTrueFoundryChatServer,
136
+ type TfyAgentSpec,
137
+ type TrueFoundryChatServer,
138
+ } from "@truefoundry/assistant-ui-runtime";
139
+
140
+ interface MySpec extends TfyAgentSpec {
141
+ workspaceId: string;
142
+ deploymentId: string;
143
+ }
144
+
145
+ const server: TrueFoundryChatServer<MySpec> = createTrueFoundryChatServer<MySpec>({
146
+ apiKey,
147
+ baseUrl,
148
+ });
149
+
150
+ const session = await server.getSession({ sessionId: "ses_abc" });
151
+ console.log(session.agentSpec?.workspaceId); // string | undefined
152
+ ```
153
+
154
+ ---
155
+
156
+ ## Exports
157
+
158
+ | Export | Kind | Purpose |
159
+ | ------ | ---- | ------- |
160
+ | `createTrueFoundryChatServer` | Function | Build a `TrueFoundryChatServer` from gateway credentials / clients |
161
+ | `CreateTrueFoundryChatServerOptions` | Type | Options bag above |
162
+ | `TrueFoundryChatServer<TSpec>` | Type | `AgentChatServer` + `getGatewayClients()` |
163
+ | `TfyAgentSpec`, `TfySession`, `TfyTurn`, … | Types | Concrete gateway DTOs |
164
+ | `isTfyToolInfo`, `getTfyUsage`, … | Guards / helpers | Narrow / extract gateway event fields |
165
+
166
+ Import path:
167
+
168
+ ```ts
169
+ "@truefoundry/assistant-ui-runtime/plugins/truefoundry-agent-server-adapter"
170
+ ```
171
+
172
+ (or the main `@truefoundry/assistant-ui-runtime` entry, which re-exports these symbols).
173
+
174
+ ---
175
+
176
+ ## License
177
+
178
+ See [LICENSE](../../../../LICENSE).
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ getTfyMcpInitServers,
4
+ getTfyThreadState,
5
+ getTfyUsage,
6
+ isTfyMcpToolInfo,
7
+ isTfySystemToolInfo,
8
+ isTfyToolInfo,
9
+ } from "./guards.js";
10
+
11
+ const usage = {
12
+ inputTokens: 10,
13
+ outputTokens: 5,
14
+ inputTokensBreakdown: {
15
+ harness: 1,
16
+ skills: 2,
17
+ instructions: 3,
18
+ toolDefinitions: 4,
19
+ messages: 5,
20
+ },
21
+ };
22
+
23
+ describe("tool info", () => {
24
+ it("discriminates on type", () => {
25
+ const system = { type: "truefoundry-system", name: "ask_user_question" };
26
+ const mcp = {
27
+ type: "mcp",
28
+ name: "search",
29
+ serverId: "s1",
30
+ serverName: "github",
31
+ };
32
+
33
+ expect(isTfySystemToolInfo(system)).toBe(true);
34
+ expect(isTfyMcpToolInfo(system)).toBe(false);
35
+ expect(isTfyMcpToolInfo(mcp)).toBe(true);
36
+ expect(isTfySystemToolInfo(mcp)).toBe(false);
37
+ expect(isTfyToolInfo(system) && isTfyToolInfo(mcp)).toBe(true);
38
+ });
39
+
40
+ it("rejects an mcp shape missing its server attribution", () => {
41
+ expect(isTfyMcpToolInfo({ type: "mcp", name: "search" })).toBe(false);
42
+ });
43
+
44
+ it("rejects absent and non-object values", () => {
45
+ for (const value of [undefined, null, "mcp", 0, []]) {
46
+ expect(isTfyToolInfo(value)).toBe(false);
47
+ }
48
+ });
49
+ });
50
+
51
+ describe("getTfyUsage", () => {
52
+ it("returns usage with its breakdown intact", () => {
53
+ expect(getTfyUsage({ usage })).toEqual(usage);
54
+ });
55
+
56
+ it("rejects usage whose breakdown is absent or incomplete", () => {
57
+ expect(getTfyUsage({ usage: { inputTokens: 1, outputTokens: 2 } })).toBeUndefined();
58
+ expect(
59
+ getTfyUsage({
60
+ usage: { ...usage, inputTokensBreakdown: { harness: 1 } },
61
+ }),
62
+ ).toBeUndefined();
63
+ });
64
+
65
+ it("returns undefined when there is no usage at all", () => {
66
+ expect(getTfyUsage({})).toBeUndefined();
67
+ expect(getTfyUsage(undefined)).toBeUndefined();
68
+ });
69
+ });
70
+
71
+ describe("getTfyThreadState", () => {
72
+ it("accepts done with output and error with a message", () => {
73
+ const done = { status: "done", output: { type: "model.message" } };
74
+ const errored = { status: "error", error: "boom" };
75
+
76
+ expect(getTfyThreadState({ state: done })).toEqual(done);
77
+ expect(getTfyThreadState({ state: errored })).toEqual(errored);
78
+ });
79
+
80
+ it("rejects done without output, since the gateway always sends one", () => {
81
+ expect(getTfyThreadState({ state: { status: "done" } })).toBeUndefined();
82
+ });
83
+
84
+ it("rejects an unknown status", () => {
85
+ expect(getTfyThreadState({ state: { status: "running" } })).toBeUndefined();
86
+ expect(getTfyThreadState({})).toBeUndefined();
87
+ });
88
+ });
89
+
90
+ describe("getTfyMcpInitServers", () => {
91
+ it("returns the servers when every entry is identifiable", () => {
92
+ const servers = [
93
+ { id: "a", name: "github", transportType: "http" },
94
+ { id: "b", name: "slack" },
95
+ ];
96
+ expect(getTfyMcpInitServers({ mcpServers: servers })).toEqual(servers);
97
+ });
98
+
99
+ it("rejects the whole array if any entry is malformed", () => {
100
+ expect(
101
+ getTfyMcpInitServers({ mcpServers: [{ id: "a", name: "ok" }, { id: "b" }] }),
102
+ ).toBeUndefined();
103
+ });
104
+
105
+ it("rejects a non-array", () => {
106
+ expect(getTfyMcpInitServers({ mcpServers: {} })).toBeUndefined();
107
+ expect(getTfyMcpInitServers({})).toBeUndefined();
108
+ });
109
+
110
+ it("accepts an empty list", () => {
111
+ expect(getTfyMcpInitServers({ mcpServers: [] })).toEqual([]);
112
+ });
113
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Point-of-use narrowing for the event half of the gateway protocol.
3
+ *
4
+ * `AgentChatServer` hardcodes the runtime's event types on listEvents,
5
+ * listTurnEvents, subscribeToTurn and prepareAndExecuteTurn — there is no
6
+ * generic to override them from here. So instead of typing those channels,
7
+ * hosts call these guards on the values they receive.
8
+ *
9
+ * They validate rather than cast: this data comes off the network, and the
10
+ * runtime types the relevant fields as `unknown` precisely because nothing
11
+ * has checked them yet.
12
+ */
13
+
14
+ import type {
15
+ TfyMcpServerInitInfo,
16
+ TfyMcpToolInfo,
17
+ TfyModelMessageUsage,
18
+ TfySystemToolInfo,
19
+ TfyThreadState,
20
+ TfyToolInfo,
21
+ } from "./types.js";
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === "object" && value !== null;
25
+ }
26
+
27
+ function hasNumbers<K extends string>(
28
+ value: unknown,
29
+ keys: readonly K[],
30
+ ): value is Record<string, unknown> & Record<K, number> {
31
+ return isRecord(value) && keys.every((key) => typeof value[key] === "number");
32
+ }
33
+
34
+ /**
35
+ * Identifies built-in tools such as `ask_user_question` and `create_sub_agent`.
36
+ * Note `toolInfo` is legitimately absent on streamed deltas, so callers must
37
+ * keep their `function.name` fallback rather than treating absence as an error.
38
+ */
39
+ export function isTfySystemToolInfo(
40
+ toolInfo: unknown,
41
+ ): toolInfo is TfySystemToolInfo {
42
+ return (
43
+ isRecord(toolInfo) &&
44
+ toolInfo.type === "truefoundry-system" &&
45
+ typeof toolInfo.name === "string"
46
+ );
47
+ }
48
+
49
+ /** Carries `serverId` / `serverName`, so the UI can attribute a call to its MCP server. */
50
+ export function isTfyMcpToolInfo(toolInfo: unknown): toolInfo is TfyMcpToolInfo {
51
+ return (
52
+ isRecord(toolInfo) &&
53
+ toolInfo.type === "mcp" &&
54
+ typeof toolInfo.name === "string" &&
55
+ typeof toolInfo.serverId === "string" &&
56
+ typeof toolInfo.serverName === "string"
57
+ );
58
+ }
59
+
60
+ export function isTfyToolInfo(toolInfo: unknown): toolInfo is TfyToolInfo {
61
+ return isTfySystemToolInfo(toolInfo) || isTfyMcpToolInfo(toolInfo);
62
+ }
63
+
64
+ const USAGE_BREAKDOWN_KEYS = [
65
+ "harness",
66
+ "skills",
67
+ "instructions",
68
+ "toolDefinitions",
69
+ "messages",
70
+ ] as const;
71
+
72
+ /**
73
+ * Token usage including the TrueFoundry-specific `inputTokensBreakdown`, which
74
+ * attributes input tokens across harness, skills, instructions, tool
75
+ * definitions and messages.
76
+ */
77
+ export function getTfyUsage(
78
+ source: { usage?: unknown } | null | undefined,
79
+ ): TfyModelMessageUsage | undefined {
80
+ const usage = source?.usage;
81
+ if (!hasNumbers(usage, ["inputTokens", "outputTokens"])) {
82
+ return undefined;
83
+ }
84
+ if (!hasNumbers(usage.inputTokensBreakdown, USAGE_BREAKDOWN_KEYS)) {
85
+ return undefined;
86
+ }
87
+ return usage as unknown as TfyModelMessageUsage;
88
+ }
89
+
90
+ /**
91
+ * Completion state of a sub-agent thread. The runtime types `thread.done`'s
92
+ * `state` as `unknown`, so a sub-agent that errored is otherwise
93
+ * indistinguishable from one that succeeded.
94
+ */
95
+ export function getTfyThreadState(
96
+ event: { state?: unknown } | null | undefined,
97
+ ): TfyThreadState | undefined {
98
+ const state = event?.state;
99
+ if (!isRecord(state)) {
100
+ return undefined;
101
+ }
102
+ if (state.status === "done" && isRecord(state.output)) {
103
+ return state as unknown as TfyThreadState;
104
+ }
105
+ if (state.status === "error" && typeof state.error === "string") {
106
+ return state as unknown as TfyThreadState;
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ /**
112
+ * Servers from an `mcp.initialize` event, including each one's `transportType`.
113
+ * The runtime models this event with an index signature, so the array is
114
+ * `unknown` until checked.
115
+ */
116
+ export function getTfyMcpInitServers(
117
+ event: { mcpServers?: unknown } | null | undefined,
118
+ ): TfyMcpServerInitInfo[] | undefined {
119
+ const servers = event?.mcpServers;
120
+ if (!Array.isArray(servers)) {
121
+ return undefined;
122
+ }
123
+ const valid = servers.every(
124
+ (server) =>
125
+ isRecord(server) &&
126
+ typeof server.id === "string" &&
127
+ typeof server.name === "string",
128
+ );
129
+ return valid ? (servers as TfyMcpServerInitInfo[]) : undefined;
130
+ }
@@ -4,18 +4,61 @@ import { PrivateAgentSessionClient } from "truefoundry-gateway-sdk/agents/privat
4
4
  import type { AgentDraftSession } from "truefoundry-gateway-sdk/agents/private";
5
5
  import type {
6
6
  AgentChatServer,
7
- AgentSpec,
8
7
  ListResult,
9
- Session,
10
- Turn,
11
8
  TurnInputItem,
12
9
  PreviousTurnIdInput,
10
+ UpdateSessionRequest,
13
11
  } from "../../server/types.js";
14
12
  import type {
13
+ SessionEventItem,
15
14
  TurnEvent,
16
15
  TurnStreamData,
17
- SessionEventItem,
18
16
  } from "../../server/events.js";
17
+ import type {
18
+ TfyAgentSpec,
19
+ TfyCreateSessionRequest,
20
+ TfyListSessionsParams,
21
+ TfySession,
22
+ TfyTurn,
23
+ TfyTurnState,
24
+ } from "./types.js";
25
+
26
+ export {
27
+ type TfyAgentSpec,
28
+ type TfySkillMount,
29
+ type TfyMcpServerMount,
30
+ type TfyModelParams,
31
+ type TfyRuntimeConfig,
32
+ type TfyResponseFormat,
33
+ type TfySubject,
34
+ type ToolsSelectorItem,
35
+ type ToolsSelectorTag,
36
+ type RequireApprovalToolSelectorItem,
37
+ type RequireApprovalToolsSelectorTag,
38
+ type TfyTurn,
39
+ type TfyTurnState,
40
+ type TfyTurnCancelledReason,
41
+ type TfyTurnStateDoneOutput,
42
+ type TfySession,
43
+ type TfyCreateSessionRequest,
44
+ type TfyListSessionsParams,
45
+ type TfyToolInfo,
46
+ type TfySystemToolInfo,
47
+ type TfyMcpToolInfo,
48
+ type TfyModelMessageUsage,
49
+ type TfyFinishReason,
50
+ type TfyThreadState,
51
+ type TfyMcpServerInitInfo,
52
+ } from "./types.js";
53
+
54
+ export {
55
+ isTfyToolInfo,
56
+ isTfySystemToolInfo,
57
+ isTfyMcpToolInfo,
58
+ getTfyUsage,
59
+ getTfyThreadState,
60
+ getTfyMcpInitServers,
61
+ } from "./guards.js";
19
62
 
20
63
  type GwSession = AgentSession | AgentDraftSession;
21
64
 
@@ -28,26 +71,42 @@ export type CreateTrueFoundryChatServerOptions = {
28
71
  deleteSession?: (req: { sessionId: string }) => Promise<void>;
29
72
  };
30
73
 
31
- export type TrueFoundryChatServer = AgentChatServer & {
32
- /** Escape hatch for hosts that still need raw gateway clients. */
33
- getGatewayClients(): {
34
- client: AgentSessionClient;
35
- privateClient: PrivateAgentSessionClient;
74
+ /**
75
+ * Only the spec is generic. Session/Turn/list-params are the concrete Tfy*
76
+ * types because the adapter builds them as fixed object literals — a generic
77
+ * there would type fields that nothing ever populates. The spec is safe: the
78
+ * gateway SDK serializes with `unrecognizedObjectKeys: "passthrough"`, so
79
+ * host-added spec fields survive the round trip.
80
+ */
81
+ export type TrueFoundryChatServer<TSpec extends TfyAgentSpec = TfyAgentSpec> =
82
+ AgentChatServer<
83
+ TSpec,
84
+ TfySession<TSpec>,
85
+ TfyCreateSessionRequest<TSpec>,
86
+ TfyListSessionsParams,
87
+ UpdateSessionRequest<TSpec>,
88
+ TfyTurn
89
+ > & {
90
+ /** Escape hatch for hosts that still need raw gateway clients. */
91
+ getGatewayClients(): {
92
+ client: AgentSessionClient;
93
+ privateClient: PrivateAgentSessionClient;
94
+ };
36
95
  };
37
- };
38
96
 
39
97
  function isDraft(session: GwSession): session is AgentDraftSession {
40
98
  return (session as AgentDraftSession).type === "session/draft";
41
99
  }
42
100
 
43
- function toSession(raw: GwSession): Session {
101
+ function toSession<TSpec extends TfyAgentSpec>(raw: GwSession): TfySession<TSpec> {
44
102
  const mutable = isDraft(raw);
45
103
  return {
46
104
  id: raw.id,
47
105
  title: raw.title,
48
106
  agentName: raw.agentName,
49
- ...(mutable ? { agentSpec: raw.agentSpec as AgentSpec } : {}),
107
+ ...(mutable ? { agentSpec: raw.agentSpec as TSpec } : {}),
50
108
  isMutable: mutable,
109
+ createdBySubject: raw.createdBySubject,
51
110
  createdAt: raw.createdAt,
52
111
  updatedAt: raw.updatedAt,
53
112
  };
@@ -58,15 +117,17 @@ function toTurn(raw: {
58
117
  sessionId: string;
59
118
  previousTurnId?: string | null;
60
119
  input?: TurnInputItem[];
61
- state: Turn["state"];
120
+ state: unknown;
121
+ createdBySubject: TfyTurn["createdBySubject"];
62
122
  createdAt: string;
63
- }): Turn {
123
+ }): TfyTurn {
64
124
  return {
65
125
  id: raw.id,
66
126
  sessionId: raw.sessionId,
67
127
  previousTurnId: raw.previousTurnId,
68
- input: raw.input as TurnInputItem[] | undefined,
69
- state: raw.state as Turn["state"],
128
+ input: raw.input,
129
+ state: raw.state as TfyTurnState,
130
+ createdBySubject: raw.createdBySubject,
70
131
  createdAt: raw.createdAt,
71
132
  };
72
133
  }
@@ -94,9 +155,11 @@ async function toListResult<TIn, TOut>(
94
155
  * (populated by createSession/listSessions) determines which gateway client
95
156
  * to call. No try/catch fallback, no double network calls.
96
157
  */
97
- export function createTrueFoundryChatServer(
158
+ export function createTrueFoundryChatServer<
159
+ TSpec extends TfyAgentSpec = TfyAgentSpec,
160
+ >(
98
161
  opts: CreateTrueFoundryChatServerOptions,
99
- ): TrueFoundryChatServer {
162
+ ): TrueFoundryChatServer<TSpec> {
100
163
  const gatewayOpts = { apiKey: opts.apiKey, baseUrl: opts.baseUrl };
101
164
  const client = opts.client ?? new AgentSessionClient(gatewayOpts);
102
165
  const privateClient =
@@ -104,7 +167,10 @@ export function createTrueFoundryChatServer(
104
167
 
105
168
  const sessionTypeCache = new Map<string, boolean>();
106
169
 
107
- function cacheSessionType(session: Session): void {
170
+ function cacheSessionType(session: {
171
+ id: string;
172
+ isMutable: boolean;
173
+ }): void {
108
174
  sessionTypeCache.set(session.id, session.isMutable);
109
175
  }
110
176
 
@@ -122,22 +188,28 @@ export function createTrueFoundryChatServer(
122
188
  );
123
189
  }
124
190
 
125
- const server: TrueFoundryChatServer = {
191
+ const server: TrueFoundryChatServer<TSpec> = {
126
192
  async createSession(req) {
127
193
  if (req.agentSpec != null) {
128
194
  const draft = await privateClient.createDraftSession({
129
- agentSpec: req.agentSpec as never,
195
+ agentSpec: req.agentSpec,
130
196
  ...(req.agentName != null ? { agentName: req.agentName } : {}),
197
+ ...(req.tfyMetadata != null
198
+ ? { tfyMetadata: req.tfyMetadata }
199
+ : {}),
131
200
  });
132
- const session = toSession(draft);
201
+ const session = toSession<TSpec>(draft);
133
202
  cacheSessionType(session);
134
203
  return session;
135
204
  }
136
205
  if (req.agentName != null) {
137
206
  const named = await client.createSession({
138
207
  agentName: req.agentName,
208
+ ...(req.tfyMetadata != null
209
+ ? { tfyMetadata: req.tfyMetadata }
210
+ : {}),
139
211
  });
140
- const session = toSession(named);
212
+ const session = toSession<TSpec>(named);
141
213
  cacheSessionType(session);
142
214
  return session;
143
215
  }
@@ -150,9 +222,10 @@ export function createTrueFoundryChatServer(
150
222
  order: req?.order,
151
223
  pageToken: req?.pageToken,
152
224
  startTimestamp: req?.startTimestamp,
225
+ endTimestamp: req?.endTimestamp,
153
226
  ...(req?.agentName != null ? { agentName: req.agentName } : {}),
154
227
  });
155
- const result = await toListResult(page, toSession);
228
+ const result = await toListResult(page, (s) => toSession<TSpec>(s));
156
229
  for (const session of result.data) {
157
230
  cacheSessionType(session);
158
231
  }
@@ -161,7 +234,7 @@ export function createTrueFoundryChatServer(
161
234
 
162
235
  async getSession({ sessionId }) {
163
236
  const raw = await getSessionObj(sessionId);
164
- const session = toSession(raw);
237
+ const session = toSession<TSpec>(raw);
165
238
  cacheSessionType(session);
166
239
  return session;
167
240
  },
@@ -174,9 +247,9 @@ export function createTrueFoundryChatServer(
174
247
  );
175
248
  }
176
249
  if (req.agentSpec != null) {
177
- await raw.update({ agentSpec: req.agentSpec as never });
250
+ await raw.update({ agentSpec: req.agentSpec });
178
251
  }
179
- return toSession(raw);
252
+ return toSession<TSpec>(raw);
180
253
  },
181
254
 
182
255
  prepareAndExecuteTurn(req: {
@@ -217,12 +290,13 @@ export function createTrueFoundryChatServer(
217
290
  await opts.deleteSession({ sessionId });
218
291
  },
219
292
 
220
- async listTurns({ sessionId, limit, pageToken, order }) {
293
+ // The runtime's signature offers `order`, but the gateway's listTurns
294
+ // takes no such param — forwarding it silently did nothing.
295
+ async listTurns({ sessionId, limit, pageToken }) {
221
296
  const raw = await getSessionObj(sessionId);
222
297
  const page = await raw.listTurns({
223
298
  ...(limit != null ? { limit } : {}),
224
299
  ...(pageToken != null ? { pageToken } : {}),
225
- ...(order != null ? { order } : {}),
226
300
  });
227
301
  return toListResult(page, (turn) => toTurn(turn));
228
302
  },