insika 0.0.1 → 0.2.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 +361 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +366 -0
- data/docs/AGENTS.md +618 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +117 -0
- data/docs/DEPLOY.md +354 -0
- data/docs/EMBEDDING.md +198 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +232 -0
- data/docs/OBSERVABILITY.md +374 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +375 -0
- data/docs/SKILLS.md +284 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +255 -0
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +153 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +303 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +58 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +43 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_refinement.rb +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +112 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +60 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +89 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +43 -0
- data/lib/insika/context/priority.rb +30 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +123 -0
- data/lib/insika/context/providers/skill.rb +24 -0
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +539 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +382 -0
- data/lib/insika/dsl/server_boot.rb +98 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +364 -0
- data/lib/insika/edge_limiter.rb +268 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +249 -0
- data/lib/insika/errors.rb +201 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +132 -0
- data/lib/insika/executor.rb +1995 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +115 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +102 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +123 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +143 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +168 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +102 -0
- data/lib/insika/safety/moderator.rb +94 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/app.rb +1022 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +118 -0
- data/lib/insika/server/responses.rb +165 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +220 -0
- data/lib/insika/skill_store.rb +127 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1693 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +70 -0
- data/lib/insika/studio/forms.rb +335 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +137 -0
- data/lib/insika/studio/views/settings.erb +168 -0
- data/lib/insika/studio/views/skills.erb +141 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +219 -0
- data/lib/insika/tools/load_skill.rb +99 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +162 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +249 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +220 -4
- metadata +412 -8
data/docs/REFINEMENT.md
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Refinement
|
|
3
|
+
parent: Operate & prove it
|
|
4
|
+
nav_order: 5
|
|
5
|
+
permalink: /refinement/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Refinement
|
|
9
|
+
|
|
10
|
+
An agent in production teaches you something every hour, and by default none of it
|
|
11
|
+
comes back. The engine already records the evidence — every tool call with its
|
|
12
|
+
arguments and result, every transcript, every failed turn — and nobody reads it.
|
|
13
|
+
So the same wrong answer is served to the next customer until a human happens to
|
|
14
|
+
look.
|
|
15
|
+
|
|
16
|
+
Refinement is the loop that closes that gap. It reads a window of an agent's own
|
|
17
|
+
traffic and reports **what broke, how often, and in which conversations**.
|
|
18
|
+
|
|
19
|
+
The report is the default and it is the whole feature until you turn on more: it
|
|
20
|
+
calls no model and it changes nothing about your agent — no prompt is rewritten, no
|
|
21
|
+
tool is touched. That is deliberate. A report you can read in thirty seconds is
|
|
22
|
+
worth more than an automatic edit you cannot verify, and it is the honest way to
|
|
23
|
+
find out whether the evidence in your own deployment is good enough to act on.
|
|
24
|
+
|
|
25
|
+
Opt in (`mode: propose`) and the loop goes one step further: a model proposes a
|
|
26
|
+
small, anchored edit to the instruction files you listed, the edit is scored by
|
|
27
|
+
**running** the agent's test cases with it applied, and a human approves it before
|
|
28
|
+
it reaches anyone. Every part of that is below, including what it cannot catch.
|
|
29
|
+
|
|
30
|
+
## Running one
|
|
31
|
+
|
|
32
|
+
From the CLI, against the same database the engine uses:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
INSIKA_DB=insika.db bin/insika refine --agent bia
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
bia — completed (last 200 session(s))
|
|
40
|
+
tool_error ×24 shipping_quote failed: cep is required
|
|
41
|
+
sessions: 9f2c1a04…, 4b7e5590…, c1d0aa31…
|
|
42
|
+
repetition ×7 customer repeated themselves
|
|
43
|
+
quero saber o frete, quanto fica pro meu endereço
|
|
44
|
+
sessions: 9f2c1a04…, 77bb0e12…
|
|
45
|
+
safe_reply ×3 a canned safe reply was served instead of an answer
|
|
46
|
+
sessions: 2a55f0c9…
|
|
47
|
+
tool_unused ×1 search_voucher was never called in this window
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or from **Studio → Refinement**: pick the agent, press Run, and the same report
|
|
51
|
+
renders with each session id linking to its transcript. Every finding is a claim
|
|
52
|
+
you can go and check.
|
|
53
|
+
|
|
54
|
+
There is no scheduler in the engine, by design. A run is one command
|
|
55
|
+
(`run_refinement`), and the CLI and the button are the two ways to fire it. If you
|
|
56
|
+
want it on a timer, point your own cron at the authenticated route the button uses
|
|
57
|
+
— that keeps the engine free of a background ticker and of the single-node
|
|
58
|
+
assumption one would bring.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
insika refine ─┐
|
|
62
|
+
Studio button ─┼─▶ run_refinement ─▶ read the window ─▶ ranked report (stored)
|
|
63
|
+
your cron ────┘ (tasks, transcripts, tool traces)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## The window
|
|
67
|
+
|
|
68
|
+
By default, a run is **incremental**: it reads everything since the previous run
|
|
69
|
+
for that agent, so running it twice in a row is quiet. The first run — or any run
|
|
70
|
+
with `--full` — reads the configured window instead, which defaults to the 200
|
|
71
|
+
most recent conversations.
|
|
72
|
+
|
|
73
|
+
| Flag | Meaning |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `--last-sessions N` | the N most recent conversations, however many turns those took |
|
|
76
|
+
| `--since ISO8601` | only turns from that instant on |
|
|
77
|
+
| `--full` | ignore the previous run and use the configured window |
|
|
78
|
+
| `--exclude PREFIXES` | drop sessions whose id starts with any of these (e.g. `loadtest-,debug-`) |
|
|
79
|
+
| `--json` | the run record as JSON, for a pipeline |
|
|
80
|
+
|
|
81
|
+
The window is stored on the run, so a report read months later still says what it
|
|
82
|
+
looked at.
|
|
83
|
+
|
|
84
|
+
## Synthetic traffic
|
|
85
|
+
|
|
86
|
+
If load tests, evals or debug conversations land in the same store as real ones,
|
|
87
|
+
they will dominate the report — on the pilot they outnumbered real conversations
|
|
88
|
+
and buried every genuine finding, including 203 turns failing for a reason that
|
|
89
|
+
only occurs under the load-test profile.
|
|
90
|
+
|
|
91
|
+
`exclude_sessions` (or `--exclude`) drops sessions by id prefix. It defaults to
|
|
92
|
+
**nothing**: a report does not get to decide what counts as real traffic. What it
|
|
93
|
+
dropped is counted on the run and shown next to the findings, so a filtered window
|
|
94
|
+
never reads like a clean deployment.
|
|
95
|
+
|
|
96
|
+
## What it looks for
|
|
97
|
+
|
|
98
|
+
Each finding is aggregated: forty instances of one broken argument are **one**
|
|
99
|
+
finding with a count of forty, not forty rows. Findings are ranked by count times
|
|
100
|
+
severity, and each carries up to five session ids as provenance — ids only, so the
|
|
101
|
+
record itself holds no conversation content.
|
|
102
|
+
|
|
103
|
+
| Finding | What it means | Read from |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `tool_error` | a tool returned an error, grouped by tool and by a normalized error signature | tool traces |
|
|
106
|
+
| `task_failed` | a turn died, grouped by its error | the task's executions |
|
|
107
|
+
| `repetition` | the customer said the same thing twice in a row — the outside view of an instruction the agent is not following | the transcript |
|
|
108
|
+
| `safe_reply` | a canned safe reply reached the customer instead of an answer | the transcript |
|
|
109
|
+
| `tool_unused` | a tool the agent is allowed to use never fired in the whole window | profile vs tool traces |
|
|
110
|
+
|
|
111
|
+
Two notes on honesty, because a report that overstates what it knows is worse than
|
|
112
|
+
no report:
|
|
113
|
+
|
|
114
|
+
- **`repetition` is a heuristic**, not a judgment: token overlap between
|
|
115
|
+
consecutive customer messages, with short messages ignored so a repeated "hi"
|
|
116
|
+
does not count. It calls no model.
|
|
117
|
+
- **`safe_reply` cannot tell you which rule fired.** Guardrail decisions and
|
|
118
|
+
edge-limit hits are emitted as events and never stored, so the canned reply in
|
|
119
|
+
the transcript is their only durable footprint. The finding tells you the agent
|
|
120
|
+
gave up; the guardrail configuration tells you why it might have.
|
|
121
|
+
|
|
122
|
+
Numbers and ids are normalized out of the grouping key, so `product 4711 not found`
|
|
123
|
+
and `product 4712 not found` are recognized as one defect.
|
|
124
|
+
|
|
125
|
+
## Who wrote a message
|
|
126
|
+
|
|
127
|
+
A `role` says where a message sits in the conversation, not who wrote it, and the two
|
|
128
|
+
come apart constantly: the engine delivers a subagent's result as a `user` turn, a
|
|
129
|
+
guardrail answers as `assistant` with no model involved, a consumer composes context
|
|
130
|
+
blocks into the input, and in an imported transcript a human operator types after a
|
|
131
|
+
handoff. Read without that distinction, the first run over real traffic reported
|
|
132
|
+
**219** "the customer repeated themselves" that were the engine reading its own
|
|
133
|
+
injected fragment back.
|
|
134
|
+
|
|
135
|
+
So a stored message may carry an `origin`:
|
|
136
|
+
|
|
137
|
+
| origin | who |
|
|
138
|
+
|---|---|
|
|
139
|
+
| *(absent)* | the natural producer for the role — a customer for `user`, the model for `assistant` |
|
|
140
|
+
| `customer` | a person, said explicitly |
|
|
141
|
+
| `agent` | the model, said explicitly |
|
|
142
|
+
| `engine` | Insika itself, or a consumer composing on its behalf |
|
|
143
|
+
| `operator` | a **human** on the assistant side (a handoff) — set by whatever imports the transcript |
|
|
144
|
+
|
|
145
|
+
The engine stamps what it truthfully knows: a delegation result it wrote, a guardrail
|
|
146
|
+
reply it produced. A consumer declares its own composed input by sending `"origin"` on
|
|
147
|
+
`POST /v1/responses`. Nothing else changes — a message with no origin reads exactly as
|
|
148
|
+
it did before, so no transcript needs migrating.
|
|
149
|
+
|
|
150
|
+
`repetition` counts only what a customer said and `safe_reply` reads only what the
|
|
151
|
+
engine said, both from this field. A message that declares nothing falls back to the
|
|
152
|
+
old guess (an injected fragment opens with its own tag, `<store_cep_required> …`,
|
|
153
|
+
which no customer types) — that heuristic now runs only on messages that made no
|
|
154
|
+
claim about themselves.
|
|
155
|
+
|
|
156
|
+
## Privacy
|
|
157
|
+
|
|
158
|
+
A report may quote customer words — that is the point of the `repetition` snippet —
|
|
159
|
+
so every snippet goes through the same redaction a customer-facing turn does
|
|
160
|
+
(`[REDACTED:cpf]` and friends, see [Security](SECURITY.md)). Tool arguments and
|
|
161
|
+
results are never copied into a report at all; only the error signature is. And the
|
|
162
|
+
run record stores session ids, never their contents.
|
|
163
|
+
|
|
164
|
+
That redaction covers what the engine's detectors cover — formatted CPF/CNPJ and
|
|
165
|
+
API secrets. It is not a general PII scrubber: a phone number written into a chat
|
|
166
|
+
can survive into a snippet. Treat the Refinement page as what it is — an operator
|
|
167
|
+
surface behind the Studio login, next to the transcripts themselves.
|
|
168
|
+
|
|
169
|
+
## Configuration
|
|
170
|
+
|
|
171
|
+
Refinement needs no opt-in to report: reading your own traffic writes nothing, so
|
|
172
|
+
an agent with no configuration at all can be run. The optional block on the agent
|
|
173
|
+
sets the defaults:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
Insika.agent "bia" do
|
|
177
|
+
model "deepseek-v4-flash"
|
|
178
|
+
refine window: { last_sessions: 200 }, max_findings: 20
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
| Key | Default | Meaning |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `window.last_sessions` | 200 | conversations read when the run is not incremental |
|
|
185
|
+
| `max_findings` | 20 | cap on the report |
|
|
186
|
+
| `exclude_sessions` | none | session-id prefixes to drop |
|
|
187
|
+
| `mode` | `"report"` | `report` reads and writes nothing. `propose` allows a gated, human-approved edit (below). `auto_apply` lets a gate-passing edit land unattended — off by default, and read [what it costs you](#applying-without-a-human) first. A mode the engine does not know is refused, never silently downgraded |
|
|
188
|
+
| `files` | none | the ONLY files a proposal may edit. Empty means report-only |
|
|
189
|
+
| `proposer` | the platform `utility_model` | which model writes the candidate (`"deepseek/deepseek-v4-flash"` or a bare model name). Neither set means no proposal — the engine never picks a model to spend your budget on |
|
|
190
|
+
| `proposers` | falls back to `proposer` | a **panel**: several models, each writing its own candidate. `["deepseek/deepseek-v4-flash", {model: "gpt-5-mini", provider: "openai"}]` — either syntax |
|
|
191
|
+
| `budget.tokens` | unlimited | what one run may spend across every proposal and every gate replay |
|
|
192
|
+
| `max_edits` | 3 | edits a single proposal may carry |
|
|
193
|
+
| `auto_apply_max_edits` | 1 | edits an **unattended** apply may carry. A bigger diff waits for a person |
|
|
194
|
+
| `max_bytes` | 1200 | size of one edit's replacement text |
|
|
195
|
+
| `max_total_growth` | 0.15 | how much a proposal may grow a file, as a fraction of its current size |
|
|
196
|
+
|
|
197
|
+
## Events
|
|
198
|
+
|
|
199
|
+
A run emits two events, both counts and no content, so any subscriber (including
|
|
200
|
+
the OpenTelemetry bridge — see [Observability](OBSERVABILITY.md)) can watch it:
|
|
201
|
+
|
|
202
|
+
| Event | Data |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `:refinement_started` | agent, run id, window |
|
|
205
|
+
| `:refinement_report` | agent, run id, status, findings, sessions, turns |
|
|
206
|
+
| `:refinement_proposed` | agent, run id, candidates, proposers, edits |
|
|
207
|
+
| `:refinement_gated` | agent, run id, passed, reason, cases, passed_cases, regressions, candidates, tokens |
|
|
208
|
+
| `:refinement_applied` | agent, run id, by, files, edits |
|
|
209
|
+
| `:refinement_rejected` | agent, run id, by |
|
|
210
|
+
| `:refinement_auto_apply_skipped` | agent, run id, edits, max_edits |
|
|
211
|
+
|
|
212
|
+
File **names** appear on the applied event, because an operator needs to know what
|
|
213
|
+
changed. File **contents** never do.
|
|
214
|
+
|
|
215
|
+
## Changing the agent: the gate
|
|
216
|
+
|
|
217
|
+
A report tells you what broke. Changing the prompt because of it is a separate,
|
|
218
|
+
opt-in step, and the whole design is in one sentence: **a proposed edit is scored by
|
|
219
|
+
running it, and a human approves it before it reaches anyone.**
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
proposal ──▶ clone the agent ──▶ apply the edits to the CLONE ──▶ replay the golden
|
|
223
|
+
set ──▶ compare to the accepted baseline ──▶ a human approves ──▶ write
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Nothing here asks a model whether an edit looks good. That measures nothing. What
|
|
227
|
+
the gate measures is whether the agent still passes the cases it was passing, on
|
|
228
|
+
real turns, with its real tools and guardrails.
|
|
229
|
+
|
|
230
|
+
### What a proposal looks like
|
|
231
|
+
|
|
232
|
+
Data, not a rewritten file:
|
|
233
|
+
|
|
234
|
+
```jsonc
|
|
235
|
+
{
|
|
236
|
+
"rationale": "Two findings share a cause: TOOLS.md never says the CEP is required.",
|
|
237
|
+
"edits": [{
|
|
238
|
+
"file": "TOOLS.md", // must be in `files`
|
|
239
|
+
"op": "replace", // replace | append
|
|
240
|
+
"anchor": "## shipping_quote", // a label for the reviewer
|
|
241
|
+
"before": "Use shipping_quote to quote freight.", // must still match, exactly and once
|
|
242
|
+
"after": "Use shipping_quote to quote freight. Always ask for the CEP first.",
|
|
243
|
+
"addresses": ["tool_error:shipping_quote"]
|
|
244
|
+
}]
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Anchored and small is not a style preference. It makes the diff a five-second
|
|
249
|
+
decision instead of a code review, it makes the gate's result attributable to an
|
|
250
|
+
edit you can point at, and it makes staleness detectable: if `before` no longer
|
|
251
|
+
matches the file, the edit is dropped rather than applied by fuzzy match — which is
|
|
252
|
+
how a loop like this would otherwise silently overwrite something you wrote.
|
|
253
|
+
|
|
254
|
+
An edit that breaks a bound is dropped **with a reason** and the rest of the
|
|
255
|
+
proposal still goes to the gate. A proposal whose every edit dropped is refused
|
|
256
|
+
before anything runs.
|
|
257
|
+
|
|
258
|
+
### Who writes it
|
|
259
|
+
|
|
260
|
+
You can hand a candidate to the API yourself. Or press **Propose a fix** on a
|
|
261
|
+
finished report and the model named by `proposer` writes one, from the findings and
|
|
262
|
+
the current text of the allowlisted files.
|
|
263
|
+
|
|
264
|
+
That model is the weakest link in the loop, and it is built to be. It is shown the
|
|
265
|
+
evidence and the files it may edit; it produces data that is then bounded (allowlist,
|
|
266
|
+
size, growth, an anchor that must still match) and **scored by replaying your golden
|
|
267
|
+
set**. A hallucinated rationale, a misread finding, an invented anchor — the worst
|
|
268
|
+
outcome of each is a candidate that gets dropped or fails to move a score, and never
|
|
269
|
+
reaches a customer. Nothing it says is trusted; it is measured.
|
|
270
|
+
|
|
271
|
+
Three things follow from that, and they are worth knowing before you press the
|
|
272
|
+
button:
|
|
273
|
+
|
|
274
|
+
- **It only sees the files on your allowlist.** Not the rest of the prompt, not your
|
|
275
|
+
guardrails, not your tools. A model that can read a file it cannot edit proposes
|
|
276
|
+
edits to it, which drop, which spends your attention on rejects.
|
|
277
|
+
- **It only sees findings.** A run with none refuses to propose rather than inventing
|
|
278
|
+
an improvement to a prompt that is working.
|
|
279
|
+
- **It costs money twice** — once to write the candidate, once for the gate's replay,
|
|
280
|
+
which is a real conversation per case. So a proposal is a deliberate press, never a
|
|
281
|
+
timer, and the engine refuses if no `proposer` is configured rather than picking a
|
|
282
|
+
model for you.
|
|
283
|
+
|
|
284
|
+
The report and the proposal run in the same place they always did: `insika refine`
|
|
285
|
+
and Studio → Refinement. The proposal is Studio-only, because the gate's replay goes
|
|
286
|
+
through the deployment's own `/v1/responses` — the CLI runs without booting the app,
|
|
287
|
+
which is what makes it safe against a live volume, and it is not going to start a
|
|
288
|
+
server to grade an edit.
|
|
289
|
+
|
|
290
|
+
**What the proposals actually look like**, from running this against a real
|
|
291
|
+
production-shaped agent (a 22 KB persona, an 11 KB tool guide, seventeen findings
|
|
292
|
+
from its own traffic): most were edits a human would have made — reuse the search
|
|
293
|
+
result you already have instead of searching again, say one honest sentence when a
|
|
294
|
+
tool fails instead of retrying it. One was not, and it is the failure mode to know
|
|
295
|
+
about: **an infrastructure finding invites prose that cannot work.** Shown tool
|
|
296
|
+
errors that were really a blocked destination and a refused connection, the model
|
|
297
|
+
proposed instructing the agent to "always use https" — advice about something the
|
|
298
|
+
agent does not control and cannot obey. Naming that trap in the proposer's own
|
|
299
|
+
instructions removed it, and the model now says out loud which findings it is
|
|
300
|
+
declining to address. It will not catch every case: when you review a proposal, the
|
|
301
|
+
first question worth asking is whether the finding it addresses is behaviour at all.
|
|
302
|
+
|
|
303
|
+
### More than one proposer
|
|
304
|
+
|
|
305
|
+
`proposers` asks several models the same question and gates every answer:
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
refine mode: "propose", files: %w[TOOLS.md],
|
|
309
|
+
proposers: ["deepseek/deepseek-v4-flash", "gpt-5-mini"],
|
|
310
|
+
budget: { tokens: 200_000 }
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
They are **independent, not consensus-seeking**. Each is shown the same findings and
|
|
314
|
+
the same files and writes its own candidate; the gate then scores each one and you
|
|
315
|
+
are shown the best survivor, with the others listed under it. Convergence only ever
|
|
316
|
+
breaks a tie: two models agreeing on wording is weak evidence, and a golden case
|
|
317
|
+
passing is strong evidence. Ranking is highest score, then the smaller diff, then how
|
|
318
|
+
many models converged.
|
|
319
|
+
|
|
320
|
+
Two models that write the *identical* edit are gated once, not twice — the agreement
|
|
321
|
+
is recorded and the replay is not paid for again. A model that answers prose, times
|
|
322
|
+
out or 500s takes itself out of the panel and the rest proceeds; all of them failing
|
|
323
|
+
is an error, not a silent empty result. The panel runs concurrently and is capped at
|
|
324
|
+
the subagent fan-out (8, `INSIKA_SUBAGENT_FANOUT_CAP`).
|
|
325
|
+
|
|
326
|
+
A panel of one is exactly what `proposer` already did, which is why nothing changes
|
|
327
|
+
for an agent that names a single model.
|
|
328
|
+
|
|
329
|
+
### What a run may spend
|
|
330
|
+
|
|
331
|
+
A panel of 3 over a 7-case golden set is 3 model calls plus **21 replayed
|
|
332
|
+
conversations**, each a real turn with real tools. That is the honest objection to
|
|
333
|
+
this whole feature, and `budget.tokens` is the answer to it: a ceiling checked before
|
|
334
|
+
each expensive step, never in the middle of one. A candidate the run could not afford
|
|
335
|
+
is recorded as "not gated — the budget was spent", never dropped in silence, and the
|
|
336
|
+
run's cost is on the record where you can see whether the loop earns its keep.
|
|
337
|
+
|
|
338
|
+
Two things to know about the number.
|
|
339
|
+
|
|
340
|
+
**It counts the prompt cache.** A turn on a 27 KB pack reports `total_tokens: 88` with
|
|
341
|
+
`cached_tokens: 26624` — the engine's `total_tokens` is input + output and deliberately
|
|
342
|
+
excludes the cached prefix. A budget built on that alone would let a run send hundreds
|
|
343
|
+
of times what its ceiling said, so the budget bills `total + cached` and records the
|
|
344
|
+
cached share separately. On a real panel run against the pilot: **382,325 tokens spent,
|
|
345
|
+
362,752 of them cached** — 95%. Cached tokens are cheaper than fresh ones; they are not
|
|
346
|
+
free, and a ceiling has to see them.
|
|
347
|
+
|
|
348
|
+
**And when a provider reports nothing**, that leg is tallied as *unmetered* rather than
|
|
349
|
+
as zero, because a budget that quietly reads unmetered spend as free stops being a
|
|
350
|
+
budget. If your provider is silent, the bounds that still hold are structural: the
|
|
351
|
+
fan-out cap on the panel, `max_edits`, and the gate's own refusals.
|
|
352
|
+
|
|
353
|
+
### What the gate needs
|
|
354
|
+
|
|
355
|
+
Two things, and it refuses without either:
|
|
356
|
+
|
|
357
|
+
- **Golden cases for the agent.** No cases, no gate, no writes. Declaring them is
|
|
358
|
+
the price of admission to automated editing — and the cheapest thing you can do
|
|
359
|
+
to make this safe. See [Evals](EVALS.md).
|
|
360
|
+
- **A recorded baseline** — the accepted state of those cases:
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
insika evals:import # the corpus into the store
|
|
364
|
+
insika evals:baseline import # the accepted state, per agent
|
|
365
|
+
insika evals:baseline show
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Without one, "did anything regress?" has no answer, and a gate that answered
|
|
369
|
+
"nothing regressed" would be reporting that it did not look. So it refuses.
|
|
370
|
+
|
|
371
|
+
It also refuses a baseline in which **nothing passes**. A regression is measured
|
|
372
|
+
against a case that *was* passing, so an all-red baseline cannot produce one and
|
|
373
|
+
every candidate — including a harmful one — sails through. If that is where you
|
|
374
|
+
are, the agent needs fixing before it needs refining: get to a green run, record
|
|
375
|
+
it, then gate.
|
|
376
|
+
|
|
377
|
+
- **A judge, if your baseline was recorded with one.** A rubric'd case with no judge
|
|
378
|
+
verdict counts as a pass, so replaying without a judge against a judged baseline
|
|
379
|
+
does not measure less — it measures backwards, and every candidate reads as an
|
|
380
|
+
improvement. Measured: gating the pilot with no judge configured reported **6/6, no
|
|
381
|
+
regression** against a baseline the same corpus had just scored **2/6**. So the gate
|
|
382
|
+
refuses that combination. Configure the panel in Studio → Settings → Evals (or
|
|
383
|
+
`settings["evals"]["judges"]`), or re-record the baseline without a judge — then both
|
|
384
|
+
sides are equally deterministic, which is weak but not inverted.
|
|
385
|
+
|
|
386
|
+
With the judge on, the same two candidates were **rejected**: both dropped
|
|
387
|
+
`status-pedido` from 1.0 to 0.7, one also dropped `saudacao`. That is the gate doing
|
|
388
|
+
its job, on edits the broken version had waved through.
|
|
389
|
+
|
|
390
|
+
The clone is a throwaway agent (`<agent>-cand-<run>`) with the same profile, tools
|
|
391
|
+
and guardrails, and it is deleted afterwards — including when the replay fails.
|
|
392
|
+
**Any** regression disqualifies the candidate. A case that was already failing does
|
|
393
|
+
not: refinement exists to fix those.
|
|
394
|
+
|
|
395
|
+
### Approving
|
|
396
|
+
|
|
397
|
+
A candidate that passes the gate parks the run at `awaiting_approval` and shows up
|
|
398
|
+
on the Refinement page with the diff, what it claims to address, and its score. You
|
|
399
|
+
approve or reject; nothing applies itself.
|
|
400
|
+
|
|
401
|
+
Approving writes each edit through the agent's file store, which versions the
|
|
402
|
+
previous content — so **rollback is the Restore button that was already there**, in
|
|
403
|
+
the file's History. There is no separate undo to learn.
|
|
404
|
+
|
|
405
|
+
Between the gate and your approval, someone may have edited the same file by hand.
|
|
406
|
+
The apply re-checks every `before` against the file as it is now and refuses the
|
|
407
|
+
whole proposal if anything drifted. A partial application would leave a prompt in a
|
|
408
|
+
state nobody reviewed and the gate never scored.
|
|
409
|
+
|
|
410
|
+
### Applying without a human
|
|
411
|
+
|
|
412
|
+
`mode: "auto_apply"` is the one setting that lets a prompt change while nobody is
|
|
413
|
+
watching. It is off by default and it is deliberately narrow — it needs **all** of:
|
|
414
|
+
|
|
415
|
+
- the mode, set explicitly on that agent;
|
|
416
|
+
- a gate **pass** with zero regressions (a refused candidate is never auto-applied);
|
|
417
|
+
- a diff no larger than `auto_apply_max_edits`, which defaults to **1**.
|
|
418
|
+
|
|
419
|
+
A candidate that passes but is too large is **not rejected** — it waits for a person.
|
|
420
|
+
"Too big to apply unattended" and "wrong" are different verdicts, and collapsing them
|
|
421
|
+
would throw away a proposal the gate already paid to score.
|
|
422
|
+
|
|
423
|
+
An unattended apply goes through the same code an approval does: the same staleness
|
|
424
|
+
re-check, the same versioned write, the same `:refinement_applied` event. So the undo
|
|
425
|
+
is the same one — Restore in the file's History — and the Refinement page shows what
|
|
426
|
+
changed, why, and the link to get there.
|
|
427
|
+
|
|
428
|
+
The honest framing: this trades your review for your golden set. It is worth turning
|
|
429
|
+
on when the cases genuinely cover the behaviour you care about, and it is a bad idea
|
|
430
|
+
before that — see [what the gate cannot catch](#what-the-gate-can-and-cannot-catch),
|
|
431
|
+
which is the list of things auto-apply will happily wave through.
|
|
432
|
+
|
|
433
|
+
### What the gate can and cannot catch
|
|
434
|
+
|
|
435
|
+
Worth being precise about, because the gate is easy to trust more than it deserves.
|
|
436
|
+
Everything below was measured by running it against a real production-shaped agent,
|
|
437
|
+
not reasoned about.
|
|
438
|
+
|
|
439
|
+
**The gate is only as strong as your golden set.** This is the whole caveat and the
|
|
440
|
+
rest is detail. A regression is "a case that was passing now fails" — so an edit
|
|
441
|
+
that breaks something no case covers passes cleanly. Two shallow cases wave almost
|
|
442
|
+
anything through. If you want the loop to protect a behaviour, there has to be a
|
|
443
|
+
case for that behaviour; that is the price [the gate](#what-the-gate-needs) is charging,
|
|
444
|
+
and it is charged in curation work, not in configuration.
|
|
445
|
+
|
|
446
|
+
Three things it will **not** catch, and two of them are the engine working correctly:
|
|
447
|
+
|
|
448
|
+
- **An edit cannot remove a tool, so the gate will never see one disappear.** Tool
|
|
449
|
+
availability comes from the agent's `tools_allow`, not from prose. An instruction
|
|
450
|
+
like "never call `search_products`" is advice the model routinely overrides; the
|
|
451
|
+
tool is still attached and still gets called. If you want a tool gone, remove it
|
|
452
|
+
from the agent — which refinement cannot do, by design.
|
|
453
|
+
- **PII in a reply is redacted before the gate could grade it.** The output
|
|
454
|
+
guardrail runs on the turn, so a `must_not: [cpf]` case cannot fail because of an
|
|
455
|
+
edit that tells the agent to leak one. The protection is real; it just means this
|
|
456
|
+
is not the layer that measures it.
|
|
457
|
+
- **A small edit in a large prompt may change nothing at all.** A paragraph appended
|
|
458
|
+
to the end of an 11 KB instruction set routinely loses to the rest of it. A gate
|
|
459
|
+
pass on such an edit is honest — nothing changed — but it is not evidence that the
|
|
460
|
+
edit *worked*, and approving it adds prompt with no effect.
|
|
461
|
+
|
|
462
|
+
What it does catch reliably is the class that matters most in practice: an edit that
|
|
463
|
+
changes **what the agent says** in a way one of your cases checks. Formatting,
|
|
464
|
+
tone, how much it asks before acting, whether it follows a policy. That is where
|
|
465
|
+
prompt edits have real leverage, and it is also where they do damage.
|
|
466
|
+
|
|
467
|
+
## What this is not
|
|
468
|
+
|
|
469
|
+
It has no scheduler: a run happens because a person or a cron asked for one, never
|
|
470
|
+
because a timer inside the engine went off. A proposal is written when you ask for
|
|
471
|
+
one, and — unless you turned on [`auto_apply`](#applying-without-a-human), which is
|
|
472
|
+
off until you do and bounded when you do — applied when you approve it. And it cannot
|
|
473
|
+
touch your guardrails, tools, policies, model pins or limits, and not because a prompt
|
|
474
|
+
tells it not to — there is no code path (see [Security](SECURITY.md)).
|
|
475
|
+
|
|
476
|
+
And if the findings turn out to be noise in your deployment, the correct answer is
|
|
477
|
+
to stop at the report. That is a valid steady state, not a half-finished setup.
|
data/docs/RELEASING.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Releasing
|
|
3
|
+
parent: Ship it
|
|
4
|
+
nav_order: 5
|
|
5
|
+
permalink: /releasing/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Releasing
|
|
9
|
+
|
|
10
|
+
How an `insika` gem release is cut, and how the install is **proven** before the
|
|
11
|
+
push. The rule that matters: a green suite is not a green gem —
|
|
12
|
+
the suite resolves everything by path, so the entire class of packaging failure
|
|
13
|
+
is invisible to it. Do not publish on rspec alone.
|
|
14
|
+
|
|
15
|
+
## Before anything
|
|
16
|
+
|
|
17
|
+
1. The suite is green: `bundle exec rspec`.
|
|
18
|
+
2. `lib/insika/version.rb` carries the version being published.
|
|
19
|
+
3. Every new `lib/` file is **tracked in git**. The gemspec's `files` come from
|
|
20
|
+
`git ls-files`: an untracked file builds without a warning and the installed
|
|
21
|
+
gem fails at `require` — this is exactly the failure this proof exists to catch.
|
|
22
|
+
|
|
23
|
+
## Cut the gem
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem build insika.gemspec # -> insika-<version>.gem
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Prove the install — from OUTSIDE the repo
|
|
30
|
+
|
|
31
|
+
Install into a clean gem home and run the four shapes from a directory that is
|
|
32
|
+
not the checkout, with the repo's `lib/` nowhere on the load path:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
T=$(mktemp -d)
|
|
36
|
+
gem install --install-dir "$T/gemhome" insika-<version>.gem
|
|
37
|
+
cd "$T"
|
|
38
|
+
|
|
39
|
+
# 1. reply in-process
|
|
40
|
+
GEM_HOME="$T/gemhome" GEM_PATH="$T/gemhome" ruby -e '
|
|
41
|
+
require "insika"
|
|
42
|
+
agent = Insika.agent("assistant") { model "deepseek-v4-flash"; provider :deepseek }
|
|
43
|
+
puts agent.reply("hi")' # needs DEEPSEEK_API_KEY
|
|
44
|
+
|
|
45
|
+
# 2. serve — /studio login 200, /v1/responses streams, /start.md 200
|
|
46
|
+
# 3. Insika::Server.rack_app mounted under the host's own router (Rack::URLMap)
|
|
47
|
+
# 4. Insika.embed(backend: Insika::Stores::SQLite.new(path: "e1.db")) { … }.reply
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Shapes 2–4 are the ones that fail when a file is missing from the gem (the
|
|
51
|
+
Studio's `views/`, `assets/dist/`, the onboarding docs); run all four.
|
|
52
|
+
|
|
53
|
+
Then the load guard, from the **installed** gem — no test double:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
GEM_HOME="$T/gemhome" GEM_PATH="$T/gemhome" ruby -e '
|
|
57
|
+
require "insika"
|
|
58
|
+
abort "leak" if %w[RubyLLM Roda Falcon SQLite3 OpenTelemetry].any? { |c| Object.const_defined?(c) }
|
|
59
|
+
puts "clean"'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Publish
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
gem push insika-<version>.gem
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Publishing is irreversible in practice — a yanked 0.1.0 is a bad first
|
|
69
|
+
impression. The version number is cheap; the name is not. The install proof before
|
|
70
|
+
push, and nothing else.
|