@pasko70/pibo 2.2.2 → 2.2.3

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.
@@ -15,6 +15,7 @@ export function previewCustomAgentCreate(input, options = {}) {
15
15
  profileName: input.displayName,
16
16
  displayName: input.displayName,
17
17
  profileAliases: [],
18
+ folderId: input.folderId,
18
19
  description: input.description,
19
20
  runtimeInstanceId: sanitizeRuntimeInstanceId(input.runtimeInstanceId),
20
21
  runtimeOptions: cloneRuntimeOptions(input.runtimeOptions),
@@ -48,6 +49,7 @@ export function previewCustomAgentUpdate(existing, input, options = {}) {
48
49
  ...existing,
49
50
  profileName,
50
51
  displayName: input.displayName ?? existing.displayName,
52
+ folderId: input.folderId === undefined ? existing.folderId : input.folderId ?? undefined,
51
53
  description: input.description === undefined ? existing.description : input.description,
52
54
  runtimeInstanceId: input.runtimeInstanceId === undefined ? existing.runtimeInstanceId : sanitizeRuntimeInstanceId(input.runtimeInstanceId),
53
55
  runtimeOptions: input.runtimeOptions === undefined ? existing.runtimeOptions : cloneRuntimeOptions(input.runtimeOptions),
@@ -88,11 +90,19 @@ export class CustomAgentStore {
88
90
  if (resolvedPath !== ":memory:")
89
91
  this.db.exec("PRAGMA journal_mode = WAL");
90
92
  this.db.exec(`
93
+ CREATE TABLE IF NOT EXISTS chat_agent_folders (
94
+ id TEXT PRIMARY KEY,
95
+ name TEXT NOT NULL COLLATE NOCASE UNIQUE,
96
+ created_at TEXT NOT NULL,
97
+ updated_at TEXT NOT NULL
98
+ );
99
+
91
100
  CREATE TABLE IF NOT EXISTS chat_agents (
92
101
  id TEXT PRIMARY KEY,
93
102
  profile_name TEXT NOT NULL UNIQUE,
94
103
  display_name TEXT NOT NULL,
95
104
  description TEXT,
105
+ folder_id TEXT REFERENCES chat_agent_folders(id) ON DELETE SET NULL,
96
106
  runtime_instance_id TEXT NOT NULL DEFAULT 'pi',
97
107
  runtime_options_json TEXT NOT NULL DEFAULT '{}',
98
108
  native_subagents INTEGER,
@@ -121,6 +131,7 @@ export class CustomAgentStore {
121
131
  );
122
132
 
123
133
  `);
134
+ this.migrateAgentFolderColumn();
124
135
  this.migrateProfileAliasTable();
125
136
  this.migrateArchivedAtColumn();
126
137
  this.migrateAutoContextFilesColumn();
@@ -137,6 +148,38 @@ export class CustomAgentStore {
137
148
  this.migrateLegacyProfileNames();
138
149
  this.migrateDuplicateProfileNames();
139
150
  }
151
+ listFolders() {
152
+ const rows = this.db.prepare("SELECT * FROM chat_agent_folders ORDER BY name COLLATE NOCASE ASC, created_at ASC").all();
153
+ return rows.map(folderFromRow);
154
+ }
155
+ getFolder(id) {
156
+ const row = this.db.prepare("SELECT * FROM chat_agent_folders WHERE id = ?").get(id);
157
+ return row ? folderFromRow(row) : undefined;
158
+ }
159
+ createFolder(name) {
160
+ const normalizedName = normalizeCustomAgentFolderName(name);
161
+ this.requireFolderNameAvailable(normalizedName);
162
+ const now = new Date().toISOString();
163
+ const id = `agent_folder_${randomUUID()}`;
164
+ this.db.prepare("INSERT INTO chat_agent_folders (id, name, created_at, updated_at) VALUES (?, ?, ?, ?)").run(id, normalizedName, now, now);
165
+ return this.getFolder(id);
166
+ }
167
+ renameFolder(id, name) {
168
+ const existing = this.getFolder(id);
169
+ if (!existing)
170
+ return undefined;
171
+ const normalizedName = normalizeCustomAgentFolderName(name);
172
+ this.requireFolderNameAvailable(normalizedName, id);
173
+ this.db.prepare("UPDATE chat_agent_folders SET name = ?, updated_at = ? WHERE id = ?").run(normalizedName, new Date().toISOString(), id);
174
+ return this.getFolder(id);
175
+ }
176
+ deleteFolder(id) {
177
+ const assigned = this.db.prepare("SELECT COUNT(*) AS count FROM chat_agents WHERE folder_id = ?").get(id);
178
+ if (assigned.count > 0)
179
+ throw new Error("Move agents out of this folder before deleting it");
180
+ const result = this.db.prepare("DELETE FROM chat_agent_folders WHERE id = ?").run(id);
181
+ return Number(result.changes ?? 0) > 0;
182
+ }
140
183
  list(options = {}) {
141
184
  this.migrateLegacyProfileNames();
142
185
  this.migrateDuplicateProfileNames();
@@ -153,6 +196,7 @@ export class CustomAgentStore {
153
196
  create(input) {
154
197
  this.migrateLegacyProfileNames();
155
198
  this.requireProfileNameAvailable(input.displayName);
199
+ this.requireFolderExists(input.folderId);
156
200
  const agent = previewCustomAgentCreate(input);
157
201
  this.insert(agent);
158
202
  const created = this.get(agent.id);
@@ -167,6 +211,7 @@ export class CustomAgentStore {
167
211
  return undefined;
168
212
  const profileName = input.displayName ?? existing.displayName;
169
213
  this.requireProfileNameAvailable(profileName, id);
214
+ this.requireFolderExists(input.folderId);
170
215
  const updated = previewCustomAgentUpdate(existing, input);
171
216
  this.db
172
217
  .prepare(`
@@ -174,6 +219,7 @@ export class CustomAgentStore {
174
219
  profile_name = ?,
175
220
  display_name = ?,
176
221
  description = ?,
222
+ folder_id = ?,
177
223
  runtime_instance_id = ?,
178
224
  runtime_options_json = ?,
179
225
  native_subagents = ?,
@@ -199,7 +245,7 @@ export class CustomAgentStore {
199
245
  updated_at = ?
200
246
  WHERE id = ?
201
247
  `)
202
- .run(updated.profileName, updated.displayName, updated.description ?? null, updated.runtimeInstanceId, JSON.stringify(updated.runtimeOptions), serializeBoolean(updated.nativeSubagents), JSON.stringify(updated.nativeTools), JSON.stringify(updated.skills), JSON.stringify(updated.contextFiles), JSON.stringify(sanitizeSubagents(updated.subagents)), JSON.stringify(updated.mcpServers), JSON.stringify(updated.piPackages), updated.mainModel ? JSON.stringify(updated.mainModel) : null, updated.subagentModel ? JSON.stringify(updated.subagentModel) : null, updated.thinkingLevel ?? null, updated.mainThinkingLevel ?? null, updated.subagentThinkingLevel ?? null, serializeBoolean(updated.fast), serializeBoolean(updated.mainFast), serializeBoolean(updated.subagentFast), updated.builtinTools, JSON.stringify(updated.builtinToolNames), updated.autoContextFiles ? 1 : 0, updated.runControl ? 1 : 0, updated.goalControl ? 1 : 0, updated.updatedAt, id);
248
+ .run(updated.profileName, updated.displayName, updated.description ?? null, updated.folderId ?? null, updated.runtimeInstanceId, JSON.stringify(updated.runtimeOptions), serializeBoolean(updated.nativeSubagents), JSON.stringify(updated.nativeTools), JSON.stringify(updated.skills), JSON.stringify(updated.contextFiles), JSON.stringify(sanitizeSubagents(updated.subagents)), JSON.stringify(updated.mcpServers), JSON.stringify(updated.piPackages), updated.mainModel ? JSON.stringify(updated.mainModel) : null, updated.subagentModel ? JSON.stringify(updated.subagentModel) : null, updated.thinkingLevel ?? null, updated.mainThinkingLevel ?? null, updated.subagentThinkingLevel ?? null, serializeBoolean(updated.fast), serializeBoolean(updated.mainFast), serializeBoolean(updated.subagentFast), updated.builtinTools, JSON.stringify(updated.builtinToolNames), updated.autoContextFiles ? 1 : 0, updated.runControl ? 1 : 0, updated.goalControl ? 1 : 0, updated.updatedAt, id);
203
249
  return this.get(id);
204
250
  }
205
251
  setArchived(id, archived) {
@@ -229,6 +275,7 @@ export class CustomAgentStore {
229
275
  profile_name,
230
276
  display_name,
231
277
  description,
278
+ folder_id,
232
279
  runtime_instance_id,
233
280
  runtime_options_json,
234
281
  native_subagents,
@@ -254,9 +301,20 @@ export class CustomAgentStore {
254
301
  created_at,
255
302
  updated_at,
256
303
  archived_at
257
- ) VALUES (${Array.from({ length: 29 }, () => "?").join(", ")})
304
+ ) VALUES (${Array.from({ length: 30 }, () => "?").join(", ")})
258
305
  `)
259
- .run(agent.id, agent.profileName, agent.displayName, agent.description ?? null, agent.runtimeInstanceId, JSON.stringify(agent.runtimeOptions), serializeBoolean(agent.nativeSubagents), JSON.stringify(agent.nativeTools), JSON.stringify(agent.skills), JSON.stringify(agent.contextFiles), JSON.stringify(sanitizeSubagents(agent.subagents)), JSON.stringify(agent.mcpServers), JSON.stringify(agent.piPackages), agent.mainModel ? JSON.stringify(agent.mainModel) : null, agent.subagentModel ? JSON.stringify(agent.subagentModel) : null, agent.thinkingLevel ?? null, agent.mainThinkingLevel ?? null, agent.subagentThinkingLevel ?? null, serializeBoolean(agent.fast), serializeBoolean(agent.mainFast), serializeBoolean(agent.subagentFast), agent.builtinTools, JSON.stringify(agent.builtinToolNames), agent.autoContextFiles ? 1 : 0, agent.runControl ? 1 : 0, agent.goalControl ? 1 : 0, agent.createdAt, agent.updatedAt, agent.archivedAt ?? null);
306
+ .run(agent.id, agent.profileName, agent.displayName, agent.description ?? null, agent.folderId ?? null, agent.runtimeInstanceId, JSON.stringify(agent.runtimeOptions), serializeBoolean(agent.nativeSubagents), JSON.stringify(agent.nativeTools), JSON.stringify(agent.skills), JSON.stringify(agent.contextFiles), JSON.stringify(sanitizeSubagents(agent.subagents)), JSON.stringify(agent.mcpServers), JSON.stringify(agent.piPackages), agent.mainModel ? JSON.stringify(agent.mainModel) : null, agent.subagentModel ? JSON.stringify(agent.subagentModel) : null, agent.thinkingLevel ?? null, agent.mainThinkingLevel ?? null, agent.subagentThinkingLevel ?? null, serializeBoolean(agent.fast), serializeBoolean(agent.mainFast), serializeBoolean(agent.subagentFast), agent.builtinTools, JSON.stringify(agent.builtinToolNames), agent.autoContextFiles ? 1 : 0, agent.runControl ? 1 : 0, agent.goalControl ? 1 : 0, agent.createdAt, agent.updatedAt, agent.archivedAt ?? null);
307
+ }
308
+ requireFolderExists(folderId) {
309
+ if (folderId === undefined || folderId === null)
310
+ return;
311
+ if (!this.getFolder(folderId))
312
+ throw new Error(`Agent folder "${folderId}" does not exist`);
313
+ }
314
+ requireFolderNameAvailable(name, currentId) {
315
+ const row = this.db.prepare("SELECT id FROM chat_agent_folders WHERE name = ? COLLATE NOCASE").get(name);
316
+ if (row && row.id !== currentId)
317
+ throw new Error(`Agent folder "${name}" already exists`);
260
318
  }
261
319
  requireProfileNameAvailable(profileName, currentId) {
262
320
  const row = this.db.prepare("SELECT id FROM chat_agents WHERE profile_name = ?").get(profileName);
@@ -306,6 +364,12 @@ export class CustomAgentStore {
306
364
  .run(nextName, nextName, row.id);
307
365
  }
308
366
  }
367
+ migrateAgentFolderColumn() {
368
+ if (!this.tableColumns().has("folder_id")) {
369
+ this.db.prepare("ALTER TABLE chat_agents ADD COLUMN folder_id TEXT REFERENCES chat_agent_folders(id) ON DELETE SET NULL").run();
370
+ }
371
+ this.db.exec("CREATE INDEX IF NOT EXISTS chat_agents_folder_id_idx ON chat_agents(folder_id)");
372
+ }
309
373
  migrateProfileAliasTable() {
310
374
  this.db.exec(`
311
375
  CREATE TABLE IF NOT EXISTS chat_agent_profile_aliases (
@@ -573,6 +637,7 @@ function agentFromRow(row, profileAliases) {
573
637
  profileName: row.profile_name,
574
638
  displayName: row.display_name,
575
639
  profileAliases: profileAliases.filter((alias) => alias !== row.profile_name),
640
+ folderId: row.folder_id ?? undefined,
576
641
  description: row.description ?? undefined,
577
642
  runtimeInstanceId: sanitizeRuntimeInstanceId(row.runtime_instance_id),
578
643
  runtimeOptions: parseRuntimeOptions(row.runtime_options_json),
@@ -601,6 +666,22 @@ function agentFromRow(row, profileAliases) {
601
666
  archivedAt: row.archived_at ?? undefined,
602
667
  };
603
668
  }
669
+ function folderFromRow(row) {
670
+ return {
671
+ id: row.id,
672
+ name: row.name,
673
+ createdAt: row.created_at,
674
+ updatedAt: row.updated_at,
675
+ };
676
+ }
677
+ export function normalizeCustomAgentFolderName(value) {
678
+ const name = value.replace(/\s+/g, " ").trim();
679
+ if (!name)
680
+ throw new Error("Agent folder name is required");
681
+ if (name.length > 80)
682
+ throw new Error("Agent folder name is too long");
683
+ return name;
684
+ }
604
685
  function parseStringArray(value) {
605
686
  try {
606
687
  const parsed = JSON.parse(value);
@@ -21,6 +21,20 @@ export function roomResourcePath(pathname) {
21
21
  throw new PiboWebHttpError("Invalid room id", 400);
22
22
  }
23
23
  }
24
+ export function agentFolderResourceId(pathname) {
25
+ const prefix = `${CHAT_WEB_API_PREFIX}/agent-folders/`;
26
+ if (!pathname.startsWith(prefix))
27
+ return undefined;
28
+ const encodedId = pathname.slice(prefix.length);
29
+ if (!encodedId || encodedId.includes("/"))
30
+ return undefined;
31
+ try {
32
+ return decodeURIComponent(encodedId);
33
+ }
34
+ catch {
35
+ throw new PiboWebHttpError("Invalid agent folder id", 400);
36
+ }
37
+ }
24
38
  export function agentResourceId(pathname) {
25
39
  const prefix = `${CHAT_WEB_API_PREFIX}/agents/`;
26
40
  if (!pathname.startsWith(prefix))
@@ -116,6 +116,30 @@ export function normalizeAgentDisplayName(value, fallback = "new-agent") {
116
116
  }
117
117
  return name;
118
118
  }
119
+ export function normalizeAgentFolderName(value) {
120
+ if (typeof value !== "string")
121
+ throw new PiboWebHttpError("Agent folder name must be a string", 400);
122
+ const name = value.replace(/\s+/g, " ").trim();
123
+ if (!name)
124
+ throw new PiboWebHttpError("Agent folder name is required", 400);
125
+ if (name.length > 80)
126
+ throw new PiboWebHttpError("Agent folder name is too long", 400);
127
+ return name;
128
+ }
129
+ export function normalizeAgentFolderId(value) {
130
+ if (value === undefined)
131
+ return undefined;
132
+ if (value === null || value === "")
133
+ return null;
134
+ if (typeof value !== "string")
135
+ throw new PiboWebHttpError("Agent folder id must be a string", 400);
136
+ const folderId = value.trim();
137
+ if (!folderId)
138
+ return null;
139
+ if (folderId.length > 160)
140
+ throw new PiboWebHttpError("Agent folder id is too long", 400);
141
+ return folderId;
142
+ }
119
143
  export function normalizeAgentDescription(value) {
120
144
  if (value === undefined || value === null)
121
145
  return undefined;
@@ -661,6 +685,7 @@ export function createAgentInput(body) {
661
685
  return {
662
686
  displayName: normalizeAgentDisplayName(body.displayName),
663
687
  description: normalizeAgentDescription(body.description),
688
+ folderId: normalizeAgentFolderId(body.folderId) ?? undefined,
664
689
  runtimeInstanceId: normalizeAgentRuntimeInstanceId(body.runtimeInstanceId),
665
690
  runtimeOptions: normalizeAgentRuntimeOptions(body.runtimeOptions),
666
691
  nativeSubagents: normalizeNativeSubagents(body.nativeSubagents),
@@ -691,6 +716,8 @@ export function createAgentUpdate(body) {
691
716
  update.displayName = normalizeAgentDisplayName(body.displayName);
692
717
  if (body.description !== undefined)
693
718
  update.description = normalizeAgentDescription(body.description);
719
+ if (body.folderId !== undefined)
720
+ update.folderId = normalizeAgentFolderId(body.folderId);
694
721
  if (body.runtimeInstanceId !== undefined)
695
722
  update.runtimeInstanceId = normalizeAgentRuntimeInstanceId(body.runtimeInstanceId);
696
723
  if (body.runtimeOptions !== undefined)
@@ -50,8 +50,8 @@ import { ensurePrivateChatUploadDirectory, prepareChatFileAttachments, resolveDo
50
50
  import { chatSettingsRoute, chatSettingsRouteInvalidatesBootstrapCatalog, chatSettingsRouteRequiresSameOrigin, handleChatSettingsRoute, } from "./chat-settings-routes.js";
51
51
  import { chatCapabilityRoute, chatCapabilityRouteRequiresSameOrigin, handleChatCapabilityRoute, } from "./chat-capability-routes.js";
52
52
  import { chatUserSkillRoute, chatUserSkillRouteRequiresSameOrigin, handleChatUserSkillRoute, syncChatUserSkills, } from "./chat-user-skill-routes.js";
53
- import { CHAT_WEB_API_PREFIX, agentResourceId, projectResourcePath, projectSessionResourceId, projectWorkflowHumanActionsResource, projectWorkflowSessionStartResource, roomResourcePath, sessionActionResource, sessionResourceId, signalResource, workflowArchiveResourceId, workflowCatalogResourceId, workflowDraftActionResource, workflowDraftManualTriggerRunResource, workflowDraftResourceId, workflowDuplicateResourceId, workflowNextDraftResourceId, workflowPickerKind, workflowPromptAssetResourceId, workflowVersionResource, } from "./chat-api-routes.js";
54
- import { assertProjectSessionPatchFields, buildStreamingFixtureSchedule, createAgentInput, createAgentUpdate, createRoomUpdate, createSessionUpdate, normalizeAgentArchived, normalizeClientTxnId, normalizeMessageDelivery, normalizeMessageText, normalizeParentRoomId, normalizeProjectArchived, normalizeProjectDescription, normalizeProjectPath, normalizeProjectSessionArchived, normalizeRoomDeleteConfirmation, normalizeRoomName, normalizeRoomTopic, normalizeRoomType, normalizeRoomWorkspace, normalizeSessionDeleteConfirmation, normalizeSessionTitle, normalizeStreamingFixtureCadenceMs, normalizeStreamingFixtureDeltas, normalizeStreamingFixtureMix, normalizeStreamingFixturePreludeMessages, normalizeStreamingFixturePreludeOnly, normalizeStreamingFixtureProfile, normalizeStreamingFixtureSuppressLiveDeltas, normalizeStreamingFixtureTraceSnapshots, resolveCreateSessionProfile, } from "./chat-request-normalizers.js";
53
+ import { CHAT_WEB_API_PREFIX, agentFolderResourceId, agentResourceId, projectResourcePath, projectSessionResourceId, projectWorkflowHumanActionsResource, projectWorkflowSessionStartResource, roomResourcePath, sessionActionResource, sessionResourceId, signalResource, workflowArchiveResourceId, workflowCatalogResourceId, workflowDraftActionResource, workflowDraftManualTriggerRunResource, workflowDraftResourceId, workflowDuplicateResourceId, workflowNextDraftResourceId, workflowPickerKind, workflowPromptAssetResourceId, workflowVersionResource, } from "./chat-api-routes.js";
54
+ import { assertProjectSessionPatchFields, buildStreamingFixtureSchedule, createAgentInput, createAgentUpdate, createRoomUpdate, createSessionUpdate, normalizeAgentArchived, normalizeAgentFolderName, normalizeClientTxnId, normalizeMessageDelivery, normalizeMessageText, normalizeParentRoomId, normalizeProjectArchived, normalizeProjectDescription, normalizeProjectPath, normalizeProjectSessionArchived, normalizeRoomDeleteConfirmation, normalizeRoomName, normalizeRoomTopic, normalizeRoomType, normalizeRoomWorkspace, normalizeSessionDeleteConfirmation, normalizeSessionTitle, normalizeStreamingFixtureCadenceMs, normalizeStreamingFixtureDeltas, normalizeStreamingFixtureMix, normalizeStreamingFixturePreludeMessages, normalizeStreamingFixturePreludeOnly, normalizeStreamingFixtureProfile, normalizeStreamingFixtureSuppressLiveDeltas, normalizeStreamingFixtureTraceSnapshots, resolveCreateSessionProfile, } from "./chat-request-normalizers.js";
55
55
  import { ChatWorkflowArchiveStore, ChatWorkflowDraftStore, ChatWorkflowLifecycleEventStore, ChatWorkflowPromptAssetStore, ChatWorkflowPublishedVersionStore, ChatWorkflowTombstoneStore, hashWorkflowDefinitionJson, normalizeWorkflowPromptAssetLabel, sanitizeWorkflowDiagnostics, } from "./workflow-persistence.js";
56
56
  import { normalizeProjectWorkflowHumanActionBody, projectWorkflowHumanActionDiagnosticResponse, projectWorkflowHumanActionLifecyclePayload, projectWorkflowHumanActionRuntimeDiagnostic, projectWorkflowHumanActionSubmittedLifecyclePayload, projectWorkflowPendingHumanActionFromToken, validateProjectWorkflowHumanActionRequest, } from "./project-workflow-human-actions.js";
57
57
  import { createProjectWorkflowRunCurrent, createProjectWorkflowSessionSnapshot, normalizeProjectWorkflowSessionConfiguration, workflowVersionFromSnapshot, } from "./project-workflow-sessions.js";
@@ -81,6 +81,7 @@ function loadBootstrapCatalog(state, context, webSession) {
81
81
  ]).then(([modelCatalog, agentCatalog]) => ({
82
82
  agents: context.channelContext.getProfiles?.() ?? [],
83
83
  customAgents: serializeCustomAgents(state.agentStore.list({ includeArchived: true }), context),
84
+ agentFolders: state.agentStore.listFolders(),
84
85
  modelDefaults: loadChatModelDefaults(process.cwd()),
85
86
  modelCatalog,
86
87
  agentCatalog,
@@ -904,6 +905,22 @@ function serializeCustomAgent(agent, context) {
904
905
  function serializeCustomAgents(agents, context) {
905
906
  return agents.map((agent) => serializeCustomAgent(agent, context));
906
907
  }
908
+ function requireAgentFolder(state, folderId) {
909
+ const folder = state.agentStore.getFolder(folderId);
910
+ if (!folder)
911
+ throw new PiboWebHttpError("Agent folder not found", 404);
912
+ return folder;
913
+ }
914
+ function requireAgentFolderNameAvailable(state, name, currentId) {
915
+ const conflict = state.agentStore.listFolders().find((folder) => folder.id !== currentId && folder.name.localeCompare(name, undefined, { sensitivity: "accent" }) === 0);
916
+ if (conflict)
917
+ throw new PiboWebHttpError(`Agent folder "${name}" already exists`, 409);
918
+ }
919
+ function requireAgentFolderAssignmentAvailable(state, folderId) {
920
+ if (!folderId)
921
+ return;
922
+ requireAgentFolder(state, folderId);
923
+ }
907
924
  const RUNTIME_PROFILE_UPDATE_FIELDS = new Set([
908
925
  "runtimeInstanceId",
909
926
  "runtimeOptions",
@@ -4280,6 +4297,43 @@ export function createChatWebApp(options = {}) {
4280
4297
  invalidateBootstrapCatalogCache: () => invalidateBootstrapCatalogCache(state),
4281
4298
  });
4282
4299
  }
4300
+ if (url.pathname === `${CHAT_WEB_API_PREFIX}/agent-folders` && request.method === "GET") {
4301
+ await requireSession(request, context);
4302
+ return responseJson({ folders: state.agentStore.listFolders() });
4303
+ }
4304
+ if (url.pathname === `${CHAT_WEB_API_PREFIX}/agent-folders` && request.method === "POST") {
4305
+ requireSameOriginJsonRequest(request);
4306
+ await requireSession(request, context);
4307
+ const body = await readJsonBody(request);
4308
+ const name = normalizeAgentFolderName(body.name);
4309
+ requireAgentFolderNameAvailable(state, name);
4310
+ const folder = state.agentStore.createFolder(name);
4311
+ invalidateBootstrapCatalogCache(state);
4312
+ return responseJson({ folder }, { status: 201 });
4313
+ }
4314
+ const patchAgentFolderId = agentFolderResourceId(url.pathname);
4315
+ if (patchAgentFolderId && request.method === "PATCH") {
4316
+ requireSameOriginJsonRequest(request);
4317
+ await requireSession(request, context);
4318
+ requireAgentFolder(state, patchAgentFolderId);
4319
+ const body = await readJsonBody(request);
4320
+ const name = normalizeAgentFolderName(body.name);
4321
+ requireAgentFolderNameAvailable(state, name, patchAgentFolderId);
4322
+ const folder = state.agentStore.renameFolder(patchAgentFolderId, name);
4323
+ invalidateBootstrapCatalogCache(state);
4324
+ return responseJson({ folder: requireAgentFolder(state, folder?.id ?? patchAgentFolderId) });
4325
+ }
4326
+ if (patchAgentFolderId && request.method === "DELETE") {
4327
+ requireSameOriginJsonRequest(request);
4328
+ await requireSession(request, context);
4329
+ const folder = requireAgentFolder(state, patchAgentFolderId);
4330
+ const assignedAgent = state.agentStore.list({ includeArchived: true }).find((agent) => agent.folderId === folder.id);
4331
+ if (assignedAgent)
4332
+ throw new PiboWebHttpError("Move agents out of this folder before deleting it.", 409);
4333
+ state.agentStore.deleteFolder(folder.id);
4334
+ invalidateBootstrapCatalogCache(state);
4335
+ return responseJson({ deletedFolderId: folder.id });
4336
+ }
4283
4337
  if (url.pathname === `${CHAT_WEB_API_PREFIX}/agents` && request.method === "GET") {
4284
4338
  const webSession = await requireSession(request, context);
4285
4339
  const includeArchived = parseBooleanSearchParam(url, "includeArchived");
@@ -4291,6 +4345,7 @@ export function createChatWebApp(options = {}) {
4291
4345
  const body = await readJsonBody(request);
4292
4346
  const input = normalizeCreateRuntimeFeatureOverrides(createAgentInput(body), context);
4293
4347
  requireAgentProfileNameAvailable(state, context, input.displayName);
4348
+ requireAgentFolderAssignmentAvailable(state, input.folderId);
4294
4349
  await requireValidCustomAgentRuntime(previewCustomAgentCreate(input), context);
4295
4350
  const agent = state.agentStore.create(input);
4296
4351
  context.channelContext.upsertProfile?.(createCustomAgentProfileDefinition(agent));
@@ -4308,6 +4363,7 @@ export function createChatWebApp(options = {}) {
4308
4363
  const archived = normalizeAgentArchived(body.archived);
4309
4364
  if (update.displayName)
4310
4365
  requireAgentProfileNameAvailable(state, context, update.displayName, existing.id);
4366
+ requireAgentFolderAssignmentAvailable(state, update.folderId);
4311
4367
  if (customAgentUpdateAffectsRuntime(update)) {
4312
4368
  await requireValidCustomAgentRuntime(previewCustomAgentUpdate(existing, update), context);
4313
4369
  }