insika 0.1.0 → 0.3.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 +199 -5
- data/README.md +8 -2
- data/bin/insika +231 -13
- data/docs/AGENTS.md +505 -6
- data/docs/API.md +56 -0
- data/docs/CHANNELS.md +100 -10
- data/docs/CONTEXT.md +147 -19
- data/docs/DEPLOY.md +34 -11
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +20 -1
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +117 -0
- data/docs/LOADTEST.md +17 -10
- data/docs/OBSERVABILITY.md +65 -2
- data/docs/REFINEMENT.md +9 -9
- data/docs/RELEASING.md +34 -7
- data/docs/RUNNING-LOCAL.md +4 -4
- data/docs/SECURITY.md +85 -11
- data/docs/SKILLS.md +189 -3
- data/docs/SOAK.md +127 -0
- data/docs/TOOLS.md +70 -2
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/domain.md +115 -0
- data/docs/index.md +2 -2
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +228 -26
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +166 -0
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +132 -24
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +80 -6
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +9 -9
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +145 -13
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +6 -4
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/create_agent.rb +2 -2
- data/lib/insika/commands/create_session.rb +1 -1
- data/lib/insika/commands/delete_llm_provider.rb +1 -1
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_tenant_data.rb +95 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +12 -12
- data/lib/insika/commands/import_mcp_tools.rb +1 -1
- data/lib/insika/commands/import_tools.rb +4 -4
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/resolve_refinement.rb +1 -1
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/run_refinement.rb +5 -5
- data/lib/insika/commands/send_message.rb +112 -15
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/set_agent_tools.rb +1 -1
- data/lib/insika/commands/set_skill_agents.rb +60 -19
- data/lib/insika/commands/trigger_workflow.rb +1 -1
- data/lib/insika/commands/update_agent.rb +1 -1
- data/lib/insika/commands/write_data_tool.rb +1 -1
- data/lib/insika/commands/write_golden.rb +1 -1
- data/lib/insika/commands/write_skill.rb +19 -9
- data/lib/insika/config_store.rb +8 -4
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +23 -5
- data/lib/insika/context/fragment.rb +31 -3
- data/lib/insika/context/priority.rb +6 -2
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +30 -2
- data/lib/insika/context/providers/request.rb +1 -1
- data/lib/insika/context/providers/session.rb +17 -2
- data/lib/insika/context/providers/skill.rb +7 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +128 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +962 -7
- data/lib/insika/dsl/runtime.rb +20 -11
- data/lib/insika/dsl/server_boot.rb +74 -4
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +152 -15
- data/lib/insika/edge_limiter.rb +167 -8
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +22 -12
- data/lib/insika/errors.rb +72 -5
- data/lib/insika/evals/assertions.rb +15 -14
- data/lib/insika/evals/baseline.rb +3 -3
- data/lib/insika/evals/golden.rb +8 -8
- data/lib/insika/evals/judge.rb +7 -7
- data/lib/insika/evals/pairwise.rb +21 -9
- data/lib/insika/evals/report.rb +2 -2
- data/lib/insika/evals/runner.rb +6 -6
- data/lib/insika/evals/transport.rb +2 -2
- data/lib/insika/event_stream.rb +23 -5
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +1092 -160
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +221 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +3 -3
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/http_client.rb +3 -3
- data/lib/insika/inbound_log.rb +1 -1
- data/lib/insika/llm_configurator.rb +3 -3
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +4 -4
- data/lib/insika/mcp_tool_ingestor.rb +6 -6
- data/lib/insika/media.rb +298 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +8 -3
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +8 -3
- data/lib/insika/outbox_store.rb +44 -6
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +3 -4
- data/lib/insika/pack.rb +3 -3
- data/lib/insika/pack_importer.rb +17 -15
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/pending_action_store.rb +1 -1
- data/lib/insika/plugin/loader.rb +2 -2
- data/lib/insika/policy/policy.rb +1 -1
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +34 -7
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +6 -3
- data/lib/insika/recovery.rb +47 -6
- data/lib/insika/refinement/candidate.rb +4 -4
- data/lib/insika/refinement/evidence_collector.rb +6 -6
- data/lib/insika/refinement/gate.rb +7 -7
- data/lib/insika/refinement/panel.rb +7 -7
- data/lib/insika/refinement/proposer.rb +10 -10
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +211 -0
- data/lib/insika/retention.rb +281 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +46 -6
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +34 -115
- data/lib/insika/safety/factory.rb +18 -5
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +20 -5
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +10 -6
- data/lib/insika/safety/output_validator.rb +13 -7
- data/lib/insika/safety/safe_responses.rb +1 -1
- data/lib/insika/sandbox/boundary.rb +2 -2
- data/lib/insika/sandbox.rb +1 -1
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +366 -54
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +31 -7
- data/lib/insika/server/responses.rb +58 -9
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +11 -7
- data/lib/insika/session_store.rb +66 -3
- data/lib/insika/settings_store.rb +15 -5
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +131 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +11 -2
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1058 -75
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +27 -26
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +274 -22
- data/lib/insika/studio/nav_icons.rb +7 -2
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +629 -86
- data/lib/insika/studio/views/agents.erb +11 -7
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +4 -1
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +2 -1
- data/lib/insika/studio/views/layout.erb +1 -0
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +133 -3
- data/lib/insika/studio/views/settings.erb +9 -12
- data/lib/insika/studio/views/skills.erb +66 -12
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +0 -1
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +22 -2
- data/lib/insika/telemetry/pricing.rb +3 -3
- data/lib/insika/telemetry/recorder.rb +1 -1
- data/lib/insika/telemetry.rb +2 -2
- data/lib/insika/testing/store_contract.rb +54 -33
- data/lib/insika/tick.rb +146 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +25 -15
- data/lib/insika/tool_envelope.rb +70 -1
- data/lib/insika/tool_manifest.rb +11 -7
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_store.rb +1 -1
- data/lib/insika/tool_trace_store.rb +1 -1
- data/lib/insika/tools/concurrency.rb +2 -2
- data/lib/insika/tools/data_defined_tool.rb +14 -5
- data/lib/insika/tools/generate_image.rb +44 -0
- data/lib/insika/tools/load_skill.rb +61 -3
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +4 -4
- data/lib/insika/tools/subagents.rb +1 -1
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +54 -13
- data/lib/insika/turn_timing.rb +24 -4
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +372 -34
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +122 -16
- metadata +95 -2
- data/lib/insika/server/admin_auth.rb +0 -29
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<header class="page-head">
|
|
2
|
+
<div>
|
|
3
|
+
<nav class="crumbs"><span>harvest</span></nav>
|
|
4
|
+
<h1>Harvest<span class="sub"><%= @agent ? "#{@agent}" : "no agent" %></span></h1>
|
|
5
|
+
</div>
|
|
6
|
+
</header>
|
|
7
|
+
|
|
8
|
+
<% if insika[:harvest_store].nil? %>
|
|
9
|
+
<div class="empty">No harvest — the store is not wired.</div>
|
|
10
|
+
<% elsif @agents.empty? %>
|
|
11
|
+
<div class="empty">No agents yet. Add a <span class="mono">harvest:</span> block to an agent's pack to mine its finished traffic.</div>
|
|
12
|
+
<% else %>
|
|
13
|
+
<div class="card">
|
|
14
|
+
<form method="post" action="/studio/harvest" class="actions">
|
|
15
|
+
<%== csrf_tag %>
|
|
16
|
+
<label>
|
|
17
|
+
Agent
|
|
18
|
+
<select name="agent">
|
|
19
|
+
<% @agents.each do |id| %>
|
|
20
|
+
<option value="<%= id %>"<%== ' selected' if id == @agent %>><%= id %></option>
|
|
21
|
+
<% end %>
|
|
22
|
+
</select>
|
|
23
|
+
</label>
|
|
24
|
+
<label title="Ignore the mined markers and re-read the whole window (the ledger dedup still protects).">
|
|
25
|
+
<input type="checkbox" name="full" value="1"> full window
|
|
26
|
+
</label>
|
|
27
|
+
<button type="submit" class="primary">Mine now</button>
|
|
28
|
+
</form>
|
|
29
|
+
<div class="skill-desc muted">
|
|
30
|
+
Reads this agent's finished conversations, asks the configured miner for SKILL
|
|
31
|
+
proposals, and filters them through the negative list and the evidence ledger.
|
|
32
|
+
Nothing is ever applied automatically — a candidate only lands after the double
|
|
33
|
+
gate AND a human approval.
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<% if @agents.size > 1 %>
|
|
38
|
+
<div class="filter-bar">
|
|
39
|
+
<% @agents.each do |id| %>
|
|
40
|
+
<a class="btn" href="/studio/harvest?agent=<%= Rack::Utils.escape(id) %>"<%== ' aria-current="page"' if id == @agent %>><%= id %></a>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<%# The human's inbox. %>
|
|
46
|
+
<% if @candidates.any? %>
|
|
47
|
+
<h2>Awaiting approval<span class="sub">the human's answer</span></h2>
|
|
48
|
+
<% @candidates.each do |cand| %>
|
|
49
|
+
<div class="card">
|
|
50
|
+
<div class="skill-desc">
|
|
51
|
+
<span class="pill ok">gated</span>
|
|
52
|
+
<span class="mono"><%= cand.name %></span>
|
|
53
|
+
<span class="muted">· <%= cand.description %></span>
|
|
54
|
+
</div>
|
|
55
|
+
<% Array(cand.triggers).each do |t| %>
|
|
56
|
+
<span class="pill"><%= t %></span>
|
|
57
|
+
<% end %>
|
|
58
|
+
<div class="skill-desc muted">
|
|
59
|
+
store <span class="mono"><%= cand.agent %></span> · proposed by
|
|
60
|
+
<span class="mono"><%= cand.proposer %></span> · <%= (cand.eval_gate || {})["passed_cases"] %>/<%= (cand.eval_gate || {})["cases"] %> case(s) no regression
|
|
61
|
+
· conversion <%= (cand.conversion_gate || {})["current"] %> vs <%= (cand.conversion_gate || {})["baseline"] %> (threshold <%= (cand.conversion_gate || {})["threshold"] %>)
|
|
62
|
+
</div>
|
|
63
|
+
<% if (excerpt = candidate_excerpt(cand)).any? %>
|
|
64
|
+
<details class="skill-desc">
|
|
65
|
+
<summary>evidence excerpt</summary>
|
|
66
|
+
<% excerpt.each do |sid, idx, text| %>
|
|
67
|
+
<div class="preview muted">
|
|
68
|
+
<a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid %></a> [<%= idx %>]: <%= text %>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</details>
|
|
72
|
+
<% end %>
|
|
73
|
+
<% Array(cand.origin).each do |sid| %>
|
|
74
|
+
<a class="mono muted" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid %></a>
|
|
75
|
+
<% end %>
|
|
76
|
+
<form method="post" action="/studio/harvest/promote" class="actions">
|
|
77
|
+
<%== csrf_tag %>
|
|
78
|
+
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
79
|
+
<input type="hidden" name="candidate_id" value="<%= cand.id %>">
|
|
80
|
+
<label>
|
|
81
|
+
Note
|
|
82
|
+
<input type="text" name="note" placeholder="why (optional)">
|
|
83
|
+
</label>
|
|
84
|
+
<button type="submit" class="primary">Promote</button>
|
|
85
|
+
<button type="submit" class="ghost" formaction="/studio/harvest/reject">Reject</button>
|
|
86
|
+
</form>
|
|
87
|
+
<div class="skill-desc muted">
|
|
88
|
+
Promoting snapshots the pre-promotion state, writes the skill into this store's
|
|
89
|
+
scope, enables it on the allowlist and appends the promotion log row. Rollback
|
|
90
|
+
is the one-click mirror.
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% end %>
|
|
95
|
+
|
|
96
|
+
<%# Gated-but-blocked: the ruler's hole, named. %>
|
|
97
|
+
<% if @blocked.any? %>
|
|
98
|
+
<h2>Blocked<span class="sub">gated, waiting on the ruler</span></h2>
|
|
99
|
+
<div class="card">
|
|
100
|
+
<ul class="history">
|
|
101
|
+
<% @blocked.each do |cand| %>
|
|
102
|
+
<li>
|
|
103
|
+
<span class="pill err">blocked</span>
|
|
104
|
+
<span class="mono"><%= cand.name %></span>
|
|
105
|
+
<span class="preview muted">
|
|
106
|
+
<% reason = (cand.conversion_gate || {})["reason"] %>
|
|
107
|
+
<% if reason == "no_frozen_baseline" %>
|
|
108
|
+
the store has no frozen funnel baseline — freeze it first on the
|
|
109
|
+
<a href="/studio/funnel">Funnel page</a>
|
|
110
|
+
<% elsif reason %>
|
|
111
|
+
<%= reason %>
|
|
112
|
+
<% else %>
|
|
113
|
+
<%= (cand.eval_gate || {})["reason"] %>
|
|
114
|
+
<% end %>
|
|
115
|
+
</span>
|
|
116
|
+
</li>
|
|
117
|
+
<% end %>
|
|
118
|
+
</ul>
|
|
119
|
+
</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
|
|
122
|
+
<%# Mined, not yet gated. %>
|
|
123
|
+
<% if @pending_gates.any? %>
|
|
124
|
+
<h2>Pending<span class="sub">mined, awaiting the double gate</span></h2>
|
|
125
|
+
<div class="card">
|
|
126
|
+
<ul class="history">
|
|
127
|
+
<% @pending_gates.each do |cand| %>
|
|
128
|
+
<li>
|
|
129
|
+
<span class="pill">pending</span>
|
|
130
|
+
<span class="mono"><%= cand.name %></span>
|
|
131
|
+
<span class="preview muted"><%= cand.description %></span>
|
|
132
|
+
<form method="post" action="/studio/harvest/gate" class="actions inline">
|
|
133
|
+
<%== csrf_tag %>
|
|
134
|
+
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
135
|
+
<input type="hidden" name="candidate_id" value="<%= cand.id %>">
|
|
136
|
+
<button type="submit" class="primary">Gate it</button>
|
|
137
|
+
<button type="submit" class="ghost" formaction="/studio/harvest/reject">Reject</button>
|
|
138
|
+
</form>
|
|
139
|
+
</li>
|
|
140
|
+
<% end %>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
<% end %>
|
|
144
|
+
|
|
145
|
+
<%# The promotion log — the single ledger, with the rollback mirror. %>
|
|
146
|
+
<% if @promotions.any? %>
|
|
147
|
+
<h2>Promoted<span class="sub">the append-only log</span></h2>
|
|
148
|
+
<div class="card">
|
|
149
|
+
<ul class="history">
|
|
150
|
+
<% @promotions.each do |row| %>
|
|
151
|
+
<li>
|
|
152
|
+
<% if row.rolled_back_at %>
|
|
153
|
+
<span class="pill err">rolled back</span>
|
|
154
|
+
<% else %>
|
|
155
|
+
<span class="pill ok">promoted</span>
|
|
156
|
+
<% end %>
|
|
157
|
+
<span class="mono"><%= row.skill %></span>
|
|
158
|
+
<span class="preview muted">
|
|
159
|
+
<%= row.approver %> · <%= row.at %>
|
|
160
|
+
<% if row.rolled_back_at %>· rolled back <%= row.rolled_back_at %><% end %>
|
|
161
|
+
</span>
|
|
162
|
+
<% unless row.rolled_back_at %>
|
|
163
|
+
<form method="post" action="/studio/harvest/rollback" class="actions inline">
|
|
164
|
+
<%== csrf_tag %>
|
|
165
|
+
<input type="hidden" name="agent" value="<%= row.agent %>">
|
|
166
|
+
<input type="hidden" name="snapshot_ref" value="<%= row.snapshot_ref %>">
|
|
167
|
+
<input type="text" name="reason" placeholder="why (optional)">
|
|
168
|
+
<button type="submit" class="ghost">Rollback</button>
|
|
169
|
+
</form>
|
|
170
|
+
<% end %>
|
|
171
|
+
</li>
|
|
172
|
+
<% end %>
|
|
173
|
+
</ul>
|
|
174
|
+
</div>
|
|
175
|
+
<% end %>
|
|
176
|
+
|
|
177
|
+
<%# The negative list block. %>
|
|
178
|
+
<% if @negative && @negative.rules.any? %>
|
|
179
|
+
<h2>Negative list<span class="sub">the versioned seed, engine-applied</span></h2>
|
|
180
|
+
<div class="card">
|
|
181
|
+
<ul class="history">
|
|
182
|
+
<% @negative.rules.each do |rule| %>
|
|
183
|
+
<li>
|
|
184
|
+
<span class="pill"><%= rule.rule %></span>
|
|
185
|
+
<span class="preview muted"><%= rule.pattern %><%= " — #{rule.note}" if presence(rule.note) %></span>
|
|
186
|
+
<span class="preview muted">rejected: <%= negative_rule_counts[rule.rule] || 0 %></span>
|
|
187
|
+
</li>
|
|
188
|
+
<% end %>
|
|
189
|
+
</ul>
|
|
190
|
+
<div class="skill-desc muted">
|
|
191
|
+
The list is pack data on the profile (hot-editable); the versioned
|
|
192
|
+
rules file seeds it via
|
|
193
|
+
<span class="mono">insika harvest:negative import --agent ID --file F</span>.
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<% end %>
|
|
197
|
+
|
|
198
|
+
<%# The ruler block. %>
|
|
199
|
+
<% if @criterion %>
|
|
200
|
+
<h2>The conversion criterion<span class="sub">frozen before the first promotion</span></h2>
|
|
201
|
+
<div class="card">
|
|
202
|
+
<div class="skill-desc">
|
|
203
|
+
<span class="pill">metric <%= @criterion.rule.metric %></span>
|
|
204
|
+
<span class="pill">window <%= @criterion.rule.window %></span>
|
|
205
|
+
<span class="pill">not worse than <%= @criterion.rule.threshold %></span>
|
|
206
|
+
<span class="mono muted"><%= @criterion.sha %></span>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="skill-desc muted">
|
|
209
|
+
The ruler is the frozen criterion file, read-only here; the
|
|
210
|
+
store's frozen baseline lives on the <a href="/studio/funnel">Funnel page</a>. A
|
|
211
|
+
store without one can mine, but the gate parks its candidates.
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
<% end %>
|
|
215
|
+
|
|
216
|
+
<% if @runs.any? %>
|
|
217
|
+
<h2>Runs<span class="sub">most recent first</span></h2>
|
|
218
|
+
<div class="card">
|
|
219
|
+
<ul class="history">
|
|
220
|
+
<% @runs.each do |run| %>
|
|
221
|
+
<li>
|
|
222
|
+
<span class="pill <%= run.status == 'failed' ? 'err' : 'ok' %>"><%= run.status %></span>
|
|
223
|
+
<span class="mono"><%= run.id[0, 12] %></span>
|
|
224
|
+
<span class="preview muted">
|
|
225
|
+
<%= run.candidates %> candidate(s) ·
|
|
226
|
+
<% if run.cost && run.cost["spent"].to_i.positive? %><%= run.cost["spent"] %> tokens · <% end %>
|
|
227
|
+
<%= run.started_at %>
|
|
228
|
+
</span>
|
|
229
|
+
</li>
|
|
230
|
+
<% end %>
|
|
231
|
+
</ul>
|
|
232
|
+
</div>
|
|
233
|
+
<% end %>
|
|
234
|
+
<% end %>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
<h1>Overview<span class="sub">at a glance</span></h1>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="head-actions">
|
|
9
|
+
<%== agent_filter_form("/studio/home", @agent) %>
|
|
9
10
|
<span class="pill <%= @active_now.positive? ? "ok online" : "info" %> plain"><%= @active_now %> active now</span>
|
|
10
11
|
<a class="btn primary" href="/studio/playground">Open playground →</a>
|
|
11
12
|
</div>
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
<% @recent.each do |s| %>
|
|
59
60
|
<li>
|
|
60
61
|
<a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
|
|
61
|
-
<span class="avatar sm"><%= s.id[0, 2].upcase %></span>
|
|
62
|
+
<span class="avatar sm <%= avatar_class(s.id) %>"><%= s.id[0, 2].upcase %></span>
|
|
62
63
|
<span class="convo-main">
|
|
63
64
|
<span class="convo-id mono"><%= s.id[0, 20] %></span>
|
|
64
65
|
<span class="convo-sub"><%= Array(s.messages).size %> msg · <%= time_ago(s.updated_at) %></span>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<meta name="csp-nonce" content="<%= csp_nonce %>">
|
|
7
7
|
<title>Insika Studio</title>
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="<%= asset_path("favicon.svg") %>">
|
|
8
9
|
<link rel="stylesheet" href="<%= asset_path("application.css") %>">
|
|
9
10
|
<script type="module" src="<%= asset_path("application.js") %>" defer></script>
|
|
10
11
|
</head>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<header class="page-head">
|
|
2
|
+
<div>
|
|
3
|
+
<nav class="crumbs"><span>operate</span></nav>
|
|
4
|
+
<h1>Parity<span class="sub">the shadow experiment</span></h1>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="head-actions">
|
|
7
|
+
<%== agent_filter_form("/studio/parity", @agent) %>
|
|
8
|
+
</div>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<% if @criterion.nil? %>
|
|
12
|
+
<div class="empty">
|
|
13
|
+
No shadow channel is registered. Set <span class="mono">INSIKA_RELAY_SHADOW=1</span> and
|
|
14
|
+
restart — this page appears only when a channel is actually running in shadow.
|
|
15
|
+
</div>
|
|
16
|
+
<% elsif @pairs.empty? %>
|
|
17
|
+
<div class="empty">
|
|
18
|
+
No pairs yet. A mirrored exchange records a pair the moment our turn ends; the
|
|
19
|
+
incumbent's half lands when the mirror reports its reply
|
|
20
|
+
(<span class="mono">POST /channels/relay/shadow-reply</span>).
|
|
21
|
+
</div>
|
|
22
|
+
<% else %>
|
|
23
|
+
<% r = @report %>
|
|
24
|
+
<div class="card">
|
|
25
|
+
<div class="skill-desc">
|
|
26
|
+
<span class="pill <%= { pass: 'ok', fail: 'err', insufficient: 'warn', invalid: 'err' }.fetch(r.verdict, 'info') %>"><%= r.verdict %></span>
|
|
27
|
+
<span class="muted"><%= r.reason %></span>
|
|
28
|
+
</div>
|
|
29
|
+
<% sha_check = r.checks.find { |c| c.id == :criterion_sha } %>
|
|
30
|
+
<div class="skill-desc">
|
|
31
|
+
<span class="mono"><%= r.criterion_sha %></span>
|
|
32
|
+
<% if sha_check&.met %>
|
|
33
|
+
<span class="pill ok">frozen · matches every pair in the window</span>
|
|
34
|
+
<% else %>
|
|
35
|
+
<span class="pill err">NOT VERIFIED — the window's pairs do not all carry this frozen hash</span>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<h2>Checks<span class="sub">the arithmetic, not the conclusion</span></h2>
|
|
41
|
+
<div class="card">
|
|
42
|
+
<table class="grid">
|
|
43
|
+
<thead><tr><th>check</th><th>required</th><th>actual</th><th></th></tr></thead>
|
|
44
|
+
<tbody>
|
|
45
|
+
<% r.checks.each do |c| %>
|
|
46
|
+
<tr>
|
|
47
|
+
<td class="mono"><%= c.id %></td>
|
|
48
|
+
<td><%= c.required %></td>
|
|
49
|
+
<td><% if c.actual.is_a?(Hash) %><%= c.actual.map { |k, v| "#{k}: #{v}" }.join(" · ") %><% else %><%= c.actual %><% end %></td>
|
|
50
|
+
<td><span class="pill <%= c.met ? 'ok' : 'err' %>"><%= c.met ? "met" : "no" %></span></td>
|
|
51
|
+
</tr>
|
|
52
|
+
<% end %>
|
|
53
|
+
</tbody>
|
|
54
|
+
</table>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<h2>Pairs / day<span class="sub">floor <%= @criterion.rule.pairs_per_day %> · a gap day invalidates the window</span></h2>
|
|
58
|
+
<div class="card">
|
|
59
|
+
<table class="grid">
|
|
60
|
+
<thead><tr><th>date</th><th>pairs</th><th>decided</th><th></th></tr></thead>
|
|
61
|
+
<tbody>
|
|
62
|
+
<% r.daily.each do |d| %>
|
|
63
|
+
<tr>
|
|
64
|
+
<td class="mono"><%= d[:date] %></td>
|
|
65
|
+
<td><%= d[:pairs] %></td>
|
|
66
|
+
<td><%= d[:decided] %></td>
|
|
67
|
+
<td><% if d[:floor].zero? %><span class="muted">partial day — reported, not required</span><% elsif d[:pairs] >= d[:floor] %><span class="pill ok">met</span><% else %><span class="pill err">below floor</span><% end %></td>
|
|
68
|
+
</tr>
|
|
69
|
+
<% end %>
|
|
70
|
+
</tbody>
|
|
71
|
+
</table>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<% if r.counts %>
|
|
75
|
+
<h2>Per store<span class="sub">a store blocks its own cut even when the aggregate passes</span></h2>
|
|
76
|
+
<div class="card">
|
|
77
|
+
<table class="grid">
|
|
78
|
+
<thead><tr><th>store</th><th>decided</th><th>win-or-tie</th><th>worse</th><th>meets floor</th></tr></thead>
|
|
79
|
+
<tbody>
|
|
80
|
+
<% r.per_agent.each do |agent, stats| %>
|
|
81
|
+
<tr>
|
|
82
|
+
<td class="mono"><%= agent %></td>
|
|
83
|
+
<td><%= stats[:decided] %></td>
|
|
84
|
+
<td><%= stats[:win_or_tie] %></td>
|
|
85
|
+
<td><%= stats[:worse] %></td>
|
|
86
|
+
<td><% if stats[:meets].nil? %><span class="muted">below per-store minimum</span><% elsif stats[:meets] %><span class="pill ok">yes</span><% else %><span class="pill err">no</span><% end %></td>
|
|
87
|
+
</tr>
|
|
88
|
+
<% end %>
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<h2>Judge agreement<span class="sub">split / unknown / order-dependent</span></h2>
|
|
94
|
+
<div class="card">
|
|
95
|
+
<div class="skill-desc">
|
|
96
|
+
<span class="pill <%= r.counts[:split].positive? ? 'warn' : 'ok' %>"><%= r.counts[:split] %> split</span>
|
|
97
|
+
<span class="pill <%= r.counts[:unknown].positive? ? 'warn' : 'ok' %>"><%= r.counts[:unknown] %> unknown</span>
|
|
98
|
+
<span class="pill <%= @agreement[:order_dependent].positive? ? 'warn' : 'ok' %>"><%= @agreement[:order_dependent] %> flipped with order</span>
|
|
99
|
+
<span class="muted">of <%= r.counts[:judged] %> judged pairs</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<% end %>
|
|
103
|
+
|
|
104
|
+
<h2>Judge a batch</h2>
|
|
105
|
+
<div class="card">
|
|
106
|
+
<form method="post" action="/studio/parity" class="actions">
|
|
107
|
+
<%== csrf_tag %>
|
|
108
|
+
<label>
|
|
109
|
+
Agent (all stores when blank)
|
|
110
|
+
<input type="text" name="agent" placeholder="agent-id">
|
|
111
|
+
</label>
|
|
112
|
+
<label>
|
|
113
|
+
Limit
|
|
114
|
+
<input type="number" name="limit" value="50" min="1" max="200">
|
|
115
|
+
</label>
|
|
116
|
+
<button type="submit" class="primary">Judge <%= @pending %> complete pair(s)</button>
|
|
117
|
+
</form>
|
|
118
|
+
<div class="skill-desc muted">
|
|
119
|
+
Cost: <span class="mono"><%= @judge_cost %></span> provider calls
|
|
120
|
+
(<%= @pending %> pairs × <%= @criterion.rule.min_judge_models %> judges × 2 orders).
|
|
121
|
+
<% if Array((@judge_models || {})["judges"]).empty? %>
|
|
122
|
+
<span class="pill err">no judges configured in settings['evals'] — the press will refuse</span>
|
|
123
|
+
<% end %>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<h2>Recent pairs<span class="sub">inbound · incumbent · insika</span></h2>
|
|
128
|
+
<div class="card">
|
|
129
|
+
<ul class="history">
|
|
130
|
+
<% @pairs.each do |p| %>
|
|
131
|
+
<li>
|
|
132
|
+
<span class="pill <%= { open: 'warn', complete: 'info', silent: 'info', judged: 'ok', incomplete: 'err' }.fetch(p.status, 'info') %>"><%= p.status %></span>
|
|
133
|
+
<span class="mono"><%= p.id[0, 12] %></span>
|
|
134
|
+
<% if p.agent %><span class="mono"><%= p.agent %></span><% end %>
|
|
135
|
+
<% if p.outcome %><span class="pill"><%= p.outcome %></span><% end %>
|
|
136
|
+
<% if p.session_id %><a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(p.session_id) %>">session →</a><% end %>
|
|
137
|
+
<div class="skill-desc muted"><strong>customer:</strong> <%= p.inbound.to_s %></div>
|
|
138
|
+
<div class="skill-desc"><strong>incumbent:</strong> <%= p.incumbent_reply.to_s %></div>
|
|
139
|
+
<div class="skill-desc"><strong>insika:</strong> <%= p.insika_reply.to_s %></div>
|
|
140
|
+
<% if p.verdict.is_a?(Hash) && presence(p.verdict["reason"]) %>
|
|
141
|
+
<div class="skill-desc muted">why: <%= p.verdict["reason"] %></div>
|
|
142
|
+
<% end %>
|
|
143
|
+
</li>
|
|
144
|
+
<% end %>
|
|
145
|
+
</ul>
|
|
146
|
+
</div>
|
|
147
|
+
<% end %>
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
</select>
|
|
16
16
|
</label>
|
|
17
17
|
<label class="chat-field">session
|
|
18
|
-
<input type="text" name="session_id" value="<%= @session_id %>" placeholder="empty = new conversation"
|
|
18
|
+
<input type="text" name="session_id" value="<%= @session_id %>" placeholder="empty = new conversation"
|
|
19
|
+
list="recent-sessions" autocomplete="off">
|
|
20
|
+
<datalist id="recent-sessions">
|
|
21
|
+
<% Array(@recent).each do |s| %>
|
|
22
|
+
<option value="<%= s.id %>" label="<%= [time_ago(s.updated_at), presence(session_preview(s).to_s[0, 60])].compact.join(" · ") %>">
|
|
23
|
+
<% end %>
|
|
24
|
+
</datalist>
|
|
19
25
|
</label>
|
|
20
26
|
<span class="pill info chat-status" data-live-transcript-target="status">disconnected</span>
|
|
21
27
|
<details class="pin chat-pin">
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
</div>
|
|
40
40
|
<% end %>
|
|
41
41
|
|
|
42
|
-
<%#
|
|
42
|
+
<%# The review IS the product. A gated proposal shows the exact
|
|
43
43
|
edit, what it claims to fix, and how it scored against the accepted baseline,
|
|
44
44
|
because approving is a decision and a decision needs those three things. %>
|
|
45
45
|
<% if @proposal %>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
<span class="mono"><%= @proposal.gate["passed_cases"] %>/<%= @proposal.gate["cases"] %> case(s)</span>
|
|
52
52
|
<span class="muted">no regression against <%= @proposal.gate["baseline_cases"] %> baselined case(s)</span>
|
|
53
53
|
<span class="muted">· proposed by <span class="mono"><%= cand["proposer"] %></span></span>
|
|
54
|
-
<%#
|
|
54
|
+
<%# When a PANEL ran, the operator is looking at the winner of
|
|
55
55
|
several, and which ones it beat is part of the decision. %>
|
|
56
56
|
<% if @proposal.panel.size > 1 %>
|
|
57
57
|
<span class="muted">· best of <%= @proposal.panel.size %> candidate(s)</span>
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
<% else %>
|
|
126
126
|
<h2>Findings<span class="sub"><%= @run.findings.size %> · <%= @run.status %><% if @run.excluded.to_i.positive? %> · <%= @run.excluded %> turn(s) excluded<% end %> · <%= @run.finished_at || @run.started_at %></span></h2>
|
|
127
127
|
|
|
128
|
-
<%#
|
|
128
|
+
<%# The model writes the candidate, the gate scores it. Offered on
|
|
129
129
|
a finished report and only when nothing is already awaiting an answer: a second
|
|
130
130
|
proposal while one is pending would ask the operator to review two edits to the
|
|
131
131
|
same file that were never scored together. %>
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
</div>
|
|
148
148
|
<% end %>
|
|
149
149
|
|
|
150
|
-
<%#
|
|
150
|
+
<%# `mode: auto_apply` changed this agent's files with nobody in
|
|
151
151
|
the loop, so the page owes the operator exactly three things: what changed, why,
|
|
152
152
|
and where to undo it. The write versioned, so History IS the undo. %>
|
|
153
153
|
<% if @run.status == :applied && (@run.decision || {})["by"] == "auto_apply" %>
|
|
@@ -12,6 +12,82 @@
|
|
|
12
12
|
<a class="btn" href="/studio/playground?session_id=<%= Rack::Utils.escape(@session.id) %>">Continue in playground →</a>
|
|
13
13
|
</header>
|
|
14
14
|
|
|
15
|
+
<% briefing = @session.respond_to?(:briefing) ? (@session.briefing || {}) : {} %>
|
|
16
|
+
<% fields = briefing["fields"] || {} %>
|
|
17
|
+
<% next_step = briefing["next_step"] %>
|
|
18
|
+
<% if fields.any? || next_step %>
|
|
19
|
+
<div class="card">
|
|
20
|
+
<div class="trace-head">
|
|
21
|
+
<h2>Briefing</h2>
|
|
22
|
+
<span class="muted">this conversation's working state — read-only, written by the agent</span>
|
|
23
|
+
</div>
|
|
24
|
+
<% fields.sort.each do |name, value| %>
|
|
25
|
+
<div class="kv"><span><%= name %></span><div><%= value %></div></div>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% if next_step %>
|
|
28
|
+
<div class="kv"><span>next step</span><div><%= next_step %></div></div>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
|
|
33
|
+
<% ctx_traces = @context_traces || [] %>
|
|
34
|
+
<% unless ctx_traces.empty? %>
|
|
35
|
+
<div class="card tooltrace">
|
|
36
|
+
<div class="trace-head">
|
|
37
|
+
<h2>Context</h2>
|
|
38
|
+
<span class="muted">tokens by category per turn (estimated) — no content stored</span>
|
|
39
|
+
</div>
|
|
40
|
+
<% ctx_traces.each do |c| %>
|
|
41
|
+
<div class="trace-turn">
|
|
42
|
+
<div class="turn-label">turn <%= c["turn"] %> · <%= c["at"] %></div>
|
|
43
|
+
<%# Skills injected by context (triggers/skills_eager) leave NO tool call in
|
|
44
|
+
the transcript, so this card is the only record that they were active.
|
|
45
|
+
The names live in the body, but the count belongs in the SUMMARY: the
|
|
46
|
+
card is collapsed, and "was the skill on?" is the question an operator
|
|
47
|
+
asks on every single turn. %>
|
|
48
|
+
<% skills = Array((c["categories"] || {}).dig("skilltrigger", "labels")).uniq %>
|
|
49
|
+
<details class="trace-item ok">
|
|
50
|
+
<summary>
|
|
51
|
+
<span class="badge"><%= c["used"] %> / <%= c["cap"] %></span>
|
|
52
|
+
<strong>context</strong>
|
|
53
|
+
<%# the prefix cache-hit of the turn + the first block
|
|
54
|
+
whose bytes changed vs the previous turn (PII-free category id). %>
|
|
55
|
+
<% cache = c["cache"] %>
|
|
56
|
+
<% if cache && cache["hit_pct"] %><span class="badge"><%= cache["hit_pct"] %>% cached</span><% end %>
|
|
57
|
+
<% if cache && cache["invalidation_reason"] %>
|
|
58
|
+
<span class="lat">broke: <%= cache["invalidation_reason"] %></span>
|
|
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>
|
|
77
|
+
<% end %>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<% 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
|
+
|
|
15
91
|
<% traces = @tool_traces || {} %>
|
|
16
92
|
<% unless traces.empty? %>
|
|
17
93
|
<div class="card tooltrace">
|
|
@@ -49,14 +125,68 @@
|
|
|
49
125
|
</div>
|
|
50
126
|
<% end %>
|
|
51
127
|
|
|
128
|
+
<%# the session's stage history — outcomes folded to the agent's
|
|
129
|
+
declared stages (unmapped kinds render the raw outcome: the hole is visible).
|
|
130
|
+
Without a declaration the block renders the raw outcomes. %>
|
|
131
|
+
<% history = @stage_history || {} %>
|
|
132
|
+
<% unless history.empty? %>
|
|
133
|
+
<div class="card">
|
|
134
|
+
<div class="trace-head">
|
|
135
|
+
<h2>Stage history</h2>
|
|
136
|
+
<span class="muted">
|
|
137
|
+
<% if history[:declaration] %>
|
|
138
|
+
<%= history[:declaration].stages.join(" → ") %> · outcome funnel
|
|
139
|
+
<% else %>
|
|
140
|
+
no funnel declared — raw outcomes
|
|
141
|
+
<% end %>
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
<div class="kv">
|
|
145
|
+
<% history[:rows].each do |row| %>
|
|
146
|
+
<% if row[:stage] %>
|
|
147
|
+
<span><%= row[:outcome] %></span>
|
|
148
|
+
<div>→ <strong><%= row[:stage] %></strong> <span class="lat"><%= row[:at] %></span></div>
|
|
149
|
+
<% else %>
|
|
150
|
+
<span><%= row[:outcome] %></span>
|
|
151
|
+
<div><span class="lat">not a declared stage</span> <span class="lat"><%= row[:at] %></span></div>
|
|
152
|
+
<% end %>
|
|
153
|
+
<% end %>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
<% end %>
|
|
157
|
+
|
|
52
158
|
<div class="card transcript">
|
|
53
159
|
<% if Array(@session.messages).empty? %>
|
|
54
160
|
<div class="empty">This session has no messages.</div>
|
|
55
161
|
<% else %>
|
|
56
162
|
<div class="thread">
|
|
57
|
-
<%#
|
|
58
|
-
|
|
59
|
-
|
|
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 %>
|
|
60
190
|
</div>
|
|
61
191
|
<% end %>
|
|
62
192
|
</div>
|