@dexto/server 1.6.8 → 1.6.9

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.
@@ -584,6 +584,96 @@ export declare function createLlmRouter(getAgent: GetAgentFn): OpenAPIHono<impor
584
584
  status: 404;
585
585
  };
586
586
  };
587
+ } & {
588
+ "/llm/model-picker-state": {
589
+ $get: {
590
+ input: {};
591
+ output: {
592
+ custom: {
593
+ model: string;
594
+ provider: "openai" | "openai-compatible" | "anthropic" | "google" | "groq" | "xai" | "cohere" | "minimax" | "glm" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | "dexto-nova";
595
+ supportedFileTypes: ("image" | "audio" | "pdf")[];
596
+ source: "custom" | "catalog" | "local-installed";
597
+ displayName?: string | undefined;
598
+ }[];
599
+ featured: {
600
+ model: string;
601
+ provider: "openai" | "openai-compatible" | "anthropic" | "google" | "groq" | "xai" | "cohere" | "minimax" | "glm" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | "dexto-nova";
602
+ supportedFileTypes: ("image" | "audio" | "pdf")[];
603
+ source: "custom" | "catalog" | "local-installed";
604
+ displayName?: string | undefined;
605
+ }[];
606
+ recents: {
607
+ model: string;
608
+ provider: "openai" | "openai-compatible" | "anthropic" | "google" | "groq" | "xai" | "cohere" | "minimax" | "glm" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | "dexto-nova";
609
+ supportedFileTypes: ("image" | "audio" | "pdf")[];
610
+ source: "custom" | "catalog" | "local-installed";
611
+ displayName?: string | undefined;
612
+ }[];
613
+ favorites: {
614
+ model: string;
615
+ provider: "openai" | "openai-compatible" | "anthropic" | "google" | "groq" | "xai" | "cohere" | "minimax" | "glm" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | "dexto-nova";
616
+ supportedFileTypes: ("image" | "audio" | "pdf")[];
617
+ source: "custom" | "catalog" | "local-installed";
618
+ displayName?: string | undefined;
619
+ }[];
620
+ };
621
+ outputFormat: "json";
622
+ status: 200;
623
+ };
624
+ };
625
+ } & {
626
+ "/llm/model-picker-state/recents": {
627
+ $post: {
628
+ input: {
629
+ json: {
630
+ model: string;
631
+ provider: "openai" | "openai-compatible" | "anthropic" | "google" | "groq" | "xai" | "cohere" | "minimax" | "glm" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | "dexto-nova";
632
+ };
633
+ };
634
+ output: {
635
+ ok: true;
636
+ };
637
+ outputFormat: "json";
638
+ status: 200;
639
+ };
640
+ };
641
+ } & {
642
+ "/llm/model-picker-state/favorites/toggle": {
643
+ $post: {
644
+ input: {
645
+ json: {
646
+ model: string;
647
+ provider: "openai" | "openai-compatible" | "anthropic" | "google" | "groq" | "xai" | "cohere" | "minimax" | "glm" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | "dexto-nova";
648
+ };
649
+ };
650
+ output: {
651
+ ok: true;
652
+ isFavorite: boolean;
653
+ };
654
+ outputFormat: "json";
655
+ status: 200;
656
+ };
657
+ };
658
+ } & {
659
+ "/llm/model-picker-state/favorites": {
660
+ $put: {
661
+ input: {
662
+ json: {
663
+ favorites: {
664
+ model: string;
665
+ provider: "openai" | "openai-compatible" | "anthropic" | "google" | "groq" | "xai" | "cohere" | "minimax" | "glm" | "openrouter" | "litellm" | "glama" | "vertex" | "bedrock" | "local" | "ollama" | "dexto-nova";
666
+ }[];
667
+ };
668
+ };
669
+ output: {
670
+ ok: true;
671
+ count: number;
672
+ };
673
+ outputFormat: "json";
674
+ status: 200;
675
+ };
676
+ };
587
677
  } & {
588
678
  "/llm/capabilities": {
589
679
  $get: {
@@ -1 +1 @@
1
- {"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../../src/hono/routes/llm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAwB9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAOpC,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAmFrE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCA0J5C,CAAP;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCA5JA,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAohBD"}
1
+ {"version":3,"file":"llm.d.ts","sourceRoot":"","sources":["../../../src/hono/routes/llm.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAkB,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAkC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAQpC,KAAK,UAAU,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AA0IrE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCAkGhC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oCAnKS,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA24B9B"}
@@ -1,5 +1,5 @@
1
1
  import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
2
- import { DextoRuntimeError, ErrorScope, ErrorType } from "@dexto/core";
2
+ import { DextoRuntimeError, ErrorScope, ErrorType, logger } from "@dexto/core";
3
3
  import {
4
4
  LLM_REGISTRY,
5
5
  LLM_PROVIDERS,
@@ -7,7 +7,9 @@ import {
7
7
  supportsBaseURL,
8
8
  getAllModelsForProvider,
9
9
  getCuratedModelsForProvider,
10
+ getCuratedModelRefsForProviders,
10
11
  getSupportedFileTypesForModel,
12
+ getLocalModelById,
11
13
  getReasoningProfile,
12
14
  LLMUpdatesSchema
13
15
  } from "@dexto/core";
@@ -16,14 +18,24 @@ import {
16
18
  loadCustomModels,
17
19
  saveCustomModel,
18
20
  deleteCustomModel,
21
+ loadModelPickerState,
22
+ saveModelPickerState,
23
+ recordRecentModel,
24
+ toggleFavoriteModel,
25
+ setFavoriteModels,
26
+ pruneModelPickerState,
27
+ toModelPickerKey,
28
+ getAllInstalledModels,
19
29
  CustomModelSchema,
20
30
  isDextoAuthEnabled
21
31
  } from "@dexto/agent-management";
22
32
  import {
23
33
  ProviderCatalogSchema,
24
34
  ModelFlatSchema,
25
- LLMConfigResponseSchema
35
+ LLMConfigResponseSchema,
36
+ StandardErrorEnvelopeSchema
26
37
  } from "../schemas/responses.js";
38
+ const MODEL_PICKER_FEATURED_LIMIT = 8;
27
39
  const CurrentQuerySchema = z.object({
28
40
  sessionId: z.string().optional().describe("Session identifier to retrieve session-specific LLM configuration")
29
41
  }).strict().describe("Query parameters for getting current LLM configuration");
@@ -51,6 +63,46 @@ const SwitchLLMBodySchema = LLMUpdatesSchema.and(
51
63
  sessionId: z.string().optional().describe("Session identifier for session-specific LLM configuration")
52
64
  })
53
65
  ).describe("LLM switch request body with optional session ID and LLM fields");
66
+ const ModelPickerModelRefSchema = z.object({
67
+ provider: z.enum(LLM_PROVIDERS).describe("LLM provider"),
68
+ model: z.string().trim().min(1).describe("Model ID")
69
+ }).strict().describe("Provider/model pair for model picker state operations");
70
+ const ModelPickerEntrySchema = z.object({
71
+ provider: z.enum(LLM_PROVIDERS).describe("LLM provider"),
72
+ model: z.string().describe("Model ID"),
73
+ displayName: z.string().optional().describe("Human-readable model name"),
74
+ supportedFileTypes: z.array(z.enum(SUPPORTED_FILE_TYPES)).describe("File types supported by this model"),
75
+ source: z.enum(["catalog", "custom", "local-installed"]).describe("Where this model comes from")
76
+ }).strict().describe("Hydrated model picker entry");
77
+ const ModelPickerErrorSchema = StandardErrorEnvelopeSchema.describe(
78
+ "Standard error response for model picker endpoints"
79
+ );
80
+ const ModelPickerErrorResponses = {
81
+ 400: {
82
+ description: "Validation or request error",
83
+ content: {
84
+ "application/json": {
85
+ schema: ModelPickerErrorSchema
86
+ }
87
+ }
88
+ },
89
+ 404: {
90
+ description: "Resource not found",
91
+ content: {
92
+ "application/json": {
93
+ schema: ModelPickerErrorSchema
94
+ }
95
+ }
96
+ },
97
+ 500: {
98
+ description: "Internal server error",
99
+ content: {
100
+ "application/json": {
101
+ schema: ModelPickerErrorSchema
102
+ }
103
+ }
104
+ }
105
+ };
54
106
  function createLlmRouter(getAgent) {
55
107
  const app = new OpenAPIHono();
56
108
  const currentRoute = createRoute({
@@ -284,6 +336,217 @@ function createLlmRouter(getAgent) {
284
336
  }
285
337
  }
286
338
  });
339
+ const modelPickerStateRoute = createRoute({
340
+ method: "get",
341
+ path: "/llm/model-picker-state",
342
+ summary: "Model Picker State",
343
+ description: "Returns hydrated Featured, Recents, Favorites, and Custom sections for the model picker.",
344
+ tags: ["llm"],
345
+ responses: {
346
+ 200: {
347
+ description: "Hydrated model picker sections",
348
+ content: {
349
+ "application/json": {
350
+ schema: z.object({
351
+ featured: z.array(ModelPickerEntrySchema).describe("Curated featured models"),
352
+ recents: z.array(ModelPickerEntrySchema).describe("Most recently used models"),
353
+ favorites: z.array(ModelPickerEntrySchema).describe("User favorited models"),
354
+ custom: z.array(ModelPickerEntrySchema).describe("User-defined custom models")
355
+ }).strict()
356
+ }
357
+ }
358
+ },
359
+ ...ModelPickerErrorResponses
360
+ }
361
+ });
362
+ const recordRecentModelRoute = createRoute({
363
+ method: "post",
364
+ path: "/llm/model-picker-state/recents",
365
+ summary: "Record Recent Model",
366
+ description: "Records a model selection in recents.",
367
+ tags: ["llm"],
368
+ request: {
369
+ body: {
370
+ required: true,
371
+ content: {
372
+ "application/json": {
373
+ schema: ModelPickerModelRefSchema
374
+ }
375
+ }
376
+ }
377
+ },
378
+ responses: {
379
+ 200: {
380
+ description: "Recent model recorded",
381
+ content: {
382
+ "application/json": {
383
+ schema: z.object({
384
+ ok: z.literal(true).describe("Success indicator")
385
+ }).strict()
386
+ }
387
+ }
388
+ },
389
+ ...ModelPickerErrorResponses
390
+ }
391
+ });
392
+ const toggleFavoriteModelRoute = createRoute({
393
+ method: "post",
394
+ path: "/llm/model-picker-state/favorites/toggle",
395
+ summary: "Toggle Favorite Model",
396
+ description: "Adds or removes a model from favorites.",
397
+ tags: ["llm"],
398
+ request: {
399
+ body: {
400
+ required: true,
401
+ content: {
402
+ "application/json": {
403
+ schema: ModelPickerModelRefSchema
404
+ }
405
+ }
406
+ }
407
+ },
408
+ responses: {
409
+ 200: {
410
+ description: "Favorite toggled",
411
+ content: {
412
+ "application/json": {
413
+ schema: z.object({
414
+ ok: z.literal(true).describe("Success indicator"),
415
+ isFavorite: z.boolean().describe("Whether the model is now favorited")
416
+ }).strict()
417
+ }
418
+ }
419
+ },
420
+ ...ModelPickerErrorResponses
421
+ }
422
+ });
423
+ const setFavoritesRoute = createRoute({
424
+ method: "put",
425
+ path: "/llm/model-picker-state/favorites",
426
+ summary: "Set Favorite Models",
427
+ description: "Replaces favorite models list. Used by migration or bulk updates.",
428
+ tags: ["llm"],
429
+ request: {
430
+ body: {
431
+ required: true,
432
+ content: {
433
+ "application/json": {
434
+ schema: z.object({
435
+ favorites: z.array(ModelPickerModelRefSchema).describe("Complete list of favorite model references")
436
+ }).strict()
437
+ }
438
+ }
439
+ }
440
+ },
441
+ responses: {
442
+ 200: {
443
+ description: "Favorites updated",
444
+ content: {
445
+ "application/json": {
446
+ schema: z.object({
447
+ ok: z.literal(true).describe("Success indicator"),
448
+ count: z.number().int().nonnegative().describe("Number of favorites persisted")
449
+ }).strict()
450
+ }
451
+ }
452
+ },
453
+ ...ModelPickerErrorResponses
454
+ }
455
+ });
456
+ const isProviderEnabled = (provider) => provider !== "dexto-nova" || isDextoAuthEnabled();
457
+ const dedupeEntries = (entries) => {
458
+ const seen = /* @__PURE__ */ new Set();
459
+ const deduped = [];
460
+ for (const entry of entries) {
461
+ const key = toModelPickerKey(entry);
462
+ if (seen.has(key)) {
463
+ continue;
464
+ }
465
+ seen.add(key);
466
+ deduped.push(entry);
467
+ }
468
+ return deduped;
469
+ };
470
+ const buildModelPickerSections = async () => {
471
+ const byKey = /* @__PURE__ */ new Map();
472
+ const customSection = [];
473
+ for (const provider of LLM_PROVIDERS) {
474
+ if (!isProviderEnabled(provider)) {
475
+ continue;
476
+ }
477
+ const providerInfo = LLM_REGISTRY[provider];
478
+ for (const model of getAllModelsForProvider(provider)) {
479
+ const supportedFileTypes = Array.isArray(model.supportedFileTypes) && model.supportedFileTypes.length > 0 ? model.supportedFileTypes : providerInfo.supportedFileTypes;
480
+ const entry = {
481
+ provider,
482
+ model: model.name,
483
+ displayName: model.displayName || model.name,
484
+ supportedFileTypes,
485
+ source: "catalog"
486
+ };
487
+ const key = toModelPickerKey(entry);
488
+ if (!byKey.has(key)) {
489
+ byKey.set(key, entry);
490
+ }
491
+ }
492
+ }
493
+ const customModels = await loadCustomModels();
494
+ for (const customModel of customModels) {
495
+ const provider = customModel.provider ?? "openai-compatible";
496
+ if (!isProviderEnabled(provider)) {
497
+ continue;
498
+ }
499
+ const providerInfo = LLM_REGISTRY[provider];
500
+ const entry = {
501
+ provider,
502
+ model: customModel.name,
503
+ displayName: customModel.displayName || customModel.name,
504
+ supportedFileTypes: providerInfo?.supportedFileTypes ?? [],
505
+ source: "custom"
506
+ };
507
+ byKey.set(toModelPickerKey(entry), entry);
508
+ customSection.push(entry);
509
+ }
510
+ const localProviderSupportedFileTypes = LLM_REGISTRY.local.supportedFileTypes;
511
+ const installedLocalModels = await getAllInstalledModels();
512
+ for (const installedModel of installedLocalModels) {
513
+ const modelInfo = getLocalModelById(installedModel.id);
514
+ const entry = {
515
+ provider: "local",
516
+ model: installedModel.id,
517
+ displayName: modelInfo?.name || installedModel.id,
518
+ supportedFileTypes: localProviderSupportedFileTypes,
519
+ source: "local-installed"
520
+ };
521
+ byKey.set(toModelPickerKey(entry), entry);
522
+ }
523
+ const featuredProviders = LLM_PROVIDERS.filter((provider) => isProviderEnabled(provider));
524
+ const featured = getCuratedModelRefsForProviders({
525
+ providers: featuredProviders,
526
+ max: MODEL_PICKER_FEATURED_LIMIT
527
+ }).map((ref) => byKey.get(toModelPickerKey(ref))).filter((entry) => Boolean(entry));
528
+ const state = await loadModelPickerState();
529
+ const pruned = pruneModelPickerState({
530
+ state,
531
+ allowedKeys: new Set(byKey.keys())
532
+ });
533
+ const shouldPersistPrunedState = state.recents.length !== pruned.recents.length || state.favorites.length !== pruned.favorites.length;
534
+ if (shouldPersistPrunedState) {
535
+ void saveModelPickerState(pruned).catch((error) => {
536
+ logger.warn(
537
+ `Failed to persist pruned model picker state: ${error instanceof Error ? error.message : String(error)}`
538
+ );
539
+ });
540
+ }
541
+ const recents = pruned.recents.map((entry) => byKey.get(toModelPickerKey(entry))).filter((entry) => Boolean(entry));
542
+ const favorites = pruned.favorites.map((entry) => byKey.get(toModelPickerKey(entry))).filter((entry) => Boolean(entry));
543
+ return {
544
+ featured: dedupeEntries(featured),
545
+ recents,
546
+ favorites,
547
+ custom: dedupeEntries(customSection)
548
+ };
549
+ };
287
550
  return app.openapi(currentRoute, async (ctx) => {
288
551
  const agent = await getAgent(ctx);
289
552
  const { sessionId } = ctx.req.valid("query");
@@ -405,6 +668,13 @@ function createLlmRouter(getAgent) {
405
668
  const raw = ctx.req.valid("json");
406
669
  const { sessionId, ...llmUpdates } = raw;
407
670
  const config = await agent.switchLLM(llmUpdates, sessionId);
671
+ try {
672
+ await recordRecentModel({
673
+ provider: config.provider,
674
+ model: config.model
675
+ });
676
+ } catch {
677
+ }
408
678
  const { apiKey, ...configWithoutKey } = config;
409
679
  return ctx.json({
410
680
  config: {
@@ -434,6 +704,29 @@ function createLlmRouter(getAgent) {
434
704
  );
435
705
  }
436
706
  return ctx.json({ ok: true, deleted: name }, 200);
707
+ }).openapi(modelPickerStateRoute, async (ctx) => {
708
+ const sections = await buildModelPickerSections();
709
+ return ctx.json(sections);
710
+ }).openapi(recordRecentModelRoute, async (ctx) => {
711
+ const modelRef = ctx.req.valid("json");
712
+ await recordRecentModel(modelRef);
713
+ return ctx.json({ ok: true });
714
+ }).openapi(toggleFavoriteModelRoute, async (ctx) => {
715
+ const modelRef = ctx.req.valid("json");
716
+ const result = await toggleFavoriteModel(modelRef);
717
+ return ctx.json({
718
+ ok: true,
719
+ isFavorite: result.isFavorite
720
+ });
721
+ }).openapi(setFavoritesRoute, async (ctx) => {
722
+ const payload = ctx.req.valid("json");
723
+ const state = await setFavoriteModels({
724
+ favorites: payload.favorites
725
+ });
726
+ return ctx.json({
727
+ ok: true,
728
+ count: state.favorites.length
729
+ });
437
730
  }).openapi(capabilitiesRoute, (ctx) => {
438
731
  const { provider, model } = ctx.req.valid("query");
439
732
  let supportedFileTypes;
@@ -26,6 +26,17 @@ var import_responses = require("../schemas/responses.js");
26
26
  const CreateSessionSchema = import_zod_openapi.z.object({
27
27
  sessionId: import_zod_openapi.z.string().optional().describe("A custom ID for the new session")
28
28
  }).describe("Request body for creating a new session");
29
+ function mapSessionMetadata(sessionId, metadata, defaults) {
30
+ return {
31
+ id: sessionId,
32
+ createdAt: metadata?.createdAt ?? defaults?.createdAt ?? null,
33
+ lastActivity: metadata?.lastActivity ?? defaults?.lastActivity ?? null,
34
+ messageCount: metadata?.messageCount ?? defaults?.messageCount ?? 0,
35
+ title: metadata?.title ?? defaults?.title ?? null,
36
+ workspaceId: metadata?.workspaceId ?? defaults?.workspaceId ?? null,
37
+ parentSessionId: metadata?.parentSessionId ?? defaults?.parentSessionId ?? null
38
+ };
39
+ }
29
40
  function createSessionsRouter(getAgent) {
30
41
  const app = new import_zod_openapi.OpenAPIHono();
31
42
  const listRoute = (0, import_zod_openapi.createRoute)({
@@ -91,6 +102,48 @@ function createSessionsRouter(getAgent) {
91
102
  }
92
103
  }
93
104
  });
105
+ const forkRoute = (0, import_zod_openapi.createRoute)({
106
+ method: "post",
107
+ path: "/sessions/{sessionId}/fork",
108
+ summary: "Fork Session",
109
+ description: "Creates a new child session by cloning the specified parent session history and metadata lineage.",
110
+ tags: ["sessions"],
111
+ request: {
112
+ params: import_zod_openapi.z.object({
113
+ sessionId: import_zod_openapi.z.string().describe("Parent session identifier")
114
+ })
115
+ },
116
+ responses: {
117
+ 201: {
118
+ description: "Forked session created successfully",
119
+ content: {
120
+ "application/json": {
121
+ schema: import_zod_openapi.z.object({
122
+ session: import_responses.SessionMetadataSchema.describe(
123
+ "Newly created child session metadata"
124
+ )
125
+ }).strict()
126
+ }
127
+ }
128
+ },
129
+ 400: {
130
+ description: "Invalid fork request (for example, max session limit reached)",
131
+ content: {
132
+ "application/json": {
133
+ schema: import_responses.StandardErrorEnvelopeSchema
134
+ }
135
+ }
136
+ },
137
+ 404: {
138
+ description: "Parent session not found",
139
+ content: {
140
+ "application/json": {
141
+ schema: import_responses.StandardErrorEnvelopeSchema
142
+ }
143
+ }
144
+ }
145
+ }
146
+ });
94
147
  const historyRoute = (0, import_zod_openapi.createRoute)({
95
148
  method: "get",
96
149
  path: "/sessions/{sessionId}/history",
@@ -274,23 +327,9 @@ function createSessionsRouter(getAgent) {
274
327
  sessionIds.map(async (id) => {
275
328
  try {
276
329
  const metadata = await agent.getSessionMetadata(id);
277
- return {
278
- id,
279
- createdAt: metadata?.createdAt || null,
280
- lastActivity: metadata?.lastActivity || null,
281
- messageCount: metadata?.messageCount || 0,
282
- title: metadata?.title || null,
283
- workspaceId: metadata?.workspaceId || null
284
- };
330
+ return mapSessionMetadata(id, metadata);
285
331
  } catch {
286
- return {
287
- id,
288
- createdAt: null,
289
- lastActivity: null,
290
- messageCount: 0,
291
- title: null,
292
- workspaceId: null
293
- };
332
+ return mapSessionMetadata(id, void 0);
294
333
  }
295
334
  })
296
335
  );
@@ -302,14 +341,21 @@ function createSessionsRouter(getAgent) {
302
341
  const metadata = await agent.getSessionMetadata(session.id);
303
342
  return ctx.json(
304
343
  {
305
- session: {
306
- id: session.id,
307
- createdAt: metadata?.createdAt || Date.now(),
308
- lastActivity: metadata?.lastActivity || Date.now(),
309
- messageCount: metadata?.messageCount || 0,
310
- title: metadata?.title || null,
311
- workspaceId: metadata?.workspaceId || null
312
- }
344
+ session: mapSessionMetadata(session.id, metadata, {
345
+ createdAt: Date.now(),
346
+ lastActivity: Date.now()
347
+ })
348
+ },
349
+ 201
350
+ );
351
+ }).openapi(forkRoute, async (ctx) => {
352
+ const agent = await getAgent(ctx);
353
+ const { sessionId: parentSessionId } = ctx.req.valid("param");
354
+ const session = await agent.forkSession(parentSessionId);
355
+ const metadata = await agent.getSessionMetadata(session.id);
356
+ return ctx.json(
357
+ {
358
+ session: mapSessionMetadata(session.id, metadata)
313
359
  },
314
360
  201
315
361
  );
@@ -320,12 +366,7 @@ function createSessionsRouter(getAgent) {
320
366
  const history = await agent.getSessionHistory(sessionId);
321
367
  return ctx.json({
322
368
  session: {
323
- id: sessionId,
324
- createdAt: metadata?.createdAt || null,
325
- lastActivity: metadata?.lastActivity || null,
326
- messageCount: metadata?.messageCount || 0,
327
- title: metadata?.title || null,
328
- workspaceId: metadata?.workspaceId || null,
369
+ ...mapSessionMetadata(sessionId, metadata),
329
370
  history: history.length
330
371
  }
331
372
  });
@@ -386,12 +427,7 @@ function createSessionsRouter(getAgent) {
386
427
  return ctx.json(
387
428
  {
388
429
  session: {
389
- id: sessionId,
390
- createdAt: metadata?.createdAt || null,
391
- lastActivity: metadata?.lastActivity || null,
392
- messageCount: metadata?.messageCount || 0,
393
- title: metadata?.title || null,
394
- workspaceId: metadata?.workspaceId || null,
430
+ ...mapSessionMetadata(sessionId, metadata),
395
431
  isBusy
396
432
  }
397
433
  },
@@ -404,14 +440,7 @@ function createSessionsRouter(getAgent) {
404
440
  await agent.setSessionTitle(sessionId, title);
405
441
  const metadata = await agent.getSessionMetadata(sessionId);
406
442
  return ctx.json({
407
- session: {
408
- id: sessionId,
409
- createdAt: metadata?.createdAt || null,
410
- lastActivity: metadata?.lastActivity || null,
411
- messageCount: metadata?.messageCount || 0,
412
- title: metadata?.title || title,
413
- workspaceId: metadata?.workspaceId || null
414
- }
443
+ session: mapSessionMetadata(sessionId, metadata, { title })
415
444
  });
416
445
  }).openapi(generateTitleRoute, async (ctx) => {
417
446
  const agent = await getAgent(ctx);