@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.
Files changed (38) hide show
  1. package/README.md +193 -579
  2. package/dist/chunk-3A2EPLQG.js +93 -0
  3. package/dist/chunk-3A2EPLQG.js.map +1 -0
  4. package/dist/chunk-SQDOTGP2.js +292 -0
  5. package/dist/chunk-SQDOTGP2.js.map +1 -0
  6. package/dist/index.d.ts +24 -36
  7. package/dist/index.js +276 -249
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +134 -5
  10. package/dist/plugins/truefoundry-agent-server-adapter/index.js +16 -195
  11. package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -1
  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-VUBzoJT2.d.ts → types-DbNsU075.d.ts} +212 -19
  16. package/package.json +10 -5
  17. package/src/{private → draft}/agentSpec.ts +14 -17
  18. package/src/{private → draft}/draftSessionBridge.ts +1 -2
  19. package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
  20. package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +2 -1
  21. package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
  22. package/src/draftAgentConfig.test.ts +2 -1
  23. package/src/index.ts +71 -7
  24. package/src/plugins/truefoundry-agent-server-adapter/README.md +178 -0
  25. package/src/plugins/truefoundry-agent-server-adapter/guards.test.ts +113 -0
  26. package/src/plugins/truefoundry-agent-server-adapter/guards.ts +130 -0
  27. package/src/plugins/truefoundry-agent-server-adapter/index.ts +154 -40
  28. package/src/plugins/truefoundry-agent-server-adapter/types.ts +137 -0
  29. package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
  30. package/src/server/index.ts +23 -0
  31. package/src/server/types.ts +272 -21
  32. package/src/truefoundryExtras.ts +4 -1
  33. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +1 -1
  34. package/src/types.ts +1 -2
  35. package/src/useTrueFoundryAgentMessages.test.tsx +261 -1
  36. package/src/useTrueFoundryAgentMessages.ts +284 -176
  37. package/src/useTrueFoundryAgentRuntime.ts +31 -21
  38. /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
+ }