@truefoundry/assistant-ui-runtime 0.1.6-rc.0 → 0.1.7
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 +193 -579
- package/dist/chunk-3A2EPLQG.js +93 -0
- package/dist/chunk-3A2EPLQG.js.map +1 -0
- package/dist/chunk-SQDOTGP2.js +292 -0
- package/dist/chunk-SQDOTGP2.js.map +1 -0
- package/dist/index.d.ts +24 -36
- package/dist/index.js +276 -249
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +134 -5
- 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-DbNsU075.d.ts} +212 -19
- package/package.json +10 -5
- package/src/{private → draft}/agentSpec.ts +14 -17
- package/src/{private → draft}/draftSessionBridge.ts +1 -2
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +2 -1
- package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
- package/src/draftAgentConfig.test.ts +2 -1
- package/src/index.ts +71 -7
- 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 +154 -40
- package/src/plugins/truefoundry-agent-server-adapter/types.ts +137 -0
- package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
- package/src/server/index.ts +23 -0
- package/src/server/types.ts +272 -21
- package/src/truefoundryExtras.ts +4 -1
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +1 -1
- package/src/types.ts +1 -2
- package/src/useTrueFoundryAgentMessages.test.tsx +261 -1
- package/src/useTrueFoundryAgentMessages.ts +284 -176
- package/src/useTrueFoundryAgentRuntime.ts +31 -21
- /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
|
@@ -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
|
+
}
|