@truefoundry/assistant-ui-runtime 0.1.16 → 0.1.18
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 +13 -0
- package/README.md +4 -2
- package/dist/{chunk-S4SH2KT2.js → chunk-N6LJ3LR2.js} +160 -21
- package/dist/chunk-N6LJ3LR2.js.map +1 -0
- package/dist/index.d.ts +26 -5
- package/dist/index.js +147 -58
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +6 -2
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/{types-VU3Fc17W.d.ts → types-BRrQpoVy.d.ts} +62 -54
- package/package.json +2 -2
- package/src/hooks.ts +19 -19
- package/src/index.ts +7 -1
- package/src/plugins/truefoundry-agent-server-adapter/README.md +1 -1
- package/src/plugins/truefoundry-agent-server-adapter/cp.test.ts +3 -3
- package/src/plugins/truefoundry-agent-server-adapter/cp.ts +51 -10
- package/src/plugins/truefoundry-agent-server-adapter/modelReasoningEffort.test.ts +171 -0
- package/src/plugins/truefoundry-agent-server-adapter/modelReasoningEffort.ts +145 -0
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.test.ts +85 -4
- package/src/plugins/truefoundry-agent-server-adapter/normalizeAgentSpec.ts +67 -17
- package/src/server/index.ts +2 -0
- package/src/server/types.ts +117 -90
- package/src/truefoundryExtras.test.ts +161 -0
- package/src/truefoundryExtras.ts +136 -1
- package/dist/chunk-S4SH2KT2.js.map +0 -1
package/src/server/types.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
ActionRequiredEvent,
|
|
11
11
|
SessionEventItem,
|
|
12
12
|
TurnEvent,
|
|
13
|
-
TurnStreamData
|
|
13
|
+
TurnStreamData,
|
|
14
14
|
} from "./events.js";
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
@@ -74,19 +74,19 @@ export type SearchAgentSelectorParams = {
|
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
* Mounts written to AgentSpec.skills[] / AgentSpec.mcpServers[].
|
|
78
|
-
*
|
|
79
|
-
* These are opaque to the runtime — it stores and forwards them but never reads
|
|
80
|
-
* a field, and the backend owns the shape (the gateway identifies a skill by
|
|
81
|
-
* `fqn`, with no `id` or `name` anywhere). So the base constrains only that a
|
|
82
|
-
* mount is an object; hosts intersect their concrete mount type over it, as
|
|
83
|
-
* `TfySkillMount` / `TfyMcpServerMount` do in the gateway adapter.
|
|
84
|
-
*
|
|
85
|
-
* Naming a field here would not just be unread, it would be wrong: a base with
|
|
86
|
-
* required fields rejects the backend's own payloads, and one with only optional
|
|
87
|
-
* fields is a weak type, which TypeScript rejects for a source that shares no
|
|
88
|
-
* property with it — the gateway's registry skill shares none.
|
|
89
|
-
*/
|
|
77
|
+
* Mounts written to AgentSpec.skills[] / AgentSpec.mcpServers[].
|
|
78
|
+
*
|
|
79
|
+
* These are opaque to the runtime — it stores and forwards them but never reads
|
|
80
|
+
* a field, and the backend owns the shape (the gateway identifies a skill by
|
|
81
|
+
* `fqn`, with no `id` or `name` anywhere). So the base constrains only that a
|
|
82
|
+
* mount is an object; hosts intersect their concrete mount type over it, as
|
|
83
|
+
* `TfySkillMount` / `TfyMcpServerMount` do in the gateway adapter.
|
|
84
|
+
*
|
|
85
|
+
* Naming a field here would not just be unread, it would be wrong: a base with
|
|
86
|
+
* required fields rejects the backend's own payloads, and one with only optional
|
|
87
|
+
* fields is a weak type, which TypeScript rejects for a source that shares no
|
|
88
|
+
* property with it — the gateway's registry skill shares none.
|
|
89
|
+
*/
|
|
90
90
|
export type SkillMount = object;
|
|
91
91
|
|
|
92
92
|
export type McpServerMount = object;
|
|
@@ -116,10 +116,10 @@ export interface AgentRuntimeConfig {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* SDK-owned agent definition — fields the FE reads/writes.
|
|
120
|
-
* Host widens `model` / `skills` / `mcpServers` / `config` via type params,
|
|
121
|
-
* and adds extra fields via `TSpec extends AgentSpec<...>`.
|
|
122
|
-
*/
|
|
119
|
+
* SDK-owned agent definition — fields the FE reads/writes.
|
|
120
|
+
* Host widens `model` / `skills` / `mcpServers` / `config` via type params,
|
|
121
|
+
* and adds extra fields via `TSpec extends AgentSpec<...>`.
|
|
122
|
+
*/
|
|
123
123
|
export interface AgentSpec<
|
|
124
124
|
TModel extends Model = Model,
|
|
125
125
|
TSkill extends SkillMount = SkillMount,
|
|
@@ -193,7 +193,10 @@ export type PreviousTurnIdInput = "auto" | "none" | string;
|
|
|
193
193
|
|
|
194
194
|
export type UserMessageContent =
|
|
195
195
|
| string
|
|
196
|
-
| Array<
|
|
196
|
+
| Array<
|
|
197
|
+
| { type: "text"; text: string }
|
|
198
|
+
| { type: "file"; name: string; data: string }
|
|
199
|
+
>;
|
|
197
200
|
|
|
198
201
|
export interface UserMessage {
|
|
199
202
|
type: "user.message";
|
|
@@ -265,11 +268,11 @@ export interface Turn {
|
|
|
265
268
|
// ---------------------------------------------------------------------------
|
|
266
269
|
|
|
267
270
|
/**
|
|
268
|
-
* Chat / session port — the runtime calls these.
|
|
269
|
-
*
|
|
270
|
-
* All session ops are flat (sessionId param). No gateway client dependency.
|
|
271
|
-
* `createTrueFoundryServer` is one possible implementation (TFY adapter).
|
|
272
|
-
*/
|
|
271
|
+
* Chat / session port — the runtime calls these.
|
|
272
|
+
*
|
|
273
|
+
* All session ops are flat (sessionId param). No gateway client dependency.
|
|
274
|
+
* `createTrueFoundryServer` is one possible implementation (TFY adapter).
|
|
275
|
+
*/
|
|
273
276
|
export interface AgentChatServer<
|
|
274
277
|
TSpec extends AgentSpec = AgentSpec,
|
|
275
278
|
TSession extends Session<TSpec> = Session<TSpec>,
|
|
@@ -284,44 +287,44 @@ export interface AgentChatServer<
|
|
|
284
287
|
updateSession(req: TUpdate): Promise<TSession>;
|
|
285
288
|
|
|
286
289
|
createTurn(req: {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
sessionId: string;
|
|
291
|
+
input?: TurnInputItem[];
|
|
292
|
+
previousTurnId?: PreviousTurnIdInput;
|
|
293
|
+
abortSignal?: AbortSignal;
|
|
294
|
+
headers?: Record<string, string>;
|
|
292
295
|
}): AsyncIterable<TurnStreamData>;
|
|
293
296
|
|
|
294
297
|
cancelSession(req: { sessionId: string }): Promise<void>;
|
|
295
298
|
deleteSession?(req: { sessionId: string }): Promise<void>;
|
|
296
299
|
|
|
297
300
|
listTurns(req: {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
301
|
+
sessionId: string;
|
|
302
|
+
limit?: number;
|
|
303
|
+
pageToken?: string;
|
|
304
|
+
order?: ListSessionsOrder;
|
|
302
305
|
}): Promise<ListResult<TTurn>>;
|
|
303
306
|
getTurn(req: { sessionId: string; turnId: string }): Promise<TTurn>;
|
|
304
307
|
listEvents(req: {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
308
|
+
sessionId: string;
|
|
309
|
+
pageToken?: string;
|
|
310
|
+
lastTurnId?: string;
|
|
311
|
+
limit?: number;
|
|
309
312
|
}): Promise<ListResult<SessionEventItem>>;
|
|
310
313
|
|
|
311
314
|
/** Optional per-turn event listing (hydrate in-flight turn content). */
|
|
312
315
|
listTurnEvents?(req: {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
sessionId: string;
|
|
317
|
+
turnId: string;
|
|
318
|
+
limit?: number;
|
|
319
|
+
pageToken?: string;
|
|
320
|
+
order?: ListSessionsOrder;
|
|
318
321
|
}): Promise<ListResult<TurnEvent>>;
|
|
319
322
|
|
|
320
323
|
subscribeToTurn?(req: {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
sessionId: string;
|
|
325
|
+
turnId: string;
|
|
326
|
+
afterSequenceNumber?: number;
|
|
327
|
+
abortSignal?: AbortSignal;
|
|
325
328
|
}): AsyncIterable<TurnStreamData>;
|
|
326
329
|
|
|
327
330
|
/**
|
|
@@ -330,10 +333,10 @@ export interface AgentChatServer<
|
|
|
330
333
|
* directly use `sandboxId`.
|
|
331
334
|
*/
|
|
332
335
|
downloadSandboxFile?(req: {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
sessionId: string;
|
|
337
|
+
turnId: string;
|
|
338
|
+
sandboxId: string;
|
|
339
|
+
path: string;
|
|
337
340
|
}): Promise<Blob>;
|
|
338
341
|
}
|
|
339
342
|
|
|
@@ -365,9 +368,9 @@ export interface AgentBuilderCapabilitiesResponse {
|
|
|
365
368
|
}
|
|
366
369
|
|
|
367
370
|
/**
|
|
368
|
-
* Builder catalog + persist port — atoms call these.
|
|
369
|
-
* Passed separately from the runtime's chat server.
|
|
370
|
-
*/
|
|
371
|
+
* Builder catalog + persist port — atoms call these.
|
|
372
|
+
* Passed separately from the runtime's chat server.
|
|
373
|
+
*/
|
|
371
374
|
export interface AgentBuilderServer<
|
|
372
375
|
TSpec extends AgentSpec = AgentSpec,
|
|
373
376
|
TModel extends ModelSelectorEntry = ModelSelectorEntry,
|
|
@@ -375,7 +378,8 @@ export interface AgentBuilderServer<
|
|
|
375
378
|
TMcp extends ConnectorSelectorEntry = ConnectorSelectorEntry,
|
|
376
379
|
TAgent extends AgentSelectorEntry = AgentSelectorEntry,
|
|
377
380
|
TSave = SaveAgentResult,
|
|
378
|
-
TCapabilities extends AgentBuilderCapabilitiesResponse =
|
|
381
|
+
TCapabilities extends AgentBuilderCapabilitiesResponse =
|
|
382
|
+
AgentBuilderCapabilitiesResponse,
|
|
379
383
|
> {
|
|
380
384
|
getCapabilities(): Promise<TCapabilities>;
|
|
381
385
|
getModels(): Promise<TModel[]>;
|
|
@@ -391,28 +395,30 @@ export interface AgentBuilderServer<
|
|
|
391
395
|
// ---------------------------------------------------------------------------
|
|
392
396
|
|
|
393
397
|
/**
|
|
394
|
-
* Provider type id. Reserved literal: `"custom"` for user-defined providers;
|
|
395
|
-
* any other string is a builtin (e.g. `"openai"`, `"anthropic"`).
|
|
396
|
-
*
|
|
397
|
-
* Note: `string | "custom"` is useless in TypeScript (`"custom"` ⊆ `string`),
|
|
398
|
-
* so this stays `string` and `"custom"` is a documented convention.
|
|
399
|
-
*/
|
|
398
|
+
* Provider type id. Reserved literal: `"custom"` for user-defined providers;
|
|
399
|
+
* any other string is a builtin (e.g. `"openai"`, `"anthropic"`).
|
|
400
|
+
*
|
|
401
|
+
* Note: `string | "custom"` is useless in TypeScript (`"custom"` ⊆ `string`),
|
|
402
|
+
* so this stays `string` and `"custom"` is a documented convention.
|
|
403
|
+
*/
|
|
400
404
|
export type ProviderType = string;
|
|
401
405
|
|
|
402
406
|
/**
|
|
403
|
-
* Model row — form "Model ID" + "Display name".
|
|
404
|
-
* Host extends for properties, etc.
|
|
405
|
-
*/
|
|
407
|
+
* Model row — form "Model ID" + "Display name".
|
|
408
|
+
* Host extends for properties, etc.
|
|
409
|
+
*/
|
|
406
410
|
export interface ModelEntry {
|
|
407
411
|
id: string;
|
|
408
412
|
name: string;
|
|
409
413
|
}
|
|
410
414
|
|
|
411
415
|
/**
|
|
412
|
-
* Write config for create/update (custom form + catalog "Save key").
|
|
413
|
-
* Host extends. `baseUrl` present iff `type === "custom"`.
|
|
414
|
-
*/
|
|
415
|
-
export interface ModelProviderConfigBase<
|
|
416
|
+
* Write config for create/update (custom form + catalog "Save key").
|
|
417
|
+
* Host extends. `baseUrl` present iff `type === "custom"`.
|
|
418
|
+
*/
|
|
419
|
+
export interface ModelProviderConfigBase<
|
|
420
|
+
TModel extends ModelEntry = ModelEntry,
|
|
421
|
+
> {
|
|
416
422
|
type: ProviderType;
|
|
417
423
|
name: string;
|
|
418
424
|
/** Present iff `type === "custom"`. */
|
|
@@ -422,9 +428,9 @@ export interface ModelProviderConfigBase<TModel extends ModelEntry = ModelEntry>
|
|
|
422
428
|
}
|
|
423
429
|
|
|
424
430
|
/**
|
|
425
|
-
* Configured provider card (list/read). No raw `apiKey`.
|
|
426
|
-
* Host extends for apiKeySet, timestamps, etc.
|
|
427
|
-
*/
|
|
431
|
+
* Configured provider card (list/read). No raw `apiKey`.
|
|
432
|
+
* Host extends for apiKeySet, timestamps, etc.
|
|
433
|
+
*/
|
|
428
434
|
export interface ModelProviderBase<TModel extends ModelEntry = ModelEntry> {
|
|
429
435
|
id: string;
|
|
430
436
|
type: ProviderType;
|
|
@@ -435,11 +441,13 @@ export interface ModelProviderBase<TModel extends ModelEntry = ModelEntry> {
|
|
|
435
441
|
}
|
|
436
442
|
|
|
437
443
|
/**
|
|
438
|
-
* Discovery-only catalog provider (AVAILABLE list).
|
|
439
|
-
* `type` must not be `"custom"` — custom providers use the custom form.
|
|
440
|
-
* Host extends for richer model rows.
|
|
441
|
-
*/
|
|
442
|
-
export interface ModelProviderCatalogEntry<
|
|
444
|
+
* Discovery-only catalog provider (AVAILABLE list).
|
|
445
|
+
* `type` must not be `"custom"` — custom providers use the custom form.
|
|
446
|
+
* Host extends for richer model rows.
|
|
447
|
+
*/
|
|
448
|
+
export interface ModelProviderCatalogEntry<
|
|
449
|
+
TModel extends ModelEntry = ModelEntry,
|
|
450
|
+
> {
|
|
443
451
|
type: ProviderType;
|
|
444
452
|
name: string;
|
|
445
453
|
models: TModel[];
|
|
@@ -458,9 +466,12 @@ export type UpdateModelProviderRequest<TModel extends ModelEntry = ModelEntry> =
|
|
|
458
466
|
export interface ModelCatalogServer<
|
|
459
467
|
TModel extends ModelEntry = ModelEntry,
|
|
460
468
|
TProvider extends ModelProviderBase<TModel> = ModelProviderBase<TModel>,
|
|
461
|
-
TCatalogProvider extends ModelProviderCatalogEntry<TModel> =
|
|
462
|
-
|
|
463
|
-
|
|
469
|
+
TCatalogProvider extends ModelProviderCatalogEntry<TModel> =
|
|
470
|
+
ModelProviderCatalogEntry<TModel>,
|
|
471
|
+
TCreate extends CreateModelProviderRequest<TModel> =
|
|
472
|
+
CreateModelProviderRequest<TModel>,
|
|
473
|
+
TUpdate extends UpdateModelProviderRequest<TModel> =
|
|
474
|
+
UpdateModelProviderRequest<TModel>,
|
|
464
475
|
> {
|
|
465
476
|
getModelProviderCatalog(): Promise<TCatalogProvider[]>;
|
|
466
477
|
listModelProviders(): Promise<TProvider[]>;
|
|
@@ -506,12 +517,13 @@ export type ConnectorAuthPublic =
|
|
|
506
517
|
| ConnectorAuthPublicNone;
|
|
507
518
|
|
|
508
519
|
/**
|
|
509
|
-
* MCP / connector create-edit config. Host extends for extra fields, etc.
|
|
510
|
-
*/
|
|
520
|
+
* MCP / connector create-edit config. Host extends for extra fields, etc.
|
|
521
|
+
*/
|
|
511
522
|
export interface ConnectorConfigBase<
|
|
512
523
|
TAuth extends ConnectorAuth = ConnectorAuth,
|
|
513
524
|
> {
|
|
514
525
|
name: string;
|
|
526
|
+
description: string;
|
|
515
527
|
url: string;
|
|
516
528
|
auth: TAuth;
|
|
517
529
|
}
|
|
@@ -547,12 +559,14 @@ export interface ConnectorCatalogEntry<
|
|
|
547
559
|
}
|
|
548
560
|
|
|
549
561
|
/** Create connector — no `id`; server assigns it. Host extends. */
|
|
550
|
-
export type CreateConnectorRequest<
|
|
551
|
-
|
|
562
|
+
export type CreateConnectorRequest<
|
|
563
|
+
TAuth extends ConnectorAuth = ConnectorAuth,
|
|
564
|
+
> = ConnectorConfigBase<TAuth>;
|
|
552
565
|
|
|
553
566
|
/** Update connector — `id` required. Host extends. */
|
|
554
|
-
export type UpdateConnectorRequest<
|
|
555
|
-
|
|
567
|
+
export type UpdateConnectorRequest<
|
|
568
|
+
TAuth extends ConnectorAuth = ConnectorAuth,
|
|
569
|
+
> = ConnectorConfigBase<TAuth> & { id: string };
|
|
556
570
|
|
|
557
571
|
export interface AuthenticateConnectorRequest {
|
|
558
572
|
id: string;
|
|
@@ -664,7 +678,6 @@ export interface SkillCatalogServer<
|
|
|
664
678
|
|
|
665
679
|
/** Mutable sandbox provider settings shared by catalog rows, create, and update. */
|
|
666
680
|
export interface SandboxConfig {
|
|
667
|
-
snapshotName: string;
|
|
668
681
|
execTimeoutMs: number;
|
|
669
682
|
autoStopIntervalInMinutes: number;
|
|
670
683
|
autoArchiveIntervalInMinutes: number;
|
|
@@ -688,6 +701,18 @@ export interface SandboxBase extends SandboxConfig {
|
|
|
688
701
|
isConnected: boolean;
|
|
689
702
|
}
|
|
690
703
|
|
|
704
|
+
export type SandboxSnapshotSyncStatus = {
|
|
705
|
+
status: "pending" | "ready" | "failed";
|
|
706
|
+
statusReason?: string | null;
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
export interface SandboxProviderListEntry<
|
|
710
|
+
TSandbox extends SandboxBase = SandboxBase,
|
|
711
|
+
> {
|
|
712
|
+
data: TSandbox;
|
|
713
|
+
snapshotSyncStatus: SandboxSnapshotSyncStatus;
|
|
714
|
+
}
|
|
715
|
+
|
|
691
716
|
export interface CreateSandboxRequest extends SandboxConfig {
|
|
692
717
|
/** `SandboxCatalogEntry.id` used to create this sandbox provider. */
|
|
693
718
|
catalogId: string;
|
|
@@ -714,9 +739,11 @@ export interface SandboxCatalogServer<
|
|
|
714
739
|
TCatalogEntry extends SandboxCatalogEntry = SandboxCatalogEntry,
|
|
715
740
|
TCreate extends CreateSandboxRequest = CreateSandboxRequest,
|
|
716
741
|
TUpdate extends UpdateSandboxRequest = UpdateSandboxRequest,
|
|
742
|
+
TListEntry extends SandboxProviderListEntry<TProvider> =
|
|
743
|
+
SandboxProviderListEntry<TProvider>,
|
|
717
744
|
> {
|
|
718
745
|
getSandboxProviderCatalog(): Promise<TCatalogEntry[]>;
|
|
719
|
-
listSandboxProviders(req?: { query?: string }): Promise<
|
|
746
|
+
listSandboxProviders(req?: { query?: string }): Promise<TListEntry[]>;
|
|
720
747
|
createSandboxProvider(req: TCreate): Promise<TProvider>;
|
|
721
748
|
updateSandboxProvider(req: TUpdate): Promise<TProvider>;
|
|
722
749
|
deleteSandboxProvider?(req: { id: string }): Promise<void>;
|
|
@@ -730,10 +757,10 @@ export type AgentLibraryEntry = AgentSelectorEntry;
|
|
|
730
757
|
export type SearchAgentsParams = SearchAgentSelectorParams;
|
|
731
758
|
|
|
732
759
|
/**
|
|
733
|
-
* Settings management aggregate — modelCatalog + connectorCatalog + optional
|
|
734
|
-
* skill and sandbox catalogs.
|
|
735
|
-
* Hosts may pass the whole object to an app shell, or a focused sub-port to a page.
|
|
736
|
-
*/
|
|
760
|
+
* Settings management aggregate — modelCatalog + connectorCatalog + optional
|
|
761
|
+
* skill and sandbox catalogs.
|
|
762
|
+
* Hosts may pass the whole object to an app shell, or a focused sub-port to a page.
|
|
763
|
+
*/
|
|
737
764
|
export interface CatalogServer<
|
|
738
765
|
TModelCatalog extends ModelCatalogServer = ModelCatalogServer,
|
|
739
766
|
TConnectorCatalog extends ConnectorCatalogServer = ConnectorCatalogServer,
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AssistantClient } from "@assistant-ui/store";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getTrueFoundryExtras,
|
|
6
|
+
trueFoundryExtras,
|
|
7
|
+
tryGetTrueFoundryExtras,
|
|
8
|
+
type TrueFoundryRuntimeExtras,
|
|
9
|
+
} from "./truefoundryExtras.js";
|
|
10
|
+
|
|
11
|
+
function clientWithExtras(
|
|
12
|
+
extras: unknown,
|
|
13
|
+
): AssistantClient {
|
|
14
|
+
return {
|
|
15
|
+
thread: () => ({
|
|
16
|
+
getState: () => ({ extras }),
|
|
17
|
+
}),
|
|
18
|
+
} as unknown as AssistantClient;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("tryGetTrueFoundryExtras", () => {
|
|
22
|
+
it("reads extras from the current client", () => {
|
|
23
|
+
const extras = trueFoundryExtras.provide({
|
|
24
|
+
pendingApprovals: [],
|
|
25
|
+
pendingToolResponses: [],
|
|
26
|
+
pendingMcpAuth: null,
|
|
27
|
+
resumeUnavailable: false,
|
|
28
|
+
sandboxId: undefined,
|
|
29
|
+
respondToToolApproval: vi.fn(),
|
|
30
|
+
respondToToolResponse: vi.fn(),
|
|
31
|
+
resumeMcpAuth: vi.fn(),
|
|
32
|
+
downloadSandboxFile: vi.fn(),
|
|
33
|
+
cancel: vi.fn(),
|
|
34
|
+
resetFromTurn: vi.fn(),
|
|
35
|
+
reload: vi.fn(),
|
|
36
|
+
hasOlderHistory: false,
|
|
37
|
+
isLoadingOlderHistory: false,
|
|
38
|
+
loadOlderHistory: vi.fn(),
|
|
39
|
+
draft: null,
|
|
40
|
+
} satisfies TrueFoundryRuntimeExtras);
|
|
41
|
+
|
|
42
|
+
expect(tryGetTrueFoundryExtras(clientWithExtras(extras))).toBe(extras);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("walks nested readonly clients (Object.create parent) to find root extras", () => {
|
|
46
|
+
const respondToToolApproval = vi.fn();
|
|
47
|
+
const extras = trueFoundryExtras.provide({
|
|
48
|
+
pendingApprovals: [{ approvalId: "a1", threadId: "child-1", toolName: "bash", args: {}, argsText: "{}" }],
|
|
49
|
+
pendingToolResponses: [],
|
|
50
|
+
pendingMcpAuth: null,
|
|
51
|
+
resumeUnavailable: false,
|
|
52
|
+
sandboxId: undefined,
|
|
53
|
+
respondToToolApproval,
|
|
54
|
+
respondToToolResponse: vi.fn(),
|
|
55
|
+
resumeMcpAuth: vi.fn(),
|
|
56
|
+
downloadSandboxFile: vi.fn(),
|
|
57
|
+
cancel: vi.fn(),
|
|
58
|
+
resetFromTurn: vi.fn(),
|
|
59
|
+
reload: vi.fn(),
|
|
60
|
+
hasOlderHistory: false,
|
|
61
|
+
isLoadingOlderHistory: false,
|
|
62
|
+
loadOlderHistory: vi.fn(),
|
|
63
|
+
draft: null,
|
|
64
|
+
} satisfies TrueFoundryRuntimeExtras);
|
|
65
|
+
|
|
66
|
+
const root = clientWithExtras(extras);
|
|
67
|
+
// Mirrors ReadonlyThreadProvider: nested AUI is Object.create(parent) with
|
|
68
|
+
// thread overwritten to a readonly client that has no extras.
|
|
69
|
+
const nested = Object.assign(Object.create(root), {
|
|
70
|
+
thread: () => ({
|
|
71
|
+
getState: () => ({ extras: undefined }),
|
|
72
|
+
}),
|
|
73
|
+
}) as AssistantClient;
|
|
74
|
+
|
|
75
|
+
expect(tryGetTrueFoundryExtras(nested)).toBe(extras);
|
|
76
|
+
// Namespace .get must walk too (not only the named helper).
|
|
77
|
+
expect(trueFoundryExtras.get(nested)).toBe(extras);
|
|
78
|
+
getTrueFoundryExtras(nested).respondToToolApproval({
|
|
79
|
+
approvalId: "a1",
|
|
80
|
+
approved: true,
|
|
81
|
+
});
|
|
82
|
+
expect(respondToToolApproval).toHaveBeenCalledWith({
|
|
83
|
+
approvalId: "a1",
|
|
84
|
+
approved: true,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns undefined when no ancestor has TrueFoundry extras", () => {
|
|
89
|
+
expect(tryGetTrueFoundryExtras(clientWithExtras(undefined))).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("survives RootAssistantClient-style proxies that throw on missing accessors", () => {
|
|
93
|
+
const extras = trueFoundryExtras.provide({
|
|
94
|
+
pendingApprovals: [],
|
|
95
|
+
pendingToolResponses: [],
|
|
96
|
+
pendingMcpAuth: null,
|
|
97
|
+
resumeUnavailable: false,
|
|
98
|
+
sandboxId: undefined,
|
|
99
|
+
respondToToolApproval: vi.fn(),
|
|
100
|
+
respondToToolResponse: vi.fn(),
|
|
101
|
+
resumeMcpAuth: vi.fn(),
|
|
102
|
+
downloadSandboxFile: vi.fn(),
|
|
103
|
+
cancel: vi.fn(),
|
|
104
|
+
resetFromTurn: vi.fn(),
|
|
105
|
+
reload: vi.fn(),
|
|
106
|
+
hasOlderHistory: false,
|
|
107
|
+
isLoadingOlderHistory: false,
|
|
108
|
+
loadOlderHistory: vi.fn(),
|
|
109
|
+
draft: null,
|
|
110
|
+
} satisfies TrueFoundryRuntimeExtras);
|
|
111
|
+
|
|
112
|
+
// Mirrors @assistant-ui/store createRootAssistantClient: empty proxy that
|
|
113
|
+
// throws on any get (e.g. "subscribe" / "thread").
|
|
114
|
+
const rootProto = new Proxy(
|
|
115
|
+
{},
|
|
116
|
+
{
|
|
117
|
+
get(_, prop) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`The current scope does not have a "${String(prop)}" property.`,
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
const root = Object.assign(Object.create(rootProto), {
|
|
125
|
+
subscribe: (cb: () => void) => {
|
|
126
|
+
cb();
|
|
127
|
+
return () => {};
|
|
128
|
+
},
|
|
129
|
+
thread: () => ({
|
|
130
|
+
getState: () => ({ extras }),
|
|
131
|
+
}),
|
|
132
|
+
}) as AssistantClient;
|
|
133
|
+
|
|
134
|
+
expect(tryGetTrueFoundryExtras(root)).toBe(extras);
|
|
135
|
+
|
|
136
|
+
// useTrueFoundryRuntimeExtras walks .subscribe up the same chain.
|
|
137
|
+
const nested = Object.assign(Object.create(root), {
|
|
138
|
+
thread: () => ({
|
|
139
|
+
getState: () => ({ extras: undefined }),
|
|
140
|
+
}),
|
|
141
|
+
}) as AssistantClient;
|
|
142
|
+
expect(tryGetTrueFoundryExtras(nested)).toBe(extras);
|
|
143
|
+
expect(() => {
|
|
144
|
+
let current: object | null = nested;
|
|
145
|
+
const seen = new Set<object>();
|
|
146
|
+
while (current != null && !seen.has(current)) {
|
|
147
|
+
seen.add(current);
|
|
148
|
+
try {
|
|
149
|
+
const subscribe = (current as { subscribe?: (cb: () => void) => () => void })
|
|
150
|
+
.subscribe;
|
|
151
|
+
if (typeof subscribe === "function") {
|
|
152
|
+
subscribe(() => {});
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
current = Object.getPrototypeOf(current);
|
|
158
|
+
}
|
|
159
|
+
}).not.toThrow();
|
|
160
|
+
});
|
|
161
|
+
});
|
package/src/truefoundryExtras.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import { createRuntimeExtras } from "@assistant-ui/core/internal";
|
|
4
|
+
import type { AssistantClient } from "@assistant-ui/store";
|
|
5
|
+
import { useAui } from "@assistant-ui/store";
|
|
6
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
2
7
|
import type { McpAuthRequiredEvent } from "./server/index.js";
|
|
3
8
|
|
|
4
9
|
import type { AgentSpec } from "./server/types.js";
|
|
@@ -42,7 +47,7 @@ export type TrueFoundryRuntimeExtras = {
|
|
|
42
47
|
draft: TrueFoundryDraftRuntimeExtras | null;
|
|
43
48
|
};
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
const extrasBrand = createRuntimeExtras<TrueFoundryRuntimeExtras>(
|
|
46
51
|
"useTrueFoundryAgentRuntime",
|
|
47
52
|
);
|
|
48
53
|
|
|
@@ -62,3 +67,133 @@ export const EMPTY_DRAFT_EXTRAS: TrueFoundryDraftRuntimeExtras = {
|
|
|
62
67
|
throw new Error("Draft agent extras are only available in draft mode.");
|
|
63
68
|
},
|
|
64
69
|
};
|
|
70
|
+
|
|
71
|
+
type SubscribeFn = (onStoreChange: () => void) => () => void;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Walk `Object.create(parent)` AUI clients. Stops before leaving the chain.
|
|
75
|
+
* Callers must try/catch RootAssistantClient proxy gets (it throws on any
|
|
76
|
+
* missing accessor such as `subscribe` / `thread`).
|
|
77
|
+
*/
|
|
78
|
+
function walkAssistantClientAncestors(
|
|
79
|
+
client: AssistantClient,
|
|
80
|
+
visit: (current: object) => "continue" | "stop",
|
|
81
|
+
): void {
|
|
82
|
+
let current: object | null = client;
|
|
83
|
+
const seen = new Set<object>();
|
|
84
|
+
while (current != null && !seen.has(current)) {
|
|
85
|
+
seen.add(current);
|
|
86
|
+
if (visit(current) === "stop") return;
|
|
87
|
+
const parent = Object.getPrototypeOf(current);
|
|
88
|
+
if (parent == null || parent === Object.prototype) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
current = parent;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `PartPrimitive.Messages` wraps sub-agent threads in `ReadonlyThreadProvider`,
|
|
97
|
+
* which shadows `thread` (and therefore `thread.extras`) with a readonly client
|
|
98
|
+
* that has no TrueFoundry extras. Nested AUI clients are `Object.create(parent)`,
|
|
99
|
+
* so walk the prototype chain to reach the root runtime extras.
|
|
100
|
+
*/
|
|
101
|
+
export function tryGetTrueFoundryExtras(
|
|
102
|
+
client: AssistantClient,
|
|
103
|
+
): TrueFoundryRuntimeExtras | undefined {
|
|
104
|
+
let found: TrueFoundryRuntimeExtras | undefined;
|
|
105
|
+
walkAssistantClientAncestors(client, (current) => {
|
|
106
|
+
try {
|
|
107
|
+
const thread = (current as AssistantClient).thread;
|
|
108
|
+
if (typeof thread === "function") {
|
|
109
|
+
const extras = extrasBrand.tryGet(thread().getState().extras);
|
|
110
|
+
if (extras != null) {
|
|
111
|
+
found = extras;
|
|
112
|
+
return "stop";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// Nested/readonly clients may lack thread; RootAssistantClient proxy
|
|
117
|
+
// throws on missing scope accessors ("thread" / "subscribe").
|
|
118
|
+
}
|
|
119
|
+
return "continue";
|
|
120
|
+
});
|
|
121
|
+
return found;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getTrueFoundryExtras(client: AssistantClient): TrueFoundryRuntimeExtras {
|
|
125
|
+
const extras = tryGetTrueFoundryExtras(client);
|
|
126
|
+
if (extras == null) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"The current thread is not backed by the useTrueFoundryAgentRuntime runtime.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return extras;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function subscribeClientChain(client: AssistantClient): SubscribeFn {
|
|
135
|
+
return (onStoreChange) => {
|
|
136
|
+
const unsubs: Array<() => void> = [];
|
|
137
|
+
walkAssistantClientAncestors(client, (current) => {
|
|
138
|
+
try {
|
|
139
|
+
const subscribe = (current as { subscribe?: SubscribeFn }).subscribe;
|
|
140
|
+
if (typeof subscribe === "function") {
|
|
141
|
+
unsubs.push(subscribe(onStoreChange));
|
|
142
|
+
}
|
|
143
|
+
return "continue";
|
|
144
|
+
} catch {
|
|
145
|
+
// RootAssistantClient proxy — no further usable ancestors.
|
|
146
|
+
return "stop";
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
return () => {
|
|
150
|
+
for (const unsub of unsubs) {
|
|
151
|
+
unsub();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolves TrueFoundry extras from the nearest ancestor runtime, including
|
|
159
|
+
* inside nested readonly sub-agent renderers (`PartPrimitive.Messages`).
|
|
160
|
+
*/
|
|
161
|
+
export function useTrueFoundryRuntimeExtras(): TrueFoundryRuntimeExtras | undefined {
|
|
162
|
+
const aui = useAui();
|
|
163
|
+
const subscribe = useCallback(subscribeClientChain(aui), [aui]);
|
|
164
|
+
const getSnapshot = useCallback(() => tryGetTrueFoundryExtras(aui), [aui]);
|
|
165
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function useTrueFoundryExtrasApi(): TrueFoundryRuntimeExtras;
|
|
169
|
+
function useTrueFoundryExtrasApi<S>(select: (extras: TrueFoundryRuntimeExtras) => S): S;
|
|
170
|
+
function useTrueFoundryExtrasApi<S>(
|
|
171
|
+
select: (extras: TrueFoundryRuntimeExtras) => S,
|
|
172
|
+
fallback: S,
|
|
173
|
+
): S;
|
|
174
|
+
function useTrueFoundryExtrasApi<S>(
|
|
175
|
+
select?: (extras: TrueFoundryRuntimeExtras) => S,
|
|
176
|
+
fallback?: S,
|
|
177
|
+
): TrueFoundryRuntimeExtras | S {
|
|
178
|
+
const extras = useTrueFoundryRuntimeExtras();
|
|
179
|
+
const hasFallback = arguments.length >= 2;
|
|
180
|
+
if (extras == null) {
|
|
181
|
+
if (hasFallback) return fallback as S;
|
|
182
|
+
throw new Error(
|
|
183
|
+
"The current thread is not backed by the useTrueFoundryAgentRuntime runtime.",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
return select != null ? select(extras) : extras;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Brand + provide/tryGet from assistant-ui; get/use walk ancestor AUI clients so
|
|
191
|
+
* nested readonly sub-agent threads still resolve root TrueFoundry extras.
|
|
192
|
+
*/
|
|
193
|
+
export const trueFoundryExtras = {
|
|
194
|
+
provide: extrasBrand.provide,
|
|
195
|
+
is: extrasBrand.is,
|
|
196
|
+
tryGet: extrasBrand.tryGet,
|
|
197
|
+
get: getTrueFoundryExtras,
|
|
198
|
+
use: useTrueFoundryExtrasApi,
|
|
199
|
+
};
|