@shawnstack/quickforge 1.6.7 → 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.
Files changed (168) hide show
  1. package/README.md +10 -10
  2. package/dist/assets/AgentProfilesPage-5eqU0Cjk.js +1 -0
  3. package/dist/assets/ChatPanelHost-BNFeNRy-.js +244 -0
  4. package/dist/assets/{PluginsPage-DDxjlhiK.js → PluginsPage-C5OClNdT.js} +1 -1
  5. package/dist/assets/ScheduledTasksPage-yT10oaaM.js +2 -0
  6. package/dist/assets/{SettingsWorkspacePage-B_7o3noo.js → SettingsWorkspacePage-DuXwIeLd.js} +346 -335
  7. package/dist/assets/SharedConversationPage-ClpUSa0F.js +1 -0
  8. package/dist/assets/TerminalDock-BmL-FeKv.js +2 -0
  9. package/dist/assets/WorkspaceInspector-yH23pXpj.js +13 -0
  10. package/dist/assets/abnfDiagram-VRR7QNED-Dl_Fvlqg.js +1 -0
  11. package/dist/assets/arc-Bhg4c0ht.js +1 -0
  12. package/dist/assets/architecture-TIHT7OUA-BgP-IMeo.js +1 -0
  13. package/dist/assets/architectureDiagram-ZJ3FMSHR-BuojN27B.js +36 -0
  14. package/dist/assets/array-CYkMkqnU.js +1 -0
  15. package/dist/assets/blockDiagram-677ZJIJ3-alDLbrR1.js +132 -0
  16. package/dist/assets/c4Diagram-LMCZKHZV-D1lKa4VT.js +10 -0
  17. package/dist/assets/channel-Rz20dKTK.js +1 -0
  18. package/dist/assets/chunk-2Q5K7J3B-Cb14ZF_k.js +1 -0
  19. package/dist/assets/chunk-32BRIVSS-CwWLNjMZ.js +1 -0
  20. package/dist/assets/chunk-52WLFC77-Be569m7d.js +10 -0
  21. package/dist/assets/chunk-5FCAYU7R-BSvoiWV1.js +1 -0
  22. package/dist/assets/chunk-5HE753X5-CRgsy8WS.js +1 -0
  23. package/dist/assets/chunk-5JV3BV7I-CdW477UR.js +1 -0
  24. package/dist/assets/chunk-5TONJI2A-CdtKzSmp.js +2 -0
  25. package/dist/assets/chunk-5VM5RSS4-BKjjssLZ.js +15 -0
  26. package/dist/assets/chunk-7BUUIJ7U-ByAXiXdn.js +1 -0
  27. package/dist/assets/chunk-BIQX33UG-DeG8SCT1.js +1 -0
  28. package/dist/assets/chunk-C7G6YPKG-D_zHT68Q.js +1 -0
  29. package/dist/assets/chunk-CQNSW5MT-CHpHoUSu.js +1 -0
  30. package/dist/assets/chunk-CYSBUYHQ-CRP50Vov.js +1 -0
  31. package/dist/assets/chunk-EMLP6XTP-DX6uQQ2v.js +1 -0
  32. package/dist/assets/chunk-EX3LRPZG-DAJRNkHR.js +231 -0
  33. package/dist/assets/chunk-FWX5IMBZ-XbM5yomC.js +2 -0
  34. package/dist/assets/chunk-HOUHSVGY-Ci0ljnb1.js +1 -0
  35. package/dist/assets/chunk-ICXQ74PX-Dy2WJ71z.js +2 -0
  36. package/dist/assets/chunk-JG7HCLWE-2PrkzHkt.js +2 -0
  37. package/dist/assets/chunk-JWPE2WC7-D32uacSM.js +1 -0
  38. package/dist/assets/chunk-KEIR6QF5-77hsebd7.js +161 -0
  39. package/dist/assets/chunk-MOJQB5TN-DIAFLtiS.js +88 -0
  40. package/dist/assets/chunk-MOZMSUNE-BbscOaEz.js +1 -0
  41. package/dist/assets/chunk-OGEWGWER-ivBVxzKe.js +1 -0
  42. package/dist/assets/chunk-OSBZ3O6U-frD0hkdx.js +1 -0
  43. package/dist/assets/chunk-PUDLZKDR-B-37Rxae.js +156 -0
  44. package/dist/assets/chunk-Q4XR5HBZ-Y90m2RTe.js +11 -0
  45. package/dist/assets/chunk-QBLGF6JB-DixBqslB.js +1 -0
  46. package/dist/assets/chunk-R7FJI6CG-DXS1UXn_.js +1 -0
  47. package/dist/assets/chunk-RYQCIY6F-n4JxhNd0.js +1 -0
  48. package/dist/assets/chunk-U6XO7XAA-yHlgxMmn.js +2 -0
  49. package/dist/assets/chunk-V7JOEXUC-Z6kuZfbl.js +206 -0
  50. package/dist/assets/chunk-VAUOI2AC-BOZvd2Sn.js +1 -0
  51. package/dist/assets/chunk-VR4S4FIN-Cs_SIQma.js +1 -0
  52. package/dist/assets/chunk-WYO6CB5R-CGExYbV-.js +128 -0
  53. package/dist/assets/chunk-XXDRQBXY-CD_EGCtT.js +1 -0
  54. package/dist/assets/chunk-Y2CYZVJY-CQltPwYk.js +1 -0
  55. package/dist/assets/chunk-YOTPTUD7-DopPXWSl.js +1 -0
  56. package/dist/assets/chunk-ZGVPDNZ5-CJHOeqMb.js +62 -0
  57. package/dist/assets/chunk-ZIRB5QZD-BBVc5mf6.js +32 -0
  58. package/dist/assets/classDiagram-OUVF2IWQ-C_ozTKCt.js +1 -0
  59. package/dist/assets/classDiagram-v2-EOCWNBFH-FOfFbbZF.js +1 -0
  60. package/dist/assets/cose-bilkent-JH36ORCC-DLjV-dTm.js +1 -0
  61. package/dist/assets/cynefin-VYW2F7L2-BzPhLtd5.js +1 -0
  62. package/dist/assets/cynefinDiagram-TSTJHNR4-BfH3zVS-.js +62 -0
  63. package/dist/assets/cytoscape.esm-DwdZnGOm.js +321 -0
  64. package/dist/assets/dagre-CY3D84Cl.js +1 -0
  65. package/dist/assets/dagre-VKFMJZFB-BIhwZCIZ.js +4 -0
  66. package/dist/assets/defaultLocale-Beh6XjaL.js +1 -0
  67. package/dist/assets/diagram-FQU43EPY-BHnzNM2w.js +3 -0
  68. package/dist/assets/diagram-G47NLZAW-C7-ZlmFT.js +24 -0
  69. package/dist/assets/diagram-NH7WQ7WH-C-P9Y7IR.js +24 -0
  70. package/dist/assets/diagram-OA4YK3LP-Ci5lZ_T5.js +30 -0
  71. package/dist/assets/diagram-WEI45ONY-C1xuK2bs.js +41 -0
  72. package/dist/assets/dist-DelZBZpK.js +1 -0
  73. package/dist/assets/ebnfDiagram-CCIWWBDH-DKQ5NWMD.js +1 -0
  74. package/dist/assets/erDiagram-Q63AITRT-HAWySadP.js +85 -0
  75. package/dist/assets/eventmodeling-45OFAUF4-BcgGbVJ-.js +1 -0
  76. package/dist/assets/flowDiagram-23GEKE2U-dtkzcC8E.js +1 -0
  77. package/dist/assets/ganttDiagram-NO4QXBWP-C1hwIcOA.js +292 -0
  78. package/dist/assets/gitGraph-TEB2WS4Q-VByWboBU.js +1 -0
  79. package/dist/assets/gitGraphDiagram-IHSO6WYX-DYUYxs1R.js +106 -0
  80. package/dist/assets/graphlib-CAISHZh4.js +1 -0
  81. package/dist/assets/icons-C7j5jdKo.js +1 -0
  82. package/dist/assets/index-Qof73j0o.css +3 -0
  83. package/dist/assets/index-_ut7awrd.js +63 -0
  84. package/dist/assets/info-DKCQHKI2-CZeDLZxx.js +1 -0
  85. package/dist/assets/infoDiagram-FWYZ7A6U-CnIL6tv4.js +2 -0
  86. package/dist/assets/init-Rr1s_RiX.js +1 -0
  87. package/dist/assets/ishikawaDiagram-FXEZZL3T-B7WXNycQ.js +70 -0
  88. package/dist/assets/journeyDiagram-5HDEW3XC-D4HvioyY.js +139 -0
  89. package/dist/assets/kanban-definition-HUTT4EX6-CDs763eS.js +89 -0
  90. package/dist/assets/line-ikKkQiFp.js +1 -0
  91. package/dist/assets/linear-GqPyxQkN.js +1 -0
  92. package/dist/assets/map-CJwjCvwr.js +1 -0
  93. package/dist/assets/{mcp-servers-dialog-CJ4HKWVZ.js → mcp-servers-dialog-DC2Zfojn.js} +2 -2
  94. package/dist/assets/mermaid-parser.core-CoE8cccr.js +4 -0
  95. package/dist/assets/mermaid-renderer-BmbSAmpG.js +2 -0
  96. package/dist/assets/mermaid.core-DlFSL90j.js +11 -0
  97. package/dist/assets/mindmap-definition-LN4V7U3C-DLdq0K9C.js +96 -0
  98. package/dist/assets/{monaco-FCb94ino.js → monaco-CdOyGlWD.js} +1 -1
  99. package/dist/assets/ordinal-CibZU3PM.js +1 -0
  100. package/dist/assets/otel-BNGM50C-.js +3 -0
  101. package/dist/assets/packet-7NZHBO7P-decrrxyX.js +1 -0
  102. package/dist/assets/path-BAQ3hXlG.js +1 -0
  103. package/dist/assets/pegDiagram-2B236MQR-Cm1kPRC2.js +1 -0
  104. package/dist/assets/pi-ai-Cr3rpCXT.js +131 -0
  105. package/dist/assets/pi-web-ui-DoRGtREc.js +2803 -0
  106. package/dist/assets/pie-RZYD4A2V-J1CasQZl.js +1 -0
  107. package/dist/assets/pieDiagram-ENE6RG2P-Df05wjjo.js +39 -0
  108. package/dist/assets/quadrantDiagram-ABIIQ3AL-BjCwGxJC.js +7 -0
  109. package/dist/assets/radar-I7S5WNFK-B-sZytUA.js +1 -0
  110. package/dist/assets/railroad-3IZDKUUU-BSaqNPm8.js +1 -0
  111. package/dist/assets/railroad-abnf-AHOZXSZD-lzZR7PxT.js +1 -0
  112. package/dist/assets/railroad-ebnf-EBAXGLYW-DQtz1Fh9.js +1 -0
  113. package/dist/assets/railroad-peg-LSFZ7HO6-CpMsuGY3.js +1 -0
  114. package/dist/assets/railroadDiagram-RFXS5EU6-CTUvpKkv.js +1 -0
  115. package/dist/assets/{react-vendor-BR9MG2D-.js → react-vendor-VqdnQHHS.js} +1 -1
  116. package/dist/assets/requirementDiagram-TGXJPOKE-BU1kvEku.js +84 -0
  117. package/dist/assets/rough.esm-Djo4Abte.js +1 -0
  118. package/dist/assets/sankeyDiagram-HTMAVEWB-BHmvxVqH.js +40 -0
  119. package/dist/assets/sequenceDiagram-DBY2YBRQ-CTpwICgg.js +162 -0
  120. package/dist/assets/sizeCapture-X5ZJPWSS-BbLhGaa4.js +1 -0
  121. package/dist/assets/{skills-dialog-CmCLl7K_.js → skills-dialog-Mnj8fZwq.js} +1 -1
  122. package/dist/assets/src-B4Tvg3gT.js +1 -0
  123. package/dist/assets/stateDiagram-2N3HPSRC-BxuDwIgT.js +1 -0
  124. package/dist/assets/stateDiagram-v2-6OUMAXLB-BGLshxcY.js +1 -0
  125. package/dist/assets/swimlanes-5IMT3BWC-EkZsiylV.js +2 -0
  126. package/dist/assets/swimlanesDiagram-G3AALYLV-CqdqxA-b.js +8 -0
  127. package/dist/assets/timeline-definition-FHXFAJF6-C9ewOCTa.js +120 -0
  128. package/dist/assets/treeView-QDETBFTQ-Byb71heW.js +1 -0
  129. package/dist/assets/treemap-6X3UGDF4-CMV1SJsq.js +1 -0
  130. package/dist/assets/vennDiagram-L72KCM5P-CR49Y4Lp.js +34 -0
  131. package/dist/assets/wardley-OPB4EBWU-DtCHtr2V.js +1 -0
  132. package/dist/assets/wardleyDiagram-EHGQE667-Cn0GgwWi.js +78 -0
  133. package/dist/assets/xychartDiagram-FW5EYKEG-DlmPNAvd.js +7 -0
  134. package/dist/index.html +8 -8
  135. package/package.json +4 -3
  136. package/server/agent-manager.mjs +174 -9
  137. package/server/agent-profile-files.mjs +86 -6
  138. package/server/agent-profile-schema.mjs +197 -0
  139. package/server/agent-profiles.mjs +172 -41
  140. package/server/ai-http-logger.mjs +1 -1
  141. package/server/auto-compaction.mjs +2 -2
  142. package/server/project-route-context.mjs +25 -0
  143. package/server/routes/agent-profiles.mjs +10 -4
  144. package/server/routes/models.mjs +1 -1
  145. package/server/routes/project.mjs +6 -10
  146. package/server/routes/scheduled-tasks.mjs +1 -1
  147. package/server/routes/workspace.mjs +185 -12
  148. package/server/session-utils.mjs +1 -1
  149. package/server/storage.mjs +34 -0
  150. package/server/subagents.mjs +64 -0
  151. package/server/tools/definitions.mjs +17 -1
  152. package/server/utils/platform.mjs +25 -2
  153. package/dist/assets/AgentProfilesPage-XJdWmkQb.js +0 -1
  154. package/dist/assets/ChatPanelHost-BrUHIzBw.js +0 -242
  155. package/dist/assets/ScheduledTasksPage-Bw-pSpbv.js +0 -2
  156. package/dist/assets/SharedConversationPage-B5DPQla3.js +0 -1
  157. package/dist/assets/TerminalDock-CVObjSM6.js +0 -2
  158. package/dist/assets/WorkspaceInspector-Dk5887WR.js +0 -3
  159. package/dist/assets/WorkspaceReaderDialog-D-_BBvud.js +0 -1
  160. package/dist/assets/diff-line-counts-DagL0jQr.js +0 -10
  161. package/dist/assets/icons-DHoaB5uq.js +0 -1
  162. package/dist/assets/index-C77QCHE9.css +0 -3
  163. package/dist/assets/index-DeUvsbzW.js +0 -63
  164. package/dist/assets/pi-ai-Cx633yhb.js +0 -134
  165. package/dist/assets/pi-web-ui-DFNE2m5b.js +0 -2770
  166. package/dist/assets/useAppTheme-BwZ5OEf3.js +0 -1
  167. /package/dist/assets/{plugin-api-UKg_cgSG.js → plugin-api-BbUkYm3Y.js} +0 -0
  168. /package/dist/assets/{xterm-BtSXYfUR.js → xterm-CGiQzDiR.js} +0 -0
@@ -1,5 +1,7 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
  import { randomUUID } from 'node:crypto'
3
+ import { promises as fs } from 'node:fs'
4
+ import path from 'node:path'
3
5
  import { Agent } from '@earendil-works/pi-agent-core'
4
6
  import { streamSimpleWithAiHttpLogging } from './ai-http-logger.mjs'
5
7
  import { loadSkillToolContext, abortRunningCommand } from './tools/index.mjs'
@@ -11,8 +13,20 @@ import {
11
13
  formatSubagentTask,
12
14
  } from './subagents.mjs'
13
15
  import { agentProfileSnapshot, getAgentProfile } from './agent-profiles.mjs'
16
+ import { agentProfileFromMarkdown } from './agent-profile-files.mjs'
17
+ import {
18
+ applyCapabilityPolicy,
19
+ inferCapabilityPolicy,
20
+ modelReferenceSnapshot,
21
+ normalizeCapabilityPolicy,
22
+ normalizeModelReference,
23
+ resolveAgentProfileModel,
24
+ resolveAgentProfileThinkingLevel,
25
+ validateAgentProfileTools,
26
+ validateModelReference,
27
+ } from './agent-profile-schema.mjs'
14
28
  import { projectContextFromId, defaultGlobalWorkspaceContext, readProjectConfig } from './project-config.mjs'
15
- import { readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue } from './storage.mjs'
29
+ import { ensureStorage, readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue, tempAgentsDir } from './storage.mjs'
16
30
  import { logger } from './utils/logger.mjs'
17
31
  import { buildSystemPrompt, generateAiTitle, generateTitle } from './session-utils.mjs'
18
32
  import { restoreReasoningContentInPayload } from './reasoning-cache.mjs'
@@ -385,6 +399,21 @@ function addToolTimingToEvent(session, event) {
385
399
  return event
386
400
  }
387
401
 
402
+ export function markLatestAssistantProcessFinished(messages, finishedAt = Date.now()) {
403
+ if (!Array.isArray(messages)) return false
404
+ for (let index = messages.length - 1; index >= 0; index--) {
405
+ const message = messages[index]
406
+ if (!message || message.role !== 'assistant') continue
407
+ const details = message.details && typeof message.details === 'object' && !Array.isArray(message.details)
408
+ ? message.details
409
+ : {}
410
+ if (details.quickforgeProcessFinishedAt !== undefined) return false
411
+ messages[index] = { ...message, details: { ...details, quickforgeProcessFinishedAt: finishedAt } }
412
+ return true
413
+ }
414
+ return false
415
+ }
416
+
388
417
  function updateSessionMessages(session, messages) {
389
418
  session.agent.state.messages = messages
390
419
  }
@@ -831,10 +860,109 @@ ${scopeText}
831
860
  </review_command_invocation>`
832
861
  }
833
862
 
863
+ const tempSubagentNameRegex = /^[a-z][a-z0-9_-]{1,39}$/
864
+
865
+ function frontmatterString(value) {
866
+ return String(value ?? '').replace(/\r?\n/g, ' ').replace(/"/g, '\\"')
867
+ }
868
+
869
+ function normalizeTemporarySubagentSpec(input) {
870
+ const spec = input && typeof input === 'object' && !Array.isArray(input) ? input : null
871
+ if (!spec || spec.type !== 'temporary') return null
872
+ const name = String(spec.name || '').trim().toLowerCase()
873
+ if (!tempSubagentNameRegex.test(name)) {
874
+ const error = new Error('Temporary subagent name must start with a letter and contain only lowercase letters, numbers, underscores, or hyphens')
875
+ error.statusCode = 400
876
+ throw error
877
+ }
878
+ const instructions = String(spec.instructions || '').trim()
879
+ if (!instructions) {
880
+ const error = new Error('Temporary subagent instructions are required')
881
+ error.statusCode = 400
882
+ throw error
883
+ }
884
+ const requestedTools = Array.isArray(spec.tools)
885
+ ? [...new Set(spec.tools.map((toolName) => String(toolName || '').trim()).filter(Boolean))]
886
+ : ['read_file', 'grep_files', 'run_command']
887
+ const capabilityPolicy = normalizeCapabilityPolicy(spec.capabilityPolicy || 'readonly-research', requestedTools)
888
+ validateAgentProfileTools(requestedTools, capabilityPolicy)
889
+ const allowedTools = applyCapabilityPolicy(requestedTools, capabilityPolicy)
890
+ const model = validateModelReference(normalizeModelReference(spec.model))
891
+ return {
892
+ name,
893
+ label: String(spec.label || spec.description || name).trim().slice(0, 80) || name,
894
+ description: String(spec.description || '').trim().slice(0, 500),
895
+ systemPrompt: instructions,
896
+ allowedTools,
897
+ capabilityPolicy,
898
+ model,
899
+ maxRuntimeMs: spec.maxRuntimeMs,
900
+ maxToolCalls: spec.maxToolCalls,
901
+ }
902
+ }
903
+
904
+ function safeTempPathSegment(value, fallback) {
905
+ const text = String(value || '').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 80)
906
+ return text || fallback
907
+ }
908
+
909
+ function temporarySubagentMarkdown(spec, parentSession, runId, expiresAt) {
910
+ return `---
911
+ name: ${spec.name}
912
+ label: "${frontmatterString(spec.label)}"
913
+ description: "${frontmatterString(spec.description)}"
914
+ source: temporary
915
+ lifecycle: temporary
916
+ createdBy: ai
917
+ createdAt: ${new Date().toISOString()}
918
+ parentSessionId: ${frontmatterString(parentSession.sessionId)}
919
+ runId: ${runId}
920
+ expiresAt: ${expiresAt}
921
+ enabled-as-subagent: true
922
+ capabilityPolicy: ${spec.capabilityPolicy}
923
+ tools: ${spec.allowedTools.join(', ')}
924
+ model:
925
+ mode: ${spec.model?.mode === 'fixed' ? 'fixed' : 'inherit'}${spec.model?.mode === 'fixed' ? `
926
+ provider: ${frontmatterString(spec.model.provider)}
927
+ modelId: ${frontmatterString(spec.model.modelId)}${spec.model.api ? `
928
+ api: ${frontmatterString(spec.model.api)}` : ''}${spec.model.baseUrl ? `
929
+ baseUrl: ${frontmatterString(spec.model.baseUrl)}` : ''}` : ''}
930
+ max-runtime-ms: ${Math.max(1000, Math.min(Number(spec.maxRuntimeMs || SUBAGENT_DEFAULT_TIMEOUT_MS), 30 * 60 * 1000))}
931
+ max-tool-calls: ${Math.max(1, Math.min(Number(spec.maxToolCalls || 300), 300))}
932
+ ---
933
+ ${spec.systemPrompt}
934
+ `
935
+ }
936
+
937
+ async function createTemporarySubagentProfile(parentSession, spec) {
938
+ await ensureStorage()
939
+ const runId = `run_${Date.now()}_${randomUUID().slice(0, 8)}`
940
+ const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
941
+ const dir = path.join(tempAgentsDir, safeTempPathSegment(parentSession.sessionId, 'session'), runId)
942
+ await fs.mkdir(dir, { recursive: true })
943
+ const file = path.join(dir, `${spec.name}.md`)
944
+ const markdown = temporarySubagentMarkdown(spec, parentSession, runId, expiresAt)
945
+ await fs.writeFile(file, markdown, 'utf8')
946
+ const profile = agentProfileFromMarkdown(file, markdown, {
947
+ source: 'temporary',
948
+ idPrefix: `temporary:${safeTempPathSegment(parentSession.sessionId, 'session')}:${runId}`,
949
+ relativePath: path.relative(tempAgentsDir, file).replace(/\\/g, '/'),
950
+ })
951
+ if (!profile) throw new Error('Failed to create temporary subagent profile')
952
+ profile.readonly = true
953
+ return profile
954
+ }
955
+
956
+ async function resolveSubagentProfile(parentSession, requestedSubagent) {
957
+ const temporarySpec = normalizeTemporarySubagentSpec(requestedSubagent)
958
+ if (temporarySpec) return createTemporarySubagentProfile(parentSession, temporarySpec)
959
+ return getAgentProfile(requestedSubagent, { workspaceRoot: parentSession.projectContext?.workspaceRoot })
960
+ }
961
+
834
962
  async function runSubagent(parentSession, params, parentSignal, onUpdate) {
835
- const profile = await getAgentProfile(params?.subagent, { workspaceRoot: parentSession.projectContext?.workspaceRoot })
963
+ const profile = await resolveSubagentProfile(parentSession, params?.subagent)
836
964
  if (!profile || !profile.enabledAsSubagent) {
837
- const error = new Error(`Unknown or disabled subagent: ${params?.subagent || ''}`)
965
+ const error = new Error(`Unknown or disabled subagent: ${typeof params?.subagent === 'string' ? params.subagent : params?.subagent?.name || ''}`)
838
966
  error.statusCode = 400
839
967
  throw error
840
968
  }
@@ -854,6 +982,10 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
854
982
  }
855
983
 
856
984
  const timeoutMs = Math.max(1000, Math.min(Number(definition.maxRuntimeMs || SUBAGENT_DEFAULT_TIMEOUT_MS), 30 * 60 * 1000))
985
+ definition.capabilityPolicy ||= inferCapabilityPolicy(definition.allowedTools || [])
986
+ definition.allowedTools = applyCapabilityPolicy(definition.allowedTools || [], definition.capabilityPolicy)
987
+ const { model: subagentModel, info: subagentModelInfo } = await resolveAgentProfileModel(definition, parentSession.model, readStore)
988
+ const subagentThinkingLevel = resolveAgentProfileThinkingLevel(definition, parentSession.thinkingLevel, subagentModel)
857
989
  const subagentSessionId = `${parentSession.sessionId}:subagent:${definition.name}:${randomUUID()}`
858
990
  const startedAt = Date.now()
859
991
  let toolCalls = 0
@@ -898,6 +1030,11 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
898
1030
  toolCalls,
899
1031
  allowedTools: definition.allowedTools,
900
1032
  timeoutMs,
1033
+ source: definition.source,
1034
+ lifecycle: definition.lifecycle,
1035
+ profilePath: definition.filePath,
1036
+ capabilityPolicy: definition.capabilityPolicy,
1037
+ model: subagentModelInfo,
901
1038
  durationMs: Date.now() - startedAt,
902
1039
  messages: latestMessages,
903
1040
  tools: toolsForClient,
@@ -932,8 +1069,8 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
932
1069
  const subagent = new Agent({
933
1070
  initialState: {
934
1071
  systemPrompt,
935
- model: parentSession.model,
936
- thinkingLevel: parentSession.thinkingLevel,
1072
+ model: subagentModel,
1073
+ thinkingLevel: subagentThinkingLevel,
937
1074
  messages: [],
938
1075
  tools,
939
1076
  },
@@ -1013,6 +1150,11 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
1013
1150
  toolCalls,
1014
1151
  allowedTools: definition.allowedTools,
1015
1152
  timeoutMs,
1153
+ source: definition.source,
1154
+ lifecycle: definition.lifecycle,
1155
+ profilePath: definition.filePath,
1156
+ capabilityPolicy: definition.capabilityPolicy,
1157
+ model: subagentModelInfo,
1016
1158
  durationMs: Date.now() - startedAt,
1017
1159
  messages: latestMessages,
1018
1160
  tools: toolsForClient,
@@ -1229,6 +1371,8 @@ export async function createAgent(sessionId, config = {}) {
1229
1371
  const profileSystemPrompt = agentProfile?.systemPrompt ? `\n\n<agent_profile_instructions>\nAgent Profile: ${agentProfile.label || agentProfile.name}\n${agentProfile.systemPrompt}\n</agent_profile_instructions>` : ''
1230
1372
  const resolvedSystemPrompt = systemPrompt ?? `${await buildSystemPrompt(projectId)}${profileSystemPrompt}`
1231
1373
 
1374
+ let resolvedAgentProfile = agentProfile
1375
+
1232
1376
  // Resolve model
1233
1377
  let resolvedModel = model
1234
1378
  if (!resolvedModel) {
@@ -1241,6 +1385,17 @@ export async function createAgent(sessionId, config = {}) {
1241
1385
  // ignore
1242
1386
  }
1243
1387
  }
1388
+ if (agentProfile?.model?.mode === 'fixed') {
1389
+ resolvedModel = (await resolveAgentProfileModel(agentProfile, resolvedModel, readStore)).model
1390
+ resolvedAgentProfile = {
1391
+ ...agentProfile,
1392
+ model: modelReferenceSnapshot(agentProfile.model),
1393
+ }
1394
+ }
1395
+ if (resolvedAgentProfile && !resolvedModel) throw new Error('No active model is configured for the agent session.')
1396
+ const resolvedThinkingLevel = agentProfile
1397
+ ? resolveAgentProfileThinkingLevel(agentProfile, thinkingLevel, resolvedModel)
1398
+ : thinkingLevel
1244
1399
 
1245
1400
  // Build skills tools for enabled skills, plus workspace tools when a project is available.
1246
1401
  const profileToolNames = Array.isArray(agentProfile?.allowedTools) ? agentProfile.allowedTools : null
@@ -1279,7 +1434,7 @@ export async function createAgent(sessionId, config = {}) {
1279
1434
  initialState: {
1280
1435
  systemPrompt: resolvedSystemPrompt,
1281
1436
  model: resolvedModel,
1282
- thinkingLevel,
1437
+ thinkingLevel: resolvedThinkingLevel,
1283
1438
  messages,
1284
1439
  tools,
1285
1440
  },
@@ -1300,7 +1455,14 @@ export async function createAgent(sessionId, config = {}) {
1300
1455
  const isSkillTool = toolName === 'activate_skill' || toolName === 'read_skill_resource'
1301
1456
  if (isSkillTool) return undefined
1302
1457
  if (profileToolNames && !profileToolNames.includes(toolName)) return { block: true, reason: `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.` }
1303
- if (toolName === 'run_subagent') return undefined
1458
+ if (toolName === 'run_subagent') {
1459
+ const requested = context.args?.subagent
1460
+ const policy = requested && typeof requested === 'object' ? normalizeCapabilityPolicy(requested.capabilityPolicy || 'readonly-research', Array.isArray(requested.tools) ? requested.tools : []) : null
1461
+ if (policy && ['code-edit', 'docs-edit'].includes(policy) && !hasFullAccess(currentSession)) {
1462
+ return { block: true, reason: `Temporary subagent capability policy ${policy} requires full access or approval.` }
1463
+ }
1464
+ return undefined
1465
+ }
1304
1466
  if (isMcpToolName(toolName) || isPluginToolName(toolName)) {
1305
1467
  if (!hasFullAccess(currentSession)) return createApprovalPromise(currentSession, toolCallId, toolName, context.args)
1306
1468
  return undefined
@@ -1328,7 +1490,7 @@ export async function createAgent(sessionId, config = {}) {
1328
1490
  accessMode,
1329
1491
  yoloMode: resolvedYoloMode,
1330
1492
  model: resolvedModel,
1331
- thinkingLevel,
1493
+ thinkingLevel: resolvedThinkingLevel,
1332
1494
  scope,
1333
1495
  title,
1334
1496
  createdAt,
@@ -1348,7 +1510,7 @@ export async function createAgent(sessionId, config = {}) {
1348
1510
  toolTimings: new Map(),
1349
1511
  getApiKey,
1350
1512
  contextCompaction,
1351
- agentProfile: agentProfile ? agentProfileSnapshot(agentProfile) : null,
1513
+ agentProfile: resolvedAgentProfile ? agentProfileSnapshot(resolvedAgentProfile) : null,
1352
1514
  idleRetention,
1353
1515
  lastTransformedContextMessages: null,
1354
1516
  autoCompacting: false,
@@ -1374,6 +1536,9 @@ export async function createAgent(sessionId, config = {}) {
1374
1536
  ? 'error'
1375
1537
  : 'idle'
1376
1538
  : undefined
1539
+ if (timedEvent.type === 'agent_end') {
1540
+ markLatestAssistantProcessFinished(agent.state.messages)
1541
+ }
1377
1542
  const forwardEvent = timedEvent.type === 'agent_end'
1378
1543
  ? {
1379
1544
  ...timedEvent,
@@ -1,9 +1,10 @@
1
1
  import { existsSync, promises as fs } from 'node:fs'
2
2
  import os from 'node:os'
3
3
  import path from 'node:path'
4
- import { dataDir } from './storage.mjs'
4
+ import { userAgentsDir } from './storage.mjs'
5
5
  import { logger } from './utils/logger.mjs'
6
6
  import { firstOptionalBoolean, firstString, parseFrontmatter, splitDelimitedList } from './frontmatter.mjs'
7
+ import { normalizeAgentProfileThinkingLevel, normalizeCapabilityPolicy, normalizeModelReference, validateAgentProfileTools, validateModelReference } from './agent-profile-schema.mjs'
7
8
 
8
9
  const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000
9
10
  const DEFAULT_MAX_TOOL_CALLS = 300
@@ -19,7 +20,6 @@ const toolAliases = new Map([
19
20
  ])
20
21
 
21
22
  const claudeUserAgentsDir = path.join(os.homedir(), '.claude', 'agents')
22
- const userAgentsDir = path.join(dataDir, 'agents')
23
23
 
24
24
  function normalizeString(value) {
25
25
  return typeof value === 'string' && value.trim() ? value.trim() : undefined
@@ -60,6 +60,64 @@ function hasMutationTool(allowedTools) {
60
60
  return allowedTools.some((toolName) => toolName === 'write_file' || toolName === 'edit_file')
61
61
  }
62
62
 
63
+ function quoteFrontmatter(value) {
64
+ return `"${String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, ' ')}"`
65
+ }
66
+
67
+ function formatModelFrontmatter(model) {
68
+ if (!model || model.mode !== 'fixed') return 'model:\n mode: inherit'
69
+ return [
70
+ 'model:',
71
+ ' mode: fixed',
72
+ ` provider: ${quoteFrontmatter(model.provider)}`,
73
+ ` modelId: ${quoteFrontmatter(model.modelId)}`,
74
+ model.api ? ` api: ${quoteFrontmatter(model.api)}` : '',
75
+ model.baseUrl ? ` baseUrl: ${quoteFrontmatter(model.baseUrl)}` : '',
76
+ ].filter(Boolean).join('\n')
77
+ }
78
+
79
+ export function userAgentProfileFilePath(name) {
80
+ const normalized = normalizeName(name)
81
+ if (!normalized) throw new Error('Invalid agent profile name')
82
+ return path.join(userAgentsDir, `${normalized}.md`)
83
+ }
84
+
85
+ export function agentProfileToMarkdown(profile) {
86
+ return `---
87
+ id: ${quoteFrontmatter(profile.id)}
88
+ name: ${profile.name}
89
+ label: ${quoteFrontmatter(profile.label || profile.name)}
90
+ description: ${quoteFrontmatter(profile.description || '')}
91
+ source: user
92
+ lifecycle: persistent
93
+ managed: true
94
+ managedBy: quickforge
95
+ readonly: false
96
+ enabled-as-subagent: ${profile.enabledAsSubagent === true ? 'true' : 'false'}
97
+ capabilityPolicy: ${profile.capabilityPolicy || 'review-only'}
98
+ tools: ${(profile.allowedTools || []).join(', ')}
99
+ ${formatModelFrontmatter(profile.model)}
100
+ thinking-level: ${normalizeAgentProfileThinkingLevel(profile.thinkingLevel)}
101
+ max-runtime-ms: ${profile.maxRuntimeMs || DEFAULT_MAX_RUNTIME_MS}
102
+ max-tool-calls: ${profile.maxToolCalls || DEFAULT_MAX_TOOL_CALLS}
103
+ created-at: ${quoteFrontmatter(profile.createdAt || new Date().toISOString())}
104
+ updated-at: ${quoteFrontmatter(profile.updatedAt || new Date().toISOString())}
105
+ ---
106
+ ${String(profile.systemPrompt || '').trim()}
107
+ `
108
+ }
109
+
110
+ export async function writeUserAgentProfileMarkdown(profile) {
111
+ await fs.mkdir(userAgentsDir, { recursive: true })
112
+ const file = userAgentProfileFilePath(profile.name)
113
+ await fs.writeFile(file, agentProfileToMarkdown(profile), 'utf8')
114
+ return file
115
+ }
116
+
117
+ export async function deleteUserAgentProfileMarkdown(profile) {
118
+ await fs.rm(userAgentProfileFilePath(profile.name), { force: true })
119
+ }
120
+
63
121
  export function agentProfileFromMarkdown(file, text, options = {}) {
64
122
  const parsed = parseFrontmatter(text)
65
123
  if (!parsed.body) return null
@@ -72,30 +130,52 @@ export function agentProfileFromMarkdown(file, text, options = {}) {
72
130
  const allowedTools = normalizeTools(
73
131
  metadata.tools ?? metadata['allowed-tools'] ?? metadata.allowedTools,
74
132
  )
133
+ const capabilityPolicy = normalizeCapabilityPolicy(
134
+ metadata.capabilityPolicy ?? metadata['capability-policy'] ?? metadata.capability_policy,
135
+ allowedTools,
136
+ )
137
+ validateAgentProfileTools(allowedTools, capabilityPolicy)
138
+ const model = validateModelReference(normalizeModelReference(metadata.model, metadata))
75
139
  const label = firstString(metadata.label, metadata.displayName, metadata.title) || name
76
140
  const enabledAsSubagent = firstOptionalBoolean(
77
141
  metadata['enabled-as-subagent'],
78
142
  metadata.enabled_as_subagent,
79
143
  metadata.enabledAsSubagent,
80
144
  )
145
+ const source = firstString(metadata.source) || options.source || 'file'
146
+ const lifecycle = firstString(metadata.lifecycle) || (source === 'temporary' ? 'temporary' : 'persistent')
147
+ const explicitReadonly = firstOptionalBoolean(metadata.readonly, metadata['read-only'])
148
+ const managedBy = firstString(metadata.managedBy, metadata['managed-by'])
149
+ const profileId = firstString(metadata.id) || `${options.idPrefix || source || 'file'}:${name}`
81
150
 
82
151
  return {
83
- id: `${options.idPrefix || 'file'}:${name}`,
152
+ id: profileId,
84
153
  name,
85
154
  label: label.slice(0, 80),
86
155
  description: String(firstString(metadata.description) || '').slice(0, 500),
87
156
  systemPrompt: parsed.body,
88
157
  allowedTools,
158
+ capabilityPolicy,
159
+ model,
160
+ thinkingLevel: normalizeAgentProfileThinkingLevel(
161
+ metadata['thinking-level'] ?? metadata.thinking_level ?? metadata.thinkingLevel,
162
+ ),
163
+ lifecycle,
164
+ managed: firstOptionalBoolean(metadata.managed) === true,
165
+ managedBy,
166
+ expiresAt: firstString(metadata.expiresAt, metadata['expires-at']),
167
+ parentSessionId: firstString(metadata.parentSessionId, metadata['parent-session-id']),
168
+ runId: firstString(metadata.runId, metadata['run-id']),
89
169
  maxRuntimeMs: normalizeRuntime(metadata['max-runtime-ms'] ?? metadata.max_runtime_ms ?? metadata.maxRuntimeMs),
90
170
  maxToolCalls: normalizeToolCalls(metadata['max-tool-calls'] ?? metadata.max_tool_calls ?? metadata.maxToolCalls),
91
171
  enabledAsSubagent: enabledAsSubagent === undefined ? true : enabledAsSubagent,
92
172
  builtin: false,
93
- source: options.source || 'file',
94
- readonly: true,
173
+ source,
174
+ readonly: explicitReadonly === undefined ? true : explicitReadonly,
95
175
  filePath: file,
96
176
  relativePath: options.relativePath || path.basename(file),
97
177
  allowFileMutations: hasMutationTool(allowedTools),
98
- createdAt: 'file',
178
+ createdAt: firstString(metadata.createdAt, metadata['created-at']) || 'file',
99
179
  updatedAt: 'file',
100
180
  }
101
181
  }
@@ -0,0 +1,197 @@
1
+ export const AGENT_PROFILE_TOOL_NAMES = ['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command']
2
+ export const AGENT_PROFILE_MODEL_INHERIT = { mode: 'inherit' }
3
+ export const AGENT_PROFILE_THINKING_LEVELS = ['inherit', 'off', 'low', 'medium', 'high', 'xhigh']
4
+
5
+ const agentProfileThinkingLevels = new Set(AGENT_PROFILE_THINKING_LEVELS)
6
+
7
+ export const CAPABILITY_POLICIES = {
8
+ 'readonly-research': {
9
+ readonly: true,
10
+ allowedTools: ['read_file', 'grep_files', 'run_command'],
11
+ },
12
+ 'safe-validation': {
13
+ readonly: true,
14
+ allowedTools: ['read_file', 'grep_files', 'run_command'],
15
+ },
16
+ 'review-only': {
17
+ readonly: true,
18
+ allowedTools: ['read_file', 'grep_files'],
19
+ },
20
+ 'docs-edit': {
21
+ readonly: false,
22
+ allowedTools: ['read_file', 'grep_files', 'write_file', 'edit_file'],
23
+ pathScope: ['docs/**', 'README.md', 'CHANGELOG.md'],
24
+ },
25
+ 'code-edit': {
26
+ readonly: false,
27
+ allowedTools: ['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command'],
28
+ },
29
+ }
30
+
31
+ const policyNames = new Set(Object.keys(CAPABILITY_POLICIES))
32
+ const allowedToolNames = new Set(AGENT_PROFILE_TOOL_NAMES)
33
+
34
+ function requestError(message, statusCode = 400) {
35
+ const error = new Error(message)
36
+ error.statusCode = statusCode
37
+ return error
38
+ }
39
+
40
+ function normalizeString(value) {
41
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined
42
+ }
43
+
44
+ function sameBaseUrl(a, b) {
45
+ return String(a || '').trim().replace(/\/$/, '') === String(b || '').trim().replace(/\/$/, '')
46
+ }
47
+
48
+ function readModelField(value, ...keys) {
49
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
50
+ for (const key of keys) {
51
+ const text = normalizeString(value[key])
52
+ if (text) return text
53
+ }
54
+ return undefined
55
+ }
56
+
57
+ export function inferCapabilityPolicy(allowedTools = []) {
58
+ if (allowedTools.some((toolName) => toolName === 'write_file' || toolName === 'edit_file')) return 'code-edit'
59
+ if (allowedTools.includes('run_command')) return 'readonly-research'
60
+ return 'review-only'
61
+ }
62
+
63
+ export function normalizeCapabilityPolicy(value, allowedTools = []) {
64
+ const policy = normalizeString(value)?.toLowerCase()
65
+ if (policy && policyNames.has(policy)) return policy
66
+ return inferCapabilityPolicy(allowedTools)
67
+ }
68
+
69
+ export function validateAgentProfileTools(allowedTools = [], capabilityPolicy = inferCapabilityPolicy(allowedTools)) {
70
+ const policy = CAPABILITY_POLICIES[capabilityPolicy]
71
+ if (!policy) throw requestError(`Unsupported capability policy: ${capabilityPolicy}`)
72
+ const allowedByPolicy = new Set(policy.allowedTools)
73
+ for (const toolName of allowedTools) {
74
+ if (!allowedToolNames.has(toolName)) throw requestError(`Unsupported tool for custom agent: ${toolName}`)
75
+ if (!allowedByPolicy.has(toolName)) throw requestError(`Tool ${toolName} is not allowed by capability policy ${capabilityPolicy}`)
76
+ }
77
+ return true
78
+ }
79
+
80
+ export function applyCapabilityPolicy(allowedTools = [], capabilityPolicy = inferCapabilityPolicy(allowedTools)) {
81
+ const policy = CAPABILITY_POLICIES[capabilityPolicy] || CAPABILITY_POLICIES['review-only']
82
+ const allowedByPolicy = new Set(policy.allowedTools)
83
+ return allowedTools.filter((toolName) => allowedByPolicy.has(toolName))
84
+ }
85
+
86
+ export function normalizeModelReference(input, metadata = {}) {
87
+ const modelValue = input === undefined ? metadata.model : input
88
+ const flatMode = normalizeString(metadata['model-mode'] ?? metadata.modelMode)?.toLowerCase()
89
+ const flatProvider = normalizeString(metadata['model-provider'] ?? metadata.modelProvider ?? metadata.provider)
90
+ const flatModelId = normalizeString(metadata['model-id'] ?? metadata.model_id ?? metadata.modelId)
91
+ const flatApi = normalizeString(metadata['model-api'] ?? metadata.modelApi ?? metadata.api)
92
+ const flatBaseUrl = normalizeString(metadata['model-base-url'] ?? metadata.modelBaseUrl ?? metadata.baseUrl)
93
+
94
+ if (flatMode === 'fixed' || flatProvider || flatModelId) {
95
+ return {
96
+ mode: 'fixed',
97
+ provider: flatProvider,
98
+ modelId: flatModelId,
99
+ api: flatApi,
100
+ baseUrl: flatBaseUrl,
101
+ }
102
+ }
103
+
104
+ if (modelValue === undefined || modelValue === null || modelValue === '') return { ...AGENT_PROFILE_MODEL_INHERIT }
105
+ if (typeof modelValue === 'string') {
106
+ const normalized = modelValue.trim().toLowerCase()
107
+ if (!normalized || normalized === 'inherit' || normalized === 'parent' || normalized === 'default') return { ...AGENT_PROFILE_MODEL_INHERIT }
108
+ const colon = modelValue.indexOf(':')
109
+ if (colon > 0) {
110
+ return {
111
+ mode: 'fixed',
112
+ provider: modelValue.slice(0, colon).trim(),
113
+ modelId: modelValue.slice(colon + 1).trim(),
114
+ }
115
+ }
116
+ return { ...AGENT_PROFILE_MODEL_INHERIT }
117
+ }
118
+ if (!modelValue || typeof modelValue !== 'object' || Array.isArray(modelValue)) return { ...AGENT_PROFILE_MODEL_INHERIT }
119
+
120
+ const mode = normalizeString(modelValue.mode)?.toLowerCase()
121
+ if (!mode || mode === 'inherit' || mode === 'parent' || mode === 'default') return { ...AGENT_PROFILE_MODEL_INHERIT }
122
+ if (mode !== 'fixed') return { ...AGENT_PROFILE_MODEL_INHERIT }
123
+
124
+ return {
125
+ mode: 'fixed',
126
+ provider: readModelField(modelValue, 'provider'),
127
+ modelId: readModelField(modelValue, 'modelId', 'model-id', 'model_id', 'id', 'model'),
128
+ api: readModelField(modelValue, 'api'),
129
+ baseUrl: readModelField(modelValue, 'baseUrl', 'base-url', 'base_url'),
130
+ }
131
+ }
132
+
133
+ export function validateModelReference(modelRef) {
134
+ const ref = modelRef || AGENT_PROFILE_MODEL_INHERIT
135
+ if (ref.mode !== 'fixed') return { ...AGENT_PROFILE_MODEL_INHERIT }
136
+ if (!ref.provider || !ref.modelId) throw requestError('Fixed agent model requires provider and modelId')
137
+ return {
138
+ mode: 'fixed',
139
+ provider: ref.provider,
140
+ modelId: ref.modelId,
141
+ api: ref.api,
142
+ baseUrl: ref.baseUrl,
143
+ }
144
+ }
145
+
146
+ export function normalizeAgentProfileThinkingLevel(value, fallback = 'inherit') {
147
+ const normalized = normalizeString(value)?.toLowerCase() || fallback
148
+ if (!agentProfileThinkingLevels.has(normalized)) {
149
+ throw requestError(`Unsupported agent thinking level: ${normalized}`)
150
+ }
151
+ return normalized
152
+ }
153
+
154
+ export function resolveAgentProfileThinkingLevel(profile, inheritedThinkingLevel, model) {
155
+ const configured = normalizeAgentProfileThinkingLevel(profile?.thinkingLevel)
156
+ const requested = configured === 'inherit'
157
+ ? normalizeAgentProfileThinkingLevel(inheritedThinkingLevel || 'off', 'off')
158
+ : configured
159
+ return model?.reasoning === true ? requested : 'off'
160
+ }
161
+
162
+ export function modelReferenceSnapshot(modelRef) {
163
+ const ref = modelRef || AGENT_PROFILE_MODEL_INHERIT
164
+ if (ref.mode !== 'fixed') return { ...AGENT_PROFILE_MODEL_INHERIT }
165
+ return {
166
+ mode: 'fixed',
167
+ provider: ref.provider,
168
+ modelId: ref.modelId,
169
+ ...(ref.api ? { api: ref.api } : {}),
170
+ ...(ref.baseUrl ? { baseUrl: ref.baseUrl } : {}),
171
+ }
172
+ }
173
+
174
+ export async function resolveAgentProfileModel(profile, parentModel, readStore) {
175
+ const ref = profile?.model || AGENT_PROFILE_MODEL_INHERIT
176
+ if (!ref || ref.mode !== 'fixed') {
177
+ if (!parentModel) throw requestError('No active model is configured for the parent session.')
178
+ return { model: parentModel, info: { ...modelReferenceSnapshot(ref), inherited: true } }
179
+ }
180
+
181
+ const validRef = validateModelReference(ref)
182
+ const store = await readStore('custom-providers').catch(() => ({}))
183
+ const providers = Array.isArray(store) ? store : Object.values(store || {})
184
+ const configuredModels = providers
185
+ .flatMap((provider) => Array.isArray(provider?.models) ? provider.models : [])
186
+ .filter((model) => model && typeof model === 'object')
187
+ const model = configuredModels.find((candidate) => (
188
+ candidate.provider === validRef.provider &&
189
+ candidate.id === validRef.modelId &&
190
+ (!validRef.api || candidate.api === validRef.api) &&
191
+ (!validRef.baseUrl || sameBaseUrl(candidate.baseUrl, validRef.baseUrl))
192
+ ))
193
+ if (!model) {
194
+ throw requestError(`Agent profile ${profile?.name || ''} requested model ${validRef.provider}/${validRef.modelId}, but it is not configured.`)
195
+ }
196
+ return { model, info: { ...modelReferenceSnapshot(validRef), inherited: false } }
197
+ }