insika 0.0.1 → 0.2.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 (277) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +361 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -2
  5. data/bin/insika +366 -0
  6. data/docs/AGENTS.md +618 -0
  7. data/docs/ARCHITECTURE.md +333 -0
  8. data/docs/BENCHMARK.md +114 -0
  9. data/docs/CHANNELS.md +453 -0
  10. data/docs/CONTEXT.md +117 -0
  11. data/docs/DEPLOY.md +354 -0
  12. data/docs/EMBEDDING.md +198 -0
  13. data/docs/EVALS.md +273 -0
  14. data/docs/LOADTEST.md +232 -0
  15. data/docs/OBSERVABILITY.md +374 -0
  16. data/docs/PLUGINS.md +211 -0
  17. data/docs/REFINEMENT.md +477 -0
  18. data/docs/RELEASING.md +70 -0
  19. data/docs/RUNNING-LOCAL.md +153 -0
  20. data/docs/SANDBOX.md +114 -0
  21. data/docs/SECURITY.md +375 -0
  22. data/docs/SKILLS.md +284 -0
  23. data/docs/TOOLS.md +302 -0
  24. data/docs/WHY.md +137 -0
  25. data/docs/WORKFLOWS.md +225 -0
  26. data/docs/build.md +14 -0
  27. data/docs/index.md +68 -0
  28. data/docs/onboarding/start.md +126 -0
  29. data/docs/operate.md +12 -0
  30. data/docs/ship.md +10 -0
  31. data/docs/understand.md +10 -0
  32. data/lib/insika/agent_file_store.rb +125 -0
  33. data/lib/insika/agent_profile.rb +255 -0
  34. data/lib/insika/alert_dispatcher.rb +139 -0
  35. data/lib/insika/allowlist.rb +28 -0
  36. data/lib/insika/baseline_store.rb +74 -0
  37. data/lib/insika/budget_ledger.rb +135 -0
  38. data/lib/insika/capability/resolved_tool.rb +34 -0
  39. data/lib/insika/capability_registry.rb +112 -0
  40. data/lib/insika/channel_delivery.rb +153 -0
  41. data/lib/insika/channel_registry.rb +30 -0
  42. data/lib/insika/channels/relay.rb +178 -0
  43. data/lib/insika/channels/web/widget.js +283 -0
  44. data/lib/insika/channels/web.rb +211 -0
  45. data/lib/insika/channels/webhook.rb +58 -0
  46. data/lib/insika/chat_builder.rb +303 -0
  47. data/lib/insika/checkpoint.rb +13 -0
  48. data/lib/insika/checkpoint_store.rb +153 -0
  49. data/lib/insika/circuit_state.rb +114 -0
  50. data/lib/insika/coercion.rb +58 -0
  51. data/lib/insika/command.rb +32 -0
  52. data/lib/insika/command_bus.rb +39 -0
  53. data/lib/insika/commands/agent_payload.rb +43 -0
  54. data/lib/insika/commands/approve_action.rb +46 -0
  55. data/lib/insika/commands/cancel_task.rb +33 -0
  56. data/lib/insika/commands/create_agent.rb +54 -0
  57. data/lib/insika/commands/create_session.rb +67 -0
  58. data/lib/insika/commands/delete_agent.rb +33 -0
  59. data/lib/insika/commands/delete_agent_file.rb +50 -0
  60. data/lib/insika/commands/delete_data_tool.rb +33 -0
  61. data/lib/insika/commands/delete_llm_provider.rb +36 -0
  62. data/lib/insika/commands/delete_mcp.rb +30 -0
  63. data/lib/insika/commands/delete_skill.rb +43 -0
  64. data/lib/insika/commands/delete_system_file.rb +29 -0
  65. data/lib/insika/commands/gate_refinement.rb +245 -0
  66. data/lib/insika/commands/import_mcp_tools.rb +48 -0
  67. data/lib/insika/commands/import_tools.rb +81 -0
  68. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  69. data/lib/insika/commands/memory_add_note.rb +32 -0
  70. data/lib/insika/commands/memory_forget_fact.rb +32 -0
  71. data/lib/insika/commands/memory_put_fact.rb +35 -0
  72. data/lib/insika/commands/pause_task.rb +29 -0
  73. data/lib/insika/commands/resolve_refinement.rb +126 -0
  74. data/lib/insika/commands/restore_agent_file.rb +36 -0
  75. data/lib/insika/commands/restore_data_tool.rb +34 -0
  76. data/lib/insika/commands/restore_system_file.rb +31 -0
  77. data/lib/insika/commands/resume_task.rb +85 -0
  78. data/lib/insika/commands/revoke_token.rb +39 -0
  79. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  80. data/lib/insika/commands/run_refinement.rb +133 -0
  81. data/lib/insika/commands/send_message.rb +150 -0
  82. data/lib/insika/commands/set_agent_tools.rb +39 -0
  83. data/lib/insika/commands/set_skill_agents.rb +112 -0
  84. data/lib/insika/commands/trigger_workflow.rb +80 -0
  85. data/lib/insika/commands/update_agent.rb +49 -0
  86. data/lib/insika/commands/update_settings.rb +33 -0
  87. data/lib/insika/commands/upsert_llm_provider.rb +34 -0
  88. data/lib/insika/commands/upsert_mcp.rb +32 -0
  89. data/lib/insika/commands/write_agent_file.rb +57 -0
  90. data/lib/insika/commands/write_data_tool.rb +43 -0
  91. data/lib/insika/commands/write_golden.rb +58 -0
  92. data/lib/insika/commands/write_skill.rb +60 -0
  93. data/lib/insika/commands/write_system_file.rb +31 -0
  94. data/lib/insika/config_store.rb +89 -0
  95. data/lib/insika/context/builder.rb +166 -0
  96. data/lib/insika/context/catalog_provider.rb +23 -0
  97. data/lib/insika/context/fragment.rb +43 -0
  98. data/lib/insika/context/priority.rb +30 -0
  99. data/lib/insika/context/provider.rb +19 -0
  100. data/lib/insika/context/providers/memory.rb +60 -0
  101. data/lib/insika/context/providers/prompt.rb +105 -0
  102. data/lib/insika/context/providers/request.rb +32 -0
  103. data/lib/insika/context/providers/session.rb +123 -0
  104. data/lib/insika/context/providers/skill.rb +24 -0
  105. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  106. data/lib/insika/context/providers/tool_search.rb +20 -0
  107. data/lib/insika/context_trace_store.rb +92 -0
  108. data/lib/insika/delegation_store.rb +153 -0
  109. data/lib/insika/doctor.rb +539 -0
  110. data/lib/insika/dsl/definition.rb +55 -0
  111. data/lib/insika/dsl/runtime.rb +382 -0
  112. data/lib/insika/dsl/server_boot.rb +98 -0
  113. data/lib/insika/dsl/system.rb +93 -0
  114. data/lib/insika/dsl/workflow_adapter.rb +59 -0
  115. data/lib/insika/dsl.rb +364 -0
  116. data/lib/insika/edge_limiter.rb +268 -0
  117. data/lib/insika/egress_guard.rb +75 -0
  118. data/lib/insika/env_schema.rb +249 -0
  119. data/lib/insika/errors.rb +201 -0
  120. data/lib/insika/evals/assertions.rb +247 -0
  121. data/lib/insika/evals/baseline.rb +69 -0
  122. data/lib/insika/evals/golden.rb +172 -0
  123. data/lib/insika/evals/judge.rb +225 -0
  124. data/lib/insika/evals/pairwise.rb +178 -0
  125. data/lib/insika/evals/report.rb +115 -0
  126. data/lib/insika/evals/runner.rb +141 -0
  127. data/lib/insika/evals/transport.rb +178 -0
  128. data/lib/insika/event.rb +18 -0
  129. data/lib/insika/event_stream.rb +132 -0
  130. data/lib/insika/executor.rb +1995 -0
  131. data/lib/insika/frontmatter.rb +42 -0
  132. data/lib/insika/golden_store.rb +145 -0
  133. data/lib/insika/hooks.rb +48 -0
  134. data/lib/insika/http_client.rb +63 -0
  135. data/lib/insika/inbound_log.rb +84 -0
  136. data/lib/insika/llm_configurator.rb +99 -0
  137. data/lib/insika/llm_provider_store.rb +83 -0
  138. data/lib/insika/loop_detector.rb +143 -0
  139. data/lib/insika/mcp_http_client.rb +67 -0
  140. data/lib/insika/mcp_store.rb +115 -0
  141. data/lib/insika/mcp_tool_ingestor.rb +143 -0
  142. data/lib/insika/memory_store.rb +93 -0
  143. data/lib/insika/message_origin.rb +76 -0
  144. data/lib/insika/middleware.rb +36 -0
  145. data/lib/insika/model_policy.rb +52 -0
  146. data/lib/insika/model_resolver.rb +176 -0
  147. data/lib/insika/model_selection.rb +115 -0
  148. data/lib/insika/onboarding.rb +208 -0
  149. data/lib/insika/outbox_store.rb +166 -0
  150. data/lib/insika/overlay_tool_registry.rb +102 -0
  151. data/lib/insika/pack.rb +102 -0
  152. data/lib/insika/pack_importer.rb +123 -0
  153. data/lib/insika/pending_action_store.rb +120 -0
  154. data/lib/insika/plugin/loader.rb +356 -0
  155. data/lib/insika/plugin.rb +35 -0
  156. data/lib/insika/policy/engine.rb +83 -0
  157. data/lib/insika/policy/policy.rb +120 -0
  158. data/lib/insika/policy_registry.rb +23 -0
  159. data/lib/insika/profile_source.rb +143 -0
  160. data/lib/insika/prompt_catalog.rb +61 -0
  161. data/lib/insika/provider_error_classifier.rb +160 -0
  162. data/lib/insika/queue_policy.rb +167 -0
  163. data/lib/insika/recovery.rb +168 -0
  164. data/lib/insika/refinement/candidate.rb +159 -0
  165. data/lib/insika/refinement/evidence_collector.rb +371 -0
  166. data/lib/insika/refinement/gate.rb +234 -0
  167. data/lib/insika/refinement/panel.rb +222 -0
  168. data/lib/insika/refinement/proposer.rb +262 -0
  169. data/lib/insika/refinement_store.rb +295 -0
  170. data/lib/insika/registry.rb +59 -0
  171. data/lib/insika/reliability.rb +185 -0
  172. data/lib/insika/safety/config.rb +109 -0
  173. data/lib/insika/safety/detectors.rb +176 -0
  174. data/lib/insika/safety/factory.rb +102 -0
  175. data/lib/insika/safety/input_guardrail.rb +102 -0
  176. data/lib/insika/safety/moderator.rb +94 -0
  177. data/lib/insika/safety/output_filter.rb +79 -0
  178. data/lib/insika/safety/output_validator.rb +101 -0
  179. data/lib/insika/safety/safe_responses.rb +47 -0
  180. data/lib/insika/sandbox/boundary.rb +93 -0
  181. data/lib/insika/sandbox/docker.rb +74 -0
  182. data/lib/insika/sandbox/local.rb +33 -0
  183. data/lib/insika/sandbox/runner.rb +80 -0
  184. data/lib/insika/sandbox.rb +85 -0
  185. data/lib/insika/schema_guard.rb +147 -0
  186. data/lib/insika/secret_masking.rb +34 -0
  187. data/lib/insika/server/a2a/agent_card.rb +27 -0
  188. data/lib/insika/server/a2a/app.rb +112 -0
  189. data/lib/insika/server/a2a/client.rb +101 -0
  190. data/lib/insika/server/a2a/errors.rb +32 -0
  191. data/lib/insika/server/a2a/http.rb +42 -0
  192. data/lib/insika/server/a2a/message.rb +27 -0
  193. data/lib/insika/server/a2a/protocol.rb +45 -0
  194. data/lib/insika/server/a2a/remotes.rb +25 -0
  195. data/lib/insika/server/a2a/task_projection.rb +40 -0
  196. data/lib/insika/server/app.rb +1022 -0
  197. data/lib/insika/server/boot.rb +119 -0
  198. data/lib/insika/server/rack_app.rb +118 -0
  199. data/lib/insika/server/responses.rb +165 -0
  200. data/lib/insika/server/sse_body.rb +96 -0
  201. data/lib/insika/server/tenant_auth.rb +61 -0
  202. data/lib/insika/session_actor.rb +162 -0
  203. data/lib/insika/session_store.rb +143 -0
  204. data/lib/insika/settings_store.rb +154 -0
  205. data/lib/insika/shutdown.rb +125 -0
  206. data/lib/insika/skill_catalog.rb +220 -0
  207. data/lib/insika/skill_store.rb +127 -0
  208. data/lib/insika/steer_injector.rb +110 -0
  209. data/lib/insika/store.rb +52 -0
  210. data/lib/insika/stores/memory.rb +123 -0
  211. data/lib/insika/stores/sqlite.rb +183 -0
  212. data/lib/insika/studio/app.rb +1693 -0
  213. data/lib/insika/studio/assets/dist/application.css +1 -0
  214. data/lib/insika/studio/assets/dist/application.js +70 -0
  215. data/lib/insika/studio/forms.rb +335 -0
  216. data/lib/insika/studio/nav_icons.rb +31 -0
  217. data/lib/insika/studio/views/_message.erb +44 -0
  218. data/lib/insika/studio/views/agent_detail.erb +285 -0
  219. data/lib/insika/studio/views/agents.erb +63 -0
  220. data/lib/insika/studio/views/approvals.erb +41 -0
  221. data/lib/insika/studio/views/chats.erb +34 -0
  222. data/lib/insika/studio/views/evals.erb +83 -0
  223. data/lib/insika/studio/views/home.erb +72 -0
  224. data/lib/insika/studio/views/layout.erb +94 -0
  225. data/lib/insika/studio/views/login.erb +17 -0
  226. data/lib/insika/studio/views/mcp.erb +91 -0
  227. data/lib/insika/studio/views/not_found.erb +5 -0
  228. data/lib/insika/studio/views/playground.erb +47 -0
  229. data/lib/insika/studio/views/refinement.erb +234 -0
  230. data/lib/insika/studio/views/session.erb +137 -0
  231. data/lib/insika/studio/views/settings.erb +168 -0
  232. data/lib/insika/studio/views/skills.erb +141 -0
  233. data/lib/insika/studio/views/system_files.erb +65 -0
  234. data/lib/insika/studio/views/task.erb +105 -0
  235. data/lib/insika/studio/views/tasks.erb +33 -0
  236. data/lib/insika/studio/views/tool_edit.erb +107 -0
  237. data/lib/insika/studio/views/tools.erb +89 -0
  238. data/lib/insika/subagent_graph.rb +96 -0
  239. data/lib/insika/system_file_store.rb +96 -0
  240. data/lib/insika/task_actor.rb +128 -0
  241. data/lib/insika/task_store.rb +250 -0
  242. data/lib/insika/telemetry/pricing.rb +104 -0
  243. data/lib/insika/telemetry/recorder.rb +228 -0
  244. data/lib/insika/telemetry.rb +127 -0
  245. data/lib/insika/testing/store_contract.rb +270 -0
  246. data/lib/insika/tick.rb +122 -0
  247. data/lib/insika/token_estimator.rb +16 -0
  248. data/lib/insika/token_store.rb +168 -0
  249. data/lib/insika/tool_assembly.rb +140 -0
  250. data/lib/insika/tool_catalog.rb +89 -0
  251. data/lib/insika/tool_definition.rb +518 -0
  252. data/lib/insika/tool_envelope.rb +140 -0
  253. data/lib/insika/tool_manifest.rb +218 -0
  254. data/lib/insika/tool_output_compressor.rb +100 -0
  255. data/lib/insika/tool_registry.rb +21 -0
  256. data/lib/insika/tool_store.rb +135 -0
  257. data/lib/insika/tool_trace_store.rb +92 -0
  258. data/lib/insika/tools/a2a_remote.rb +48 -0
  259. data/lib/insika/tools/agent_enum.rb +68 -0
  260. data/lib/insika/tools/concurrency.rb +54 -0
  261. data/lib/insika/tools/data_defined_tool.rb +219 -0
  262. data/lib/insika/tools/load_skill.rb +99 -0
  263. data/lib/insika/tools/remember.rb +53 -0
  264. data/lib/insika/tools/stuck_signal.rb +44 -0
  265. data/lib/insika/tools/subagent.rb +75 -0
  266. data/lib/insika/tools/subagents.rb +77 -0
  267. data/lib/insika/tools/tool_search.rb +94 -0
  268. data/lib/insika/turn_output.rb +139 -0
  269. data/lib/insika/turn_state.rb +162 -0
  270. data/lib/insika/turn_timing.rb +56 -0
  271. data/lib/insika/usage_ledger.rb +47 -0
  272. data/lib/insika/version.rb +3 -1
  273. data/lib/insika/wiring/graph.rb +249 -0
  274. data/lib/insika/workflow.rb +185 -0
  275. data/lib/insika/workflow_registry.rb +33 -0
  276. data/lib/insika.rb +220 -4
  277. metadata +412 -8
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require_relative "protocol"
5
+ require_relative "errors"
6
+ require_relative "message"
7
+ require_relative "task_projection"
8
+ require_relative "agent_card"
9
+
10
+ module Insika
11
+ module Server
12
+ module A2A
13
+ # A2A edge handler: injected sub-app mounted by Server::App. A2A is
14
+ # TRANSPORT — translates JSON-RPC↔Command on the SAME bus, projects
15
+ # Task→A2A, and NEVER leaks an exception (always an error object).
16
+ class App
17
+ def initialize(command_bus:, task_store:, session_store:, profiles:, skill_catalog:, config:)
18
+ @command_bus = command_bus
19
+ @task_store = task_store
20
+ @session_store = session_store
21
+ @profiles = Insika::ProfileSource.coerce(profiles)
22
+ @skill_catalog = skill_catalog
23
+ @config = config # { a2a_agent:, base_url: }
24
+ end
25
+
26
+ # POST /a2a — `body` is already the deserialized Hash. -> JSON-RPC envelope.
27
+ def rpc(body)
28
+ kind, parsed = Protocol.parse(body)
29
+ return Protocol.error(parsed[:id], parsed[:code], parsed[:message]) if kind == :error
30
+
31
+ dispatch_method(parsed[:id], parsed[:method], parsed[:params])
32
+ rescue StandardError => e # safety net: parse ok but something slipped through
33
+ code, message = Errors.from_exception(e)
34
+ Protocol.error(nil, code, message)
35
+ end
36
+
37
+ # GET /.well-known/agent-card.json
38
+ def agent_card
39
+ agent = @profiles[@config[:a2a_agent]]
40
+ skills = @skill_catalog.effective(agent.skills)
41
+ AgentCard.build(agent: agent, base_url: @config[:base_url], skills: skills)
42
+ end
43
+
44
+ private
45
+
46
+ def dispatch_method(id, method, params)
47
+ case method
48
+ when "message/send" then message_send(id, params)
49
+ when "tasks/get" then tasks_get(id, params)
50
+ when "tasks/cancel" then tasks_cancel(id, params)
51
+ else Protocol.error(id, Errors::METHOD_NOT_FOUND, "method '#{method}' not supported")
52
+ end
53
+ rescue StandardError => e # map core error -> A2A code
54
+ code, message = Errors.from_exception(e)
55
+ Protocol.error(id, code, message)
56
+ end
57
+
58
+ # message/send returns the Task; missing contextId -> creates a session (the
59
+ # server assigns the contextId; ensures a transcript for tasks/get).
60
+ def message_send(id, params)
61
+ message = params["message"] || {}
62
+ session_id = message["contextId"] || params["contextId"]
63
+ session_id ||= @command_bus.dispatch(build(:create_session, {})).id
64
+ text = Message.text_from(message)
65
+ result = @command_bus.dispatch(build(:send_message,
66
+ agent: @config[:a2a_agent], message: text, session_id: session_id))
67
+ task = @task_store.find(result[:task_id])
68
+ Protocol.result(id, TaskProjection.call(task, at: now))
69
+ end
70
+
71
+ def tasks_get(id, params)
72
+ task = @task_store.find(params["id"])
73
+ return Protocol.error(id, Errors::TASK_NOT_FOUND, "task not found: #{params['id']}") if task.nil?
74
+
75
+ Protocol.result(id, TaskProjection.call(task, at: now,
76
+ content: terminal_content(task),
77
+ error: terminal_error(task)))
78
+ end
79
+
80
+ def tasks_cancel(id, params)
81
+ task_id = params["id"]
82
+ return Protocol.error(id, Errors::TASK_NOT_FOUND, "task not found: #{task_id}") if @task_store.find(task_id).nil?
83
+
84
+ @command_bus.dispatch(build(:cancel_task, task_id: task_id))
85
+ Protocol.result(id, TaskProjection.call(@task_store.find(task_id), at: now))
86
+ end
87
+
88
+ # Final content = last `assistant` message in the session transcript.
89
+ def terminal_content(task)
90
+ return nil unless task.session_id
91
+
92
+ session = @session_store.find(task.session_id)
93
+ return nil unless session
94
+
95
+ msg = session.messages.reverse.find { |m| (m["role"] || m[:role]).to_s == "assistant" }
96
+ msg && (msg["content"] || msg[:content])
97
+ end
98
+
99
+ # Error message from the last :failed Execution.
100
+ def terminal_error(task)
101
+ exec = task.executions.last
102
+ return nil unless exec && exec.outcome.to_s == "failed"
103
+
104
+ exec.error && (exec.error["message"] || exec.error[:message])
105
+ end
106
+
107
+ def build(type, payload) = Insika::Command.build(type, payload, transport: :a2a)
108
+ def now = Time.now.utc.iso8601
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "message"
4
+
5
+ module Insika
6
+ module Server
7
+ module A2A
8
+ # JSON-RPC response envelope carrying `error` (the remote refused/failed).
9
+ class RemoteError < StandardError
10
+ attr_reader :code
11
+
12
+ def initialize(code, message)
13
+ @code = code
14
+ super(message)
15
+ end
16
+ end
17
+
18
+ # Outbound A2A client: calls a remote A2A agent. PURE — the
19
+ # `http` (post_json(url, body) -> Hash) is injected; the smoke test uses loopback.
20
+ class Client
21
+ TERMINAL = %w[completed failed canceled rejected input-required].freeze
22
+ POLL_DELAY = 0.02
23
+
24
+ def initialize(http:, poll_max: 30, sleeper: nil)
25
+ @http = http
26
+ @poll_max = poll_max
27
+ @sleeper = sleeper || ->(seconds) { Async::Task.current&.sleep(seconds) }
28
+ @id_seq = 0
29
+ end
30
+
31
+ # -> remote A2A Task (Hash) | raise RemoteError.
32
+ def send_message(url, text, context_id: nil)
33
+ message = { "role" => "user", "parts" => [{ "kind" => "text", "text" => text.to_s }] }
34
+ message["contextId"] = context_id if context_id
35
+ parse_envelope(@http.post_json(url, request("message/send", { "message" => message })))
36
+ end
37
+
38
+ # -> remote A2A Task (Hash) | raise RemoteError.
39
+ def get_task(url, task_id)
40
+ parse_envelope(@http.post_json(url, request("tasks/get", { "id" => task_id })))
41
+ end
42
+
43
+ # High level: send + poll until terminal. ALWAYS does ≥1 get_task — the
44
+ # inbound message/send projects the Task WITHOUT `status.message` (the content
45
+ # only comes from tasks/get). -> { text:, state:, id: } | { error:, state:, id: }.
46
+ # NEVER raises (wraps RemoteError).
47
+ def call(url, text, context_id: nil)
48
+ sent = send_message(url, text, context_id: context_id)
49
+ id = remote_id(sent)
50
+ task = get_task(url, id)
51
+ attempts = 0
52
+ until TERMINAL.include?(remote_state(task)) || attempts >= @poll_max
53
+ @sleeper.call(POLL_DELAY)
54
+ attempts += 1
55
+ task = get_task(url, id)
56
+ end
57
+ project(task, id)
58
+ rescue RemoteError => e
59
+ { error: e.message, state: "failed", id: nil }
60
+ end
61
+
62
+ private
63
+
64
+ def request(method, params)
65
+ @id_seq += 1
66
+ { "jsonrpc" => "2.0", "id" => @id_seq, "method" => method, "params" => params }
67
+ end
68
+
69
+ def parse_envelope(response)
70
+ raise RemoteError.new(nil, "invalid A2A response") unless response.is_a?(Hash)
71
+ return response["result"] if response.key?("result")
72
+
73
+ err = response["error"]
74
+ raise RemoteError.new(err && err["code"], (err && err["message"]).to_s) if err
75
+
76
+ raise RemoteError.new(nil, "A2A response has neither result nor error")
77
+ end
78
+
79
+ def remote_state(task) = task.is_a?(Hash) ? task.dig("status", "state").to_s : ""
80
+ def remote_text(task) = Message.text_from(task.is_a?(Hash) ? task.dig("status", "message") : nil)
81
+ def remote_id(task) = task.is_a?(Hash) ? task["id"] : nil
82
+
83
+ # completed/input-required -> text; failed/canceled/rejected -> error
84
+ # (text if present, else the state itself — avoids the "".truthy footgun);
85
+ # exhausted poll (non-terminal) -> error.
86
+ def project(task, id)
87
+ state = remote_state(task)
88
+ text = remote_text(task)
89
+ case state
90
+ when "completed", "input-required"
91
+ { text: text, state: state, id: id }
92
+ when "failed", "canceled", "rejected"
93
+ { error: text.empty? ? state : text, state: state, id: id }
94
+ else
95
+ { error: "remote task did not complete (state: #{state})", state: state, id: id }
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Insika
4
+ module Server
5
+ module A2A
6
+ # A2A adapter error map: standard JSON-RPC 2.0 codes +
7
+ # A2A extensions. The `App` NEVER leaks an exception — always an error object.
8
+ module Errors
9
+ PARSE_ERROR = -32_700
10
+ INVALID_REQUEST = -32_600
11
+ METHOD_NOT_FOUND = -32_601
12
+ INVALID_PARAMS = -32_602
13
+ INTERNAL_ERROR = -32_603
14
+ TASK_NOT_FOUND = -32_001 # A2A TaskNotFoundError
15
+ TASK_NOT_CANCELABLE = -32_002 # A2A TaskNotCancelableError
16
+
17
+ # Core exception -> [code, message]. A task `NotFoundError` is handled
18
+ # EXPLICITLY in the App (tasks/get returns TASK_NOT_FOUND directly), so
19
+ # here `NotFoundError` falls into INVALID_PARAMS (missing agent/session in the
20
+ # request). Anything else -> INTERNAL_ERROR (generic message, no stack).
21
+ def self.from_exception(error)
22
+ case error
23
+ when Insika::ValidationError, Insika::NotFoundError
24
+ [INVALID_PARAMS, error.message]
25
+ else
26
+ [INTERNAL_ERROR, "internal error"]
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Insika
6
+ module Server
7
+ module A2A
8
+ # Production HTTP adapter: implements the duck-type
9
+ # `post_json(url, body) -> Hash` over async-http (runs on the turn's reactor).
10
+ # BOUNDARY — the lib require lives here, never in lib/insika.rb/wiring-load.
11
+ # The Client (the logic) is tested with a fake; this adapter, with a light test.
12
+ class Http
13
+ HEADERS = [["content-type", "application/json"], ["accept", "application/json"]].freeze
14
+
15
+ def initialize(internet: nil)
16
+ @internet = internet # lazy: Async::HTTP::Internet.new
17
+ end
18
+
19
+ # POST JSON -> Hash (parsed JSON-RPC, string keys).
20
+ def post_json(url, body)
21
+ response = internet.post(url, HEADERS, JSON.generate(body))
22
+ JSON.parse(response.read.to_s)
23
+ end
24
+
25
+ def close
26
+ @internet&.close
27
+ rescue StandardError
28
+ nil # best-effort
29
+ end
30
+
31
+ private
32
+
33
+ def internet
34
+ @internet ||= begin
35
+ require "async/http/internet"
36
+ Async::HTTP::Internet.new
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Insika
4
+ module Server
5
+ module A2A
6
+ # Message parts translation: TextPart only in this slice.
7
+ # Tolerates `kind` (A2A ~v0.2+) and `type` (older spec) at the boundary.
8
+ module Message
9
+ # A2A Message (Hash) -> String (concatenates the TextParts). "" if none.
10
+ def self.text_from(a2a_message)
11
+ parts = a2a_message.is_a?(Hash) ? Array(a2a_message["parts"] || a2a_message[:parts]) : []
12
+ parts.filter_map do |part|
13
+ next unless part.is_a?(Hash)
14
+
15
+ kind = part["kind"] || part["type"] || part[:kind] || part[:type]
16
+ (part["text"] || part[:text]).to_s if kind.to_s == "text"
17
+ end.join
18
+ end
19
+
20
+ # String -> A2A Message (role agent, 1 TextPart).
21
+ def self.agent_message(text)
22
+ { role: "agent", parts: [{ kind: "text", text: text.to_s }] }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Insika
6
+ module Server
7
+ module A2A
8
+ # JSON-RPC 2.0 envelope. PURE — receives the already
9
+ # deserialized Hash (raw JSON parsing belongs to Server::App). NEVER raises.
10
+ module Protocol
11
+ VERSION = "2.0"
12
+
13
+ # -> [:ok, { id:, method:, params: }] | [:error, { id:, code:, message: }]
14
+ def self.parse(body)
15
+ return err(nil, Errors::INVALID_REQUEST, "request must be a JSON-RPC object") unless body.is_a?(Hash)
16
+
17
+ id = body["id"] # missing -> nil (we always respond; no notifications in this slice)
18
+ return err(id, Errors::INVALID_REQUEST, "jsonrpc must be '2.0'") unless body["jsonrpc"] == VERSION
19
+
20
+ method = body["method"]
21
+ return err(id, Errors::INVALID_REQUEST, "method missing") unless method.is_a?(String) && !method.empty?
22
+
23
+ [:ok, { id: id, method: method, params: body["params"] || {} }]
24
+ end
25
+
26
+ # { jsonrpc:, id:, result: }
27
+ def self.result(id, result)
28
+ { jsonrpc: VERSION, id: id, result: result }
29
+ end
30
+
31
+ # { jsonrpc:, id:, error: { code:, message:, data? } }
32
+ def self.error(id, code, message, data: nil)
33
+ err = { code: code, message: message }
34
+ err[:data] = data unless data.nil?
35
+ { jsonrpc: VERSION, id: id, error: err }
36
+ end
37
+
38
+ def self.err(id, code, message)
39
+ [:error, { id: id, code: code, message: message }]
40
+ end
41
+ private_class_method :err
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Insika
4
+ module Server
5
+ module A2A
6
+ # Remote A2A agents config. Parses "id=url,id2=url2".
7
+ module Remotes
8
+ Remote = Data.define(:id, :url, :description)
9
+
10
+ # -> [Remote]. Malformed entries (no '=', empty id/url) are ignored
11
+ # with a warn. env nil/"" -> [].
12
+ def self.parse(env_string, descriptions: {})
13
+ env_string.to_s.split(",").filter_map do |entry|
14
+ id, url = entry.split("=", 2).map { |s| s.to_s.strip }
15
+ if id.to_s.empty? || url.to_s.empty?
16
+ warn "[a2a] malformed remote ignored: #{entry.inspect} (use id=url)"
17
+ next
18
+ end
19
+ Remote.new(id: id, url: url, description: descriptions[id])
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "message"
4
+
5
+ module Insika
6
+ module Server
7
+ module A2A
8
+ # Task (core) -> A2A Task projection. PURE.
9
+ module TaskProjection
10
+ # State map. Unknown state -> "unknown".
11
+ STATE = {
12
+ queued: "submitted", running: "working", waiting: "input-required",
13
+ paused: "working", completed: "completed", failed: "failed", cancelled: "canceled"
14
+ }.freeze
15
+
16
+ # task (TaskStore::Task) + content/error (String|nil) + at (ISO8601) -> Hash A2A Task.
17
+ def self.call(task, at:, content: nil, error: nil)
18
+ state = STATE.fetch(task.status.to_sym, "unknown")
19
+ status = { state: state, timestamp: at }
20
+ msg = status_message(state, content, error)
21
+ status[:message] = msg if msg
22
+
23
+ {
24
+ id: task.id, contextId: task.session_id, kind: "task",
25
+ status: status, artifacts: [], history: []
26
+ }
27
+ end
28
+
29
+ # completed -> content; failed -> error; other states have no message.
30
+ def self.status_message(state, content, error)
31
+ case state
32
+ when "completed" then content && Message.agent_message(content)
33
+ when "failed" then error && Message.agent_message(error)
34
+ end
35
+ end
36
+ private_class_method :status_message
37
+ end
38
+ end
39
+ end
40
+ end