insika 0.8.0 → 1.0.1
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 +312 -2
- data/README.md +10 -6
- data/bin/insika +4 -1
- data/bin/insika-router +0 -1
- data/docs/AGENTS.md +142 -18
- data/docs/API.md +73 -0
- data/docs/ARCHITECTURE.md +56 -50
- data/docs/ARTIFACTS.md +33 -3
- data/docs/BENCHMARK.md +50 -7
- data/docs/CHANNELS.md +19 -2
- data/docs/CONTEXT.md +96 -27
- data/docs/CROSS-HARNESS-BENCH.md +157 -0
- data/docs/DEPLOY.md +70 -13
- data/docs/EMBEDDING.md +10 -0
- data/docs/EVALS.md +129 -10
- data/docs/FACTS.md +12 -6
- data/docs/HARVEST.md +8 -3
- data/docs/KNOWLEDGE.md +14 -0
- data/docs/MEDIA.md +6 -2
- data/docs/OBSERVABILITY.md +164 -28
- data/docs/PERFORMANCE.md +110 -0
- data/docs/PLUGINS.md +4 -4
- data/docs/POLICY.md +26 -14
- data/docs/RELEASING.md +9 -0
- data/docs/ROUTER.md +7 -0
- data/docs/RUBYLLM_2_MIGRATION.md +307 -0
- data/docs/SECURITY.md +34 -1
- data/docs/SHARED-CONVERSATIONS.md +40 -0
- data/docs/SKILLS.md +5 -3
- data/docs/STUDIO.md +122 -0
- data/docs/TOOLS.md +238 -42
- data/docs/operate.md +3 -0
- data/docs/prompts/ADD-TOOL.md +12 -2
- data/docs/prompts/DIAGNOSE-TURN.md +3 -0
- data/docs/prompts/GO-LIVE.md +3 -1
- data/lib/insika/agent_defaults.rb +140 -0
- data/lib/insika/agent_file_store.rb +12 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +11 -0
- data/lib/insika/channels/web/widget.js +33 -0
- data/lib/insika/channels/web.rb +5 -2
- data/lib/insika/chat_builder.rb +124 -28
- data/lib/insika/checkpoint.rb +6 -1
- data/lib/insika/checkpoint_store.rb +59 -7
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/delete_kit.rb +26 -0
- data/lib/insika/commands/gate_harvest.rb +3 -0
- data/lib/insika/commands/review_guardrail_flag.rb +38 -0
- data/lib/insika/commands/run_distillation.rb +5 -8
- data/lib/insika/commands/run_harvest.rb +17 -1
- data/lib/insika/commands/seed_session.rb +128 -0
- data/lib/insika/commands/send_message.rb +9 -3
- data/lib/insika/commands/update_settings.rb +23 -1
- data/lib/insika/commands/write_kit.rb +27 -0
- data/lib/insika/compaction.rb +33 -7
- data/lib/insika/config_store.rb +87 -7
- data/lib/insika/context/builder.rb +29 -9
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +7 -2
- data/lib/insika/context/providers/briefing.rb +11 -8
- data/lib/insika/context/providers/fence_notice.rb +27 -0
- data/lib/insika/context/providers/knowledge.rb +32 -6
- data/lib/insika/context/providers/memory.rb +61 -8
- data/lib/insika/context/providers/pending_confirmation.rb +52 -0
- data/lib/insika/context/providers/prompt.rb +17 -4
- data/lib/insika/context/providers/session.rb +46 -6
- data/lib/insika/context/reranker.rb +75 -0
- data/lib/insika/distill.rb +22 -13
- data/lib/insika/doctor.rb +142 -4
- data/lib/insika/dsl/runtime.rb +37 -4
- data/lib/insika/dsl/server_boot.rb +4 -12
- data/lib/insika/dsl.rb +19 -5
- data/lib/insika/edge_limiter.rb +27 -16
- data/lib/insika/egress_guard.rb +11 -3
- data/lib/insika/env_schema.rb +33 -73
- data/lib/insika/errors.rb +1 -0
- data/lib/insika/evals/assertions.rb +259 -18
- data/lib/insika/evals/golden.rb +230 -4
- data/lib/insika/evals/judge.rb +19 -10
- data/lib/insika/evals/moderator_compare.rb +47 -0
- data/lib/insika/evals/report.rb +61 -9
- data/lib/insika/evals/runner.rb +64 -4
- data/lib/insika/evals/simulator.rb +21 -3
- data/lib/insika/evals/transport.rb +317 -15
- data/lib/insika/event_stream.rb +63 -7
- data/lib/insika/evidence.rb +105 -17
- data/lib/insika/executor.rb +698 -164
- data/lib/insika/fence.rb +96 -0
- data/lib/insika/golden_store.rb +3 -0
- data/lib/insika/harvest.rb +26 -14
- data/lib/insika/harvest_engine.rb +1 -0
- data/lib/insika/harvest_store.rb +12 -0
- data/lib/insika/http_client.rb +87 -4
- data/lib/insika/kits.rb +54 -0
- data/lib/insika/knowledge.rb +34 -20
- data/lib/insika/llm_configurator.rb +46 -4
- data/lib/insika/llm_provider_store.rb +1 -3
- data/lib/insika/llm_trace_store.rb +54 -0
- data/lib/insika/mcp_client.rb +27 -63
- data/lib/insika/mcp_live_tool.rb +55 -9
- data/lib/insika/mcp_store.rb +37 -3
- data/lib/insika/mcp_tool_registry.rb +76 -26
- data/lib/insika/media.rb +38 -25
- data/lib/insika/memory_store.rb +16 -3
- data/lib/insika/memory_trim.rb +60 -0
- data/lib/insika/middleware.rb +12 -3
- data/lib/insika/model_metrics_store.rb +285 -0
- data/lib/insika/model_registry.rb +110 -0
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +14 -21
- data/lib/insika/model_visible.rb +17 -6
- data/lib/insika/onboarding.rb +1 -0
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +30 -2
- data/lib/insika/pack_importer.rb +54 -9
- data/lib/insika/packaging.rb +14 -5
- data/lib/insika/pending_action_store.rb +45 -18
- data/lib/insika/plugin/loader.rb +4 -2
- data/lib/insika/prefix_fingerprint.rb +32 -27
- data/lib/insika/profile_source.rb +5 -0
- data/lib/insika/provider_error_classifier.rb +18 -3
- data/lib/insika/queue_policy.rb +7 -2
- data/lib/insika/refinement/proposer.rb +42 -17
- data/lib/insika/reliability.rb +12 -3
- data/lib/insika/router/app.rb +14 -5
- data/lib/insika/router/session_key.rb +35 -5
- data/lib/insika/safety/config.rb +20 -3
- data/lib/insika/safety/corpus.rb +10 -3
- data/lib/insika/safety/factory.rb +39 -1
- data/lib/insika/safety/input_guardrail.rb +21 -1
- data/lib/insika/safety/judge_moderator.rb +70 -0
- data/lib/insika/safety/output_validator.rb +29 -5
- data/lib/insika/safety/prompt_echo.rb +89 -0
- data/lib/insika/schema_guard.rb +53 -76
- data/lib/insika/server/app.rb +58 -7
- data/lib/insika/server/rack_app.rb +2 -0
- data/lib/insika/server/responses.rb +50 -5
- data/lib/insika/session_actor.rb +58 -28
- data/lib/insika/session_store.rb +163 -11
- data/lib/insika/settings_store.rb +57 -2
- data/lib/insika/shared_conversations.rb +305 -0
- data/lib/insika/skill_catalog.rb +28 -2
- data/lib/insika/skill_store.rb +6 -0
- data/lib/insika/soak/runner.rb +1 -1
- data/lib/insika/spoken_transcript.rb +31 -0
- data/lib/insika/steer_injector.rb +7 -48
- data/lib/insika/store.rb +29 -1
- data/lib/insika/stores/json_value.rb +37 -0
- data/lib/insika/stores/memory.rb +22 -35
- data/lib/insika/stores/postgres.rb +295 -0
- data/lib/insika/stores/sqlite.rb +257 -43
- data/lib/insika/stores/turn_counter.rb +25 -0
- data/lib/insika/studio/app.rb +1459 -163
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +25 -25
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- data/lib/insika/studio/forms.rb +189 -12
- data/lib/insika/studio/nav_icons.rb +3 -0
- data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
- data/lib/insika/studio/views/_agent_picker.erb +15 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
- data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
- data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
- data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
- data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
- data/lib/insika/studio/views/_agents_master.erb +8 -12
- data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
- data/lib/insika/studio/views/_cfg_budget.erb +20 -0
- data/lib/insika/studio/views/_cfg_distill.erb +26 -0
- data/lib/insika/studio/views/_cfg_followup.erb +30 -0
- data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
- data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
- data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
- data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
- data/lib/insika/studio/views/_chats_master.erb +66 -0
- data/lib/insika/studio/views/_customers_master.erb +37 -0
- data/lib/insika/studio/views/_empty.erb +15 -0
- data/lib/insika/studio/views/_facts_master.erb +42 -0
- data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
- data/lib/insika/studio/views/_identity.erb +16 -0
- data/lib/insika/studio/views/_kpi.erb +17 -0
- data/lib/insika/studio/views/_message.erb +40 -2
- data/lib/insika/studio/views/_model_chart.erb +82 -0
- data/lib/insika/studio/views/_refinement_master.erb +38 -0
- data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
- data/lib/insika/studio/views/_tool_users.erb +17 -0
- data/lib/insika/studio/views/_why.erb +53 -0
- data/lib/insika/studio/views/agent_detail.erb +17 -10
- data/lib/insika/studio/views/agents.erb +28 -16
- data/lib/insika/studio/views/approvals.erb +16 -11
- data/lib/insika/studio/views/artifact.erb +20 -7
- data/lib/insika/studio/views/artifacts.erb +19 -14
- data/lib/insika/studio/views/chats.erb +22 -29
- data/lib/insika/studio/views/customer.erb +88 -43
- data/lib/insika/studio/views/customers.erb +16 -26
- data/lib/insika/studio/views/evals.erb +49 -31
- data/lib/insika/studio/views/fact_detail.erb +114 -0
- data/lib/insika/studio/views/facts.erb +15 -116
- data/lib/insika/studio/views/followups.erb +63 -52
- data/lib/insika/studio/views/funnel.erb +104 -43
- data/lib/insika/studio/views/harvest.erb +85 -59
- data/lib/insika/studio/views/home.erb +138 -23
- data/lib/insika/studio/views/inbox.erb +36 -0
- data/lib/insika/studio/views/kits.erb +176 -0
- data/lib/insika/studio/views/knowledge.erb +92 -80
- data/lib/insika/studio/views/layout.erb +42 -11
- data/lib/insika/studio/views/live.erb +45 -0
- data/lib/insika/studio/views/mcp.erb +43 -13
- data/lib/insika/studio/views/models.erb +187 -0
- data/lib/insika/studio/views/parity.erb +38 -32
- data/lib/insika/studio/views/playground.erb +13 -9
- data/lib/insika/studio/views/refinement.erb +15 -224
- data/lib/insika/studio/views/refinement_detail.erb +243 -0
- data/lib/insika/studio/views/session.erb +153 -118
- data/lib/insika/studio/views/settings.erb +86 -22
- data/lib/insika/studio/views/skills.erb +53 -19
- data/lib/insika/studio/views/system_files.erb +7 -7
- data/lib/insika/studio/views/task.erb +45 -15
- data/lib/insika/studio/views/tasks.erb +35 -18
- data/lib/insika/studio/views/tools.erb +62 -73
- data/lib/insika/task_actor.rb +4 -1
- data/lib/insika/task_store.rb +67 -1
- data/lib/insika/telemetry/pricing.rb +16 -14
- data/lib/insika/telemetry/recorder.rb +115 -9
- data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
- data/lib/insika/telemetry.rb +12 -2
- data/lib/insika/templates/daily-digest/README.md +13 -0
- data/lib/insika/templates/daily-digest/agent.rb +22 -11
- data/lib/insika/testing/store_contract.rb +65 -0
- data/lib/insika/text_tool_call.rb +66 -0
- data/lib/insika/tick.rb +7 -1
- data/lib/insika/token_store.rb +5 -0
- data/lib/insika/tool_assembly.rb +21 -13
- data/lib/insika/tool_batch.rb +1 -1
- data/lib/insika/tool_catalog.rb +12 -2
- data/lib/insika/tool_definition.rb +82 -13
- data/lib/insika/tool_envelope.rb +266 -29
- data/lib/insika/tool_manifest.rb +1 -0
- data/lib/insika/tool_store.rb +12 -4
- data/lib/insika/tool_trace_store.rb +15 -8
- data/lib/insika/tool_usage_report.rb +14 -4
- data/lib/insika/tools/a2a_remote.rb +1 -1
- data/lib/insika/tools/agent_enum.rb +1 -1
- data/lib/insika/tools/confirm_pending.rb +112 -0
- data/lib/insika/tools/data_defined_tool.rb +24 -15
- data/lib/insika/tools/generate_image.rb +2 -2
- data/lib/insika/tools/load_knowledge.rb +7 -3
- data/lib/insika/tools/load_skill.rb +3 -3
- data/lib/insika/tools/present.rb +115 -0
- data/lib/insika/tools/remember.rb +7 -2
- data/lib/insika/tools/run_persona_eval.rb +9 -6
- data/lib/insika/tools/save_artifact.rb +16 -6
- data/lib/insika/tools/schedule_followup.rb +3 -3
- data/lib/insika/tools/stuck_signal.rb +3 -3
- data/lib/insika/tools/subagent.rb +5 -5
- data/lib/insika/tools/subagents.rb +3 -3
- data/lib/insika/tools/tool_search.rb +11 -7
- data/lib/insika/tools/tts.rb +2 -2
- data/lib/insika/tools/update_briefing.rb +4 -4
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +31 -1
- data/lib/insika/turn_timing.rb +51 -14
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +88 -16
- data/lib/insika/wiring/graph_chat.rb +4 -0
- data/lib/insika/workflow.rb +25 -64
- data/lib/insika.rb +23 -3
- metadata +74 -13
- data/lib/insika/commands/import_mcp_tools.rb +0 -48
- data/lib/insika/mcp_http_client.rb +0 -67
- data/lib/insika/mcp_tool_ingestor.rb +0 -165
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
<%# Refinement run detail — the DETAIL pane of the Refinement miller shell.
|
|
2
|
+
Full page (direct visit/refresh): the two-column shell — master run list
|
|
3
|
+
plus this pane. Turbo-Frame request (a row click, a Run/propose/resolve
|
|
4
|
+
POST's redirect): the pane alone, so the master's scroll state and
|
|
5
|
+
selection survive every interaction. @selected is a
|
|
6
|
+
Insika::RefinementStore::Run — its `status` picks what shows below the
|
|
7
|
+
findings: a gated proposal awaiting approval, what auto_apply already
|
|
8
|
+
changed, or why the gate refused. The review IS the product: a gated
|
|
9
|
+
proposal shows the exact edit, what it claims to fix, and how it scored
|
|
10
|
+
against the accepted baseline, because approving is a decision and a
|
|
11
|
+
decision needs those three things. %>
|
|
12
|
+
<%# Roda renders with plain locals — no local_assigns hash (Rails-ism). %>
|
|
13
|
+
<% frame_only = defined?(frame_only) && frame_only %>
|
|
14
|
+
<% unless frame_only %>
|
|
15
|
+
<header class="page-head">
|
|
16
|
+
<div>
|
|
17
|
+
<nav class="crumbs"><a href="/studio/refinement">refinement</a></nav>
|
|
18
|
+
<h1>Refinement<span class="sub"><%= @agent || "no agent" %></span></h1>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="head-actions">
|
|
21
|
+
<%== agent_filter_form("/studio/refinement", @agent) %>
|
|
22
|
+
</div>
|
|
23
|
+
</header>
|
|
24
|
+
|
|
25
|
+
<div class="drill" data-controller="list-filter">
|
|
26
|
+
<%== render("_refinement_master", locals: { active_id: @selected&.id }) %>
|
|
27
|
+
<% end %>
|
|
28
|
+
<turbo-frame id="refinement-detail" class="drill-pane drill-detail">
|
|
29
|
+
<%# A frame response never sees the layout, so the flash strip lives here
|
|
30
|
+
too — without it, run/propose/resolve confirmations would vanish on
|
|
31
|
+
frame submits (the layout's copy only renders on full pages). %>
|
|
32
|
+
<% if request.env["HTTP_TURBO_FRAME"] == "refinement-detail" %>
|
|
33
|
+
<% if flash["error"] %><div class="flash err" role="alert"><%= flash["error"] %></div><% end %>
|
|
34
|
+
<% if flash["notice"] %><div class="flash ok" role="status"><%= flash["notice"] %></div><% end %>
|
|
35
|
+
<% end %>
|
|
36
|
+
<div class="drill-pane-head">
|
|
37
|
+
<div class="title">
|
|
38
|
+
<% if @selected %>
|
|
39
|
+
<strong>Conversation review</strong>
|
|
40
|
+
<span class="status <%= status_class(@selected.status) %>"><%= @selected.status %></span>
|
|
41
|
+
<span class="count">
|
|
42
|
+
<%= @selected.findings.size %> finding(s) · <%= refinement_window(@selected.window) %> ·
|
|
43
|
+
<%= refinement_elapsed(@selected) %>
|
|
44
|
+
</span>
|
|
45
|
+
<% else %>
|
|
46
|
+
<strong>No run yet</strong>
|
|
47
|
+
<span class="count">for <span class="mono"><%= @agent %></span></span>
|
|
48
|
+
<% end %>
|
|
49
|
+
</div>
|
|
50
|
+
<% if @agent %>
|
|
51
|
+
<form method="post" action="/studio/refinement" class="actions inline" data-turbo-frame="_top">
|
|
52
|
+
<%== csrf_tag %>
|
|
53
|
+
<input type="hidden" name="agent" value="<%= @agent %>">
|
|
54
|
+
<label class="check" title="Ignore the previous run and read the full window instead of only what happened since.">
|
|
55
|
+
<input type="checkbox" name="full" value="1"> full window
|
|
56
|
+
</label>
|
|
57
|
+
<button type="submit" class="btn primary btn-sm" data-turbo-submits-with="Running…">Run</button>
|
|
58
|
+
</form>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="drill-pane-body refinement-review">
|
|
62
|
+
<% if @agent.nil? %>
|
|
63
|
+
<div class="empty">No agents yet. Create one first — a refinement run reads an agent's own traffic.</div>
|
|
64
|
+
<% elsif @selected.nil? %>
|
|
65
|
+
<div class="empty">No run yet for <span class="mono"><%= @agent %></span>. Press Run to read the last conversations.</div>
|
|
66
|
+
<% else %>
|
|
67
|
+
<%# The review IS the product. A gated proposal shows the exact
|
|
68
|
+
edit, what it claims to fix, and how it scored against the accepted baseline,
|
|
69
|
+
because approving is a decision and a decision needs those three things. %>
|
|
70
|
+
<% if @selected.status == :awaiting_approval %>
|
|
71
|
+
<div class="section">
|
|
72
|
+
<div class="section-head">
|
|
73
|
+
<h2>Proposal<span class="sub">gated · awaiting your approval</span></h2>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="card">
|
|
76
|
+
<% cand = @selected.candidate || {} %>
|
|
77
|
+
<div class="skill-desc">
|
|
78
|
+
<span class="pill ok">gate passed</span>
|
|
79
|
+
<span class="mono"><%= @selected.gate["passed_cases"] %>/<%= @selected.gate["cases"] %> case(s)</span>
|
|
80
|
+
<span class="muted">no regression against <%= @selected.gate["baseline_cases"] %> baselined case(s)</span>
|
|
81
|
+
<span class="muted">· proposed by <span class="mono"><%= cand["proposer"] %></span></span>
|
|
82
|
+
<%# When a PANEL ran, the operator is looking at the winner of
|
|
83
|
+
several, and which ones it beat is part of the decision. %>
|
|
84
|
+
<% if @selected.panel.size > 1 %>
|
|
85
|
+
<span class="muted">· best of <%= @selected.panel.size %> candidate(s)</span>
|
|
86
|
+
<% end %>
|
|
87
|
+
<% if @selected.cost && @selected.cost["spent"].to_i.positive? %>
|
|
88
|
+
<span class="muted">· <%= @selected.cost["spent"] %> tokens<% if @selected.cost["cached"].to_i.positive? %>
|
|
89
|
+
(<%= @selected.cost["cached"] %> cached)<% end %></span>
|
|
90
|
+
<% end %>
|
|
91
|
+
</div>
|
|
92
|
+
<% if presence(cand["rationale"]) %>
|
|
93
|
+
<div class="skill-desc muted"><%= cand["rationale"] %></div>
|
|
94
|
+
<% end %>
|
|
95
|
+
|
|
96
|
+
<% Array(cand["edits"]).each do |e| %>
|
|
97
|
+
<div class="skill-desc">
|
|
98
|
+
<span class="pill"><%= e["op"] %></span>
|
|
99
|
+
<span class="mono"><%= e["file"] %></span>
|
|
100
|
+
<% if presence(e["anchor"]) %><span class="muted"><%= e["anchor"] %></span><% end %>
|
|
101
|
+
<% if Array(e["addresses"]).any? %>
|
|
102
|
+
<span class="muted">· addresses <%= Array(e["addresses"]).join(", ") %></span>
|
|
103
|
+
<% end %>
|
|
104
|
+
</div>
|
|
105
|
+
<pre class="diff"><% if e["op"] == "replace" %><span class="del">- <%= e["before"] %></span>
|
|
106
|
+
<% end %><span class="ins">+ <%= e["after"] %></span></pre>
|
|
107
|
+
<% end %>
|
|
108
|
+
|
|
109
|
+
<form method="post" action="/studio/refinement/resolve" class="actions" data-turbo-frame="_top">
|
|
110
|
+
<%== csrf_tag %>
|
|
111
|
+
<input type="hidden" name="agent" value="<%= @agent %>">
|
|
112
|
+
<input type="hidden" name="run_id" value="<%= @selected.id %>">
|
|
113
|
+
<label>
|
|
114
|
+
Note
|
|
115
|
+
<input type="text" name="note" placeholder="why (optional)">
|
|
116
|
+
</label>
|
|
117
|
+
<button type="submit" class="primary" name="decision" value="approved" data-turbo-submits-with="Applying…">Approve & apply</button>
|
|
118
|
+
<button type="submit" class="danger" name="decision" value="rejected" data-turbo-submits-with="Rejecting…">Reject</button>
|
|
119
|
+
</form>
|
|
120
|
+
<div class="skill-desc muted">
|
|
121
|
+
Approving writes the edits to the agent's files and versions the previous
|
|
122
|
+
content — undo it any time from the file's History.
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<%# The candidates that LOST. Shown because the panel's value is comparative: an
|
|
127
|
+
operator deciding whether the loop earns its keep needs to see that three
|
|
128
|
+
models were asked and what the other two wrote. %>
|
|
129
|
+
<% losers = @selected.panel.reject { |e| (e["candidate"] || {})["id"] == cand["id"] } %>
|
|
130
|
+
<% if losers.any? %>
|
|
131
|
+
<details class="card review-details">
|
|
132
|
+
<summary>Also proposed <span class="muted">· scored and not chosen</span></summary>
|
|
133
|
+
<ul class="history">
|
|
134
|
+
<% losers.each do |entry| %>
|
|
135
|
+
<% other = entry["candidate"] || {}; verdict = entry["gate"] || {} %>
|
|
136
|
+
<li>
|
|
137
|
+
<span class="pill <%= verdict["passed"] ? 'ok' : 'err' %>"><%= verdict["passed"] ? "passed" : "no" %></span>
|
|
138
|
+
<span class="mono"><%= Array(entry["proposers"]).join(" + ") %></span>
|
|
139
|
+
<span class="preview muted">
|
|
140
|
+
<%= Array(other["edits"]).size %> edit(s)
|
|
141
|
+
<% if verdict["cases"].to_i.positive? %>· <%= verdict["passed_cases"] %>/<%= verdict["cases"] %> case(s)<% end %>
|
|
142
|
+
<% if presence(verdict["reason"]) %>· <%= verdict["reason"] %><% end %>
|
|
143
|
+
</span>
|
|
144
|
+
</li>
|
|
145
|
+
<% end %>
|
|
146
|
+
</ul>
|
|
147
|
+
</details>
|
|
148
|
+
<% end %>
|
|
149
|
+
</div>
|
|
150
|
+
<% end %>
|
|
151
|
+
|
|
152
|
+
<%# `mode: auto_apply` changed this agent's files with nobody in
|
|
153
|
+
the loop, so the page owes the operator exactly three things: what changed, why,
|
|
154
|
+
and where to undo it. The write versioned, so History IS the undo. %>
|
|
155
|
+
<% if @selected.status == :applied && (@selected.decision || {})["by"] == "auto_apply" %>
|
|
156
|
+
<div class="card">
|
|
157
|
+
<div class="skill-desc">
|
|
158
|
+
<span class="pill ok">auto-applied</span>
|
|
159
|
+
<span class="muted"><%= (@selected.decision || {})["note"] %> · <%= (@selected.decision || {})["at"] %></span>
|
|
160
|
+
</div>
|
|
161
|
+
<% Array((@selected.candidate || {})["edits"]).each do |e| %>
|
|
162
|
+
<div class="skill-desc">
|
|
163
|
+
<span class="pill"><%= e["op"] %></span>
|
|
164
|
+
<a class="mono" href="/studio/agents/<%= Rack::Utils.escape(@agent) %>?file=<%= Rack::Utils.escape(e["file"]) %>"><%= e["file"] %></a>
|
|
165
|
+
</div>
|
|
166
|
+
<pre class="diff"><% if e["op"] == "replace" %><span class="del">- <%= e["before"] %></span>
|
|
167
|
+
<% end %><span class="ins">+ <%= e["after"] %></span></pre>
|
|
168
|
+
<% end %>
|
|
169
|
+
<div class="skill-desc muted">
|
|
170
|
+
Undo it from the file's History — the previous content is one click away.
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<% end %>
|
|
174
|
+
|
|
175
|
+
<%# A gate that refused is the most useful thing on this page and the easiest to
|
|
176
|
+
lose: without it, pressing Propose and getting nothing back reads as a bug. %>
|
|
177
|
+
<% if @selected.status == :rejected && @selected.gate.is_a?(Hash) && !@selected.gate_passed? %>
|
|
178
|
+
<div class="card">
|
|
179
|
+
<div class="skill-desc">
|
|
180
|
+
<span class="pill err">gate refused</span>
|
|
181
|
+
<span class="mono"><%= @selected.gate["reason"] %></span>
|
|
182
|
+
</div>
|
|
183
|
+
<% Array((@selected.candidate || {})["dropped"]).each do |d| %>
|
|
184
|
+
<div class="skill-desc muted">dropped <span class="mono"><%= d["file"] %></span> — <%= d["reason"] %></div>
|
|
185
|
+
<% end %>
|
|
186
|
+
</div>
|
|
187
|
+
<% end %>
|
|
188
|
+
|
|
189
|
+
<h2>Findings<span class="sub"><%= @selected.findings.size %> · <%= @selected.status %><% if @selected.excluded.to_i.positive? %> · <%= @selected.excluded %> turn(s) excluded<% end %> · <%= @selected.finished_at || @selected.started_at %></span></h2>
|
|
190
|
+
|
|
191
|
+
<%# The model writes the candidate, the gate scores it. Offered on
|
|
192
|
+
a finished report and only when nothing is already awaiting an answer: a second
|
|
193
|
+
proposal while one is pending would ask the operator to review two edits to the
|
|
194
|
+
same file that were never scored together. %>
|
|
195
|
+
<% if @selected.status == :completed && @selected.findings.any? %>
|
|
196
|
+
<div class="card">
|
|
197
|
+
<form method="post" action="/studio/refinement/propose" class="actions" data-turbo-frame="_top">
|
|
198
|
+
<%== csrf_tag %>
|
|
199
|
+
<input type="hidden" name="agent" value="<%= @agent %>">
|
|
200
|
+
<input type="hidden" name="run_id" value="<%= @selected.id %>">
|
|
201
|
+
<button type="submit" class="primary" data-turbo-submits-with="Evaluating…">Propose a fix</button>
|
|
202
|
+
</form>
|
|
203
|
+
<div class="skill-desc muted">
|
|
204
|
+
Asks this agent's configured proposer(s) for a small, anchored edit to its
|
|
205
|
+
allowlisted files, then scores each one by replaying the golden set against a
|
|
206
|
+
throwaway clone and keeps the best. Nothing is written until you approve it.
|
|
207
|
+
Takes a while and costs provider calls — the replay is a real conversation per
|
|
208
|
+
case, per candidate.
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
<% end %>
|
|
212
|
+
|
|
213
|
+
<% if @selected.status == :failed %>
|
|
214
|
+
<div class="empty">This run failed: <span class="mono"><%= @selected.error %></span></div>
|
|
215
|
+
<% elsif @selected.findings.empty? %>
|
|
216
|
+
<div class="empty">Nothing found in this window. Either traffic was clean, or the window was empty.</div>
|
|
217
|
+
<% else %>
|
|
218
|
+
<div class="card">
|
|
219
|
+
<ul class="history">
|
|
220
|
+
<% @selected.findings.each do |f| %>
|
|
221
|
+
<li>
|
|
222
|
+
<span class="pill"><%= f["kind"] %></span>
|
|
223
|
+
<span class="mono">×<%= f["count"] %></span>
|
|
224
|
+
<span class="preview"><%= f["title"] %></span>
|
|
225
|
+
<% if f["detail"] %><div class="skill-desc muted"><%= f["detail"] %></div><% end %>
|
|
226
|
+
<% if Array(f["sessions"]).any? %>
|
|
227
|
+
<div class="skill-desc muted">
|
|
228
|
+
<% Array(f["sessions"]).each do |sid| %>
|
|
229
|
+
<a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid[0, 12] %></a>
|
|
230
|
+
<% end %>
|
|
231
|
+
</div>
|
|
232
|
+
<% end %>
|
|
233
|
+
</li>
|
|
234
|
+
<% end %>
|
|
235
|
+
</ul>
|
|
236
|
+
</div>
|
|
237
|
+
<% end %>
|
|
238
|
+
<% end %>
|
|
239
|
+
</div>
|
|
240
|
+
</turbo-frame>
|
|
241
|
+
<% unless frame_only %>
|
|
242
|
+
</div>
|
|
243
|
+
<% end %>
|
|
@@ -8,95 +8,120 @@
|
|
|
8
8
|
<%# Roda renders with plain locals — no local_assigns hash (Rails-ism). %>
|
|
9
9
|
<% frame_only = defined?(frame_only) && frame_only %>
|
|
10
10
|
<% unless frame_only %>
|
|
11
|
-
<header class="page-head">
|
|
11
|
+
<header class="page-head operation-head">
|
|
12
12
|
<div>
|
|
13
|
-
<nav class="crumbs"><a href="/studio/chats">chats</a
|
|
14
|
-
<h1>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</span>
|
|
19
|
-
<span class="sub"><%= Array(@session.messages).size %> message(s) · updated <%= @session.updated_at %></span>
|
|
20
|
-
</h1>
|
|
13
|
+
<nav class="crumbs"><a href="/studio/chats<%= @agent ? "?agent=#{Rack::Utils.escape(@agent)}" : "" %>">chats</a></nav>
|
|
14
|
+
<h1>Chats<span class="sub">Read the conversation and its history</span></h1>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="head-actions">
|
|
17
|
+
<a class="btn primary" href="/studio/playground">New conversation</a>
|
|
21
18
|
</div>
|
|
22
|
-
<a class="btn" href="/studio/playground?session_id=<%= Rack::Utils.escape(@session.id) %>">Continue in playground →</a>
|
|
23
19
|
</header>
|
|
24
20
|
|
|
25
|
-
<div class="drill" data-controller="list-filter">
|
|
26
|
-
|
|
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>
|
|
21
|
+
<div class="drill operation-workspace" data-controller="list-filter">
|
|
22
|
+
<%== render("_chats_master", locals: { active_id: @session.id }) %>
|
|
47
23
|
<% end %>
|
|
48
|
-
<turbo-frame id="session-detail" class="drill-pane drill-detail session-pane">
|
|
24
|
+
<turbo-frame id="session-detail" class="drill-pane drill-detail session-pane operation-detail">
|
|
49
25
|
<div class="session-columns">
|
|
50
26
|
<%# ── Center: the transcript ───────────────────────────────────── %>
|
|
51
|
-
|
|
27
|
+
<%# live-transcript (T11): the SAME SSE island the playground uses,
|
|
28
|
+
scoped to THIS session — a turn landing on it while an operator has
|
|
29
|
+
it open here streams in live, right alongside the replayed history
|
|
30
|
+
below. The controller lives on THIS section, not the outer
|
|
31
|
+
turbo-frame: a row click in the master re-renders this whole
|
|
32
|
+
section from scratch (Turbo's default frame swap replaces the
|
|
33
|
+
frame's content wholesale, no morphing configured anywhere in this
|
|
34
|
+
app — see live_transcript_reconnect.test.mjs), so Stimulus tears
|
|
35
|
+
down the old controller (closing its EventSource) and boots a
|
|
36
|
+
fresh one scoped to the newly-selected session_id. Putting the
|
|
37
|
+
controller on the turbo-frame element itself would defeat this:
|
|
38
|
+
that element persists across the swap, so its session-value would
|
|
39
|
+
change under a live controller instance with no valueChanged
|
|
40
|
+
callback to notice — the stream would keep serving the PREVIOUS
|
|
41
|
+
session. %>
|
|
42
|
+
<section class="session-transcript" aria-label="transcript"
|
|
43
|
+
data-controller="live-transcript"
|
|
44
|
+
data-live-transcript-session-value="<%= @session.id %>"
|
|
45
|
+
data-live-transcript-why-value="/studio/sessions/<%= Rack::Utils.escape(@session.id) %>">
|
|
52
46
|
<%# Session markers in the margin: start, agent,
|
|
53
47
|
shape — ghost pills, right-aligned, from data the view already
|
|
54
48
|
has. %>
|
|
55
49
|
<% first_at = Array(@session.messages).first&.[]("at") %>
|
|
56
50
|
<% marker_agent = session_agent(@session) %>
|
|
51
|
+
<header class="operation-summary">
|
|
52
|
+
<div class="section-head">
|
|
53
|
+
<h2><%= presence(session_customer_label(@session)) || "Conversation" %></h2>
|
|
54
|
+
<p class="muted"><%= marker_agent.empty? ? "No agent recorded" : marker_agent %><% if (phone = session_customer_phone(@session)) %> · <%= phone %><% end %> · <%= Array(@session.messages).size %> messages</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="actions">
|
|
57
|
+
<span class="copy-id" data-controller="clipboard" data-clipboard-text-value="<%= @session.id %>">
|
|
58
|
+
<button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy" aria-label="Copy full session id">copy id</button>
|
|
59
|
+
</span>
|
|
60
|
+
<span class="pill info" data-live-transcript-target="status" role="status">connecting…</span>
|
|
61
|
+
<a class="btn btn-sm" href="/studio/playground?session_id=<%= Rack::Utils.escape(@session.id) %>" data-turbo-frame="_top">Continue in playground</a>
|
|
62
|
+
<%# Narrow screens: the metadata margin is a popover drawer (no JS — the
|
|
63
|
+
Popover API toggles it, Esc and a click outside close it). %>
|
|
64
|
+
<button type="button" class="btn btn-sm details-toggle" popovertarget="session-details">Details</button>
|
|
65
|
+
</div>
|
|
66
|
+
</header>
|
|
57
67
|
<div class="marker-row">
|
|
58
|
-
<% unless short_time(first_at).empty? %><span class="ghost-pill">session · <%= short_time(first_at) %></span><% end %>
|
|
59
68
|
<% unless marker_agent.empty? %><span class="ghost-pill">agent · <%= marker_agent %></span><% end %>
|
|
60
|
-
|
|
69
|
+
<% unless short_time(first_at).empty? %><span class="ghost-pill">session · <%= short_time(first_at) %></span><% end %>
|
|
70
|
+
<span class="ghost-pill">updated <%= time_ago(@session.updated_at) %></span>
|
|
61
71
|
</div>
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
73
|
+
<div class="thread" data-live-transcript-target="stream">
|
|
74
|
+
<%# A skill loaded by the model shows up here on its own: load_skill is a tool,
|
|
75
|
+
so its body is a persisted `role: tool` message. A skill injected by CONTEXT
|
|
76
|
+
(triggers/skills_eager) is part of the system prompt — never a message — so
|
|
77
|
+
the thread had no trace of it at all. Synthesized from the context trace so
|
|
78
|
+
both activation paths read the same way in the transcript.
|
|
72
79
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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 }) %>
|
|
80
|
+
Placed by TIME, not by counting turns: every message of a turn is persisted
|
|
81
|
+
together at the end of it, and the context stage always runs before that, so
|
|
82
|
+
each entry lands at the top of its own turn. Ordinal counting would drift on
|
|
83
|
+
a steered message (one that joins a turn already running). A message with no
|
|
84
|
+
`at` (older sessions) simply flushes nothing. %>
|
|
85
|
+
<% pending = (@context_traces || [])
|
|
86
|
+
.select { |c| Array((c["categories"] || {}).dig("skilltrigger", "labels")).any? }
|
|
87
|
+
.sort_by { |c| c["at"].to_s } %>
|
|
88
|
+
<% Array(@session.messages).each_with_index do |m, i| %>
|
|
89
|
+
<% while (entry = pending.first) && !m["at"].to_s.empty? && entry["at"].to_s <= m["at"].to_s %>
|
|
90
|
+
<% pending.shift %>
|
|
91
|
+
<% labels = Array(entry["categories"]["skilltrigger"]["labels"]).uniq %>
|
|
92
|
+
<details class="toolcard skill">
|
|
93
|
+
<summary><%== chip_icon(:skill) %><strong>skills · <%= skill_mode(labels) %> (<%= labels.length %>)</strong><span class="at"><%= entry["at"] %></span></summary>
|
|
94
|
+
<pre><%= labels.map { |l| skill_label(l) }.join("\n") %></pre>
|
|
95
|
+
</details>
|
|
93
96
|
<% end %>
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
<%# `prev` is what makes a STEERED message visible: a customer message that lands
|
|
98
|
+
right after a tool result arrived mid-run. See _message.erb. %>
|
|
99
|
+
<%== render("_message", locals: { m: m, prev: i.positive? ? @session.messages[i - 1] : nil,
|
|
100
|
+
review: { path: "/studio/sessions/#{Rack::Utils.escape_path(@session.id)}/guardrail", message: i } }) %>
|
|
101
|
+
<% if (w = @why && @why[i]) %><%== render("_why", locals: { w: w, sid: @session.id, index: i }) %><% end %>
|
|
102
|
+
<% end %>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="empty" data-live-transcript-target="empty"<%== ' hidden' unless Array(@session.messages).empty? %>>This session has no messages.</div>
|
|
96
105
|
</section>
|
|
97
106
|
|
|
98
107
|
<%# ── Right margin: the session's metadata ──────────────────────── %>
|
|
99
|
-
<
|
|
108
|
+
<div class="col-resize" role="separator" aria-orientation="vertical" aria-label="Resize details" tabindex="0"
|
|
109
|
+
data-controller="col-resize" data-col-resize-key-value="session-details" data-col-resize-side-value="next"
|
|
110
|
+
data-action="pointerdown->col-resize#start keydown->col-resize#key dblclick->col-resize#reset"></div>
|
|
111
|
+
<aside class="session-margin" id="session-details" popover aria-label="session metadata">
|
|
112
|
+
<button type="button" class="btn btn-sm details-close" popovertarget="session-details" popovertargetaction="hide">Close</button>
|
|
113
|
+
<%# Who this conversation is with — the vars the caller sent, plus how
|
|
114
|
+
many conversations share the customer key (a link to them). %>
|
|
115
|
+
<% customer = presence(session_customer(@session)) %>
|
|
116
|
+
<% if customer %>
|
|
117
|
+
<div class="card">
|
|
118
|
+
<div class="trace-head"><h2>Customer</h2></div>
|
|
119
|
+
<div class="kv"><span>key</span><div class="mono"><%= customer %></div></div>
|
|
120
|
+
<% if (name = presence(session_customer_label(@session))) && name != customer %><div class="kv"><span>name</span><div><%= name %></div></div><% end %>
|
|
121
|
+
<% if (phone = session_customer_phone(@session)) %><div class="kv"><span>phone</span><div><%= phone %></div></div><% end %>
|
|
122
|
+
<div class="kv"><span>chats</span><div><a href="/studio/chats?q=<%= Rack::Utils.escape(customer) %>" data-turbo-frame="_top"><%= customer_chat_count(customer) %> conversation(s)</a></div></div>
|
|
123
|
+
</div>
|
|
124
|
+
<% end %>
|
|
100
125
|
<% briefing = @session.respond_to?(:briefing) ? (@session.briefing || {}) : {} %>
|
|
101
126
|
<% fields = briefing["fields"] || {} %>
|
|
102
127
|
<% next_step = briefing["next_step"] %>
|
|
@@ -105,22 +130,50 @@
|
|
|
105
130
|
<div class="trace-head">
|
|
106
131
|
<h2>Briefing</h2>
|
|
107
132
|
</div>
|
|
133
|
+
<% if next_step %>
|
|
134
|
+
<p class="operation-next"><span class="muted">next step</span><br><strong><%= next_step %></strong></p>
|
|
135
|
+
<% end %>
|
|
108
136
|
<% fields.sort.each do |name, value| %>
|
|
109
137
|
<div class="kv"><span><%= name %></span><div><%= value %></div></div>
|
|
110
138
|
<% end %>
|
|
111
|
-
<% if next_step %>
|
|
112
|
-
<div class="kv"><span>next step</span><div><%= next_step %></div></div>
|
|
113
|
-
<% end %>
|
|
114
139
|
</div>
|
|
115
140
|
<% end %>
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
<%# the session's stage history — outcomes folded to the agent's
|
|
143
|
+
declared stages (unmapped kinds render the raw outcome: the hole is visible).
|
|
144
|
+
Without a declaration the block renders the raw outcomes. %>
|
|
145
|
+
<% history = @stage_history || {} %>
|
|
146
|
+
<% unless history.empty? %>
|
|
147
|
+
<div class="card">
|
|
120
148
|
<div class="trace-head">
|
|
121
|
-
<h2>
|
|
122
|
-
<span class="muted">
|
|
149
|
+
<h2>Stage history</h2>
|
|
150
|
+
<span class="muted">
|
|
151
|
+
<% if history[:declaration] %>
|
|
152
|
+
<%= history[:declaration].stages.join(" → ") %> · outcome funnel
|
|
153
|
+
<% else %>
|
|
154
|
+
no funnel declared — raw outcomes
|
|
155
|
+
<% end %>
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="kv">
|
|
159
|
+
<% history[:rows].each do |row| %>
|
|
160
|
+
<% if row[:stage] %>
|
|
161
|
+
<span><%= row[:outcome] %></span>
|
|
162
|
+
<div>→ <strong><%= row[:stage] %></strong> <span class="lat"><%= row[:at] %></span></div>
|
|
163
|
+
<% else %>
|
|
164
|
+
<span><%= row[:outcome] %></span>
|
|
165
|
+
<div><span class="lat">not a declared stage</span> <span class="lat"><%= row[:at] %></span></div>
|
|
166
|
+
<% end %>
|
|
167
|
+
<% end %>
|
|
123
168
|
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<% end %>
|
|
171
|
+
|
|
172
|
+
<% ctx_traces = @context_traces || [] %>
|
|
173
|
+
<% unless ctx_traces.empty? %>
|
|
174
|
+
<details class="card tooltrace operation-diagnostics">
|
|
175
|
+
<summary>Context <span class="muted">· <%= ctx_traces.size %> turns</span></summary>
|
|
176
|
+
<p class="muted">Estimated tokens by category for each turn.</p>
|
|
124
177
|
<% ctx_traces.each do |c| %>
|
|
125
178
|
<div class="trace-turn">
|
|
126
179
|
<div class="turn-label">turn <%= c["turn"] %> · <%= c["at"] %></div>
|
|
@@ -169,27 +222,39 @@
|
|
|
169
222
|
</details>
|
|
170
223
|
</div>
|
|
171
224
|
<% end %>
|
|
172
|
-
</
|
|
225
|
+
</details>
|
|
173
226
|
<% end %>
|
|
174
227
|
|
|
175
|
-
<% traces = @tool_traces ||
|
|
228
|
+
<% traces = @tool_traces || [] %>
|
|
176
229
|
<% unless traces.empty? %>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
<% traces.keys.sort.each do |turn| %>
|
|
230
|
+
<% errors = traces.flatten.count { |trace| !trace["ok"] || trace["gate"] } %>
|
|
231
|
+
<details class="card tooltrace operation-diagnostics">
|
|
232
|
+
<summary>Tool calls <span class="muted">· <%= traces.sum(&:size) %></span><% if errors.positive? %> <span class="status err"><%= errors %> failed or blocked</span><% end %></summary>
|
|
233
|
+
<p class="muted">Arguments and responses are masked.</p>
|
|
234
|
+
<% traces.each do |group| %>
|
|
183
235
|
<div class="trace-turn">
|
|
184
|
-
<div class="turn-label">turn <%= turn %></div>
|
|
185
|
-
|
|
236
|
+
<div class="turn-label">turn <%= group.first["turn"] %></div>
|
|
237
|
+
<%# Waterfall: when each call ran inside the turn (SVG geometry, CSP-safe). %>
|
|
238
|
+
<% bars = trace_waterfall(group) %>
|
|
239
|
+
<% if bars.size > 1 %>
|
|
240
|
+
<svg class="waterfall" viewBox="0 0 100 <%= bars.size * 6 %>" preserveAspectRatio="none" role="img"
|
|
241
|
+
aria-label="<%= bars.map { |b| "#{b[:tool]} #{b[:ms]}ms" }.join(", ") %>">
|
|
242
|
+
<% bars.each_with_index do |b, i| %>
|
|
243
|
+
<rect class="<%= b[:ok] ? "ok" : "err" %>" x="<%= b[:x] %>" y="<%= i * 6 + 1 %>" width="<%= b[:w] %>" height="4" rx="0.6"><title><%= b[:tool] %> · <%= b[:ms] %>ms</title></rect>
|
|
244
|
+
<% end %>
|
|
245
|
+
</svg>
|
|
246
|
+
<% end %>
|
|
247
|
+
<% group.each do |t| %>
|
|
186
248
|
<details class="trace-item <%= t["ok"] ? "ok" : "err" %>">
|
|
187
249
|
<summary>
|
|
188
|
-
<span class="badge"><%= t["ok"] ? "ok" : "error" %></span>
|
|
250
|
+
<span class="badge"><%= t["gate"] ? "blocked: #{t["gate"]}" : (t["ok"] ? "ok" : "error") %></span>
|
|
189
251
|
<strong><%= t["tool"] %></strong>
|
|
190
252
|
<span class="lat"><%= t["ms"] %>ms</span>
|
|
191
253
|
<span class="at"><%= t["at"] %></span>
|
|
192
254
|
</summary>
|
|
255
|
+
<% if (loaded = loaded_skill_name(t)) %>
|
|
256
|
+
<div class="kv"><span>skill</span><a class="mono" href="/studio/skills/<%= Rack::Utils.escape(loaded) %>" data-turbo-frame="_top"><%= loaded %></a></div>
|
|
257
|
+
<% end %>
|
|
193
258
|
<div class="kv"><span>args</span>
|
|
194
259
|
<div class="copywrap" data-controller="clipboard">
|
|
195
260
|
<button type="button" class="copy-btn" data-clipboard-target="button" data-action="clipboard#copy">copy</button>
|
|
@@ -206,37 +271,7 @@
|
|
|
206
271
|
<% end %>
|
|
207
272
|
</div>
|
|
208
273
|
<% end %>
|
|
209
|
-
</
|
|
210
|
-
<% end %>
|
|
211
|
-
|
|
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>
|
|
274
|
+
</details>
|
|
240
275
|
<% end %>
|
|
241
276
|
</aside>
|
|
242
277
|
</div>
|