@truefoundry/assistant-ui-runtime 0.1.21 → 0.1.22

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.
@@ -314,6 +314,7 @@ interface AgentSpec<TModel extends Model = Model, TSkill extends SkillMount = Sk
314
314
  config?: TConfig;
315
315
  instructions?: string;
316
316
  variables?: Record<string, string>;
317
+ description?: string;
317
318
  }
318
319
  interface Session<TSpec extends AgentSpec = AgentSpec> {
319
320
  id: string;
@@ -346,10 +347,12 @@ type PageParams = {
346
347
  pageToken?: string;
347
348
  };
348
349
  interface ListSessionsParams extends PageParams {
349
- /** Host-owned agent identity filter. Hosts that key agents by name pass that name here. */
350
+ /** Host-owned agent identity filter. Omit for all sessions (current user). */
350
351
  agentId?: string;
351
- /** Host-specific filter (e.g. TFY startTimestamp). */
352
+ /** Inclusive lower bound on session activity (ISO-8601). */
352
353
  startTimestamp?: string;
354
+ /** Inclusive upper bound on session activity (ISO-8601). */
355
+ endTimestamp?: string;
353
356
  }
354
357
  type PreviousTurnIdInput = "auto" | "none" | string;
355
358
  type UserMessageContent = string | Array<{
@@ -386,11 +389,25 @@ type TurnInputItem = UserMessage | UserToolApprovalEvent | UserToolResponseEvent
386
389
  type TurnStateRunning = {
387
390
  status: "running";
388
391
  };
392
+ /**
393
+ * Aggregated token metrics on a finished turn (`turn.done.state.metrics`).
394
+ * Host maps wire snake_case (`total_input_tokens`, …) → camelCase here.
395
+ */
396
+ interface TurnDoneMetrics {
397
+ totalInputTokens: number;
398
+ totalOutputTokens: number;
399
+ totalTokens: number;
400
+ totalCacheReadTokens: number;
401
+ totalCacheWriteTokens: number;
402
+ totalReasoningTokens: number;
403
+ }
389
404
  type TurnStateDone = {
390
405
  status: "done";
391
406
  output?: unknown;
392
407
  requiredActions?: ActionRequiredEvent[];
393
408
  completedAt: string;
409
+ /** Present when the host reports per-turn token totals. */
410
+ metrics?: TurnDoneMetrics;
394
411
  };
395
412
  type TurnStateCancelled = {
396
413
  status: "cancelled";
@@ -491,6 +508,8 @@ interface SaveAgentRequest<TSpec extends AgentSpec = AgentSpec> {
491
508
  interface SaveAgentResult {
492
509
  /** Immutable id allocated by the host registry. */
493
510
  agentId?: string;
511
+ /** Version id allocated by the host registry for this save. */
512
+ versionId?: string;
494
513
  /** Timestamp returned when the active mutable session was updated. */
495
514
  sessionUpdatedAt?: string;
496
515
  }
@@ -827,18 +846,110 @@ interface CatalogServer<TModelCatalog extends ModelCatalogServer = ModelCatalogS
827
846
  sandboxCatalog?: TSandboxCatalog;
828
847
  }
829
848
  /**
830
- * Composed host port: chat + builder + optional settings catalog.
849
+ * Published agent identity + spec for the agent-detail Overview (read-only).
850
+ * Host widens `TSpec` (and may extend this DTO) for mounts / config / extras.
851
+ */
852
+ interface AgentDetail<TSpec extends AgentSpec = AgentSpec> {
853
+ agentId: string;
854
+ /** Display name (e.g. "release-notes-writer"). */
855
+ name: string;
856
+ agentSpec: TSpec;
857
+ }
858
+ /** Stream vs non-stream bodies for one language on the Use In Code tab. */
859
+ interface CodeSnippetSampleCode {
860
+ stream: string;
861
+ nonStream: string;
862
+ }
863
+ /**
864
+ * One language row for Use In Code.
865
+ * Host maps wire `sample_code` / `non_stream` → camelCase here.
866
+ */
867
+ interface CodeSnippet<TSample extends CodeSnippetSampleCode = CodeSnippetSampleCode> {
868
+ /** Sidebar label (e.g. "TypeScript"). */
869
+ labelName: string;
870
+ /** Highlighter / tab id (e.g. "typescript"). */
871
+ language: string;
872
+ icon?: string;
873
+ sampleCode: TSample;
874
+ }
875
+ /**
876
+ * Aggregated session metrics for the sessions list sidebar.
877
+ * Host maps wire snake_case (`total_turns`, `total_cost_in_usd`, …) → camelCase.
878
+ */
879
+ interface SessionListMetrics {
880
+ totalTurns: number;
881
+ totalCostInUsd: number;
882
+ totalDurationMs: number;
883
+ }
884
+ /**
885
+ * One row in the Agent Sessions list (left pane).
886
+ *
887
+ * Binding: `agentName` → named / immutable agent; `agentSpec` → mutable / draft.
888
+ * Host may send one, both, or neither depending on how the session was created.
889
+ */
890
+ interface SessionListEntry<TSpec extends AgentSpec = AgentSpec> {
891
+ id: string;
892
+ title?: string | null;
893
+ createdAt: string;
894
+ updatedAt: string;
895
+ lastActivityAt: string;
896
+ metrics: SessionListMetrics;
897
+ /** Present when bound to a published (immutable) agent. */
898
+ agentName?: string | null;
899
+ /** Present when bound to a mutable / draft agent spec. */
900
+ agentSpec?: TSpec;
901
+ }
902
+ /** Params for `AgentSessionsServer.listSessionEvents` (session event timeline). */
903
+ interface ListSessionEventsParams extends Pick<PageParams, "limit" | "pageToken"> {
904
+ sessionId: string;
905
+ }
906
+ /**
907
+ * Optional plug-in for agent-detail UI: Overview, Use In Code, sessions list,
908
+ * and per-session event timeline. Omit `sessions` on `AgentUIServerPort` when
909
+ * the host has no agent-detail surface.
910
+ *
911
+ * Read-only — create/update/delete stay on `AgentChatServer` / `AgentBuilderServer`.
912
+ */
913
+ interface AgentSessionsServer<TSpec extends AgentSpec = AgentSpec, TDetail extends AgentDetail<TSpec> = AgentDetail<TSpec>, TSnippet extends CodeSnippet = CodeSnippet, TListEntry extends SessionListEntry<TSpec> = SessionListEntry<TSpec>, TList extends ListSessionsParams = ListSessionsParams> {
914
+ /** Fetch published agent details by id for the Overview tab. */
915
+ getAgent(req: {
916
+ agentId: string;
917
+ }): Promise<TDetail>;
918
+ /** Fetch Use In Code snippets for the agent (one row per language). */
919
+ getCodeSnippets(req: {
920
+ agentId: string;
921
+ }): Promise<TSnippet[]>;
922
+ /**
923
+ * List sessions for the current user. Pass `agentId` to scope to one agent;
924
+ * omit for all sessions. Use `startTimestamp` / `endTimestamp` for date filters.
925
+ */
926
+ listSessions(req?: TList): Promise<ListResult<TListEntry>>;
927
+ /**
928
+ * Fetch the session event timeline (right pane). Paginate with `pageToken`
929
+ * until exhausted; rebuild turns from `turn.created` / `turn.done` +
930
+ * nested `TurnEvent`s. Per-turn token metrics live on `turn.done.state.metrics`.
931
+ */
932
+ listSessionEvents(req: ListSessionEventsParams): Promise<ListResult<SessionEventItem>>;
933
+ }
934
+ /**
935
+ * Composed host port: chat + builder + optional settings catalog + optional
936
+ * agent-detail / sessions shell.
831
937
  *
832
938
  * `catalog` is optional — if the host passes it, settings UI can call
833
939
  * `useCatalogServer()` / show modelCatalog, connectorCatalog, and skillCatalog;
834
940
  * if omitted, those surfaces stay hidden.
835
941
  *
942
+ * `sessions` is optional — if the host passes it, agent-detail UI can call
943
+ * `useAgentSessionsServer()` / Overview + sessions under an agent; if omitted,
944
+ * that surface stays hidden.
945
+ *
836
946
  * trueforge-ui re-exports this as `AgentUIServer`.
837
947
  */
838
- type AgentUIServerPort<TChat extends AgentChatServer = AgentChatServer, TBuilder extends AgentBuilderServer = AgentBuilderServer, TCatalog extends CatalogServer = CatalogServer> = TChat & TBuilder & {
948
+ type AgentUIServerPort<TChat extends AgentChatServer = AgentChatServer, TBuilder extends AgentBuilderServer = AgentBuilderServer, TCatalog extends CatalogServer = CatalogServer, TSessions extends AgentSessionsServer = AgentSessionsServer> = TChat & TBuilder & {
839
949
  catalog?: TCatalog;
950
+ sessions?: TSessions;
840
951
  };
841
952
  /** Host-facing alias used by trueforge-ui. */
842
953
  type AgentUIServer = AgentUIServerPort;
843
954
 
844
- export type { ModelCatalogServer as $, AgentSpec as A, ConnectorBase as B, CatalogServer as C, ConnectorCatalogEntry as D, ConnectorCatalogServer as E, ConnectorConfigBase as F, ConnectorSelectorEntry as G, ConnectorState as H, CreateConnectorRequest as I, CreateModelProviderRequest as J, CreateSandboxProviderRequest as K, CreateSandboxRequest as L, McpAuthRequiredEvent as M, CreateSessionRequest as N, CreateSkillRequest as O, CreateSkillRequestBase as P, DefinedSkill as Q, GithubSkill as R, Session as S, TurnEvent as T, UserToolResponseEvent as U, ImportGithubSkillRequest as V, ListResult as W, ListSessionsOrder as X, ListSessionsParams as Y, McpServerMount as Z, Model as _, AgentChatServer as a, ToolResponseEvent as a$, ModelEntry as a0, ModelMessageEvent as a1, ModelParams as a2, ModelProperties as a3, ModelProviderBase as a4, ModelProviderCatalogEntry as a5, ModelProviderConfigBase as a6, ModelSelection as a7, ModelSelectorEntry as a8, PageParams as a9, ToolApprovalRequiredEvent as aA, ToolBase as aB, ToolCall as aC, ToolResponseRequiredEvent as aD, TurnState as aE, TurnStateDone as aF, TurnStreamData as aG, TurnStreamingEvent as aH, UpdateConnectorRequest as aI, UpdateModelProviderRequest as aJ, UpdateSandboxProviderRequest as aK, UpdateSandboxRequest as aL, UpdateSessionRequest as aM, UserMessage as aN, DeltaEvents as aO, ActionRequiredEvent as aP, AgentInfo as aQ, AgentParent as aR, ChunkDeltaToolCall as aS, McpInitializeEvent as aT, McpServerAuthInfo as aU, ModelMessageContentPart as aV, ModelMessageDeltaEvent as aW, ThreadDoneEvent as aX, ToolCallFunction as aY, ToolCallRef as aZ, ToolInfo as a_, PreviousTurnIdInput as aa, ProviderEntry as ab, ProviderType as ac, RegistrySkill as ad, SandboxBase as ae, SandboxCatalogEntry as af, SandboxCatalogServer as ag, SandboxConfig as ah, SandboxCreatedEvent as ai, SandboxProviderBase as aj, SandboxProviderCatalogEntry as ak, SandboxProviderConfig as al, SandboxProviderListEntry as am, SandboxSnapshotSyncStatus as an, SaveAgentRequest as ao, SaveAgentResult as ap, SearchAgentSelectorParams as aq, SearchAgentsParams as ar, SelectRegistrySkillRequest as as, SessionEventItem as at, SkillBase as au, SkillCatalogEntry as av, SkillCatalogServer as aw, SkillConfigBase as ax, SkillMount as ay, SkillSelectorEntry as az, ThreadCreatedEvent as b, TurnCreatedEvent as b0, TurnDoneEvent as b1, TurnStateCancelled as b2, TurnStateError as b3, TurnStateRunning as b4, UserMessageContent as b5, UserToolApprovalEvent as c, Turn as d, TurnInputItem as e, AgentBuilderCapabilitiesResponse as f, AgentBuilderServer as g, AgentCapabilityConfig as h, AgentLibraryEntry as i, AgentRuntimeConfig as j, AgentSelectorEntry as k, AgentSkill as l, AgentUIServer as m, AgentUIServerPort as n, ApprovalDecision as o, AuthenticateConnectorRequest as p, ConnectorAuth as q, ConnectorAuthApiKey as r, ConnectorAuthNone as s, ConnectorAuthOAuth as t, ConnectorAuthPublic as u, ConnectorAuthPublicApiKey as v, ConnectorAuthPublicNone as w, ConnectorAuthPublicOAuth as x, ConnectorAuthType as y, ConnectorAuthenticationResult as z };
955
+ export type { ListSessionEventsParams as $, AgentSpec as A, ConnectorAuthPublicNone as B, CatalogServer as C, ConnectorAuthPublicOAuth as D, ConnectorAuthType as E, ConnectorAuthenticationResult as F, ConnectorBase as G, ConnectorCatalogEntry as H, ConnectorCatalogServer as I, ConnectorConfigBase as J, ConnectorSelectorEntry as K, ConnectorState as L, McpAuthRequiredEvent as M, CreateConnectorRequest as N, CreateModelProviderRequest as O, CreateSandboxProviderRequest as P, CreateSandboxRequest as Q, CreateSessionRequest as R, Session as S, TurnEvent as T, UserToolResponseEvent as U, CreateSkillRequest as V, CreateSkillRequestBase as W, DefinedSkill as X, GithubSkill as Y, ImportGithubSkillRequest as Z, ListResult as _, AgentChatServer as a, McpInitializeEvent as a$, ListSessionsOrder as a0, ListSessionsParams as a1, McpServerMount as a2, Model as a3, ModelCatalogServer as a4, ModelEntry as a5, ModelMessageEvent as a6, ModelParams as a7, ModelProperties as a8, ModelProviderBase as a9, SessionListMetrics as aA, SkillBase as aB, SkillCatalogEntry as aC, SkillCatalogServer as aD, SkillConfigBase as aE, SkillMount as aF, SkillSelectorEntry as aG, ToolApprovalRequiredEvent as aH, ToolBase as aI, ToolCall as aJ, ToolResponseRequiredEvent as aK, TurnDoneMetrics as aL, TurnState as aM, TurnStateDone as aN, TurnStreamData as aO, TurnStreamingEvent as aP, UpdateConnectorRequest as aQ, UpdateModelProviderRequest as aR, UpdateSandboxProviderRequest as aS, UpdateSandboxRequest as aT, UpdateSessionRequest as aU, UserMessage as aV, DeltaEvents as aW, ActionRequiredEvent as aX, AgentInfo as aY, AgentParent as aZ, ChunkDeltaToolCall as a_, ModelProviderCatalogEntry as aa, ModelProviderConfigBase as ab, ModelSelection as ac, ModelSelectorEntry as ad, PageParams as ae, PreviousTurnIdInput as af, ProviderEntry as ag, ProviderType as ah, RegistrySkill as ai, SandboxBase as aj, SandboxCatalogEntry as ak, SandboxCatalogServer as al, SandboxConfig as am, SandboxCreatedEvent as an, SandboxProviderBase as ao, SandboxProviderCatalogEntry as ap, SandboxProviderConfig as aq, SandboxProviderListEntry as ar, SandboxSnapshotSyncStatus as as, SaveAgentRequest as at, SaveAgentResult as au, SearchAgentSelectorParams as av, SearchAgentsParams as aw, SelectRegistrySkillRequest as ax, SessionEventItem as ay, SessionListEntry as az, ThreadCreatedEvent as b, McpServerAuthInfo as b0, ModelMessageContentPart as b1, ModelMessageDeltaEvent as b2, ThreadDoneEvent as b3, ToolCallFunction as b4, ToolCallRef as b5, ToolInfo as b6, ToolResponseEvent as b7, TurnCreatedEvent as b8, TurnDoneEvent as b9, TurnStateCancelled as ba, TurnStateError as bb, TurnStateRunning as bc, UserMessageContent as bd, UserToolApprovalEvent as c, Turn as d, TurnInputItem as e, AgentBuilderCapabilitiesResponse as f, AgentBuilderServer as g, AgentCapabilityConfig as h, AgentDetail as i, AgentLibraryEntry as j, AgentRuntimeConfig as k, AgentSelectorEntry as l, AgentSessionsServer as m, AgentSkill as n, AgentUIServer as o, AgentUIServerPort as p, ApprovalDecision as q, AuthenticateConnectorRequest as r, CodeSnippet as s, CodeSnippetSampleCode as t, ConnectorAuth as u, ConnectorAuthApiKey as v, ConnectorAuthNone as w, ConnectorAuthOAuth as x, ConnectorAuthPublic as y, ConnectorAuthPublicApiKey as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truefoundry/assistant-ui-runtime",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "TrueFoundry Gateway agent runtime adapter for assistant-ui",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/src/index.ts CHANGED
@@ -93,6 +93,7 @@ export type {
93
93
  Session,
94
94
  Turn,
95
95
  TurnState,
96
+ TurnDoneMetrics,
96
97
  TurnStateDone,
97
98
  TurnInputItem,
98
99
  AgentSpec,
@@ -158,6 +159,13 @@ export type {
158
159
  UpdateSandboxProviderRequest,
159
160
  SandboxCatalogServer,
160
161
  CatalogServer,
162
+ AgentDetail,
163
+ CodeSnippetSampleCode,
164
+ CodeSnippet,
165
+ SessionListMetrics,
166
+ SessionListEntry,
167
+ ListSessionEventsParams,
168
+ AgentSessionsServer,
161
169
  AgentUIServerPort,
162
170
  AgentUIServer,
163
171
  ProviderEntry,
@@ -208,6 +216,8 @@ export {
208
216
  type TfyConnectorSelectorEntry,
209
217
  type TfyAgentSelectorEntry,
210
218
  type TfyAgentSpec,
219
+ type TfyCollaborator,
220
+ type TfySaveAgentResult,
211
221
  type TfySkillMount,
212
222
  type TfyMcpServerMount,
213
223
  type TfyModelParams,
@@ -9,8 +9,9 @@ import {
9
9
  normalizeMcpServers,
10
10
  resolveGatewayURL,
11
11
  saveAgent,
12
+ saveAgentResultFromCp,
12
13
  SAVE_AGENT_COLLABORATORS,
13
- SAVE_AGENT_METADATA_TAGS,
14
+ SAVE_AGENT_TAGS,
14
15
  toCamelCaseDeep,
15
16
  toSnakeCaseDeep,
16
17
  } from "./cp.js";
@@ -467,7 +468,7 @@ describe("toSnakeCaseDeep", () => {
467
468
  });
468
469
 
469
470
  describe("buildSaveAgentManifest", () => {
470
- it("hardcodes type / metadata_tags / collaborators and snake_cases spec fields", () => {
471
+ it("spreads the snake-cased spec and defaults missing tags / collaborators", () => {
471
472
  const manifest = buildSaveAgentManifest("my-agent", {
472
473
  model: {
473
474
  name: "ai-foundry/claude-sonnet-4-6",
@@ -502,7 +503,7 @@ describe("buildSaveAgentManifest", () => {
502
503
  name: "ai-foundry/claude-sonnet-4-6",
503
504
  params: { max_tokens: 8192, reasoning_effort: "medium" },
504
505
  },
505
- metadata_tags: { ...SAVE_AGENT_METADATA_TAGS },
506
+ tags: { ...SAVE_AGENT_TAGS },
506
507
  collaborators: [...SAVE_AGENT_COLLABORATORS],
507
508
  instructions: "Be helpful",
508
509
  config: {
@@ -527,6 +528,45 @@ describe("buildSaveAgentManifest", () => {
527
528
  });
528
529
  });
529
530
 
531
+ it("keeps host tags, collaborators, and extra spec fields", () => {
532
+ const manifest = buildSaveAgentManifest("named", {
533
+ model: {
534
+ name: "openai-main/gpt-4.1",
535
+ params: { maxTokens: 4096, temperature: 0.2 },
536
+ },
537
+ description: "My agent",
538
+ instructions: "Be concise",
539
+ variables: { city: "Berlin" },
540
+ messages: [{ role: "user", content: "Hello {{city}}" }],
541
+ responseFormat: {
542
+ type: "json_schema",
543
+ jsonSchema: { name: "answer", schema: { type: "object" } },
544
+ },
545
+ tags: { env: "test", owner: "platform" },
546
+ collaborators: [
547
+ { subject: "user:ada@example.com", roleId: "agent-manager" },
548
+ ],
549
+ } as never);
550
+
551
+ expect(manifest.description).toBe("My agent");
552
+ expect(manifest.tags).toEqual({ env: "test", owner: "platform" });
553
+ expect(manifest.collaborators).toEqual([
554
+ { subject: "user:ada@example.com", role_id: "agent-manager" },
555
+ ]);
556
+ expect(manifest.variables).toEqual({ city: "Berlin" });
557
+ expect(manifest.messages).toEqual([
558
+ { role: "user", content: "Hello {{city}}" },
559
+ ]);
560
+ expect(manifest.response_format).toEqual({
561
+ type: "json_schema",
562
+ json_schema: { name: "answer", schema: { type: "object" } },
563
+ });
564
+ expect(manifest.model).toEqual({
565
+ name: "openai-main/gpt-4.1",
566
+ params: { max_tokens: 4096, temperature: 0.2 },
567
+ });
568
+ });
569
+
530
570
  it("normalizes UI catalog mounts and defaults enable_tools / preload", () => {
531
571
  const manifest = buildSaveAgentManifest("draft-save", {
532
572
  model: { name: "openai-main/gpt-4.1" },
@@ -572,7 +612,7 @@ describe("buildSaveAgentManifest", () => {
572
612
  describe("saveAgent", () => {
573
613
  it("PUTs { manifest } to /api/svc/v1/agents", async () => {
574
614
  const fetchMock = vi.fn(async () =>
575
- Response.json({ id: "ag_1", name: "my-agent" }),
615
+ Response.json({ data: { id: "ag_1", name: "my-agent" } }),
576
616
  );
577
617
  vi.stubGlobal("fetch", fetchMock);
578
618
 
@@ -581,10 +621,11 @@ describe("saveAgent", () => {
581
621
  {
582
622
  agentName: "my-agent",
583
623
  agentSpec: { model: { name: "openai-main/gpt-4.1" } },
624
+ intent: "create",
584
625
  },
585
626
  );
586
627
 
587
- expect(result).toEqual({ id: "ag_1", name: "my-agent" });
628
+ expect(result).toEqual({ agentId: "ag_1" });
588
629
  expect(fetchMock).toHaveBeenCalledWith(
589
630
  "https://cp.example/api/svc/v1/agents",
590
631
  expect.objectContaining({
@@ -604,7 +645,195 @@ describe("saveAgent", () => {
604
645
  expect(body.manifest.type).toBe("truefoundry-agent");
605
646
  expect(body.manifest.name).toBe("my-agent");
606
647
  expect(body.manifest.model).toEqual({ name: "openai-main/gpt-4.1" });
607
- expect(body.manifest.metadata_tags).toEqual(SAVE_AGENT_METADATA_TAGS);
648
+ expect(body.manifest.tags).toEqual(SAVE_AGENT_TAGS);
608
649
  expect(body.manifest.collaborators).toEqual([...SAVE_AGENT_COLLABORATORS]);
609
650
  });
651
+
652
+ it("extracts agentId and versionId from a wrapped CP version response", async () => {
653
+ vi.stubGlobal(
654
+ "fetch",
655
+ vi.fn(async () =>
656
+ Response.json({
657
+ data: { id: "ver_9", agentId: "ag_1", name: "my-agent" },
658
+ }),
659
+ ),
660
+ );
661
+
662
+ const result = await saveAgent(
663
+ { apiKey: "key", cpURL: "https://cp.example/" },
664
+ {
665
+ agentName: "my-agent",
666
+ agentSpec: { model: { name: "openai-main/gpt-4.1" } },
667
+ intent: "update",
668
+ },
669
+ );
670
+
671
+ expect(result).toEqual({ agentId: "ag_1", versionId: "ver_9" });
672
+ });
673
+ });
674
+
675
+ describe("saveAgentResultFromCp", () => {
676
+ it("reads { data: { id, name } } (no agentId) as the agent itself", () => {
677
+ expect(
678
+ saveAgentResultFromCp({ data: { id: "ag_1", name: "my-agent" } }),
679
+ ).toEqual({ agentId: "ag_1" });
680
+ });
681
+
682
+ it("reads { data: { id, agentId } } as version + agent", () => {
683
+ expect(
684
+ saveAgentResultFromCp({ data: { id: "ver_1", agentId: "ag_2" } }),
685
+ ).toEqual({ agentId: "ag_2", versionId: "ver_1" });
686
+ });
687
+
688
+ it("returns {} for malformed payloads", () => {
689
+ expect(saveAgentResultFromCp(null)).toEqual({});
690
+ expect(saveAgentResultFromCp({ id: "not-wrapped" })).toEqual({});
691
+ });
692
+ });
693
+
694
+ describe("old saved-agent manifest round-trip", () => {
695
+ const oldManifest = {
696
+ type: "truefoundry-agent",
697
+ name: "ask-ai-agent",
698
+ description: "Answer questions",
699
+ model: {
700
+ name: "openai-main/gpt-4.1",
701
+ params: { max_tokens: 8192, reasoning_effort: "medium" },
702
+ },
703
+ instructions: "Be helpful",
704
+ // Variable names are user keys — `my_city` must never become `myCity`.
705
+ variables: {
706
+ city: "Berlin",
707
+ my_city: { default_value: "Pune", description: "home town" },
708
+ },
709
+ messages: [{ role: "user", content: "Hello {{city}}" }],
710
+ // Uppercase tag keys are the regression case: snake-casing them once
711
+ // produced "_t_f_y__a_l_p_h_a__…" on the wire.
712
+ tags: {
713
+ env: "prod",
714
+ owner: "platform",
715
+ TFY_ALPHA_ENABLE_OPENUI: "true",
716
+ },
717
+ collaborators: [
718
+ { subject: "team:everyone", role_id: "agent-access" },
719
+ ],
720
+ response_format: {
721
+ type: "json_schema",
722
+ json_schema: {
723
+ name: "answer",
724
+ // Schema property names are user data — `user_name` must
725
+ // survive both directions verbatim.
726
+ schema: {
727
+ type: "object",
728
+ properties: { user_name: { type: "string" } },
729
+ },
730
+ },
731
+ },
732
+ config: {
733
+ iteration_limit: 50,
734
+ ask_user_questions: { enabled: true },
735
+ sandbox: { enabled: true, file_downloads: true },
736
+ },
737
+ mcp_servers: [
738
+ {
739
+ type: "truefoundry-mcp-registry",
740
+ name: "gmail",
741
+ enable_tools: ["@read-only"],
742
+ preload: true,
743
+ },
744
+ ],
745
+ skills: [
746
+ {
747
+ type: "truefoundry-skills-registry",
748
+ fqn: "agent-skill:tfy/skills/web:1",
749
+ preload: false,
750
+ },
751
+ ],
752
+ };
753
+
754
+ it("reads snake_case fields into camelCase spec and writes them back", () => {
755
+ const spec = agentSpecFromCpManifest(oldManifest);
756
+ expect(spec?.description).toBe("Answer questions");
757
+ // User keys verbatim; `{ default_value }` records collapse to strings.
758
+ expect(spec?.variables).toEqual({ city: "Berlin", my_city: "Pune" });
759
+ expect(spec?.messages).toEqual([
760
+ { role: "user", content: "Hello {{city}}" },
761
+ ]);
762
+ expect(spec?.tags).toEqual({
763
+ env: "prod",
764
+ owner: "platform",
765
+ TFY_ALPHA_ENABLE_OPENUI: "true",
766
+ });
767
+ expect(spec?.collaborators).toEqual([
768
+ { subject: "team:everyone", roleId: "agent-access" },
769
+ ]);
770
+ expect(spec?.responseFormat).toEqual({
771
+ type: "json_schema",
772
+ jsonSchema: {
773
+ name: "answer",
774
+ schema: {
775
+ type: "object",
776
+ properties: { user_name: { type: "string" } },
777
+ },
778
+ },
779
+ });
780
+ expect(spec?.model.params).toEqual({
781
+ maxTokens: 8192,
782
+ reasoningEffort: "medium",
783
+ });
784
+ expect(spec?.config).toEqual({
785
+ iterationLimit: 50,
786
+ askUserQuestions: { enabled: true },
787
+ sandbox: { enabled: true, fileDownloads: true },
788
+ });
789
+
790
+ const saved = buildSaveAgentManifest("ask-ai-agent", spec!);
791
+ expect(saved.description).toBe("Answer questions");
792
+ expect(saved.variables).toEqual({ city: "Berlin", my_city: "Pune" });
793
+ expect(saved.messages).toEqual([
794
+ { role: "user", content: "Hello {{city}}" },
795
+ ]);
796
+ expect(saved.tags).toEqual({
797
+ env: "prod",
798
+ owner: "platform",
799
+ TFY_ALPHA_ENABLE_OPENUI: "true",
800
+ });
801
+ expect(saved.collaborators).toEqual([
802
+ { subject: "team:everyone", role_id: "agent-access" },
803
+ ]);
804
+ expect(saved.response_format).toEqual({
805
+ type: "json_schema",
806
+ json_schema: {
807
+ name: "answer",
808
+ schema: {
809
+ type: "object",
810
+ properties: { user_name: { type: "string" } },
811
+ },
812
+ },
813
+ });
814
+ expect(saved.model).toEqual({
815
+ name: "openai-main/gpt-4.1",
816
+ params: { max_tokens: 8192, reasoning_effort: "medium" },
817
+ });
818
+ expect(saved.config).toEqual({
819
+ iteration_limit: 50,
820
+ ask_user_questions: { enabled: true },
821
+ sandbox: { enabled: true, file_downloads: true },
822
+ });
823
+ expect(saved.mcp_servers).toEqual([
824
+ {
825
+ type: "truefoundry-mcp-registry",
826
+ name: "gmail",
827
+ enable_tools: ["@read-only"],
828
+ preload: true,
829
+ },
830
+ ]);
831
+ expect(saved.skills).toEqual([
832
+ {
833
+ type: "truefoundry-skills-registry",
834
+ fqn: "agent-skill:tfy/skills/web:1",
835
+ preload: false,
836
+ },
837
+ ]);
838
+ });
610
839
  });