insika 0.0.1 → 0.1.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 (260) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +295 -0
  3. data/LICENSE +21 -0
  4. data/README.md +136 -2
  5. data/bin/insika +351 -0
  6. data/docs/AGENTS.md +494 -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 +100 -0
  11. data/docs/DEPLOY.md +334 -0
  12. data/docs/EMBEDDING.md +194 -0
  13. data/docs/EVALS.md +273 -0
  14. data/docs/LOADTEST.md +231 -0
  15. data/docs/OBSERVABILITY.md +365 -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 +362 -0
  22. data/docs/SKILLS.md +98 -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 +188 -0
  34. data/lib/insika/allowlist.rb +28 -0
  35. data/lib/insika/baseline_store.rb +74 -0
  36. data/lib/insika/capability/resolved_tool.rb +34 -0
  37. data/lib/insika/capability_registry.rb +112 -0
  38. data/lib/insika/channel_delivery.rb +150 -0
  39. data/lib/insika/channel_registry.rb +30 -0
  40. data/lib/insika/channels/relay.rb +178 -0
  41. data/lib/insika/channels/web/widget.js +283 -0
  42. data/lib/insika/channels/web.rb +211 -0
  43. data/lib/insika/chat_builder.rb +254 -0
  44. data/lib/insika/checkpoint.rb +13 -0
  45. data/lib/insika/checkpoint_store.rb +153 -0
  46. data/lib/insika/coercion.rb +50 -0
  47. data/lib/insika/command.rb +32 -0
  48. data/lib/insika/command_bus.rb +39 -0
  49. data/lib/insika/commands/agent_payload.rb +41 -0
  50. data/lib/insika/commands/approve_action.rb +46 -0
  51. data/lib/insika/commands/cancel_task.rb +33 -0
  52. data/lib/insika/commands/create_agent.rb +54 -0
  53. data/lib/insika/commands/create_session.rb +67 -0
  54. data/lib/insika/commands/delete_agent.rb +33 -0
  55. data/lib/insika/commands/delete_agent_file.rb +50 -0
  56. data/lib/insika/commands/delete_data_tool.rb +33 -0
  57. data/lib/insika/commands/delete_llm_provider.rb +36 -0
  58. data/lib/insika/commands/delete_mcp.rb +30 -0
  59. data/lib/insika/commands/delete_system_file.rb +29 -0
  60. data/lib/insika/commands/gate_refinement.rb +245 -0
  61. data/lib/insika/commands/import_mcp_tools.rb +48 -0
  62. data/lib/insika/commands/import_tools.rb +81 -0
  63. data/lib/insika/commands/memory_add_note.rb +32 -0
  64. data/lib/insika/commands/memory_forget_fact.rb +32 -0
  65. data/lib/insika/commands/memory_put_fact.rb +35 -0
  66. data/lib/insika/commands/pause_task.rb +29 -0
  67. data/lib/insika/commands/resolve_refinement.rb +126 -0
  68. data/lib/insika/commands/restore_agent_file.rb +36 -0
  69. data/lib/insika/commands/restore_data_tool.rb +34 -0
  70. data/lib/insika/commands/restore_system_file.rb +31 -0
  71. data/lib/insika/commands/resume_task.rb +85 -0
  72. data/lib/insika/commands/run_refinement.rb +133 -0
  73. data/lib/insika/commands/send_message.rb +150 -0
  74. data/lib/insika/commands/set_agent_tools.rb +39 -0
  75. data/lib/insika/commands/set_skill_agents.rb +71 -0
  76. data/lib/insika/commands/trigger_workflow.rb +80 -0
  77. data/lib/insika/commands/update_agent.rb +49 -0
  78. data/lib/insika/commands/update_settings.rb +33 -0
  79. data/lib/insika/commands/upsert_llm_provider.rb +34 -0
  80. data/lib/insika/commands/upsert_mcp.rb +32 -0
  81. data/lib/insika/commands/write_agent_file.rb +57 -0
  82. data/lib/insika/commands/write_data_tool.rb +43 -0
  83. data/lib/insika/commands/write_golden.rb +58 -0
  84. data/lib/insika/commands/write_skill.rb +50 -0
  85. data/lib/insika/commands/write_system_file.rb +31 -0
  86. data/lib/insika/config_store.rb +85 -0
  87. data/lib/insika/context/builder.rb +166 -0
  88. data/lib/insika/context/catalog_provider.rb +23 -0
  89. data/lib/insika/context/fragment.rb +19 -0
  90. data/lib/insika/context/priority.rb +29 -0
  91. data/lib/insika/context/provider.rb +19 -0
  92. data/lib/insika/context/providers/memory.rb +60 -0
  93. data/lib/insika/context/providers/prompt.rb +105 -0
  94. data/lib/insika/context/providers/request.rb +32 -0
  95. data/lib/insika/context/providers/session.rb +108 -0
  96. data/lib/insika/context/providers/skill.rb +20 -0
  97. data/lib/insika/context/providers/tool_search.rb +20 -0
  98. data/lib/insika/delegation_store.rb +153 -0
  99. data/lib/insika/doctor.rb +294 -0
  100. data/lib/insika/dsl/definition.rb +55 -0
  101. data/lib/insika/dsl/runtime.rb +379 -0
  102. data/lib/insika/dsl/server_boot.rb +97 -0
  103. data/lib/insika/dsl/system.rb +93 -0
  104. data/lib/insika/dsl/workflow_adapter.rb +59 -0
  105. data/lib/insika/dsl.rb +307 -0
  106. data/lib/insika/edge_limiter.rb +130 -0
  107. data/lib/insika/egress_guard.rb +75 -0
  108. data/lib/insika/env_schema.rb +246 -0
  109. data/lib/insika/errors.rb +145 -0
  110. data/lib/insika/evals/assertions.rb +247 -0
  111. data/lib/insika/evals/baseline.rb +69 -0
  112. data/lib/insika/evals/golden.rb +172 -0
  113. data/lib/insika/evals/judge.rb +225 -0
  114. data/lib/insika/evals/pairwise.rb +178 -0
  115. data/lib/insika/evals/report.rb +115 -0
  116. data/lib/insika/evals/runner.rb +141 -0
  117. data/lib/insika/evals/transport.rb +178 -0
  118. data/lib/insika/event.rb +18 -0
  119. data/lib/insika/event_stream.rb +114 -0
  120. data/lib/insika/executor.rb +1680 -0
  121. data/lib/insika/frontmatter.rb +42 -0
  122. data/lib/insika/golden_store.rb +145 -0
  123. data/lib/insika/hooks.rb +48 -0
  124. data/lib/insika/http_client.rb +63 -0
  125. data/lib/insika/inbound_log.rb +84 -0
  126. data/lib/insika/llm_configurator.rb +99 -0
  127. data/lib/insika/llm_provider_store.rb +83 -0
  128. data/lib/insika/mcp_http_client.rb +67 -0
  129. data/lib/insika/mcp_store.rb +115 -0
  130. data/lib/insika/mcp_tool_ingestor.rb +143 -0
  131. data/lib/insika/memory_store.rb +93 -0
  132. data/lib/insika/message_origin.rb +76 -0
  133. data/lib/insika/middleware.rb +36 -0
  134. data/lib/insika/model_policy.rb +52 -0
  135. data/lib/insika/model_resolver.rb +176 -0
  136. data/lib/insika/model_selection.rb +114 -0
  137. data/lib/insika/onboarding.rb +208 -0
  138. data/lib/insika/outbox_store.rb +166 -0
  139. data/lib/insika/overlay_tool_registry.rb +103 -0
  140. data/lib/insika/pack.rb +102 -0
  141. data/lib/insika/pack_importer.rb +121 -0
  142. data/lib/insika/pending_action_store.rb +120 -0
  143. data/lib/insika/plugin/loader.rb +356 -0
  144. data/lib/insika/plugin.rb +35 -0
  145. data/lib/insika/policy/engine.rb +83 -0
  146. data/lib/insika/policy/policy.rb +120 -0
  147. data/lib/insika/policy_registry.rb +23 -0
  148. data/lib/insika/profile_source.rb +137 -0
  149. data/lib/insika/prompt_catalog.rb +61 -0
  150. data/lib/insika/queue_policy.rb +167 -0
  151. data/lib/insika/recovery.rb +127 -0
  152. data/lib/insika/refinement/candidate.rb +159 -0
  153. data/lib/insika/refinement/evidence_collector.rb +371 -0
  154. data/lib/insika/refinement/gate.rb +234 -0
  155. data/lib/insika/refinement/panel.rb +222 -0
  156. data/lib/insika/refinement/proposer.rb +262 -0
  157. data/lib/insika/refinement_store.rb +295 -0
  158. data/lib/insika/registry.rb +59 -0
  159. data/lib/insika/safety/config.rb +109 -0
  160. data/lib/insika/safety/detectors.rb +176 -0
  161. data/lib/insika/safety/factory.rb +102 -0
  162. data/lib/insika/safety/input_guardrail.rb +87 -0
  163. data/lib/insika/safety/moderator.rb +86 -0
  164. data/lib/insika/safety/output_filter.rb +79 -0
  165. data/lib/insika/safety/output_validator.rb +101 -0
  166. data/lib/insika/safety/safe_responses.rb +47 -0
  167. data/lib/insika/sandbox/boundary.rb +93 -0
  168. data/lib/insika/sandbox/docker.rb +74 -0
  169. data/lib/insika/sandbox/local.rb +33 -0
  170. data/lib/insika/sandbox/runner.rb +80 -0
  171. data/lib/insika/sandbox.rb +85 -0
  172. data/lib/insika/schema_guard.rb +147 -0
  173. data/lib/insika/secret_masking.rb +34 -0
  174. data/lib/insika/server/a2a/agent_card.rb +27 -0
  175. data/lib/insika/server/a2a/app.rb +112 -0
  176. data/lib/insika/server/a2a/client.rb +101 -0
  177. data/lib/insika/server/a2a/errors.rb +32 -0
  178. data/lib/insika/server/a2a/http.rb +42 -0
  179. data/lib/insika/server/a2a/message.rb +27 -0
  180. data/lib/insika/server/a2a/protocol.rb +45 -0
  181. data/lib/insika/server/a2a/remotes.rb +25 -0
  182. data/lib/insika/server/a2a/task_projection.rb +40 -0
  183. data/lib/insika/server/admin_auth.rb +29 -0
  184. data/lib/insika/server/app.rb +850 -0
  185. data/lib/insika/server/boot.rb +119 -0
  186. data/lib/insika/server/rack_app.rb +110 -0
  187. data/lib/insika/server/responses.rb +155 -0
  188. data/lib/insika/server/sse_body.rb +96 -0
  189. data/lib/insika/session_actor.rb +162 -0
  190. data/lib/insika/session_store.rb +143 -0
  191. data/lib/insika/settings_store.rb +154 -0
  192. data/lib/insika/shutdown.rb +125 -0
  193. data/lib/insika/skill_catalog.rb +113 -0
  194. data/lib/insika/skill_store.rb +79 -0
  195. data/lib/insika/steer_injector.rb +110 -0
  196. data/lib/insika/store.rb +52 -0
  197. data/lib/insika/stores/memory.rb +123 -0
  198. data/lib/insika/stores/sqlite.rb +183 -0
  199. data/lib/insika/studio/app.rb +1571 -0
  200. data/lib/insika/studio/assets/dist/application.css +1 -0
  201. data/lib/insika/studio/assets/dist/application.js +69 -0
  202. data/lib/insika/studio/forms.rb +340 -0
  203. data/lib/insika/studio/nav_icons.rb +31 -0
  204. data/lib/insika/studio/views/_message.erb +44 -0
  205. data/lib/insika/studio/views/agent_detail.erb +285 -0
  206. data/lib/insika/studio/views/agents.erb +63 -0
  207. data/lib/insika/studio/views/approvals.erb +41 -0
  208. data/lib/insika/studio/views/chats.erb +34 -0
  209. data/lib/insika/studio/views/evals.erb +83 -0
  210. data/lib/insika/studio/views/home.erb +72 -0
  211. data/lib/insika/studio/views/layout.erb +94 -0
  212. data/lib/insika/studio/views/login.erb +17 -0
  213. data/lib/insika/studio/views/mcp.erb +91 -0
  214. data/lib/insika/studio/views/not_found.erb +5 -0
  215. data/lib/insika/studio/views/playground.erb +47 -0
  216. data/lib/insika/studio/views/refinement.erb +234 -0
  217. data/lib/insika/studio/views/session.erb +62 -0
  218. data/lib/insika/studio/views/settings.erb +173 -0
  219. data/lib/insika/studio/views/skills.erb +86 -0
  220. data/lib/insika/studio/views/system_files.erb +65 -0
  221. data/lib/insika/studio/views/task.erb +105 -0
  222. data/lib/insika/studio/views/tasks.erb +33 -0
  223. data/lib/insika/studio/views/tool_edit.erb +107 -0
  224. data/lib/insika/studio/views/tools.erb +89 -0
  225. data/lib/insika/subagent_graph.rb +96 -0
  226. data/lib/insika/system_file_store.rb +96 -0
  227. data/lib/insika/task_actor.rb +128 -0
  228. data/lib/insika/task_store.rb +250 -0
  229. data/lib/insika/telemetry/pricing.rb +104 -0
  230. data/lib/insika/telemetry/recorder.rb +228 -0
  231. data/lib/insika/telemetry.rb +127 -0
  232. data/lib/insika/testing/store_contract.rb +270 -0
  233. data/lib/insika/token_estimator.rb +16 -0
  234. data/lib/insika/tool_assembly.rb +140 -0
  235. data/lib/insika/tool_catalog.rb +89 -0
  236. data/lib/insika/tool_definition.rb +518 -0
  237. data/lib/insika/tool_envelope.rb +140 -0
  238. data/lib/insika/tool_manifest.rb +218 -0
  239. data/lib/insika/tool_registry.rb +21 -0
  240. data/lib/insika/tool_store.rb +135 -0
  241. data/lib/insika/tool_trace_store.rb +92 -0
  242. data/lib/insika/tools/a2a_remote.rb +48 -0
  243. data/lib/insika/tools/agent_enum.rb +68 -0
  244. data/lib/insika/tools/concurrency.rb +54 -0
  245. data/lib/insika/tools/data_defined_tool.rb +220 -0
  246. data/lib/insika/tools/load_skill.rb +41 -0
  247. data/lib/insika/tools/remember.rb +53 -0
  248. data/lib/insika/tools/subagent.rb +75 -0
  249. data/lib/insika/tools/subagents.rb +77 -0
  250. data/lib/insika/tools/tool_search.rb +94 -0
  251. data/lib/insika/turn_output.rb +139 -0
  252. data/lib/insika/turn_state.rb +158 -0
  253. data/lib/insika/turn_timing.rb +56 -0
  254. data/lib/insika/usage_ledger.rb +47 -0
  255. data/lib/insika/version.rb +3 -1
  256. data/lib/insika/wiring/graph.rb +198 -0
  257. data/lib/insika/workflow.rb +185 -0
  258. data/lib/insika/workflow_registry.rb +33 -0
  259. data/lib/insika.rb +203 -4
  260. metadata +395 -8
@@ -0,0 +1,365 @@
1
+ ---
2
+ title: Observability
3
+ parent: Operate & prove it
4
+ nav_order: 1
5
+ permalink: /observability/
6
+ ---
7
+
8
+ # Observability — OpenTelemetry (opt-in)
9
+
10
+ Insika already has an observability spine: the **event stream**. Every turn emits
11
+ structured events (`task_started`, `tool_call`/`tool_result`, `data_tool_call`,
12
+ `task_completed`/`task_failed`/`task_cancelled`), each stamped with
13
+ `task_id`/`session_id`/`seq`/`at`. The OpenTelemetry bridge is a **consumer** of
14
+ that stream: it translates the events into OTEL **spans** and **metrics**. The core
15
+ (Executor, tools) never gains an OTEL call — events observe, telemetry translates.
16
+ It's the same "events observe" principle the SSE surface already uses.
17
+
18
+ Not every event on the stream is a turn. Operator actions, refinement runs
19
+ (`:refinement_started`, `:refinement_report`, `:refinement_proposed`,
20
+ `:refinement_gated`, `:refinement_applied`, `:refinement_rejected` — see
21
+ [Refinement](REFINEMENT.md)),
22
+ authoring writes (`:golden_written`, `:agent_file_written`, …), queue bookkeeping
23
+ (`:turn_coalesced`, `:turn_steered`, `:turn_steer_released`, `:turn_interrupted` — see
24
+ [Agents](AGENTS.md#queue_mode--when-a-message-arrives-while-the-agent-is-busy)) and
25
+ channel delivery (`:channel_delivered` — see [Channels](CHANNELS.md))
26
+ travel the same stream and are **ignored** by the bridge: they open no span and
27
+ touch no instrument, because they are not part of a turn's latency or cost. Any
28
+ other subscriber still sees them.
29
+
30
+ They are worth subscribing to even so, because each is the ONLY record of
31
+ something that left no task of its own behind:
32
+
33
+ | Event | Data | What it answers |
34
+ |---|---|---|
35
+ | `:turn_coalesced` | `task_id`, `merged`, `arrivals[]` | the fragments a customer typed in a row arrived as separate messages, and when |
36
+ | `:turn_steered` | `task_id`, `count`, `total` | a message arrived mid-run and was appended to the turn in flight |
37
+ | `:turn_steer_released` | `task_id`, `released_as`, `count` | the run could not absorb it, so it became the turn `released_as` |
38
+ | `:turn_interrupted` | `task_id`, `replaced_by` | the turn was abandoned mid-run, and which turn replaced it |
39
+ | `:channel_delivered` | `channel`, `outbox_id`, `status`, `attempts`, `error` | the answer reached the platform (or did not) — the turn completing says nothing about that |
40
+
41
+ `:channel_delivered` is the one worth alerting on: a turn can be `:task_completed`
42
+ and correct while the customer got nothing, because delivery is a separate,
43
+ retried, out-of-band step. `status: "failed"` means the reply is sitting in the
44
+ outbox and the customer is still waiting.
45
+
46
+ Counts, ids and times only — never message content. The text lives in the
47
+ transcript, which is the surface that is allowed to carry it.
48
+
49
+ The bridge speaks the standard the market already runs on: point any OTLP backend
50
+ at Insika and a real turn shows up as a full trace, next to counters and histograms
51
+ you can chart without touching a span.
52
+
53
+ **This page is a convention, not an integration.** Insika ships no dashboard, no
54
+ backend config, no vendor file. It ships a stable set of attribute and instrument
55
+ names, and the recipes below tell you what to chart against them — in whatever you
56
+ already run.
57
+
58
+ ## Contents
59
+
60
+ - [Turning it on](#turning-it-on-opt-in-parity-when-off)
61
+ - [Traces: the span reference](#traces-the-span-reference)
62
+ - [Metrics: the instrument reference](#metrics-the-instrument-reference)
63
+ - [Attribute reference](#attribute-reference)
64
+ - [Estimated cost](#estimated-cost)
65
+ - [Dashboards you can build](#dashboards-you-can-build)
66
+ - [Stability contract](#stability-contract)
67
+ - [Local: a standalone collector](#local-a-standalone-collector)
68
+ - [Production](#production)
69
+ - [Design (why it's safe)](#design-why-its-safe)
70
+
71
+ ## Turning it on (opt-in, parity when off)
72
+
73
+ Enabled by environment — no new code flag:
74
+
75
+ - `INSIKA_OTEL=1`, **or**
76
+ - the standard OTEL envs (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_TRACES_EXPORTER`).
77
+
78
+ Destination, protocol and headers follow the **OTEL SDK's default config** (env):
79
+ point `OTEL_EXPORTER_OTLP_ENDPOINT` at your collector. `OTEL_SERVICE_NAME` names
80
+ the service (default `insika`).
81
+
82
+ Metrics ride the same switch and the same standard env — no Insika-specific toggle
83
+ is invented for them. `OTEL_METRICS_EXPORTER=none` turns metrics off while traces
84
+ stay on; `OTEL_METRIC_EXPORT_INTERVAL` (ms) sets the export period. If the metrics
85
+ SDK is not in the bundle at all, the bridge degrades to traces only rather than
86
+ failing to boot.
87
+
88
+ **Off (the default):** `Insika::Telemetry.setup` returns `nil`, the OTEL gems are
89
+ **never loaded** (lazy `require`, like the LLM client), and nothing is
90
+ instrumented — zero overhead. This is enforced by a test
91
+ (`spec/insika/load_guard_spec.rb`: "require insika does not load
92
+ OpenTelemetry").
93
+
94
+ ## Traces: the span reference
95
+
96
+ One span per **turn**, with **child spans** per tool, correlated by `task_id`.
97
+ Latency is the span duration, reconstructed from the events' real `at` timestamps.
98
+
99
+ | Span | Emitted for | Parent |
100
+ |------|-------------|--------|
101
+ | `insika.turn` | one per turn, opened on `task_started`, closed on the terminal event | root |
102
+ | `insika.tool` | one per tool call, `tool_call` → `tool_result` (FIFO-correlated) | `insika.turn` |
103
+ | `insika.data_tool` | one per data-tool call — point-in-time (the engine emits a single event) | `insika.turn` |
104
+
105
+ A turn that ends with `task_failed` also carries the OTEL **error status**, with
106
+ the failure message.
107
+
108
+ ## Metrics: the instrument reference
109
+
110
+ The same events feed instruments, so volume, latency, tokens and cost are chartable
111
+ **without aggregating spans** — which not every backend does, and none does cheaply
112
+ at retention. Metrics are recorded on the *terminal* event, so every point already
113
+ knows its outcome.
114
+
115
+ | Instrument | Type | Unit | Recorded when |
116
+ |------------|------|------|----------------|
117
+ | `insika.turns` | counter | `{turn}` | a turn reaches a terminal state |
118
+ | `insika.turn.duration` | histogram | `s` | same, when both timestamps are known |
119
+ | `insika.tokens` | counter | `{token}` | the turn reported usage |
120
+ | `insika.cost` | counter | `{USD}` | the turn's model is priced (see below) |
121
+ | `insika.tool.calls` | counter | `{call}` | a tool call completes |
122
+ | `insika.tool.duration` | histogram | `s` | a `tool_call`/`tool_result` pair completes |
123
+
124
+ `insika.tool.duration` is deliberately **not** recorded for data-tools: those are a
125
+ single point-in-time event, so there is no measured duration to report. A tool left
126
+ open by a mid-turn failure is not counted as a completed call either — its span is
127
+ closed, but a failed call must not inflate the success histogram.
128
+
129
+ ## Attribute reference
130
+
131
+ The same names are used on spans and on metrics. **Metrics carry a deliberate
132
+ low-cardinality subset** — `task_id` and `session_id` are span-only, because a
133
+ metric attribute with per-turn cardinality is how you destroy a metrics backend.
134
+
135
+ | Attribute | Type | On spans | On metrics | Meaning |
136
+ |-----------|------|----------|------------|---------|
137
+ | `insika.task_id` | string | `turn` | — | the turn's id (correlates with `/v1/responses`, the Studio, the task store) |
138
+ | `insika.session_id` | string | `turn` | — | the chat this turn belongs to |
139
+ | `insika.agent` | string | `turn` | all | the agent profile that ran the turn |
140
+ | `insika.tenant` | string | `turn` | all | the **operator-set** tenant (see below); absent when the command declared none |
141
+ | `insika.command` | string | `turn` | all | command type (`send_message`, `trigger_workflow`, …) |
142
+ | `insika.status` | string | `turn` | turn instruments | `ok` / `error` / `cancelled` / `abandoned` |
143
+ | `insika.model` | string | `turn` | all except tool | model id the provider reported |
144
+ | `insika.model_source` | string | `turn` | — | which config layer resolved the model (chat / agent / model / global) |
145
+ | `insika.tokens.input` | int | `turn` | — | input tokens (**includes** the cached ones) |
146
+ | `insika.tokens.output` | int | `turn` | — | output tokens |
147
+ | `insika.tokens.total` | int | `turn` | — | input + output |
148
+ | `insika.tokens.cached` | int | `turn` | — | cache **reads**, a subset of `tokens.input` |
149
+ | `insika.tokens.cache_creation` | int | `turn` | — | cache **writes**, *not* inside `tokens.input` |
150
+ | `insika.token.type` | string | — | `insika.tokens` | `input` / `output` / `cached` / `cache_creation` |
151
+ | `insika.cost.usd` | double | `turn` | — | estimated cost of the turn (span attribute; the metric is the `insika.cost` counter) |
152
+ | `insika.tool` | string | `tool`, `data_tool` | tool instruments | tool name |
153
+ | `insika.tool.kind` | string | — | tool instruments | `tool` / `data_tool` |
154
+ | `insika.http.status` | int | `data_tool` | `insika.tool.calls` | HTTP status the data-tool got back |
155
+
156
+ ### `insika.tenant`
157
+
158
+ The tenant is the **explicit** tenant of the Command (`Command.build(…, tenant:)`)
159
+ — the one grouping label an operator sets deliberately, typically the merchant,
160
+ workspace or customer the turn belongs to. It is **not** the memory scope, which
161
+ falls back to the chat id: putting per-chat cardinality on a metric attribute is
162
+ exactly the failure this distinction avoids. A command with no tenant emits **no
163
+ attribute at all**, rather than a null or an `"unknown"` bucket.
164
+
165
+ ### Cardinality budget
166
+
167
+ Metric cardinality is roughly `agents × tenants × models × statuses × commands`
168
+ (and `agents × tenants × tools` for the tool instruments). All of those are
169
+ operator-controlled and closed-ish sets. Keep them that way: if you find yourself
170
+ wanting per-chat or per-user metrics, that is a **trace** query, and the span
171
+ attributes are there for it.
172
+
173
+ ## Estimated cost
174
+
175
+ Insika ships **no prices**. They change weekly, differ per contract and per region,
176
+ and a stale table inside the engine would be worse than no number. You declare the
177
+ rates; Insika multiplies. Unset, no cost is reported anywhere.
178
+
179
+ Set `INSIKA_MODEL_PRICING` to a JSON object of model id → rates in **USD per
180
+ million tokens**:
181
+
182
+ ```bash
183
+ INSIKA_MODEL_PRICING='{
184
+ "deepseek-chat": {"input": 0.27, "output": 1.10, "cached_input": 0.07},
185
+ "claude-sonnet-4-5": {"input": 3.00, "output": 15.00, "cached_input": 0.30, "cache_write": 3.75}
186
+ }'
187
+ ```
188
+
189
+ - A key matches the model id the provider reports, **with or without** the
190
+ `provider/` prefix — `deepseek/deepseek-chat` and `deepseek-chat` both hit the
191
+ same entry.
192
+ - `input` / `output` are required (one of them is enough for the entry to load).
193
+ - `cached_input`, when given, bills cache **reads** at that rate and subtracts them
194
+ from the fresh input. Omit it and cached tokens simply stay at the input rate.
195
+ - `cache_write`, when given, bills cache **creation** tokens at that rate. Omit it
196
+ and they are billed at the input rate.
197
+ - An **unpriced model reports nothing** — no attribute, no metric point. A missing
198
+ price is not a zero cost, and a dashboard should show the gap.
199
+ - A malformed table degrades to "no cost". Telemetry config can never stop a boot.
200
+
201
+ The number is an **estimate for trend and attribution**, not a bill. Reconcile
202
+ against your provider's invoice, never the other way round.
203
+
204
+ ## Dashboards you can build
205
+
206
+ Written against the convention, not against a product. Each recipe is
207
+ *instrument → aggregation → group-by*; the PromQL line is one illustration of the
208
+ shape, and translates directly to whatever query language your backend uses.
209
+
210
+ > **Names get normalized.** Prometheus-style backends rewrite OTLP names: dots
211
+ > become underscores, counters gain `_total`, and a real (non-annotation) unit is
212
+ > appended — so `insika.turn.duration` in `s` becomes
213
+ > `insika_turn_duration_seconds`. Annotation units like `{turn}` are dropped.
214
+ > Check your exporter's mapping; the convention below is the OTLP spelling.
215
+
216
+ **Turn volume by agent**
217
+ `insika.turns`, rate, grouped by `insika.agent`.
218
+ ```promql
219
+ sum by (insika_agent) (rate(insika_turns_total[5m]))
220
+ ```
221
+
222
+ **Error rate by agent** — the single most useful panel.
223
+ `insika.turns` filtered to `insika.status="error"`, over the same counter unfiltered.
224
+ ```promql
225
+ sum by (insika_agent) (rate(insika_turns_total{insika_status="error"}[5m]))
226
+ / sum by (insika_agent) (rate(insika_turns_total[5m]))
227
+ ```
228
+
229
+ **Latency p95 by agent**
230
+ `insika.turn.duration`, 95th percentile, grouped by `insika.agent`.
231
+ ```promql
232
+ histogram_quantile(0.95, sum by (le, insika_agent) (rate(insika_turn_duration_seconds_bucket[5m])))
233
+ ```
234
+ Turn latency is dominated by the provider, not by the engine — see
235
+ [BENCHMARK.md](BENCHMARK.md) for the engine's own overhead, which is sub-millisecond
236
+ and will not show up here.
237
+
238
+ **Token burn by model**
239
+ `insika.tokens`, rate, grouped by `insika.model` and `insika.token.type`. Splitting
240
+ by type is what makes the cache visible: a healthy prompt cache shows `cached`
241
+ climbing while `input` stays flat.
242
+
243
+ **Cache hit ratio**
244
+ `insika.tokens` filtered to `insika.token.type="cached"` over the same counter
245
+ filtered to `input`. This is the number that moves your bill.
246
+
247
+ **Spend per tenant**
248
+ `insika.cost`, rate (or `increase` over a billing window), grouped by
249
+ `insika.tenant`. Swap the group-by for `insika.agent` to get spend per agent — the
250
+ same attribution the per-agent token ceilings in [SECURITY.md](SECURITY.md) act on.
251
+
252
+ **Tool reliability**
253
+ `insika.tool.calls`, rate, grouped by `insika.tool`; for data-tools add
254
+ `insika.http.status` to see which upstream is failing. Pair it with
255
+ `insika.tool.duration` p95 grouped by `insika.tool` to find the slow one.
256
+
257
+ **Workflows vs chats**
258
+ Any turn instrument grouped by `insika.command` — `trigger_workflow` and
259
+ `send_message` have very different latency and token profiles, and mixing them in
260
+ one average hides both.
261
+
262
+ **From a chart to the actual conversation**
263
+ Every panel above is grouped by attributes that also exist on the `insika.turn`
264
+ span. Filter your trace view by the same `insika.agent` / `insika.tenant` /
265
+ `insika.status`, open a trace, and `insika.task_id` and `insika.session_id` take you
266
+ to the exact turn in the Studio.
267
+
268
+ ## Stability contract
269
+
270
+ These names are an interface. Dashboards, alerts and recording rules are built on
271
+ top of them, and renaming one breaks all of them silently — a chart does not error,
272
+ it just goes flat.
273
+
274
+ - Instrument names, units and attribute keys on this page are **stable**. They
275
+ change only in a major version, and only with a note in `CHANGELOG.md`.
276
+ - Growth is **additive**: new attributes and new instruments may appear in a minor
277
+ version. Do not write a query that assumes a fixed attribute set.
278
+ - An attribute whose value is unknown is **omitted**, never emitted as `null`,
279
+ `""` or `"unknown"`. Handle absence in your queries rather than expecting a
280
+ placeholder bucket.
281
+ - Everything under `insika.*` is ours. Standard OTEL resource attributes
282
+ (`service.name`, and so on) come from the SDK and follow OTEL's own conventions.
283
+
284
+ ## Local: a standalone collector
285
+
286
+ To see traces on your machine, run a standalone OTLP collector — the quickest is
287
+ Jaeger all-in-one (OTLP on `4318`, UI on `16686`):
288
+
289
+ ```bash
290
+ docker run --rm -d --name jaeger \
291
+ -p 16686:16686 -p 4318:4318 \
292
+ jaegertracing/all-in-one:latest
293
+ ```
294
+
295
+ Then boot the engine with OTEL enabled, pointed at the collector:
296
+
297
+ ```bash
298
+ INSIKA_OTEL=1 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
299
+ DEEPSEEK_API_KEY=sk-... bundle exec ruby scripts/serve_real.rb
300
+ ```
301
+
302
+ `serve_real` prints `OTEL → on (traces + metrics to OTLP)` on boot. Chat at
303
+ `http://localhost:9292/studio` and open the traces at
304
+ **`http://localhost:16686`** (service `insika`): one `insika.turn` span per turn,
305
+ with `insika.tool`/`insika.data_tool` children and the attributes above. Stop the
306
+ collector with `docker rm -f jaeger`.
307
+
308
+ Jaeger stores traces only — to see the metrics too, point the same endpoint at an
309
+ OTLP collector that fans out to a metrics store, or add `OTEL_METRICS_EXPORTER=none`
310
+ to silence the metrics exporter's retries while you work on traces.
311
+
312
+ > We don't version a `docker-compose` file — the collector is standalone, a
313
+ > local run-it decision. The one-liner above is the whole recipe.
314
+
315
+ ## Production
316
+
317
+ Set the OTEL envs on the deployment and every worker exports to your collector:
318
+
319
+ ```bash
320
+ INSIKA_OTEL=1
321
+ OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-collector>:4318
322
+ OTEL_SERVICE_NAME=insika # optional; names the service
323
+ INSIKA_MODEL_PRICING='{...}' # optional; unlocks the cost attribute + counter
324
+ # plus any standard OTEL_EXPORTER_OTLP_HEADERS your backend needs
325
+ ```
326
+
327
+ The bridge is attached once, at boot, on the serving reactor — in `config.ru`,
328
+ `scripts/serve_real.rb` and the DSL's `serve` (so `Insika.agent { … }.serve` exports
329
+ too). It subscribes to the event stream in a long-lived fiber, sibling to serving.
330
+
331
+ ## Design (why it's safe)
332
+
333
+ - **`Telemetry::Recorder`** — pure: translates event → spans and instruments against
334
+ a duck-typed `tracer` (`start_span`/`set_attribute`/`record_error`/`finish`) and
335
+ `meter` (`create_counter`/`create_histogram`). It does not reference
336
+ `OpenTelemetry::` and is unit-tested with fakes. `record` **never raises**
337
+ (telemetry must not bring down a turn); orphan events are ignored; tool
338
+ correlation is FIFO; abandoned turns (e.g. `kill -9`, no terminal event) are
339
+ bounded and evicted (`MAX_OPEN`) and counted as `status="abandoned"`.
340
+ - **`Telemetry::Pricing`** — pure: an operator-declared rates table times the turn's
341
+ usage. No network, no bundled price list, no exception path.
342
+ - **`Telemetry.setup`** — the gem boundary: lazy `require` + configures the SDK +
343
+ returns a `Recorder` wired to the real tracer and meter. Covered by a smoke test
344
+ against the **real OTEL SDK** with in-memory exporters (span name/attributes/
345
+ hierarchy/error status; instrument names/units/attributes).
346
+ - **No reader means no meter.** If every metric reader is disabled, `setup` injects
347
+ no meter at all. Recording into a provider nothing drains would accumulate a point
348
+ per attribute set forever — a slow leak dressed as telemetry.
349
+ - **`Telemetry.attach`** — subscribes the recorder to the event stream inside the
350
+ reactor. No-op when the recorder is `nil` (disabled path), before touching the
351
+ reactor.
352
+
353
+ ## Backpressure
354
+
355
+ Each event-stream subscription is capped at 1000 buffered events; a telemetry
356
+ consumer that fell far behind would have its subscription closed (telemetry stops,
357
+ the turn does not). Span and instrument operations are cheap, so there's ample
358
+ headroom.
359
+
360
+ ---
361
+
362
+ *Packaging note.* Today the bridge lives in the Insika repo as an opt-in core
363
+ module — no separate install, no separate versioning. When Insika extracts its
364
+ subsystems into gems it becomes `insika-otel`; because the bridge is already a
365
+ pure event-stream consumer with a single gem boundary, that cut lands clean.
data/docs/PLUGINS.md ADDED
@@ -0,0 +1,211 @@
1
+ ---
2
+ title: Plugins
3
+ parent: Build an agent
4
+ nav_order: 7
5
+ permalink: /plugins/
6
+ ---
7
+
8
+ # Plugins
9
+
10
+ Insika has **two tiers of extension**, and picking the right one is almost always
11
+ obvious once you ask a single question: *does this need Ruby to run in-process?*
12
+
13
+ | | **Tier 1 — data** | **Tier 2 — code** |
14
+ |---|---|---|
15
+ | What you write | JSON/YAML config | a Ruby gem (or a directory) |
16
+ | Ships as | a row in SQLite | a `.rb` file loaded at boot |
17
+ | Takes effect | **hot** — no restart | at the next restart |
18
+ | Can add | HTTP tools, MCP toolsets, skills, prompts | tools, workflows, capabilities, policies, middleware, hooks, context providers |
19
+ | Reach for it when | you are calling an external API | logic must run in-process, or you are extending the engine itself |
20
+
21
+ Tier 1 is the path for "the community adds integrations". Tier 2 is the path for
22
+ "the community extends the engine". Most integrations are tier 1, and you should
23
+ feel mildly suspicious of yourself when you reach past it.
24
+
25
+ ## Tier 1 — extend with data
26
+
27
+ Nothing to install and nothing to deploy: a **data tool** is an HTTP call
28
+ described by config, and an **MCP import** turns a whole MCP server's toolset
29
+ into data tools in one call. Both are covered in [Tools](TOOLS.md) — the schema,
30
+ the `{{param}}` / `{{ctx.*}}` / `{{secret.*}}` placeholders, the four write paths,
31
+ and the egress guard.
32
+
33
+ Skills are the other half of tier 1: a `SKILL.md` is knowledge, not code, and it
34
+ can be authored in the Studio or shipped inside a pack. See [Skills](SKILLS.md).
35
+
36
+ ## Tier 2 — extend with code
37
+
38
+ A code plugin is **a directory with a manifest**. The manifest is discovery
39
+ without execution: Insika reads and validates it first, and only then requires
40
+ your Ruby.
41
+
42
+ ```
43
+ my-plugin/
44
+ ├── insika.plugin.yml # the manifest — always read first
45
+ ├── plugin.rb # the entry: defines a module with .register(api)
46
+ └── skills/ # optional: SKILL.md files shipped with the plugin
47
+ ```
48
+
49
+ ```yaml
50
+ # insika.plugin.yml
51
+ id: weather # unique; the id everything else keys on
52
+ name: Weather
53
+ description: Weather lookups. Ships the get_weather tool and a weather_report skill.
54
+ entry: plugin.rb # required to register anything; omit for a skills-only plugin
55
+ module: WeatherPlugin # must respond to .register(api)
56
+ contracts: # the PUBLIC surface — declared here or ignored
57
+ tools: [get_weather]
58
+ workflows: []
59
+ capabilities: []
60
+ channels: []
61
+ tool_metadata:
62
+ get_weather:
63
+ optional: false # optional tools require per-agent opt-in
64
+ side_effect: false # true ⇒ not re-run on resume (checkpointed)
65
+ skills: [skills] # directories, relative to the plugin root
66
+ prompts: []
67
+ ```
68
+
69
+ ```ruby
70
+ # plugin.rb
71
+ require "ruby_llm"
72
+
73
+ module WeatherPlugin
74
+ class GetWeather < RubyLLM::Tool
75
+ description "Looks up the current weather for a city"
76
+ param :city, desc: "City name"
77
+
78
+ def execute(city:) = { city: city, temp_c: 24, condition: "sunny" }
79
+ end
80
+
81
+ def self.register(api)
82
+ api.register_tool("get_weather", GetWeather)
83
+ end
84
+ end
85
+ ```
86
+
87
+ Two runnable ones live in the repo:
88
+ [`plugins/weather`](https://github.com/guizaols/insika/tree/main/plugins/weather)
89
+ (the minimal shape) and
90
+ [`plugins/insika-code`](https://github.com/guizaols/insika/tree/main/plugins/insika-code)
91
+ (a real toolset: file read/write/edit, grep, shell — sandboxed, with the
92
+ side-effecting tools marked so an agent can gate them behind approval).
93
+
94
+ ### What `register(api)` can register
95
+
96
+ | Call | Registers | Declared in `contracts`? |
97
+ |---|---|---|
98
+ | `register_tool(name, klass)` | a tool the model can call | **yes** — `contracts.tools` |
99
+ | `register_workflow(name, callable)` | a named workflow (see [Architecture](ARCHITECTURE.md)) | **yes** — `contracts.workflows` |
100
+ | `register_capability(name, tool:)` | an intent that resolves to a tool | **yes** — `contracts.capabilities` |
101
+ | `register_channel(name, instance)` | a way in and out for people (see [Channels](CHANNELS.md)) | **yes** — `contracts.channels` |
102
+ | `register_policy(name, klass)` | a policy for the resolution stage | no |
103
+ | `register_middleware(instance)` | a wrap around the turn pipeline | no |
104
+ | `register_context_provider(instance)` | a source of prompt context | no |
105
+ | `register_hook(:tool, before:, after:)` | alters one stage's input/output (`:task`, `:prompt`, `:agent`, `:tool`) | no |
106
+
107
+ Anything addressable by name must be declared in `contracts`; registering an
108
+ undeclared name logs a warning and is **ignored**, so a plugin cannot quietly
109
+ widen its own surface between versions. For a channel the name is also a URL
110
+ segment (`/channels/<name>/…`), so the declaration is what stops a plugin
111
+ from mounting a route nobody asked for.
112
+
113
+ `api.config` returns the manifest's `config` hash, frozen.
114
+
115
+ ### Failure is contained, and quiet
116
+
117
+ Registration is staged and committed atomically: if `register(api)` raises
118
+ halfway through, everything it staged is rolled back and the plugin is
119
+ discarded. **Boot continues** — one bad plugin must not take the deployment
120
+ down.
121
+
122
+ > ⚠️ The cost of that choice: a broken plugin is a `warn` on stderr, not a crash.
123
+ > If a tool is missing, check the boot log and the `:plugin_loaded` events before
124
+ > suspecting the allowlist.
125
+
126
+ The same posture applies to config: if `config_schema` is present and `config`
127
+ fails it, the plugin is **skipped** (fail-closed) with the validation errors
128
+ printed. The validator is a deliberate subset of JSON Schema — `type`,
129
+ `properties`, `required`, `additionalProperties`, `enum` — and an unsupported
130
+ keyword is itself an error rather than being silently ignored.
131
+
132
+ ### Discovery and enabling
133
+
134
+ Plugins come from three kinds of root, and they differ in **who has to say yes**:
135
+
136
+ | Root | How it is found | Enabled by default |
137
+ |---|---|---|
138
+ | **Gem** | the gem calls `Insika::Plugin.announce(__dir__)` when its `lib/` loads | **yes** — installing it is the consent |
139
+ | **Workspace** | a directory in the deployment's plugin roots | no — must be listed in `enabled:` |
140
+ | **Bundled** | `plugins/` in this repo | no — must be listed in `enabled:` |
141
+
142
+ `disabled:` is an absolute veto: an id listed there never loads, even if it is
143
+ also in `enabled:` (deny wins, the same rule as every allowlist in the engine).
144
+
145
+ A gem announces itself explicitly — Insika never scans the load path or your
146
+ installed gems:
147
+
148
+ ```ruby
149
+ # lib/insika-plugin-acme.rb
150
+ require "insika/plugin"
151
+ Insika::Plugin.announce(File.expand_path("../..", __dir__))
152
+ ```
153
+
154
+ Discovery details worth knowing: the manifest may be named `insika.plugin.yml`
155
+ (preferred) or `plugin.yml` (deprecated — it warns); one manifest per directory,
156
+ with `insika.plugin.yml` winning if both exist; and if two roots ship the same
157
+ `id`, the **first root wins** and the second is skipped.
158
+
159
+ ### Secrets
160
+
161
+ Put the *name* of the environment variable in the manifest, never the value:
162
+
163
+ ```yaml
164
+ config:
165
+ api_key_env: ACME_API_KEY
166
+ config_schema:
167
+ type: object
168
+ required: [api_key_env]
169
+ properties:
170
+ api_key_env: { type: string }
171
+ ```
172
+
173
+ The manifest is committed; the secret is not. This mirrors how data tools handle
174
+ `{{secret.*}}`.
175
+
176
+ ## Publishing a plugin
177
+
178
+ - **Name it `insika-plugin-<thing>`.** The convention *is* the registry for now:
179
+ a predictable RubyGems prefix plus a curated list here beats a hub nobody has
180
+ had a reason to build yet.
181
+ - **Announce in your gem's entry file** (above), so installing it is enough.
182
+ - **Treat `contracts` as your public API.** Renaming a tool, changing its
183
+ parameters, or adding a `required` config key are breaking changes for the
184
+ agents that allowlist them by name. Version accordingly.
185
+ - **Do not require `insika` at load time** if you can avoid it —
186
+ `insika/plugin` is deliberately dependency-free so a plugin gem can announce
187
+ itself before anything else of ours is loaded.
188
+
189
+ > **Compatibility, stated honestly:** Insika is pre-1.0 and nothing is tagged
190
+ > yet. The manifest keys and the `register(api)` surface above are what a plugin
191
+ > depends on; changes to them are listed in
192
+ > [`CHANGELOG.md`](https://github.com/guizaols/insika/blob/main/CHANGELOG.md).
193
+ > Until 1.0, pin the engine version you tested against.
194
+
195
+ ## Choosing a tier
196
+
197
+ | You want to… | Tier |
198
+ |---|---|
199
+ | call a REST API the model can invoke | **1** — data tool |
200
+ | adopt an existing MCP server's tools | **1** — MCP import |
201
+ | add domain knowledge or a procedure | **1** — a skill |
202
+ | touch the filesystem, run a subprocess, hold state in-process | **2** |
203
+ | add a policy, a middleware, or a context provider | **2** |
204
+ | package the above for other deployments to install | **2**, as a gem |
205
+
206
+ ## See also
207
+
208
+ - [Tools](TOOLS.md) — data tools, MCP ingestion, allowlists, and the egress guard.
209
+ - [Skills](SKILLS.md) — `SKILL.md`, progressive disclosure, and skill packs.
210
+ - [Sandbox](SANDBOX.md) — confining a code plugin that touches the filesystem or shell.
211
+ - [Architecture](ARCHITECTURE.md) — where plugins hook into the turn pipeline.