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
@@ -0,0 +1,51 @@
1
+ <!-- ── Agent memory (tenant = id) ────────────────────────────────────────── -->
2
+ <section class="section" id="memory" data-tabs-target="panel">
3
+ <div class="section-head">
4
+ <h2>Memory</h2>
5
+ <p class="muted">facts and notes the agent reads each turn (tenant <code><%= @agent.id %></code>).</p>
6
+ </div>
7
+ <div class="card">
8
+ <h3>Facts</h3>
9
+ <% if @facts.empty? %>
10
+ <p class="muted">No facts.</p>
11
+ <% else %>
12
+ <ul class="kv">
13
+ <% @facts.each do |f| %>
14
+ <li>
15
+ <span class="k mono"><%= f.key %></span>
16
+ <span class="v"><%= f.value %></span>
17
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/forget" data-turbo-confirm="Forget &quot;<%= f.key %>&quot;? The agent loses this fact from the next turn on.">
18
+ <%== csrf_tag %>
19
+ <input type="hidden" name="key" value="<%= f.key %>">
20
+ <button type="submit" class="ghost danger" data-turbo-submits-with="Forgetting…">Forget</button>
21
+ </form>
22
+ </li>
23
+ <% end %>
24
+ </ul>
25
+ <% end %>
26
+ <form class="inline-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/fact">
27
+ <%== csrf_tag %>
28
+ <input type="text" name="key" placeholder="key (e.g. customer_name)" required>
29
+ <input type="text" name="value" placeholder="value" required>
30
+ <button type="submit" class="primary" data-turbo-submits-with="Saving…">Save fact</button>
31
+ </form>
32
+ </div>
33
+
34
+ <div class="card">
35
+ <h3>Notes <span class="muted">(recent)</span></h3>
36
+ <% if @notes.empty? %>
37
+ <p class="muted">No notes.</p>
38
+ <% else %>
39
+ <ul class="notes">
40
+ <% @notes.each do |n| %>
41
+ <li><span class="muted mono"><%= n.created_at %></span> <%= n.text %></li>
42
+ <% end %>
43
+ </ul>
44
+ <% end %>
45
+ <form class="inline-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/note">
46
+ <%== csrf_tag %>
47
+ <input type="text" name="text" placeholder="free-form note…" required>
48
+ <button type="submit" class="primary" data-turbo-submits-with="Adding…">Add note</button>
49
+ </form>
50
+ </div>
51
+ </section>
@@ -0,0 +1,31 @@
1
+ <!-- ── Outcomes (WS7) ────────────────────────────────────────────────────── -->
2
+ <section class="section" id="outcomes" data-tabs-target="panel">
3
+ <div class="section-head">
4
+ <h2>Outcomes</h2>
5
+ <p class="muted">business results recorded via <code>POST /v1/outcomes</code> — last outcome and per-day series. The engine transports them; it does not interpret them.</p>
6
+ </div>
7
+ <div class="card">
8
+ <% if @latest_outcome %>
9
+ <p>last
10
+ <span class="pill <%= @latest_outcome[:outcome] == "conversion" ? "ok" : "warn" %>"><%= @latest_outcome[:outcome] %><%== format(" · R$ %0.2f", @latest_outcome[:value]) if @latest_outcome[:value] && @latest_outcome[:value].positive? %></span>
11
+ <span class="muted mono"><%= @latest_outcome[:at] %></span>
12
+ </p>
13
+ <% end %>
14
+ <% if @outcome_series.nil? || @outcome_series.empty? %>
15
+ <p class="muted">No outcomes recorded.</p>
16
+ <% else %>
17
+ <ul class="kv">
18
+ <% @outcome_series.sort.reverse.each do |day, by_outcome| %>
19
+ <li>
20
+ <span class="k mono"><%= day %></span>
21
+ <span class="v"><%= by_outcome.map { |name, cell|
22
+ bits = ["#{name} ×#{cell[:count]}"]
23
+ bits << format("R$ %0.2f", cell[:value]) if cell[:value].to_f.positive?
24
+ bits.join(" · ")
25
+ }.join(" · ") %></span>
26
+ </li>
27
+ <% end %>
28
+ </ul>
29
+ <% end %>
30
+ </div>
31
+ </section>
@@ -0,0 +1,108 @@
1
+ <!-- ── Prompts / identity (drill: file list | editor, like skills) ────────── -->
2
+ <section class="section" id="prompts" data-tabs-target="panel">
3
+ <div class="section-head">
4
+ <h2>Prompts <span class="pill info plain"><%= @prompt_files.size %></span></h2>
5
+ <p class="muted">the agent's own identity (store-backed, versioned). Empty = uses the deployment default.</p>
6
+ </div>
7
+
8
+ <div class="drill" data-controller="list-filter">
9
+ <aside class="drill-pane drill-master">
10
+ <div class="drill-pane-head">
11
+ <div class="title"><strong>Prompt files <span class="count"><%= @prompt_files.size %></span></strong></div>
12
+ <a class="btn primary btn-sm" href="<%= agent_path(@agent.id) %>/prompts/new#prompts" data-turbo-action="advance">+ new</a>
13
+ </div>
14
+ <div class="drill-search">
15
+ <input type="search" class="filter-input" placeholder="Search prompts…" aria-label="Filter prompts"
16
+ data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
17
+ </div>
18
+ <nav class="drill-pane-body drill-list" aria-label="prompt files">
19
+ <% if @prompt_files.empty? %>
20
+ <div class="empty">No prompt files yet — create the first one.</div>
21
+ <% else %>
22
+ <% @prompt_files.each do |pf| %>
23
+ <a class="drill-item<%== ' active' if @prompt_selected == pf[:name] %>"
24
+ href="<%= agent_path(@agent.id) %>/prompts/<%= Rack::Utils.escape(pf[:name]) %>#prompts"
25
+ data-turbo-action="advance"
26
+ data-list-filter-target="item" data-filter-text="<%= pf[:name] %>">
27
+ <div class="drill-item-name"><span class="txt" title="<%= pf[:name] %>"><%= pf[:name] %></span></div>
28
+ <div class="drill-item-sub"><%= pf[:versions].size %> version<%= 's' unless pf[:versions].size == 1 %></div>
29
+ </a>
30
+ <% end %>
31
+ <div class="empty" data-list-filter-target="empty" hidden>No prompts match your filter.</div>
32
+ <% end %>
33
+ </nav>
34
+ </aside>
35
+
36
+ <section class="drill-pane drill-detail">
37
+ <% if @prompt_selected.nil? %>
38
+ <div class="drill-empty">
39
+ <div>
40
+ <h2>Prompts</h2>
41
+ <p>Pick a prompt file on the left to edit it — or <a href="<%= agent_path(@agent.id) %>/prompts/new#prompts" data-turbo-action="advance">create a new one</a>.</p>
42
+ </div>
43
+ </div>
44
+ <% else %>
45
+ <% new = @prompt_selected == "" %>
46
+ <% pf = new ? nil : @prompt_files.find { |f| f[:name] == @prompt_selected } %>
47
+ <div class="drill-pane-head">
48
+ <div class="title">
49
+ <strong class="mono"><%= new ? "New prompt file" : @prompt_selected %></strong>
50
+ </div>
51
+ <button type="submit" form="prompt-form" class="btn primary" data-turbo-submits-with="Saving…"><%= new ? "Create prompt" : "Save prompt" %></button>
52
+ </div>
53
+ <div class="drill-pane-body">
54
+ <form id="prompt-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts" data-controller="dirty-guard">
55
+ <%== csrf_tag %>
56
+ <% if new %>
57
+ <label>file name <span class="muted">(e.g. <code>IDENTITY.md</code>)</span>
58
+ <input type="text" name="file" placeholder="IDENTITY.md" required>
59
+ </label>
60
+ <% else %>
61
+ <input type="hidden" name="file" value="<%= @prompt_selected %>">
62
+ <% end %>
63
+ <label>content <span class="muted">(markdown — the agent's identity/voice)</span></label>
64
+ <div data-controller="code-editor" data-code-editor-language-value="markdown" data-code-editor-preview-value="true">
65
+ <textarea name="content" data-code-editor-target="source" rows="18"><%= new ? "" : (pf ? pf[:content] : "") %></textarea>
66
+ </div>
67
+ </form>
68
+
69
+ <% if !new && pf %>
70
+ <% unless pf[:versions].empty? %>
71
+ <div class="drill-section">
72
+ <div class="drill-section-head">
73
+ <h3>Versions</h3>
74
+ <span class="muted">store-backed history — restore replaces the current content.</span>
75
+ </div>
76
+ <ul class="versions-list">
77
+ <% pf[:versions].each_with_index do |v, i| %>
78
+ <li>
79
+ <span class="muted mono"><%= v["at"] %></span>
80
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts/restore">
81
+ <%== csrf_tag %>
82
+ <input type="hidden" name="file" value="<%= pf[:name] %>">
83
+ <input type="hidden" name="version" value="<%= i %>">
84
+ <button type="submit" class="ghost">Restore</button>
85
+ </form>
86
+ </li>
87
+ <% end %>
88
+ </ul>
89
+ </div>
90
+ <% end %>
91
+
92
+ <div class="drill-section">
93
+ <div class="drill-section-head">
94
+ <h3>Remove</h3>
95
+ <span class="muted">deletes the file and drops it from <code>prompt_files</code> — the agent falls back to the deployment default.</span>
96
+ </div>
97
+ <form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts/delete" data-turbo-confirm="Remove prompt file &quot;<%= pf[:name] %>&quot;? This can't be undone.">
98
+ <%== csrf_tag %>
99
+ <input type="hidden" name="file" value="<%= pf[:name] %>">
100
+ <div class="actions"><button type="submit" class="danger" data-turbo-submits-with="Removing…">Remove this file</button></div>
101
+ </form>
102
+ </div>
103
+ <% end %>
104
+ </div>
105
+ <% end %>
106
+ </section>
107
+ </div>
108
+ </section>
@@ -0,0 +1,38 @@
1
+ <!-- ── Agent skills ──────────────────────────────────────────────────────── -->
2
+ <section class="section" id="skills" data-tabs-target="panel">
3
+ <div class="section-head">
4
+ <h2>Skills</h2>
5
+ <p class="muted">which catalog skills this agent may load (<code>load_skill</code>).</p>
6
+ </div>
7
+ <form class="card" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/skills" data-controller="toggle-counter">
8
+ <%== csrf_tag %>
9
+ <div class="matrix-topline">
10
+ <% if @all_skills.empty? %>
11
+ <span class="muted">No skills in the catalog yet — create one under <a href="/studio/skills/new" data-turbo-frame="_top">skills → new</a>.</span>
12
+ <% else %>
13
+ <span class="count-badge mono" data-toggle-counter-target="count"><%= @agent_skills.nil? ? "all" : "#{@agent_skills.size}/#{@all_skills.size} on" %></span>
14
+ <label class="switch" title="open allowlist — every catalog skill flows through">
15
+ <input type="checkbox" name="all_skills" value="1"<%== " checked" if @agent_skills.nil? %>
16
+ data-toggle-counter-target="all" data-action="change->toggle-counter#update">
17
+ <span class="switch-track" aria-hidden="true"></span>
18
+ <span class="switch-label">all skills</span>
19
+ </label>
20
+ <% end %>
21
+ </div>
22
+ <% unless @all_skills.empty? %>
23
+ <div class="check-grid tool-grid" data-toggle-counter-target="grid">
24
+ <% @all_skills.sort_by(&:name).each do |s| %>
25
+ <label class="check tool-check">
26
+ <input type="checkbox" name="skills[]" value="<%= s.name %>"<%== " checked" if @agent_skills&.include?(s.name) %>
27
+ data-toggle-counter-target="tool" data-locked="false" data-action="change->toggle-counter#update">
28
+ <span class="mono"><%= s.name %></span>
29
+ </label>
30
+ <% end %>
31
+ </div>
32
+ <% end %>
33
+ <div class="actions">
34
+ <button type="submit" class="primary" data-turbo-submits-with="Saving…">Save skills</button>
35
+ <a class="btn ghost" href="/studio/tools" data-turbo-frame="_top">Edit tools →</a>
36
+ </div>
37
+ </form>
38
+ </section>
@@ -0,0 +1,44 @@
1
+ <%# Master column of the Agents miller shell — rendered by the
2
+ index (agents.erb) and the detail (agent_detail.erb). Rows target the
3
+ agent-detail turbo-frame with data-turbo-action="advance": the detail
4
+ loads beside the list while the URL moves, so refresh/deep-link render the
5
+ same screen. Locals: active_id lights the current row (nil on the index);
6
+ show_outcomes renders the last-outcome pills (index only — the detail's
7
+ outcomes tab is THAT agent's series, so foreign pills would blur it). %>
8
+ <aside class="drill-pane drill-master">
9
+ <div class="drill-pane-head">
10
+ <div class="title"><strong>Agents <span class="count"><%= @agents.size %></span></strong></div>
11
+ <a class="btn primary btn-sm" href="/studio/agents" data-turbo-frame="_top">+ new</a>
12
+ </div>
13
+ <div class="drill-search">
14
+ <input type="search" class="filter-input" placeholder="Filter agents by id, model or provider…" aria-label="Filter agents"
15
+ data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
16
+ </div>
17
+ <nav class="drill-pane-body drill-list" aria-label="agents" data-controller="active-link">
18
+ <% active_id = defined?(active_id) ? active_id : nil %>
19
+ <% show_outcomes = defined?(show_outcomes) ? show_outcomes : false %>
20
+ <% if @agents.empty? %>
21
+ <div class="empty">No agents yet. Create your first agent from the pane on the right.</div>
22
+ <% else %>
23
+ <% @agents.each do |a| %>
24
+ <a class="drill-item<%== ' active' if a.id == active_id %>"
25
+ href="/studio/agents/<%= Rack::Utils.escape(a.id) %>"
26
+ data-turbo-frame="agent-detail" data-turbo-action="advance"
27
+ data-list-filter-target="item" data-filter-text="<%= a.id %> <%= a.model %> <%= a.provider %>"
28
+ data-active-link-target="item" data-action="click->active-link#select">
29
+ <div class="drill-item-name">
30
+ <span class="avatar sm <%= avatar_class(a.id) %>"><%= a.id[0, 2].upcase %></span>
31
+ <span class="txt" title="<%= a.id %>"><%= a.id %></span>
32
+ <% if a.memory %><span class="pill plain">mem</span><% end %>
33
+ <% if show_outcomes && (latest = @latest_outcomes&.dig(a.id)) %>
34
+ <span class="pill <%= latest[:outcome] == "conversion" ? "ok" : "warn" %>"
35
+ title="last outcome recorded via /v1/outcomes"><%= latest[:outcome] %><%== format(" · R$ %0.2f", latest[:value]) if latest[:value] && latest[:value].positive? %></span>
36
+ <% end %>
37
+ </div>
38
+ <div class="drill-item-sub"><%= a.model %><%== " · #{h(a.provider)}" if a.provider %></div>
39
+ </a>
40
+ <% end %>
41
+ <div class="empty" data-list-filter-target="empty" hidden>No agents match your filter.</div>
42
+ <% end %>
43
+ </nav>
44
+ </aside>
@@ -1,44 +1,61 @@
1
- <%# Shared transcript bubble/tool-card — one visual language for the
2
- playground history, the optimistic echo and the read-only session viewer.
3
- Locals: m = stored message hash {"role","content","at"}; prev = the message before
4
- it in the thread, or nil (used only to spot a steered message). `content` may be a
5
- String or a structured payload (rendered as pretty JSON, never #inspect).
1
+ <%# Shared transcript bubble/tool-chip — one visual language for the
2
+ playground history, the optimistic echo and the read-only session viewer
3
+ (transcript as product). Locals: m = stored message hash
4
+ {"role","content","at"}; prev = the message before it in the thread, or nil
5
+ (used only to spot a steered message). `content` may be a String or a
6
+ structured payload (rendered as pretty JSON, never #inspect).
6
7
  Assistant bubbles carry data-controller="markdown" (markdown.js re-escapes
7
- before markup — safe under the strict CSP). %>
8
+ before markup — safe under the strict CSP). The markup contract here is
9
+ mirrored by live_transcript_controller.js — change both or neither. %>
8
10
  <% role = m["role"].to_s %>
9
11
  <% at = short_time(m["at"]) %>
10
12
  <% if role == "tool" %>
13
+ <% artifact_url = artifact_link_in(m["content"]) %>
14
+ <% if artifact_url %>
15
+ <p class="artifact-chip"><%== chip_icon(:artifact) %><a class="btn sm" href="<%= artifact_url %>" target="_blank" rel="noopener">open artifact</a></p>
16
+ <% end %>
11
17
  <details class="toolcard result">
12
- <summary><span class="arrow">←</span><strong>tool result</strong><% unless at.empty? %><time class="stamp" datetime="<%= m["at"] %>"><%= at %></time><% end %></summary>
18
+ <summary><%== chip_icon(:result) %><strong>tool result</strong><% unless at.empty? %><time class="stamp" datetime="<%= m["at"] %>"><%= at %></time><% end %></summary>
13
19
  <pre><%= message_content(m["content"]) %></pre>
14
20
  </details>
15
21
  <% else %>
22
+ <%# `role` says where a message sits; `origin` says who wrote it, and the two
23
+ come apart (the engine delivering a subagent result as a "user" turn, a
24
+ guardrail's canned reply, a human operator after a handoff). Shown only
25
+ when it is NOT the natural producer for the role — otherwise every bubble
26
+ would carry a label that says nothing. %>
27
+ <% origin = Insika::MessageOrigin.origin_of(m) unless Insika::MessageOrigin.customer?(m) || Insika::MessageOrigin.agent?(m) %>
28
+ <%# STEERED: a customer message sitting right after a tool result arrived
29
+ while the turn was running and was appended at a tool-batch boundary. Derived
30
+ from position, not from a field — a steered message declares no origin, and it
31
+ is right not to: a person typed it. Nothing else in the engine ever puts a user
32
+ message after a tool result. Without the marker an operator reads two customer
33
+ messages in one turn with no explanation. %>
34
+ <% steered = role == "user" && prev && prev["role"].to_s == "tool" %>
35
+ <% calls = Array(m["tool_calls"]) %>
36
+ <%# A tool-call step with no prose renders no bubble at all — only its chips.
37
+ An empty bubble would be noise between "the model spoke" and "it called". %>
38
+ <% show_bubble = !(m["content"].to_s.empty? && calls.any?) %>
16
39
  <div class="msg <%= role %>">
17
- <div class="who"><%= role[0, 1].upcase %></div>
18
- <div class="bubble">
19
- <%# `role` says where a message sits; `origin` says who wrote it, and the two
20
- come apart (the engine delivering a subagent result as a "user" turn, a
21
- guardrail's canned reply, a human operator after a handoff). Shown only
22
- when it is NOT the natural producer for the role — otherwise every bubble
23
- would carry a label that says nothing. %>
24
- <% origin = Insika::MessageOrigin.origin_of(m) unless Insika::MessageOrigin.customer?(m) || Insika::MessageOrigin.agent?(m) %>
25
- <%# STEERED: a customer message sitting right after a tool result arrived
26
- while the turn was running and was appended at a tool-batch boundary. Derived
27
- from position, not from a field — a steered message declares no origin, and it
28
- is right not to: a person typed it. Nothing else in the engine ever puts a user
29
- message after a tool result. Without the marker an operator reads two customer
30
- messages in one turn with no explanation. %>
31
- <% steered = role == "user" && prev && prev["role"].to_s == "tool" %>
32
- <div class="role-tag"><%= role %><% if origin %> · <span class="origin"><%= origin %></span><% end %><% if steered %> · <span class="origin">steered</span><% end %><% unless at.empty? %> · <time class="stamp" datetime="<%= m["at"] %>"><%= at %></time><% end %></div>
33
- <% unless m["content"].to_s.empty? && Array(m["tool_calls"]).any? %>
34
- <div class="content<%== ' md' if role == 'assistant' %>"<%== ' data-controller="markdown"' if role == 'assistant' %>><%= message_content(m["content"]) %></div>
35
- <% end %>
36
- <% Array(m["tool_calls"]).each do |tc| %>
37
- <details class="toolcard">
38
- <summary><span class="arrow">→</span><strong><%= tc["name"] %></strong></summary>
39
- <pre><%= message_content(tc["arguments"]) %></pre>
40
- </details>
41
- <% end %>
40
+ <div class="msg-meta">
41
+ <span class="msg-role"><%= role %></span>
42
+ <% if origin %><span class="origin"><%= origin %></span><% end %>
43
+ <% if steered %><span class="origin">steered</span><% end %>
44
+ <% unless at.empty? %><time class="stamp" datetime="<%= m["at"] %>"><%= at %></time><% end %>
42
45
  </div>
46
+ <% if show_bubble %>
47
+ <div class="bubble">
48
+ <div class="content<%== ' md' if role == 'assistant' %>"<%== ' data-controller="markdown"' if role == 'assistant' %>><%= message_content(m["content"]) %></div>
49
+ </div>
50
+ <% end %>
43
51
  </div>
52
+ <%# Tool calls render BETWEEN bubbles (chips), not inside them — the same
53
+ reading order the live SSE stream shows: prose → call → result. %>
54
+ <% calls.each do |tc| %>
55
+ <% preview = message_content(tc["arguments"]).to_s.gsub(/\s+/, " ").strip %>
56
+ <details class="toolcard call">
57
+ <summary><%== chip_icon(:call) %><strong><%= tc["name"] %></strong><span class="chip-sub"><%= preview[0, 60] %><%== "…" if preview.size > 60 %></span></summary>
58
+ <pre><%= message_content(tc["arguments"]) %></pre>
59
+ </details>
60
+ <% end %>
44
61
  <% end %>