agency-lang 0.8.1 → 0.9.0

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 (501) hide show
  1. package/dist/lib/agents/agency-agent/agent.agency +381 -1604
  2. package/dist/lib/agents/agency-agent/agent.js +2877 -8316
  3. package/dist/lib/agents/agency-agent/images/images.js +6 -1
  4. package/dist/lib/agents/agency-agent/lib/args.agency +88 -0
  5. package/dist/lib/agents/agency-agent/lib/args.js +386 -0
  6. package/dist/lib/agents/agency-agent/lib/attachments.js +38 -3
  7. package/dist/lib/agents/agency-agent/lib/capabilities.agency +22 -20
  8. package/dist/lib/agents/agency-agent/lib/capabilities.js +96 -13
  9. package/dist/lib/agents/agency-agent/lib/capabilitiesUi.agency +330 -0
  10. package/dist/lib/agents/agency-agent/lib/capabilitiesUi.js +2314 -0
  11. package/dist/lib/agents/agency-agent/lib/commandPalette.agency +38 -0
  12. package/dist/lib/agents/agency-agent/lib/commandPalette.js +494 -0
  13. package/dist/lib/agents/agency-agent/lib/config.js +8 -2
  14. package/dist/lib/agents/agency-agent/lib/header.agency +82 -0
  15. package/dist/lib/agents/agency-agent/lib/header.js +1024 -0
  16. package/dist/lib/agents/agency-agent/lib/images.agency +62 -0
  17. package/dist/lib/agents/agency-agent/lib/images.js +645 -0
  18. package/dist/lib/agents/agency-agent/lib/mcp.agency +103 -0
  19. package/dist/lib/agents/agency-agent/lib/mcp.js +1152 -0
  20. package/dist/lib/agents/agency-agent/lib/modelFilters.js +8 -2
  21. package/dist/lib/agents/agency-agent/lib/models.agency +1 -2
  22. package/dist/lib/agents/agency-agent/lib/models.js +38 -18
  23. package/dist/lib/agents/agency-agent/lib/policy.agency +111 -0
  24. package/dist/lib/agents/agency-agent/lib/policy.js +971 -0
  25. package/dist/lib/agents/agency-agent/lib/resolution.js +51 -8
  26. package/dist/lib/agents/agency-agent/lib/search.agency +21 -3
  27. package/dist/lib/agents/agency-agent/lib/search.js +282 -20
  28. package/dist/lib/agents/agency-agent/lib/session.agency +331 -0
  29. package/dist/lib/agents/agency-agent/lib/session.js +1884 -0
  30. package/dist/lib/agents/agency-agent/lib/settings.js +73 -18
  31. package/dist/lib/agents/agency-agent/lib/slots.agency +0 -1
  32. package/dist/lib/agents/agency-agent/lib/slots.js +10 -4
  33. package/dist/lib/agents/agency-agent/lib/utils/renderLLMCallResponse.agency +27 -0
  34. package/dist/lib/agents/agency-agent/lib/utils/renderLLMCallResponse.js +353 -0
  35. package/dist/lib/agents/agency-agent/lib/utils.agency +42 -0
  36. package/dist/lib/agents/agency-agent/lib/utils.js +416 -7
  37. package/dist/lib/agents/agency-agent/prompts/codeSample.js +24 -1
  38. package/dist/lib/agents/agency-agent/prompts/main-large.md +166 -0
  39. package/dist/lib/agents/agency-agent/prompts/main-small.md +26 -0
  40. package/dist/lib/agents/agency-agent/shared.agency +0 -3
  41. package/dist/lib/agents/agency-agent/shared.js +118 -27
  42. package/dist/lib/agents/agency-agent/subagents/code.agency +148 -51
  43. package/dist/lib/agents/agency-agent/subagents/code.js +669 -174
  44. package/dist/lib/agents/agency-agent/subagents/explorer.agency +1 -2
  45. package/dist/lib/agents/agency-agent/subagents/explorer.js +19 -21
  46. package/dist/lib/agents/agency-agent/subagents/oracle.agency +1 -2
  47. package/dist/lib/agents/agency-agent/subagents/oracle.js +19 -21
  48. package/dist/lib/agents/agency-agent/subagents/research.agency +10 -25
  49. package/dist/lib/agents/agency-agent/subagents/research.js +26 -89
  50. package/dist/lib/agents/agency-agent/subagents/review.agency +60 -4
  51. package/dist/lib/agents/agency-agent/subagents/review.js +657 -25
  52. package/dist/lib/agents/agency-agent/subagents/verify.agency +23 -77
  53. package/dist/lib/agents/agency-agent/subagents/verify.js +265 -186
  54. package/dist/lib/agents/agency-agent/tests/agentDir.js +19 -1
  55. package/dist/lib/agents/agency-agent/tests/agentTurn.agency +31 -4
  56. package/dist/lib/agents/agency-agent/tests/agentTurn.js +468 -11
  57. package/dist/lib/agents/agency-agent/tests/agentTurn.test.json +30 -0
  58. package/dist/lib/agents/agency-agent/tests/attachments.agency +6 -1
  59. package/dist/lib/agents/agency-agent/tests/attachments.js +287 -207
  60. package/dist/lib/agents/agency-agent/tests/attachmentsTurn.agency +5 -10
  61. package/dist/lib/agents/agency-agent/tests/attachmentsTurn.js +60 -141
  62. package/dist/lib/agents/agency-agent/tests/attachmentsTurn.test.json +0 -16
  63. package/dist/lib/agents/agency-agent/tests/capabilities.js +82 -1
  64. package/dist/lib/agents/agency-agent/tests/execPolicy.js +8 -2
  65. package/dist/lib/agents/agency-agent/tests/gitPolicy.js +8 -2
  66. package/dist/lib/agents/agency-agent/tests/imageTool.agency +1 -1
  67. package/dist/lib/agents/agency-agent/tests/imageTool.js +59 -2
  68. package/dist/lib/agents/agency-agent/tests/mcpGating.js +18 -2
  69. package/dist/lib/agents/agency-agent/tests/memoryWiring.js +82 -1
  70. package/dist/lib/agents/agency-agent/tests/modelFilters.js +13 -1
  71. package/dist/lib/agents/agency-agent/tests/models.js +12 -2
  72. package/dist/lib/agents/agency-agent/tests/oneShotRounds.js +6 -1
  73. package/dist/lib/agents/agency-agent/tests/resolution.js +52 -3
  74. package/dist/lib/agents/agency-agent/tests/search.js +35 -4
  75. package/dist/lib/agents/agency-agent/tests/settings.agency +5 -1
  76. package/dist/lib/agents/agency-agent/tests/settings.js +118 -42
  77. package/dist/lib/agents/agency-agent/tests/slots.js +6 -1
  78. package/dist/lib/agents/agency-agent/tests/toolWiring.js +18 -2
  79. package/dist/lib/agents/agency-agent/tests/turnFailure.agency +8 -3
  80. package/dist/lib/agents/agency-agent/tests/turnFailure.js +19 -4
  81. package/dist/lib/agents/eval/goalJudge.js +8 -1
  82. package/dist/lib/agents/eval/judge.js +6 -1
  83. package/dist/lib/agents/eval/judgePairwise.js +6 -1
  84. package/dist/lib/agents/optimize/gepaReflect.js +6 -1
  85. package/dist/lib/agents/optimize/mutatePrompt.js +6 -1
  86. package/dist/lib/agents/policy/agent.js +84 -11
  87. package/dist/lib/agents/review/agent.js +60 -3
  88. package/dist/lib/backends/agencyGenerator.d.ts +12 -0
  89. package/dist/lib/backends/agencyGenerator.finalizeBlock.test.d.ts +1 -0
  90. package/dist/lib/backends/agencyGenerator.finalizeBlock.test.js +36 -0
  91. package/dist/lib/backends/agencyGenerator.guardBlock.test.d.ts +1 -0
  92. package/dist/lib/backends/agencyGenerator.guardBlock.test.js +33 -0
  93. package/dist/lib/backends/agencyGenerator.js +35 -1
  94. package/dist/lib/backends/agencyGenerator.test.js +52 -0
  95. package/dist/lib/backends/draftSchemaCodegen.test.d.ts +1 -0
  96. package/dist/lib/backends/draftSchemaCodegen.test.js +38 -0
  97. package/dist/lib/backends/finalizeBinderCodegen.test.d.ts +1 -0
  98. package/dist/lib/backends/finalizeBinderCodegen.test.js +26 -0
  99. package/dist/lib/backends/functionContainsDestructiveBlock.d.ts +22 -0
  100. package/dist/lib/backends/functionContainsDestructiveBlock.js +30 -0
  101. package/dist/lib/backends/functionContainsDestructiveBlock.test.d.ts +1 -0
  102. package/dist/lib/backends/functionContainsDestructiveBlock.test.js +31 -0
  103. package/dist/lib/backends/typescriptBuilder/destructiveTracking.d.ts +19 -12
  104. package/dist/lib/backends/typescriptBuilder/destructiveTracking.js +31 -17
  105. package/dist/lib/backends/typescriptBuilder/destructiveTracking.test.js +15 -8
  106. package/dist/lib/backends/typescriptBuilder/finalizeCodegen.d.ts +104 -0
  107. package/dist/lib/backends/typescriptBuilder/finalizeCodegen.js +146 -0
  108. package/dist/lib/backends/typescriptBuilder/nameClassifier.d.ts +2 -11
  109. package/dist/lib/backends/typescriptBuilder/nameClassifier.js +5 -30
  110. package/dist/lib/backends/typescriptBuilder/scopeManager.d.ts +9 -0
  111. package/dist/lib/backends/typescriptBuilder/scopeManager.js +30 -0
  112. package/dist/lib/backends/typescriptBuilder/stepPathTracker.js +2 -1
  113. package/dist/lib/backends/typescriptBuilder.d.ts +38 -5
  114. package/dist/lib/backends/typescriptBuilder.js +173 -31
  115. package/dist/lib/cli/agent.d.ts +4 -1
  116. package/dist/lib/cli/agent.js +2 -2
  117. package/dist/lib/cli/budget.d.ts +19 -0
  118. package/dist/lib/cli/budget.js +44 -0
  119. package/dist/lib/cli/budget.test.d.ts +1 -0
  120. package/dist/lib/cli/budget.test.js +46 -0
  121. package/dist/lib/cli/budgetRun.spawn.test.d.ts +1 -0
  122. package/dist/lib/cli/budgetRun.spawn.test.js +109 -0
  123. package/dist/lib/cli/commands.d.ts +3 -0
  124. package/dist/lib/cli/commands.js +11 -2
  125. package/dist/lib/cli/doc.js +5 -0
  126. package/dist/lib/cli/doctor.js +2 -2
  127. package/dist/lib/cli/doctor.test.js +3 -3
  128. package/dist/lib/cli/runBundledAgent.d.ts +4 -1
  129. package/dist/lib/cli/runBundledAgent.js +23 -7
  130. package/dist/lib/cli/runBundledAgent.test.js +40 -8
  131. package/dist/lib/cli/serve.js +16 -28
  132. package/dist/lib/compilationUnit.js +6 -3
  133. package/dist/lib/config.d.ts +4 -1
  134. package/dist/lib/config.js +17 -2
  135. package/dist/lib/config.test.js +7 -1
  136. package/dist/lib/constants.d.ts +9 -0
  137. package/dist/lib/constants.js +9 -0
  138. package/dist/lib/ir/prettyPrint.js +11 -1
  139. package/dist/lib/lowering/patternLowering.js +39 -0
  140. package/dist/lib/lsp/builtinHover.js +4 -3
  141. package/dist/lib/lsp/inlayHint.js +2 -1
  142. package/dist/lib/lsp/typeResolution.js +2 -1
  143. package/dist/lib/parsers/destructiveBlock.test.d.ts +1 -0
  144. package/dist/lib/parsers/destructiveBlock.test.js +36 -0
  145. package/dist/lib/parsers/finalizeBinder.test.d.ts +1 -0
  146. package/dist/lib/parsers/finalizeBinder.test.js +59 -0
  147. package/dist/lib/parsers/finalizeBlock.test.d.ts +1 -0
  148. package/dist/lib/parsers/finalizeBlock.test.js +37 -0
  149. package/dist/lib/parsers/guardBlock.test.d.ts +1 -0
  150. package/dist/lib/parsers/guardBlock.test.js +81 -0
  151. package/dist/lib/parsers/parsers.d.ts +26 -0
  152. package/dist/lib/parsers/parsers.js +71 -4
  153. package/dist/lib/preprocessors/guardDesugar.d.ts +24 -0
  154. package/dist/lib/preprocessors/guardDesugar.js +69 -0
  155. package/dist/lib/preprocessors/guardDesugar.test.d.ts +1 -0
  156. package/dist/lib/preprocessors/guardDesugar.test.js +48 -0
  157. package/dist/lib/preprocessors/liftCallbacks.js +3 -0
  158. package/dist/lib/preprocessors/parallelDesugar.js +17 -1
  159. package/dist/lib/preprocessors/parallelDesugar.test.js +24 -0
  160. package/dist/lib/preprocessors/prunePreludeShadows.d.ts +0 -20
  161. package/dist/lib/preprocessors/prunePreludeShadows.js +11 -0
  162. package/dist/lib/preprocessors/typescriptPreprocessor.js +11 -0
  163. package/dist/lib/runtime/__tests__/testHelpers.js +1 -1
  164. package/dist/lib/runtime/abortedResult.d.ts +100 -0
  165. package/dist/lib/runtime/abortedResult.js +217 -0
  166. package/dist/lib/runtime/abortedResult.test.d.ts +1 -0
  167. package/dist/lib/runtime/abortedResult.test.js +295 -0
  168. package/dist/lib/runtime/agencyFunction.js +18 -2
  169. package/dist/lib/runtime/agencyFunction.test.js +39 -0
  170. package/dist/lib/runtime/asyncContext.d.ts +1 -1
  171. package/dist/lib/runtime/asyncContext.js +11 -2
  172. package/dist/lib/runtime/blockNames.d.ts +11 -0
  173. package/dist/lib/runtime/blockNames.js +21 -0
  174. package/dist/lib/runtime/blockNames.test.d.ts +1 -0
  175. package/dist/lib/runtime/blockNames.test.js +18 -0
  176. package/dist/lib/runtime/budgetExit.d.ts +20 -0
  177. package/dist/lib/runtime/budgetExit.js +35 -0
  178. package/dist/lib/runtime/budgetExit.test.d.ts +1 -0
  179. package/dist/lib/runtime/budgetExit.test.js +59 -0
  180. package/dist/lib/runtime/call.js +77 -0
  181. package/dist/lib/runtime/configOverrides.test.js +1 -1
  182. package/dist/lib/runtime/deterministicClient.d.ts +9 -1
  183. package/dist/lib/runtime/deterministicClient.js +23 -1
  184. package/dist/lib/runtime/effectMerge.d.ts +31 -0
  185. package/dist/lib/runtime/effectMerge.js +93 -0
  186. package/dist/lib/runtime/effectMerge.test.d.ts +1 -0
  187. package/dist/lib/runtime/effectMerge.test.js +91 -0
  188. package/dist/lib/runtime/errors.d.ts +8 -0
  189. package/dist/lib/runtime/errors.js +10 -0
  190. package/dist/lib/runtime/guard.d.ts +259 -32
  191. package/dist/lib/runtime/guard.js +353 -54
  192. package/dist/lib/runtime/guard.test.js +207 -23
  193. package/dist/lib/runtime/guardScope.d.ts +74 -0
  194. package/dist/lib/runtime/guardScope.js +140 -0
  195. package/dist/lib/runtime/guardScope.test.d.ts +1 -0
  196. package/dist/lib/runtime/guardScope.test.js +228 -0
  197. package/dist/lib/runtime/guardTripInterrupt.d.ts +61 -0
  198. package/dist/lib/runtime/guardTripInterrupt.js +273 -0
  199. package/dist/lib/runtime/index.d.ts +3 -1
  200. package/dist/lib/runtime/index.js +3 -1
  201. package/dist/lib/runtime/interrupts.d.ts +14 -8
  202. package/dist/lib/runtime/interrupts.js +81 -25
  203. package/dist/lib/runtime/interrupts.test.js +58 -11
  204. package/dist/lib/runtime/intrinsicToolSchema.test.d.ts +1 -0
  205. package/dist/lib/runtime/intrinsicToolSchema.test.js +188 -0
  206. package/dist/lib/runtime/intrinsicTools.d.ts +88 -0
  207. package/dist/lib/runtime/intrinsicTools.js +82 -0
  208. package/dist/lib/runtime/ipc.js +8 -2
  209. package/dist/lib/runtime/node.js +7 -0
  210. package/dist/lib/runtime/prompt.d.ts +18 -0
  211. package/dist/lib/runtime/prompt.js +423 -201
  212. package/dist/lib/runtime/promptLabels.test.d.ts +1 -0
  213. package/dist/lib/runtime/promptLabels.test.js +159 -0
  214. package/dist/lib/runtime/promptRunner.d.ts +7 -2
  215. package/dist/lib/runtime/result.d.ts +6 -4
  216. package/dist/lib/runtime/result.js +37 -2
  217. package/dist/lib/runtime/result.test.js +58 -0
  218. package/dist/lib/runtime/revivers/functionRefReviver.js +27 -0
  219. package/dist/lib/runtime/revivers/functionRefReviver.test.js +41 -0
  220. package/dist/lib/runtime/rootBudget.d.ts +13 -0
  221. package/dist/lib/runtime/rootBudget.js +52 -0
  222. package/dist/lib/runtime/rootBudget.test.d.ts +1 -0
  223. package/dist/lib/runtime/rootBudget.test.js +70 -0
  224. package/dist/lib/runtime/runBatch.js +169 -9
  225. package/dist/lib/runtime/runPolicyHandler.d.ts +1 -1
  226. package/dist/lib/runtime/runPolicyHandler.js +5 -1
  227. package/dist/lib/runtime/runner.d.ts +9 -0
  228. package/dist/lib/runtime/runner.js +77 -8
  229. package/dist/lib/runtime/runner.test.js +19 -3
  230. package/dist/lib/runtime/saveDraftTool.d.ts +13 -0
  231. package/dist/lib/runtime/saveDraftTool.js +70 -0
  232. package/dist/lib/runtime/saveDraftTool.test.d.ts +1 -0
  233. package/dist/lib/runtime/saveDraftTool.test.js +114 -0
  234. package/dist/lib/runtime/state/context.d.ts +21 -3
  235. package/dist/lib/runtime/state/context.js +21 -3
  236. package/dist/lib/runtime/state/messageThread.d.ts +45 -2
  237. package/dist/lib/runtime/state/messageThread.js +84 -5
  238. package/dist/lib/runtime/state/messageThread.test.js +142 -0
  239. package/dist/lib/runtime/state/stateStack.d.ts +175 -6
  240. package/dist/lib/runtime/state/stateStack.js +293 -11
  241. package/dist/lib/runtime/state/stateStack.test.js +75 -1
  242. package/dist/lib/runtime/types.d.ts +19 -1
  243. package/dist/lib/runtime/utils.js +19 -3
  244. package/dist/lib/runtime/utils.test.js +42 -0
  245. package/dist/lib/serve/createServeHandler.d.ts +34 -0
  246. package/dist/lib/serve/createServeHandler.js +43 -0
  247. package/dist/lib/serve/createServeHandler.test.d.ts +1 -0
  248. package/dist/lib/serve/createServeHandler.test.js +115 -0
  249. package/dist/lib/serve/http/adapter.d.ts +8 -7
  250. package/dist/lib/serve/http/adapter.js +6 -2
  251. package/dist/lib/serve/http/adapter.test.js +7 -3
  252. package/dist/lib/serve/http/functionFrame.integration.test.js +0 -1
  253. package/dist/lib/serve/metadata.d.ts +22 -0
  254. package/dist/lib/serve/metadata.js +33 -0
  255. package/dist/lib/serve/metadata.test.d.ts +1 -0
  256. package/dist/lib/serve/metadata.test.js +46 -0
  257. package/dist/lib/serve/public.d.ts +8 -0
  258. package/dist/lib/serve/public.js +4 -0
  259. package/dist/lib/serve/public.test.d.ts +1 -0
  260. package/dist/lib/serve/public.test.js +16 -0
  261. package/dist/lib/statelogClient.d.ts +25 -4
  262. package/dist/lib/statelogClient.js +21 -1
  263. package/dist/lib/stdlib/appleNotes.d.ts +111 -0
  264. package/dist/lib/stdlib/appleNotes.js +314 -0
  265. package/dist/lib/stdlib/builtins.d.ts +7 -0
  266. package/dist/lib/stdlib/builtins.js +32 -3
  267. package/dist/lib/stdlib/cli.js +10 -8
  268. package/dist/lib/stdlib/git.d.ts +4 -0
  269. package/dist/lib/stdlib/git.js +24 -1
  270. package/dist/lib/stdlib/http.js +8 -0
  271. package/dist/lib/stdlib/markdown.d.ts +8 -0
  272. package/dist/lib/stdlib/markdown.js +238 -0
  273. package/dist/lib/stdlib/thread.d.ts +15 -7
  274. package/dist/lib/stdlib/thread.js +71 -27
  275. package/dist/lib/stdlib/wikipedia.js +17 -3
  276. package/dist/lib/symbolTable.d.ts +19 -0
  277. package/dist/lib/symbolTable.importModule.test.d.ts +1 -0
  278. package/dist/lib/symbolTable.importModule.test.js +54 -0
  279. package/dist/lib/symbolTable.js +59 -5
  280. package/dist/lib/templates/applescript/notes/accountWalk.d.ts +4 -0
  281. package/dist/lib/templates/applescript/notes/accountWalk.js +18 -0
  282. package/dist/lib/templates/applescript/notes/appendBody.d.ts +6 -0
  283. package/dist/lib/templates/applescript/notes/appendBody.js +15 -0
  284. package/dist/lib/templates/applescript/notes/appendScoped.d.ts +6 -0
  285. package/dist/lib/templates/applescript/notes/appendScoped.js +12 -0
  286. package/dist/lib/templates/applescript/notes/appendUnscoped.d.ts +6 -0
  287. package/dist/lib/templates/applescript/notes/appendUnscoped.js +12 -0
  288. package/dist/lib/templates/applescript/notes/create.d.ts +6 -0
  289. package/dist/lib/templates/applescript/notes/create.js +20 -0
  290. package/dist/lib/templates/applescript/notes/deleteScoped.d.ts +4 -0
  291. package/dist/lib/templates/applescript/notes/deleteScoped.js +13 -0
  292. package/dist/lib/templates/applescript/notes/deleteUnscoped.d.ts +4 -0
  293. package/dist/lib/templates/applescript/notes/deleteUnscoped.js +13 -0
  294. package/dist/lib/templates/applescript/notes/folderExists.d.ts +4 -0
  295. package/dist/lib/templates/applescript/notes/folderExists.js +11 -0
  296. package/dist/lib/templates/applescript/notes/listAll.d.ts +6 -0
  297. package/dist/lib/templates/applescript/notes/listAll.js +16 -0
  298. package/dist/lib/templates/applescript/notes/listFolders.d.ts +4 -0
  299. package/dist/lib/templates/applescript/notes/listFolders.js +20 -0
  300. package/dist/lib/templates/applescript/notes/listInFolder.d.ts +6 -0
  301. package/dist/lib/templates/applescript/notes/listInFolder.js +16 -0
  302. package/dist/lib/templates/applescript/notes/noteRow.d.ts +6 -0
  303. package/dist/lib/templates/applescript/notes/noteRow.js +13 -0
  304. package/dist/lib/templates/applescript/notes/preflight.d.ts +6 -0
  305. package/dist/lib/templates/applescript/notes/preflight.js +16 -0
  306. package/dist/lib/templates/applescript/notes/readScoped.d.ts +4 -0
  307. package/dist/lib/templates/applescript/notes/readScoped.js +13 -0
  308. package/dist/lib/templates/applescript/notes/readUnscoped.d.ts +4 -0
  309. package/dist/lib/templates/applescript/notes/readUnscoped.js +13 -0
  310. package/dist/lib/templates/applescript/notes/searchAll.d.ts +6 -0
  311. package/dist/lib/templates/applescript/notes/searchAll.js +16 -0
  312. package/dist/lib/templates/applescript/notes/searchInFolder.d.ts +6 -0
  313. package/dist/lib/templates/applescript/notes/searchInFolder.js +16 -0
  314. package/dist/lib/templates/applescript/notes/withTimeout.d.ts +7 -0
  315. package/dist/lib/templates/applescript/notes/withTimeout.js +13 -0
  316. package/dist/lib/templates/backends/agency/template.d.ts +1 -1
  317. package/dist/lib/templates/backends/agency/template.js +1 -1
  318. package/dist/lib/templates/backends/typescriptGenerator/blockSetup.d.ts +3 -1
  319. package/dist/lib/templates/backends/typescriptGenerator/blockSetup.js +10 -1
  320. package/dist/lib/templates/backends/typescriptGenerator/finalizeClosure.d.ts +10 -0
  321. package/dist/lib/templates/backends/typescriptGenerator/finalizeClosure.js +14 -0
  322. package/dist/lib/templates/backends/typescriptGenerator/functionCatchFailure.d.ts +2 -1
  323. package/dist/lib/templates/backends/typescriptGenerator/functionCatchFailure.js +6 -1
  324. package/dist/lib/templates/backends/typescriptGenerator/imports.d.ts +1 -1
  325. package/dist/lib/templates/backends/typescriptGenerator/imports.js +4 -1
  326. package/dist/lib/templates/backends/typescriptGenerator/withHandlerWrapper.d.ts +9 -0
  327. package/dist/lib/templates/backends/typescriptGenerator/withHandlerWrapper.js +14 -0
  328. package/dist/lib/templates/runtime/guardTripMessage.d.ts +9 -0
  329. package/dist/lib/templates/runtime/guardTripMessage.js +9 -0
  330. package/dist/lib/typeChecker/anySentinelRetired.test.d.ts +1 -0
  331. package/dist/lib/typeChecker/anySentinelRetired.test.js +53 -0
  332. package/dist/lib/typeChecker/assignability.d.ts +3 -3
  333. package/dist/lib/typeChecker/assignability.js +10 -9
  334. package/dist/lib/typeChecker/assignability.test.js +14 -1
  335. package/dist/lib/typeChecker/builtins.js +30 -20
  336. package/dist/lib/typeChecker/checker.d.ts +1 -1
  337. package/dist/lib/typeChecker/checker.js +91 -28
  338. package/dist/lib/typeChecker/diagnosticExplanations.js +51 -0
  339. package/dist/lib/typeChecker/diagnostics.d.ts +60 -0
  340. package/dist/lib/typeChecker/diagnostics.js +60 -0
  341. package/dist/lib/typeChecker/effectPayloadCheck.js +2 -1
  342. package/dist/lib/typeChecker/effectSets.js +2 -1
  343. package/dist/lib/typeChecker/finalize.test.d.ts +1 -0
  344. package/dist/lib/typeChecker/finalize.test.js +312 -0
  345. package/dist/lib/typeChecker/finalizeBinder.d.ts +21 -0
  346. package/dist/lib/typeChecker/finalizeBinder.js +70 -0
  347. package/dist/lib/typeChecker/finalizeBinder.test.d.ts +1 -0
  348. package/dist/lib/typeChecker/finalizeBinder.test.js +132 -0
  349. package/dist/lib/typeChecker/finalizeChecks.d.ts +16 -0
  350. package/dist/lib/typeChecker/finalizeChecks.js +161 -0
  351. package/dist/lib/typeChecker/fixtureTypeCheck.integration.test.js +3 -0
  352. package/dist/lib/typeChecker/flow.js +14 -12
  353. package/dist/lib/typeChecker/flow.test.js +12 -11
  354. package/dist/lib/typeChecker/flowBuilder.js +34 -2
  355. package/dist/lib/typeChecker/functionTypeRaises.js +2 -1
  356. package/dist/lib/typeChecker/functionValueEffects.js +2 -1
  357. package/dist/lib/typeChecker/guardConstruct.test.d.ts +1 -0
  358. package/dist/lib/typeChecker/guardConstruct.test.js +143 -0
  359. package/dist/lib/typeChecker/index.d.ts +1 -1
  360. package/dist/lib/typeChecker/index.js +44 -7
  361. package/dist/lib/typeChecker/inference.d.ts +1 -1
  362. package/dist/lib/typeChecker/inference.js +9 -12
  363. package/dist/lib/typeChecker/interruptAnalysis.js +8 -3
  364. package/dist/lib/typeChecker/matchExprTypes.js +7 -8
  365. package/dist/lib/typeChecker/missingImportDiagnostic.d.ts +12 -0
  366. package/dist/lib/typeChecker/missingImportDiagnostic.js +69 -0
  367. package/dist/lib/typeChecker/missingImportDiagnostic.test.d.ts +1 -0
  368. package/dist/lib/typeChecker/missingImportDiagnostic.test.js +183 -0
  369. package/dist/lib/typeChecker/narrowing.js +2 -1
  370. package/dist/lib/typeChecker/reservedNameDeclaration.test.js +21 -1
  371. package/dist/lib/typeChecker/resolveCall.js +26 -21
  372. package/dist/lib/typeChecker/saveDraft.test.d.ts +1 -0
  373. package/dist/lib/typeChecker/saveDraft.test.js +139 -0
  374. package/dist/lib/typeChecker/scope.d.ts +5 -1
  375. package/dist/lib/typeChecker/scope.js +6 -0
  376. package/dist/lib/typeChecker/scope.test.js +9 -8
  377. package/dist/lib/typeChecker/scopes.js +16 -11
  378. package/dist/lib/typeChecker/synthesizer.d.ts +2 -2
  379. package/dist/lib/typeChecker/synthesizer.js +100 -68
  380. package/dist/lib/typeChecker/typeCases.d.ts +1 -1
  381. package/dist/lib/typeChecker/typeCases.js +3 -2
  382. package/dist/lib/typeChecker/typeCases.test.js +2 -1
  383. package/dist/lib/typeChecker/types.d.ts +10 -10
  384. package/dist/lib/typeChecker/utils.d.ts +7 -2
  385. package/dist/lib/typeChecker/utils.js +6 -4
  386. package/dist/lib/types/finalizeBlock.d.ts +19 -0
  387. package/dist/lib/types/finalizeBlock.js +1 -0
  388. package/dist/lib/types/guardBlock.d.ts +25 -0
  389. package/dist/lib/types/guardBlock.js +1 -0
  390. package/dist/lib/types/markDestructiveRan.d.ts +8 -0
  391. package/dist/lib/types/markDestructiveRan.js +1 -0
  392. package/dist/lib/types/parallelBlock.d.ts +5 -0
  393. package/dist/lib/types.d.ts +7 -1
  394. package/dist/lib/types.js +3 -0
  395. package/dist/lib/utils/bodySlots.js +27 -0
  396. package/dist/lib/utils/bodySlots.test.d.ts +1 -0
  397. package/dist/lib/utils/bodySlots.test.js +17 -0
  398. package/dist/lib/utils/node.js +3 -0
  399. package/dist/scripts/agency.js +62 -11
  400. package/package.json +8 -2
  401. package/stdlib/agency/eval.js +33 -6
  402. package/stdlib/agency/local.js +28 -13
  403. package/stdlib/agency.agency +159 -119
  404. package/stdlib/agency.js +1409 -196
  405. package/stdlib/agent.agency +1 -296
  406. package/stdlib/agent.js +13 -1162
  407. package/stdlib/agents/agency.agency +88 -0
  408. package/stdlib/agents/agency.js +1415 -0
  409. package/stdlib/agents/coding.agency +67 -0
  410. package/stdlib/agents/coding.js +633 -0
  411. package/stdlib/agents/expert.agency +197 -0
  412. package/stdlib/agents/expert.js +1828 -0
  413. package/stdlib/agents/planner.agency +176 -0
  414. package/stdlib/agents/planner.js +2561 -0
  415. package/stdlib/agents/research.agency +64 -0
  416. package/stdlib/agents/research.js +744 -0
  417. package/stdlib/agents/shared.agency +21 -0
  418. package/stdlib/agents/shared.js +516 -0
  419. package/stdlib/args.js +8 -2
  420. package/stdlib/array.js +17 -13
  421. package/stdlib/auth/keyring.agency +8 -4
  422. package/stdlib/auth/keyring.js +16 -6
  423. package/stdlib/auth/oauth.agency +4 -2
  424. package/stdlib/auth/oauth.js +14 -6
  425. package/stdlib/calendar.agency +27 -21
  426. package/stdlib/calendar.js +20 -8
  427. package/stdlib/capabilities.agency +9 -0
  428. package/stdlib/capabilities.js +12 -1
  429. package/stdlib/clipboard.agency +4 -2
  430. package/stdlib/clipboard.js +17 -4
  431. package/stdlib/concurrency.js +8 -2
  432. package/stdlib/data/finance/dbnomics.js +45 -5
  433. package/stdlib/data/finance/edgar.js +66 -8
  434. package/stdlib/data/finance/fred.js +71 -11
  435. package/stdlib/data/finance/gdelt.js +37 -5
  436. package/stdlib/data/people/littlesis.js +85 -20
  437. package/stdlib/data/tech/hackernews.js +100 -24
  438. package/stdlib/data/tech/yc.js +76 -20
  439. package/stdlib/data/usaspending.js +62 -13
  440. package/stdlib/data/wikidata.js +103 -18
  441. package/stdlib/date.js +22 -16
  442. package/stdlib/docs/cli/agent.md +7 -0
  443. package/stdlib/docs/cli/run.md +2 -0
  444. package/stdlib/docs/diagnostics/effects.md +12 -0
  445. package/stdlib/docs/diagnostics/index.md +12 -0
  446. package/stdlib/docs/diagnostics/names.md +30 -0
  447. package/stdlib/docs/diagnostics/tools.md +93 -0
  448. package/stdlib/docs/guide/guards.md +141 -44
  449. package/stdlib/docs/guide/llm-part-2.md +20 -3
  450. package/stdlib/docs/guide/partial-results.md +166 -0
  451. package/stdlib/docs/guide/ts-helpers.md +2 -0
  452. package/stdlib/fs.agency +24 -12
  453. package/stdlib/fs.js +80 -52
  454. package/stdlib/git.agency +109 -24
  455. package/stdlib/git.js +346 -29
  456. package/stdlib/http.js +10 -4
  457. package/stdlib/image.js +8 -2
  458. package/stdlib/index.agency +67 -4
  459. package/stdlib/index.js +432 -27
  460. package/stdlib/llm.js +41 -10
  461. package/stdlib/markdown.agency +26 -0
  462. package/stdlib/markdown.js +140 -7
  463. package/stdlib/math.js +12 -6
  464. package/stdlib/mcp.js +12 -6
  465. package/stdlib/memory.agency +14 -12
  466. package/stdlib/memory.js +62 -23
  467. package/stdlib/messaging/email.agency +53 -47
  468. package/stdlib/messaging/email.js +17 -5
  469. package/stdlib/messaging/imessage.agency +7 -5
  470. package/stdlib/messaging/imessage.js +11 -3
  471. package/stdlib/messaging/sms.agency +10 -8
  472. package/stdlib/messaging/sms.js +11 -3
  473. package/stdlib/notes/apple.agency +360 -0
  474. package/stdlib/notes/apple.js +1948 -0
  475. package/stdlib/object.js +24 -9
  476. package/stdlib/path.js +14 -8
  477. package/stdlib/policy.agency +19 -9
  478. package/stdlib/policy.js +162 -28
  479. package/stdlib/shell.agency +36 -9
  480. package/stdlib/shell.js +54 -33
  481. package/stdlib/skills.agency +59 -20
  482. package/stdlib/skills.js +470 -41
  483. package/stdlib/speech.js +15 -4
  484. package/stdlib/statelog.js +23 -8
  485. package/stdlib/strategy.js +42 -7
  486. package/stdlib/syntax.js +11 -5
  487. package/stdlib/system.js +28 -13
  488. package/stdlib/tag.js +13 -7
  489. package/stdlib/thread.agency +20 -69
  490. package/stdlib/thread.js +154 -132
  491. package/stdlib/ui/chart.js +16 -5
  492. package/stdlib/ui/cli.js +27 -8
  493. package/stdlib/ui/layout.js +70 -19
  494. package/stdlib/ui/table.js +19 -8
  495. package/stdlib/ui.js +438 -57
  496. package/stdlib/validation.js +18 -12
  497. package/stdlib/weather.js +10 -4
  498. package/stdlib/web/browser.js +8 -2
  499. package/stdlib/web/search.js +9 -3
  500. package/stdlib/wikipedia.js +10 -4
  501. package/dist/lib/agents/agency-agent/prompts/system.md +0 -4
@@ -1,9 +1,4 @@
1
- import { getVersion } from "std::agency"
2
- import {
3
- printLocalCatalog,
4
- localModelsSupported,
5
- listModelNames,
6
- } from "std::agency/local"
1
+ import { localModelsSupported, listModelNames } from "std::agency/local"
7
2
  import { parseArgs } from "std::args"
8
3
  import { map } from "std::array"
9
4
  import { today } from "std::date"
@@ -11,7 +6,6 @@ import { generateImage } from "std::image"
11
6
  import { applyAgentCwd, setAgentCwd } from "std::index"
12
7
  import { listHostedModels, hostedModelInfo, setLlmOptions } from "std::llm"
13
8
  import { setMemoryId, disableMemory } from "std::memory"
14
- import { keys } from "std::object"
15
9
  import { basename, dirname } from "std::path"
16
10
  import {
17
11
  ScopedRuleFields,
@@ -24,7 +18,6 @@ import {
24
18
  builtinPolicyNames,
25
19
  BUILTIN_POLICIES,
26
20
  } from "std::policy"
27
- import { loadMcpToolsWithStatus, mergeMcpServers, isMcpAvailable, readProjectMcpConfig } from "std::mcp"
28
21
  import { exists } from "std::shell"
29
22
  import { commandsDir, expandSlash } from "std::skills"
30
23
  import { highlight } from "std::syntax"
@@ -45,19 +38,33 @@ import {
45
38
  clearScreen,
46
39
  clearHistory,
47
40
  } from "std::ui/cli"
48
- import { box, render } from "std::ui/layout"
49
41
 
50
- import { figs, title } from "./images/images.agency"
42
+ import { parseAgentArgs } from "./lib/args.agency"
51
43
  import { detectAttachments, modalityFilter } from "./lib/attachments.agency"
44
+ import { showCapabilities, editCapabilities } from "./lib/capabilitiesUi.agency"
45
+ import { mergedPalette, projectCommands } from "./lib/commandPalette.agency"
52
46
  import {
53
47
  POLICY_PATH,
54
48
  SESSION_POLICY_PATH,
55
49
  HISTORY_PATH,
56
50
  ALWAYS_FIELDS,
57
51
  } from "./lib/config.agency"
52
+ import { getHeader } from "./lib/header.agency"
53
+ import { generateImageFile } from "./lib/images.agency"
54
+ import { getMcpTools, maybeLoadMcp, mcpSlashCommand } from "./lib/mcp.agency"
58
55
  import { filterHostedModels, ModelFilters } from "./lib/modelFilters.agency"
59
- import { parseModelFlag, parseModelSpec, knownProviders, embeddingSpecHasProvider, missingProviderEnvVar, Resolved } from "./lib/models.agency"
60
- import { SLOTS } from "./lib/slots.agency"
56
+ import {
57
+ parseModelFlag,
58
+ parseModelSpec,
59
+ knownProviders,
60
+ missingProviderEnvVar,
61
+ Resolved,
62
+ } from "./lib/models.agency"
63
+ import {
64
+ givePolicyChoice,
65
+ printPolicyHelp,
66
+ getPolicyForAgent,
67
+ } from "./lib/policy.agency"
61
68
  import {
62
69
  buildLayers,
63
70
  resolveAll,
@@ -72,21 +79,29 @@ import {
72
79
  availableBackendItems,
73
80
  } from "./lib/search.agency"
74
81
  import {
75
- CAPABILITY_FIELDS,
76
- CapabilityPatch,
77
- mergeCapabilityOverride,
78
- removeCapabilityField,
79
- providerLacksEmbeddings,
80
- } from "./lib/capabilities.agency"
82
+ Session,
83
+ switchModel,
84
+ reactToSlotChange,
85
+ modelChoiceItems,
86
+ parseModelsFilters,
87
+ markMemoryEmbeddingsPinned,
88
+ pickModelsForAgent,
89
+ } from "./lib/session.agency"
81
90
  import {
82
- Settings,
83
91
  getCapabilitySettings,
84
92
  getModelSettings,
85
- getMcpServers,
86
93
  loadSettings,
87
94
  saveSettings,
88
95
  } from "./lib/settings.agency"
89
- import { truncate, formatArgs, formatToolResponse } from "./lib/utils.agency"
96
+ import { SLOTS } from "./lib/slots.agency"
97
+ import {
98
+ truncate,
99
+ formatArgs,
100
+ formatToolResponse,
101
+ retryReasonLabel,
102
+ formatTurnFailure,
103
+ } from "./lib/utils.agency"
104
+ import { renderLLMCallResponse } from "./lib/utils/renderLLMCallResponse.agency"
90
105
  import {
91
106
  configureModels,
92
107
  configureLocalModel,
@@ -106,7 +121,6 @@ import {
106
121
  overrideFromResolved,
107
122
  embeddingsOverride,
108
123
  providerEnvMissing,
109
- demoteProvider,
110
124
  } from "./shared.agency"
111
125
  import { codeAgent, setCodeOneShot } from "./subagents/code.agency"
112
126
  import { explorerAgent } from "./subagents/explorer.agency"
@@ -126,14 +140,18 @@ import { reviewAgent } from "./subagents/review.agency"
126
140
  /*
127
141
  * Agency Agent — helps users create and modify Agency code.
128
142
  *
129
- * Multi-thread architecture: the agent splits work across two
130
- * specialists, each with its own thread / system prompt / tool set:
143
+ * Multi-thread architecture: the coordinator (`mainAgent`) runs the
144
+ * user's message with the specialists registered as tools. Each
145
+ * specialist has its own thread / system prompt / tool set, and the
146
+ * coordinator's LLM delegates by calling one as a tool and getting its
147
+ * reply back (call/return — not a flat hop/handoff loop):
131
148
  * - `code.agency` — writes, edits, typechecks, runs code
132
149
  * - `research.agency` — looks up docs, fetches URLs, browses web
150
+ * - `review.agency` — parse/typecheck feedback on Agency code
151
+ * - `oracle.agency` — read-only critique on a stronger model
152
+ * - `explorer.agency` — read-only broad codebase/docs synthesis
133
153
  *
134
154
  * The standard library does the heavy lifting:
135
- * - `std::agent.route` runs the per-turn hop loop, owns the
136
- * handoff signal, and force-answers when the hop limit is hit.
137
155
  * - `std::policy.cliPolicyHandler` provides the interactive
138
156
  * approve/reject/always menu and persists "always" decisions to
139
157
  * `~/.agency-agent/policy.json`.
@@ -146,7 +164,7 @@ import { reviewAgent } from "./subagents/review.agency"
146
164
  * `mainAgent` and printing the reply into the scroll output.
147
165
  * 2. Per-effect ALWAYS_FIELDS map for the policy handler's
148
166
  * "approve-always-here" option.
149
- * 3. Wire the two specialists into one `RouterConfig`.
167
+ * 3. Wire the specialists into `mainAgentTools`.
150
168
  *
151
169
  * Other features:
152
170
  * - Doc-as-skills: bundles `docs/site/guide` etc. (copied to
@@ -159,28 +177,29 @@ import { reviewAgent } from "./subagents/review.agency"
159
177
  * editing `.agency` files.
160
178
  */
161
179
 
162
- // Verbose tool-call tracing. Enabled either by `AGENT_DEBUG=1` (legacy)
163
- // or by passing `--debug` / `--verbose` on the command line (set in main()).
164
180
  let AGENT_DEBUG: boolean = env("AGENT_DEBUG") == "1"
165
181
  let VERBOSE: boolean = false
166
182
 
167
- // Whether the agent is running in interactive (TTY) mode. Set in main()
168
- // before any tool calls happen, so the callbacks below can suppress
169
- // scroll-output writes when stdout is destined for a Unix pipe.
183
+ // Whether the agent is running in interactive (TTY) mode.
170
184
  let _isInteractive: boolean = true
171
185
 
172
- // The `--policy` argument (a built-in name or a policy-file path), or ""
173
- // when the flag is absent. Set in main() before `setupSession` runs;
174
- // consumed there to override the saved policy for this run only.
186
+ // The `--policy` argument (a built-in name or a policy-file path),
187
+ // or "" when absent
175
188
  let _policyArg: string = ""
176
189
 
177
- // Live tool-call tracing. Always shown in the interactive REPL; in
178
- // one-shot mode it's suppressed so piped output is just the reply,
179
- // unless the user opts in with `--verbose` / `--debug` (AGENT_DEBUG).
190
+ // Live tool-call tracing. Always shown in the interactive REPL, but in
191
+ // one-shot mode it's suppressed unless the user opts in
192
+ // with `--verbose` / `--debug` (AGENT_DEBUG). A function, not a static
193
+ // const: it reads globals that `main()` sets after module init, so the
194
+ // value must be computed per call.
180
195
  def _showTraces(): boolean {
181
196
  return _isInteractive || VERBOSE || AGENT_DEBUG
182
197
  }
183
198
 
199
+ /**************************
200
+ **** CALLBACKS / HOOKS ****
201
+ ***************************/
202
+
184
203
  callback("onToolCallStart") as data {
185
204
  if (_showTraces()) {
186
205
  pushMessage(color.yellow("⏺ ${data.toolName}(${formatArgs(data.args)})"))
@@ -206,6 +225,21 @@ callback("onLLMCallStart") as data {
206
225
  }
207
226
 
208
227
  callback("onLLMCallEnd") as data {
228
+ if (data.result.hostedToolResults) {
229
+ for (result in data.result.hostedToolResults) {
230
+ match(result.tool) {
231
+ "web_search" => {
232
+ const queries = map(result.queries, \q -> "\"${q}\"").join(", ")
233
+ // there's also result.sources and result.citations
234
+ pushMessage(
235
+ color.yellow(
236
+ "⏺ web_search(provider: '${result.provider}', queries: [${queries}])",
237
+ ),
238
+ )
239
+ }
240
+ }
241
+ }
242
+ }
209
243
  if (AGENT_DEBUG || VERBOSE) {
210
244
  pushMessage(
211
245
  color.green.dim("💬 [${data.model}] ${renderLLMCallResponse(data.result)}"),
@@ -213,42 +247,6 @@ callback("onLLMCallEnd") as data {
213
247
  }
214
248
  }
215
249
 
216
- // Map a transient LLM retry reason (an `LLMRetryReason` from the runtime
217
- // retry layer) to a short, human-facing label. `connectionLost` is the
218
- // lost-internet / closed-the-laptop case; the rest cover provider hiccups
219
- // and our own per-call timeout.
220
- export def retryReasonLabel(reason: string): string {
221
- if (reason == "connectionLost") {
222
- return "Connection lost"
223
- }
224
- if (reason == "timeout") {
225
- return "Request timed out"
226
- }
227
- if (reason == "streamInterrupted") {
228
- return "Connection interrupted"
229
- }
230
- if (reason == "rateLimit") {
231
- return "Rate limited"
232
- }
233
- if (reason == "overloaded") {
234
- return "Model overloaded"
235
- }
236
- if (reason == "serverError") {
237
- return "Server error"
238
- }
239
- if (reason == "invalidStructuredOutput") {
240
- return "Output failed validation"
241
- }
242
- return reason
243
- }
244
-
245
- // A transient LLM failure (lost connection, timeout, server hiccup) the
246
- // runtime is about to retry. Surface a dim, one-line heads-up so the user
247
- // understands the pause instead of staring at a frozen prompt — this is the
248
- // "lost internet / closed the laptop" experience the abort taxonomy made
249
- // distinguishable. Gated like the other traces so piped one-shot output
250
- // stays clean. `attempt` is the 1-based retry number; `maxRetries` is the
251
- // configured retry budget.
252
250
  callback("onLLMRetry") as data {
253
251
  if (_showTraces()) {
254
252
  pushMessage(
@@ -259,58 +257,11 @@ callback("onLLMRetry") as data {
259
257
  }
260
258
  }
261
259
 
262
- // Turn a terminal (retries-exhausted) LLM failure into a short, actionable
263
- // message. The runtime's exhausted-retry error text embeds the reason in
264
- // parens, e.g. "LLM call failed after 3 attempts (connectionLost): ...". We
265
- // recognize the transport reasons that mean "the network or model dropped"
266
- // and give concrete advice; anything else echoes the raw error so nothing
267
- // is hidden.
268
- export def formatTurnFailure(errText: string): string {
269
- if (errText.includes("(connectionLost)") || errText.includes("(streamInterrupted)")) {
270
- return "⚠ Lost the connection to the model and couldn't recover after retrying.\nCheck your internet connection and try again."
271
- }
272
- if (errText.includes("(timeout)")) {
273
- return "⚠ The model took too long to respond — the request timed out after retrying.\nTry again, perhaps with a smaller request."
274
- }
275
- if (errText.includes("(rateLimit)") || errText.includes("(overloaded)") || errText.includes("(serverError)")) {
276
- return "⚠ The model provider is busy and the request failed after retrying.\nWait a moment and try again."
277
- }
278
- return "⚠ The request failed: ${errText}"
279
- }
280
-
281
- type ToolCallData = {
282
- name: string;
283
- arguments: any
284
- }
285
-
286
- type LLMResponse = {
287
- output?: string;
288
- toolCalls: ToolCallData[];
289
- usage: { inputTokens: number; outputTokens: number; totalTokens: number; cachedInputTokens: number };
290
- cost: { inputCost: number; outputCost: number; cachedInputCost: number; totalCost: number; currency: string };
291
- model: string
292
- }
293
-
294
- def renderLLMCallResponse(data: LLMResponse): string {
295
- let response = []
296
- if (data.output != "" && data.output != null && data.output != undefined) {
297
- response.push(data.output)
298
- }
299
- if (data.toolCalls.length > 0) {
300
- for (toolCall in data.toolCalls) {
301
- response.push(`${toolCall.name}(${formatArgs(toolCall.arguments)})`)
302
- }
303
- }
304
- return response.join("\n")
305
- }
306
-
307
260
  def loadAgentsMd(dir: string): string {
308
261
  """
309
262
  Read `AGENTS.md` from `dir` if it exists and return it wrapped in a
310
263
  <project_context> block ready to splice into a system prompt. Returns
311
- an empty string if there is no AGENTS.md or it cannot be read. The
312
- read is auto-approved — the user implicitly opted in to scanning the
313
- project directory by running the agent there.
264
+ an empty string if there is no AGENTS.md or it cannot be read.
314
265
  """
315
266
  if (!exists("AGENTS.md", dir)) {
316
267
  return ""
@@ -319,518 +270,166 @@ def loadAgentsMd(dir: string): string {
319
270
  if (isFailure(result)) {
320
271
  return ""
321
272
  }
322
- return "\n\n<project_context>\n${result.value}\n</project_context>"
273
+ return """
274
+
275
+ <project_context>
276
+ ${result.value}
277
+ </project_context>
278
+ """
323
279
  }
324
280
 
325
281
  // Per-turn grounding context. Captured at startup and re-used so the
326
- // LLM doesn't have to re-ground each turn. Initialized in `main()`.
282
+ // LLM doesn't have to re-ground each turn.
327
283
  let _context: string = ""
328
284
 
329
- // Project-local slash commands. `cwd()` works at module-init time so
330
- // we anchor at the user's project root (not the agent's source dir).
331
- // `with approve` auto-approves the `glob`/`read` during init the
332
- // user implicitly opted in by running the agent in this directory.
333
- static const projectCommands = commandsDir("${cwd()}/.claude/commands") with approve
334
-
335
- def builtinPalette(): Record<string, string> {
336
- return {
337
- "/exit": "Exit the agent",
338
- "/clear": "Clear the conversation transcript",
339
- "/clear-history": "Clear the input history (recall + saved file)",
340
- "/cost": "Show cumulative LLM cost and tokens",
341
- "/model": "Switch the model (bare to pick, or slot=model)",
342
- "/models": "List / filter the hosted model catalog",
343
- "/local": "Switch to a local model",
344
- "/search": "Choose the web search backend (hosted / Tavily / Brave / off)",
345
- "/settings": "View and change capability settings for the current model",
346
- "/mcp": "List configured MCP servers and their tools",
347
- "/paste": "Multi-line paste mode (Ctrl+D submits, Ctrl+C cancels)",
348
- "/help": "Show available slash commands"
349
- }
350
- }
351
-
352
- // Built-ins win over file commands so a user `/clear.md` can't shadow
353
- // the built-in `/clear`. Build file entries first, then overlay builtins.
354
- def mergedPalette(): Record<string, string> {
355
- let out: Record<string, string> = {}
356
- for (cmd in projectCommands) {
357
- let label = cmd.description
358
- if (cmd.argHint != "") {
359
- label = "${cmd.description} ${cmd.argHint}"
360
- }
361
- out["/${cmd.name}"] = label
362
- }
363
- const builtins = builtinPalette()
364
- for (key in builtins) {
365
- out[key] = builtins[key]
366
- }
367
- return out
368
- }
369
-
370
- // In-session model selection state. One mutable cell holding the current
371
- // Session (the pin + per-slot overrides chosen via /model this run). switchModel
372
- // is pure over this value — it copies, never mutates — so it stays testable.
373
- export type Session = {
374
- pin: string;
375
- slots: Record<string, string>
376
- }
377
-
285
+ // In-session model selection state (the pin + per-slot overrides chosen via
286
+ // `/model` this run). `switchModel` is pure over this value it copies,
287
+ // never mutates so the REPL folds its return back in here.
378
288
  let _session: Session = {
379
289
  pin: "",
380
290
  slots: {}
381
291
  }
382
292
 
383
- /** Apply an in-session model selection. Copies `prior` (never mutates), folds in
384
- the new spec (bare → pin; slot=model → per-slot), resolves, applies, and returns
385
- the new session + assignment + the slots that changed (so the caller can react,
386
- e.g. the cross-provider recall notice). Turn-boundary semantics: takes effect on
387
- the NEXT turn; no in-flight call is mutated. */
388
- export def switchModel(
389
- prior: Session,
390
- spec: string,
391
- ctx: Ctx,
392
- before: Record<string, Resolved>,
393
- ): { session: Session; resolved: Record<string, Resolved>; changes: SlotChange[] } {
394
- // The caller builds ctx from currentProvider(), which returns the
395
- // PROMOTED name on openai sessions. priceDefault and the built-in
396
- // tables key by public provider names, so demote before resolving —
397
- // otherwise a switch that lets any slot fall through to the built-in
398
- // defaults fails with "openai-responses has no built-in defaults".
399
- const ctx2: Ctx = {
400
- provider: demoteProvider(ctx.provider),
401
- price: ctx.price
402
- }
403
- let nextSlots: Record<string, string> = {}
404
- for (key in Object.keys(prior.slots)) {
405
- nextSlots[key] = prior.slots[key]
406
- }
407
- let next: Session = {
408
- pin: prior.pin,
409
- slots: nextSlots
410
- }
411
- const parsed = parseModelFlag(spec)
412
- if (parsed.slot != "" && !SLOTS.includes(parsed.slot)) {
413
- pushMessage(
414
- color.red("Unknown model slot '${parsed.slot}'. Valid slots: ${SLOTS.join(", ")}."),
415
- )
416
- return {
417
- session: prior,
418
- resolved: before,
419
- changes: []
420
- }
421
- }
422
- if (parsed.slot == "embedding" && !embeddingSpecHasProvider(parsed.model)) {
423
- // The embedding slot exists to point AWAY from the chat provider, so
424
- // the provider can never be inferred. Require the explicit form.
425
- pushMessage(
426
- color.red(
427
- "Embedding models need the provider/model form, e.g. embedding=openai/text-embedding-3-small.",
428
- ),
429
- )
430
- return {
431
- session: prior,
432
- resolved: before,
433
- changes: []
434
- }
435
- }
436
- if (parsed.slot == "") {
437
- next.pin = parsed.model
438
- } else {
439
- next.slots[parsed.slot] = parsed.model
440
- }
441
-
442
- // A saved provider (settings.model.provider) means a provider is established,
443
- // so a unified provider/model spec should not be split during resolution.
444
- const settings = getModelSettings(loadSettings())
445
- const providerEstablished = settings.provider != null && settings.provider != ""
446
- const layers = buildLayers(
447
- next.pin,
448
- next.slots,
449
- "",
450
- {},
451
- settings,
452
- knownProviders(),
453
- providerEstablished,
293
+ def searchSlashCommand(): boolean {
294
+ const opts = availableBackendItems()
295
+ const choice = chooseOption(
296
+ "Web search backend",
297
+ "Pick how this agent searches the web. Current: ${getSearchBackend()}",
298
+ opts,
299
+ false,
300
+ true,
454
301
  )
455
- const all = resolveAll(layers, ctx2)
456
- if (isFailure(all)) {
457
- pushMessage(color.red("Could not switch model: ${all.error}"))
458
- return {
459
- session: prior,
460
- resolved: before,
461
- changes: []
462
- }
463
- }
464
- const resolved = promoteAll(all.value)
465
- applyResolved(resolved)
466
- return {
467
- session: next,
468
- resolved: resolved,
469
- changes: diffResolved(before, resolved)
302
+ if (choice != "" && choice != null && choice != undefined) {
303
+ setSearchBackend(choice)
304
+ pushMessage("Web search backend set to: ${choice}")
470
305
  }
306
+ return true
471
307
  }
472
308
 
473
- /** React to one resolved-slot change. Phase 1: a `main`-slot provider change
474
- means the embedding space behind memory changed, so pause memory and warn.
475
- (std::memory has no tier-2-only toggle yet -- full pause; structured recall
476
- returns when a selective toggle lands.) Returns true if it emitted the notice. */
477
- export def reactToSlotChange(change: SlotChange): boolean {
478
- if (change.slot == "main" && change.before.provider != change.after.provider) {
479
- // Skip the pause only when startup actually enabled memory WITH the
480
- // override. A set-but-keyless slot took the fallback path, so memory
481
- // runs on derived embeddings and the provider change still shifts
482
- // the embedding space behind recall.
483
- if (_memoryEmbeddingsPinned) {
484
- return false
485
- }
486
- pushMessage(
487
- color.dim(
488
- "Warning: provider changed - memory is paused until restart (recall would be unreliable).",
489
- ),
490
- )
491
- // `with approve`: the agent is deliberately auto-pausing memory on a
492
- // provider switch, not asking the user — so don't surface an approval prompt.
493
- disableMemory() with approve
494
- return true
495
- }
496
- if (change.slot == "embedding") {
497
- const override = overrideFromResolved(change.after)
498
- if (override != null) {
499
- const missing = missingProviderEnvVar(override.provider)
500
- if (missing != "") {
501
- pushMessage(
502
- color.yellow(
503
- "Warning: the embedding slot needs ${missing}, which is not set. The assignment takes effect at next agent start, and memory falls back to the default embedding behavior until it is exported.",
504
- ),
505
- )
506
- return true
507
- }
508
- // The healthy path must not be silent: enableMemory already ran,
509
- // so the user would otherwise believe embeddings switched now.
309
+ def costSlashCommand(): boolean {
310
+ // Derive BOTH the header total and the per-model breakdown from the
311
+ // same process-wide accumulator (getModelCosts), so the `Cost:` /
312
+ // `Tokens:` lines and the rows below them can never visibly disagree.
313
+ // (getCost()/getTokens() read the per-branch accumulator, a different
314
+ // scope adjacent in the output, they could in principle diverge.)
315
+ // Rows are sorted by cost descending by getModelCosts(); a subagent
316
+ // on a pricier model (e.g. the oracle on opus-4.8) shows up here.
317
+ const byModel = getModelCosts()
318
+ let totalCost = 0.0
319
+ let totalTokens = 0
320
+ for (m in byModel) {
321
+ totalCost = totalCost + m.cost
322
+ totalTokens = totalTokens + m.inputTokens + m.outputTokens
323
+ }
324
+ pushMessage("Cost: $${totalCost.toFixed(4)}")
325
+ pushMessage("Tokens: ${totalTokens}")
326
+ if (byModel.length > 0) {
327
+ pushMessage("")
328
+ pushMessage("By model:")
329
+ for (m in byModel) {
510
330
  pushMessage(
511
- color.dim(
512
- "Embedding slot set: memory embeddings use ${override.provider}/${override.model} at next agent start.",
513
- ),
331
+ " ${m.model} ↑${m.inputTokens} ↓${m.outputTokens} $${m.cost.toFixed(4)}",
514
332
  )
515
- return true
516
333
  }
517
334
  }
518
- return false
519
- }
520
-
521
- // ChoiceItem list for the model pickers: the hosted catalog, filtered, each
522
- // labeled with provider + input price for an informed pick.
523
- export def modelChoiceItems(filters: ModelFilters): ChoiceItem[] {
524
- let items: ChoiceItem[] = []
525
- for (model in filterHostedModels(listHostedModels(), filters)) {
526
- const label = "${model.name} (${model.provider}, $${model.inputCost}/1M in)"
527
- items.push({
528
- key: model.name,
529
- label: label
530
- })
531
- }
532
- return items
533
- }
534
-
535
- // Parse a `/models [provider]` command line into filters. Bare `/models` (or a
536
- // trailing-space-only arg) yields no filters; `/models <provider>` narrows to
537
- // one provider. Extracted from the handler so it is unit-testable.
538
- export def parseModelsFilters(cmd: string): ModelFilters {
539
- const prefix = "/models "
540
- if (!cmd.startsWith(prefix)) {
541
- return {}
542
- }
543
- const arg = cmd.substring(prefix.length).trim()
544
- if (arg == "") {
545
- return {}
546
- }
547
- return {
548
- provider: arg
549
- }
550
- }
551
-
552
- /** Human label for a capability source layer. Interprets the source
553
- strings resolveCapabilities emits (capabilities.agency). */
554
- def capabilitySourceLabel(source: string): string {
555
- if (source == "default") {
556
- return "built-in default"
557
- }
558
- if (source == "provider") {
559
- return "provider default: ${getCapabilityProvider()}"
560
- }
561
- if (source == "model") {
562
- return "built-in for ${getCapabilityModelKey()}"
563
- }
564
- if (source == "user-global") {
565
- return "your global override"
566
- }
567
- if (source == "user-provider") {
568
- return "your override for ${getCapabilityProvider()}"
569
- }
570
- return "your override for ${getCapabilityModelKey()}"
335
+ return true
571
336
  }
572
337
 
573
- /** One "value (source)" cell. The typed Capabilities shape cannot be
574
- indexed by a dynamic key, so this is the renderer's per-field switch. */
575
- def capabilityRow(fieldKey: string): string {
576
- const resolved = getCapabilitiesResolved()
577
- const values = resolved.values
578
- const sources = resolved.sources
579
- if (fieldKey == "prompt") {
580
- return "${values.prompt} (${capabilitySourceLabel(sources.prompt)})"
581
- }
582
- if (fieldKey == "summarize") {
583
- return "${values.summarize} (${capabilitySourceLabel(sources.summarize)})"
338
+ def modelSlashCommand(): boolean {
339
+ let spec = ""
340
+ if (trimmed != "/model") {
341
+ spec = trimmed.substring(7).trim()
584
342
  }
585
- if (fieldKey == "memory") {
586
- return "${values.memory} (${capabilitySourceLabel(sources.memory)})"
343
+ if (spec == "") {
344
+ pushMessage("Current models:")
345
+ const current = getResolvedSlots()
346
+ for (slot in Object.keys(current)) {
347
+ const resolved = current[slot]
348
+ if (resolved != null) {
349
+ pushMessage(" ${slot}: ${resolved.model} (${resolved.via})")
350
+ }
351
+ }
352
+ const items = modelChoiceItems({})
353
+ const choice = chooseOption(
354
+ "Model",
355
+ "Pick a model, or type one (or slot=model, e.g. reasoning=claude-opus-4-8). Empty to cancel.",
356
+ items,
357
+ allowFreeText: true,
358
+ allowCancel: true,
359
+ )
360
+ if (choice == "" || choice == null) {
361
+ return true
362
+ }
363
+ spec = choice
587
364
  }
588
- let cap = "none"
589
- if (values.maxTokens != null) {
590
- cap = "${values.maxTokens}"
365
+ const ctx: Ctx = {
366
+ provider: currentProvider(),
367
+ price: "standard"
591
368
  }
592
- return "${cap} (${capabilitySourceLabel(sources.maxTokens)})"
593
- }
594
-
595
- /** Render the current profile with per-field provenance. */
596
- def showCapabilities() {
597
- pushMessage("Capabilities for ${getCapabilityModelKey()} (${getCapabilityProvider()}):")
598
- for (field in CAPABILITY_FIELDS) {
599
- pushMessage(" ${field.label} ${capabilityRow(field.key)}")
369
+ const out = switchModel(_session, spec, ctx, getResolvedSlots())
370
+ _session = out.session
371
+ for (change in out.changes) {
372
+ reactToSlotChange(change)
600
373
  }
601
- }
602
-
603
- /** The settings.json key a scope writes under. Global's key is unused. */
604
- def scopeTargetName(scopeKey: string): string {
605
- if (scopeKey == "provider") {
606
- return getCapabilityProvider()
374
+ pushMessage("Model set:")
375
+ for (slot in Object.keys(out.resolved)) {
376
+ const resolved = out.resolved[slot]
377
+ if (resolved != null) {
378
+ pushMessage(color.dim(" ${slot}: ${resolved.model} (${resolved.via})"))
379
+ }
607
380
  }
608
- return getCapabilityModelKey()
609
- }
610
-
611
- /** Persist one override patch at a scope, then re-resolve. */
612
- def saveCapabilityOverride(scopeKey: string, patch: CapabilityPatch) {
613
- let settings = loadSettings()
614
- settings.capabilities = mergeCapabilityOverride(
615
- getCapabilitySettings(settings),
616
- scopeKey,
617
- scopeTargetName(scopeKey),
618
- patch,
619
- )
620
- saveSettings(settings)
621
- refreshCapabilities(getCapabilityModelKey(), getCapabilityProvider())
622
- }
623
-
624
- /** Remove one field override at a scope, then re-resolve. */
625
- def removeCapabilityOverride(scopeKey: string, fieldKey: string) {
626
- let settings = loadSettings()
627
- settings.capabilities = removeCapabilityField(
628
- getCapabilitySettings(settings),
629
- scopeKey,
630
- scopeTargetName(scopeKey),
631
- fieldKey,
632
- )
633
- saveSettings(settings)
634
- refreshCapabilities(getCapabilityModelKey(), getCapabilityProvider())
381
+ return true
635
382
  }
636
383
 
637
- /** Ask which scope an override applies to. Returns "" on cancel. */
638
- def pickOverrideScope(): string {
639
- let scopeItems: ChoiceItem[] = []
640
- scopeItems.push({
641
- key: "model",
642
- label: "this model (${getCapabilityModelKey()})"
643
- })
644
- scopeItems.push({
645
- key: "provider",
646
- label: "this provider (${getCapabilityProvider()})"
647
- })
648
- scopeItems.push({
649
- key: "global",
650
- label: "all models"
651
- })
652
- const scopeKey = chooseOption("Scope", "Where should this apply?", scopeItems, allowCancel: true)
653
- if (scopeKey == null) {
654
- return ""
384
+ def modelsSlashCommand(trimmed: string): boolean {
385
+ const filters = parseModelsFilters(trimmed)
386
+ const models = filterHostedModels(listHostedModels(), filters)
387
+ if (models.length == 0) {
388
+ pushMessage("No models match.")
389
+ return true
655
390
  }
656
- return scopeKey
657
- }
658
-
659
- /** Re-apply the resolved cap as the branch default. After a reset the
660
- resolved value falls back to a built-in layer, which is always numeric
661
- for known providers, so live-apply covers removal too. */
662
- def applyLiveMaxTokens() {
663
- const capValue = getCapabilities().maxTokens
664
- if (capValue != null) {
665
- setLlmOptions({ maxTokens: capValue })
391
+ for (model in models) {
392
+ pushMessage(
393
+ " ${model.name} (${model.provider}, $${model.inputCost}/1M in, ${model.contextWindow} ctx)",
394
+ )
666
395
  }
396
+ return true
667
397
  }
668
398
 
669
- /** Positive-integer parse. parseInt and isNaN appear nowhere in the
670
- repo's .agency code, so this uses a digits check instead. Returns -1
671
- for anything that is not all digits. */
672
- def parsePositiveInt(text: string): number {
673
- // Bound to a const because the parser rejects method calls on string
674
- // LITERALS ("0123456789".includes(...) does not parse; a variable does).
675
- const digits = "0123456789"
676
- const trimmed = text.trim()
677
- if (trimmed == "") {
678
- return -1
679
- }
680
- for (ch in trimmed.split("")) {
681
- if (!digits.includes(ch)) {
682
- return -1
683
- }
399
+ def localSlashCommand(): boolean {
400
+ if (!localModelsSupported()) {
401
+ pushMessage(
402
+ color.yellow(
403
+ "Local models need smoltalk-llama-cpp. Install: ! npm i -g smoltalk-llama-cpp",
404
+ ),
405
+ )
406
+ return true
684
407
  }
685
- return Number(trimmed)
686
- }
687
-
688
- /** Interactive edit: field → value → scope → save → live-apply. */
689
- def editCapabilities() {
690
- let fieldItems: ChoiceItem[] = []
691
- for (field in CAPABILITY_FIELDS) {
692
- fieldItems.push({
693
- key: field.key,
694
- label: "${field.label} — ${field.description}"
408
+ let items: ChoiceItem[] = []
409
+ for (modelName in listModelNames()) {
410
+ items.push({
411
+ key: modelName.name,
412
+ label: modelName.name
695
413
  })
696
414
  }
697
- const fieldKey = chooseOption(
698
- "Settings",
699
- "Pick a setting to change (empty to exit).",
700
- fieldItems,
415
+ const choice = chooseOption(
416
+ "Local model",
417
+ "Pick a local model, or type an hf: URI / .gguf path (downloads on first use):",
418
+ items,
419
+ allowFreeText: true,
701
420
  allowCancel: true,
702
421
  )
703
- if (fieldKey == "" || fieldKey == null) {
704
- return
705
- }
706
-
707
- let patch: CapabilityPatch = {}
708
- if (fieldKey == "prompt") {
709
- let promptItems: ChoiceItem[] = []
710
- promptItems.push({
711
- key: "large",
712
- label: "large — full coordinator prompt"
713
- })
714
- promptItems.push({
715
- key: "small",
716
- label: "small — compact prompt for small-context models"
717
- })
718
- const picked = chooseOption("prompt", "System prompt size.", promptItems, allowCancel: true)
719
- if (picked == "" || picked == null) {
720
- return
721
- }
722
- patch = {
723
- prompt: picked
724
- }
725
- }
726
- if (fieldKey == "summarize" || fieldKey == "memory") {
727
- let onOffItems: ChoiceItem[] = []
728
- onOffItems.push({
729
- key: "on",
730
- label: "on"
731
- })
732
- onOffItems.push({
733
- key: "off",
734
- label: "off"
735
- })
736
- const picked = chooseOption(fieldKey, "Enable ${fieldKey}?", onOffItems, allowCancel: true)
737
- if (picked == "" || picked == null) {
738
- return
739
- }
740
- if (fieldKey == "summarize") {
741
- patch = {
742
- summarize: picked == "on"
743
- }
744
- } else {
745
- patch = {
746
- memory: picked == "on"
747
- }
748
- if (picked == "on") {
749
- const status = embeddingKeyStatus(getResolvedSlots())
750
- if (status.state == "key-missing") {
751
- pushMessage(
752
- color.red(
753
- "Cannot enable memory: the embedding slot needs ${status.varName}, which is not set. Export it, or point the embedding slot elsewhere via /model.",
754
- ),
755
- )
756
- return
757
- }
758
- if (status.state == "no-override" && providerLacksEmbeddings(getCapabilityProvider())) {
759
- pushMessage(
760
- color.dim(
761
- "Note: ${getCapabilityProvider()} has no embeddings endpoint, so Tier-2 semantic recall stays off. Point the embedding slot at a provider that has one, e.g. /model embedding=openai/text-embedding-3-small.",
762
- ),
763
- )
764
- }
765
- }
766
- }
767
- }
768
- if (fieldKey == "maxTokens") {
769
- let capItems: ChoiceItem[] = []
770
- capItems.push({
771
- key: "reset",
772
- label: "reset — remove my override, use the default"
773
- })
774
- const picked = chooseOption(
775
- "maxTokens",
776
- "Default output-token cap. Type a number, or pick reset.",
777
- capItems,
778
- allowFreeText: true,
779
- allowCancel: true,
780
- )
781
- if (picked == "" || picked == null) {
782
- return
783
- }
784
- if (picked == "reset") {
785
- const scopeKey = pickOverrideScope()
786
- if (scopeKey == "") {
787
- return
788
- }
789
- removeCapabilityOverride(scopeKey, "maxTokens")
790
- applyLiveMaxTokens()
791
- pushMessage("Override removed.")
792
- showCapabilities()
793
- return
794
- }
795
- const parsed = parsePositiveInt(picked)
796
- if (parsed <= 0) {
797
- pushMessage(color.red("Not a positive number: ${picked}"))
798
- return
799
- }
800
- patch = {
801
- maxTokens: parsed
802
- }
803
- }
804
-
805
- const scopeKey = pickOverrideScope()
806
- if (scopeKey == "") {
807
- return
808
- }
809
- saveCapabilityOverride(scopeKey, patch)
810
-
811
- // Live-apply what can apply now; explain what can't.
812
- if (fieldKey == "memory") {
813
- if (getCapabilities().memory) {
814
- // The ON direction cannot live-apply. enableAgentMemory's
815
- // `with approve` cannot override the outer policy handler that
816
- // startInteractive installed, so the memory-dir fs interrupts
817
- // would surface as policy prompts mid-flow, or get denied and
818
- // half-apply the toggle. The OFF direction stays live below:
819
- // disableMemory raises no fs interrupts and already runs in-REPL
820
- // from reactToSlotChange.
821
- pushMessage(color.dim("Memory turns on at next agent start (enabling mid-session would raise policy prompts)."))
822
- } else {
823
- disableMemory() with approve
824
- }
825
- }
826
- if (fieldKey == "maxTokens") {
827
- applyLiveMaxTokens()
422
+ if (choice == "" || choice == null) {
423
+ return true
828
424
  }
829
- if (fieldKey == "prompt") {
830
- pushMessage(color.dim("Prompt change applies on next agent start (this session's system message is already sent)."))
425
+ const before = getResolvedSlots()
426
+ configureLocalModel(choice)
427
+ configureSearch(true)
428
+ for (change in diffResolved(before, getResolvedSlots())) {
429
+ reactToSlotChange(change)
831
430
  }
832
- pushMessage("Saved.")
833
- showCapabilities()
431
+ pushMessage("Switched to local model: ${choice}")
432
+ return true
834
433
  }
835
434
 
836
435
  // Slash-command + user-message dispatcher invoked by `repl()` for
@@ -842,618 +441,121 @@ def _runTurn(msg: string): boolean {
842
441
  // raw `msg` so the LLM sees the exact text the user typed when
843
442
  // nothing matches.
844
443
  const trimmed = msg.trim()
845
- if (trimmed == "") {
846
- return true
847
- }
848
- if (trimmed == "/exit" || trimmed == "/quit") {
849
- return false
850
- }
851
- if (trimmed == "/clear") {
852
- clearMessages()
853
- return true
854
- }
855
- if (trimmed == "/clear-history") {
856
- clearHistory()
857
- pushMessage("Input history cleared.")
858
- return true
859
- }
860
- if (trimmed == "/help") {
861
- pushMessage(
862
- "Commands: /exit, /clear, /clear-history, /cost, /model, /models, /local, /search, /settings, /mcp, /paste, /help",
863
- )
864
- return true
865
- }
866
- if (trimmed == "/settings") {
867
- showCapabilities()
868
- editCapabilities()
869
- return true
870
- }
871
- if (trimmed == "/mcp") {
872
- if (!isMcpAvailable()) {
873
- pushMessage(color.yellow("MCP is not installed. Run: npm install @agency-lang/mcp"))
874
- return true
875
- }
876
- const merged = mergeMcpServers(getMcpServers(loadSettings()), readProjectMcpConfig(cwd()))
877
- if (keys(merged).length == 0) {
878
- pushMessage(color.dim("No MCP servers configured. Add an mcpServers block to agency.json or settings.json."))
444
+ return match(trimmed) {
445
+ "" => true
446
+ "/exit" => false
447
+ "/quit" => false
448
+ "/clear" => {
449
+ clearMessages()
879
450
  return true
880
451
  }
881
- pushMessage(color.bold("MCP servers:"))
882
- for (name in keys(merged)) {
883
- // mcpServerStatus is populated at session start. A server present in the
884
- // config but absent from the status map was never loaded (e.g. /mcp run
885
- // before startup finished) — show it as unknown rather than connected.
886
- let status = mcpServerStatus[name]
887
- if (status == null) {
888
- status = "not loaded"
889
- }
890
- pushMessage(" ${name} — ${status}")
891
- }
892
- pushMessage(color.dim("Loaded ${mcpTools.length} MCP tool(s) this session."))
893
- return true
894
- }
895
- if (trimmed == "/search") {
896
- const opts = availableBackendItems()
897
- const choice = chooseOption(
898
- "Web search backend",
899
- "Pick how this agent searches the web. Current: ${getSearchBackend()}",
900
- opts,
901
- false,
902
- true,
903
- )
904
- if (choice != "" && choice != null && choice != undefined) {
905
- setSearchBackend(choice)
906
- pushMessage("Web search backend set to: ${choice}")
907
- }
908
- return true
909
- }
910
- if (trimmed == "/cost") {
911
- // Derive BOTH the header total and the per-model breakdown from the
912
- // same process-wide accumulator (getModelCosts), so the `Cost:` /
913
- // `Tokens:` lines and the rows below them can never visibly disagree.
914
- // (getCost()/getTokens() read the per-branch accumulator, a different
915
- // scope — adjacent in the output, they could in principle diverge.)
916
- // Rows are sorted by cost descending by getModelCosts(); a subagent
917
- // on a pricier model (e.g. the oracle on opus-4.8) shows up here.
918
- const byModel = getModelCosts()
919
- let totalCost = 0.0
920
- let totalTokens = 0
921
- for (m in byModel) {
922
- totalCost = totalCost + m.cost
923
- totalTokens = totalTokens + m.inputTokens + m.outputTokens
924
- }
925
- pushMessage("Cost: $${totalCost.toFixed(4)}")
926
- pushMessage("Tokens: ${totalTokens}")
927
- if (byModel.length > 0) {
928
- pushMessage("")
929
- pushMessage("By model:")
930
- for (m in byModel) {
931
- pushMessage(
932
- " ${m.model} ↑${m.inputTokens} ↓${m.outputTokens} $${m.cost.toFixed(4)}",
933
- )
934
- }
935
- }
936
- return true
937
- }
938
- if (trimmed == "/model" || trimmed.startsWith("/model ")) {
939
- let spec = ""
940
- if (trimmed != "/model") {
941
- spec = trimmed.substring(7).trim()
942
- }
943
- if (spec == "") {
944
- pushMessage("Current models:")
945
- const current = getResolvedSlots()
946
- for (slot in Object.keys(current)) {
947
- const resolved = current[slot]
948
- if (resolved != null) {
949
- pushMessage(" ${slot}: ${resolved.model} (${resolved.via})")
950
- }
951
- }
952
- const items = modelChoiceItems({})
953
- const choice = chooseOption(
954
- "Model",
955
- "Pick a model, or type one (or slot=model, e.g. reasoning=claude-opus-4-8). Empty to cancel.",
956
- items,
957
- allowFreeText: true,
958
- allowCancel: true,
959
- )
960
- if (choice == "" || choice == null) {
961
- return true
962
- }
963
- spec = choice
964
- }
965
- const ctx: Ctx = {
966
- provider: currentProvider(),
967
- price: "standard"
968
- }
969
- const out = switchModel(_session, spec, ctx, getResolvedSlots())
970
- _session = out.session
971
- for (change in out.changes) {
972
- reactToSlotChange(change)
973
- }
974
- pushMessage("Model set:")
975
- for (slot in Object.keys(out.resolved)) {
976
- const resolved = out.resolved[slot]
977
- if (resolved != null) {
978
- pushMessage(color.dim(" ${slot}: ${resolved.model} (${resolved.via})"))
979
- }
980
- }
981
- return true
982
- }
983
- if (trimmed == "/models" || trimmed.startsWith("/models ")) {
984
- const filters = parseModelsFilters(trimmed)
985
- const models = filterHostedModels(listHostedModels(), filters)
986
- if (models.length == 0) {
987
- pushMessage("No models match.")
452
+ "/clear-history" => {
453
+ clearHistory()
454
+ pushMessage("Input history cleared.")
988
455
  return true
989
456
  }
990
- for (model in models) {
457
+ "/help" => {
991
458
  pushMessage(
992
- " ${model.name} (${model.provider}, $${model.inputCost}/1M in, ${model.contextWindow} ctx)",
993
- )
994
- }
995
- return true
996
- }
997
- if (trimmed == "/local") {
998
- if (!localModelsSupported()) {
999
- pushMessage(
1000
- color.yellow(
1001
- "Local models need smoltalk-llama-cpp. Install: ! npm i -g smoltalk-llama-cpp",
1002
- ),
459
+ "Commands: /exit, /clear, /clear-history, /cost, /model, /models, /local, /search, /settings, /mcp, /paste, /help",
1003
460
  )
1004
461
  return true
1005
462
  }
1006
- let items: ChoiceItem[] = []
1007
- for (modelName in listModelNames()) {
1008
- items.push({
1009
- key: modelName.name,
1010
- label: modelName.name
1011
- })
1012
- }
1013
- const choice = chooseOption(
1014
- "Local model",
1015
- "Pick a local model, or type an hf: URI / .gguf path (downloads on first use):",
1016
- items,
1017
- allowFreeText: true,
1018
- allowCancel: true,
1019
- )
1020
- if (choice == "" || choice == null) {
463
+ "/settings" => {
464
+ showCapabilities()
465
+ editCapabilities()
1021
466
  return true
1022
467
  }
1023
- const before = getResolvedSlots()
1024
- configureLocalModel(choice)
1025
- configureSearch(true)
1026
- for (change in diffResolved(before, getResolvedSlots())) {
1027
- reactToSlotChange(change)
1028
- }
1029
- pushMessage("Switched to local model: ${choice}")
1030
- return true
1031
- }
1032
-
1033
- // Hand the message to the agent. `agentReply` does the slash-command
1034
- // expansion and the actual agent work; the REPL just renders the reply.
1035
- // Wrap in `try` so a terminal LLM failure (e.g. the connection dropped and
1036
- // retries were exhausted) renders a friendly heads-up instead of a raw
1037
- // Failure object. Aborts/interrupts are NOT caught here — `try` re-throws
1038
- // them so an Esc-cancel still reaches the policy handler.
1039
- const result = try agentReply(msg)
1040
- if (result is success(reply)) {
1041
- if (reply != "" && reply != null && reply != undefined) {
1042
- pushMessage(highlight("${reply}\n", language: "markdown"))
1043
- } else {
1044
- pushMessage(color.red("No reply generated."))
468
+ "/mcp" => mcpSlashCommand()
469
+ "/search" => searchSlashCommand()
470
+ "/cost" => costSlashCommand()
471
+ "/model" => modelSlashCommand()
472
+ "/models" => modelsSlashCommand(trimmed)
473
+ "/local" => localSlashCommand()
474
+ _ => {
475
+ if (trimmed.startsWith("/model ")) {
476
+ return modelSlashCommand()
477
+ }
478
+ if (trimmed.startsWith("/models ")) {
479
+ return modelsSlashCommand(trimmed)
480
+ }
481
+ return renderUserTurn(trimmed, msg)
1045
482
  }
1046
- } else if (result is failure(f)) {
1047
- pushMessage(color.red(formatTurnFailure("${f.error}")))
1048
483
  }
1049
- return true
1050
484
  }
1051
485
 
1052
486
  let first = true
1053
487
 
1054
- // Valid `--agent` targets. Empty / "main" routes through the coordinator;
1055
- // the rest route the starting prompt's first turn directly to that
1056
- // subagent. Used to validate the flag and to dispatch in `agentReplyVia`.
488
+ // Valid `--agent` targets. Empty / "main" routes through the coordinator.
1057
489
  static const START_AGENTS = ["main", "code", "research", "oracle", "explorer", "review"]
1058
-
1059
- static const MAIN_PROMPT_LARGE = """
1060
- You are the top-level coordinator of an Agency-language assistant. You
1061
- receive every user message and decide how to respond.
1062
-
1063
- You have five subagent tools (each running in its own isolated
1064
- context) and one direct tool:
1065
-
1066
- - `codeAgent(userMsg)` — anything that touches code or the filesystem:
1067
- reading, writing, editing, typechecking, running shell commands,
1068
- and answering Agency syntax / CLI questions. Use this for any task
1069
- that involves inspecting or modifying source code.
1070
- - `researchAgent(userMsg)` — web search, URL fetches, Wikipedia,
1071
- external API lookups, summarizing external content.
1072
- - `reviewAgent(userMsg)` — reviews Agency code for syntax and type
1073
- errors. Call this after `codeAgent` produces non-trivial new or
1074
- modified Agency code, passing the code to be reviewed.
1075
- - `oracleAgent(userMsg)` — a read-only senior reviewer on a stronger
1076
- reasoning model. Read the **Oracle** section below — you are
1077
- expected to use this tool **frequently**.
1078
- - `explorerAgent(userMsg)` — a read-only researcher that produces
1079
- broad, synthesizing answers about the codebase or bundled docs.
1080
- Read the **Explorer** section below.
1081
- - `generateImageFile(prompt, path, size, images)` — generate an image
1082
- from a text prompt (or modify existing images by passing their paths
1083
- in `images`) and save it to `path`. Call it directly whenever the
1084
- user asks you to create, draw, edit, or restyle an image — do NOT
1085
- route image generation to `codeAgent`.
1086
-
1087
- **Answer directly (no tool call) when** the message is conversational,
1088
- a clarifying question, or something you can answer from context alone.
1089
-
1090
- **Delegate when** the message clearly needs one of the subagents'
1091
- capabilities. Pick one based on what the message actually needs —
1092
- don't pre-emptively call multiple subagents.
1093
-
1094
- ## Picking between oracle, explorer, and code agent
1095
-
1096
- These three overlap on "reads the codebase," but they're for
1097
- different jobs. Pick by **the shape of the answer the user wants**:
1098
-
1099
- - **oracle** → sharp verdict on a specific plan, diff, or bug.
1100
- Output: short, decisive ("this plan won't work because X").
1101
- - **explorer** → broad synthesis from reading many files.
1102
- Output: structured overview ("Agency's five main features
1103
- are..."). Use for "summarize", "tour", "what are the main",
1104
- "how does X work across...".
1105
- - **codeAgent** → targeted action: edit, run, typecheck, or
1106
- answer a focused Agency-syntax question. Output: the edit
1107
- itself, or a concise factual reply.
1108
-
1109
- Default: if the user asks a broad/synthesizing question,
1110
- `explorerAgent` beats `codeAgent` every time. The code agent is
1111
- optimized for terse action and will under-read on broad asks.
1112
-
1113
- Subagents return summary text. Surface that result to the user (in
1114
- your own words if you're combining multiple results), formatted as
1115
- Markdown.
1116
-
1117
- ## Oracle
1118
-
1119
- The oracle is the most powerful tool you have. Use it **FREQUENTLY**.
1120
- It is a read-only senior reviewer running on a stronger reasoning
1121
- model than you. It can read the codebase and the Agency docs but
1122
- cannot write, edit, or run anything. Its job is to **think hard so
1123
- you don't have to guess**.
1124
-
1125
- Call the oracle:
1126
-
1127
- - **Before** dispatching `codeAgent` for any non-trivial task — ask
1128
- the oracle to sanity-check the plan, find existing code that
1129
- already solves the problem, or suggest a simpler approach.
1130
- - **After** `codeAgent` produces a non-trivial diff — ask the oracle
1131
- to review the work for correctness, missed edge cases, and
1132
- better alternatives.
1133
- - When `codeAgent` reports it's stuck on a bug after one or two
1134
- attempts — ask the oracle what's actually going on.
1135
- - When the user proposes a plan or approach — ask the oracle
1136
- whether the plan is sound, **before** you act on it. If the
1137
- oracle finds a flaw, surface that to the user *before* taking
1138
- any action.
1139
- - Whenever you're tempted to guess about the codebase ("I think
1140
- there's probably already a helper for X") — ask the oracle to
1141
- look.
1142
-
1143
- **Tell the user when you're consulting the oracle.** Say something
1144
- like "Let me ask the oracle to review this plan before we start"
1145
- or "I'll have the oracle look for an existing implementation." The
1146
- visibility is half the value — the user benefits from knowing a
1147
- second opinion is being sought.
1148
-
1149
- Pass the oracle a self-contained question with full context. It
1150
- does not see your conversation. Include the user's request, the
1151
- plan or diff under review, relevant file paths, and the specific
1152
- question you want answered.
1153
-
1154
- Default bias: when in doubt, **consult the oracle**. The cost of
1155
- asking is small; the cost of executing a flawed plan is large.
1156
-
1157
- ## Explorer
1158
-
1159
- The explorer is your go-to for **broad, synthesizing questions**
1160
- about the codebase or the bundled Agency docs. It is read-only and
1161
- runs on a stronger reasoning model with extended thinking. Its job
1162
- is **coverage** — it reads widely and returns a structured synthesis.
1163
-
1164
- Call the explorer when the user asks:
1165
-
1166
- - "Summarize the Agency docs" / "What are the main features?"
1167
- - "Give me a tour of `lib/X/`" / "What's in this module?"
1168
- - "How does X work across the codebase?"
1169
- - "Walk me through the compilation pipeline"
1170
- - Anything where a good answer requires reading 5+ files and
1171
- organizing the findings by theme.
1172
-
1173
- Do NOT use the explorer for:
1174
- - Specific factual lookups ("what does function X return?") — use
1175
- `codeAgent`.
1176
- - Plan critique or bug diagnosis — use `oracleAgent`.
1177
- - External / web research — use `researchAgent`.
1178
-
1179
- **Tell the user when you're consulting the explorer.** "Let me have
1180
- the explorer go through the docs and put together an overview." Like
1181
- with the oracle, the visibility is part of the value — the user
1182
- benefits from knowing breadth is being applied.
1183
-
1184
- Pass the explorer a self-contained question with explicit scope
1185
- ("all of `docs/site/guide/`", "the `lib/parsers/` module"). It does
1186
- not see your conversation. Be clear about the level of detail
1187
- expected.
1188
-
1189
- ## Style
1190
-
1191
- Never start a response by calling the user's question or idea good,
1192
- great, fascinating, profound, excellent, or perfect. Skip flattery
1193
- and respond directly. Don't pad replies with "happy to help",
1194
- "certainly", or trailing summaries the user can read in the diff.
1195
-
1196
- **Use ASCII diagrams when they clarify.** For control flow, state
1197
- machines, pipelines, module relationships, or any "how do the parts
1198
- fit together" answer, draw a small ASCII diagram in a fenced
1199
- ```text block. Boxes, arrows, and trees beat paragraphs for
1200
- structural explanations:
1201
-
1202
- ```text
1203
- parse → SymbolTable.build → preprocess → TypeScriptBuilder → printTs
1204
- ```
1205
-
1206
- Keep diagrams small. Skip them where prose or code is clearer —
1207
- diagrams earn their space by showing **relationships** or **flow**.
1208
-
1209
- ## Be proactive
1210
-
1211
- When the user asks you to look at, debug, or change a file or some code,
1212
- **delegate to `codeAgent` to do it** — don't ask the user to paste a file
1213
- or describe code a subagent could read. The code agent has `read`, `glob`,
1214
- and `ls` and resolves relative paths against the user's working directory
1215
- automatically, so a bare filename like `foo.agency` is enough. Only ask
1216
- the user for information you genuinely cannot obtain through a subagent.
1217
-
1218
- ## Answer before action
1219
-
1220
- When the user asks a question, asks for an opinion, or asks how to
1221
- plan or approach something, **answer the question first**. Don't
1222
- jump straight into delegating to a subagent or making tool calls
1223
- unless the user has clearly asked for an action ("do X", "fix Y",
1224
- "build Z"). If the user is exploring or thinking out loud, think
1225
- with them — don't sprint to implementation.
1226
- """
1227
-
1228
- // Compact coordinator prompt for small-context models, ~350 tokens
1229
- // instead of ~3,400. Selected when the capability profile says
1230
- // prompt: "small". An 8K-context model loses 40% of its window to the
1231
- // large prompt before the user types anything. The phrase "compact
1232
- // coordinator" is a deliberate marker that verification greps for in
1233
- // statelogs.
1234
- static const MAIN_PROMPT_SMALL = """
1235
- You are the compact coordinator of an Agency-language assistant. Decide
1236
- how to answer each user message.
1237
-
1238
- Tools (each runs in its own context; pass a self-contained message):
1239
- - `codeAgent(userMsg)` — anything touching code or files: read, write,
1240
- edit, run, typecheck. Also Agency syntax and CLI questions.
1241
- - `researchAgent(userMsg)` — web search, URL fetches, external facts.
1242
- - `reviewAgent(userMsg)` — check non-trivial new Agency code for
1243
- syntax and type errors; pass the code to review.
1244
- - `oracleAgent(userMsg)` — deep reasoning on a hard question; include
1245
- all needed context in the message.
1246
- - `explorerAgent(userMsg)` — broad read-only codebase/docs questions.
1247
- - `generateImageFile(prompt, path, size, images)` — create or edit an
1248
- image; do not route image work to codeAgent.
1249
-
1250
- Routing rules:
1251
- - Simple chat, greetings, quick factual answers: reply directly, no
1252
- tools.
1253
- - Anything code- or file-related: codeAgent. Current/external info:
1254
- researchAgent. Broad "summarize/tour/how does X work" questions:
1255
- explorerAgent.
1256
- - Surface tool results to the user concisely; do not re-run a tool the
1257
- user did not ask to re-run.
1258
-
1259
- Style: plain, direct answers in Markdown. No preamble. Keep replies
1260
- short unless the task demands detail.
1261
- """
1262
-
1263
- // Exported for tests: agent-cwd resolution of edit inputs is a spec
1264
- // invariant with no other observable (the deterministic image client
1265
- // ignores its images argument). generateImage accepts path / URL /
1266
- // data-URI strings, so only local paths get agent-cwd resolution —
1267
- // running a URL through applyAgentCwd would mangle it into
1268
- // "<agentCwd>/https:/…".
1269
- export def resolveEditInputs(images: string[]): string[] {
1270
- return map(images) as inputPath {
1271
- if (inputPath.startsWith("http://") || inputPath.startsWith("https://") || inputPath.startsWith("data:")) {
1272
- return inputPath
1273
- }
1274
- return applyAgentCwd(inputPath)
1275
- }
1276
- }
1277
-
1278
- // Direct image generation for the coordinator. Saves to disk and
1279
- // returns the path — never base64, which would flood the LLM context.
1280
- // The write rides the std::writeBinary interrupt gate (writes are not
1281
- // auto-approved by policy) and generation cost accrues to the agent's
1282
- // cost tracking like any llm() call.
1283
- export def generateImageFile(
1284
- prompt: string,
1285
- path: string,
1286
- size: string = "",
1287
- images: string[] = [],
1288
- ): string {
1289
- """
1290
- Generate an image from a text prompt and save it to `path`. To MODIFY
1291
- existing images (edit / variation), pass their paths in `images`.
1292
- Returns the saved path on success. Use this when the user asks to
1293
- create, draw, edit, or restyle an image.
1294
-
1295
- @param prompt - What to generate, or how to modify the input images.
1296
- @param path - Where to save the resulting image (e.g. "diagram.png").
1297
- @param size - Optional size like "1024x1024".
1298
- @param images - Optional input image paths to edit / vary.
1299
- """
1300
- // LLM-supplied paths resolve against the agent cwd, like every other
1301
- // agent file tool.
1302
- const inputs = resolveEditInputs(images)
1303
- const generated = generateImage(prompt, size: size, images: inputs)
1304
- if (isFailure(generated)) {
1305
- return "Image generation failed: ${generated.error}"
1306
- }
1307
- // writeBinary rejects an absolute filename when dir is set, so split
1308
- // path into (dir, name); useAgentCwd resolves a relative dir against
1309
- // the agent cwd (absolute dirs pass through untouched).
1310
- const written = writeBinary(
1311
- basename(path),
1312
- generated.value.base64,
1313
- dirname(path),
1314
- useAgentCwd: true,
1315
- )
1316
- if (isFailure(written)) {
1317
- return "Generated the image, but saving to ${path} failed: ${written.error}"
1318
- }
1319
- // Show the model what it made (generate -> view -> refine). The loop
1320
- // inlines the bytes when it builds the injected user message; the
1321
- // path resolves the same way the write did.
1322
- attachToReply(image(applyAgentCwd(path)))
1323
- return "Saved image to ${path}"
1324
- }
490
+ static const MAIN_PROMPT_LARGE = read("prompts/main-large.md") with approve
491
+ static const MAIN_PROMPT_SMALL = read("prompts/main-small.md") with approve
1325
492
 
1326
493
  static const mainAgentTools = [
1327
- researchAgent.partial(allowHandoff: false),
1328
- codeAgent.partial(allowHandoff: false),
1329
- reviewAgent.partial(allowHandoff: false),
1330
- oracleAgent.partial(allowHandoff: false),
1331
- explorerAgent.partial(allowHandoff: false),
494
+ researchAgent,
495
+ codeAgent,
496
+ reviewAgent,
497
+ oracleAgent,
498
+ explorerAgent,
1332
499
  generateImageFile,
1333
500
  ]
1334
501
 
1335
- // MCP tools are appended to the coordinator's tools. ORDERING CONTRACT:
1336
- // maybeLoadMcp (session start, in main()) must run before mainAgent's first
1337
- // turn, or MCP tools silently vanish.
1338
- let mcpTools: any[] = []
1339
- // Per-server load outcome ("connected" / "unavailable"), for /mcp.
1340
- let mcpServerStatus: Record<string, string> = {}
1341
-
1342
- export def setMcpTools(tools: any[]) {
1343
- mcpTools = tools
1344
- }
1345
-
1346
- def setMcpServerStatus(status: Record<string, string>) {
1347
- mcpServerStatus = status
1348
- }
1349
-
1350
- // MCP OAuth notification. The @agency-lang/mcp package opens the browser itself
1351
- // (macOS-only today); we only print the URL so the user can complete auth.
1352
- def onMcpOAuth(data: any) {
1353
- print(color.yellow("MCP authorization needed — opening browser. If it does not open, visit:\n${data.authUrl}"))
1354
- }
1355
-
1356
- /** Load MCP tools from the project agency.json merged with the global settings
1357
- and hand them to the coordinator. No-op when @agency-lang/mcp is not installed
1358
- or no servers are configured. loadMcpTools warns-and-skips internally on any
1359
- per-server failure, so it always returns an array. */
1360
- def maybeLoadMcp(settings: Settings) {
1361
- if (!isMcpAvailable()) {
1362
- return
1363
- }
1364
- const merged = mergeMcpServers(getMcpServers(settings), readProjectMcpConfig(cwd()))
1365
- if (keys(merged).length == 0) {
1366
- return
1367
- }
1368
- // Always returns a result (per-server failures warn-and-skip internally), so
1369
- // there is no Failure branch. The status map drives /mcp.
1370
- const loaded = loadMcpToolsWithStatus(merged, onMcpOAuth)
1371
- setMcpTools(loaded.tools)
1372
- setMcpServerStatus(loaded.status)
1373
- }
1374
-
1375
502
  /** Enable memory when the capability profile allows it. When the user
1376
503
  pointed the embedding slot at a provider, embeddings run there. A
1377
504
  missing API key degrades to the default embedding behavior with one
1378
- notice. It never blocks startup. */
1379
- // Whether startup actually enabled memory WITH the embedding override.
1380
- // The pause guard in reactToSlotChange consults this instead of
1381
- // re-deriving from the slots: a keyless override took the fallback path,
1382
- // so memory runs on DERIVED embeddings and a main-provider change still
1383
- // shifts the embedding space. Exported mutator so tests can drive the
1384
- // guard directly.
1385
- let _memoryEmbeddingsPinned = false
1386
-
1387
- export def markMemoryEmbeddingsPinned(pinned: boolean) {
1388
- _memoryEmbeddingsPinned = pinned
1389
- }
1390
-
505
+ notice. It never blocks startup. The `_memoryEmbeddingsPinned` guard that
506
+ `reactToSlotChange` consults lives in `./lib/session.agency`; we set it
507
+ here via `markMemoryEmbeddingsPinned`. */
1391
508
  def maybeEnableMemory() {
1392
509
  const status = embeddingKeyStatus(getResolvedSlots())
1393
510
  const plan = memoryEnablePlan(getCapabilities().memory, status)
1394
511
  markMemoryEmbeddingsPinned(plan == "enable-override")
1395
- if (plan == "skip") {
1396
- return
1397
- }
1398
- if (plan == "enable-override") {
1399
- enableAgentMemory(status.override)
1400
- return
1401
- }
1402
- if (plan == "enable-fallback") {
1403
- print(
1404
- color.dim(
1405
- "Memory: the embedding slot needs ${status.varName}, which is not set. Falling back to the default embedding behavior.",
1406
- ),
1407
- )
512
+ return match(plan) {
513
+ "skip" => null
514
+ "enable-override" => {
515
+ enableAgentMemory(status.override)
516
+ return null
517
+ }
518
+ "enable-fallback" => {
519
+ print(
520
+ color.dim(
521
+ "Memory: the embedding slot needs ${status.varName}, which is not set. Falling back to the default embedding behavior.",
522
+ ),
523
+ )
524
+ enableAgentMemory()
525
+ return null
526
+ }
527
+ _ => {
528
+ enableAgentMemory()
529
+ return null
530
+ }
1408
531
  }
1409
- enableAgentMemory()
1410
532
  }
1411
533
 
1412
534
  def mainAgent(prompt: string | (string | Attachment)[]): string {
1413
- const doSummarize = getCapabilities().summarize
1414
- thread(label: "main", summarize: doSummarize, session: "main") {
535
+ // const doSummarize = getCapabilities().summarize
536
+ thread(label: "main", session: "main") {
1415
537
  setMemoryId("main")
1416
538
  if (first) {
1417
- // `_context` carries the per-run grounding (date, cwd, AGENTS.md),
1418
- // set by `setupSession`. Appending it to the system prompt is what
1419
- // actually gets that context to the LLM.
1420
- let sysPrompt = MAIN_PROMPT_LARGE
1421
- if (getCapabilities().prompt == "small") {
1422
- sysPrompt = MAIN_PROMPT_SMALL
539
+ const promptSize = getCapabilities().prompt
540
+ const sysPrompt = match(promptSize) {
541
+ "small" => MAIN_PROMPT_SMALL catch ""
542
+ _ => MAIN_PROMPT_LARGE catch ""
1423
543
  }
1424
544
  systemMessage("${sysPrompt}${_context}")
1425
545
  first = false
1426
546
  }
1427
- const result = llm(prompt, {
547
+ const result = llm(
548
+ prompt,
549
+ {
1428
550
  memory: true,
1429
- tools: [...mainAgentTools, ...mcpTools]
1430
- })
551
+ tools: [...mainAgentTools, ...getMcpTools()]
552
+ },
553
+ )
1431
554
  }
1432
555
  return result
1433
556
  }
1434
557
 
1435
- // Dispatch one turn. `target` empty (or "main") runs the coordinator
1436
- // `mainAgent`, which routes as usual; any subagent name routes the turn
1437
- // directly to that subagent. All subagents share `(userMsg, allowHandoff)`
1438
- // and the seed turn never hands off (allowHandoff: false). Used by both
1439
- // the one-shot path and the interactive seed turn.
1440
- export def agentReplyVia(target: string, userMsg: string): string {
1441
- const expanded = expandSlash(userMsg, projectCommands)
1442
- if (target == "code") {
1443
- return codeAgent(expanded, false)
1444
- }
1445
- if (target == "research") {
1446
- return researchAgent(expanded, false)
1447
- }
1448
- if (target == "oracle") {
1449
- return oracleAgent(expanded, false)
1450
- }
1451
- if (target == "explorer") {
1452
- return explorerAgent(expanded, false)
1453
- }
1454
- if (target == "review") {
1455
- return reviewAgent(expanded, false)
1456
- }
558
+ def checkForAttachments(expanded: string): Attachment[] {
1457
559
  // Auto-attach image/PDF files the user referenced (drag-drop or typed
1458
560
  // path), filtered to what the resolved model accepts. Never silent:
1459
561
  // every attach and every skip prints a visible line.
@@ -1464,11 +566,7 @@ export def agentReplyVia(target: string, userMsg: string): string {
1464
566
  )
1465
567
  }
1466
568
  if (detected.attached.length == 0) {
1467
- // Deliberate special case: a text-only turn stays a bare string. A
1468
- // one-element array is API-equivalent, but the stored thread message
1469
- // shape (string vs parts array) — and with it every serialized
1470
- // session and message fixture — would change for EVERY turn.
1471
- return mainAgent(expanded)
569
+ return []
1472
570
  }
1473
571
  for (item in detected.attached) {
1474
572
  pushMessage(color.dim("📎 attached ${item.label}"))
@@ -1476,174 +574,66 @@ export def agentReplyVia(target: string, userMsg: string): string {
1476
574
  const attachments = map(detected.attached) as item {
1477
575
  return item.attachment
1478
576
  }
1479
- const parts: (string | Attachment)[] = [expanded, ...attachments]
1480
- return mainAgent(parts)
577
+ return attachments
1481
578
  }
1482
579
 
1483
- // The agent's core turn, decoupled from the terminal: expand any project
1484
- // slash command, then run the prompt through `mainAgent` and return the
1485
- // reply. The REPL (`main` / `_runTurn`) owns input, output, and built-in
1486
- // commands; everything the *agent* does for a user message lives here, so
1487
- // it can be driven and tested without a terminal or user input. See
1488
- // `tests/agentTurn.agency`.
1489
- export def agentReply(userMsg: string): string {
1490
- return agentReplyVia("", userMsg)
580
+ type AgentTarget =
581
+ | "code"
582
+ | "explorer"
583
+ | "main"
584
+ | "oracle"
585
+ | "research"
586
+ | "review"
587
+ | "coordinator"
588
+
589
+ export def dispatchAgent(agentName: AgentTarget, userMsg: string): string {
590
+ const expanded = expandSlash(userMsg, projectCommands)
591
+ return match(agentName) {
592
+ "code" => codeAgent(expanded)
593
+ "research" => researchAgent(expanded)
594
+ "oracle" => oracleAgent(expanded)
595
+ "explorer" => explorerAgent(expanded)
596
+ "review" => reviewAgent(expanded)
597
+ _ => {
598
+ const attachments = checkForAttachments(expanded)
599
+ return mainAgent([expanded, ...attachments])
600
+ }
601
+ }
1491
602
  }
1492
603
 
1493
- def roundedCost(): string {
1494
- return "$${getCost().toFixed(4)}"
604
+ def renderUserTurn(trimmed: string, msg: string): boolean {
605
+ const result = try dispatchAgent("coordinator", msg)
606
+ renderAgentResponse(result)
607
+ return true
1495
608
  }
1496
609
 
1497
610
  def _buildStatus(): { left: string; right: string; context: string } {
611
+ const roundedCost = "$${getCost().toFixed(4)}"
1498
612
  return {
1499
613
  left: "",
1500
- right: roundedCost(),
614
+ right: roundedCost,
1501
615
  context: ""
1502
616
  }
1503
617
  }
1504
618
 
1505
- def sample(arr: any[]): any {
1506
- return arr[Math.floor(Math.random() * arr.length)]
1507
- }
1508
-
1509
- def printHeader() {
1510
- const fig = sample(figs)
1511
- const data = box(
1512
- title: "Agency",
1513
- padding: 1,
1514
- borderColor: "cyan",
1515
- titleColor: "cyan",
1516
- width: "full",
1517
- ) as b {
1518
- b.row(gap: 1) as r {
1519
- r.column(width: "66%") as left {
1520
- left.text("Welcome to the Agency Agent!", bold: true)
1521
- left.text("Ask me to write code, look up docs, or just chat.")
1522
- left.text("All costs are estimates. Actual costs may be higher.", dim: true)
1523
- left.hline()
1524
- left.text("Getting Started", bold: true, fgColor: "cyan")
1525
- left.text("/help for commands · /exit to quit", dim: true)
1526
- }
1527
- r.vline()
1528
- r.column(width: "30%", align: "center") as right {
1529
- right.raw(fig, align: "center")
1530
- }
1531
- }
1532
- }
1533
- print(render(data, color: true))
1534
- }
1535
-
1536
- // Print `--policy` usage and list the built-in policies (name + one-line
1537
- // description). Shown when `--policy` is passed with no value.
1538
- def printPolicyHelp() {
1539
- print(color.bold("Usage: agency agent --policy <name | path>"))
1540
- print(
1541
- "Select the approval policy for this run: a built-in name below, or a path to a policy JSON file.",
1542
- )
1543
- print("")
1544
- print(color.bold("Built-in policies:"))
1545
- for (p in BUILTIN_POLICIES) {
1546
- print(" ${color.cyan(p.name)}")
1547
- print(" ${p.description}")
1548
- }
1549
- }
1550
-
1551
- def givePolicyChoice() {
1552
- const title = "Welcome to the agency agent. Please pick a policy to start."
1553
- const body = "Don't worry, you can change this later. The policy just controls how the agent asks for your approval when it wants to do something. You can also create your own custom policy file at ${POLICY_PATH} and the agent will use it automatically."
1554
- let items: ChoiceItem[] = [
1555
- {
1556
- key: "minimal",
1557
- label: "minimal default policy, you do most approvals manually"
1558
- },
1559
- {
1560
- key: "recommended",
1561
- label: "recommended default policy, allow reading files and browsing the web, no writes"
1562
- },
1563
- ]
1564
-
1565
- const answer = chooseOption(title, body, items, allowFreeText: true)
1566
- return match(answer) {
1567
- "minimal" => minimalAutoApprovePolicy
1568
- "recommended" => recommendedAutoApprovePolicy
1569
- }
1570
- }
1571
-
1572
619
  // Shared session setup for both interactive and one-shot modes. Builds
1573
- // the per-run grounding context, loads (or initializes) the policy, and
1574
- // returns the installed CLI policy handler. `interactive` gates the
1575
- // first-run policy-choice prompt: one-shot has no user to answer it, so
1576
- // it falls back to the recommended default (reads/web auto-approved;
1577
- // writes still require approval, which a non-interactive run can't
1578
- // grant — those tasks block, which is the safe behavior).
620
+ // the grounding context, loads (or initializes) the policy, and
621
+ // returns the installed CLI policy handler.
1579
622
  def setupSession(interactive: boolean): any {
1580
- // Point every path-taking tool at the user's working directory so the
1581
- // agent's relative file/shell commands resolve against where the user
1582
- // launched it. The user (via the agent) can change this later with
1583
- // setAgentCwd.
1584
623
  setAgentCwd(cwd())
1585
624
 
1586
625
  // Grounding: the LLM shouldn't have to ask where it is or what day it
1587
- // is. Project context: inline an AGENTS.md from the workspace root if
1588
- // present so the LLM follows the project's conventions.
626
+ // is. Also load `AGENTS.md` if present.
1589
627
  const projectContext = loadAgentsMd(cwd()) with approve
1590
- _context = "\n\nCurrent date: ${today()}\nCurrent working directory: ${cwd()}${projectContext}"
628
+ _context = """
1591
629
 
1592
- // Which file the handler persists against, and an optional in-memory
1593
- // policy to start from. Normally the user's saved policy loaded from
1594
- // POLICY_PATH; a `--policy` override redirects this (see below) so the
1595
- // saved policy is never read or written.
1596
- let policyFile = POLICY_PATH
1597
- let sessionPolicy: any = null
630
+ Current date: ${today()}
631
+ Current working directory: ${cwd()}
632
+ ${projectContext}
633
+ """
634
+
635
+ const { policyFile, sessionPolicy } = getPolicyForAgent(_policyArg, interactive)
1598
636
 
1599
- if (_policyArg != "") {
1600
- // `--policy` override: a built-in name or a path to a policy file.
1601
- // The cwd-scoped built-ins pin the launch directory, so we resolve
1602
- // them against `cwd()` (what `setAgentCwd` just stored, and what the
1603
- // write/git/bash interrupts resolve their default paths back to).
1604
- const builtin = builtinPolicy(_policyArg, cwd())
1605
- if (builtin != null) {
1606
- // Hand the policy to the handler in memory (no disk write): a
1607
- // one-off, cwd-scoped override must not clobber the saved policy
1608
- // or leak into a later run from a different directory. Any new
1609
- // interactive "always" decision persists to the session file.
1610
- policyFile = SESSION_POLICY_PATH
1611
- sessionPolicy = builtin
1612
- } else {
1613
- // Not a built-in name — treat it as a policy file path and use it
1614
- // directly (the handler loads/persists it). Validate up front so a
1615
- // typo fails fast instead of silently starting from empty.
1616
- const loaded = parsePolicyFile(_policyArg)
1617
- if (loaded is failure(f)) {
1618
- print(
1619
- color.red(
1620
- "--policy '${_policyArg}' is neither a built-in policy (${builtinPolicyNames().join(", ")}) nor a readable policy file (${f.status}).",
1621
- ),
1622
- )
1623
- process.exit(1)
1624
- }
1625
- policyFile = _policyArg
1626
- }
1627
- } else {
1628
- const policy = parsePolicyFile(POLICY_PATH)
1629
- if (policy is failure(f)) {
1630
- if (f.status == "doesnt-exist") {
1631
- if (interactive) {
1632
- print(color.yellow("No existing policy found at ${POLICY_PATH}."))
1633
- setPolicy(POLICY_PATH, givePolicyChoice())
1634
- } else {
1635
- setPolicy(POLICY_PATH, recommendedAutoApprovePolicy)
1636
- }
1637
- } else {
1638
- print(color.red("Failed to load policy: ${JSON.stringify(f.error)}"))
1639
- process.exit(1)
1640
- }
1641
- } else {
1642
- setPolicy(POLICY_PATH, policy.value)
1643
- }
1644
- }
1645
- // Bind the handler to a local var so `with handler` parses (the
1646
- // `with` clause only accepts an identifier, not a call expression).
1647
637
  return cliPolicyHandler(
1648
638
  file: policyFile,
1649
639
  fields: ALWAYS_FIELDS,
@@ -1651,59 +641,45 @@ def setupSession(interactive: boolean): any {
1651
641
  )
1652
642
  }
1653
643
 
1654
- // One-shot entry: run a single turn through the same `mainAgent` the
1655
- // REPL uses, with the same session setup and policy handler, and return
1656
- // the reply. Used for `agency agent -p "..."`, a positional query, or
1657
- // piped stdin. Slash commands are expanded so `-p /foo bar` works like
1658
- // typing it in the REPL; the loop-only built-ins (`/exit` etc.) are
1659
- // meaningless here and aren't checked.
1660
644
  def oneShotAgent(target: string, prompt: string): string {
1661
645
  _isInteractive = false
1662
- const handler = setupSession(false)
1663
- let reply: string = ""
646
+ const handler = setupSession(interactive: false)
1664
647
  handle {
1665
- // `try` converts a terminal LLM failure (retries exhausted on a dropped
1666
- // connection / timeout) into a friendly message; aborts/interrupts still
1667
- // propagate to the handler below.
1668
- const result = try agentReplyVia(target, prompt)
1669
- if (result is success(r)) {
1670
- reply = r
1671
- } else if (result is failure(f)) {
1672
- reply = formatTurnFailure("${f.error}")
648
+ const result = try dispatchAgent(target, prompt)
649
+ return match(result) {
650
+ success(r) => r
651
+ failure(f) => formatTurnFailure("${f.error}")
1673
652
  }
1674
- } with (data) {
1675
- return handler(data)
1676
- }
1677
- return reply
653
+ } with handler
1678
654
  }
1679
655
 
1680
- // Render one seeded turn into the REPL scroll area: echo the auto-run
1681
- // prompt so the user sees what was asked, then push the agent's reply.
1682
- // Mirrors the reply-rendering half of `_runTurn`.
1683
- def _runSeedTurn(target: string, msg: string) {
656
+ /* A seeded turn is if the user invokes the Agency agent with
657
+ an initial prompt by using the agency doctor command, for example.
658
+ It means that agent starts with an initial prompt already there to respond to. */
659
+ def runSeedTurn(target: string, msg: string) {
1684
660
  pushMessage(color.dim("> ${msg}"))
1685
- const result = try agentReplyVia(target, msg)
1686
- if (result is success(reply)) {
1687
- if (reply != "" && reply != null && reply != undefined) {
661
+ const response = try dispatchAgent(target, msg)
662
+ renderAgentResponse(response)
663
+ }
664
+
665
+ def renderAgentResponse(
666
+ response: Result<string | null, { error: string }>,
667
+ ): void {
668
+ if (response is success(reply)) {
669
+ if (reply != "" && reply != null) {
1688
670
  pushMessage(highlight("${reply}\n", language: "markdown"))
1689
671
  } else {
1690
672
  pushMessage(color.red("No reply generated."))
1691
673
  }
1692
- } else if (result is failure(f)) {
674
+ } else if (response is failure(f)) {
1693
675
  pushMessage(color.red(formatTurnFailure("${f.error}")))
1694
676
  }
1695
677
  }
1696
678
 
1697
- // Start the interactive REPL. When `seedPrompt` is non-empty it is run as
1698
- // the first turn (routed via `seedTarget`) *inside* the handle block so
1699
- // its interrupts reach the policy `handler`; the user is then left at the
1700
- // prompt. `seedPrompt == ""` is the plain REPL with no seed. Subsequent
1701
- // turns always go through `_runTurn` (the coordinator) regardless of
1702
- // `seedTarget` — the target only kicks off the first turn.
1703
679
  def startInteractive(handler: any, seedTarget: string, seedPrompt: string) {
1704
680
  handle {
1705
681
  if (seedPrompt != "") {
1706
- _runSeedTurn(seedTarget, seedPrompt)
682
+ runSeedTurn(seedTarget, seedPrompt)
1707
683
  }
1708
684
  repl(
1709
685
  status: _buildStatus,
@@ -1713,107 +689,20 @@ def startInteractive(handler: any, seedTarget: string, seedPrompt: string) {
1713
689
  historyMax: 1000,
1714
690
  paletteCommands: mergedPalette(),
1715
691
  )
1716
- } with (data) {
1717
- return handler(data)
1718
- }
692
+ } with handler
1719
693
  print(color.cyan("\nGoodbye!"))
1720
694
  }
1721
695
 
1722
696
  node main() {
1723
697
  // Parse CLI flags first — `parseArgs` exits on --help / --version /
1724
698
  // usage errors, before any handlers or the TUI are installed.
1725
- const args = parseArgs(
1726
- {
1727
- programName: "agency agent",
1728
- description: "Agency language assistant — interactive REPL by default; one-shot when piped or passed a query / --print.",
1729
- version: getVersion(),
1730
- flags: {
1731
- print: {
1732
- type: "boolean",
1733
- short: "p",
1734
- description: "Run one-shot: print the reply to stdout and exit (no REPL)"
1735
- },
1736
- debug: {
1737
- type: "boolean",
1738
- description: "Log tool returns (toolName, result, timing). Same as AGENT_DEBUG=1"
1739
- },
1740
- verbose: {
1741
- type: "boolean",
1742
- description: "Echo tool-call starts to stdout in non-interactive mode"
1743
- },
1744
- model: {
1745
- type: "string",
1746
- description: "Model for all LLM calls (overrides provider auto-detection)"
1747
- },
1748
- fastmodel: {
1749
- type: "string",
1750
- description: "Model for ordinary work (default: per detected provider)"
1751
- },
1752
- slowmodel: {
1753
- type: "string",
1754
- description: "Model for deep reasoning, e.g. the oracle/explorer subagents"
1755
- },
1756
- provider: {
1757
- type: "string",
1758
- description: "Force the LLM provider. Given alone (openai/anthropic/google/openrouter) it selects that provider's default models and skips env auto-detection; pair it with --model for any other provider (e.g. litellm, openai-compat, or a custom/local model)"
1759
- },
1760
- interactive: {
1761
- type: "boolean",
1762
- short: "i",
1763
- description: "Run the given prompt as the first turn of an interactive session, then hand over the REPL (instead of one-shot)"
1764
- },
1765
- agent: {
1766
- type: "string",
1767
- description: "Route the starting prompt to a named subagent: code, research, oracle, explorer, review (default: coordinator)"
1768
- },
1769
- policy: {
1770
- type: "string",
1771
- optional: true,
1772
- description: "Approval policy for this run: a built-in name (minimal, recommended, with-writes, approve-all) or a path to a policy JSON file. with-writes auto-approves file writes and git changes scoped to the current directory and its children; approve-all approves EVERY interrupt with no scoping (sandbox use only). Overrides the saved policy without persisting it. Pass --policy with no value to list the built-in policies."
1773
- },
1774
- "agent-home": {
1775
- type: "string",
1776
- description: "Directory to use instead of ~/.agency-agent for settings, policy, and history (equivalent to the AGENCY_AGENT_HOME env var; the flag wins when both are set)"
1777
- },
1778
- local: {
1779
- type: "string",
1780
- optional: true,
1781
- description: "Run a local model (downloads if needed): a curated short name, an hf: URI, or a .gguf path. Run without a value for guided setup. Pins all slots to the local model; mutually exclusive with --model/--fastmodel/--slowmodel. Requires: npm i -g smoltalk-llama-cpp"
1782
- },
1783
- "local-model": {
1784
- type: "string",
1785
- optional: true,
1786
- description: "Deprecated alias for --local."
1787
- },
1788
- trace: {
1789
- type: "string",
1790
- optional: true,
1791
- description: "Write an execution trace to this file for debugging (bare --trace writes <runId>.agencytrace in the current directory)"
1792
- },
1793
- "log-file": {
1794
- type: "string",
1795
- description: "Append structured statelog events (one JSON object per line) to this file for debugging"
1796
- },
1797
- "max-tool-call-rounds": {
1798
- type: "number",
1799
- description: "Max LLM tool-call rounds before a tool loop halts (default 10 interactive, 50 in one-shot/-p mode)"
1800
- },
1801
- "max-tool-result-chars": {
1802
- type: "number",
1803
- description: "Max chars of a single tool result fed back to the model (0 disables; default 100000)"
1804
- }
1805
- }
1806
- },
1807
- )
699
+ const args = parseAgentArgs()
1808
700
 
1809
- // NOTE: --trace / --log-file / --agent-home are declared here only for
701
+ // NOTE: --trace / --log / --agent-home are declared here only for
1810
702
  // --help and so parseArgs accepts them. runBundledAgent extracts them from
1811
703
  // the forwarded args and passes them to this process via env vars
1812
704
  // (AGENCY_CONFIG_OVERRIDES for the first two, AGENCY_AGENT_HOME for the
1813
- // last), read before this main() runs — the RuntimeContext consumes the
1814
- // config overrides at construction, and lib/config.agency derives the
1815
- // settings/policy/history paths from the agent home in its static-const
1816
- // initializers. Do not re-handle them here.
705
+ // last).
1817
706
 
1818
707
  if (args.flags.debug) {
1819
708
  AGENT_DEBUG = true
@@ -1821,6 +710,21 @@ node main() {
1821
710
  if (args.flags.verbose) {
1822
711
  VERBOSE = true
1823
712
  }
713
+
714
+ if (isFailure(MAIN_PROMPT_LARGE)) {
715
+ print(
716
+ color.red("Failed to read prompts/main-large.md: ${MAIN_PROMPT_LARGE.error}"),
717
+ )
718
+ process.exit(1)
719
+ }
720
+
721
+ if (isFailure(MAIN_PROMPT_SMALL)) {
722
+ print(
723
+ color.red("Failed to read prompts/main-small.md: ${MAIN_PROMPT_SMALL.error}"),
724
+ )
725
+ process.exit(1)
726
+ }
727
+
1824
728
  // `--policy` is an optional-value flag: a bare `--policy` (no name or
1825
729
  // path) prints the usage + the built-in policies and exits, rather than
1826
730
  // silently doing nothing.
@@ -1830,100 +734,7 @@ node main() {
1830
734
  }
1831
735
  _policyArg = args.flags.policy ?? ""
1832
736
 
1833
- // Model setup. `--local` (deprecated alias: `--local-model`) is an optional-
1834
- // value flag with three states: absent (null); bare empty-string for guided
1835
- // setup; a value for that local model. `--local` and the hosted flags
1836
- // (--model / --fastmodel / --slowmodel) are mutually exclusive -- local and
1837
- // hosted are different in kind (provider llama-cpp, no embedding endpoint).
1838
- const localVal = args.flags["local"] ?? args.flags["local-model"]
1839
- const modelArg = args.flags.model ?? ""
1840
- const hasModelFlags = modelArg != "" || (args.flags.fastmodel ?? "") != "" || (args.flags.slowmodel ?? "") != ""
1841
- if (localVal != null && hasModelFlags) {
1842
- print(
1843
- color.red("Pass either --model (hosted) or --local (local), not both."),
1844
- )
1845
- process.exit(1)
1846
- }
1847
- if (localVal == "") {
1848
- // Bare --local: guided setup. Non-interactive can't prompt, so keep the
1849
- // print-catalog-and-exit behavior there.
1850
- if (!isTTY()) {
1851
- printLocalCatalog()
1852
- process.exit(0)
1853
- }
1854
- if (!localModelsSupported()) {
1855
- print(color.yellow("Local models need the smoltalk-llama-cpp package."))
1856
- print("Install it, then re-run: ! npm i -g smoltalk-llama-cpp")
1857
- process.exit(1)
1858
- }
1859
- let items: ChoiceItem[] = []
1860
- for (modelName in listModelNames()) {
1861
- items.push({
1862
- key: modelName.name,
1863
- label: modelName.name
1864
- })
1865
- }
1866
- const choice = chooseOption(
1867
- "Local model",
1868
- "Pick a model, or type an hf: URI / .gguf path (downloads on first use):",
1869
- items,
1870
- allowFreeText: true,
1871
- allowCancel: true,
1872
- )
1873
- if (choice == "" || choice == null) {
1874
- process.exit(0)
1875
- }
1876
- configureLocalModel(choice)
1877
- configureSearch(true)
1878
- // fall through into the session on the chosen local model
1879
- } else if (localVal != null) {
1880
- configureLocalModel(localVal)
1881
- configureSearch(true)
1882
- } else {
1883
- // Hosted path. --model accepts `slot=model` (per-slot) or a bare model
1884
- // (global pin); explicit --fastmodel/--slowmodel win over a --model slot=.
1885
- const parsed = parseModelFlag(modelArg)
1886
- let modelGlobal = ""
1887
- let fastFlag = ""
1888
- let slowFlag = ""
1889
- let embeddingFlag = ""
1890
- if (modelArg != "") {
1891
- if (parsed.slot == "") {
1892
- modelGlobal = parsed.model
1893
- } else if (parsed.slot == "main") {
1894
- fastFlag = parsed.model
1895
- } else if (parsed.slot == "reasoning") {
1896
- slowFlag = parsed.model
1897
- } else if (parsed.slot == "embedding") {
1898
- if (!embeddingSpecHasProvider(parsed.model)) {
1899
- print(
1900
- color.red(
1901
- "Embedding models need the provider/model form, e.g. --model embedding=openai/text-embedding-3-small.",
1902
- ),
1903
- )
1904
- process.exit(1)
1905
- }
1906
- embeddingFlag = parsed.model
1907
- } else {
1908
- print(
1909
- color.red(
1910
- "Unknown model slot '${parsed.slot}' in --model. Valid slots: ${SLOTS.join(", ")}.",
1911
- ),
1912
- )
1913
- process.exit(1)
1914
- }
1915
- }
1916
- const fm = args.flags.fastmodel ?? ""
1917
- const sm = args.flags.slowmodel ?? ""
1918
- if (fm != "") {
1919
- fastFlag = fm
1920
- }
1921
- if (sm != "") {
1922
- slowFlag = sm
1923
- }
1924
- configureModels(modelGlobal, fastFlag, slowFlag, embeddingFlag, args.flags.provider ?? "")
1925
- configureSearch(false)
1926
- }
737
+ pickModelsForAgent(args)
1927
738
 
1928
739
  // Persistent knowledge graph, gated on the capability profile. Must run
1929
740
  // before any handler/REPL is installed.
@@ -1939,83 +750,53 @@ node main() {
1939
750
  const positionalQuery = args.positionals.join(" ")
1940
751
  const hasQuery = positionalQuery != ""
1941
752
  const startAgent = args.flags.agent ?? ""
753
+ // Validate the target subagent name up front (empty = coordinator).
754
+ if (startAgent != "" && !START_AGENTS.includes(startAgent)) {
755
+ print(
756
+ color.red(
757
+ "Unknown --agent value: ${startAgent}. Valid: ${START_AGENTS.join(", ")}",
758
+ ),
759
+ )
760
+ process.exit(1)
761
+ }
762
+
1942
763
  const wantInteractive = args.flags.interactive
1943
764
 
1944
765
  // Seeded interactive: run the prompt as the first turn, then hand over the
1945
766
  // REPL (needs a TTY + query, and not --print). Everything else is one-shot.
1946
767
  const seededInteractive = wantInteractive && hasQuery && isTTY() && !args.flags.print
1947
- const runningOneShot = !seededInteractive && (args.flags.print || hasQuery || !isTTY())
768
+ const isOneShot = !seededInteractive && (args.flags.print || hasQuery || !isTTY())
1948
769
 
1949
- // Tool-loop caps. One-shot runs (--print / piped / a positional query) have
1950
- // no REPL to continue an autonomous task, so maxToolCallRounds defaults
1951
- // higher (resolveMaxToolCallRounds); an explicit --max-tool-call-rounds always
1952
- // wins. Set as branch-scoped defaults on the root branch BEFORE any subagent
1953
- // is dispatched, so every (fork-seeded) subagent inherits them. setLlmOptions
1954
- // merges only present keys, so this never clobbers the model defaults above.
1955
- // Separate calls, each gated on its own value: setLlmOptions only skips
1956
- // `undefined`, not `null`, so passing an absent key would clobber the bag.
1957
- const maxRounds = resolveMaxToolCallRounds(args.flags["max-tool-call-rounds"] ?? null, runningOneShot)
770
+ const maxRounds = resolveMaxToolCallRounds(
771
+ args.flags["max-tool-call-rounds"] ?? null,
772
+ isOneShot,
773
+ )
1958
774
  if (maxRounds != null) {
1959
- setLlmOptions({ maxToolCallRounds: maxRounds })
775
+ setLlmOptions({
776
+ maxToolCallRounds: maxRounds
777
+ })
1960
778
  }
1961
779
  const maxResultChars = args.flags["max-tool-result-chars"]
1962
780
  if (maxResultChars != null) {
1963
- setLlmOptions({ maxToolResultChars: maxResultChars })
781
+ setLlmOptions({
782
+ maxToolResultChars: maxResultChars
783
+ })
1964
784
  }
1965
785
 
1966
- // Per-model default output cap from the capability profile. The hosted
1967
- // default of 20000 carries the adaptive-thinking empty-reply fix that
1968
- // used to live here as a TEMPORARY block. Local profiles cap lower.
1969
- // Branch-scoped, so every subagent inherits it. A per-call
1970
- // llm(..., { maxTokens }) still wins, e.g. the summarizer's 256.
1971
786
  const capsMaxTokens = getCapabilities().maxTokens
1972
787
  if (capsMaxTokens != null) {
1973
- setLlmOptions({ maxTokens: capsMaxTokens })
788
+ setLlmOptions({
789
+ maxTokens: capsMaxTokens
790
+ })
1974
791
  }
1975
792
 
1976
793
  // One-shot autonomy: tell the code subagent there is no human to answer
1977
794
  // questions and no next turn, so it must drive to a finished artifact.
1978
795
  // Set before any dispatch so both the pinned `--agent code` path and the
1979
796
  // coordinator (which calls codeAgent as a tool) pick it up.
1980
- setCodeOneShot(runningOneShot)
797
+ setCodeOneShot(isOneShot)
1981
798
 
1982
- // Validate the target subagent name up front (empty = coordinator).
1983
- if (startAgent != "" && !START_AGENTS.includes(startAgent)) {
1984
- print(
1985
- color.red(
1986
- "Unknown --agent value: ${startAgent}. Valid: ${START_AGENTS.join(", ")}",
1987
- ),
1988
- )
1989
- process.exit(1)
1990
- }
1991
-
1992
- // Seeded interactive: run the given prompt as the first turn, then hand
1993
- // the REPL to the user. Requires a seed prompt and a TTY (the REPL reads
1994
- // stdin). `--print` wins if both are passed — its whole job is to print
1995
- // and exit with no REPL. Without a TTY (or with `--print`) we fall
1996
- // through to one-shot below; `--interactive` with no prompt falls through
1997
- // to the plain REPL.
1998
- if (seededInteractive) {
1999
- setTitle("Agency Agent")
2000
- clearScreen()
2001
- printHeader()
2002
- const seededHandler = setupSession(true)
2003
- startInteractive(seededHandler, startAgent, positionalQuery)
2004
- process.exit(0)
2005
- }
2006
-
2007
- // One-shot invocation: run one turn through the same agent the REPL uses
2008
- // and write the reply to stdout. No banner, no REPL. Entered when
2009
- // `--print` / `-p` is passed, a positional query is given, or stdin is
2010
- // piped (no TTY) — so the agent behaves like a Unix filter:
2011
- // `echo "..." | agency agent`. `--interactive` is NOT a one-shot trigger
2012
- // on its own: with a TTY and no prompt it falls through to the plain
2013
- // REPL; without a TTY the `!isTTY()` check below routes it here to
2014
- // degrade gracefully. The prompt comes from the positional when present,
2015
- // otherwise from stdin. (Title/clearScreen are skipped so piped output
2016
- // stays free of escape codes.)
2017
- const forceOneShot = args.flags.print || hasQuery
2018
- if (forceOneShot || !isTTY()) {
799
+ if (isOneShot) {
2019
800
  let prompt = positionalQuery
2020
801
  if (!hasQuery) {
2021
802
  const fromStdin = readStdin()
@@ -2025,17 +806,13 @@ node main() {
2025
806
  prompt = fromStdin
2026
807
  }
2027
808
  print(oneShotAgent(startAgent, prompt))
2028
- process.exit(0)
809
+ const roundedCost = "$${getCost().toFixed(4)}"
810
+ print(color.dim("\nSession cost: ${roundedCost}"))
811
+ } else {
812
+ setTitle("Agency Agent")
813
+ clearScreen()
814
+ print(getHeader())
815
+ const handler = setupSession(interactive: true)
816
+ startInteractive(handler, startAgent, positionalQuery)
2029
817
  }
2030
-
2031
- // Plain interactive REPL (no seed). `startInteractive` owns the runloop;
2032
- // every Enter dispatches into `_runTurn` which calls `mainAgent`.
2033
- // Interrupts raised during a turn propagate to the policy handler
2034
- // installed by the enclosing `handle` block via the AsyncLocalStorage
2035
- // handler stack.
2036
- setTitle("Agency Agent")
2037
- clearScreen()
2038
- printHeader()
2039
- const handler = setupSession(true)
2040
- startInteractive(handler, "", "")
2041
818
  }