riffer-rig 0.12.0 → 0.13.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 (303) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +129 -0
  3. data/README.md +12 -12
  4. data/lib/riffer/rig/acp/host.rb +67 -24
  5. data/lib/riffer/rig/acp/mapper.rb +242 -0
  6. data/lib/riffer/rig/acp.rb +125 -122
  7. data/lib/riffer/rig/bundled/bash.rb +5 -1
  8. data/lib/riffer/rig/cli/flags.rb +52 -32
  9. data/lib/riffer/rig/cli/sessions.rb +52 -29
  10. data/lib/riffer/rig/cli.rb +24 -13
  11. data/lib/riffer/rig/command/context.rb +1 -1
  12. data/lib/riffer/rig/command.rb +4 -2
  13. data/lib/riffer/rig/commands/auth.rb +23 -23
  14. data/lib/riffer/rig/commands/cost.rb +59 -0
  15. data/lib/riffer/rig/commands/model.rb +24 -18
  16. data/lib/riffer/rig/commands/reload.rb +1 -1
  17. data/lib/riffer/rig/commands/skill.rb +1 -1
  18. data/lib/riffer/rig/commands/trace/replay.rb +88 -0
  19. data/lib/riffer/rig/commands/trace.rb +25 -0
  20. data/lib/riffer/rig/credentials/provider_fields.rb +47 -0
  21. data/lib/riffer/rig/credentials.rb +94 -48
  22. data/lib/riffer/rig/env.rb +1 -1
  23. data/lib/riffer/rig/events/after_response.rb +4 -11
  24. data/lib/riffer/rig/events/after_tool_call.rb +15 -13
  25. data/lib/riffer/rig/events/before_prompt.rb +4 -11
  26. data/lib/riffer/rig/events/before_request.rb +4 -11
  27. data/lib/riffer/rig/events/before_tool_call.rb +12 -13
  28. data/lib/riffer/rig/events/command_output.rb +4 -11
  29. data/lib/riffer/rig/events/job_exit.rb +28 -0
  30. data/lib/riffer/rig/events/notify.rb +4 -11
  31. data/lib/riffer/rig/events/session_end.rb +4 -11
  32. data/lib/riffer/rig/events/session_start.rb +4 -11
  33. data/lib/riffer/rig/events/skill_activated.rb +4 -11
  34. data/lib/riffer/rig/events/turn_end.rb +4 -11
  35. data/lib/riffer/rig/extension.rb +5 -4
  36. data/lib/riffer/rig/headless/host.rb +2 -14
  37. data/lib/riffer/rig/headless/ndjson.rb +1 -1
  38. data/lib/riffer/rig/headless.rb +120 -26
  39. data/lib/riffer/rig/hosts/mirror.rb +8 -5
  40. data/lib/riffer/rig/hosts/non_interactive.rb +24 -0
  41. data/lib/riffer/rig/hosts/null.rb +2 -21
  42. data/lib/riffer/rig/loader/credentials.rb +23 -0
  43. data/lib/riffer/rig/loader/extension_source.rb +177 -0
  44. data/lib/riffer/rig/loader/options.rb +35 -0
  45. data/lib/riffer/rig/loader.rb +96 -329
  46. data/lib/riffer/rig/mcp/auth.rb +3 -1
  47. data/lib/riffer/rig/mcp/declaration.rb +1 -1
  48. data/lib/riffer/rig/provider_setup.rb +2 -2
  49. data/lib/riffer/rig/providers.rb +5 -0
  50. data/lib/riffer/rig/registrar.rb +1 -1
  51. data/lib/riffer/rig/runtime/abort.rb +66 -0
  52. data/lib/riffer/rig/runtime/concurrency.rb +71 -0
  53. data/lib/riffer/rig/runtime/error_message.rb +27 -0
  54. data/lib/riffer/rig/runtime/hooks.rb +6 -2
  55. data/lib/riffer/rig/runtime/job_completion.rb +31 -0
  56. data/lib/riffer/rig/runtime/job_envelope.rb +36 -0
  57. data/lib/riffer/rig/runtime/job_inbox.rb +74 -0
  58. data/lib/riffer/rig/runtime/job_notification.rb +31 -0
  59. data/lib/riffer/rig/runtime/mcp_registrar.rb +91 -0
  60. data/lib/riffer/rig/runtime/request_guardrail.rb +1 -1
  61. data/lib/riffer/rig/runtime/snapshot.rb +80 -0
  62. data/lib/riffer/rig/runtime/system_prompt.rb +56 -0
  63. data/lib/riffer/rig/runtime/tool_runtime.rb +47 -7
  64. data/lib/riffer/rig/runtime/usage.rb +19 -0
  65. data/lib/riffer/rig/runtime.rb +329 -243
  66. data/lib/riffer/rig/sdk.rb +29 -7
  67. data/lib/riffer/rig/settings/file.rb +27 -0
  68. data/lib/riffer/rig/settings/model_options.rb +64 -0
  69. data/lib/riffer/rig/settings.rb +0 -129
  70. data/lib/riffer/rig/stores/entries.rb +23 -0
  71. data/lib/riffer/rig/stores/header.rb +3 -17
  72. data/lib/riffer/rig/stores/jsonl.rb +7 -6
  73. data/lib/riffer/rig/stores/message_entry.rb +10 -10
  74. data/lib/riffer/rig/stores/model_entry.rb +10 -10
  75. data/lib/riffer/rig/stores/recorder.rb +34 -13
  76. data/lib/riffer/rig/stores/skill_entry.rb +10 -10
  77. data/lib/riffer/rig/stores/stamped.rb +9 -0
  78. data/lib/riffer/rig/stores/timestamp.rb +23 -0
  79. data/lib/riffer/rig/support/json_file.rb +53 -0
  80. data/lib/riffer/rig/support/typed_value/schema.rb +16 -0
  81. data/lib/riffer/rig/support/typed_value.rb +17 -0
  82. data/lib/riffer/rig/terminal/autocomplete.rb +133 -0
  83. data/lib/riffer/rig/terminal/banner.rb +48 -11
  84. data/lib/riffer/rig/terminal/blocking_prompter.rb +37 -0
  85. data/lib/riffer/rig/terminal/builtin.rb +16 -0
  86. data/lib/riffer/rig/terminal/columns.rb +63 -0
  87. data/lib/riffer/rig/terminal/console.rb +49 -0
  88. data/lib/riffer/rig/terminal/echo.rb +26 -0
  89. data/lib/riffer/rig/terminal/equalizer.rb +20 -0
  90. data/lib/riffer/rig/terminal/frame.rb +339 -0
  91. data/lib/riffer/rig/terminal/frame_prompter.rb +63 -0
  92. data/lib/riffer/rig/terminal/help.rb +41 -0
  93. data/lib/riffer/rig/terminal/host.rb +7 -28
  94. data/lib/riffer/rig/terminal/job_counter.rb +33 -0
  95. data/lib/riffer/rig/terminal/job_exit_notice.rb +11 -0
  96. data/lib/riffer/rig/terminal/key.rb +38 -0
  97. data/lib/riffer/rig/terminal/key_decoder.rb +219 -0
  98. data/lib/riffer/rig/terminal/key_pipeline.rb +111 -0
  99. data/lib/riffer/rig/terminal/line_editor.rb +183 -0
  100. data/lib/riffer/rig/terminal/markdown/blocks.rb +96 -0
  101. data/lib/riffer/rig/terminal/markdown/flow.rb +77 -0
  102. data/lib/riffer/rig/terminal/markdown/inline.rb +85 -0
  103. data/lib/riffer/rig/terminal/markdown/span.rb +25 -0
  104. data/lib/riffer/rig/terminal/markdown/table.rb +166 -0
  105. data/lib/riffer/rig/terminal/markdown.rb +187 -0
  106. data/lib/riffer/rig/terminal/message_queue/message.rb +13 -0
  107. data/lib/riffer/rig/terminal/message_queue.rb +27 -0
  108. data/lib/riffer/rig/terminal/opening.rb +50 -0
  109. data/lib/riffer/rig/terminal/peek.rb +74 -0
  110. data/lib/riffer/rig/terminal/picker.rb +47 -22
  111. data/lib/riffer/rig/terminal/question.rb +37 -0
  112. data/lib/riffer/rig/terminal/quiet_display.rb +48 -0
  113. data/lib/riffer/rig/terminal/renderer.rb +19 -250
  114. data/lib/riffer/rig/terminal/repl.rb +242 -0
  115. data/lib/riffer/rig/terminal/replay/turn.rb +23 -0
  116. data/lib/riffer/rig/terminal/replay.rb +76 -0
  117. data/lib/riffer/rig/terminal/session.rb +2 -6
  118. data/lib/riffer/rig/terminal/tally.rb +12 -0
  119. data/lib/riffer/rig/terminal/theme.rb +34 -1
  120. data/lib/riffer/rig/terminal/ticker.rb +189 -0
  121. data/lib/riffer/rig/terminal/ticker_display.rb +91 -0
  122. data/lib/riffer/rig/terminal/turn_buffer/stats.rb +19 -0
  123. data/lib/riffer/rig/terminal/turn_buffer.rb +133 -0
  124. data/lib/riffer/rig/terminal/turn_report.rb +100 -0
  125. data/lib/riffer/rig/terminal/wrapper.rb +46 -62
  126. data/lib/riffer/rig/terminal.rb +192 -104
  127. data/lib/riffer/rig/tools/bash/capture.rb +58 -0
  128. data/lib/riffer/rig/tools/bash/job.rb +84 -0
  129. data/lib/riffer/rig/tools/bash/job_log.rb +117 -0
  130. data/lib/riffer/rig/tools/bash/jobs.rb +197 -0
  131. data/lib/riffer/rig/tools/bash/kill.rb +24 -0
  132. data/lib/riffer/rig/tools/bash/kill_group.rb +15 -0
  133. data/lib/riffer/rig/tools/bash/monotonic_clock.rb +8 -0
  134. data/lib/riffer/rig/tools/bash/output.rb +30 -0
  135. data/lib/riffer/rig/tools/bash/truncation.rb +12 -0
  136. data/lib/riffer/rig/tools/bash.rb +42 -29
  137. data/lib/riffer/rig/tools/read.rb +2 -2
  138. data/lib/riffer/rig/trust.rb +9 -18
  139. data/lib/riffer/rig/version.rb +1 -1
  140. data/sig/generated/riffer/rig/acp/host.rbs +29 -8
  141. data/sig/generated/riffer/rig/acp/mapper.rbs +108 -0
  142. data/sig/generated/riffer/rig/acp.rbs +46 -53
  143. data/sig/generated/riffer/rig/cli/flags.rbs +17 -18
  144. data/sig/generated/riffer/rig/cli/sessions.rbs +25 -11
  145. data/sig/generated/riffer/rig/cli.rbs +6 -3
  146. data/sig/generated/riffer/rig/command.rbs +5 -3
  147. data/sig/generated/riffer/rig/commands/auth.rbs +3 -7
  148. data/sig/generated/riffer/rig/commands/cost.rbs +31 -0
  149. data/sig/generated/riffer/rig/commands/model.rbs +2 -0
  150. data/sig/generated/riffer/rig/commands/trace/replay.rbs +53 -0
  151. data/sig/generated/riffer/rig/commands/trace.rbs +14 -0
  152. data/sig/generated/riffer/rig/credentials/provider_fields.rbs +26 -0
  153. data/sig/generated/riffer/rig/credentials.rbs +36 -15
  154. data/sig/generated/riffer/rig/events/after_response.rbs +3 -7
  155. data/sig/generated/riffer/rig/events/after_tool_call.rbs +14 -9
  156. data/sig/generated/riffer/rig/events/before_prompt.rbs +3 -7
  157. data/sig/generated/riffer/rig/events/before_request.rbs +3 -7
  158. data/sig/generated/riffer/rig/events/before_tool_call.rbs +11 -9
  159. data/sig/generated/riffer/rig/events/command_output.rbs +3 -7
  160. data/sig/generated/riffer/rig/events/job_exit.rbs +24 -0
  161. data/sig/generated/riffer/rig/events/notify.rbs +3 -7
  162. data/sig/generated/riffer/rig/events/session_end.rbs +3 -7
  163. data/sig/generated/riffer/rig/events/session_start.rbs +3 -7
  164. data/sig/generated/riffer/rig/events/skill_activated.rbs +3 -7
  165. data/sig/generated/riffer/rig/events/turn_end.rbs +3 -7
  166. data/sig/generated/riffer/rig/extension.rbs +3 -3
  167. data/sig/generated/riffer/rig/headless/host.rbs +2 -10
  168. data/sig/generated/riffer/rig/headless.rbs +41 -10
  169. data/sig/generated/riffer/rig/hosts/mirror.rbs +7 -6
  170. data/sig/generated/riffer/rig/hosts/non_interactive.rbs +18 -0
  171. data/sig/generated/riffer/rig/hosts/null.rbs +2 -15
  172. data/sig/generated/riffer/rig/loader/credentials.rbs +12 -0
  173. data/sig/generated/riffer/rig/loader/extension_source.rbs +89 -0
  174. data/sig/generated/riffer/rig/loader/options.rbs +25 -0
  175. data/sig/generated/riffer/rig/loader.rbs +28 -134
  176. data/sig/generated/riffer/rig/provider_setup.rbs +1 -1
  177. data/sig/generated/riffer/rig/providers.rbs +3 -0
  178. data/sig/generated/riffer/rig/runtime/abort.rbs +36 -0
  179. data/sig/generated/riffer/rig/runtime/concurrency.rbs +41 -0
  180. data/sig/generated/riffer/rig/runtime/error_message.rbs +13 -0
  181. data/sig/generated/riffer/rig/runtime/hooks.rbs +3 -1
  182. data/sig/generated/riffer/rig/runtime/job_completion.rbs +28 -0
  183. data/sig/generated/riffer/rig/runtime/job_envelope.rbs +17 -0
  184. data/sig/generated/riffer/rig/runtime/job_inbox.rbs +40 -0
  185. data/sig/generated/riffer/rig/runtime/job_notification.rbs +22 -0
  186. data/sig/generated/riffer/rig/runtime/mcp_registrar.rbs +46 -0
  187. data/sig/generated/riffer/rig/runtime/snapshot.rbs +42 -0
  188. data/sig/generated/riffer/rig/runtime/system_prompt.rbs +37 -0
  189. data/sig/generated/riffer/rig/runtime/tool_runtime.rbs +25 -1
  190. data/sig/generated/riffer/rig/runtime/usage.rbs +11 -0
  191. data/sig/generated/riffer/rig/runtime.rbs +130 -92
  192. data/sig/generated/riffer/rig/sdk.rbs +11 -1
  193. data/sig/generated/riffer/rig/settings/file.rbs +19 -0
  194. data/sig/generated/riffer/rig/settings/model_options.rbs +31 -0
  195. data/sig/generated/riffer/rig/settings.rbs +0 -66
  196. data/sig/generated/riffer/rig/stores/entries.rbs +12 -0
  197. data/sig/generated/riffer/rig/stores/header.rbs +2 -5
  198. data/sig/generated/riffer/rig/stores/message_entry.rbs +9 -7
  199. data/sig/generated/riffer/rig/stores/model_entry.rbs +9 -7
  200. data/sig/generated/riffer/rig/stores/recorder.rbs +16 -4
  201. data/sig/generated/riffer/rig/stores/skill_entry.rbs +9 -7
  202. data/sig/generated/riffer/rig/stores/stamped.rbs +7 -0
  203. data/sig/generated/riffer/rig/stores/timestamp.rbs +13 -0
  204. data/sig/generated/riffer/rig/support/json_file.rbs +23 -0
  205. data/sig/generated/riffer/rig/support/typed_value/schema.rbs +14 -0
  206. data/sig/generated/riffer/rig/support/typed_value.rbs +13 -0
  207. data/sig/generated/riffer/rig/terminal/autocomplete.rbs +72 -0
  208. data/sig/generated/riffer/rig/terminal/banner.rbs +25 -4
  209. data/sig/generated/riffer/rig/terminal/blocking_prompter.rbs +23 -0
  210. data/sig/generated/riffer/rig/terminal/builtin.rbs +13 -0
  211. data/sig/generated/riffer/rig/terminal/columns.rbs +43 -0
  212. data/sig/generated/riffer/rig/terminal/console.rbs +32 -0
  213. data/sig/generated/riffer/rig/terminal/echo.rbs +15 -0
  214. data/sig/generated/riffer/rig/terminal/equalizer.rbs +12 -0
  215. data/sig/generated/riffer/rig/terminal/frame.rbs +169 -0
  216. data/sig/generated/riffer/rig/terminal/frame_prompter.rbs +34 -0
  217. data/sig/generated/riffer/rig/terminal/help.rbs +22 -0
  218. data/sig/generated/riffer/rig/terminal/host.rbs +5 -11
  219. data/sig/generated/riffer/rig/terminal/job_counter.rbs +21 -0
  220. data/sig/generated/riffer/rig/terminal/job_exit_notice.rbs +7 -0
  221. data/sig/generated/riffer/rig/terminal/key.rbs +27 -0
  222. data/sig/generated/riffer/rig/terminal/key_decoder.rbs +81 -0
  223. data/sig/generated/riffer/rig/terminal/key_pipeline.rbs +54 -0
  224. data/sig/generated/riffer/rig/terminal/line_editor.rbs +108 -0
  225. data/sig/generated/riffer/rig/terminal/markdown/blocks.rbs +57 -0
  226. data/sig/generated/riffer/rig/terminal/markdown/flow.rbs +42 -0
  227. data/sig/generated/riffer/rig/terminal/markdown/inline.rbs +41 -0
  228. data/sig/generated/riffer/rig/terminal/markdown/span.rbs +20 -0
  229. data/sig/generated/riffer/rig/terminal/markdown/table.rbs +101 -0
  230. data/sig/generated/riffer/rig/terminal/markdown.rbs +112 -0
  231. data/sig/generated/riffer/rig/terminal/message_queue/message.rbs +10 -0
  232. data/sig/generated/riffer/rig/terminal/message_queue.rbs +17 -0
  233. data/sig/generated/riffer/rig/terminal/opening.rbs +33 -0
  234. data/sig/generated/riffer/rig/terminal/peek.rbs +37 -0
  235. data/sig/generated/riffer/rig/terminal/picker.rbs +21 -5
  236. data/sig/generated/riffer/rig/terminal/question.rbs +21 -0
  237. data/sig/generated/riffer/rig/terminal/quiet_display.rbs +33 -0
  238. data/sig/generated/riffer/rig/terminal/renderer.rbs +10 -122
  239. data/sig/generated/riffer/rig/terminal/repl.rbs +116 -0
  240. data/sig/generated/riffer/rig/terminal/replay/turn.rbs +20 -0
  241. data/sig/generated/riffer/rig/terminal/replay.rbs +29 -0
  242. data/sig/generated/riffer/rig/terminal/session.rbs +2 -7
  243. data/sig/generated/riffer/rig/terminal/tally.rbs +7 -0
  244. data/sig/generated/riffer/rig/terminal/theme.rbs +23 -2
  245. data/sig/generated/riffer/rig/terminal/ticker.rbs +107 -0
  246. data/sig/generated/riffer/rig/terminal/ticker_display.rbs +48 -0
  247. data/sig/generated/riffer/rig/terminal/turn_buffer/stats.rbs +16 -0
  248. data/sig/generated/riffer/rig/terminal/turn_buffer.rbs +67 -0
  249. data/sig/generated/riffer/rig/terminal/turn_report.rbs +52 -0
  250. data/sig/generated/riffer/rig/terminal/wrapper.rbs +16 -14
  251. data/sig/generated/riffer/rig/terminal.rbs +55 -26
  252. data/sig/generated/riffer/rig/tools/bash/capture.rbs +39 -0
  253. data/sig/generated/riffer/rig/tools/bash/job.rbs +65 -0
  254. data/sig/generated/riffer/rig/tools/bash/job_log.rbs +60 -0
  255. data/sig/generated/riffer/rig/tools/bash/jobs.rbs +86 -0
  256. data/sig/generated/riffer/rig/tools/bash/kill.rbs +8 -0
  257. data/sig/generated/riffer/rig/tools/bash/kill_group.rbs +7 -0
  258. data/sig/generated/riffer/rig/tools/bash/monotonic_clock.rbs +6 -0
  259. data/sig/generated/riffer/rig/tools/bash/output.rbs +10 -0
  260. data/sig/generated/riffer/rig/tools/bash/truncation.rbs +7 -0
  261. data/sig/generated/riffer/rig/tools/bash.rbs +15 -13
  262. data/sig/generated/riffer/rig/trust.rbs +7 -8
  263. data/sig/manual/riffer/rig/acp/mapper.rbs +6 -0
  264. data/sig/manual/riffer/rig/commands/cost.rbs +6 -0
  265. data/sig/manual/riffer/rig/commands/trace/replay.rbs +6 -0
  266. data/sig/manual/riffer/rig/commands/trace.rbs +6 -0
  267. data/sig/manual/riffer/rig/credentials/provider_fields.rbs +6 -0
  268. data/sig/manual/riffer/rig/hosts.rbs +1 -1
  269. data/sig/manual/riffer/rig/loader/credentials.rbs +6 -0
  270. data/sig/manual/riffer/rig/runtime/error_message.rbs +6 -0
  271. data/sig/manual/riffer/rig/runtime/job_envelope.rbs +6 -0
  272. data/sig/manual/riffer/rig/runtime/usage.rbs +6 -0
  273. data/sig/manual/riffer/rig/sdk.rbs +1 -1
  274. data/sig/manual/riffer/rig/settings/file.rbs +6 -0
  275. data/sig/manual/riffer/rig/settings/model_options.rbs +6 -0
  276. data/sig/manual/riffer/rig/stores/entries.rbs +6 -0
  277. data/sig/manual/riffer/rig/stores/timestamp.rbs +6 -0
  278. data/sig/manual/riffer/rig/stores.rbs +8 -0
  279. data/sig/manual/riffer/rig/support/json_file.rbs +6 -0
  280. data/sig/manual/riffer/rig/support.rbs +10 -0
  281. data/sig/manual/riffer/rig/terminal/autocomplete.rbs +8 -0
  282. data/sig/manual/riffer/rig/terminal/columns.rbs +6 -0
  283. data/sig/manual/riffer/rig/terminal/echo.rbs +6 -0
  284. data/sig/manual/riffer/rig/terminal/equalizer.rbs +6 -0
  285. data/sig/manual/riffer/rig/terminal/help.rbs +6 -0
  286. data/sig/manual/riffer/rig/terminal/job_exit_notice.rbs +6 -0
  287. data/sig/manual/riffer/rig/terminal/markdown/blocks.rbs +6 -0
  288. data/sig/manual/riffer/rig/terminal/markdown/flow.rbs +6 -0
  289. data/sig/manual/riffer/rig/terminal/markdown/inline.rbs +6 -0
  290. data/sig/manual/riffer/rig/terminal/markdown/table.rbs +6 -0
  291. data/sig/manual/riffer/rig/terminal/markdown.rbs +6 -0
  292. data/sig/manual/riffer/rig/terminal/question.rbs +6 -0
  293. data/sig/manual/riffer/rig/terminal/replay.rbs +14 -0
  294. data/sig/manual/riffer/rig/terminal/tally.rbs +6 -0
  295. data/sig/manual/riffer/rig/terminal/turn_report.rbs +14 -0
  296. data/sig/manual/riffer/rig/terminal.rbs +29 -4
  297. metadata +184 -11
  298. data/lib/riffer/rig/terminal/animator.rb +0 -139
  299. data/lib/riffer/rig/terminal/cursor.rb +0 -44
  300. data/lib/riffer/rig/terminal/smoother.rb +0 -126
  301. data/sig/generated/riffer/rig/terminal/animator.rbs +0 -68
  302. data/sig/generated/riffer/rig/terminal/cursor.rbs +0 -31
  303. data/sig/generated/riffer/rig/terminal/smoother.rbs +0 -59
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riffer::Rig::ACP::Mapper
4
+ extend self
5
+
6
+ TOOL_KINDS = {
7
+ 'read' => ::ACP::Types::ToolKind::READ,
8
+ 'write' => ::ACP::Types::ToolKind::EDIT,
9
+ 'edit' => ::ACP::Types::ToolKind::EDIT,
10
+ 'bash' => ::ACP::Types::ToolKind::EXECUTE
11
+ }.freeze #: Hash[String, String]
12
+
13
+ CONFIRMED = 'yes' #: String
14
+ CONFIRM_OPTIONS = [
15
+ ::ACP::Types::PermissionOption.new(
16
+ option_id: CONFIRMED, name: 'Yes', kind: ::ACP::Types::PermissionOptionKind::ALLOW_ONCE
17
+ ),
18
+ ::ACP::Types::PermissionOption.new(option_id: 'no', name: 'No', kind: ::ACP::Types::PermissionOptionKind::REJECT_ONCE)
19
+ ].freeze #: Array[::ACP::Types::PermissionOption]
20
+ ANSWER_FIELD = 'answer' #: String
21
+
22
+ # Text and resource links are ACP's must-support prompt blocks; the client
23
+ # cannot send the others unless the agent advertised them.
24
+ # @rbs blocks: Array[::ACP::Types::ContentBlock::t]
25
+ # @rbs return: String
26
+ def prompt_text(blocks)
27
+ blocks.filter_map { |block| prompt_text_of(block) }.join("\n\n")
28
+ end
29
+
30
+ # @rbs content: String
31
+ # @rbs return: ::ACP::Types::SessionUpdate::AgentMessageChunk
32
+ def message_chunk(content)
33
+ ::ACP::Types::SessionUpdate::AgentMessageChunk.new(content: text_block(content))
34
+ end
35
+
36
+ # @rbs text: String
37
+ # @rbs return: ::ACP::Types::SessionUpdate::AgentMessageChunk
38
+ def notice(text)
39
+ # ACP has no update for a notice, and without blank lines around it a
40
+ # notice runs into the reply and into the next notice.
41
+ message_chunk("\n\n#{text}\n\n")
42
+ end
43
+
44
+ # @rbs return: ::ACP::Types::SessionUpdate::AgentMessageChunk
45
+ def job_exit
46
+ # ACP has no update for a job exit notice, and without a break the replies
47
+ # either side of one read as a single message.
48
+ message_chunk("\n\n")
49
+ end
50
+
51
+ # @rbs completion: Riffer::Rig::Runtime::JobCompletion
52
+ # @rbs return: ::ACP::Types::SessionUpdate::AgentMessageChunk
53
+ def job_completion(completion)
54
+ notice("Background job #{completion.job_id} #{ending(completion)}: #{completion.command}")
55
+ end
56
+
57
+ # @rbs message: Hash[Symbol, untyped]
58
+ # @rbs return: ::ACP::Types::SessionUpdate::t?
59
+ def replay_of(message)
60
+ return job_exit if message[Riffer::Rig::Runtime::JobNotification::MARKER]
61
+
62
+ case message[:role]
63
+ when 'user' then ::ACP::Types::SessionUpdate::UserMessageChunk.new(content: text_block(message[:content].to_s))
64
+ when 'assistant' then message_chunk(message[:content].to_s) unless message[:content].to_s.strip.empty?
65
+ when 'tool' then replayed_tool_call(message)
66
+ end
67
+ end
68
+
69
+ # @rbs event: Riffer::StreamEvents::ToolCallDone
70
+ # @rbs return: ::ACP::Types::SessionUpdate::ToolCall
71
+ def tool_call(event)
72
+ ::ACP::Types::SessionUpdate::ToolCall.new(
73
+ tool_call_id: event.call_id,
74
+ title: "#{event.name}(#{event.arguments})",
75
+ name: event.name,
76
+ kind: TOOL_KINDS.fetch(event.name, ::ACP::Types::ToolKind::OTHER),
77
+ status: ::ACP::Types::ToolCallStatus::IN_PROGRESS
78
+ )
79
+ end
80
+
81
+ # @rbs event: Riffer::Rig::Events::AfterToolCall
82
+ # @rbs return: ::ACP::Types::SessionUpdate::ToolCallUpdate
83
+ def tool_call_done(event)
84
+ result = event.result
85
+ ::ACP::Types::SessionUpdate::ToolCallUpdate.new(
86
+ tool_call_id: event.call_id,
87
+ status: result.error? ? ::ACP::Types::ToolCallStatus::FAILED : ::ACP::Types::ToolCallStatus::COMPLETED,
88
+ content: tool_content(result.content)
89
+ )
90
+ end
91
+
92
+ # riffer's failure reasons (:error, :other, :guardrail_blocked) have no ACP
93
+ # counterpart; refusal is the closest signal that the turn did not complete.
94
+ # @rbs stop_reason: Symbol
95
+ # @rbs return: String
96
+ def stop_reason_of(stop_reason)
97
+ case stop_reason
98
+ when :completed then ::ACP::Types::StopReason::END_TURN
99
+ when Riffer::Rig::Runtime::INTERRUPT_CANCELLED then ::ACP::Types::StopReason::CANCELLED
100
+ when :max_steps then ::ACP::Types::StopReason::MAX_TURN_REQUESTS
101
+ else
102
+ stopped = Riffer::Rig::Runtime::STOPPED_WITHOUT_COMPLETING.include?(stop_reason)
103
+ stopped ? ::ACP::Types::StopReason::MAX_TOKENS : ::ACP::Types::StopReason::REFUSAL
104
+ end
105
+ end
106
+
107
+ # @rbs options: Array[String]
108
+ # @rbs return: Array[::ACP::Types::PermissionOption]
109
+ def choices(options)
110
+ options.map do |option|
111
+ ::ACP::Types::PermissionOption.new(
112
+ option_id: option, name: option, kind: ::ACP::Types::PermissionOptionKind::ALLOW_ONCE
113
+ )
114
+ end
115
+ end
116
+
117
+ # @rbs tool_call_id: String
118
+ # @rbs question: String
119
+ # @rbs return: ::ACP::Types::SessionUpdate::ToolCall
120
+ def permission_call(tool_call_id, question)
121
+ ::ACP::Types::SessionUpdate::ToolCall.new(
122
+ tool_call_id: tool_call_id,
123
+ title: question,
124
+ kind: ::ACP::Types::ToolKind::OTHER,
125
+ status: ::ACP::Types::ToolCallStatus::PENDING
126
+ )
127
+ end
128
+
129
+ # @rbs session_id: String
130
+ # @rbs tool_call_id: String
131
+ # @rbs question: String
132
+ # @rbs options: Array[::ACP::Types::PermissionOption]
133
+ # @rbs return: ::ACP::Types::RequestPermissionRequest
134
+ def permission_request(session_id, tool_call_id, question, options)
135
+ ::ACP::Types::RequestPermissionRequest.new(
136
+ session_id: session_id,
137
+ tool_call: ::ACP::Types::ToolCallUpdate.new(
138
+ tool_call_id: tool_call_id,
139
+ title: question,
140
+ kind: ::ACP::Types::ToolKind::OTHER,
141
+ status: ::ACP::Types::ToolCallStatus::PENDING
142
+ ),
143
+ options: options
144
+ )
145
+ end
146
+
147
+ # @rbs response: ::ACP::Types::RequestPermissionResponse | ::ACP::RequestError
148
+ # @rbs return: String?
149
+ def selected_option(response)
150
+ return nil unless response.is_a?(::ACP::Types::RequestPermissionResponse)
151
+
152
+ outcome = response.outcome
153
+ outcome.option_id if outcome.is_a?(::ACP::Types::RequestPermissionOutcome::Selected)
154
+ end
155
+
156
+ # @rbs tool_call_id: String
157
+ # @rbs selected: String?
158
+ # @rbs return: ::ACP::Types::SessionUpdate::ToolCallUpdate
159
+ def permission_done(tool_call_id, selected)
160
+ ::ACP::Types::SessionUpdate::ToolCallUpdate.new(
161
+ tool_call_id: tool_call_id,
162
+ status: selected ? ::ACP::Types::ToolCallStatus::COMPLETED : ::ACP::Types::ToolCallStatus::FAILED
163
+ )
164
+ end
165
+
166
+ # @rbs session_id: String
167
+ # @rbs question: String
168
+ # @rbs return: ::ACP::Types::CreateElicitationRequest
169
+ def elicitation_request(session_id, question)
170
+ ::ACP::Types::CreateElicitationRequest.new(
171
+ message: question,
172
+ mode: ::ACP::Types::CreateElicitationRequest::Mode::Form.new(
173
+ requested_schema: ::ACP::Types::ElicitationSchema.new(
174
+ type: ::ACP::Types::ElicitationSchemaType::OBJECT,
175
+ properties: {
176
+ ANSWER_FIELD => ::ACP::Types::ElicitationPropertySchema::StringPropertySchema.new(title: question)
177
+ },
178
+ required: [ANSWER_FIELD]
179
+ ),
180
+ scope: ::ACP::Types::ElicitationSessionScope.new(session_id: session_id)
181
+ )
182
+ )
183
+ end
184
+
185
+ # @rbs response: ::ACP::Types::CreateElicitationResponse | ::ACP::RequestError
186
+ # @rbs return: String?
187
+ def elicited_answer(response)
188
+ return nil unless response.is_a?(::ACP::Types::CreateElicitationResponse)
189
+
190
+ action = response.action
191
+ return nil unless action.is_a?(::ACP::Types::CreateElicitationResponse::Action::Accept)
192
+
193
+ answer = action.content&.fetch(ANSWER_FIELD, nil)
194
+ answer if answer.is_a?(String)
195
+ end
196
+
197
+ private
198
+
199
+ # @rbs block: ::ACP::Types::ContentBlock::t
200
+ # @rbs return: String?
201
+ def prompt_text_of(block)
202
+ case block
203
+ when ::ACP::Types::ContentBlock::Text then block.text
204
+ when ::ACP::Types::ContentBlock::ResourceLink then "#{block.title || block.name} #{block.uri}"
205
+ end
206
+ end
207
+
208
+ # @rbs text: String
209
+ # @rbs return: ::ACP::Types::ContentBlock::Text
210
+ def text_block(text)
211
+ ::ACP::Types::ContentBlock::Text.new(text: text)
212
+ end
213
+
214
+ # @rbs completion: Riffer::Rig::Runtime::JobCompletion
215
+ # @rbs return: String
216
+ def ending(completion)
217
+ exit_status = completion.exit_status
218
+ exit_status ? "exited with status #{exit_status}" : "was killed by SIG#{completion.signal}"
219
+ end
220
+
221
+ # The stored result is all a replayed call has: the arguments live in the
222
+ # assistant message's tool_calls, so the title is the name alone.
223
+ # @rbs message: Hash[Symbol, untyped]
224
+ # @rbs return: ::ACP::Types::SessionUpdate::ToolCall
225
+ def replayed_tool_call(message)
226
+ name = message[:name].to_s
227
+ ::ACP::Types::SessionUpdate::ToolCall.new(
228
+ tool_call_id: message[:tool_call_id].to_s,
229
+ title: name,
230
+ name: name,
231
+ kind: TOOL_KINDS.fetch(name, ::ACP::Types::ToolKind::OTHER),
232
+ status: ::ACP::Types::ToolCallStatus::COMPLETED,
233
+ content: tool_content(message[:content].to_s)
234
+ )
235
+ end
236
+
237
+ # @rbs text: String
238
+ # @rbs return: Array[::ACP::Types::ToolCallContent::t]
239
+ def tool_content(text)
240
+ [::ACP::Types::ToolCallContent::Content.new(content: text_block(text))]
241
+ end
242
+ end
@@ -11,31 +11,29 @@ class Riffer::Rig::ACP
11
11
 
12
12
  SESSION_EXTENSION = 'acp' #: String
13
13
 
14
- TOOL_KINDS = {
15
- 'read' => ::ACP::Types::ToolKind::READ,
16
- 'write' => ::ACP::Types::ToolKind::EDIT,
17
- 'edit' => ::ACP::Types::ToolKind::EDIT,
18
- 'bash' => ::ACP::Types::ToolKind::EXECUTE
19
- }.freeze #: Hash[String, String]
20
-
21
14
  # @rbs @input: IO
22
15
  # @rbs @output: IO
23
16
  # @rbs @env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
24
17
  # @rbs @home: String
18
+ # @rbs @cwd: String
25
19
  # @rbs @build: ^(String, Riffer::Rig::Hosts::_Host, Hash[String, Riffer::Rig::Mcp::Declaration]) -> Riffer::Rig::Runtime
26
- # @rbs @client: ::ACP::AgentConnection::_Client?
20
+ # @rbs @adopt: (^(String) -> Riffer::Rig::Runtime?)?
21
+ # @rbs @client: ::ACP::AgentConnection::Client?
27
22
  # @rbs @sessions: Hash[String, Riffer::Rig::Runtime]
28
23
  # @rbs @hosts: Hash[String, Riffer::Rig::ACP::Host]
24
+ # @rbs @owned: Array[Riffer::Rig::Runtime]
29
25
  # @rbs @lock: Thread::Mutex
30
26
 
31
27
  # @rbs input: IO
32
28
  # @rbs output: IO
33
29
  # @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
34
30
  # @rbs home: String
31
+ # @rbs cwd: String
35
32
  # @rbs build: (^(String, Riffer::Rig::Hosts::_Host, Hash[String, Riffer::Rig::Mcp::Declaration]) -> Riffer::Rig::Runtime)?
33
+ # @rbs adopt: (^(String) -> Riffer::Rig::Runtime?)?
36
34
  # @rbs return: Riffer::Rig::ACP
37
- def self.for(input:, output:, env:, home:, build: nil)
38
- new(input: input, output: output, env: env, home: home, build: build)
35
+ def self.for(input:, output:, env:, home:, cwd:, build: nil, adopt: nil)
36
+ new(input: input, output: output, env: env, home: home, cwd: cwd, build: build, adopt: adopt)
39
37
  end
40
38
 
41
39
  # Builds the Runtime a session runs on: the client's MCP servers flow
@@ -49,42 +47,50 @@ class Riffer::Rig::ACP
49
47
  # @rbs home: String
50
48
  # @rbs return: Riffer::Rig::Runtime
51
49
  def self.build_runtime(cwd, host, servers, env:, home:)
52
- extension = session_extension(servers)
53
50
  Riffer::Rig::Loader.runtime(
54
- cwd: cwd, host: host, env: env, home: home, extra_extensions: extension ? [extension] : []
51
+ cwd: cwd, host: host, env: env, home: home, extra_extensions: session_extensions(servers)
55
52
  )
56
53
  end
57
54
 
58
55
  # @rbs servers: Hash[String, Riffer::Rig::Mcp::Declaration]
59
- # @rbs return: Riffer::Rig::Extension?
60
- def self.session_extension(servers)
61
- return nil if servers.empty?
56
+ # @rbs return: Array[Riffer::Rig::Extension]
57
+ def self.session_extensions(servers)
58
+ return [] if servers.empty?
62
59
 
63
- Riffer::Rig::Extension.new(SESSION_EXTENSION) do |rig|
60
+ extension = Riffer::Rig::Extension.new(SESSION_EXTENSION) do |rig|
64
61
  servers.each { |name, server| rig.mcp(name, command: server.command, args: server.args, env: server.env) }
65
62
  end
63
+ [extension]
66
64
  end
67
65
 
68
66
  # @rbs input: IO
69
67
  # @rbs output: IO
70
68
  # @rbs env: Riffer::Rig::Env | Riffer::Rig::Env::Invalid
71
69
  # @rbs home: String
70
+ # @rbs cwd: String
72
71
  # @rbs build: (^(String, Riffer::Rig::Hosts::_Host, Hash[String, Riffer::Rig::Mcp::Declaration]) -> Riffer::Rig::Runtime)?
72
+ # @rbs adopt: (^(String) -> Riffer::Rig::Runtime?)?
73
73
  # @rbs return: void
74
- def initialize(input:, output:, env:, home:, build: nil)
74
+ def initialize(input:, output:, env:, home:, cwd:, build: nil, adopt: nil)
75
75
  @input = input
76
76
  @output = output
77
77
  @env = env
78
78
  @home = home
79
- @build = build || ->(cwd, host, servers) { self.class.build_runtime(cwd, host, servers, env: @env, home: @home) }
79
+ @cwd = cwd
80
+ @build = build || lambda do |cwd, host, servers|
81
+ own(self.class.build_runtime(cwd, host, servers, env: @env, home: @home))
82
+ end
83
+ @adopt = adopt
80
84
  @client = nil
81
85
  @sessions = {}
82
86
  @hosts = {}
87
+ @owned = []
83
88
  @lock = Mutex.new
84
89
  end
85
90
 
86
91
  # Serves the connection until the client closes stdin; then the reader
87
92
  # thread ends and run returns.
93
+ # @rbs &block: ? (::ACP::AgentConnection) -> void
88
94
  # @rbs return: Integer
89
95
  def run
90
96
  connection = ::ACP::AgentConnection.new(
@@ -99,7 +105,16 @@ class Riffer::Rig::ACP
99
105
  self
100
106
  end
101
107
  connection.start.join
108
+ yield connection if block_given?
102
109
  0
110
+ ensure
111
+ # Background jobs run in process groups of their own, so only closing
112
+ # their Runtime stops them outliving this process. A turn still running is
113
+ # not waited for, so it is cancelled first to stop it starting another.
114
+ @lock.synchronize { @owned.dup }.each do |runtime|
115
+ runtime.cancel
116
+ runtime.close
117
+ end
103
118
  end
104
119
 
105
120
  # @rbs request: ::ACP::Types::NewSessionRequest
@@ -107,7 +122,7 @@ class Riffer::Rig::ACP
107
122
  def new_session(request)
108
123
  host = Riffer::Rig::ACP::Host.new(client: client)
109
124
  servers = declarations(request.mcp_servers, host)
110
- runtime = @build.call(request.cwd, host, servers)
125
+ runtime = announcing_job_exits(@build.call(request.cwd, host, servers), host)
111
126
  @lock.synchronize do
112
127
  @sessions[runtime.id] = runtime
113
128
  @hosts[runtime.id] = host
@@ -123,41 +138,58 @@ class Riffer::Rig::ACP
123
138
  runtime = session(request.session_id)
124
139
  return ::ACP::RequestError.resource_not_found unless runtime
125
140
 
126
- # @type var stop_reason: Symbol?
127
- stop_reason = nil
128
- runtime.prompt(prompt_text(request.prompt)) do |event|
141
+ commands = available_commands(runtime).to_set(&:to_h)
142
+ # A command that runs no turn sends no turn_end.
143
+ stop_reason = :completed
144
+ dispatch(runtime, Riffer::Rig::ACP::Mapper.prompt_text(request.prompt)) do |event|
129
145
  case event
130
146
  when Riffer::StreamEvents::TextDelta
131
- client.update(request.session_id, message_chunk(event.content))
147
+ client.update(request.session_id, Riffer::Rig::ACP::Mapper.message_chunk(event.content))
148
+ when Riffer::Rig::Events::CommandOutput
149
+ client.update(request.session_id, Riffer::Rig::ACP::Mapper.message_chunk(event.text))
132
150
  when Riffer::StreamEvents::ToolCallDone
133
- client.update(request.session_id, tool_call(event))
151
+ client.update(request.session_id, Riffer::Rig::ACP::Mapper.tool_call(event))
152
+ when Riffer::Rig::Events::AfterToolCall
153
+ client.update(request.session_id, Riffer::Rig::ACP::Mapper.tool_call_done(event))
154
+ when Riffer::Rig::Events::JobExit
155
+ client.update(request.session_id, Riffer::Rig::ACP::Mapper.job_exit)
134
156
  when Riffer::Rig::Events::TurnEnd
135
157
  stop_reason = event.stop_reason
136
158
  end
137
159
  end
138
- ::ACP::Types::PromptResponse.new(stop_reason: stop_reason_of(stop_reason))
139
- rescue Riffer::Rig::Runtime::BusyError, Riffer::Rig::Runtime::ClosedError => e
140
- ::ACP::RequestError.new(code: ::ACP::RequestError::INTERNAL_ERROR, message: e.message)
160
+ # A reload in the dispatch, by /reload or the automatic check, can change
161
+ # the commands; its rebuild also reorders them, which is no change.
162
+ reloaded = available_commands(runtime)
163
+ client.available_commands(request.session_id, reloaded) unless reloaded.to_set(&:to_h) == commands
164
+ ::ACP::Types::PromptResponse.new(stop_reason: Riffer::Rig::ACP::Mapper.stop_reason_of(stop_reason))
165
+ rescue Riffer::Rig::Runtime::BusyError
166
+ ::ACP::RequestError.new(
167
+ code: ::ACP::RequestError::INVALID_REQUEST,
168
+ message: 'a prompt is already running on this session; wait for it to finish or cancel it'
169
+ )
170
+ rescue StandardError => e
171
+ ::ACP::RequestError.new(
172
+ code: ::ACP::RequestError::INTERNAL_ERROR, message: Riffer::Rig::Runtime::ErrorMessage.of(e)
173
+ )
141
174
  end
142
175
 
143
- # The host takes the session id before the build, so a build-time notify
144
- # still reaches the client as an update for this session.
145
176
  # @rbs request: ::ACP::Types::LoadSessionRequest
146
177
  # @rbs return: (::ACP::Types::LoadSessionResponse | ::ACP::RequestError)
147
178
  def load_session(request)
148
179
  host = Riffer::Rig::ACP::Host.new(client: client)
149
- host.session_id = request.session_id
150
180
  loader = Riffer::Rig::Loader.new(cwd: request.cwd, host: host, env: @env, home: @home)
151
- runtime = loader.resume(request.session_id)
181
+ runtime = adopted(request.session_id) || resumed(loader, request, host)
152
182
  return ::ACP::RequestError.resource_not_found unless runtime
153
183
 
154
- @lock.synchronize do
155
- @sessions[request.session_id] = runtime
156
- @hosts[request.session_id] = host
157
- end
184
+ # A client need not answer a question while its own session/load is
185
+ # pending, and acp-sdk never times a request out, so the host takes the
186
+ # session id only once the build can no longer ask one.
187
+ host.session_id = request.session_id
188
+ @lock.synchronize { @sessions[request.session_id] = runtime }
158
189
  loader.messages(request.session_id)
159
- .filter_map { |message| replay_of(message) }
190
+ .filter_map { |message| Riffer::Rig::ACP::Mapper.replay_of(message) }
160
191
  .each { |update| client.update(request.session_id, update) }
192
+ client.available_commands(request.session_id, available_commands(runtime))
161
193
  ::ACP::Types::LoadSessionResponse.new
162
194
  rescue StandardError => e
163
195
  ::ACP::RequestError.new(code: ::ACP::RequestError::INTERNAL_ERROR, message: e.message)
@@ -167,7 +199,7 @@ class Riffer::Rig::ACP
167
199
  # @rbs return: (::ACP::Types::ListSessionsResponse | ::ACP::RequestError)
168
200
  def list_sessions(request)
169
201
  loader = Riffer::Rig::Loader.new(
170
- cwd: request.cwd || Dir.pwd,
202
+ cwd: request.cwd || @cwd,
171
203
  host: Riffer::Rig::Hosts::Null.new,
172
204
  env: @env,
173
205
  home: @home
@@ -186,7 +218,8 @@ class Riffer::Rig::ACP
186
218
  ::ACP::RequestError.new(code: ::ACP::RequestError::INTERNAL_ERROR, message: e.message)
187
219
  end
188
220
 
189
- # Runs on the transport's reader thread, so it only sets the cancel flag.
221
+ # Runs on the transport's reader thread, so it asks the turn to stop and
222
+ # never waits for it.
190
223
  # @rbs notification: ::ACP::Types::CancelNotification
191
224
  # @rbs return: void
192
225
  def cancel(notification)
@@ -211,7 +244,7 @@ class Riffer::Rig::ACP
211
244
 
212
245
  private
213
246
 
214
- # @rbs return: ::ACP::AgentConnection::_Client
247
+ # @rbs return: ::ACP::AgentConnection::Client
215
248
  def client
216
249
  @client or raise 'ACP agent used before ::ACP::AgentConnection.start built it'
217
250
  end
@@ -222,6 +255,59 @@ class Riffer::Rig::ACP
222
255
  @lock.synchronize { @sessions[id] }
223
256
  end
224
257
 
258
+ # @rbs runtime: Riffer::Rig::Runtime
259
+ # @rbs text: String
260
+ # @rbs &: (::Riffer::StreamEvents::Base | Riffer::Rig::Events::_Event) -> void
261
+ # @rbs return: void
262
+ def dispatch(runtime, text, &)
263
+ command = Riffer::Rig::Command::INVOCATION.match(text)
264
+ command ? runtime.run_command(command[1].to_s, command[2].to_s, &) : runtime.prompt(text, &)
265
+ end
266
+
267
+ # @rbs id: String
268
+ # @rbs return: Riffer::Rig::Runtime?
269
+ def adopted(id)
270
+ runtime = @adopt&.call(id)
271
+ # The Loader that built the Runtime holds this same host, so it is
272
+ # re-pointed in place rather than replaced.
273
+ case (host = runtime&.host&.host)
274
+ when Riffer::Rig::ACP::Host then host.client = client
275
+ end
276
+ runtime
277
+ end
278
+
279
+ # @rbs loader: Riffer::Rig::Loader
280
+ # @rbs request: ::ACP::Types::LoadSessionRequest
281
+ # @rbs host: Riffer::Rig::ACP::Host
282
+ # @rbs return: Riffer::Rig::Runtime?
283
+ def resumed(loader, request, host)
284
+ runtime = loader.resume(
285
+ request.session_id,
286
+ extra_extensions: self.class.session_extensions(declarations(request.mcp_servers, host))
287
+ )
288
+ runtime && own(announcing_job_exits(runtime, host))
289
+ end
290
+
291
+ # @rbs runtime: Riffer::Rig::Runtime
292
+ # @rbs return: Riffer::Rig::Runtime
293
+ def own(runtime)
294
+ @lock.synchronize { @owned << runtime }
295
+ runtime
296
+ end
297
+
298
+ # @rbs runtime: Riffer::Rig::Runtime
299
+ # @rbs host: Riffer::Rig::ACP::Host
300
+ # @rbs return: Riffer::Rig::Runtime
301
+ def announcing_job_exits(runtime, host)
302
+ # A prompt in flight marks the exit on its own stream when it tells the
303
+ # model. One starting meanwhile collects the exit from the inbox whose
304
+ # lock this subscriber holds, so it cannot stream ahead of this update.
305
+ runtime.on_job_exit do |completion|
306
+ host.update(Riffer::Rig::ACP::Mapper.job_completion(completion)) unless runtime.busy?
307
+ end
308
+ runtime
309
+ end
310
+
225
311
  # @rbs servers: Array[::ACP::Types::McpServer::t]
226
312
  # @rbs host: Riffer::Rig::ACP::Host
227
313
  # @rbs return: Hash[String, Riffer::Rig::Mcp::Declaration]
@@ -260,89 +346,6 @@ class Riffer::Rig::ACP
260
346
  server.env.to_h { |variable| [variable.name, variable.value] }
261
347
  end
262
348
 
263
- # Text and resource links are ACP's must-support prompt blocks; the client
264
- # cannot send the others unless the agent advertised them.
265
- # @rbs blocks: Array[::ACP::Types::ContentBlock::t]
266
- # @rbs return: String
267
- def prompt_text(blocks)
268
- blocks.filter_map { |block| prompt_text_of(block) }.join("\n\n")
269
- end
270
-
271
- # @rbs block: ::ACP::Types::ContentBlock::t
272
- # @rbs return: String?
273
- def prompt_text_of(block)
274
- case block
275
- when ::ACP::Types::ContentBlock::Text then block.text
276
- when ::ACP::Types::ContentBlock::ResourceLink then "#{block.title || block.name} #{block.uri}"
277
- end
278
- end
279
-
280
- # @rbs content: String
281
- # @rbs return: ::ACP::Types::SessionUpdate::AgentMessageChunk
282
- def message_chunk(content)
283
- ::ACP::Types::SessionUpdate::AgentMessageChunk.new(content: text_block(content))
284
- end
285
-
286
- # @rbs text: String
287
- # @rbs return: ::ACP::Types::ContentBlock::Text
288
- def text_block(text)
289
- ::ACP::Types::ContentBlock::Text.new(text: text)
290
- end
291
-
292
- # @rbs message: Hash[Symbol, untyped]
293
- # @rbs return: ::ACP::Types::SessionUpdate::t?
294
- def replay_of(message)
295
- case message[:role]
296
- when 'user' then ::ACP::Types::SessionUpdate::UserMessageChunk.new(content: text_block(message[:content].to_s))
297
- when 'assistant' then message_chunk(message[:content].to_s)
298
- when 'tool' then replayed_tool_call(message)
299
- end
300
- end
301
-
302
- # The stored result is all a replayed call has: the arguments live in the
303
- # assistant message's tool_calls, so the title is the name alone.
304
- # @rbs message: Hash[Symbol, untyped]
305
- # @rbs return: ::ACP::Types::SessionUpdate::ToolCall
306
- def replayed_tool_call(message)
307
- name = message[:name].to_s
308
- ::ACP::Types::SessionUpdate::ToolCall.new(
309
- tool_call_id: message[:tool_call_id].to_s,
310
- title: name,
311
- name: name,
312
- kind: TOOL_KINDS.fetch(name, ::ACP::Types::ToolKind::OTHER),
313
- status: ::ACP::Types::ToolCallStatus::COMPLETED,
314
- content: [::ACP::Types::ToolCallContent::Content.new(content: text_block(message[:content].to_s))]
315
- )
316
- end
317
-
318
- # riffer's stream has no completion boundary for a tool call, so a call goes
319
- # out once, in progress, when its arguments are complete.
320
- # @rbs event: Riffer::StreamEvents::ToolCallDone
321
- # @rbs return: ::ACP::Types::SessionUpdate::ToolCall
322
- def tool_call(event)
323
- ::ACP::Types::SessionUpdate::ToolCall.new(
324
- tool_call_id: event.call_id,
325
- title: "#{event.name}(#{event.arguments})",
326
- name: event.name,
327
- kind: TOOL_KINDS.fetch(event.name, ::ACP::Types::ToolKind::OTHER),
328
- status: ::ACP::Types::ToolCallStatus::IN_PROGRESS
329
- )
330
- end
331
-
332
- # riffer's failure reasons (:error, :other, :guardrail_blocked) have no ACP
333
- # counterpart; refusal is the closest signal that the turn did not complete.
334
- # @rbs stop_reason: Symbol?
335
- # @rbs return: String
336
- def stop_reason_of(stop_reason)
337
- case stop_reason
338
- when :completed then ::ACP::Types::StopReason::END_TURN
339
- when Riffer::Rig::Runtime::INTERRUPT_CANCELLED then ::ACP::Types::StopReason::CANCELLED
340
- when :max_steps then ::ACP::Types::StopReason::MAX_TURN_REQUESTS
341
- when :context_window, :length, :content_filter, :malformed_output then ::ACP::Types::StopReason::MAX_TOKENS
342
- else ::ACP::Types::StopReason::REFUSAL
343
- end
344
- end
345
-
346
349
  # @rbs runtime: Riffer::Rig::Runtime
347
350
  # @rbs return: Array[::ACP::Types::AvailableCommand]
348
351
  def available_commands(runtime)
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Riffer::Rig::Bundled
4
- Bash = Riffer::Rig::Extension.new('bash') { |rig| rig.tool Riffer::Rig::Tools::Bash } #: Riffer::Rig::Extension
4
+ Bash = Riffer::Rig::Extension.new('bash') do |rig|
5
+ rig.tool Riffer::Rig::Tools::Bash
6
+ rig.tool Riffer::Rig::Tools::Bash::Output
7
+ rig.tool Riffer::Rig::Tools::Bash::Kill
8
+ end #: Riffer::Rig::Extension
5
9
  end