insika 0.3.0 → 0.8.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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -0
  3. data/README.md +48 -12
  4. data/bin/insika +725 -0
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +116 -406
  7. data/docs/API.md +5 -5
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +137 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +14 -14
  12. data/docs/CONTEXT.md +63 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +87 -10
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +128 -3
  17. data/docs/FACTS.md +3 -3
  18. data/docs/HARVEST.md +5 -6
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +17 -29
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +46 -12
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +222 -0
  26. data/docs/REFINEMENT.md +14 -9
  27. data/docs/RELEASING.md +4 -4
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +5 -5
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +23 -7
  32. data/docs/SKILLS.md +11 -2
  33. data/docs/SOAK.md +3 -3
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +176 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +4 -4
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +8 -5
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +99 -17
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/channel_delivery.rb +1 -1
  62. data/lib/insika/chat_builder.rb +50 -19
  63. data/lib/insika/commands/agent_payload.rb +2 -2
  64. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  65. data/lib/insika/commands/delete_artifact.rb +35 -0
  66. data/lib/insika/commands/delete_concept.rb +34 -0
  67. data/lib/insika/commands/delete_mcp.rb +6 -2
  68. data/lib/insika/commands/delete_tenant_data.rb +15 -3
  69. data/lib/insika/commands/gate_refinement.rb +1 -1
  70. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  71. data/lib/insika/commands/restore_concept.rb +34 -0
  72. data/lib/insika/commands/seed_demo_data.rb +31 -0
  73. data/lib/insika/commands/upsert_mcp.rb +6 -3
  74. data/lib/insika/commands/write_concept.rb +57 -0
  75. data/lib/insika/compaction.rb +196 -0
  76. data/lib/insika/context/builder.rb +6 -2
  77. data/lib/insika/context/fragment.rb +4 -1
  78. data/lib/insika/context/priority.rb +8 -0
  79. data/lib/insika/context/providers/briefing.rb +53 -24
  80. data/lib/insika/context/providers/knowledge.rb +108 -0
  81. data/lib/insika/context/providers/prompt.rb +30 -24
  82. data/lib/insika/context/providers/session.rb +46 -10
  83. data/lib/insika/context_trace_store.rb +11 -1
  84. data/lib/insika/cron.rb +189 -0
  85. data/lib/insika/demo/agent_attrs.rb +43 -0
  86. data/lib/insika/demo/golden_cases.rb +81 -0
  87. data/lib/insika/demo/seeder.rb +336 -0
  88. data/lib/insika/doctor.rb +280 -17
  89. data/lib/insika/dsl/definition.rb +3 -2
  90. data/lib/insika/dsl/runtime.rb +64 -79
  91. data/lib/insika/dsl/server_boot.rb +23 -1
  92. data/lib/insika/dsl/system.rb +10 -2
  93. data/lib/insika/dsl.rb +103 -2
  94. data/lib/insika/env_schema.rb +21 -7
  95. data/lib/insika/evals/golden.rb +41 -4
  96. data/lib/insika/evals/judge.rb +47 -2
  97. data/lib/insika/evals/pairwise.rb +11 -0
  98. data/lib/insika/evals/persona.rb +98 -0
  99. data/lib/insika/evals/runner.rb +9 -0
  100. data/lib/insika/evals/simulator.rb +225 -0
  101. data/lib/insika/evals/transport.rb +84 -2
  102. data/lib/insika/event_stream.rb +10 -0
  103. data/lib/insika/executor.rb +295 -55
  104. data/lib/insika/followup_policy.rb +2 -25
  105. data/lib/insika/golden_store.rb +16 -1
  106. data/lib/insika/grounding/matcher.rb +1 -1
  107. data/lib/insika/knowledge.rb +680 -0
  108. data/lib/insika/knowledge_store.rb +140 -0
  109. data/lib/insika/loop_detector.rb +5 -34
  110. data/lib/insika/mcp_client.rb +94 -0
  111. data/lib/insika/mcp_json.rb +74 -0
  112. data/lib/insika/mcp_live_tool.rb +43 -0
  113. data/lib/insika/mcp_store.rb +98 -26
  114. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  115. data/lib/insika/mcp_tool_registry.rb +100 -0
  116. data/lib/insika/media.rb +115 -31
  117. data/lib/insika/message_origin.rb +1 -1
  118. data/lib/insika/middleware.rb +9 -0
  119. data/lib/insika/onboarding.rb +17 -1
  120. data/lib/insika/outcome_store.rb +1 -1
  121. data/lib/insika/overlay_tool_registry.rb +37 -17
  122. data/lib/insika/packaging.rb +2 -2
  123. data/lib/insika/profile_source.rb +15 -1
  124. data/lib/insika/prompt_catalog.rb +10 -0
  125. data/lib/insika/retention.rb +36 -1
  126. data/lib/insika/router/app.rb +157 -0
  127. data/lib/insika/router/backend_pool.rb +98 -0
  128. data/lib/insika/router/hash_ring.rb +55 -0
  129. data/lib/insika/router/proxy_body.rb +34 -0
  130. data/lib/insika/router/session_key.rb +54 -0
  131. data/lib/insika/router.rb +18 -0
  132. data/lib/insika/schedule.rb +177 -0
  133. data/lib/insika/schedule_engine.rb +314 -0
  134. data/lib/insika/schedule_store.rb +208 -0
  135. data/lib/insika/server/app.rb +105 -15
  136. data/lib/insika/server/rack_app.rb +5 -1
  137. data/lib/insika/server/responses.rb +5 -5
  138. data/lib/insika/session_store.rb +34 -4
  139. data/lib/insika/settings_store.rb +8 -1
  140. data/lib/insika/skill_catalog.rb +12 -0
  141. data/lib/insika/soak/runner.rb +4 -4
  142. data/lib/insika/steer_injector.rb +21 -10
  143. data/lib/insika/studio/app.rb +591 -47
  144. data/lib/insika/studio/assets/dist/application.css +1 -1
  145. data/lib/insika/studio/assets/dist/application.js +21 -21
  146. data/lib/insika/studio/forms.rb +57 -5
  147. data/lib/insika/studio/nav_icons.rb +14 -1
  148. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  149. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  150. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  151. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  152. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  153. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  154. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  155. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  156. data/lib/insika/studio/views/_agents_master.erb +44 -0
  157. data/lib/insika/studio/views/_message.erb +49 -32
  158. data/lib/insika/studio/views/agent_detail.erb +61 -820
  159. data/lib/insika/studio/views/agents.erb +70 -57
  160. data/lib/insika/studio/views/artifact.erb +23 -0
  161. data/lib/insika/studio/views/artifacts.erb +59 -0
  162. data/lib/insika/studio/views/evals.erb +2 -2
  163. data/lib/insika/studio/views/facts.erb +1 -1
  164. data/lib/insika/studio/views/funnel.erb +1 -1
  165. data/lib/insika/studio/views/home.erb +106 -67
  166. data/lib/insika/studio/views/knowledge.erb +123 -0
  167. data/lib/insika/studio/views/layout.erb +14 -11
  168. data/lib/insika/studio/views/mcp.erb +174 -80
  169. data/lib/insika/studio/views/session.erb +231 -177
  170. data/lib/insika/studio/views/settings.erb +50 -1
  171. data/lib/insika/studio/views/skills.erb +1 -1
  172. data/lib/insika/studio/views/tools.erb +24 -9
  173. data/lib/insika/telemetry/recorder.rb +49 -1
  174. data/lib/insika/templates/browser-agent/README.md +36 -0
  175. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  176. data/lib/insika/templates/daily-digest/README.md +47 -0
  177. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  178. data/lib/insika/templates/repo-explorer/README.md +36 -0
  179. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  180. data/lib/insika/templates/research-analyst/README.md +26 -0
  181. data/lib/insika/templates/research-analyst/agent.rb +68 -0
  182. data/lib/insika/templates/review-panel/README.md +20 -0
  183. data/lib/insika/templates/review-panel/agent.rb +50 -0
  184. data/lib/insika/templates/travel-planner/README.md +35 -0
  185. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  186. data/lib/insika/templates.rb +112 -0
  187. data/lib/insika/tick.rb +24 -12
  188. data/lib/insika/timezone.rb +45 -0
  189. data/lib/insika/tool_batch.rb +67 -0
  190. data/lib/insika/tool_usage_report.rb +162 -0
  191. data/lib/insika/tools/generate_image.rb +52 -7
  192. data/lib/insika/tools/load_knowledge.rb +74 -0
  193. data/lib/insika/tools/run_persona_eval.rb +328 -0
  194. data/lib/insika/tools/save_artifact.rb +95 -0
  195. data/lib/insika/turn_budget.rb +91 -0
  196. data/lib/insika/turn_output.rb +1 -1
  197. data/lib/insika/turn_state.rb +15 -4
  198. data/lib/insika/version.rb +1 -1
  199. data/lib/insika/wiring/graph.rb +184 -12
  200. data/lib/insika/wiring/graph_chat.rb +102 -0
  201. data/lib/insika.rb +64 -0
  202. metadata +109 -5
  203. data/docs/build.md +0 -14
  204. data/docs/understand.md +0 -10
@@ -40,7 +40,8 @@ module Insika
40
40
  # and units are part of the documented contract (docs/OBSERVABILITY.md) —
41
41
  # renaming one breaks every dashboard built on it.
42
42
  class Instruments
43
- attr_reader :turns, :turn_duration, :tokens, :cost, :tool_calls, :tool_duration
43
+ attr_reader :turns, :turn_duration, :tokens, :cost, :tool_calls, :tool_duration,
44
+ :cache_hit_rate, :loop_intervened, :context_compacted
44
45
 
45
46
  def initialize(meter)
46
47
  @turns = meter.create_counter("insika.turns", unit: "{turn}",
@@ -55,6 +56,12 @@ module Insika
55
56
  description: "Tool invocations")
56
57
  @tool_duration = meter.create_histogram("insika.tool.duration", unit: "s",
57
58
  description: "Wall time of a tool call")
59
+ @cache_hit_rate = meter.create_histogram("insika.cache.hit_rate", unit: "%",
60
+ description: "Prompt-cache hit rate of a turn (cached / billed prompt tokens)")
61
+ @loop_intervened = meter.create_counter("insika.tool.loop_intervened", unit: "{intervention}",
62
+ description: "Loop-detector warnings delivered to the model")
63
+ @context_compacted = meter.create_counter("insika.context.compacted", unit: "{compaction}",
64
+ description: "In-session compactions persisted (RFC-0044)")
58
65
  end
59
66
  end
60
67
 
@@ -73,6 +80,8 @@ module Insika
73
80
  when :tool_call then start_tool(meta, data)
74
81
  when :tool_result then finish_tool(meta)
75
82
  when :data_tool_call then point_tool(meta, data)
83
+ when :tool_loop_intervened then count_loop(meta, data)
84
+ when :context_compacted then count_compaction(data)
76
85
  when :task_completed then finish_turn(meta, data, :ok)
77
86
  when :task_failed then finish_turn(meta, data, :error)
78
87
  when :task_cancelled then finish_turn(meta, data, :cancelled)
@@ -166,6 +175,45 @@ module Insika
166
175
  @instruments.turns.add(1, attributes: labels)
167
176
  @instruments.turn_duration.record(seconds, attributes: labels) if seconds
168
177
  count_usage(turn, usage)
178
+ count_cache_hit(turn, usage)
179
+ end
180
+
181
+ # Same arithmetic as the Executor's per-agent series (stamp_cache_hit):
182
+ # the billed prompt is fresh input + cache reads + cache writes, and the
183
+ # hit rate is reads over the whole billed prompt — always in [0,100]. A
184
+ # turn with no billed prompt tokens (no usage, usage without the fields)
185
+ # records nothing: absence is not a 0% hit.
186
+ def count_cache_hit(turn, usage)
187
+ return unless usage
188
+
189
+ billed = usage[:input_tokens].to_i + usage[:cached_tokens].to_i +
190
+ usage[:cache_creation_tokens].to_i
191
+ return unless billed.positive?
192
+
193
+ rate = (usage[:cached_tokens].to_i * 100.0) / billed
194
+ base = turn.labels.merge(attrs("insika.model" => usage[:model]&.to_s))
195
+ @instruments.cache_hit_rate.record(rate, attributes: base)
196
+ end
197
+
198
+ # The loop detector delivered its one-shot warning (`:tool_loop_intervened`,
199
+ # counts and the tool name, never arguments). An orphan event (no open turn)
200
+ # is ignored, like every other consumer of this stream.
201
+ def count_loop(meta, data)
202
+ return unless @instruments
203
+
204
+ turn = @turns[meta[:task_id]] or return
205
+ labels = turn.labels.merge(attrs("insika.tool" => data[:name]&.to_s))
206
+ @instruments.loop_intervened.add(1, attributes: labels)
207
+ end
208
+
209
+ # A compaction persisted (`:context_compacted`, RFC-0044) — counted by
210
+ # agent/model, INDEPENDENT of any open turn: it fires post-turn, usually
211
+ # after task_completed already closed the span.
212
+ def count_compaction(data)
213
+ return unless @instruments
214
+
215
+ labels = attrs("insika.agent" => data[:agent]&.to_s, "insika.model" => data[:model]&.to_s)
216
+ @instruments.context_compacted.add(1, attributes: labels)
169
217
  end
170
218
 
171
219
  # Tokens ride ONE counter split by `insika.token.type` (instead of four
@@ -0,0 +1,36 @@
1
+ # browser-agent
2
+
3
+ **MCP trail (stdio).** A live MCP tool-loop over stdio: the
4
+ engine spawns [Playwright's MCP server](https://github.com/microsoft/playwright-mcp)
5
+ (`npx @playwright/mcp@latest`) as a child process and wires its browser
6
+ tools straight into the agent's tool-loop. No API key beyond your LLM
7
+ provider's.
8
+
9
+ ## Before you run it
10
+
11
+ Two things this template needs beyond the gem and a provider key:
12
+
13
+ 1. **Node.js and npm** — `npx` spawns the MCP server as a child process.
14
+ 2. **`INSIKA_MCP_STDIO=1`** — a stdio MCP instance is arbitrary command
15
+ execution by config, so the engine refuses to start it until you opt in
16
+ explicitly (same discipline as the egress env vars for private hosts).
17
+
18
+ ```bash
19
+ node --version # confirm Node.js is installed
20
+ INSIKA_MCP_STDIO=1 DEEPSEEK_API_KEY=sk-... ruby browser-agent/agent.rb "go to example.com and summarize the page"
21
+ ```
22
+
23
+ Without `INSIKA_MCP_STDIO=1` the reply will say the tool call failed —
24
+ that's the gate working, not a bug.
25
+
26
+ ## Swap the server
27
+
28
+ This is not a Playwright showcase — it's exactly how you plug **any** MCP
29
+ server into an agent over stdio:
30
+
31
+ ```bash
32
+ MCP_COMMAND=your-mcp-server MCP_ARGS="--flag value" INSIKA_MCP_STDIO=1 DEEPSEEK_API_KEY=sk-... ruby browser-agent/agent.rb "..."
33
+ ```
34
+
35
+ Point `MCP_COMMAND`/`MCP_ARGS` at any stdio MCP server and rewrite the
36
+ instructions for its tools — nothing else in `agent.rb` changes.
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Browser Agent
5
+ # trail: MCP
6
+ # description: Live MCP tool-loop over stdio — navigates and summarizes a real webpage via Playwright's MCP server. Requires Node.js/npm and INSIKA_MCP_STDIO=1.
7
+ # capabilities: mcp, stdio
8
+ # env: INSIKA_MCP_STDIO
9
+ # requires: Node.js and npm (npx spawns the MCP server as a child process)
10
+ # ---
11
+ #
12
+ # browser-agent — a live MCP tool-loop over STDIO: the agent
13
+ # drives a real, sandboxed browser through Playwright's MCP server
14
+ # (@playwright/mcp, no API key). stdio is arbitrary command execution by
15
+ # config, so it needs the operator's explicit opt-in (INSIKA_MCP_STDIO=1)
16
+ # and Node.js/npm on the machine — the two things this template needs
17
+ # beyond the gem and a provider key. Swap MCP_COMMAND/MCP_ARGS for any other
18
+ # MCP server and nothing else in this file changes.
19
+ #
20
+ # INSIKA_MCP_STDIO=1 DEEPSEEK_API_KEY=sk-... ruby browser-agent/agent.rb "go to example.com and summarize the page"
21
+ require "insika"
22
+ require "shellwords"
23
+
24
+ browser = Insika.agent("browser-agent") do
25
+ model "deepseek-v4-flash"
26
+ provider :deepseek
27
+
28
+ instructions <<~PROMPT
29
+ You browse the web using the browser MCP tools. Navigate to the
30
+ requested page, then extract or summarize what was asked. Never invent
31
+ page content — always navigate and read it first.
32
+ PROMPT
33
+
34
+ mcp "browser", transport: :stdio,
35
+ command: ENV.fetch("MCP_COMMAND", "npx"),
36
+ args: Shellwords.split(ENV.fetch("MCP_ARGS", "-y @playwright/mcp@latest"))
37
+ end
38
+
39
+ if __FILE__ == $PROGRAM_NAME
40
+ if ARGV.delete("--serve")
41
+ browser.serve
42
+ else
43
+ message = ARGV.join(" ")
44
+ message = "Go to https://example.com and summarize the page in two sentences." if message.empty?
45
+ puts browser.reply(message)
46
+ end
47
+ end
48
+
49
+ browser
@@ -0,0 +1,47 @@
1
+ # daily-digest
2
+
3
+ **Always-on trail.** The report pipeline in one file: a recurring
4
+ `schedule` (the engine's own tick fires it), a `save_artifact`
5
+ tool (the report destination), and a store-free skill describing
6
+ *how* to build the report. No database, no store content — the day's
7
+ numbers are fake, inline text.
8
+
9
+ ```bash
10
+ DEEPSEEK_API_KEY=sk-... ruby daily-digest/agent.rb
11
+ ```
12
+
13
+ Expected output: a reply ending in `Report: /studio/artifacts/<id>`.
14
+
15
+ ```bash
16
+ DEEPSEEK_API_KEY=sk-... ruby daily-digest/agent.rb --serve
17
+ ```
18
+
19
+ Then open `/studio`, log in with the printed token, and either wait for the
20
+ schedule (22:00 America/Sao_Paulo) or send a message to the "reporter" agent
21
+ in the Playground — the artifact lands on the Artifacts tab either way.
22
+
23
+ ## What's real here
24
+
25
+ - The **schedule** is a live engine feature: the tick actually fires this
26
+ turn at the cron time, in the same process, no external cron needed.
27
+ - The **artifact** is a real signed/authenticated URL the report is saved
28
+ to — open it and the HTML renders with its own strict CSP (`default-src
29
+ 'none'`), so the inline SVG chart has to be self-contained by
30
+ construction, not by convention.
31
+ - The **numbers** are not. Swap the literal string in `agent.rb` for a
32
+ `data_tool` against your own sales API and nothing else changes.
33
+
34
+ ## When the report needs real data
35
+
36
+ One agent doing 30–50 tool calls at one reasoning effort is how a report turn
37
+ hits the 300 s timeout. The recipe is to split the phases across agents —
38
+ `thinking: "low"` miners fanned out with `spawn_subagents`, a `thinking: "high"`
39
+ orchestrator that plans and writes. See
40
+ [Artifacts](https://github.com/guizaols/insika/blob/main/docs/ARTIFACTS.md), "Reasoning effort on a report turn",
41
+ and the `research-analyst` template for the fan-out shape.
42
+
43
+ ## Edit it
44
+
45
+ The skill's instructions are the actual report spec — change the palette,
46
+ add a second table, or add another chart. The schedule's `cron`/`tz` are
47
+ plain arguments.
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Daily Digest
5
+ # trail: Always-on
6
+ # description: A recurring schedule plus save_artifact build and publish a self-contained HTML report — no store, fake in-memory numbers only.
7
+ # capabilities: schedule, save_artifact, skill
8
+ # ---
9
+ #
10
+ # daily-digest — the report pipeline in one file: a recurring schedule (the
11
+ # engine's own tick fires it), a daily-digest skill (the inline-SVG
12
+ # pattern), the save_artifact tool (the report destination) and the
13
+ # per-agent allowlist that gates it. No store content — fake, in-memory
14
+ # "sales" numbers only.
15
+ #
16
+ # DEEPSEEK_API_KEY=sk-... ruby daily-digest/agent.rb
17
+ # DEEPSEEK_API_KEY=sk-... ruby daily-digest/agent.rb --serve
18
+ # # --serve: open /studio, log in with the printed token, then send a
19
+ # # message to the "reporter" agent in the Playground — the artifact
20
+ # # lands on the Artifacts tab. The schedule fires the same run at
21
+ # # 22:00 America/Sao_Paulo on its own.
22
+ require "insika"
23
+
24
+ todays_sales = "Coffee 128 units ($512), Tea 64 units ($192), Pastries 40 units ($160)."
25
+
26
+ reporter = Insika.agent("reporter") do
27
+ model "deepseek-v4-flash"
28
+ provider :deepseek
29
+
30
+ instructions <<~PROMPT
31
+ You produce the daily sales digest from the numbers given to you in the
32
+ message. Follow the daily-digest skill exactly, then save the finished
33
+ page with save_artifact (HTML with inline SVG). End your reply with the
34
+ artifact url, on its own line, prefixed with "Report: ".
35
+ PROMPT
36
+
37
+ # The per-agent allowlist IS the switch for save_artifact — without this
38
+ # line the model never even sees the tool.
39
+ tools %w[save_artifact]
40
+
41
+ # A recurring turn — the engine's tick fires it. session_mode:
42
+ # "new" = a fresh session per run (the report shape); the overrides raise
43
+ # the chat-time ceilings a real report needs.
44
+ schedule "daily_report", cron: "0 22 * * *", tz: "America/Sao_Paulo",
45
+ message: "Run the daily report now. Today's sales: #{todays_sales}",
46
+ session_mode: "new",
47
+ overrides: { turn_timeout: 600, max_tool_calls: 120 }
48
+
49
+ # The generic, store-free skill: how a report is BUILT — the inline-SVG
50
+ # pattern (palette, table, pure-SVG bars, light/dark). No store ids, no
51
+ # queries — that half belongs in a merchant's pack, not here.
52
+ skill "daily-digest",
53
+ description: "How to build the daily sales digest as a self-contained HTML report",
54
+ instructions: <<~MD
55
+ Build the digest as ONE self-contained HTML page:
56
+ - a <style> block with a light palette (e.g. #f8fafc bg, #0f172a text,
57
+ #6366f1 accent), plus a prefers-color-scheme: dark override;
58
+ - one <table> of the numbers (th/tbody, right-aligned amounts);
59
+ - one inline SVG bar chart (no <script>, no <img>, no <iframe>,
60
+ no external fonts or fetches — the report is served with
61
+ default-src 'none', so nothing external can load);
62
+ - a title and the run date.
63
+ Keep it readable on a phone. The report is the deliverable; the
64
+ channel message is just the link to it.
65
+ MD
66
+ end
67
+
68
+ if __FILE__ == $PROGRAM_NAME
69
+ if ARGV.delete("--serve")
70
+ reporter.serve
71
+ else
72
+ puts reporter.reply("Run the daily report now. Today's sales: #{todays_sales}")
73
+ puts "\nThen open the returned /studio/artifacts/<id> page (or --serve and look at the Artifacts tab)."
74
+ end
75
+ end
76
+
77
+ reporter
@@ -0,0 +1,36 @@
1
+ # repo-explorer
2
+
3
+ **MCP trail (http).** A live MCP tool-loop over Streamable HTTP:
4
+ the agent calls a real, running MCP server's tools — not a snapshot, not a
5
+ hand-rolled HTTP wrapper. The default target is
6
+ [DeepWiki's public MCP server](https://mcp.deepwiki.com/mcp), which needs
7
+ no API key for public repos.
8
+
9
+ ```bash
10
+ DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "how does rails/rails route a request?"
11
+ ```
12
+
13
+ This is not a DeepWiki showcase — it's exactly how you plug **any** MCP
14
+ server into an agent:
15
+
16
+ ```bash
17
+ MCP_URL=https://your-mcp-server/mcp DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "..."
18
+ ```
19
+
20
+ Point `MCP_URL` at any Streamable HTTP or SSE MCP server and nothing else
21
+ in `agent.rb` changes — swap the URL, rewrite the instructions for the new
22
+ server's tools, done.
23
+
24
+ ## Under the hood
25
+
26
+ `mcp "repo-docs", transport: :http, url: …` declares the instance; the
27
+ engine connects live, discovers its tools (`read_wiki_structure`,
28
+ `read_wiki_contents`, `ask_question`), and wires them straight into the
29
+ agent's tool-loop with group `mcp:repo-docs`. Declaring an `mcp` inside an
30
+ agent's block auto-grants that agent access to the group — see
31
+ `lib/insika/dsl.rb`'s `mcp` method if you're curious why that matters.
32
+
33
+ A public HTTPS target needs no extra configuration (same egress guard as
34
+ any data-tool). A target on a private network needs the same
35
+ `INSIKA_EGRESS_ALLOW_PRIVATE`/`INSIKA_EGRESS_HOSTS` env vars a data-tool
36
+ would.
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Repo Explorer
5
+ # trail: MCP
6
+ # description: Live MCP tool-loop over http — answers questions about any public GitHub repo via a keyless public MCP server. Point MCP_URL at any other MCP server instead.
7
+ # capabilities: mcp, http
8
+ # ---
9
+ #
10
+ # repo-explorer — a live MCP tool-loop over HTTP: the agent calls
11
+ # a real MCP server's tools (read_wiki_structure, read_wiki_contents,
12
+ # ask_question) to answer questions about a public GitHub repo. The default
13
+ # target is DeepWiki's public, keyless MCP server — swap MCP_URL for any
14
+ # other MCP server and nothing else in this file changes.
15
+ #
16
+ # DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "how does rails/rails route a request?"
17
+ # MCP_URL=https://your-mcp-server/mcp DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "..."
18
+ require "insika"
19
+
20
+ repo = Insika.agent("repo-explorer") do
21
+ model "deepseek-v4-flash"
22
+ provider :deepseek
23
+
24
+ instructions <<~PROMPT
25
+ You answer questions about public GitHub repositories using the
26
+ repo-docs MCP tools (read_wiki_structure, read_wiki_contents,
27
+ ask_question). Repos are named "owner/repo" (e.g. "rails/rails"). Never
28
+ answer from your own training data when a tool can check — call
29
+ ask_question first.
30
+ PROMPT
31
+
32
+ mcp "repo-docs", transport: :http, url: ENV.fetch("MCP_URL", "https://mcp.deepwiki.com/mcp")
33
+ end
34
+
35
+ if __FILE__ == $PROGRAM_NAME
36
+ if ARGV.delete("--serve")
37
+ repo.serve
38
+ else
39
+ message = ARGV.join(" ")
40
+ message = "In the rails/rails repo, how does routing work? One paragraph." if message.empty?
41
+ puts repo.reply(message)
42
+ end
43
+ end
44
+
45
+ repo
@@ -0,0 +1,26 @@
1
+ # research-analyst
2
+
3
+ **Advanced trail.** Four agents in one `Insika.system`: three specialists
4
+ (market, technical, risk) and a lead ("analyst") that has no expertise of
5
+ its own and must delegate. The MODEL decides to fan out — nothing in Ruby
6
+ orchestrates the parallel calls.
7
+
8
+ ```bash
9
+ DEEPSEEK_API_KEY=sk-... ruby research-analyst/agent.rb "a subscription box for specialty coffee"
10
+ ```
11
+
12
+ Under the hood: the lead calls `spawn_subagents` once with all three
13
+ specialist ids, they run **in parallel** (each in an isolated context — a
14
+ child never sees the parent's conversation), and the lead synthesizes their
15
+ three answers into one recommendation.
16
+
17
+ Nothing forces the model to delegate — that's the trade of a model-driven
18
+ pattern over a hand-coded workflow. If it answers alone instead, the fix is
19
+ the lead's prompt, not the code: it needs to be told, plainly, that it has
20
+ no expertise of its own.
21
+
22
+ ## Edit it
23
+
24
+ Add a fourth specialist (`agent("competitors") { … }`, then add it to
25
+ `subagents`), or turn any specialist into a `Insika.agent` with its own
26
+ data-tools — a subagent is an ordinary agent, capability included.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Research Analyst
5
+ # trail: Advanced
6
+ # description: Insika.system fan-out — three specialist subagents research different angles of a topic in parallel, the lead delegates and synthesizes.
7
+ # capabilities: subagents, delegation, system
8
+ # ---
9
+ #
10
+ # research-analyst — a lead agent with no expertise of its own: it must
11
+ # delegate. spawn_subagents fans out to three specialists on separate angles
12
+ # of a business idea, IN PARALLEL (each in its own isolated context), then
13
+ # the lead synthesizes one recommendation.
14
+ #
15
+ # Reasoning effort is split by ROLE, not spread evenly: the specialists answer
16
+ # one narrow question each (`thinking: "low"`), the lead plans the delegation
17
+ # and weighs three answers against each other (`thinking: "high"`). A child
18
+ # inherits the environment as a DEFAULT only, so its own `params` wins. See
19
+ # docs/ARTIFACTS.md, "Reasoning effort on a report turn".
20
+ #
21
+ # DEEPSEEK_API_KEY=sk-... ruby research-analyst/agent.rb "a subscription box for specialty coffee"
22
+ # DEEPSEEK_API_KEY=sk-... ruby research-analyst/agent.rb --serve
23
+ require "insika"
24
+
25
+ team = Insika.system do
26
+ provider :deepseek
27
+
28
+ agent("market") do
29
+ model "deepseek-v4-flash"
30
+ params thinking: "low"
31
+ instructions "Research the MARKET angle of a business idea: audience, demand, competitors. Three sentences."
32
+ end
33
+ agent("technical") do
34
+ model "deepseek-v4-flash"
35
+ params thinking: "low"
36
+ instructions "Research the TECHNICAL/OPERATIONAL angle of a business idea: what it takes to build and run it. Three sentences."
37
+ end
38
+ agent("risk") do
39
+ model "deepseek-v4-flash"
40
+ params thinking: "low"
41
+ instructions "Research the RISK angle of a business idea: what could make it fail. Three sentences."
42
+ end
43
+
44
+ agent "analyst" do
45
+ model "deepseek-v4-flash"
46
+ params thinking: "high"
47
+ instructions <<~PROMPT
48
+ You are a research LEAD with no expertise of your own — never answer
49
+ from your own knowledge. Given a business idea, call spawn_subagents
50
+ once with all three specialists (market, technical, risk), then
51
+ synthesize their findings into one short recommendation: go, no-go, or
52
+ go-with-changes, and why.
53
+ PROMPT
54
+ subagents "market", "technical", "risk"
55
+ end
56
+ end
57
+
58
+ if __FILE__ == $PROGRAM_NAME
59
+ if ARGV.delete("--serve")
60
+ team.serve
61
+ else
62
+ topic = ARGV.join(" ")
63
+ topic = "a subscription box for specialty coffee" if topic.empty?
64
+ puts team.reply("analyst", "Research this idea: #{topic}")
65
+ end
66
+ end
67
+
68
+ team
@@ -0,0 +1,20 @@
1
+ # review-panel
2
+
3
+ **Teams trail.** The `Insika.system` snippet from the gem's main examples
4
+ README, promoted to a runnable template: a "reviewer" lead with no
5
+ expertise of its own delegates to two specialists — security and
6
+ performance — IN PARALLEL, then synthesizes one prioritized fix.
7
+
8
+ ```bash
9
+ DEEPSEEK_API_KEY=sk-... ruby review-panel/agent.rb
10
+ ```
11
+
12
+ `panel.reply("reviewer", …)` — the target agent is always explicit; with
13
+ several agents in one system, inferring which one should answer would be a
14
+ guess, and a wrong guess is a silently wrong conversation.
15
+
16
+ ## Edit it
17
+
18
+ Add a third specialist (a `style` reviewer, say), list it in `subagents`,
19
+ and the lead's synthesis prompt already generalizes — it doesn't name the
20
+ specialists, just says "both".
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Review Panel
5
+ # trail: Teams
6
+ # description: Two specialists reviewed in parallel by a synthesizing lead, explicit target agent (Insika.system + subagents).
7
+ # capabilities: subagents, delegation, system
8
+ # ---
9
+ #
10
+ # review-panel — promotes the examples/README.md snippet to a runnable
11
+ # template: a "reviewer" lead that has no expertise of its own delegates to
12
+ # two specialists (security, performance) IN PARALLEL, then synthesizes.
13
+ #
14
+ # DEEPSEEK_API_KEY=sk-... ruby review-panel/agent.rb
15
+ # DEEPSEEK_API_KEY=sk-... ruby review-panel/agent.rb --serve
16
+ require "insika"
17
+
18
+ panel = Insika.system do
19
+ provider :deepseek
20
+
21
+ agent("security") { model "deepseek-v4-flash"; instructions "Review code for SECURITY issues. Two sentences." }
22
+ agent("performance") { model "deepseek-v4-flash"; instructions "Review code for PERFORMANCE issues. Two sentences." }
23
+
24
+ agent "reviewer" do
25
+ model "deepseek-v4-flash"
26
+ instructions <<~PROMPT
27
+ You are a review LEAD with no reviewing expertise of your own — never
28
+ review from your own knowledge. Call spawn_subagents once with both
29
+ specialists (security, performance), then synthesize their findings
30
+ into the single highest-priority fix.
31
+ PROMPT
32
+ subagents "security", "performance"
33
+ end
34
+ end
35
+
36
+ if __FILE__ == $PROGRAM_NAME
37
+ code = <<~RUBY
38
+ def find_user(name)
39
+ User.where("name = '\#{name}'").to_a.select { |u| u.active }
40
+ end
41
+ RUBY
42
+
43
+ if ARGV.delete("--serve")
44
+ panel.serve
45
+ else
46
+ puts panel.reply("reviewer", "Review this code:\n#{code}")
47
+ end
48
+ end
49
+
50
+ panel
@@ -0,0 +1,35 @@
1
+ # travel-planner
2
+
3
+ **Starter trail.** A trip-planning assistant built entirely from **declarative
4
+ data-tools** — no Ruby tool class, no rebuild. Three tools, three public
5
+ HTTPS APIs, zero API keys beyond your LLM provider's:
6
+
7
+ - `geocode_city` — Open-Meteo's geocoding API (city name → coordinates)
8
+ - `get_weather` — Open-Meteo's forecast API (coordinates → today's conditions)
9
+ - `convert_currency` — Frankfurter's reference exchange rates
10
+
11
+ ```bash
12
+ DEEPSEEK_API_KEY=sk-... ruby travel-planner/agent.rb "3 days in Lisbon, budget 200 USD"
13
+ ```
14
+
15
+ The model chains the first two tools itself (geocode, then weather) and
16
+ calls the third when a budget is mentioned — nothing here tells it the
17
+ order, the instructions just describe what each tool is for.
18
+
19
+ ## Egress guard (SSRF protection)
20
+
21
+ Data-tools make **server-side** HTTP calls, so the engine ships an egress
22
+ guard that is strict by default: public HTTPS only. This template works
23
+ with zero configuration because all three endpoints are public HTTPS — a
24
+ tool pointed at `http://…`, `localhost`, or a private IP would be blocked
25
+ instead (`{ error: "destination blocked: …" }` back to the model, a clean
26
+ tool error, never a crash). Opting a private/internal target in is a
27
+ deployment env var (`INSIKA_EGRESS_ALLOW_HTTP`/`_PRIVATE`/`_HOSTS`), never a
28
+ DSL setting — see `docs/TOOLS.md`'s "MCP servers" / egress sections in the
29
+ installed gem's docs for the full contract.
30
+
31
+ ## Edit it
32
+
33
+ Open `agent.rb` — it's the same file `insika new` copied and the same one
34
+ this README describes. Add a fourth data-tool, change the model, or point
35
+ an existing one at a different provider; nothing else needs to change.
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Travel Planner
5
+ # trail: Starter
6
+ # description: Weather + currency data-tools against keyless public APIs (Open-Meteo, Frankfurter) — the egress guard does its job with zero configuration.
7
+ # capabilities: data-tool, egress-guard
8
+ # ---
9
+ #
10
+ # travel-planner — plans a trip: geocodes the destination, checks today's
11
+ # weather, and converts a budget to the local currency. Three declarative
12
+ # data-tools, no Ruby tool class, no API key beyond the LLM provider's.
13
+ #
14
+ # DEEPSEEK_API_KEY=sk-... ruby travel-planner/agent.rb "3 days in Lisbon, budget 200 USD"
15
+ # DEEPSEEK_API_KEY=sk-... ruby travel-planner/agent.rb --serve
16
+ require "insika"
17
+
18
+ travel = Insika.agent("travel-planner") do
19
+ model "deepseek-v4-flash"
20
+ provider :deepseek
21
+
22
+ instructions <<~PROMPT
23
+ You are a travel-planning assistant. Given a destination and, optionally,
24
+ a budget amount + currency:
25
+ 1. geocode_city to find its coordinates — never guess them.
26
+ 2. get_weather for those coordinates and summarize today's conditions.
27
+ 3. If a budget was given, convert_currency to the destination's local
28
+ currency and report the converted amount.
29
+ Never invent coordinates, weather or exchange rates — always call the tools.
30
+ PROMPT
31
+
32
+ data_tool(
33
+ "name" => "geocode_city",
34
+ "description" => "Latitude/longitude for a city name (Open-Meteo geocoding).",
35
+ "parameters" => {
36
+ "type" => "object",
37
+ "properties" => { "city" => { "type" => "string", "description" => "city name, e.g. Lisbon" } },
38
+ "required" => ["city"]
39
+ },
40
+ "request" => { "method" => "GET", "url" => "https://geocoding-api.open-meteo.com/v1/search?name={{city}}&count=1" },
41
+ "response" => { "extract" => "body_raw" }
42
+ )
43
+
44
+ data_tool(
45
+ "name" => "get_weather",
46
+ "description" => "Current weather for a latitude/longitude (Open-Meteo).",
47
+ "parameters" => {
48
+ "type" => "object",
49
+ "properties" => {
50
+ "latitude" => { "type" => "number", "description" => "from geocode_city" },
51
+ "longitude" => { "type" => "number", "description" => "from geocode_city" }
52
+ },
53
+ "required" => %w[latitude longitude]
54
+ },
55
+ "request" => { "method" => "GET", "url" => "https://api.open-meteo.com/v1/forecast?latitude={{latitude}}&longitude={{longitude}}&current_weather=true" },
56
+ "response" => { "extract" => "body_raw" }
57
+ )
58
+
59
+ # Author the FINAL url — the HTTP client does not follow redirects.
60
+ # api.frankfurter.app now redirects to api.frankfurter.dev.
61
+ data_tool(
62
+ "name" => "convert_currency",
63
+ "description" => "Latest reference exchange rate between two currencies.",
64
+ "parameters" => {
65
+ "type" => "object",
66
+ "properties" => {
67
+ "from" => { "type" => "string", "description" => "source currency code, e.g. USD" },
68
+ "to" => { "type" => "string", "description" => "target currency code, e.g. BRL" }
69
+ },
70
+ "required" => %w[from to]
71
+ },
72
+ "request" => { "method" => "GET", "url" => "https://api.frankfurter.dev/v1/latest?from={{from}}&to={{to}}" },
73
+ "response" => { "extract" => "body_raw" }
74
+ )
75
+ end
76
+
77
+ if __FILE__ == $PROGRAM_NAME
78
+ if ARGV.delete("--serve")
79
+ travel.serve
80
+ else
81
+ message = ARGV.join(" ")
82
+ message = "I'm spending 3 days in Lisbon with a budget of 200 USD. What should I pack, and how much is that in EUR?" if message.empty?
83
+ puts travel.reply(message)
84
+ end
85
+ end
86
+
87
+ travel