@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.
- package/README.md +198 -401
- 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 +24 -29
- package/dist/index.js +264 -237
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +162 -0
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +19 -0
- package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -0
- 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-BfiFf8O1.d.ts +468 -0
- package/package.json +19 -6
- package/src/askUserQuestion.ts +3 -3
- package/src/buildEditedUserMessageContent.test.ts +2 -2
- package/src/collectPending.ts +1 -1
- package/src/convertTurnMessages.test.ts +141 -196
- package/src/convertTurnMessages.ts +130 -76
- package/src/createSubAgent.ts +1 -1
- package/src/draftAgentConfig.test.ts +26 -29
- package/src/extractTurnUserText.ts +1 -1
- package/src/foldPeerThreads.test.ts +1 -1
- package/src/foldPeerThreads.ts +3 -2
- package/src/index.ts +80 -4
- package/src/listPages.ts +21 -0
- package/src/loadSessionSnapshot.test.ts +9 -8
- package/src/loadSessionSnapshot.ts +9 -14
- package/src/mcpAuth.ts +6 -3
- package/src/messageCustomMetadata.ts +1 -1
- package/src/modelMessageContent.ts +1 -1
- package/src/modelMessageImageContent.test.ts +1 -1
- package/src/modelMessageImageContent.ts +7 -6
- 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 +359 -0
- 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/private/agentSpec.ts +8 -3
- package/src/private/draftSessionBridge.ts +14 -13
- package/src/private/truefoundryDraftThreadListAdapter.test.ts +44 -49
- package/src/private/truefoundryDraftThreadListAdapter.ts +22 -16
- package/src/requiredActionInputs.ts +1 -1
- package/src/requiredActionsFromActiveUpdate.test.ts +1 -1
- package/src/server/eventUtils.ts +120 -0
- package/src/server/events.ts +246 -0
- package/src/server/index.ts +66 -0
- package/src/server/types.ts +319 -0
- package/src/sessionSnapshot.ts +1 -1
- package/src/sessions.ts +5 -21
- package/src/streamTurn.test.ts +172 -155
- package/src/streamTurn.ts +51 -48
- package/src/toolApproval.ts +4 -4
- package/src/toolResponse.ts +4 -4
- package/src/truefoundryExtras.ts +1 -1
- package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +26 -29
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +18 -23
- package/src/truefoundryThreadListAdapter.test.ts +16 -18
- package/src/truefoundryThreadListAdapter.ts +7 -7
- package/src/turnEventHelpers.ts +1 -1
- package/src/types.ts +2 -16
- package/src/useTrueFoundryAgentMessages.test.tsx +38 -70
- package/src/useTrueFoundryAgentMessages.ts +32 -44
- package/src/useTrueFoundryAgentRuntime.ts +11 -28
- package/src/private/bindDraftAgentSession.test.ts +0 -54
- package/src/private/bindDraftAgentSession.ts +0 -28
- 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
|
+
};
|
package/src/private/agentSpec.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AgentSpec } from "../server/types.js";
|
|
2
2
|
|
|
3
|
-
export type AgentSpec
|
|
4
|
-
|
|
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 {
|
|
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
|
-
|
|
12
|
+
server: AgentChatServer,
|
|
15
13
|
): DraftSessionBridge {
|
|
16
14
|
return {
|
|
17
15
|
async getDraftAgentSpec(draftSessionId) {
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
43
|
+
const server = mockServer({
|
|
44
|
+
listSessions,
|
|
45
|
+
createSession: vi.fn(),
|
|
46
|
+
getSession: vi.fn(),
|
|
47
|
+
});
|
|
53
48
|
|
|
54
49
|
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
55
|
-
|
|
50
|
+
server,
|
|
56
51
|
defaultAgentSpec,
|
|
57
52
|
});
|
|
58
53
|
|
|
59
54
|
const result = await adapter.list();
|
|
60
55
|
|
|
61
|
-
expect(
|
|
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(
|
|
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
|
|
78
|
+
const createSession = vi.fn().mockResolvedValue(
|
|
84
79
|
mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
|
|
85
80
|
);
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
81
|
+
const server = mockServer({
|
|
82
|
+
listSessions: vi.fn(),
|
|
83
|
+
createSession,
|
|
84
|
+
getSession: vi.fn(),
|
|
85
|
+
});
|
|
91
86
|
|
|
92
87
|
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
93
|
-
|
|
88
|
+
server,
|
|
94
89
|
defaultAgentSpec,
|
|
95
90
|
});
|
|
96
91
|
|
|
97
92
|
const result = await adapter.initialize("local-thread-id");
|
|
98
93
|
|
|
99
|
-
expect(
|
|
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: [{
|
|
108
|
-
skills: [{
|
|
102
|
+
mcpServers: [{ id: "github", name: "github" }],
|
|
103
|
+
skills: [{ id: "skill-a", name: "skill-a" }],
|
|
109
104
|
};
|
|
110
|
-
const
|
|
105
|
+
const createSession = vi.fn().mockResolvedValue(
|
|
111
106
|
mockDraft("d-new", undefined, "2026-06-30T12:00:00.000Z"),
|
|
112
107
|
);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
108
|
+
const server = mockServer({
|
|
109
|
+
listSessions: vi.fn(),
|
|
110
|
+
createSession,
|
|
111
|
+
getSession: vi.fn(),
|
|
112
|
+
});
|
|
118
113
|
|
|
119
114
|
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
120
|
-
|
|
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(
|
|
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
|
|
126
|
+
const getSession = vi.fn().mockResolvedValue(
|
|
132
127
|
mockDraft("d1", undefined, "2026-06-30T10:00:00.000Z"),
|
|
133
128
|
);
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
129
|
+
const server = mockServer({
|
|
130
|
+
listSessions: vi.fn(),
|
|
131
|
+
createSession: vi.fn(),
|
|
132
|
+
getSession,
|
|
133
|
+
});
|
|
139
134
|
|
|
140
135
|
const adapter = createTrueFoundryDraftThreadListAdapter({
|
|
141
|
-
|
|
136
|
+
server,
|
|
142
137
|
defaultAgentSpec,
|
|
143
138
|
});
|
|
144
139
|
|
|
145
140
|
const result = await adapter.fetch("d1");
|
|
146
141
|
|
|
147
|
-
expect(
|
|
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
|
-
|
|
10
|
+
server: AgentChatServer;
|
|
11
11
|
defaultAgentSpec: AgentSpec;
|
|
12
12
|
getAgentSpec?: () => AgentSpec;
|
|
13
13
|
}): RemoteThreadListAdapter {
|
|
14
|
-
const {
|
|
14
|
+
const { server, defaultAgentSpec, getAgentSpec } = options;
|
|
15
15
|
|
|
16
16
|
return {
|
|
17
17
|
async list({ after } = {}) {
|
|
18
|
-
const page = await
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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.
|
|
36
|
+
nextCursor: page.nextPageToken ?? undefined,
|
|
32
37
|
};
|
|
33
38
|
},
|
|
34
39
|
|
|
35
40
|
async initialize(_threadId: string) {
|
|
36
|
-
const draft = await
|
|
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
|
|
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(
|
|
52
|
+
title: draftSessionTitle({
|
|
53
|
+
title: draft.title,
|
|
54
|
+
agentSpec: draft.agentSpec ?? defaultAgentSpec,
|
|
55
|
+
}),
|
|
50
56
|
lastMessageAt: new Date(draft.updatedAt),
|
|
51
57
|
};
|
|
52
58
|
},
|
|
@@ -64,7 +64,7 @@ describe("requiredActionsFromActiveUpdate", () => {
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
const actions = requiredActionsFromActiveUpdate(update);
|
|
67
|
-
expect(actions
|
|
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
|
+
}
|