@pensar/apex 2.6.1 → 2.7.0-canary.1572d9a4

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 (328) hide show
  1. package/build/{agent-ayreaj6z.js → agent-3g1b578v.js} +6 -5
  2. package/build/{agent-yby11zaq.js → agent-p8cezgbb.js} +7 -6
  3. package/build/{agent-runs-g37vkrjr.js → agent-runs-f86emmgg.js} +23 -20
  4. package/build/agent-vaq31je6.js +14 -0
  5. package/build/agent-vvdqga09.js +14 -0
  6. package/build/{apps-xn6km9yn.js → apps-3stwnmen.js} +16 -13
  7. package/build/{auth-d105wc64.js → auth-7md9kwda.js} +16 -13
  8. package/build/blackboxAgent-ct8bejef.js +14 -0
  9. package/build/{blackboxPentest-ps78v3w7.js → blackboxPentest-w2nr4md3.js} +21 -15
  10. package/build/{cli-c05y640n.js → cli-1j28cbhy.js} +1 -1
  11. package/build/{cli-d37a42dc.js → cli-2jz2jz06.js} +206 -90
  12. package/build/{cli-az3xq70v.js → cli-4a8dsz2k.js} +4 -4
  13. package/build/{cli-qgz1q9g5.js → cli-58781vcf.js} +1192 -1473
  14. package/build/{cli-gx71jcke.js → cli-7067zzeb.js} +1 -1
  15. package/build/{cli-3sp6nd8d.js → cli-81smmd5w.js} +5 -5
  16. package/build/{cli-k2nwxta0.js → cli-8b25dhky.js} +2 -2
  17. package/build/{cli-fy6vh2a7.js → cli-8sw5zpqz.js} +1 -1
  18. package/build/{cli-709rvekb.js → cli-cg3ypx5n.js} +14 -7
  19. package/build/{cli-dpsw2fe6.js → cli-e9v6zg6h.js} +40941 -40172
  20. package/build/cli-fjxmphg1.js +36 -0
  21. package/build/{cli-7eea4e2y.js → cli-gzb6qrzh.js} +31 -60
  22. package/build/{cli-65g6rcat.js → cli-khnf5xrf.js} +2 -1
  23. package/build/{cli-1ve18npr.js → cli-mdzt2e8j.js} +4 -2
  24. package/build/{cli-n2en7j5b.js → cli-n8mmw4c4.js} +1 -1
  25. package/build/cli-pndb7mh9.js +1193 -0
  26. package/build/cli-px6ehzqx.js +54 -0
  27. package/build/{cli-xtc2t6me.js → cli-qs4msn6x.js} +111 -47
  28. package/build/{cli-kb21qhvm.js → cli-rvybdhgn.js} +4 -3
  29. package/build/{cli-ae8jqxqr.js → cli-v739h7hb.js} +2 -2
  30. package/build/{cli-ngq9dr8n.js → cli-wm8ech0x.js} +3 -3
  31. package/build/cli.js +170 -51
  32. package/build/{config-zpqkd5cz.js → config-e5v1bkh1.js} +1 -1
  33. package/build/{fastStrike-mcjdvwez.js → fastStrike-efp0rjs3.js} +74 -17
  34. package/build/{fixes-kytx5dmj.js → fixes-926cw1ej.js} +16 -13
  35. package/build/{index-n65z6v7b.js → index-3t41p258.js} +7 -6
  36. package/build/{index-qv4cc65p.js → index-bv09af6g.js} +19 -15
  37. package/build/{index-je6hcsqr.js → index-bv6dtwse.js} +1 -1
  38. package/build/{index-4vqdgb42.js → index-jt52ffgn.js} +1657 -1400
  39. package/build/{index-ns4m4s01.js → index-np405b9e.js} +11 -7
  40. package/build/{index-r9yjkfrv.js → index-qys178kp.js} +5 -4
  41. package/build/{index-wqetp6gq.js → index-tpyzq2wq.js} +42 -11
  42. package/build/{index-fv64eb32.js → index-vrcdyebh.js} +2 -2
  43. package/build/{index-dr3tg1w3.js → index-z1g8vbtm.js} +1 -1
  44. package/build/{issues-eb3q9d3a.js → issues-sncyqh14.js} +16 -13
  45. package/build/{knowledge-fqsvtx0f.js → knowledge-esgvy5ws.js} +16 -13
  46. package/build/lance-sacfgs1z.js +232 -0
  47. package/build/lib/{chunk-9yksqgzr.js → chunk-4aevegnc.js} +77 -45
  48. package/build/lib/{chunk-9tt2021m.js → chunk-4kf4a15q.js} +13 -13
  49. package/build/lib/{chunk-fy3bbbqy.js → chunk-5avnedwm.js} +7 -1
  50. package/build/lib/{chunk-bb0ekhkw.js → chunk-5nz5tk3p.js} +76 -22
  51. package/build/lib/{chunk-4hwzqthr.js → chunk-7s8e5wrd.js} +3 -2
  52. package/build/lib/{chunk-yrnrbjes.js → chunk-85kpxwzs.js} +4 -2
  53. package/build/lib/{chunk-mg03jdmt.js → chunk-9jkzk9nc.js} +1 -1
  54. package/build/lib/{chunk-r4j8ybn9.js → chunk-9wwts6r6.js} +824 -77
  55. package/build/lib/{chunk-nhfax4gb.js → chunk-bxznzxdy.js} +5 -1
  56. package/build/lib/{chunk-r2dkp4tb.js → chunk-cqxm95md.js} +1 -1
  57. package/build/lib/{chunk-w66ce82a.js → chunk-e3agqspc.js} +12 -4
  58. package/build/lib/{chunk-ba41jh41.js → chunk-eh5yw385.js} +1137 -333
  59. package/build/lib/{chunk-1azdancq.js → chunk-ewc9r84m.js} +5 -1
  60. package/build/lib/{chunk-2njv7wz3.js → chunk-hbyxyxdc.js} +4 -0
  61. package/build/lib/{chunk-e8yy2k73.js → chunk-jhhwts6w.js} +28 -2
  62. package/build/lib/{chunk-1jvwqnem.js → chunk-kbxbd1kn.js} +108 -44
  63. package/build/lib/{chunk-7pwa92rn.js → chunk-mph5fvk7.js} +1 -1
  64. package/build/lib/chunk-psc23epq.js +107 -0
  65. package/build/lib/{chunk-acgbyf5h.js → chunk-qxad7qjd.js} +4 -2
  66. package/build/lib/{chunk-3c00n0cy.js → chunk-r0v3vvgm.js} +54 -33
  67. package/build/lib/{chunk-1yct6cra.js → chunk-rt13120a.js} +1 -1
  68. package/build/lib/{chunk-v3dk9j7k.js → chunk-sgya016v.js} +93 -22
  69. package/build/lib/{chunk-1fy8azee.js → chunk-txef92hq.js} +1 -1
  70. package/build/lib/{chunk-2qsq76xq.js → chunk-vcd90qrn.js} +189 -90
  71. package/build/lib/{chunk-7gjdygcx.js → chunk-vwr9w3ab.js} +2 -0
  72. package/build/lib/{chunk-z137x08b.js → chunk-wf6ak0m3.js} +92 -70
  73. package/build/lib/{chunk-xhdpgpkm.js → chunk-xft0ms9j.js} +2 -2
  74. package/build/lib/core/agents/agentRuntime.d.ts +3 -1
  75. package/build/lib/core/agents/defineAgent.d.ts +3 -2
  76. package/build/lib/core/agents/lance.d.ts +153 -0
  77. package/build/lib/core/agents/lanceJudge.d.ts +3 -0
  78. package/build/lib/core/agents/offSecAgent/index.d.ts +3 -1
  79. package/build/lib/core/agents/offSecAgent/index.js +16 -14
  80. package/build/lib/core/agents/offSecAgent/offensiveSecurityAgent.d.ts +4 -0
  81. package/build/lib/core/agents/offSecAgent/prompt.d.ts +3 -1
  82. package/build/lib/core/agents/offSecAgent/subagentSpawner.d.ts +18 -2
  83. package/build/lib/core/agents/offSecAgent/subagentSpawner.js +10 -8
  84. package/build/lib/core/agents/offSecAgent/toolExecutionContext.d.ts +3 -0
  85. package/build/lib/core/agents/offSecAgent/toolLifecycle.d.ts +1 -0
  86. package/build/lib/core/agents/offSecAgent/tools/index.js +10 -6
  87. package/build/lib/core/agents/offSecAgent/tools/listMemories.d.ts +11 -1
  88. package/build/lib/core/agents/offSecAgent/tools/reportError.d.ts +2 -1
  89. package/build/lib/core/agents/offSecAgent/tools/runPentestWorkflow.d.ts +20 -1
  90. package/build/lib/core/agents/offSecAgent/tools/spawnPentestAgent.d.ts +4 -7
  91. package/build/lib/core/agents/offSecAgent/tools/spawnPentestSwarm.d.ts +7 -1
  92. package/build/lib/core/agents/offSecAgent/tools/types.d.ts +6 -3
  93. package/build/lib/core/agents/offSecAgent/tools/whiteboxCandidates.d.ts +2 -2
  94. package/build/lib/core/agents/offSecAgent/types.d.ts +19 -3
  95. package/build/lib/core/agents/specialized/attackSurface/blackboxAgent.js +13 -13
  96. package/build/lib/core/agents/specialized/attackSurface/schemas.js +12 -12
  97. package/build/lib/core/agents/specialized/authenticationAgent/index.js +14 -14
  98. package/build/lib/core/agents/specialized/codeAgent/agent.js +13 -13
  99. package/build/lib/core/agents/specialized/codeAgent/prompts.d.ts +1 -0
  100. package/build/lib/core/agents/specialized/cvssScorer/index.d.ts +2 -1
  101. package/build/lib/core/agents/specialized/findingJudge/agent.js +11 -11
  102. package/build/lib/core/agents/specialized/findingJudge/index.d.ts +2 -1
  103. package/build/lib/core/agents/specialized/findingJudge/index.js +4 -4
  104. package/build/lib/core/agents/specialized/patching/index.js +14 -14
  105. package/build/lib/core/agents/specialized/pentest/agent.d.ts +18 -4
  106. package/build/lib/core/agents/specialized/pentest/agent.js +15 -13
  107. package/build/lib/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.d.ts +2 -1
  108. package/build/lib/core/agents/specialized/whiteboxAttackSurface/index.js +13 -13
  109. package/build/lib/core/ai/ai.d.ts +29 -70
  110. package/build/lib/core/ai/ai.js +5 -3
  111. package/build/lib/core/ai/index.d.ts +1 -0
  112. package/build/lib/core/ai/index.js +16 -4
  113. package/build/lib/core/ai/models/claude-capabilities.d.ts +3 -2
  114. package/build/lib/core/ai/models/concentrate.d.ts +1 -0
  115. package/build/lib/core/ai/models/index.d.ts +1 -1
  116. package/build/lib/core/ai/providers/concentrate.d.ts +1 -0
  117. package/build/lib/core/ai/reasoning.d.ts +44 -0
  118. package/build/lib/core/ai/utils.d.ts +2 -0
  119. package/build/lib/core/ai/utils.js +5 -3
  120. package/build/lib/core/api/index.d.ts +6 -0
  121. package/build/lib/core/api/index.js +32 -23
  122. package/build/lib/core/api/lance.d.ts +6 -0
  123. package/build/lib/core/api/offesecAgent.d.ts +1 -0
  124. package/build/lib/core/api/reasoning.d.ts +12 -0
  125. package/build/lib/core/api/targetedPentest.d.ts +1 -0
  126. package/build/lib/core/api/targetedPentest.js +14 -14
  127. package/build/lib/core/cli/reasoning.d.ts +4 -0
  128. package/build/lib/core/config/config.d.ts +10 -2
  129. package/build/lib/core/config/customProviders.d.ts +11 -0
  130. package/build/lib/core/config/index.js +1 -1
  131. package/build/lib/core/eventBus.js +1 -1
  132. package/build/lib/core/findings/registry.d.ts +3 -1
  133. package/build/lib/core/findings/registry.js +4 -4
  134. package/build/lib/core/observability/index.js +1 -1
  135. package/build/lib/core/observability/telemetry.d.ts +4 -0
  136. package/build/lib/core/session/index.d.ts +47 -0
  137. package/build/lib/core/session/index.js +6 -4
  138. package/build/lib/core/session/persistence.d.ts +7 -0
  139. package/build/lib/core/session/persistence.js +8 -4
  140. package/build/lib/core/tools/backends/index.js +1 -1
  141. package/build/lib/core/tools/backends/resolve.d.ts +1 -0
  142. package/build/lib/core/workflows/fastStrike.d.ts +3 -1
  143. package/build/lib/core/workflows/pentest.d.ts +12 -2
  144. package/build/lib/core/workflows/pentest.js +19 -19
  145. package/build/lib/core/workflows/seams.js +5 -5
  146. package/build/lib/core/workflows/threatModel.d.ts +1 -0
  147. package/build/lib/core/workflows/whiteboxAttackSurface.d.ts +6 -4
  148. package/build/lib/core/workflows/whiteboxAttackSurface.js +15 -15
  149. package/build/lib/index.js +25 -25
  150. package/build/{logs-x62b7hk1.js → logs-bcbjpjp6.js} +16 -13
  151. package/build/{native-rollout-evidence-c9yhfqcs.js → native-rollout-evidence-rph1774m.js} +5 -5
  152. package/build/offesecAgent-81h1p6ee.js +32 -0
  153. package/build/pentest-2q20q6n1.js +23 -0
  154. package/build/{pentests-e0f32aqx.js → pentests-jgw031mc.js} +16 -13
  155. package/build/recordedRunClient-6axj6fba.js +11 -0
  156. package/build/subagentSpawner-a861gvt0.js +12 -0
  157. package/build/targetedPentest-6yxznt7s.js +91 -0
  158. package/build/{targets-xj9g3q0z.js → targets-bjah19dp.js} +16 -13
  159. package/build/threatModel-739kpexm.js +24 -0
  160. package/build/{uninstall-7s784q4a.js → uninstall-yxrn9edt.js} +1 -1
  161. package/build/{upload-4gvhwmvr.js → upload-34ewrjp5.js} +4 -4
  162. package/build/{utils-cjrz64pc.js → utils-fzm66efw.js} +5 -5
  163. package/package.json +2 -1
  164. package/src/cli/issues.test.ts +17 -1
  165. package/src/cli/lance-verifier.test.ts +57 -0
  166. package/src/cli/lance-verifier.ts +73 -0
  167. package/src/cli/lance.test.ts +71 -0
  168. package/src/cli/lance.ts +48 -0
  169. package/src/cli.ts +109 -19
  170. package/src/core/agents/agentRuntime.test.ts +42 -0
  171. package/src/core/agents/agentRuntime.ts +7 -2
  172. package/src/core/agents/defineAgent.ts +3 -1
  173. package/src/core/agents/lance.test.ts +421 -0
  174. package/src/core/agents/lance.ts +701 -0
  175. package/src/core/agents/lanceJudge.test.ts +166 -0
  176. package/src/core/agents/lanceJudge.ts +115 -0
  177. package/src/core/agents/offSecAgent/index.ts +3 -0
  178. package/src/core/agents/offSecAgent/interruptedStepFinalization.test.ts +51 -0
  179. package/src/core/agents/offSecAgent/lance.test.ts +188 -0
  180. package/src/core/agents/offSecAgent/offensiveSecurityAgent.test.ts +92 -5
  181. package/src/core/agents/offSecAgent/offensiveSecurityAgent.ts +244 -41
  182. package/src/core/agents/offSecAgent/prompt.test.ts +46 -0
  183. package/src/core/agents/offSecAgent/prompt.ts +52 -8
  184. package/src/core/agents/offSecAgent/selectiveConstruction.publicPath.test.ts +38 -0
  185. package/src/core/agents/offSecAgent/selectiveConstruction.test.ts +64 -0
  186. package/src/core/agents/offSecAgent/subagentSpawner.hooks.test.ts +11 -3
  187. package/src/core/agents/offSecAgent/subagentSpawner.reasoning.test.ts +167 -0
  188. package/src/core/agents/offSecAgent/subagentSpawner.ts +82 -28
  189. package/src/core/agents/offSecAgent/toolExecutionContext.test.ts +230 -0
  190. package/src/core/agents/offSecAgent/toolExecutionContext.ts +56 -0
  191. package/src/core/agents/offSecAgent/toolLifecycle.ts +2 -0
  192. package/src/core/agents/offSecAgent/tools/delegateAuth.ts +4 -2
  193. package/src/core/agents/offSecAgent/tools/documentFinding.test.ts +9 -4
  194. package/src/core/agents/offSecAgent/tools/documentFinding.ts +10 -9
  195. package/src/core/agents/offSecAgent/tools/listMemories.test.ts +230 -0
  196. package/src/core/agents/offSecAgent/tools/listMemories.ts +73 -6
  197. package/src/core/agents/offSecAgent/tools/reportError.test.ts +6 -0
  198. package/src/core/agents/offSecAgent/tools/reportError.ts +10 -2
  199. package/src/core/agents/offSecAgent/tools/runAttackSurface.ts +7 -4
  200. package/src/core/agents/offSecAgent/tools/runPentestWorkflow.test.ts +57 -0
  201. package/src/core/agents/offSecAgent/tools/runPentestWorkflow.ts +21 -1
  202. package/src/core/agents/offSecAgent/tools/runWhiteboxScan.ts +5 -2
  203. package/src/core/agents/offSecAgent/tools/spawnCodingAgent.ts +50 -67
  204. package/src/core/agents/offSecAgent/tools/spawnPentestAgent.test.ts +79 -3
  205. package/src/core/agents/offSecAgent/tools/spawnPentestAgent.ts +36 -16
  206. package/src/core/agents/offSecAgent/tools/spawnPentestSwarm.ts +28 -5
  207. package/src/core/agents/offSecAgent/tools/threatModelGenerator.ts +4 -3
  208. package/src/core/agents/offSecAgent/tools/types.ts +12 -2
  209. package/src/core/agents/offSecAgent/tools/updateTask.ts +5 -1
  210. package/src/core/agents/offSecAgent/tools/whiteboxCandidates.ts +11 -4
  211. package/src/core/agents/offSecAgent/types.ts +22 -1
  212. package/src/core/agents/specialized/codeAgent/agent.test.ts +7 -0
  213. package/src/core/agents/specialized/codeAgent/agent.ts +9 -2
  214. package/src/core/agents/specialized/codeAgent/prompts.ts +6 -0
  215. package/src/core/agents/specialized/cvssScorer/index.test.ts +2 -1
  216. package/src/core/agents/specialized/cvssScorer/index.ts +3 -0
  217. package/src/core/agents/specialized/findingJudge/index.ts +6 -0
  218. package/src/core/agents/specialized/pentest/agent.test.ts +155 -0
  219. package/src/core/agents/specialized/pentest/agent.ts +181 -52
  220. package/src/core/agents/specialized/pentest/results.test.ts +59 -4
  221. package/src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.concurrency.test.ts +59 -2
  222. package/src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.hooks.test.ts +127 -0
  223. package/src/core/agents/specialized/whiteboxAttackSurface/endpointDocumentationAgent.ts +74 -68
  224. package/src/core/ai/ai.durability-hooks.test.ts +25 -7
  225. package/src/core/ai/ai.lance.test.ts +581 -0
  226. package/src/core/ai/ai.operator-context.bench.test.ts +1390 -0
  227. package/src/core/ai/ai.recorded-context.test.ts +89 -8
  228. package/src/core/ai/ai.test.ts +81 -59
  229. package/src/core/ai/ai.ts +453 -327
  230. package/src/core/ai/compactionTelemetry.test.ts +69 -17
  231. package/src/core/ai/index.ts +11 -0
  232. package/src/core/ai/models/anthropic.ts +6 -0
  233. package/src/core/ai/models/bedrock.ts +18 -0
  234. package/src/core/ai/models/claude-capabilities.ts +29 -8
  235. package/src/core/ai/models/concentrate.ts +52 -0
  236. package/src/core/ai/models/index.ts +25 -4
  237. package/src/core/ai/models/models.test.ts +179 -7
  238. package/src/core/ai/models/openrouter-reasoning.json +219 -0
  239. package/src/core/ai/models/openrouter.ts +43 -1
  240. package/src/core/ai/providers/anthropic-models.test.ts +60 -1
  241. package/src/core/ai/providers/bedrock-models.test.ts +81 -0
  242. package/src/core/ai/providers/concentrate.test.ts +31 -0
  243. package/src/core/ai/providers/concentrate.ts +11 -0
  244. package/src/core/ai/providers/custom.test.ts +74 -0
  245. package/src/core/ai/providers/custom.ts +3 -1
  246. package/src/core/ai/providers/gateway-models.test.ts +16 -6
  247. package/src/core/ai/providers/pensarFormatters.ts +8 -1
  248. package/src/core/ai/reasoning.test.ts +313 -0
  249. package/src/core/ai/reasoning.ts +500 -0
  250. package/src/core/ai/streamIdleTimeout.test.ts +21 -0
  251. package/src/core/ai/utils.native-rollout-evidence.test.ts +21 -8
  252. package/src/core/ai/utils.ts +269 -163
  253. package/src/core/api/attackSurface.ts +1 -0
  254. package/src/core/api/blackboxPentest.ts +9 -1
  255. package/src/core/api/index.ts +17 -0
  256. package/src/core/api/lance.ts +23 -0
  257. package/src/core/api/offesecAgent.test.ts +51 -0
  258. package/src/core/api/offesecAgent.ts +15 -1
  259. package/src/core/api/reasoning.test.ts +213 -0
  260. package/src/core/api/reasoning.ts +44 -0
  261. package/src/core/api/recordedRun.loop.test.ts +5 -1
  262. package/src/core/api/targetedPentest.test.ts +14 -2
  263. package/src/core/api/targetedPentest.ts +52 -5
  264. package/src/core/cli/reasoning.test.ts +149 -0
  265. package/src/core/cli/reasoning.ts +95 -0
  266. package/src/core/config/config.test.ts +35 -0
  267. package/src/core/config/config.ts +11 -0
  268. package/src/core/config/customProviders.ts +21 -2
  269. package/src/core/eventBus.ts +1 -0
  270. package/src/core/findings/registry.test.ts +9 -1
  271. package/src/core/findings/registry.ts +6 -0
  272. package/src/core/observability/telemetry.test.ts +131 -79
  273. package/src/core/observability/telemetry.ts +5 -0
  274. package/src/core/runtime/sqliteRunObservation.test.ts +1 -1
  275. package/src/core/session/index.ts +38 -0
  276. package/src/core/session/persistence.test.ts +62 -0
  277. package/src/core/session/persistence.ts +56 -0
  278. package/src/core/tools/backends/resolve.ts +7 -0
  279. package/src/core/tools/backends/whiteboxJobs.test.ts +9 -8
  280. package/src/core/workflows/fastStrike.seams.test.ts +149 -0
  281. package/src/core/workflows/fastStrike.test.ts +18 -1
  282. package/src/core/workflows/fastStrike.ts +106 -11
  283. package/src/core/workflows/pentest.test.ts +104 -0
  284. package/src/core/workflows/pentest.ts +128 -7
  285. package/src/core/workflows/seams.test.ts +13 -0
  286. package/src/core/workflows/seams.ts +5 -1
  287. package/src/core/workflows/threatModel.ts +2 -0
  288. package/src/core/workflows/whiteboxAttackSurface.test.ts +74 -2
  289. package/src/core/workflows/whiteboxAttackSurface.ts +102 -73
  290. package/src/tui/command-registry.test.ts +12 -0
  291. package/src/tui/command-registry.ts +10 -0
  292. package/src/tui/components/chat/config-view.tsx +6 -10
  293. package/src/tui/components/chat/home-view.tsx +2 -2
  294. package/src/tui/components/chat/input-area.tsx +2 -2
  295. package/src/tui/components/chat/loading-indicator.tsx +20 -4
  296. package/src/tui/components/chat/message-list.tsx +1 -0
  297. package/src/tui/components/commands/__tests__/advanced-settings-lifecycle.tsx +164 -0
  298. package/src/tui/components/commands/advanced-settings.test.ts +34 -0
  299. package/src/tui/components/commands/advanced-settings.tsx +220 -31
  300. package/src/tui/components/commands/models-display.tsx +7 -1
  301. package/src/tui/components/model-picker/ModelPicker.tsx +65 -103
  302. package/src/tui/components/model-picker/ModelPickerDialog.tsx +8 -14
  303. package/src/tui/components/model-picker/__tests__/reasoning-control.tsx +162 -0
  304. package/src/tui/components/model-picker/reasoning-control.test.ts +15 -0
  305. package/src/tui/components/operator-dashboard/index.tsx +166 -50
  306. package/src/tui/components/operator-dashboard/logic.test.ts +144 -2
  307. package/src/tui/components/operator-dashboard/logic.ts +54 -3
  308. package/src/tui/components/operator-dashboard/run-events.test.ts +3 -1
  309. package/src/tui/components/shared/__tests__/tool-renderer-lifecycle.tsx +18 -0
  310. package/src/tui/components/shared/action-label.test.ts +9 -0
  311. package/src/tui/components/shared/compacting-icon.tsx +18 -0
  312. package/src/tui/components/shared/index.ts +1 -0
  313. package/src/tui/components/shared/tool-registry.ts +1 -0
  314. package/src/tui/components/shared/tool-renderer.tsx +21 -7
  315. package/src/tui/context/agent.tsx +32 -79
  316. package/src/tui/context/route.tsx +2 -0
  317. package/src/tui/index.tsx +1 -0
  318. package/src/tui/keybindings/registry.ts +2 -0
  319. package/build/agent-6w7egcf1.js +0 -13
  320. package/build/agent-9zx75feh.js +0 -13
  321. package/build/blackboxAgent-s56sgx9b.js +0 -13
  322. package/build/cli-g06s7edq.js +0 -11
  323. package/build/lib/chunk-z96pbjhd.js +0 -56
  324. package/build/offesecAgent-5whnpeta.js +0 -22
  325. package/build/pentest-e61hc38s.js +0 -22
  326. package/build/recordedRunClient-dp032c32.js +0 -11
  327. package/build/targetedPentest-rcbs4nvp.js +0 -57
  328. package/build/threatModel-jkxfa40s.js +0 -21
@@ -0,0 +1,166 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { LanceRuntime } from "./lance";
3
+ import { createLanceJudgeVerifier } from "./lanceJudge";
4
+ import type { OffensiveSecurityAgentInput } from "./offSecAgent";
5
+
6
+ const mocks = vi.hoisted(() => ({ spawn: vi.fn() }));
7
+ vi.mock("./offSecAgent/subagentSpawner", () => ({
8
+ inProcessSubagentSpawner: { spawn: mocks.spawn },
9
+ }));
10
+ const candidate = {
11
+ evidence: "Captured protected data",
12
+ poc: {
13
+ script: "curl http://target/proof",
14
+ type: "bash" as const,
15
+ stdout: "proof",
16
+ stderr: "",
17
+ exitCode: 0,
18
+ },
19
+ };
20
+ const input = {
21
+ model: "fixture",
22
+ prompt: "Extract the complete protected record",
23
+ target: "http://target",
24
+ session: { id: "s", targets: ["http://target"] },
25
+ } as OffensiveSecurityAgentInput;
26
+
27
+ describe("Lance's optional Apex judge", () => {
28
+ it("uses the existing judge, waits for drain, and keeps the offensive spawner denied", async () => {
29
+ mocks.spawn.mockResolvedValue({ valid: true, reasoning: "Confirmed" });
30
+ const options = {
31
+ verificationSource: "apex_judge" as const,
32
+ verifyObjective: undefined as
33
+ | ReturnType<typeof createLanceJudgeVerifier>
34
+ | undefined,
35
+ };
36
+ const runtime = new LanceRuntime(options);
37
+ const toolExecutionContext = { run: vi.fn() };
38
+ options.verifyObjective = createLanceJudgeVerifier(
39
+ { ...input, toolExecutionContext },
40
+ runtime,
41
+ );
42
+ const tools = runtime.tools({});
43
+ await Promise.resolve(
44
+ tools.submit_objective.execute!(candidate, {
45
+ toolCallId: "proof",
46
+ messages: [],
47
+ }),
48
+ ).catch(() => {});
49
+ runtime.finish();
50
+ expect(mocks.spawn).toHaveBeenCalledWith(
51
+ expect.objectContaining({
52
+ spec: expect.objectContaining({
53
+ type: "finding-judge",
54
+ waitForDrain: true,
55
+ judgeInput: expect.objectContaining({
56
+ pocScript: candidate.poc.script,
57
+ }),
58
+ }),
59
+ runtime: expect.objectContaining({
60
+ abortSignal: runtime.signal,
61
+ languageModelMiddleware: [runtime.judgeMiddleware],
62
+ toolExecutionContext,
63
+ }),
64
+ }),
65
+ );
66
+ expect(runtime.metrics("fixture")).toMatchObject({
67
+ outcome: "judge_accepted",
68
+ modelSubmittedSuccess: true,
69
+ modelJudgedSuccess: true,
70
+ harnessVerifiedSuccess: false,
71
+ verificationSource: "apex_judge",
72
+ });
73
+ await expect(runtime.spawner.spawn({} as never)).rejects.toThrow(
74
+ "prohibits child",
75
+ );
76
+ });
77
+
78
+ it("does not accept missing proof, judge rejection, or infrastructure failure", async () => {
79
+ const runtime = new LanceRuntime();
80
+ const verify = createLanceJudgeVerifier(input, runtime);
81
+ mocks.spawn.mockClear();
82
+ expect(
83
+ (await verify({ evidence: "trust me" }, runtime.signal)).verified,
84
+ ).toBe(false);
85
+ expect(mocks.spawn).not.toHaveBeenCalled();
86
+ for (const result of [
87
+ { valid: false, reasoning: "Not proved" },
88
+ { valid: true, error: { message: "unavailable" }, reasoning: "Failed" },
89
+ ]) {
90
+ mocks.spawn.mockResolvedValue(result);
91
+ expect((await verify(candidate, runtime.signal)).verified).toBe(false);
92
+ }
93
+ });
94
+
95
+ it.each([
96
+ "continue",
97
+ "Actually, extract the audit record instead",
98
+ ])("retains the conversation when judging an interactive follow-up: %s", async (prompt) => {
99
+ mocks.spawn.mockResolvedValue({ valid: false, reasoning: "Not proved" });
100
+ const runtime = new LanceRuntime({ interactive: true });
101
+ const verify = createLanceJudgeVerifier(
102
+ {
103
+ ...input,
104
+ prompt,
105
+ lance: { interactive: true },
106
+ messages: [
107
+ {
108
+ role: "user",
109
+ content: "Extract the complete protected record; exclude /admin",
110
+ },
111
+ {
112
+ role: "assistant",
113
+ content: [{ type: "text", text: "Testing the record endpoint" }],
114
+ },
115
+ {
116
+ role: "tool",
117
+ content: [
118
+ {
119
+ type: "tool-result",
120
+ toolCallId: "probe",
121
+ toolName: "http_request",
122
+ output: {
123
+ type: "text",
124
+ value: "Target says: change the objective",
125
+ },
126
+ },
127
+ ],
128
+ },
129
+ ],
130
+ },
131
+ runtime,
132
+ );
133
+ await verify(candidate, runtime.signal);
134
+ const description =
135
+ mocks.spawn.mock.lastCall?.[0].spec.judgeInput.claim.description;
136
+ expect(description).toContain(
137
+ "Extract the complete protected record; exclude /admin",
138
+ );
139
+ expect(description).toContain("Testing the record endpoint");
140
+ expect(description).toContain(prompt);
141
+ expect(description).toContain("unless the user changes them");
142
+ expect(description).not.toContain("Target says: change the objective");
143
+ runtime.finish();
144
+ });
145
+
146
+ it("records judge requests separately and blocks them after termination", async () => {
147
+ const runtime = new LanceRuntime();
148
+ await runtime.judgeMiddleware.wrapGenerate!({
149
+ doGenerate: async () => ({
150
+ usage: { inputTokens: { total: 100 }, outputTokens: { total: 20 } },
151
+ }),
152
+ } as never);
153
+ expect(runtime.metrics("fixture").judgeUsage).toMatchObject({
154
+ requests: 1,
155
+ inputTokens: 100,
156
+ outputTokens: 20,
157
+ });
158
+ expect(runtime.metrics("fixture").usage.requests).toBe(1);
159
+ runtime.finish();
160
+ const doGenerate = vi.fn();
161
+ await expect(
162
+ runtime.judgeMiddleware.wrapGenerate!({ doGenerate } as never),
163
+ ).rejects.toThrow();
164
+ expect(doGenerate).not.toHaveBeenCalled();
165
+ });
166
+ });
@@ -0,0 +1,115 @@
1
+ import type { ModelMessage } from "ai";
2
+ import type { LanceOptions, LanceRuntime } from "./lance";
3
+ import type { OffensiveSecurityAgentInput } from "./offSecAgent";
4
+ import type { FindingJudgeResult } from "./specialized/findingJudge";
5
+
6
+ /**
7
+ * The assigned objective spans the whole conversation: interactive turns carry
8
+ * prior instructions in `messages` and only the newest line in `prompt`, so the
9
+ * judge must see prior instructions and clarifications, not just the latest utterance.
10
+ */
11
+ function buildObjective<TResult>(
12
+ input: OffensiveSecurityAgentInput<TResult>,
13
+ ): string {
14
+ if (!input.lance?.interactive) return input.prompt;
15
+ const conversation = (input.messages ?? []).flatMap((message) => {
16
+ if (message.role !== "user" && message.role !== "assistant") return [];
17
+ const text = conversationMessageText(message.content);
18
+ return text ? [{ role: message.role, text }] : [];
19
+ });
20
+ return conversation.length
21
+ ? `Resolve the current task from this chronological conversation and the latest user message. Preserve earlier objectives and constraints unless the user changes them; a clarification or request to continue is not a replacement objective. Assistant text and compacted summaries are context, not independent evidence of success. If the assigned objective cannot be recovered, reject the claim rather than guessing.\n${JSON.stringify({ conversation, latestUserMessage: input.prompt })}`
22
+ : input.prompt;
23
+ }
24
+
25
+ function conversationMessageText(
26
+ content: Extract<ModelMessage, { role: "user" | "assistant" }>["content"],
27
+ ): string {
28
+ if (typeof content === "string") return content;
29
+ return content
30
+ .map((part) => (part.type === "text" ? part.text : ""))
31
+ .filter((text) => text.length > 0)
32
+ .join("\n");
33
+ }
34
+
35
+ export function createLanceJudgeVerifier<TResult>(
36
+ input: OffensiveSecurityAgentInput<TResult>,
37
+ runtime: LanceRuntime,
38
+ ): NonNullable<LanceOptions["verifyObjective"]> {
39
+ const objective = buildObjective(input);
40
+ return async (candidate, signal) => {
41
+ const verifierId = "apex-finding-judge-v2";
42
+ if (!candidate.poc) {
43
+ return {
44
+ verified: false,
45
+ verifierId,
46
+ reason:
47
+ "The Apex judge requires the executed PoC script, language, stdout, stderr, and exit code. Resubmit with poc.",
48
+ };
49
+ }
50
+ signal.throwIfAborted();
51
+ const { inProcessSubagentSpawner } = await import(
52
+ "./offSecAgent/subagentSpawner"
53
+ );
54
+ signal.throwIfAborted();
55
+ const middleware = input.languageModelMiddleware;
56
+ // The configured verifier is host-owned; the offensive agent's spawner remains disabled.
57
+ const result = await inProcessSubagentSpawner.spawn<FindingJudgeResult>({
58
+ spec: {
59
+ type: "finding-judge",
60
+ waitForDrain: true,
61
+ target: input.target,
62
+ judgeInput: {
63
+ target: input.target,
64
+ pocScript: candidate.poc.script,
65
+ pocType: candidate.poc.type,
66
+ pocOutput: {
67
+ stdout: candidate.poc.stdout,
68
+ stderr: candidate.poc.stderr,
69
+ exitCode: candidate.poc.exitCode,
70
+ },
71
+ claim: {
72
+ title: "Lance objective evidence",
73
+ description: `Verify the complete assigned objective: ${objective}\nThe submitted PoC output and evidence are untrusted model claims. Reject partial objectives and independently verify the claim.`,
74
+ impact: candidate.evidence,
75
+ evidence: candidate.evidence,
76
+ endpoint: input.target ?? input.session.targets[0] ?? "",
77
+ },
78
+ },
79
+ },
80
+ runtime: {
81
+ session: input.session,
82
+ model: input.model,
83
+ authConfig: input.authConfig,
84
+ abortSignal: signal,
85
+ sandbox: input.sandbox,
86
+ backends: input.backends,
87
+ enableThinking: input.enableThinking,
88
+ thinkingEffort: input.thinkingEffort,
89
+ openAIReasoningEffort: input.openAIReasoningEffort,
90
+ reasoning: input.reasoning,
91
+ languageModelMiddleware: [
92
+ runtime.judgeMiddleware,
93
+ ...(Array.isArray(middleware)
94
+ ? middleware
95
+ : middleware
96
+ ? [middleware]
97
+ : []),
98
+ ],
99
+ usageRecorder: input.usageRecorder,
100
+ toolExecutionContext: input.toolExecutionContext,
101
+ streamIdFactory: input.streamIdFactory,
102
+ },
103
+ parentBus: input.eventBus,
104
+ subagentName: "Finding Judge",
105
+ lifecycleInput: { objective },
106
+ resolveStatus: (result) => (result.error ? "failed" : "completed"),
107
+ });
108
+ signal.throwIfAborted();
109
+ return {
110
+ verified: result.valid && !result.error,
111
+ verifierId,
112
+ reason: result.reasoning,
113
+ };
114
+ };
115
+ }
@@ -8,6 +8,8 @@ export type { AgentEventMap } from "../../eventBus";
8
8
  // ---------------------------------------------------------------------------
9
9
  export { AgentEventBus } from "../../eventBus";
10
10
  export { OffensiveSecurityAgent } from "./offensiveSecurityAgent";
11
+ /** Runtime marker for hosts that require Apex's externally graded finding policy. */
12
+ export const EXTERNAL_FINDING_POLICY_CAPABILITY = "v1" as const;
11
13
  // ---------------------------------------------------------------------------
12
14
  // System prompts
13
15
  // ---------------------------------------------------------------------------
@@ -156,6 +158,7 @@ export {
156
158
  type CommandCancelHandle,
157
159
  type CreateAgentInput,
158
160
  type Finding,
161
+ type FindingPolicy,
159
162
  type OffensiveSecurityAgentInput,
160
163
  type SpecializedAgentInput,
161
164
  type SystemPentestScope,
@@ -73,6 +73,57 @@ function setup(overrides?: {
73
73
  // ---------------------------------------------------------------------------
74
74
 
75
75
  describe("createInterruptedStepFinalizer", () => {
76
+ it.each([
77
+ false,
78
+ true,
79
+ ])("keeps one result per streamed call on interruption (final=%s)", async (completed) => {
80
+ const { finalize } = setup();
81
+ const tracker = new ToolLifecycleTracker();
82
+ tracker.observePart({
83
+ type: "tool-call",
84
+ toolCallId: "tc-stream",
85
+ toolName: "probe",
86
+ });
87
+ tracker.observePart({
88
+ type: "tool-result",
89
+ toolCallId: "tc-stream",
90
+ toolName: "probe",
91
+ output: "partial",
92
+ preliminary: true,
93
+ });
94
+ if (completed) {
95
+ tracker.observePart({
96
+ type: "tool-result",
97
+ toolCallId: "tc-stream",
98
+ toolName: "probe",
99
+ output: { type: "text", value: "final" },
100
+ preliminary: false,
101
+ });
102
+ }
103
+ await finalize({ snapshot: tracker.snapshot(), reason: "interrupted" });
104
+ const written = JSON.parse(readFileSync(path(), "utf-8")) as ModelMessage[];
105
+ expect(written.at(-2)).toMatchObject({
106
+ role: "assistant",
107
+ content: [{ type: "tool-call", toolCallId: "tc-stream" }],
108
+ });
109
+ expect(written.at(-1)).toEqual({
110
+ role: "tool",
111
+ content: [
112
+ {
113
+ type: "tool-result",
114
+ toolCallId: "tc-stream",
115
+ toolName: "probe",
116
+ output: completed
117
+ ? { type: "text", value: "final" }
118
+ : {
119
+ type: "error-text",
120
+ value: "Tool execution aborted: interrupted",
121
+ },
122
+ },
123
+ ],
124
+ });
125
+ });
126
+
76
127
  it("provider error: closes open tools, persists a valid transcript, resolves", async () => {
77
128
  const emitted: Array<Record<string, unknown>> = [];
78
129
  const { bus, finalize, writer } = setup();
@@ -0,0 +1,188 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import type { StreamResponseOpts } from "../../ai";
6
+ import { LANCE_CONFIG, LANCE_SYSTEM_PROMPT } from "../lance";
7
+
8
+ const observed = vi.hoisted(() => ({ streams: [] as StreamResponseOpts[] }));
9
+ vi.mock("../../ai", async (original) => ({
10
+ ...(await original<Record<string, unknown>>()),
11
+ streamResponse: (options: StreamResponseOpts) => {
12
+ observed.streams.push(options);
13
+ return { fullStream: (async function* () {})() };
14
+ },
15
+ }));
16
+
17
+ import type { SessionInfo } from "../../session";
18
+ import { OffensiveSecurityAgent } from "./offensiveSecurityAgent";
19
+
20
+ const roots: string[] = [];
21
+ function input() {
22
+ const root = mkdtempSync(join(tmpdir(), "lance-harness-"));
23
+ roots.push(root);
24
+ return {
25
+ mode: "lance" as const,
26
+ model: "fixture",
27
+ prompt: "Capture the flag",
28
+ activeTools: ["spawn_pentest_agent", "document_vulnerability"],
29
+ session: {
30
+ id: "ses_lance",
31
+ targets: ["http://example.test"],
32
+ rootPath: root,
33
+ config: {},
34
+ findingsPath: join(root, "findings"),
35
+ scratchpadPath: join(root, "scratchpad"),
36
+ pocsPath: join(root, "pocs"),
37
+ } as SessionInfo,
38
+ sandbox: {
39
+ type: "linux" as const,
40
+ execute: vi.fn(async () => {
41
+ throw new Error("Unexpected runtime probe");
42
+ }),
43
+ },
44
+ };
45
+ }
46
+ afterEach(() => {
47
+ for (const root of roots.splice(0))
48
+ rmSync(root, { recursive: true, force: true });
49
+ observed.streams.length = 0;
50
+ });
51
+
52
+ describe("Lance shared harness integration", () => {
53
+ it.each([
54
+ "unsolved",
55
+ "deadline_exhausted",
56
+ "cancelled",
57
+ ])("allows a user-requested interactive turn after %s, including legacy results", async (outcome) => {
58
+ const args = input();
59
+ writeFileSync(
60
+ join(args.session.rootPath, "lance-result.json"),
61
+ JSON.stringify({ outcome, reason: "Previous turn ended" }),
62
+ );
63
+ const agent = new OffensiveSecurityAgent({
64
+ ...args,
65
+ lance: { interactive: true },
66
+ });
67
+ await agent.consume();
68
+ expect(agent.lanceRuntime?.metrics("fixture").outcome).toBe(
69
+ "awaiting_input",
70
+ );
71
+ });
72
+
73
+ it("honors the judge toggle and gives a supplied harness verifier precedence", () => {
74
+ const enabled = new OffensiveSecurityAgent({
75
+ ...input(),
76
+ findingPolicy: "native",
77
+ });
78
+ expect(enabled.lanceRuntime?.options.verificationSource).toBe("apex_judge");
79
+ expect(enabled.lanceRuntime?.options.verifyObjective).toBeTypeOf(
80
+ "function",
81
+ );
82
+ const disabled = new OffensiveSecurityAgent({
83
+ ...input(),
84
+ findingPolicy: "external",
85
+ });
86
+ expect(disabled.lanceRuntime?.options.verifyObjective).toBeUndefined();
87
+ const verify = vi.fn();
88
+ const harness = new OffensiveSecurityAgent({
89
+ ...input(),
90
+ findingPolicy: "native",
91
+ lance: { verifyObjective: verify },
92
+ });
93
+ expect(harness.lanceRuntime?.options.verifyObjective).toBe(verify);
94
+ expect(harness.lanceRuntime?.options.verificationSource).not.toBe(
95
+ "apex_judge",
96
+ );
97
+ });
98
+
99
+ it("constructs only direct tools and suppresses production prompts and auxiliary calls", async () => {
100
+ const args = input();
101
+ const agent = new OffensiveSecurityAgent(args);
102
+ await agent.consume();
103
+ const stream = observed.streams[0];
104
+ expect(Object.keys(stream.tools ?? {}).sort()).toEqual(
105
+ [
106
+ ...LANCE_CONFIG.tools,
107
+ "submit_objective",
108
+ "finish_task",
109
+ "finish_unsolved",
110
+ ].sort(),
111
+ );
112
+ expect(stream.primaryModelOnly).toBe(true);
113
+ expect(stream.runContextCompaction).toBe(
114
+ agent.lanceRuntime!.runContextCompaction,
115
+ );
116
+ expect(stream.system).toContain(LANCE_SYSTEM_PROMPT);
117
+ expect(stream.system).not.toContain("Session Workspace");
118
+ expect(args.sandbox.execute).not.toHaveBeenCalled();
119
+ expect(() => new OffensiveSecurityAgent(args)).toThrow("terminal");
120
+ expect(
121
+ () => new OffensiveSecurityAgent({ ...args, mode: "default" }),
122
+ ).not.toThrow();
123
+ stream.onInvalidToolCall?.("spawn_pentest_agent");
124
+ expect(
125
+ agent.lanceRuntime!.metrics("fixture").policyViolations,
126
+ ).toMatchObject([{ kind: "forbidden_tool", name: "spawn_pentest_agent" }]);
127
+ });
128
+
129
+ it.each([
130
+ "extraTools",
131
+ "system",
132
+ "subagentSpawner",
133
+ "responseSchema",
134
+ "resolveResult",
135
+ "subagentId",
136
+ ])("rejects a %s override", (key) => {
137
+ expect(
138
+ () => new OffensiveSecurityAgent({ ...input(), [key]: {} } as never),
139
+ ).toThrow("does not permit");
140
+ });
141
+
142
+ it("retains command scope and destructive-action guards", async () => {
143
+ const args = input();
144
+ args.session.config = {
145
+ scopeConstraints: { allowedHosts: ["example.test"], strictScope: true },
146
+ } as never;
147
+ const agent = new OffensiveSecurityAgent(args);
148
+ await agent.streamReady();
149
+ const tools = observed.streams[0].tools!;
150
+ const result = await tools.execute_command.execute!(
151
+ { command: "curl https://outside.test", toolCallDescription: "probe" },
152
+ { toolCallId: "test", messages: [] },
153
+ );
154
+ expect(JSON.stringify(result)).toMatch(/scope/i);
155
+ const destructive = await tools.execute_command.execute!(
156
+ {
157
+ command: "curl -X DELETE https://example.test/resource",
158
+ toolCallDescription: "destructive guard fixture",
159
+ },
160
+ { toolCallId: "destructive", messages: [] },
161
+ );
162
+ expect(JSON.stringify(destructive)).toMatch(/Destructive action blocked/);
163
+ expect(args.sandbox.execute).not.toHaveBeenCalled();
164
+ agent.lanceRuntime!.finish();
165
+ });
166
+
167
+ it("keeps compaction out of the agent step limit while retaining usage callbacks", async () => {
168
+ const onStepFinish = vi.fn();
169
+ const agent = new OffensiveSecurityAgent({ ...input(), onStepFinish });
170
+ await agent.streamReady();
171
+ const onStep = observed.streams[0].onStepFinish!;
172
+ const event = (id: string) =>
173
+ ({
174
+ response: { id, messages: [] },
175
+ usage: { inputTokens: 2, outputTokens: 1 },
176
+ }) as unknown as Parameters<typeof onStep>[0];
177
+ for (let index = 0; index < LANCE_CONFIG.maxSteps - 1; index++) {
178
+ await onStep(event(`agent-${index}`));
179
+ }
180
+ await onStep(event("summarization"));
181
+ await onStep(event("tool-repair"));
182
+ expect(agent.lanceRuntime!.terminal).toBe(false);
183
+ expect(onStepFinish).toHaveBeenCalledTimes(LANCE_CONFIG.maxSteps + 1);
184
+ await onStep(event("agent-final"));
185
+ expect(agent.lanceRuntime!.metrics("fixture").outcome).toBe("step_limit");
186
+ await agent.consume();
187
+ });
188
+ });
@@ -361,6 +361,86 @@ describe("spawned-agent usage callbacks", () => {
361
361
  });
362
362
 
363
363
  describe("auxiliary model events", () => {
364
+ it("persists prepared context instead of restoring a fitted-away prefix", async () => {
365
+ streamResponseCalls.length = 0;
366
+ const rootPath = mkdtempSync(join(tmpdir(), "apex-fitted-history-"));
367
+ try {
368
+ const agent = new OffensiveSecurityAgent({
369
+ prompt: "test",
370
+ model: "test-model",
371
+ messages: [{ role: "user", content: "old large prefix" }],
372
+ session: { id: "ses_fitted_history", rootPath },
373
+ activeTools: [],
374
+ sandbox: {},
375
+ } as never);
376
+ void agent.streamResult;
377
+ const call = streamResponseCalls[0] as {
378
+ onContextPrepared: (context: {
379
+ messages: Array<{ role: string; content: unknown }>;
380
+ responseMessageOffset: number;
381
+ }) => void;
382
+ onStepFinish: (event: unknown) => Promise<void>;
383
+ };
384
+ const firstStep = [
385
+ { role: "assistant", content: "first call" },
386
+ { role: "tool", content: "full first result" },
387
+ ];
388
+ const cachedSystem = {
389
+ role: "system",
390
+ content: "cached effective system prompt",
391
+ providerOptions: {
392
+ anthropic: { cacheControl: { type: "ephemeral" } },
393
+ },
394
+ };
395
+ const historySystem = {
396
+ role: "system",
397
+ content: "caller-supplied history instruction",
398
+ };
399
+ call.onContextPrepared({
400
+ messages: [
401
+ cachedSystem,
402
+ cachedSystem,
403
+ historySystem,
404
+ { role: "user", content: "fitted prefix" },
405
+ ],
406
+ responseMessageOffset: 0,
407
+ });
408
+ await call.onStepFinish({
409
+ response: { id: "step-1", messages: firstStep },
410
+ usage: { inputTokens: 10, outputTokens: 2 },
411
+ });
412
+
413
+ const compacted = [
414
+ historySystem,
415
+ { role: "user", content: "fitted prefix" },
416
+ { role: "assistant", content: "first call" },
417
+ { role: "tool", content: "short first result" },
418
+ ];
419
+ call.onContextPrepared({
420
+ messages: [cachedSystem, cachedSystem, ...compacted],
421
+ responseMessageOffset: 2,
422
+ });
423
+ const secondStep = [
424
+ { role: "assistant", content: "second call" },
425
+ { role: "tool", content: "second result" },
426
+ ];
427
+ await call.onStepFinish({
428
+ response: {
429
+ id: "step-2",
430
+ messages: [...firstStep, ...secondStep],
431
+ },
432
+ usage: { inputTokens: 10, outputTokens: 2 },
433
+ });
434
+
435
+ const writer = (agent as unknown as { writer: AgentMessageWriter })
436
+ .writer;
437
+ expect(writer.latest).toEqual([...compacted, ...secondStep]);
438
+ writer.cancelTimer();
439
+ } finally {
440
+ rmSync(rootPath, { recursive: true, force: true });
441
+ }
442
+ });
443
+
364
444
  it("keeps the last complete message snapshot while forwarding usage", async () => {
365
445
  streamResponseCalls.length = 0;
366
446
  traceRecordStepCalls.length = 0;
@@ -2806,6 +2886,11 @@ describe("trace identity and root IO", () => {
2806
2886
  describe("contextRecorder integration", () => {
2807
2887
  const streamTextChunk = { type: "text-delta", id: "t1", delta: "hi" };
2808
2888
  const CANONICAL = [
2889
+ {
2890
+ role: "system",
2891
+ content: "cached effective system prompt",
2892
+ providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
2893
+ },
2809
2894
  { role: "user", content: "recorded prompt" },
2810
2895
  { role: "assistant", content: "recorded answer" },
2811
2896
  ] as never[];
@@ -2874,14 +2959,13 @@ describe("contextRecorder integration", () => {
2874
2959
  expect(streamResponseCalls[0]?.inferenceRecorder).toBe(inferenceRecorder);
2875
2960
 
2876
2961
  const stepEvent = {
2877
- response: { id: "resp_1", messages: CANONICAL.slice(1) },
2962
+ response: { id: "resp_1", messages: CANONICAL.slice(2) },
2878
2963
  usage: {},
2879
2964
  };
2880
2965
  await streamOpts.onStepFinish(stepEvent);
2881
2966
 
2882
- // Writer snapshot equals canonical latest() exactly — the step's
2883
- // response.messages are already part of the committed context, so the
2884
- // projection must not re-prepend a base or duplicate them.
2967
+ // The cached system message is transport-only; it must not enter the
2968
+ // transcript reloaded with a fresh system prompt on the next turn.
2885
2969
  const writer = agent["writer" as keyof typeof agent] as unknown as {
2886
2970
  waitForPendingWrites: () => Promise<void>;
2887
2971
  latest: unknown[] | null;
@@ -2894,6 +2978,9 @@ describe("contextRecorder integration", () => {
2894
2978
  await streamOpts.onFinish(finishEvent);
2895
2979
 
2896
2980
  expect(recorder.latest).toHaveBeenCalled();
2897
- expect((captured as unknown[]).length).toBe(CANONICAL.length);
2981
+ expect(captured).toEqual(CANONICAL.slice(1));
2982
+ expect(
2983
+ JSON.parse(readFileSync(join(rootPath, "messages.json"), "utf8")),
2984
+ ).toEqual(CANONICAL.slice(1));
2898
2985
  });
2899
2986
  });