@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.
- package/README.md +192 -579
- package/dist/chunk-3A2EPLQG.js +93 -0
- package/dist/chunk-3A2EPLQG.js.map +1 -0
- package/dist/chunk-Q2SHKMLM.js +270 -0
- package/dist/chunk-Q2SHKMLM.js.map +1 -0
- package/dist/index.d.ts +8 -16
- package/dist/index.js +21 -88
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +136 -4
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +16 -195
- package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -1
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/dist/{types-VUBzoJT2.d.ts → types-BfiFf8O1.d.ts} +17 -11
- package/package.json +10 -5
- package/src/index.ts +41 -0
- package/src/plugins/truefoundry-agent-server-adapter/README.md +178 -0
- package/src/plugins/truefoundry-agent-server-adapter/guards.test.ts +113 -0
- package/src/plugins/truefoundry-agent-server-adapter/guards.ts +130 -0
- package/src/plugins/truefoundry-agent-server-adapter/index.ts +103 -29
- package/src/plugins/truefoundry-agent-server-adapter/types.ts +135 -0
- package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
- package/src/server/types.ts +16 -10
|
@@ -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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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):
|
|
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
|
|
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:
|
|
120
|
+
state: unknown;
|
|
121
|
+
createdBySubject: TfyTurn["createdBySubject"];
|
|
62
122
|
createdAt: string;
|
|
63
|
-
}):
|
|
123
|
+
}): TfyTurn {
|
|
64
124
|
return {
|
|
65
125
|
id: raw.id,
|
|
66
126
|
sessionId: raw.sessionId,
|
|
67
127
|
previousTurnId: raw.previousTurnId,
|
|
68
|
-
input: raw.input
|
|
69
|
-
state: raw.state as
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
},
|