insika 0.3.0 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +180 -0
- data/README.md +45 -10
- data/bin/insika +684 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +94 -403
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +9 -7
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +71 -3
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +128 -3
- data/docs/FACTS.md +3 -3
- data/docs/HARVEST.md +5 -6
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +2 -2
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +15 -10
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +3 -3
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +22 -6
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +2 -2
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +152 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +4 -4
- data/docs/improve.md +20 -0
- data/docs/index.md +8 -5
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +73 -16
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/channel_delivery.rb +1 -1
- data/lib/insika/chat_builder.rb +22 -2
- data/lib/insika/commands/agent_payload.rb +2 -2
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +15 -3
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/context/priority.rb +2 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/doctor.rb +176 -8
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +60 -79
- data/lib/insika/dsl/server_boot.rb +23 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +103 -2
- data/lib/insika/env_schema.rb +16 -1
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +11 -0
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +231 -55
- data/lib/insika/followup_policy.rb +2 -25
- data/lib/insika/golden_store.rb +16 -1
- data/lib/insika/grounding/matcher.rb +1 -1
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +115 -31
- data/lib/insika/message_origin.rb +1 -1
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/onboarding.rb +17 -1
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +2 -2
- data/lib/insika/profile_source.rb +8 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/retention.rb +36 -1
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/server/app.rb +105 -15
- data/lib/insika/server/rack_app.rb +5 -1
- data/lib/insika/server/responses.rb +1 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +567 -45
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +21 -21
- data/lib/insika/studio/forms.rb +46 -5
- data/lib/insika/studio/nav_icons.rb +14 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +61 -820
- data/lib/insika/studio/views/agents.erb +70 -57
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/evals.erb +2 -2
- data/lib/insika/studio/views/facts.erb +1 -1
- data/lib/insika/studio/views/funnel.erb +1 -1
- data/lib/insika/studio/views/home.erb +106 -67
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +14 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/session.erb +231 -177
- data/lib/insika/studio/views/settings.erb +39 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +38 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +58 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tools/generate_image.rb +52 -7
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +15 -4
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +184 -12
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +57 -0
- metadata +105 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
|
@@ -1,192 +1,246 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
<h2>Briefing</h2>
|
|
22
|
-
<span class="muted">this conversation's working state — read-only, written by the agent</span>
|
|
1
|
+
<%# Session viewer — three zones: conversation list |
|
|
2
|
+
transcript | metadata margin. The master column is the same
|
|
3
|
+
recent-conversations list the chats index renders; rows frame-navigate
|
|
4
|
+
(session-detail) with the URL advancing, so refresh/deep-link render the
|
|
5
|
+
same screen. The transcript itself is the product: role-labeled bubbles,
|
|
6
|
+
tool chips between them, thinking collapsed, session markers in the
|
|
7
|
+
margin (top-right ghost pills). %>
|
|
8
|
+
<%# Roda renders with plain locals — no local_assigns hash (Rails-ism). %>
|
|
9
|
+
<% frame_only = defined?(frame_only) && frame_only %>
|
|
10
|
+
<% unless frame_only %>
|
|
11
|
+
<header class="page-head">
|
|
12
|
+
<div>
|
|
13
|
+
<nav class="crumbs"><a href="/studio/chats">chats</a> / <span>session</span></nav>
|
|
14
|
+
<h1>Session
|
|
15
|
+
<span class="copy-id" data-controller="clipboard" data-clipboard-text-value="<%= @session.id %>">
|
|
16
|
+
<span class="mono"><%= @session.id[0, 16] %></span>
|
|
17
|
+
<button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy" aria-label="Copy full session id">copy id</button>
|
|
18
|
+
</span>
|
|
19
|
+
<span class="sub"><%= Array(@session.messages).size %> message(s) · updated <%= @session.updated_at %></span>
|
|
20
|
+
</h1>
|
|
23
21
|
</div>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
<a class="btn" href="/studio/playground?session_id=<%= Rack::Utils.escape(@session.id) %>">Continue in playground →</a>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<div class="drill" data-controller="list-filter">
|
|
26
|
+
<aside class="drill-pane drill-master">
|
|
27
|
+
<div class="drill-pane-head">
|
|
28
|
+
<div class="title"><strong>Conversations <span class="count"><%= @sessions.size %></span></strong></div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="drill-search">
|
|
31
|
+
<input type="search" class="filter-input" placeholder="Filter conversations…" aria-label="Filter conversations"
|
|
32
|
+
data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
|
|
33
|
+
</div>
|
|
34
|
+
<nav class="drill-pane-body drill-list" aria-label="conversations" data-controller="active-link">
|
|
35
|
+
<% @sessions.each do |s| %>
|
|
36
|
+
<a class="drill-item<%== ' active' if s.id == @session.id %>"
|
|
37
|
+
href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>"
|
|
38
|
+
data-turbo-frame="session-detail" data-turbo-action="advance"
|
|
39
|
+
data-list-filter-target="item" data-filter-text="<%= s.id %> <%= session_preview(s) %>"
|
|
40
|
+
data-active-link-target="item" data-action="click->active-link#select">
|
|
41
|
+
<div class="drill-item-name"><span class="txt mono"><%= s.id[0, 18] %></span></div>
|
|
42
|
+
<div class="drill-item-sub"><%= Array(s.messages).size %> msg · <%= time_ago(s.updated_at) %></div>
|
|
43
|
+
</a>
|
|
44
|
+
<% end %>
|
|
45
|
+
</nav>
|
|
46
|
+
</aside>
|
|
31
47
|
<% end %>
|
|
48
|
+
<turbo-frame id="session-detail" class="drill-pane drill-detail session-pane">
|
|
49
|
+
<div class="session-columns">
|
|
50
|
+
<%# ── Center: the transcript ───────────────────────────────────── %>
|
|
51
|
+
<section class="session-transcript" aria-label="transcript">
|
|
52
|
+
<%# Session markers in the margin: start, agent,
|
|
53
|
+
shape — ghost pills, right-aligned, from data the view already
|
|
54
|
+
has. %>
|
|
55
|
+
<% first_at = Array(@session.messages).first&.[]("at") %>
|
|
56
|
+
<% marker_agent = session_agent(@session) %>
|
|
57
|
+
<div class="marker-row">
|
|
58
|
+
<% unless short_time(first_at).empty? %><span class="ghost-pill">session · <%= short_time(first_at) %></span><% end %>
|
|
59
|
+
<% unless marker_agent.empty? %><span class="ghost-pill">agent · <%= marker_agent %></span><% end %>
|
|
60
|
+
<span class="ghost-pill"><%= Array(@session.messages).size %> messages</span>
|
|
61
|
+
</div>
|
|
32
62
|
|
|
33
|
-
<%
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<% end %>
|
|
60
|
-
<% unless skills.empty? %>
|
|
61
|
-
<span class="badge">skills <%= skills.length %></span>
|
|
62
|
-
<% end %>
|
|
63
|
-
<% unless Array(c["evicted"]).empty? %>
|
|
64
|
-
<span class="lat">evicted: <%= Array(c["evicted"]).join(", ") %></span>
|
|
65
|
-
<% end %>
|
|
66
|
-
</summary>
|
|
67
|
-
<% (c["categories"] || {}).sort_by { |_k, v| -(v["tokens"] || 0) }.each do |name, cat| %>
|
|
68
|
-
<div class="kv"><span><%= name %></span>
|
|
69
|
-
<div><%= cat["tokens"] %> tokens · <%= cat["fragments"] %> fragment(s)<%== " · pinned" if (cat["pinned"] || 0).positive? %><%== " · identity" if cat["layer"] == "identity" %>
|
|
70
|
-
<%# WHICH skills the turn injected AND WHY — the only after-the-fact %>
|
|
71
|
-
<%# record of activation once the bodies arrive by context instead of a %>
|
|
72
|
-
<%# tool call. The reason is the half the operator was missing: a name %>
|
|
73
|
-
<%# alone does not say whether they triggered it or the agent always has it. %>
|
|
74
|
-
<% labels = Array(cat["labels"]).uniq %>
|
|
75
|
-
<% unless labels.empty? %>
|
|
76
|
-
<br><span class="lat"><%= labels.map { |l| skill_label(l) }.join(", ") %></span>
|
|
63
|
+
<% if Array(@session.messages).empty? %>
|
|
64
|
+
<div class="empty">This session has no messages.</div>
|
|
65
|
+
<% else %>
|
|
66
|
+
<div class="thread">
|
|
67
|
+
<%# A skill loaded by the model shows up here on its own: load_skill is a tool,
|
|
68
|
+
so its body is a persisted `role: tool` message. A skill injected by CONTEXT
|
|
69
|
+
(triggers/skills_eager) is part of the system prompt — never a message — so
|
|
70
|
+
the thread had no trace of it at all. Synthesized from the context trace so
|
|
71
|
+
both activation paths read the same way in the transcript.
|
|
72
|
+
|
|
73
|
+
Placed by TIME, not by counting turns: every message of a turn is persisted
|
|
74
|
+
together at the end of it, and the context stage always runs before that, so
|
|
75
|
+
each entry lands at the top of its own turn. Ordinal counting would drift on
|
|
76
|
+
a steered message (one that joins a turn already running). A message with no
|
|
77
|
+
`at` (older sessions) simply flushes nothing. %>
|
|
78
|
+
<% pending = (@context_traces || [])
|
|
79
|
+
.select { |c| Array((c["categories"] || {}).dig("skilltrigger", "labels")).any? }
|
|
80
|
+
.sort_by { |c| c["at"].to_s } %>
|
|
81
|
+
<% Array(@session.messages).each_with_index do |m, i| %>
|
|
82
|
+
<% while (entry = pending.first) && !m["at"].to_s.empty? && entry["at"].to_s <= m["at"].to_s %>
|
|
83
|
+
<% pending.shift %>
|
|
84
|
+
<% labels = Array(entry["categories"]["skilltrigger"]["labels"]).uniq %>
|
|
85
|
+
<details class="toolcard skill">
|
|
86
|
+
<summary><%== chip_icon(:skill) %><strong>skills · <%= skill_mode(labels) %> (<%= labels.length %>)</strong><span class="at"><%= entry["at"] %></span></summary>
|
|
87
|
+
<pre><%= labels.map { |l| skill_label(l) }.join("\n") %></pre>
|
|
88
|
+
</details>
|
|
77
89
|
<% end %>
|
|
90
|
+
<%# `prev` is what makes a STEERED message visible: a customer message that lands
|
|
91
|
+
right after a tool result arrived mid-run. See _message.erb. %>
|
|
92
|
+
<%== render("_message", locals: { m: m, prev: i.positive? ? @session.messages[i - 1] : nil }) %>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
<% end %>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<%# ── Right margin: the session's metadata ──────────────────────── %>
|
|
99
|
+
<aside class="session-margin" aria-label="session metadata">
|
|
100
|
+
<% briefing = @session.respond_to?(:briefing) ? (@session.briefing || {}) : {} %>
|
|
101
|
+
<% fields = briefing["fields"] || {} %>
|
|
102
|
+
<% next_step = briefing["next_step"] %>
|
|
103
|
+
<% if fields.any? || next_step %>
|
|
104
|
+
<div class="card">
|
|
105
|
+
<div class="trace-head">
|
|
106
|
+
<h2>Briefing</h2>
|
|
78
107
|
</div>
|
|
108
|
+
<% fields.sort.each do |name, value| %>
|
|
109
|
+
<div class="kv"><span><%= name %></span><div><%= value %></div></div>
|
|
110
|
+
<% end %>
|
|
111
|
+
<% if next_step %>
|
|
112
|
+
<div class="kv"><span>next step</span><div><%= next_step %></div></div>
|
|
113
|
+
<% end %>
|
|
79
114
|
</div>
|
|
80
115
|
<% end %>
|
|
81
|
-
<% tools = c["tools"] || {} %>
|
|
82
|
-
<div class="kv"><span>tools</span>
|
|
83
|
-
<div><%= tools["tokens"] %> tokens · <%= tools["count"] %> tool schema(s)</div>
|
|
84
|
-
</div>
|
|
85
|
-
</details>
|
|
86
|
-
</div>
|
|
87
|
-
<% end %>
|
|
88
|
-
</div>
|
|
89
|
-
<% end %>
|
|
90
116
|
|
|
91
|
-
<%
|
|
92
|
-
<% unless
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
</div>
|
|
98
|
-
<% traces.keys.sort.each do |turn| %>
|
|
99
|
-
<div class="trace-turn">
|
|
100
|
-
<div class="turn-label">turn <%= turn %></div>
|
|
101
|
-
<% traces[turn].each do |t| %>
|
|
102
|
-
<details class="trace-item <%= t["ok"] ? "ok" : "err" %>">
|
|
103
|
-
<summary>
|
|
104
|
-
<span class="badge"><%= t["ok"] ? "ok" : "error" %></span>
|
|
105
|
-
<strong><%= t["tool"] %></strong>
|
|
106
|
-
<span class="lat"><%= t["ms"] %>ms</span>
|
|
107
|
-
<span class="at"><%= t["at"] %></span>
|
|
108
|
-
</summary>
|
|
109
|
-
<div class="kv"><span>args</span>
|
|
110
|
-
<div class="copywrap" data-controller="clipboard">
|
|
111
|
-
<button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy">copy</button>
|
|
112
|
-
<pre data-clipboard-target="source"><%= t["args"] %></pre>
|
|
117
|
+
<% ctx_traces = @context_traces || [] %>
|
|
118
|
+
<% unless ctx_traces.empty? %>
|
|
119
|
+
<div class="card tooltrace">
|
|
120
|
+
<div class="trace-head">
|
|
121
|
+
<h2>Context</h2>
|
|
122
|
+
<span class="muted">tokens by category per turn (estimated)</span>
|
|
113
123
|
</div>
|
|
124
|
+
<% ctx_traces.each do |c| %>
|
|
125
|
+
<div class="trace-turn">
|
|
126
|
+
<div class="turn-label">turn <%= c["turn"] %> · <%= c["at"] %></div>
|
|
127
|
+
<%# Skills injected by context (triggers/skills_eager) leave NO tool call in
|
|
128
|
+
the transcript, so this card is the only record that they were active.
|
|
129
|
+
The names live in the body, but the count belongs in the SUMMARY: the
|
|
130
|
+
card is collapsed, and "was the skill on?" is the question an operator
|
|
131
|
+
asks on every single turn. %>
|
|
132
|
+
<% skills = Array((c["categories"] || {}).dig("skilltrigger", "labels")).uniq %>
|
|
133
|
+
<details class="trace-item ok">
|
|
134
|
+
<summary>
|
|
135
|
+
<span class="badge"><%= c["used"] %> / <%= c["cap"] %></span>
|
|
136
|
+
<strong>context</strong>
|
|
137
|
+
<%# the prefix cache-hit of the turn + the first block
|
|
138
|
+
whose bytes changed vs the previous turn (PII-free category id). %>
|
|
139
|
+
<% cache = c["cache"] %>
|
|
140
|
+
<% if cache && cache["hit_pct"] %><span class="badge"><%= cache["hit_pct"] %>% cached</span><% end %>
|
|
141
|
+
<% if cache && cache["invalidation_reason"] %>
|
|
142
|
+
<span class="lat">broke: <%= cache["invalidation_reason"] %></span>
|
|
143
|
+
<% end %>
|
|
144
|
+
<% unless skills.empty? %>
|
|
145
|
+
<span class="badge">skills <%= skills.length %></span>
|
|
146
|
+
<% end %>
|
|
147
|
+
<% unless Array(c["evicted"]).empty? %>
|
|
148
|
+
<span class="lat">evicted: <%= Array(c["evicted"]).join(", ") %></span>
|
|
149
|
+
<% end %>
|
|
150
|
+
</summary>
|
|
151
|
+
<% (c["categories"] || {}).sort_by { |_k, v| -(v["tokens"] || 0) }.each do |name, cat| %>
|
|
152
|
+
<div class="kv"><span><%= name %></span>
|
|
153
|
+
<div><%= cat["tokens"] %> tokens · <%= cat["fragments"] %> fragment(s)<%== " · pinned" if (cat["pinned"] || 0).positive? %><%== " · identity" if cat["layer"] == "identity" %>
|
|
154
|
+
<%# WHICH skills the turn injected AND WHY — the only after-the-fact %>
|
|
155
|
+
<%# record of activation once the bodies arrive by context instead of a %>
|
|
156
|
+
<%# tool call. The reason is the half the operator was missing: a name %>
|
|
157
|
+
<%# alone does not say whether they triggered it or the agent always has it. %>
|
|
158
|
+
<% labels = Array(cat["labels"]).uniq %>
|
|
159
|
+
<% unless labels.empty? %>
|
|
160
|
+
<br><span class="lat"><%= labels.map { |l| skill_label(l) }.join(", ") %></span>
|
|
161
|
+
<% end %>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<% end %>
|
|
165
|
+
<% tools = c["tools"] || {} %>
|
|
166
|
+
<div class="kv"><span>tools</span>
|
|
167
|
+
<div><%= tools["tokens"] %> tokens · <%= tools["count"] %> tool schema(s)</div>
|
|
168
|
+
</div>
|
|
169
|
+
</details>
|
|
170
|
+
</div>
|
|
171
|
+
<% end %>
|
|
114
172
|
</div>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
173
|
+
<% end %>
|
|
174
|
+
|
|
175
|
+
<% traces = @tool_traces || {} %>
|
|
176
|
+
<% unless traces.empty? %>
|
|
177
|
+
<div class="card tooltrace">
|
|
178
|
+
<div class="trace-head">
|
|
179
|
+
<h2>Tool calls</h2>
|
|
180
|
+
<span class="muted">debug — args + response, masked</span>
|
|
119
181
|
</div>
|
|
182
|
+
<% traces.keys.sort.each do |turn| %>
|
|
183
|
+
<div class="trace-turn">
|
|
184
|
+
<div class="turn-label">turn <%= turn %></div>
|
|
185
|
+
<% traces[turn].each do |t| %>
|
|
186
|
+
<details class="trace-item <%= t["ok"] ? "ok" : "err" %>">
|
|
187
|
+
<summary>
|
|
188
|
+
<span class="badge"><%= t["ok"] ? "ok" : "error" %></span>
|
|
189
|
+
<strong><%= t["tool"] %></strong>
|
|
190
|
+
<span class="lat"><%= t["ms"] %>ms</span>
|
|
191
|
+
<span class="at"><%= t["at"] %></span>
|
|
192
|
+
</summary>
|
|
193
|
+
<div class="kv"><span>args</span>
|
|
194
|
+
<div class="copywrap" data-controller="clipboard">
|
|
195
|
+
<button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy">copy</button>
|
|
196
|
+
<pre data-clipboard-target="source"><%= t["args"] %></pre>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
<div class="kv"><span>response</span>
|
|
200
|
+
<div class="copywrap" data-controller="clipboard">
|
|
201
|
+
<button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy">copy</button>
|
|
202
|
+
<pre data-clipboard-target="source"><%= t["result"] %></pre>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</details>
|
|
206
|
+
<% end %>
|
|
207
|
+
</div>
|
|
208
|
+
<% end %>
|
|
120
209
|
</div>
|
|
121
|
-
|
|
122
|
-
<% end %>
|
|
123
|
-
</div>
|
|
124
|
-
<% end %>
|
|
125
|
-
</div>
|
|
126
|
-
<% end %>
|
|
210
|
+
<% end %>
|
|
127
211
|
|
|
128
|
-
<%# the session's stage history — outcomes folded to the agent's
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<% history = @stage_history || {} %>
|
|
132
|
-
<% unless history.empty? %>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
212
|
+
<%# the session's stage history — outcomes folded to the agent's
|
|
213
|
+
declared stages (unmapped kinds render the raw outcome: the hole is visible).
|
|
214
|
+
Without a declaration the block renders the raw outcomes. %>
|
|
215
|
+
<% history = @stage_history || {} %>
|
|
216
|
+
<% unless history.empty? %>
|
|
217
|
+
<div class="card">
|
|
218
|
+
<div class="trace-head">
|
|
219
|
+
<h2>Stage history</h2>
|
|
220
|
+
<span class="muted">
|
|
221
|
+
<% if history[:declaration] %>
|
|
222
|
+
<%= history[:declaration].stages.join(" → ") %> · outcome funnel
|
|
223
|
+
<% else %>
|
|
224
|
+
no funnel declared — raw outcomes
|
|
225
|
+
<% end %>
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="kv">
|
|
229
|
+
<% history[:rows].each do |row| %>
|
|
230
|
+
<% if row[:stage] %>
|
|
231
|
+
<span><%= row[:outcome] %></span>
|
|
232
|
+
<div>→ <strong><%= row[:stage] %></strong> <span class="lat"><%= row[:at] %></span></div>
|
|
233
|
+
<% else %>
|
|
234
|
+
<span><%= row[:outcome] %></span>
|
|
235
|
+
<div><span class="lat">not a declared stage</span> <span class="lat"><%= row[:at] %></span></div>
|
|
236
|
+
<% end %>
|
|
237
|
+
<% end %>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
<% end %>
|
|
241
|
+
</aside>
|
|
242
|
+
</div>
|
|
243
|
+
</turbo-frame>
|
|
244
|
+
<% unless frame_only %>
|
|
155
245
|
</div>
|
|
156
246
|
<% end %>
|
|
157
|
-
|
|
158
|
-
<div class="card transcript">
|
|
159
|
-
<% if Array(@session.messages).empty? %>
|
|
160
|
-
<div class="empty">This session has no messages.</div>
|
|
161
|
-
<% else %>
|
|
162
|
-
<div class="thread">
|
|
163
|
-
<%# A skill loaded by the model shows up here on its own: load_skill is a tool,
|
|
164
|
-
so its body is a persisted `role: tool` message. A skill injected by CONTEXT
|
|
165
|
-
(triggers/skills_eager) is part of the system prompt — never a message — so
|
|
166
|
-
the thread had no trace of it at all. Synthesized from the context trace so
|
|
167
|
-
both activation paths read the same way in the transcript.
|
|
168
|
-
|
|
169
|
-
Placed by TIME, not by counting turns: every message of a turn is persisted
|
|
170
|
-
together at the end of it, and the context stage always runs before that, so
|
|
171
|
-
each entry lands at the top of its own turn. Ordinal counting would drift on
|
|
172
|
-
a steered message (one that joins a turn already running). A message with no
|
|
173
|
-
`at` (older sessions) simply flushes nothing. %>
|
|
174
|
-
<% pending = (@context_traces || [])
|
|
175
|
-
.select { |c| Array((c["categories"] || {}).dig("skilltrigger", "labels")).any? }
|
|
176
|
-
.sort_by { |c| c["at"].to_s } %>
|
|
177
|
-
<% Array(@session.messages).each_with_index do |m, i| %>
|
|
178
|
-
<% while (entry = pending.first) && !m["at"].to_s.empty? && entry["at"].to_s <= m["at"].to_s %>
|
|
179
|
-
<% pending.shift %>
|
|
180
|
-
<% labels = Array(entry["categories"]["skilltrigger"]["labels"]).uniq %>
|
|
181
|
-
<details class="toolcard skill">
|
|
182
|
-
<summary><span class="arrow">↑</span><strong>skills · <%= skill_mode(labels) %> (<%= labels.length %>)</strong><span class="at"><%= entry["at"] %></span></summary>
|
|
183
|
-
<pre><%= labels.map { |l| skill_label(l) }.join("\n") %></pre>
|
|
184
|
-
</details>
|
|
185
|
-
<% end %>
|
|
186
|
-
<%# `prev` is what makes a STEERED message visible: a customer message that lands
|
|
187
|
-
right after a tool result arrived mid-run. See _message.erb. %>
|
|
188
|
-
<%== render("_message", locals: { m: m, prev: i.positive? ? @session.messages[i - 1] : nil }) %>
|
|
189
|
-
<% end %>
|
|
190
|
-
</div>
|
|
191
|
-
<% end %>
|
|
192
|
-
</div>
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
["models", "Model defaults", "platform model layer (Chat › Agent › platform)"],
|
|
6
6
|
["edge", "Edge limits", "rate-limit per chat, token ceiling per agent"],
|
|
7
7
|
["evals", "Evals", "who grades a rubric, and how a panel agrees"],
|
|
8
|
-
["llm", "LLM providers", "keys, base URL, models per provider"]
|
|
8
|
+
["llm", "LLM providers", "keys, base URL, models per provider"],
|
|
9
|
+
["demo", "Demo data", "see every loop working at once"]
|
|
9
10
|
] %>
|
|
10
11
|
<div class="drill">
|
|
11
12
|
<aside class="drill-pane drill-master">
|
|
@@ -120,6 +121,43 @@
|
|
|
120
121
|
</form>
|
|
121
122
|
</div>
|
|
122
123
|
|
|
124
|
+
<% elsif @section == "demo" %>
|
|
125
|
+
<div class="drill-pane-head">
|
|
126
|
+
<div class="title"><strong>Demo data</strong></div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="drill-pane-body">
|
|
129
|
+
<p class="muted">
|
|
130
|
+
Populates the bundled <span class="mono">demo-store</span> agent with enough data
|
|
131
|
+
to see every loop working at once: a funnel with a frozen baseline, follow-ups in
|
|
132
|
+
all four states, refinement runs across the lifecycle, pending + resolved
|
|
133
|
+
approvals, distillation proposals/facts, and a golden set with a baseline.
|
|
134
|
+
The same seed the CLI's <span class="mono">insika demo:seed</span> runs.
|
|
135
|
+
</p>
|
|
136
|
+
<p class="muted">
|
|
137
|
+
This instance is currently <strong><%= @persistence.empty? ? "unknown" : @persistence %></strong>.
|
|
138
|
+
Seeding writes into <strong>this</strong> store — only fine to run against a scratch
|
|
139
|
+
or ephemeral instance, never against a store with real tenant data.
|
|
140
|
+
</p>
|
|
141
|
+
<form method="post" action="/studio/settings/seed-demo-data" class="actions"
|
|
142
|
+
data-turbo-confirm="Seed the demo-store agent into this <%= @persistence.empty? ? 'store' : @persistence %> instance?">
|
|
143
|
+
<%== csrf_tag %>
|
|
144
|
+
<label class="check">
|
|
145
|
+
<input type="checkbox" name="force" value="1">
|
|
146
|
+
reseed (append another batch) even if demo-store already exists
|
|
147
|
+
</label>
|
|
148
|
+
<button type="submit" class="primary" data-turbo-submits-with="Seeding…">Seed demo data</button>
|
|
149
|
+
</form>
|
|
150
|
+
<p class="muted">
|
|
151
|
+
Afterwards, open
|
|
152
|
+
<a href="/studio/funnel?agent=demo-store">Funnel</a>,
|
|
153
|
+
<a href="/studio/followups?agent=demo-store">Follow-ups</a>,
|
|
154
|
+
<a href="/studio/refinement?agent=demo-store">Refinement</a>,
|
|
155
|
+
<a href="/studio/approvals">Approvals</a>,
|
|
156
|
+
<a href="/studio/facts">Facts</a> and
|
|
157
|
+
<a href="/studio/evals?agent=demo-store">Evals</a>.
|
|
158
|
+
</p>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
123
161
|
<% else %>
|
|
124
162
|
<div class="drill-pane-head">
|
|
125
163
|
<div class="title"><strong>LLM providers <span class="count"><%= @providers.size %></span></strong></div>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<a class="drill-item<%== ' active' if @selected == s.name %>"
|
|
20
20
|
href="/studio/skills/<%= Rack::Utils.escape(s.name) %>"
|
|
21
21
|
data-list-filter-target="item" data-filter-text="<%= s.name %> <%= s.description %>">
|
|
22
|
-
<div class="drill-item-name"><span class="txt"><%= s.name %></span><% if @stored.include?(s.name) %><span class="pill info">authored</span><% end %></div>
|
|
22
|
+
<div class="drill-item-name"><span class="txt" title="<%= s.name %>"><%= s.name %></span><% if @stored.include?(s.name) %><span class="pill info">authored</span><% end %></div>
|
|
23
23
|
<div class="drill-item-sub"><%= s.description %></div>
|
|
24
24
|
</a>
|
|
25
25
|
<% end %>
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
<%# Tools —
|
|
2
|
-
|
|
1
|
+
<%# Tools — miller columns: [data tools + agents] master |
|
|
2
|
+
agent tool matrix in a turbo-frame. Master rows for agents frame-navigate
|
|
3
|
+
(?a=) with the URL advancing; data-tool rows keep full-page navigation to
|
|
4
|
+
their standalone editor. @sel_agent (from ?a=) drives the detail. %>
|
|
5
|
+
<%# Roda renders with plain locals — no local_assigns hash (Rails-ism). %>
|
|
6
|
+
<% frame_only = defined?(frame_only) && frame_only %>
|
|
7
|
+
<% unless frame_only %>
|
|
3
8
|
<div class="drill" data-controller="list-filter">
|
|
4
9
|
<aside class="drill-pane drill-master">
|
|
5
10
|
<div class="drill-pane-head">
|
|
@@ -10,14 +15,14 @@
|
|
|
10
15
|
<input type="search" class="filter-input" placeholder="Filter agents…" aria-label="Filter agents"
|
|
11
16
|
data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
|
|
12
17
|
</div>
|
|
13
|
-
<nav class="drill-pane-body drill-list" aria-label="tools and agents">
|
|
18
|
+
<nav class="drill-pane-body drill-list" aria-label="tools and agents" data-controller="active-link">
|
|
14
19
|
<% unless @data_tool_names.empty? %>
|
|
15
20
|
<div class="drill-group-label">Data tools · HTTP, editable</div>
|
|
16
21
|
<% @data_tool_names.sort.each do |name| %>
|
|
17
22
|
<% dropped = @dropped_tool_names.include?(name) %>
|
|
18
23
|
<a class="drill-item drill-item-sm" href="/studio/tools/def/<%= Rack::Utils.escape(name) %>"
|
|
19
24
|
<%== %( title="its definition is invalid — no agent can call it until it is fixed") if dropped %>>
|
|
20
|
-
<div class="drill-item-name"><span class="txt"><%= name %></span><span class="pill plain">data</span><% if dropped %> <span class="pill locked">dropped</span><% end %></div>
|
|
25
|
+
<div class="drill-item-name"><span class="txt" title="<%= name %>"><%= name %></span><span class="pill plain">data</span><% if dropped %> <span class="pill locked">dropped</span><% end %></div>
|
|
21
26
|
</a>
|
|
22
27
|
<% end %>
|
|
23
28
|
<% end %>
|
|
@@ -25,8 +30,10 @@
|
|
|
25
30
|
<% @agents.each do |a| %>
|
|
26
31
|
<a class="drill-item<%== ' active' if @sel_agent && @sel_agent.id == a.id %>"
|
|
27
32
|
href="/studio/tools?a=<%= Rack::Utils.escape(a.id) %>"
|
|
28
|
-
data-
|
|
29
|
-
|
|
33
|
+
data-turbo-frame="tool-detail" data-turbo-action="advance"
|
|
34
|
+
data-list-filter-target="item" data-filter-text="<%= a.id %>"
|
|
35
|
+
data-active-link-target="item" data-action="click->active-link#select">
|
|
36
|
+
<div class="drill-item-name"><span class="txt" title="<%= a.id %>"><%= a.id %></span>
|
|
30
37
|
<span class="pill plain"><%= a.tools_allow.nil? ? "all" : "#{tools_on_count(a, @tools)}/#{@tools.size}" %></span>
|
|
31
38
|
</div>
|
|
32
39
|
</a>
|
|
@@ -34,8 +41,12 @@
|
|
|
34
41
|
<div class="empty" data-list-filter-target="empty" hidden>No agents match your filter.</div>
|
|
35
42
|
</nav>
|
|
36
43
|
</aside>
|
|
37
|
-
|
|
38
|
-
<
|
|
44
|
+
<% end %>
|
|
45
|
+
<turbo-frame id="tool-detail" class="drill-pane drill-detail">
|
|
46
|
+
<% if frame_only %>
|
|
47
|
+
<% if flash["error"] %><div class="flash err" role="alert"><%= flash["error"] %></div><% end %>
|
|
48
|
+
<% if flash["notice"] %><div class="flash ok" role="status"><%= flash["notice"] %></div><% end %>
|
|
49
|
+
<% end %>
|
|
39
50
|
<% if @sel_agent.nil? %>
|
|
40
51
|
<div class="drill-empty">
|
|
41
52
|
<div>
|
|
@@ -60,6 +71,8 @@
|
|
|
60
71
|
<span class="switch-track" aria-hidden="true"></span>
|
|
61
72
|
<span class="switch-label">all tools</span>
|
|
62
73
|
</label>
|
|
74
|
+
<button type="button" class="btn ghost btn-sm" data-action="click->toggle-counter#selectNone"
|
|
75
|
+
title="uncheck every tool (deny-all — Save tools to apply)">none</button>
|
|
63
76
|
</div>
|
|
64
77
|
<p class="muted">Checked tools are allowed next turn. A <span class="pill locked">locked</span> tool is on the denylist — deny always wins, so it can't be granted here.</p>
|
|
65
78
|
<form id="tools-form" method="post" action="/studio/tools/<%= Rack::Utils.escape(a.id) %>">
|
|
@@ -84,5 +97,7 @@
|
|
|
84
97
|
</form>
|
|
85
98
|
</div>
|
|
86
99
|
<% end %>
|
|
87
|
-
</
|
|
100
|
+
</turbo-frame>
|
|
101
|
+
<% unless frame_only %>
|
|
88
102
|
</div>
|
|
103
|
+
<% end %>
|
|
@@ -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.
|