@truefoundry/trueforge-assistant-ui-runtime 0.0.0 → 0.2.0-rc.0
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/CHANGELOG.md +9 -0
- package/LICENSE +201 -0
- package/README.md +146 -4
- package/dist/chunk-2SQK6TIO.js +104 -0
- package/dist/chunk-2SQK6TIO.js.map +1 -0
- package/dist/index.d.ts +378 -0
- package/dist/index.js +4391 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +1210 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +79 -16
- package/src/askUserQuestion.ts +38 -0
- package/src/attachmentAdapter.ts +63 -0
- package/src/collectPending.ts +167 -0
- package/src/constants.ts +2 -0
- package/src/convertTurnMessages.ts +1679 -0
- package/src/createSubAgent.ts +11 -0
- package/src/draft/agentSpec.ts +34 -0
- package/src/draft/draftSessionBridge.ts +28 -0
- package/src/draft/trueforgeDraftThreadListAdapter.ts +73 -0
- package/src/draft/useDraftAgentSpec.ts +289 -0
- package/src/extractTurnUserText.ts +23 -0
- package/src/foldPeerThreads.ts +553 -0
- package/src/hooks.ts +176 -0
- package/src/index.ts +227 -0
- package/src/lastUserMessageText.ts +19 -0
- package/src/listPages.ts +19 -0
- package/src/loadSessionSnapshot.ts +34 -0
- package/src/mcpAuth.ts +35 -0
- package/src/messageCustomMetadata.ts +50 -0
- package/src/modelMessageContent.ts +149 -0
- package/src/modelMessageImageContent.ts +154 -0
- package/src/requiredActionInputs.ts +38 -0
- package/src/sandboxDownload.ts +33 -0
- package/src/server/eventUtils.ts +125 -0
- package/src/server/events.ts +232 -0
- package/src/server/index.ts +178 -0
- package/src/server/types.ts +1191 -0
- package/src/sessionListStartTimestamp.ts +6 -0
- package/src/sessionSnapshot.ts +146 -0
- package/src/sessionThreadMetadata.ts +36 -0
- package/src/sessions.ts +17 -0
- package/src/streamTurn.ts +118 -0
- package/src/toolApproval.ts +413 -0
- package/src/toolResponse.ts +346 -0
- package/src/trueforgeExtras.ts +223 -0
- package/src/trueforgeOwnedSessionsThreadListAdapter.ts +71 -0
- package/src/trueforgeThreadListAdapter.ts +69 -0
- package/src/turnEventHelpers.ts +71 -0
- package/src/turnStreamUpdate.ts +11 -0
- package/src/types.ts +84 -0
- package/src/useTrueForgeAgentMessages.ts +1138 -0
- package/src/useTrueForgeAgentRuntime.ts +308 -0
- package/index.js +0 -6
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
McpAuthRequiredEvent,
|
|
3
|
+
Turn,
|
|
4
|
+
TurnCreatedEvent,
|
|
5
|
+
TurnDoneEvent,
|
|
6
|
+
TurnEvent,
|
|
7
|
+
TurnInputItem,
|
|
8
|
+
} from './server/index.js';
|
|
9
|
+
|
|
10
|
+
import { extractTurnUserText } from './extractTurnUserText.js';
|
|
11
|
+
import { PeerThreadFoldState } from './foldPeerThreads.js';
|
|
12
|
+
import type { StoredApprovalDecision } from './toolApproval.js';
|
|
13
|
+
import type { StoredToolResponse } from './toolResponse.js';
|
|
14
|
+
import type { TurnStreamUpdate } from './turnStreamUpdate.js';
|
|
15
|
+
|
|
16
|
+
/** Session-level event item from `AgentSession.listEvents`. */
|
|
17
|
+
export interface GatewaySessionEventItem {
|
|
18
|
+
turnId: string;
|
|
19
|
+
event: TurnCreatedEvent | TurnDoneEvent | TurnEvent;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Cursor for fetching older `listEvents` pages (scroll-up history). */
|
|
23
|
+
export interface SessionHistoryPagination {
|
|
24
|
+
/** Token for the next older page; omit when exhausted. */
|
|
25
|
+
olderPageToken?: string | undefined;
|
|
26
|
+
hasOlder: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Turn metadata retained for cross-turn projection and subsequent required-action replay. */
|
|
30
|
+
export type SessionTurnRecord = Pick<Turn, 'id' | 'createdAt' | 'state' | 'input'> & {
|
|
31
|
+
/** Denormalized from `input` for user/assistant interleaving during projection. */
|
|
32
|
+
userText?: string | undefined;
|
|
33
|
+
/** Root-thread `model.message` ids ingested with this turn (for per-group projection). */
|
|
34
|
+
rootModelMessageIds?: readonly string[] | undefined;
|
|
35
|
+
/** sandboxId observed via `sandbox.created` on this or an earlier turn (session-scoped). */
|
|
36
|
+
sandboxId?: string | undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface RequiredActionsOverlay {
|
|
40
|
+
approvals: Map<string, StoredApprovalDecision>;
|
|
41
|
+
toolResponses: Map<string, StoredToolResponse>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ActiveStreamState {
|
|
45
|
+
turnId: string;
|
|
46
|
+
update: TurnStreamUpdate;
|
|
47
|
+
isContinuation: boolean;
|
|
48
|
+
streamComplete?: boolean | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PendingUserMessage {
|
|
52
|
+
turnId: string;
|
|
53
|
+
/** Gateway user.message content (text-only string or text/file parts). */
|
|
54
|
+
content: Extract<TurnInputItem, { type: 'user.message' }>['content'];
|
|
55
|
+
createdAt: Date;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SessionSnapshot {
|
|
59
|
+
fold: PeerThreadFoldState;
|
|
60
|
+
turns: SessionTurnRecord[];
|
|
61
|
+
pendingMcpAuth?: McpAuthRequiredEvent | undefined;
|
|
62
|
+
pendingUser?: PendingUserMessage | undefined;
|
|
63
|
+
activeStream?: ActiveStreamState | undefined;
|
|
64
|
+
/** Root `model.message` ids present before the active turn group started (streaming scope). */
|
|
65
|
+
groupRootBaseline?: readonly string[] | undefined;
|
|
66
|
+
requiredActions: RequiredActionsOverlay;
|
|
67
|
+
runningTurn?: Turn | undefined;
|
|
68
|
+
unstable_resume?: boolean | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Chronological `listEvents` items loaded so far (for prepend-on-scroll rebuild).
|
|
71
|
+
* Live stream commits are not appended here — they live in `turns` / `fold`.
|
|
72
|
+
*/
|
|
73
|
+
historyEvents?: readonly GatewaySessionEventItem[] | undefined;
|
|
74
|
+
historyPagination?: SessionHistoryPagination | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ProjectSessionMessagesOptions {
|
|
78
|
+
getCreatedAt?: (messageId: string, fallback: Date, replace?: boolean) => Date;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function emptyRequiredActionsOverlay(): RequiredActionsOverlay {
|
|
82
|
+
return {
|
|
83
|
+
approvals: new Map(),
|
|
84
|
+
toolResponses: new Map(),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createEmptySessionSnapshot(): SessionSnapshot {
|
|
89
|
+
return {
|
|
90
|
+
fold: new PeerThreadFoldState(),
|
|
91
|
+
turns: [],
|
|
92
|
+
requiredActions: emptyRequiredActionsOverlay(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function turnToSessionRecord(turn: Turn): SessionTurnRecord {
|
|
97
|
+
const userText = extractTurnUserText(turn.input);
|
|
98
|
+
return {
|
|
99
|
+
id: turn.id,
|
|
100
|
+
...(userText !== undefined ? { userText } : {}),
|
|
101
|
+
createdAt: turn.createdAt,
|
|
102
|
+
state: turn.state,
|
|
103
|
+
input: turn.input ?? [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Builds a SessionTurnRecord from session-level TurnCreatedEvent + TurnDoneEvent data. */
|
|
108
|
+
export function sessionEventsToSessionRecord(
|
|
109
|
+
turnId: string,
|
|
110
|
+
createdEvent: TurnCreatedEvent,
|
|
111
|
+
doneEvent: TurnDoneEvent,
|
|
112
|
+
rootModelMessageIds: readonly string[],
|
|
113
|
+
sandboxId?: string,
|
|
114
|
+
): SessionTurnRecord {
|
|
115
|
+
const userText = extractTurnUserText(createdEvent.input);
|
|
116
|
+
return {
|
|
117
|
+
id: turnId,
|
|
118
|
+
...(userText !== undefined ? { userText } : {}),
|
|
119
|
+
createdAt: createdEvent.createdAt,
|
|
120
|
+
state: doneEvent.state,
|
|
121
|
+
input: createdEvent.input ?? [],
|
|
122
|
+
rootModelMessageIds,
|
|
123
|
+
...(sandboxId != null ? { sandboxId } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Returns a new snapshot wrapper; fold maps may be mutated in place before calling. */
|
|
128
|
+
export function replaceSessionSnapshot(
|
|
129
|
+
snapshot: SessionSnapshot,
|
|
130
|
+
patch: Partial<Omit<SessionSnapshot, 'requiredActions'>> & {
|
|
131
|
+
requiredActions?: RequiredActionsOverlay;
|
|
132
|
+
},
|
|
133
|
+
): SessionSnapshot {
|
|
134
|
+
return {
|
|
135
|
+
...snapshot,
|
|
136
|
+
...patch,
|
|
137
|
+
...(patch.requiredActions != null ? { requiredActions: patch.requiredActions } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function cloneRequiredActionsOverlay(overlay: RequiredActionsOverlay): RequiredActionsOverlay {
|
|
142
|
+
return {
|
|
143
|
+
approvals: new Map(overlay.approvals),
|
|
144
|
+
toolResponses: new Map(overlay.toolResponses),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RemoteThreadMetadata } from '@assistant-ui/core';
|
|
2
|
+
|
|
3
|
+
import { draftSessionTitle } from './draft/agentSpec.js';
|
|
4
|
+
import type { AgentSpec, Session } from './server/types.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Title for a session row in a mixed (draft + named) thread list.
|
|
8
|
+
* Mutable sessions use draftSessionTitle (optionally falling back to
|
|
9
|
+
* `defaultAgentSpec`); named sessions use title → agentName → id.
|
|
10
|
+
*/
|
|
11
|
+
export function sessionDisplayTitle(session: Session, defaultAgentSpec?: AgentSpec): string {
|
|
12
|
+
if (session.isMutable) {
|
|
13
|
+
const agentSpec = session.agentSpec ?? defaultAgentSpec;
|
|
14
|
+
if (agentSpec != null) {
|
|
15
|
+
return draftSessionTitle({
|
|
16
|
+
agentSpec,
|
|
17
|
+
...(session.title === undefined ? {} : { title: session.title }),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return session.title ?? session.agentName ?? session.id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Map a Session DTO onto RemoteThreadMetadata (mutability + display name in custom). */
|
|
25
|
+
export function sessionToThreadMetadata(session: Session, title: string | undefined): RemoteThreadMetadata {
|
|
26
|
+
return {
|
|
27
|
+
status: 'regular',
|
|
28
|
+
remoteId: session.id,
|
|
29
|
+
title,
|
|
30
|
+
lastMessageAt: new Date(session.updatedAt),
|
|
31
|
+
custom: {
|
|
32
|
+
isMutable: session.isMutable,
|
|
33
|
+
...(session.agentName != null ? { agentName: session.agentName } : {}),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
package/src/sessions.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentChatServer, Session } from './server/types.js';
|
|
2
|
+
|
|
3
|
+
const inflightBySessionId = new Map<string, Promise<Session>>();
|
|
4
|
+
|
|
5
|
+
/** `sessionId` is the assistant-ui thread `remoteId` from `RemoteThreadListAdapter.initialize`. */
|
|
6
|
+
export function getSession(server: AgentChatServer, sessionId: string): Promise<Session> {
|
|
7
|
+
let inflight = inflightBySessionId.get(sessionId);
|
|
8
|
+
if (inflight == null) {
|
|
9
|
+
inflight = server.getSession({ sessionId }).finally(() => {
|
|
10
|
+
if (inflightBySessionId.get(sessionId) === inflight) {
|
|
11
|
+
inflightBySessionId.delete(sessionId);
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
inflightBySessionId.set(sessionId, inflight);
|
|
15
|
+
}
|
|
16
|
+
return inflight;
|
|
17
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { TurnStreamData } from './server/events.js';
|
|
2
|
+
import type { AgentChatServer, PreviousTurnIdInput, TurnInputItem } from './server/types.js';
|
|
3
|
+
|
|
4
|
+
import { streamTurnEvents, type UserMessageContent } from './convertTurnMessages.js';
|
|
5
|
+
import { PeerThreadFoldState } from './foldPeerThreads.js';
|
|
6
|
+
import type { RequiredActionInput } from './requiredActionInputs.js';
|
|
7
|
+
import type { TurnStreamUpdate } from './turnStreamUpdate.js';
|
|
8
|
+
|
|
9
|
+
export interface StreamTurnOptions {
|
|
10
|
+
userMessage?: UserMessageContent;
|
|
11
|
+
resumeMcpAuth?: boolean;
|
|
12
|
+
inputs?: RequiredActionInput[];
|
|
13
|
+
/**
|
|
14
|
+
* Branch anchor for createTurn. Omit for `"auto"`. Pass `"none"` for a fresh
|
|
15
|
+
* root turn.
|
|
16
|
+
*/
|
|
17
|
+
previousTurnId?: PreviousTurnIdInput;
|
|
18
|
+
/** Extra headers for the turn request. */
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildTurnInput(options: StreamTurnOptions): TurnInputItem[] {
|
|
23
|
+
if (options.inputs != null) {
|
|
24
|
+
return options.inputs;
|
|
25
|
+
}
|
|
26
|
+
if (options.resumeMcpAuth === true) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
return [{ type: 'user.message', content: options.userMessage ?? '' }];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function* streamTurnContent(
|
|
33
|
+
server: AgentChatServer,
|
|
34
|
+
sessionId: string,
|
|
35
|
+
foldState: PeerThreadFoldState,
|
|
36
|
+
options: StreamTurnOptions,
|
|
37
|
+
abortSignal: AbortSignal,
|
|
38
|
+
groupRootBaseline?: readonly string[],
|
|
39
|
+
/**
|
|
40
|
+
* Called once with the turn ID as soon as it becomes available (first
|
|
41
|
+
* `turn.created` SSE event). Use this to reconcile the locally-generated
|
|
42
|
+
* optimistic ID with the real turn ID.
|
|
43
|
+
*/
|
|
44
|
+
onTurnIdAvailable?: (turnId: string) => void,
|
|
45
|
+
): AsyncGenerator<TurnStreamUpdate> {
|
|
46
|
+
// Aborting only detaches this client from the run; the turn keeps running on
|
|
47
|
+
// the backend so switching sessions (or remounting) can reattach via
|
|
48
|
+
// `subscribeToTurn`. Stopping the run is an explicit `cancelSession` call.
|
|
49
|
+
if (abortSignal.aborted) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let turnIdNotified = false;
|
|
54
|
+
const notifyTurnId = (turnId: string) => {
|
|
55
|
+
if (!turnIdNotified) {
|
|
56
|
+
onTurnIdAvailable?.(turnId);
|
|
57
|
+
turnIdNotified = true;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const stream: AsyncIterable<TurnStreamData> = server.createTurn({
|
|
62
|
+
sessionId,
|
|
63
|
+
input: buildTurnInput(options),
|
|
64
|
+
previousTurnId: options.previousTurnId ?? 'auto',
|
|
65
|
+
abortSignal,
|
|
66
|
+
...(options.headers != null ? { headers: options.headers } : {}),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
for await (const update of streamTurnEvents(stream, foldState, groupRootBaseline, notifyTurnId)) {
|
|
71
|
+
yield update;
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** TODO: wire `afterSequenceNumber` from the last ingested stream event to skip replay on reconnect. */
|
|
82
|
+
export async function* resumeTurnStream(
|
|
83
|
+
server: AgentChatServer,
|
|
84
|
+
sessionId: string,
|
|
85
|
+
turnId: string,
|
|
86
|
+
foldState: PeerThreadFoldState,
|
|
87
|
+
abortSignal: AbortSignal,
|
|
88
|
+
afterSequenceNumber?: number,
|
|
89
|
+
groupRootBaseline?: readonly string[],
|
|
90
|
+
): AsyncGenerator<TurnStreamUpdate> {
|
|
91
|
+
// Optional on custom backends. Callers detect the gap and report it, so an
|
|
92
|
+
// empty stream here is safer than throwing mid-render.
|
|
93
|
+
if (server.subscribeToTurn == null) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (abortSignal.aborted) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
yield* streamTurnEvents(
|
|
103
|
+
server.subscribeToTurn({
|
|
104
|
+
sessionId,
|
|
105
|
+
turnId,
|
|
106
|
+
...(afterSequenceNumber != null ? { afterSequenceNumber } : {}),
|
|
107
|
+
abortSignal,
|
|
108
|
+
}),
|
|
109
|
+
foldState,
|
|
110
|
+
groupRootBaseline,
|
|
111
|
+
);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|