insika 0.1.0 → 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 +69 -3
- data/README.md +1 -1
- data/bin/insika +22 -7
- data/docs/AGENTS.md +129 -5
- data/docs/CHANNELS.md +1 -1
- data/docs/CONTEXT.md +22 -5
- data/docs/DEPLOY.md +30 -10
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +1 -1
- data/docs/LOADTEST.md +3 -2
- data/docs/OBSERVABILITY.md +11 -2
- data/docs/REFINEMENT.md +6 -6
- data/docs/RELEASING.md +7 -7
- data/docs/RUNNING-LOCAL.md +1 -1
- data/docs/SECURITY.md +24 -11
- data/docs/SKILLS.md +189 -3
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/index.md +1 -1
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +89 -22
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/channel_delivery.rb +14 -11
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +3 -3
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +7 -7
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +62 -13
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +5 -3
- 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/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/resolve_refinement.rb +1 -1
- 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 +5 -5
- data/lib/insika/commands/send_message.rb +9 -9
- 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/context/builder.rb +2 -2
- data/lib/insika/context/fragment.rb +27 -3
- data/lib/insika/context/priority.rb +3 -2
- data/lib/insika/context/providers/memory.rb +1 -1
- 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 +5 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/doctor.rb +250 -5
- data/lib/insika/dsl/runtime.rb +12 -9
- data/lib/insika/dsl/server_boot.rb +4 -3
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +72 -15
- data/lib/insika/edge_limiter.rb +144 -6
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +13 -10
- data/lib/insika/errors.rb +61 -5
- data/lib/insika/evals/assertions.rb +12 -12
- 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 +3 -3
- 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/executor.rb +423 -108
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/golden_store.rb +2 -2
- 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/message_origin.rb +2 -2
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/onboarding.rb +2 -2
- data/lib/insika/outbox_store.rb +2 -2
- 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/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/profile_source.rb +12 -6
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +2 -2
- 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 +9 -9
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +2 -2
- data/lib/insika/safety/detectors.rb +5 -5
- data/lib/insika/safety/factory.rb +3 -3
- data/lib/insika/safety/input_guardrail.rb +19 -4
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +2 -2
- data/lib/insika/safety/output_validator.rb +2 -2
- 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/server/app.rb +223 -51
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +15 -7
- data/lib/insika/server/responses.rb +18 -8
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +3 -3
- data/lib/insika/session_store.rb +1 -1
- data/lib/insika/settings_store.rb +5 -5
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +127 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +1 -1
- data/lib/insika/studio/app.rb +183 -61
- data/lib/insika/studio/assets/dist/application.js +25 -24
- data/lib/insika/studio/forms.rb +13 -18
- data/lib/insika/studio/nav_icons.rb +1 -1
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +2 -2
- data/lib/insika/studio/views/agents.erb +1 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +78 -3
- data/lib/insika/studio/views/settings.erb +7 -12
- data/lib/insika/studio/views/skills.erb +67 -12
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +1 -1
- 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 +27 -27
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +8 -8
- data/lib/insika/tool_envelope.rb +1 -1
- data/lib/insika/tool_manifest.rb +6 -6
- 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 +4 -5
- data/lib/insika/tools/load_skill.rb +61 -3
- 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/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +17 -13
- data/lib/insika/turn_timing.rb +2 -2
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +77 -26
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +32 -15
- metadata +19 -2
- data/lib/insika/server/admin_auth.rb +0 -29
data/docs/REFINEMENT.md
CHANGED
|
@@ -149,7 +149,7 @@ it did before, so no transcript needs migrating.
|
|
|
149
149
|
|
|
150
150
|
`repetition` counts only what a customer said and `safe_reply` reads only what the
|
|
151
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, `<
|
|
152
|
+
old guess (an injected fragment opens with its own tag, `<store_cep_required> …`,
|
|
153
153
|
which no customer types) — that heuristic now runs only on messages that made no
|
|
154
154
|
claim about themselves.
|
|
155
155
|
|
|
@@ -174,7 +174,7 @@ sets the defaults:
|
|
|
174
174
|
|
|
175
175
|
```ruby
|
|
176
176
|
Insika.agent "bia" do
|
|
177
|
-
model "deepseek-
|
|
177
|
+
model "deepseek-v4-flash"
|
|
178
178
|
refine window: { last_sessions: 200 }, max_findings: 20
|
|
179
179
|
end
|
|
180
180
|
```
|
|
@@ -186,8 +186,8 @@ end
|
|
|
186
186
|
| `exclude_sessions` | none | session-id prefixes to drop |
|
|
187
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
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-
|
|
190
|
-
| `proposers` | falls back to `proposer` | a **panel**: several models, each writing its own candidate. `["deepseek/deepseek-
|
|
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
191
|
| `budget.tokens` | unlimited | what one run may spend across every proposal and every gate replay |
|
|
192
192
|
| `max_edits` | 3 | edits a single proposal may carry |
|
|
193
193
|
| `auto_apply_max_edits` | 1 | edits an **unattended** apply may carry. A bigger diff waits for a person |
|
|
@@ -306,7 +306,7 @@ first question worth asking is whether the finding it addresses is behaviour at
|
|
|
306
306
|
|
|
307
307
|
```ruby
|
|
308
308
|
refine mode: "propose", files: %w[TOOLS.md],
|
|
309
|
-
proposers: ["deepseek/deepseek-
|
|
309
|
+
proposers: ["deepseek/deepseek-v4-flash", "gpt-5-mini"],
|
|
310
310
|
budget: { tokens: 200_000 }
|
|
311
311
|
```
|
|
312
312
|
|
|
@@ -440,7 +440,7 @@ not reasoned about.
|
|
|
440
440
|
rest is detail. A regression is "a case that was passing now fails" — so an edit
|
|
441
441
|
that breaks something no case covers passes cleanly. Two shallow cases wave almost
|
|
442
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 [
|
|
443
|
+
case for that behaviour; that is the price [the gate](#what-the-gate-needs) is charging,
|
|
444
444
|
and it is charged in curation work, not in configuration.
|
|
445
445
|
|
|
446
446
|
Three things it will **not** catch, and two of them are the engine working correctly:
|
data/docs/RELEASING.md
CHANGED
|
@@ -8,7 +8,7 @@ permalink: /releasing/
|
|
|
8
8
|
# Releasing
|
|
9
9
|
|
|
10
10
|
How an `insika` gem release is cut, and how the install is **proven** before the
|
|
11
|
-
push. The rule that matters
|
|
11
|
+
push. The rule that matters: a green suite is not a green gem —
|
|
12
12
|
the suite resolves everything by path, so the entire class of packaging failure
|
|
13
13
|
is invisible to it. Do not publish on rspec alone.
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ is invisible to it. Do not publish on rspec alone.
|
|
|
18
18
|
2. `lib/insika/version.rb` carries the version being published.
|
|
19
19
|
3. Every new `lib/` file is **tracked in git**. The gemspec's `files` come from
|
|
20
20
|
`git ls-files`: an untracked file builds without a warning and the installed
|
|
21
|
-
gem fails at `require` — this is exactly the failure
|
|
21
|
+
gem fails at `require` — this is exactly the failure this proof exists to catch.
|
|
22
22
|
|
|
23
23
|
## Cut the gem
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ is invisible to it. Do not publish on rspec alone.
|
|
|
26
26
|
gem build insika.gemspec # -> insika-<version>.gem
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
## Prove the install
|
|
29
|
+
## Prove the install — from OUTSIDE the repo
|
|
30
30
|
|
|
31
31
|
Install into a clean gem home and run the four shapes from a directory that is
|
|
32
32
|
not the checkout, with the repo's `lib/` nowhere on the load path:
|
|
@@ -39,7 +39,7 @@ cd "$T"
|
|
|
39
39
|
# 1. reply in-process
|
|
40
40
|
GEM_HOME="$T/gemhome" GEM_PATH="$T/gemhome" ruby -e '
|
|
41
41
|
require "insika"
|
|
42
|
-
agent = Insika.agent("assistant") { model "deepseek-
|
|
42
|
+
agent = Insika.agent("assistant") { model "deepseek-v4-flash"; provider :deepseek }
|
|
43
43
|
puts agent.reply("hi")' # needs DEEPSEEK_API_KEY
|
|
44
44
|
|
|
45
45
|
# 2. serve — /studio login 200, /v1/responses streams, /start.md 200
|
|
@@ -50,7 +50,7 @@ GEM_HOME="$T/gemhome" GEM_PATH="$T/gemhome" ruby -e '
|
|
|
50
50
|
Shapes 2–4 are the ones that fail when a file is missing from the gem (the
|
|
51
51
|
Studio's `views/`, `assets/dist/`, the onboarding docs); run all four.
|
|
52
52
|
|
|
53
|
-
Then the load guard, from the **installed** gem
|
|
53
|
+
Then the load guard, from the **installed** gem — no test double:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
GEM_HOME="$T/gemhome" GEM_PATH="$T/gemhome" ruby -e '
|
|
@@ -66,5 +66,5 @@ gem push insika-<version>.gem
|
|
|
66
66
|
```
|
|
67
67
|
|
|
68
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.
|
|
70
|
-
nothing else.
|
|
69
|
+
impression. The version number is cheap; the name is not. The install proof before
|
|
70
|
+
push, and nothing else.
|
data/docs/RUNNING-LOCAL.md
CHANGED
|
@@ -63,7 +63,7 @@ whole surface answers `503`, never open by omission.
|
|
|
63
63
|
| `BIND` | `http://localhost:9292` | host:port |
|
|
64
64
|
| `ADMIN_TOKEN` | `local-demo` | token for `/studio` |
|
|
65
65
|
| `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for the whole `/v1` + `/a2a` surface |
|
|
66
|
-
| `DEEPSEEK_MODEL` | `deepseek-
|
|
66
|
+
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model |
|
|
67
67
|
|
|
68
68
|
With persistence:
|
|
69
69
|
|
data/docs/SECURITY.md
CHANGED
|
@@ -47,16 +47,26 @@ dispatch **any** registered authoring Command (`write_agent_file`, `write_data_t
|
|
|
47
47
|
`upsert_llm_provider`, `update_settings`, `delete_agent`). Treat that token as
|
|
48
48
|
operator-grade — it is not a read key, and a leak is agent takeover, not just usage.
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
### Multi-tenant mode (`INSIKA_TENANCY=multi_tenant`)
|
|
51
|
+
|
|
52
|
+
A single operator-grade token cannot host N stores. With `INSIKA_TENANCY=multi_tenant`
|
|
53
|
+
the Bearer is resolved to a **principal** before the routes:
|
|
54
|
+
|
|
55
|
+
- **Per-tenant tokens** (`POST /v1/commands/issue_tenant_token`) scope a caller to
|
|
56
|
+
one tenant; **operator** tokens (and the legacy gateway token — an existing
|
|
57
|
+
deployment switching modes keeps its credential) have the run of the deployment.
|
|
58
|
+
- Tokens are stored **only as SHA-256 hashes**; the plaintext is shown exactly once,
|
|
59
|
+
at issue time. Rotation (`rotate_tenant_token`) and revocation (`revoke_token`)
|
|
60
|
+
are operator commands — revoking one tenant's token never touches another's.
|
|
61
|
+
- A tenant principal reaches only its **own runtime surfaces** (`/v1/sessions`,
|
|
62
|
+
`/v1/messages`, `/v1/responses`, workflow runs, and its own session/task/event
|
|
63
|
+
reads). Every authoring/provisioning/config surface answers `403` to a tenant.
|
|
64
|
+
- Isolation is the key, not a convention: a tenant's sessions live under
|
|
65
|
+
`<tenant>:<session-id>`, its commands carry `meta.tenant` (memory scoping,
|
|
66
|
+
event tagging), and reading another tenant's session/task reads as `404`.
|
|
67
|
+
|
|
68
|
+
`single_tenant` (the default) is exactly the classic behavior above — one
|
|
69
|
+
operator credential, no principal, no stamping.
|
|
60
70
|
|
|
61
71
|
## The `/v1` contract is versioned by date
|
|
62
72
|
|
|
@@ -172,7 +182,10 @@ moderator off. See [`examples/guardrails/`](https://github.com/guizaols/insika/t
|
|
|
172
182
|
- **Input** — deterministic detectors (prompt-injection, abuse) run *before* the
|
|
173
183
|
model. A flagged input gets a **safe refusal without burning a model turn** — an
|
|
174
184
|
injection or a flood never reaches the provider. An LLM moderator can be layered
|
|
175
|
-
on top.
|
|
185
|
+
on top. The moderator is **fail-open**: an error or an unparseable reply never
|
|
186
|
+
blocks a legitimate customer — but silence is not a negative. That third state
|
|
187
|
+
surfaces as a `:guardrail_flagged` event with category `moderator_unavailable`,
|
|
188
|
+
so a degraded tier is distinguishable from a healthy one in the audit stream.
|
|
176
189
|
- **Output** — moderation plus PII/secret redaction on the streamed response, and
|
|
177
190
|
a post-turn validator.
|
|
178
191
|
|
data/docs/SKILLS.md
CHANGED
|
@@ -22,6 +22,8 @@ See [`examples/skills/`](https://github.com/guizaols/insika/tree/main/examples/s
|
|
|
22
22
|
---
|
|
23
23
|
name: refunds # must equal the directory name
|
|
24
24
|
description: When and how to process a refund # the Level-1 trigger text
|
|
25
|
+
triggers: [refund, money back] # optional: deterministic activation (below)
|
|
26
|
+
companions: [refund-policy] # optional: skills this one cannot work without
|
|
25
27
|
---
|
|
26
28
|
|
|
27
29
|
<the full playbook body — loaded only on demand>
|
|
@@ -33,17 +35,121 @@ which the `name:` field must match.
|
|
|
33
35
|
## Progressive loading: two levels
|
|
34
36
|
|
|
35
37
|
- **Level 1 — metadata only.** A context provider injects an `<available_skills>`
|
|
36
|
-
list
|
|
37
|
-
|
|
38
|
-
present for allowed skills.
|
|
38
|
+
list into the system prompt — the name, the one-line description and the
|
|
39
|
+
`triggers:` of every allowed skill — telling the model to load a skill before
|
|
40
|
+
acting on it. Cheap, and always present for allowed skills. **This is the routing
|
|
41
|
+
table, and it is generated:** it cannot disagree with the allowlist, so do not
|
|
42
|
+
hand-write one in a prompt file (see [Drift guards](#drift-guards)).
|
|
39
43
|
- **Level 2 — the full body.** A built-in `load_skill` tool returns the skill body
|
|
40
44
|
on demand. It enforces the agent's skill allowlist and is wired **automatically**
|
|
41
45
|
whenever the agent has any allowed skills — you do not add it to `tools_allow`.
|
|
46
|
+
- **Deterministic activation — `triggers:`.** When the user message contains one
|
|
47
|
+
of the skill's `triggers`, the body is injected for that turn — no model
|
|
48
|
+
decision, no `load_skill` call. Only matched skills, only that turn. Use it for
|
|
49
|
+
skills that MUST fire on known phrases; model loading stays as the fallback for
|
|
50
|
+
everything else. Matching is on **whole words**, case-insensitive and
|
|
51
|
+
accent-folded: `presente` fires on *um presente* and *presénte*, never inside
|
|
52
|
+
*apresente*.
|
|
42
53
|
|
|
43
54
|
The Level-1 list is budgeted like any other context fragment
|
|
44
55
|
(see [Context](CONTEXT.md)); the Level-2 body only costs tokens on the turns that
|
|
45
56
|
open it.
|
|
46
57
|
|
|
58
|
+
### Only trigger a skill that can finish the turn alone
|
|
59
|
+
|
|
60
|
+
A `triggers:` match is not a hint — the body lands in the prompt with the
|
|
61
|
+
authority of an instruction. So put triggers only on a skill that is
|
|
62
|
+
**self-sufficient** for the turn it fires on.
|
|
63
|
+
|
|
64
|
+
The failure mode is counter-intuitive: injecting a skill that is only *part* of
|
|
65
|
+
the answer is **worse than injecting nothing**. Give the model a reference table
|
|
66
|
+
whose procedure lives in a companion skill, and it now holds a plausible
|
|
67
|
+
half-recipe — so it never calls `load_skill` for the other half, and improvises
|
|
68
|
+
the missing part. A precise trigger on the wrong kind of skill still breaks the
|
|
69
|
+
turn.
|
|
70
|
+
|
|
71
|
+
Reference tables, vocabularies and lookup maps are the skills to leave on
|
|
72
|
+
level 1. Whole procedures ("run this journey", "recover from this error") are the
|
|
73
|
+
ones worth triggering.
|
|
74
|
+
|
|
75
|
+
## Always-on skills: `skills_eager`
|
|
76
|
+
|
|
77
|
+
A skill that every turn needs — output format, the marker vocabulary, how to
|
|
78
|
+
recover from a failed tool — should not depend on the model choosing to load it.
|
|
79
|
+
Name it on the **agent** and its body is in the prompt on every turn:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Insika.agent("consultant") do
|
|
83
|
+
skills_eager "recommendation-formatting", "tool-error-recovery"
|
|
84
|
+
# skills_eager # or: every allowed skill (a corpus that fits the budget)
|
|
85
|
+
# skills_eager false # or: none — the default
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
An eager skill also **leaves level 1**: it is absent from `<available_skills>` and
|
|
90
|
+
`load_skill` refuses to serve it. There is no level 2 left to fetch, and a catalog
|
|
91
|
+
pointing at a body already in the prompt only invites a call that pays for a
|
|
92
|
+
duplicate.
|
|
93
|
+
|
|
94
|
+
### Why the agent decides, and not the skill
|
|
95
|
+
|
|
96
|
+
Eagerness used to be an `eager: true` key in the `SKILL.md` frontmatter. That put
|
|
97
|
+
the decision on the wrong object: **skills are shared.** `escalation-to-human`,
|
|
98
|
+
`recommendation-formatting` and `tool-error-recovery` each sit in several agents'
|
|
99
|
+
allowlists, and one flag on the skill forced one decision onto every agent holding
|
|
100
|
+
it — with no way to be always-on for the agent that needs it and discretionary for
|
|
101
|
+
the one that does not.
|
|
102
|
+
|
|
103
|
+
`skills_eager` is a per-agent list, so the same shared skill can be both. The
|
|
104
|
+
frontmatter key is **ignored** — `insika doctor` flags any skill still carrying it,
|
|
105
|
+
and names the agent setting that replaced it.
|
|
106
|
+
|
|
107
|
+
A name that is not in the agent's `skills` allowlist is a no-op (eagerness is
|
|
108
|
+
intersected with what the agent is allowed to see); `doctor` flags that too.
|
|
109
|
+
|
|
110
|
+
### Keep the discretionary skills on the load path
|
|
111
|
+
|
|
112
|
+
Making everything eager is a trap, and the reason is not the tokens: **it costs you
|
|
113
|
+
the signal**. When every body is present on every turn, "which skills were active"
|
|
114
|
+
is always "all of them", and you can no longer tell which one the model reached for.
|
|
115
|
+
The `load_skill` call is the only record of that choice — it is a persisted tool
|
|
116
|
+
message, so it shows up in the transcript on its own.
|
|
117
|
+
|
|
118
|
+
So the split is: **eager for what the turn always needs, `load_skill` for what the
|
|
119
|
+
turn might need.** The second group is where you want the model's choice on the
|
|
120
|
+
record, because that is the group where a wrong choice is worth seeing.
|
|
121
|
+
|
|
122
|
+
The token trade is real but smaller than it looks: eager bodies sit at a fixed
|
|
123
|
+
position ahead of the history, so they belong to the **cacheable prefix**, and they
|
|
124
|
+
are still evictable under budget pressure, unlike the pinned identity. Conditional
|
|
125
|
+
injection is what breaks that prefix, on exactly the turns it fires.
|
|
126
|
+
|
|
127
|
+
## Seeing which skills were active, and why
|
|
128
|
+
|
|
129
|
+
The load path is legible for free: `load_skill` is a tool, so the call is a
|
|
130
|
+
persisted message and shows up in the transcript on its own. The deterministic paths
|
|
131
|
+
are not a call, so the engine reports them itself — **with a reason per skill**:
|
|
132
|
+
|
|
133
|
+
| reason | what it means |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `eager` | the agent's `skills_eager` names it, so every turn gets it |
|
|
136
|
+
| `trigger:<phrase>` | this message matched that `triggers:` entry — the phrase as **authored**, so you can find the line to edit |
|
|
137
|
+
| `pack` | a plugin's own context provider supplied the body |
|
|
138
|
+
|
|
139
|
+
Where it shows up, per turn, in the Studio session screen:
|
|
140
|
+
|
|
141
|
+
- an **activation card in the transcript thread**, placed at the top of its turn and
|
|
142
|
+
in the same visual language as a tool result, so a context-injected skill and a
|
|
143
|
+
model-loaded one read the same way;
|
|
144
|
+
- the **Context card**, next to that category's token count, for the after-the-fact
|
|
145
|
+
audit;
|
|
146
|
+
- the `skill_activated` **event** (`skills: [{name, reason}]`, `source: "context"`),
|
|
147
|
+
with full task/session correlation.
|
|
148
|
+
|
|
149
|
+
All three are computed from what actually reached the prompt **after the budget
|
|
150
|
+
cut**: a body the budget evicted is reported as an eviction, never as an activation.
|
|
151
|
+
A turn that mixes both paths is labelled `mixed`, and each line keeps its own reason.
|
|
152
|
+
|
|
47
153
|
## Where skills live: the store, over a disk seed
|
|
48
154
|
|
|
49
155
|
- Skills live as **rows in SQLite** — one row per skill, holding the entire
|
|
@@ -58,6 +164,68 @@ open it.
|
|
|
58
164
|
> runtime operation (Studio / API / DSL), not a commit. See
|
|
59
165
|
> [Context](CONTEXT.md#the-volume).
|
|
60
166
|
|
|
167
|
+
## Pairs that must not break: `companions:`
|
|
168
|
+
|
|
169
|
+
Injecting *part* of an answer is worse than injecting nothing. Give the model a line
|
|
170
|
+
map whose query-construction rules live in another skill and it holds a plausible
|
|
171
|
+
half-recipe — so it never calls `load_skill` for the other half, and improvises the
|
|
172
|
+
missing part. Measured on a real pack: the map arrived by trigger, the rules did not,
|
|
173
|
+
and the searches came out malformed. Twice.
|
|
174
|
+
|
|
175
|
+
Declare the dependency and it travels with whatever brought it — a trigger match, the
|
|
176
|
+
agent's eager set, or a `load_skill` call (which returns both bodies in the one call):
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
companions: [query-construction]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Two deliberate limits:
|
|
183
|
+
|
|
184
|
+
- **One level, no transitive walk.** A cycle would be a hang and a chain a budget
|
|
185
|
+
blowout, and "cannot work without" is a direct relationship.
|
|
186
|
+
- **Never widens an allowlist.** A companion the agent is not allowed to load is
|
|
187
|
+
simply absent; `insika doctor` flags the declaration instead.
|
|
188
|
+
|
|
189
|
+
## Specializing a shared skill for one agent
|
|
190
|
+
|
|
191
|
+
Skills are shared on purpose: `escalation-to-human` belongs in several agents'
|
|
192
|
+
allowlists. But sometimes one agent needs a different version of the same skill —
|
|
193
|
+
its own return policy, its own store name — and forking it under a second name
|
|
194
|
+
throws the sharing away and leaves two things to keep in step.
|
|
195
|
+
|
|
196
|
+
So the store has a second scope, and resolution is a **precedence chain** with one
|
|
197
|
+
more dimension:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
for agent A, skill <name>: (A, <name>) in the agent scope,
|
|
201
|
+
then <name> in the shared scope
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Three cases fall out of that one rule:
|
|
205
|
+
|
|
206
|
+
| case | what exists in the store |
|
|
207
|
+
|---|---|
|
|
208
|
+
| **shared** | only the shared record — every agent gets the same body |
|
|
209
|
+
| **override** | both — the agent's wins, for that agent only |
|
|
210
|
+
| **agent-private** | only the agent record — invisible elsewhere, and the name may collide freely |
|
|
211
|
+
|
|
212
|
+
The **name never changes.** An override keeps saying `name: escalation-to-human`
|
|
213
|
+
inside, because it *is* that skill, specialized; the allowlist, the `<available_skills>`
|
|
214
|
+
list, `load_skill` and the activation card all keep showing the bare name. What
|
|
215
|
+
decides which record you get is its **position in the store**, never the frontmatter —
|
|
216
|
+
otherwise an override would clobber the shared skill for everybody.
|
|
217
|
+
|
|
218
|
+
Write one with `agent:`, and remove it the same way (which un-specializes, leaving
|
|
219
|
+
the shared skill in place):
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
dispatch(:write_skill, { name: "escalation-to-human", agent: "store-cacau", content: md })
|
|
223
|
+
dispatch(:delete_skill, { name: "escalation-to-human", agent: "store-cacau" })
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
In the Studio: **Skills → specialize for this agent**, which seeds the override from
|
|
227
|
+
the shared body.
|
|
228
|
+
|
|
61
229
|
## Making a new skill "show up"
|
|
62
230
|
|
|
63
231
|
For an agent to actually use a skill, **both** conditions must hold:
|
|
@@ -89,6 +257,24 @@ Two ways to satisfy both:
|
|
|
89
257
|
`load_skill` errors → the store row is missing or misnamed (condition 1); the
|
|
90
258
|
`name:` frontmatter must equal the directory name.
|
|
91
259
|
|
|
260
|
+
## Drift guards
|
|
261
|
+
|
|
262
|
+
A skill catalog drifts against the prose that routes to it, and every way it happened
|
|
263
|
+
on the pilot was silent — found by reading a customer conversation days later. So the
|
|
264
|
+
routing table is **generated** (above), and `insika doctor` reports the residue the
|
|
265
|
+
generator cannot remove. Every check takes mechanical inputs only — names, allowlists,
|
|
266
|
+
agent identities — because one false positive is enough for an operator to stop
|
|
267
|
+
reading the doctor:
|
|
268
|
+
|
|
269
|
+
| finding | what it means |
|
|
270
|
+
|---|---|
|
|
271
|
+
| a prompt file names a skill outside that agent's allowlist | leftover hand-written routing: the model is told to use something it cannot load |
|
|
272
|
+
| a shared skill's body names one of its own holders | specialized text in shared clothing — the other holders are served that store's policy as their own. Specialize it instead |
|
|
273
|
+
| a body references another catalog skill without declaring it a companion | the pair can still arrive apart |
|
|
274
|
+
| a declared companion is outside an agent's allowlist | the pair cannot travel for that agent, and the engine will not widen the allowlist |
|
|
275
|
+
| a skill still declares `eager:` in its frontmatter | the key is ignored; the decision moved to the agent |
|
|
276
|
+
| an agent marks a skill eager that it does not allow | the name is a no-op |
|
|
277
|
+
|
|
92
278
|
## See also
|
|
93
279
|
|
|
94
280
|
- [Context](CONTEXT.md) — how the skills list is budgeted into a turn.
|
data/docs/WHY.md
CHANGED
|
@@ -32,7 +32,7 @@ the same speed-to-first-agent story Python teams tell, without leaving Ruby:
|
|
|
32
32
|
require "insika"
|
|
33
33
|
|
|
34
34
|
agent = Insika.agent("assistant") do
|
|
35
|
-
model "deepseek-
|
|
35
|
+
model "deepseek-v4-flash"
|
|
36
36
|
provider :deepseek
|
|
37
37
|
instructions "You are a concise, friendly assistant."
|
|
38
38
|
end
|
data/docs/WORKFLOWS.md
CHANGED
|
@@ -40,8 +40,8 @@ agents by id and those agents must resolve in the same runtime:
|
|
|
40
40
|
newsroom = Insika.system do
|
|
41
41
|
provider :deepseek
|
|
42
42
|
|
|
43
|
-
agent("writer") { model "deepseek-
|
|
44
|
-
agent("editor") { model "deepseek-
|
|
43
|
+
agent("writer") { model "deepseek-v4-flash"; instructions "Write ONE paragraph." }
|
|
44
|
+
agent("editor") { model "deepseek-v4-flash"; instructions "Rewrite as ONE sentence." }
|
|
45
45
|
|
|
46
46
|
workflow "publish",
|
|
47
47
|
description: "Draft a paragraph, then tighten it.",
|
data/docs/index.md
CHANGED
|
@@ -34,7 +34,7 @@ Ruby `>= 3.3` and a provider key (the demo uses DeepSeek). The whole program:
|
|
|
34
34
|
require "insika"
|
|
35
35
|
|
|
36
36
|
assistant = Insika.agent("assistant") do
|
|
37
|
-
model "deepseek-
|
|
37
|
+
model "deepseek-v4-flash"
|
|
38
38
|
provider :deepseek
|
|
39
39
|
instructions "You are Bia, a concise and friendly assistant. Answer briefly."
|
|
40
40
|
end
|
data/docs/onboarding/start.md
CHANGED
|
@@ -51,7 +51,7 @@ require "insika"
|
|
|
51
51
|
|
|
52
52
|
assistant = Insika.agent("assistant") do
|
|
53
53
|
provider :deepseek # ← the provider slug from {{MODELS_URL}}
|
|
54
|
-
model "deepseek-
|
|
54
|
+
model "deepseek-v4-flash" # ← a model id from {{MODELS_URL}}
|
|
55
55
|
instructions "You are a concise, friendly assistant. Answer briefly."
|
|
56
56
|
end
|
|
57
57
|
|