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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Observability
|
|
3
|
+
parent: Operate & prove it
|
|
4
|
+
nav_order: 1
|
|
5
|
+
permalink: /observability/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Observability — OpenTelemetry (opt-in)
|
|
9
|
+
|
|
10
|
+
Insika already has an observability spine: the **event stream**. Every turn emits
|
|
11
|
+
structured events (`task_started`, `tool_call`/`tool_result`, `data_tool_call`,
|
|
12
|
+
`task_completed`/`task_failed`/`task_cancelled`), each stamped with
|
|
13
|
+
`task_id`/`session_id`/`seq`/`at`. The OpenTelemetry bridge is a **consumer** of
|
|
14
|
+
that stream: it translates the events into OTEL **spans** and **metrics**. The core
|
|
15
|
+
(Executor, tools) never gains an OTEL call — events observe, telemetry translates.
|
|
16
|
+
It's the same "events observe" principle the SSE surface already uses.
|
|
17
|
+
|
|
18
|
+
Not every event on the stream is a turn. Operator actions, refinement runs
|
|
19
|
+
(`:refinement_started`, `:refinement_report`, `:refinement_proposed`,
|
|
20
|
+
`:refinement_gated`, `:refinement_applied`, `:refinement_rejected` — see
|
|
21
|
+
[Refinement](REFINEMENT.md)),
|
|
22
|
+
authoring writes (`:golden_written`, `:agent_file_written`, …), queue bookkeeping
|
|
23
|
+
(`:turn_coalesced`, `:turn_steered`, `:turn_steer_released`, `:turn_interrupted` — see
|
|
24
|
+
[Agents](AGENTS.md#queue_mode--when-a-message-arrives-while-the-agent-is-busy)) and
|
|
25
|
+
channel delivery (`:channel_delivered` — see [Channels](CHANNELS.md))
|
|
26
|
+
travel the same stream and are **ignored** by the bridge: they open no span and
|
|
27
|
+
touch no instrument, because they are not part of a turn's latency or cost. Any
|
|
28
|
+
other subscriber still sees them.
|
|
29
|
+
|
|
30
|
+
They are worth subscribing to even so, because each is the ONLY record of
|
|
31
|
+
something that left no task of its own behind:
|
|
32
|
+
|
|
33
|
+
| Event | Data | What it answers |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `:turn_coalesced` | `task_id`, `merged`, `arrivals[]` | the fragments a customer typed in a row arrived as separate messages, and when |
|
|
36
|
+
| `:turn_steered` | `task_id`, `count`, `total` | a message arrived mid-run and was appended to the turn in flight |
|
|
37
|
+
| `:turn_steer_released` | `task_id`, `released_as`, `count` | the run could not absorb it, so it became the turn `released_as` |
|
|
38
|
+
| `:turn_interrupted` | `task_id`, `replaced_by` | the turn was abandoned mid-run, and which turn replaced it |
|
|
39
|
+
| `:turn_stuck` | `task_id`, `agent`, `reason`, `message` | the agent declared it could not proceed (`signal_stuck`, WS5) — the deterministic signal a consumer escalates on |
|
|
40
|
+
| `:channel_delivered` | `channel`, `outbox_id`, `status`, `attempts`, `error` | the answer reached the platform (or did not) — the turn completing says nothing about that |
|
|
41
|
+
| `:delivery_failed` | `channel`, `outbox_id`, `status`, `attempts`, `error` | a delivery exhausted its bounded retries — the alert face of the row above (WS6) |
|
|
42
|
+
| `:budget_warning` | `agent`, `tenant`, `window`, `spent`, `cap` | a calendar budget crossed its threshold (`alert_at` or a soft cap) — once per window (WS2) |
|
|
43
|
+
| `:breaker_open` | `agent`, `ref`, `tenant` | the reliability circuit breaker tripped for a `(tenant, provider/model)` — further turns fail fast until the cooldown (WS3/WS6) |
|
|
44
|
+
| `:ttft` | `task_id`, `session_id`, `ttft_ms` | the provider's time-to-first-token on the streaming envelope — only under `INSIKA_TURN_TIMING`, once per turn (WS6) |
|
|
45
|
+
|
|
46
|
+
`delivery_failed` and `breaker_open` are the two the operator config is pointed at
|
|
47
|
+
(`alerts.webhook` on the profile): each only fires when something durable did
|
|
48
|
+
not land. `:ttft` is additive debug, absent unless `INSIKA_TURN_TIMING` is set.
|
|
49
|
+
|
|
50
|
+
`:channel_delivered` is the one worth alerting on: a turn can be `:task_completed`
|
|
51
|
+
and correct while the customer got nothing, because delivery is a separate,
|
|
52
|
+
retried, out-of-band step. `status: "failed"` means the reply is sitting in the
|
|
53
|
+
outbox and the customer is still waiting.
|
|
54
|
+
|
|
55
|
+
Counts, ids and times only — never message content. The text lives in the
|
|
56
|
+
transcript, which is the surface that is allowed to carry it.
|
|
57
|
+
|
|
58
|
+
The bridge speaks the standard the market already runs on: point any OTLP backend
|
|
59
|
+
at Insika and a real turn shows up as a full trace, next to counters and histograms
|
|
60
|
+
you can chart without touching a span.
|
|
61
|
+
|
|
62
|
+
**This page is a convention, not an integration.** Insika ships no dashboard, no
|
|
63
|
+
backend config, no vendor file. It ships a stable set of attribute and instrument
|
|
64
|
+
names, and the recipes below tell you what to chart against them — in whatever you
|
|
65
|
+
already run.
|
|
66
|
+
|
|
67
|
+
## Contents
|
|
68
|
+
|
|
69
|
+
- [Turning it on](#turning-it-on-opt-in-parity-when-off)
|
|
70
|
+
- [Traces: the span reference](#traces-the-span-reference)
|
|
71
|
+
- [Metrics: the instrument reference](#metrics-the-instrument-reference)
|
|
72
|
+
- [Attribute reference](#attribute-reference)
|
|
73
|
+
- [Estimated cost](#estimated-cost)
|
|
74
|
+
- [Dashboards you can build](#dashboards-you-can-build)
|
|
75
|
+
- [Stability contract](#stability-contract)
|
|
76
|
+
- [Local: a standalone collector](#local-a-standalone-collector)
|
|
77
|
+
- [Production](#production)
|
|
78
|
+
- [Design (why it's safe)](#design-why-its-safe)
|
|
79
|
+
|
|
80
|
+
## Turning it on (opt-in, parity when off)
|
|
81
|
+
|
|
82
|
+
Enabled by environment — no new code flag:
|
|
83
|
+
|
|
84
|
+
- `INSIKA_OTEL=1`, **or**
|
|
85
|
+
- the standard OTEL envs (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_TRACES_EXPORTER`).
|
|
86
|
+
|
|
87
|
+
Destination, protocol and headers follow the **OTEL SDK's default config** (env):
|
|
88
|
+
point `OTEL_EXPORTER_OTLP_ENDPOINT` at your collector. `OTEL_SERVICE_NAME` names
|
|
89
|
+
the service (default `insika`).
|
|
90
|
+
|
|
91
|
+
Metrics ride the same switch and the same standard env — no Insika-specific toggle
|
|
92
|
+
is invented for them. `OTEL_METRICS_EXPORTER=none` turns metrics off while traces
|
|
93
|
+
stay on; `OTEL_METRIC_EXPORT_INTERVAL` (ms) sets the export period. If the metrics
|
|
94
|
+
SDK is not in the bundle at all, the bridge degrades to traces only rather than
|
|
95
|
+
failing to boot.
|
|
96
|
+
|
|
97
|
+
**Off (the default):** `Insika::Telemetry.setup` returns `nil`, the OTEL gems are
|
|
98
|
+
**never loaded** (lazy `require`, like the LLM client), and nothing is
|
|
99
|
+
instrumented — zero overhead. This is enforced by a test
|
|
100
|
+
(`spec/insika/load_guard_spec.rb`: "require insika does not load
|
|
101
|
+
OpenTelemetry").
|
|
102
|
+
|
|
103
|
+
## Traces: the span reference
|
|
104
|
+
|
|
105
|
+
One span per **turn**, with **child spans** per tool, correlated by `task_id`.
|
|
106
|
+
Latency is the span duration, reconstructed from the events' real `at` timestamps.
|
|
107
|
+
|
|
108
|
+
| Span | Emitted for | Parent |
|
|
109
|
+
|------|-------------|--------|
|
|
110
|
+
| `insika.turn` | one per turn, opened on `task_started`, closed on the terminal event | root |
|
|
111
|
+
| `insika.tool` | one per tool call, `tool_call` → `tool_result` (FIFO-correlated) | `insika.turn` |
|
|
112
|
+
| `insika.data_tool` | one per data-tool call — point-in-time (the engine emits a single event) | `insika.turn` |
|
|
113
|
+
|
|
114
|
+
A turn that ends with `task_failed` also carries the OTEL **error status**, with
|
|
115
|
+
the failure message.
|
|
116
|
+
|
|
117
|
+
## Metrics: the instrument reference
|
|
118
|
+
|
|
119
|
+
The same events feed instruments, so volume, latency, tokens and cost are chartable
|
|
120
|
+
**without aggregating spans** — which not every backend does, and none does cheaply
|
|
121
|
+
at retention. Metrics are recorded on the *terminal* event, so every point already
|
|
122
|
+
knows its outcome.
|
|
123
|
+
|
|
124
|
+
| Instrument | Type | Unit | Recorded when |
|
|
125
|
+
|------------|------|------|----------------|
|
|
126
|
+
| `insika.turns` | counter | `{turn}` | a turn reaches a terminal state |
|
|
127
|
+
| `insika.turn.duration` | histogram | `s` | same, when both timestamps are known |
|
|
128
|
+
| `insika.tokens` | counter | `{token}` | the turn reported usage |
|
|
129
|
+
| `insika.cost` | counter | `{USD}` | the turn's model is priced (see below) |
|
|
130
|
+
| `insika.tool.calls` | counter | `{call}` | a tool call completes |
|
|
131
|
+
| `insika.tool.duration` | histogram | `s` | a `tool_call`/`tool_result` pair completes |
|
|
132
|
+
|
|
133
|
+
`insika.tool.duration` is deliberately **not** recorded for data-tools: those are a
|
|
134
|
+
single point-in-time event, so there is no measured duration to report. A tool left
|
|
135
|
+
open by a mid-turn failure is not counted as a completed call either — its span is
|
|
136
|
+
closed, but a failed call must not inflate the success histogram.
|
|
137
|
+
|
|
138
|
+
## Attribute reference
|
|
139
|
+
|
|
140
|
+
The same names are used on spans and on metrics. **Metrics carry a deliberate
|
|
141
|
+
low-cardinality subset** — `task_id` and `session_id` are span-only, because a
|
|
142
|
+
metric attribute with per-turn cardinality is how you destroy a metrics backend.
|
|
143
|
+
|
|
144
|
+
| Attribute | Type | On spans | On metrics | Meaning |
|
|
145
|
+
|-----------|------|----------|------------|---------|
|
|
146
|
+
| `insika.task_id` | string | `turn` | — | the turn's id (correlates with `/v1/responses`, the Studio, the task store) |
|
|
147
|
+
| `insika.session_id` | string | `turn` | — | the chat this turn belongs to |
|
|
148
|
+
| `insika.agent` | string | `turn` | all | the agent profile that ran the turn |
|
|
149
|
+
| `insika.tenant` | string | `turn` | all | the **operator-set** tenant (see below); absent when the command declared none |
|
|
150
|
+
| `insika.command` | string | `turn` | all | command type (`send_message`, `trigger_workflow`, …) |
|
|
151
|
+
| `insika.status` | string | `turn` | turn instruments | `ok` / `error` / `cancelled` / `abandoned` |
|
|
152
|
+
| `insika.model` | string | `turn` | all except tool | model id the provider reported |
|
|
153
|
+
| `insika.model_source` | string | `turn` | — | which config layer resolved the model (chat / agent / model / global) |
|
|
154
|
+
| `insika.tokens.input` | int | `turn` | — | input tokens (**includes** the cached ones) |
|
|
155
|
+
| `insika.tokens.output` | int | `turn` | — | output tokens |
|
|
156
|
+
| `insika.tokens.total` | int | `turn` | — | input + output |
|
|
157
|
+
| `insika.tokens.cached` | int | `turn` | — | cache **reads**, a subset of `tokens.input` |
|
|
158
|
+
| `insika.tokens.cache_creation` | int | `turn` | — | cache **writes**, *not* inside `tokens.input` |
|
|
159
|
+
| `insika.token.type` | string | — | `insika.tokens` | `input` / `output` / `cached` / `cache_creation` |
|
|
160
|
+
| `insika.cost.usd` | double | `turn` | — | estimated cost of the turn (span attribute; the metric is the `insika.cost` counter) |
|
|
161
|
+
| `insika.tool` | string | `tool`, `data_tool` | tool instruments | tool name |
|
|
162
|
+
| `insika.tool.kind` | string | — | tool instruments | `tool` / `data_tool` |
|
|
163
|
+
| `insika.http.status` | int | `data_tool` | `insika.tool.calls` | HTTP status the data-tool got back |
|
|
164
|
+
|
|
165
|
+
### `insika.tenant`
|
|
166
|
+
|
|
167
|
+
The tenant is the **explicit** tenant of the Command (`Command.build(…, tenant:)`)
|
|
168
|
+
— the one grouping label an operator sets deliberately, typically the merchant,
|
|
169
|
+
workspace or customer the turn belongs to. It is **not** the memory scope, which
|
|
170
|
+
falls back to the chat id: putting per-chat cardinality on a metric attribute is
|
|
171
|
+
exactly the failure this distinction avoids. A command with no tenant emits **no
|
|
172
|
+
attribute at all**, rather than a null or an `"unknown"` bucket.
|
|
173
|
+
|
|
174
|
+
### Cardinality budget
|
|
175
|
+
|
|
176
|
+
Metric cardinality is roughly `agents × tenants × models × statuses × commands`
|
|
177
|
+
(and `agents × tenants × tools` for the tool instruments). All of those are
|
|
178
|
+
operator-controlled and closed-ish sets. Keep them that way: if you find yourself
|
|
179
|
+
wanting per-chat or per-user metrics, that is a **trace** query, and the span
|
|
180
|
+
attributes are there for it.
|
|
181
|
+
|
|
182
|
+
## Estimated cost
|
|
183
|
+
|
|
184
|
+
Insika ships **no prices**. They change weekly, differ per contract and per region,
|
|
185
|
+
and a stale table inside the engine would be worse than no number. You declare the
|
|
186
|
+
rates; Insika multiplies. Unset, no cost is reported anywhere.
|
|
187
|
+
|
|
188
|
+
Set `INSIKA_MODEL_PRICING` to a JSON object of model id → rates in **USD per
|
|
189
|
+
million tokens**:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
INSIKA_MODEL_PRICING='{
|
|
193
|
+
"deepseek-v4-flash": {"input": 0.27, "output": 1.10, "cached_input": 0.07},
|
|
194
|
+
"claude-sonnet-4-5": {"input": 3.00, "output": 15.00, "cached_input": 0.30, "cache_write": 3.75}
|
|
195
|
+
}'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- A key matches the model id the provider reports, **with or without** the
|
|
199
|
+
`provider/` prefix — `deepseek/deepseek-v4-flash` and `deepseek-v4-flash` both hit the
|
|
200
|
+
same entry.
|
|
201
|
+
- `input` / `output` are required (one of them is enough for the entry to load).
|
|
202
|
+
- `cached_input`, when given, bills cache **reads** at that rate and subtracts them
|
|
203
|
+
from the fresh input. Omit it and cached tokens simply stay at the input rate.
|
|
204
|
+
- `cache_write`, when given, bills cache **creation** tokens at that rate. Omit it
|
|
205
|
+
and they are billed at the input rate.
|
|
206
|
+
- An **unpriced model reports nothing** — no attribute, no metric point. A missing
|
|
207
|
+
price is not a zero cost, and a dashboard should show the gap.
|
|
208
|
+
- A malformed table degrades to "no cost". Telemetry config can never stop a boot.
|
|
209
|
+
|
|
210
|
+
The number is an **estimate for trend and attribution**, not a bill. Reconcile
|
|
211
|
+
against your provider's invoice, never the other way round.
|
|
212
|
+
|
|
213
|
+
## Dashboards you can build
|
|
214
|
+
|
|
215
|
+
Written against the convention, not against a product. Each recipe is
|
|
216
|
+
*instrument → aggregation → group-by*; the PromQL line is one illustration of the
|
|
217
|
+
shape, and translates directly to whatever query language your backend uses.
|
|
218
|
+
|
|
219
|
+
> **Names get normalized.** Prometheus-style backends rewrite OTLP names: dots
|
|
220
|
+
> become underscores, counters gain `_total`, and a real (non-annotation) unit is
|
|
221
|
+
> appended — so `insika.turn.duration` in `s` becomes
|
|
222
|
+
> `insika_turn_duration_seconds`. Annotation units like `{turn}` are dropped.
|
|
223
|
+
> Check your exporter's mapping; the convention below is the OTLP spelling.
|
|
224
|
+
|
|
225
|
+
**Turn volume by agent**
|
|
226
|
+
`insika.turns`, rate, grouped by `insika.agent`.
|
|
227
|
+
```promql
|
|
228
|
+
sum by (insika_agent) (rate(insika_turns_total[5m]))
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Error rate by agent** — the single most useful panel.
|
|
232
|
+
`insika.turns` filtered to `insika.status="error"`, over the same counter unfiltered.
|
|
233
|
+
```promql
|
|
234
|
+
sum by (insika_agent) (rate(insika_turns_total{insika_status="error"}[5m]))
|
|
235
|
+
/ sum by (insika_agent) (rate(insika_turns_total[5m]))
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Latency p95 by agent**
|
|
239
|
+
`insika.turn.duration`, 95th percentile, grouped by `insika.agent`.
|
|
240
|
+
```promql
|
|
241
|
+
histogram_quantile(0.95, sum by (le, insika_agent) (rate(insika_turn_duration_seconds_bucket[5m])))
|
|
242
|
+
```
|
|
243
|
+
Turn latency is dominated by the provider, not by the engine — see
|
|
244
|
+
[BENCHMARK.md](BENCHMARK.md) for the engine's own overhead, which is sub-millisecond
|
|
245
|
+
and will not show up here.
|
|
246
|
+
|
|
247
|
+
**Token burn by model**
|
|
248
|
+
`insika.tokens`, rate, grouped by `insika.model` and `insika.token.type`. Splitting
|
|
249
|
+
by type is what makes the cache visible: a healthy prompt cache shows `cached`
|
|
250
|
+
climbing while `input` stays flat.
|
|
251
|
+
|
|
252
|
+
**Cache hit ratio**
|
|
253
|
+
`insika.tokens` filtered to `insika.token.type="cached"` over the same counter
|
|
254
|
+
filtered to `input`. This is the number that moves your bill.
|
|
255
|
+
|
|
256
|
+
**Spend per tenant**
|
|
257
|
+
`insika.cost`, rate (or `increase` over a billing window), grouped by
|
|
258
|
+
`insika.tenant`. Swap the group-by for `insika.agent` to get spend per agent — the
|
|
259
|
+
same attribution the per-agent token ceilings in [SECURITY.md](SECURITY.md) act on.
|
|
260
|
+
|
|
261
|
+
**Tool reliability**
|
|
262
|
+
`insika.tool.calls`, rate, grouped by `insika.tool`; for data-tools add
|
|
263
|
+
`insika.http.status` to see which upstream is failing. Pair it with
|
|
264
|
+
`insika.tool.duration` p95 grouped by `insika.tool` to find the slow one.
|
|
265
|
+
|
|
266
|
+
**Workflows vs chats**
|
|
267
|
+
Any turn instrument grouped by `insika.command` — `trigger_workflow` and
|
|
268
|
+
`send_message` have very different latency and token profiles, and mixing them in
|
|
269
|
+
one average hides both.
|
|
270
|
+
|
|
271
|
+
**From a chart to the actual conversation**
|
|
272
|
+
Every panel above is grouped by attributes that also exist on the `insika.turn`
|
|
273
|
+
span. Filter your trace view by the same `insika.agent` / `insika.tenant` /
|
|
274
|
+
`insika.status`, open a trace, and `insika.task_id` and `insika.session_id` take you
|
|
275
|
+
to the exact turn in the Studio.
|
|
276
|
+
|
|
277
|
+
## Stability contract
|
|
278
|
+
|
|
279
|
+
These names are an interface. Dashboards, alerts and recording rules are built on
|
|
280
|
+
top of them, and renaming one breaks all of them silently — a chart does not error,
|
|
281
|
+
it just goes flat.
|
|
282
|
+
|
|
283
|
+
- Instrument names, units and attribute keys on this page are **stable**. They
|
|
284
|
+
change only in a major version, and only with a note in `CHANGELOG.md`.
|
|
285
|
+
- Growth is **additive**: new attributes and new instruments may appear in a minor
|
|
286
|
+
version. Do not write a query that assumes a fixed attribute set.
|
|
287
|
+
- An attribute whose value is unknown is **omitted**, never emitted as `null`,
|
|
288
|
+
`""` or `"unknown"`. Handle absence in your queries rather than expecting a
|
|
289
|
+
placeholder bucket.
|
|
290
|
+
- Everything under `insika.*` is ours. Standard OTEL resource attributes
|
|
291
|
+
(`service.name`, and so on) come from the SDK and follow OTEL's own conventions.
|
|
292
|
+
|
|
293
|
+
## Local: a standalone collector
|
|
294
|
+
|
|
295
|
+
To see traces on your machine, run a standalone OTLP collector — the quickest is
|
|
296
|
+
Jaeger all-in-one (OTLP on `4318`, UI on `16686`):
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
docker run --rm -d --name jaeger \
|
|
300
|
+
-p 16686:16686 -p 4318:4318 \
|
|
301
|
+
jaegertracing/all-in-one:latest
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Then boot the engine with OTEL enabled, pointed at the collector:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
INSIKA_OTEL=1 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
|
|
308
|
+
DEEPSEEK_API_KEY=sk-... bundle exec ruby scripts/serve_real.rb
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`serve_real` prints `OTEL → on (traces + metrics to OTLP)` on boot. Chat at
|
|
312
|
+
`http://localhost:9292/studio` and open the traces at
|
|
313
|
+
**`http://localhost:16686`** (service `insika`): one `insika.turn` span per turn,
|
|
314
|
+
with `insika.tool`/`insika.data_tool` children and the attributes above. Stop the
|
|
315
|
+
collector with `docker rm -f jaeger`.
|
|
316
|
+
|
|
317
|
+
Jaeger stores traces only — to see the metrics too, point the same endpoint at an
|
|
318
|
+
OTLP collector that fans out to a metrics store, or add `OTEL_METRICS_EXPORTER=none`
|
|
319
|
+
to silence the metrics exporter's retries while you work on traces.
|
|
320
|
+
|
|
321
|
+
> We don't version a `docker-compose` file — the collector is standalone, a
|
|
322
|
+
> local run-it decision. The one-liner above is the whole recipe.
|
|
323
|
+
|
|
324
|
+
## Production
|
|
325
|
+
|
|
326
|
+
Set the OTEL envs on the deployment and every worker exports to your collector:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
INSIKA_OTEL=1
|
|
330
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-collector>:4318
|
|
331
|
+
OTEL_SERVICE_NAME=insika # optional; names the service
|
|
332
|
+
INSIKA_MODEL_PRICING='{...}' # optional; unlocks the cost attribute + counter
|
|
333
|
+
# plus any standard OTEL_EXPORTER_OTLP_HEADERS your backend needs
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The bridge is attached once, at boot, on the serving reactor — in `config.ru`,
|
|
337
|
+
`scripts/serve_real.rb` and the DSL's `serve` (so `Insika.agent { … }.serve` exports
|
|
338
|
+
too). It subscribes to the event stream in a long-lived fiber, sibling to serving.
|
|
339
|
+
|
|
340
|
+
## Design (why it's safe)
|
|
341
|
+
|
|
342
|
+
- **`Telemetry::Recorder`** — pure: translates event → spans and instruments against
|
|
343
|
+
a duck-typed `tracer` (`start_span`/`set_attribute`/`record_error`/`finish`) and
|
|
344
|
+
`meter` (`create_counter`/`create_histogram`). It does not reference
|
|
345
|
+
`OpenTelemetry::` and is unit-tested with fakes. `record` **never raises**
|
|
346
|
+
(telemetry must not bring down a turn); orphan events are ignored; tool
|
|
347
|
+
correlation is FIFO; abandoned turns (e.g. `kill -9`, no terminal event) are
|
|
348
|
+
bounded and evicted (`MAX_OPEN`) and counted as `status="abandoned"`.
|
|
349
|
+
- **`Telemetry::Pricing`** — pure: an operator-declared rates table times the turn's
|
|
350
|
+
usage. No network, no bundled price list, no exception path.
|
|
351
|
+
- **`Telemetry.setup`** — the gem boundary: lazy `require` + configures the SDK +
|
|
352
|
+
returns a `Recorder` wired to the real tracer and meter. Covered by a smoke test
|
|
353
|
+
against the **real OTEL SDK** with in-memory exporters (span name/attributes/
|
|
354
|
+
hierarchy/error status; instrument names/units/attributes).
|
|
355
|
+
- **No reader means no meter.** If every metric reader is disabled, `setup` injects
|
|
356
|
+
no meter at all. Recording into a provider nothing drains would accumulate a point
|
|
357
|
+
per attribute set forever — a slow leak dressed as telemetry.
|
|
358
|
+
- **`Telemetry.attach`** — subscribes the recorder to the event stream inside the
|
|
359
|
+
reactor. No-op when the recorder is `nil` (disabled path), before touching the
|
|
360
|
+
reactor.
|
|
361
|
+
|
|
362
|
+
## Backpressure
|
|
363
|
+
|
|
364
|
+
Each event-stream subscription is capped at 1000 buffered events; a telemetry
|
|
365
|
+
consumer that fell far behind would have its subscription closed (telemetry stops,
|
|
366
|
+
the turn does not). Span and instrument operations are cheap, so there's ample
|
|
367
|
+
headroom.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
*Packaging note.* Today the bridge lives in the Insika repo as an opt-in core
|
|
372
|
+
module — no separate install, no separate versioning. When Insika extracts its
|
|
373
|
+
subsystems into gems it becomes `insika-otel`; because the bridge is already a
|
|
374
|
+
pure event-stream consumer with a single gem boundary, that cut lands clean.
|
data/docs/PLUGINS.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Plugins
|
|
3
|
+
parent: Build an agent
|
|
4
|
+
nav_order: 7
|
|
5
|
+
permalink: /plugins/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Plugins
|
|
9
|
+
|
|
10
|
+
Insika has **two tiers of extension**, and picking the right one is almost always
|
|
11
|
+
obvious once you ask a single question: *does this need Ruby to run in-process?*
|
|
12
|
+
|
|
13
|
+
| | **Tier 1 — data** | **Tier 2 — code** |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| What you write | JSON/YAML config | a Ruby gem (or a directory) |
|
|
16
|
+
| Ships as | a row in SQLite | a `.rb` file loaded at boot |
|
|
17
|
+
| Takes effect | **hot** — no restart | at the next restart |
|
|
18
|
+
| Can add | HTTP tools, MCP toolsets, skills, prompts | tools, workflows, capabilities, policies, middleware, hooks, context providers |
|
|
19
|
+
| Reach for it when | you are calling an external API | logic must run in-process, or you are extending the engine itself |
|
|
20
|
+
|
|
21
|
+
Tier 1 is the path for "the community adds integrations". Tier 2 is the path for
|
|
22
|
+
"the community extends the engine". Most integrations are tier 1, and you should
|
|
23
|
+
feel mildly suspicious of yourself when you reach past it.
|
|
24
|
+
|
|
25
|
+
## Tier 1 — extend with data
|
|
26
|
+
|
|
27
|
+
Nothing to install and nothing to deploy: a **data tool** is an HTTP call
|
|
28
|
+
described by config, and an **MCP import** turns a whole MCP server's toolset
|
|
29
|
+
into data tools in one call. Both are covered in [Tools](TOOLS.md) — the schema,
|
|
30
|
+
the `{{param}}` / `{{ctx.*}}` / `{{secret.*}}` placeholders, the four write paths,
|
|
31
|
+
and the egress guard.
|
|
32
|
+
|
|
33
|
+
Skills are the other half of tier 1: a `SKILL.md` is knowledge, not code, and it
|
|
34
|
+
can be authored in the Studio or shipped inside a pack. See [Skills](SKILLS.md).
|
|
35
|
+
|
|
36
|
+
## Tier 2 — extend with code
|
|
37
|
+
|
|
38
|
+
A code plugin is **a directory with a manifest**. The manifest is discovery
|
|
39
|
+
without execution: Insika reads and validates it first, and only then requires
|
|
40
|
+
your Ruby.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
my-plugin/
|
|
44
|
+
├── insika.plugin.yml # the manifest — always read first
|
|
45
|
+
├── plugin.rb # the entry: defines a module with .register(api)
|
|
46
|
+
└── skills/ # optional: SKILL.md files shipped with the plugin
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
# insika.plugin.yml
|
|
51
|
+
id: weather # unique; the id everything else keys on
|
|
52
|
+
name: Weather
|
|
53
|
+
description: Weather lookups. Ships the get_weather tool and a weather_report skill.
|
|
54
|
+
entry: plugin.rb # required to register anything; omit for a skills-only plugin
|
|
55
|
+
module: WeatherPlugin # must respond to .register(api)
|
|
56
|
+
contracts: # the PUBLIC surface — declared here or ignored
|
|
57
|
+
tools: [get_weather]
|
|
58
|
+
workflows: []
|
|
59
|
+
capabilities: []
|
|
60
|
+
channels: []
|
|
61
|
+
tool_metadata:
|
|
62
|
+
get_weather:
|
|
63
|
+
optional: false # optional tools require per-agent opt-in
|
|
64
|
+
side_effect: false # true ⇒ not re-run on resume (checkpointed)
|
|
65
|
+
skills: [skills] # directories, relative to the plugin root
|
|
66
|
+
prompts: []
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
# plugin.rb
|
|
71
|
+
require "ruby_llm"
|
|
72
|
+
|
|
73
|
+
module WeatherPlugin
|
|
74
|
+
class GetWeather < RubyLLM::Tool
|
|
75
|
+
description "Looks up the current weather for a city"
|
|
76
|
+
param :city, desc: "City name"
|
|
77
|
+
|
|
78
|
+
def execute(city:) = { city: city, temp_c: 24, condition: "sunny" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.register(api)
|
|
82
|
+
api.register_tool("get_weather", GetWeather)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Two runnable ones live in the repo:
|
|
88
|
+
[`plugins/weather`](https://github.com/guizaols/insika/tree/main/plugins/weather)
|
|
89
|
+
(the minimal shape) and
|
|
90
|
+
[`plugins/insika-code`](https://github.com/guizaols/insika/tree/main/plugins/insika-code)
|
|
91
|
+
(a real toolset: file read/write/edit, grep, shell — sandboxed, with the
|
|
92
|
+
side-effecting tools marked so an agent can gate them behind approval).
|
|
93
|
+
|
|
94
|
+
### What `register(api)` can register
|
|
95
|
+
|
|
96
|
+
| Call | Registers | Declared in `contracts`? |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `register_tool(name, klass)` | a tool the model can call | **yes** — `contracts.tools` |
|
|
99
|
+
| `register_workflow(name, callable)` | a named workflow (see [Architecture](ARCHITECTURE.md)) | **yes** — `contracts.workflows` |
|
|
100
|
+
| `register_capability(name, tool:)` | an intent that resolves to a tool | **yes** — `contracts.capabilities` |
|
|
101
|
+
| `register_channel(name, instance)` | a way in and out for people (see [Channels](CHANNELS.md)) | **yes** — `contracts.channels` |
|
|
102
|
+
| `register_policy(name, klass)` | a policy for the resolution stage | no |
|
|
103
|
+
| `register_middleware(instance)` | a wrap around the turn pipeline | no |
|
|
104
|
+
| `register_context_provider(instance)` | a source of prompt context | no |
|
|
105
|
+
| `register_hook(:tool, before:, after:)` | alters one stage's input/output (`:task`, `:prompt`, `:agent`, `:tool`) | no |
|
|
106
|
+
|
|
107
|
+
Anything addressable by name must be declared in `contracts`; registering an
|
|
108
|
+
undeclared name logs a warning and is **ignored**, so a plugin cannot quietly
|
|
109
|
+
widen its own surface between versions. For a channel the name is also a URL
|
|
110
|
+
segment (`/channels/<name>/…`), so the declaration is what stops a plugin
|
|
111
|
+
from mounting a route nobody asked for.
|
|
112
|
+
|
|
113
|
+
`api.config` returns the manifest's `config` hash, frozen.
|
|
114
|
+
|
|
115
|
+
### Failure is contained, and quiet
|
|
116
|
+
|
|
117
|
+
Registration is staged and committed atomically: if `register(api)` raises
|
|
118
|
+
halfway through, everything it staged is rolled back and the plugin is
|
|
119
|
+
discarded. **Boot continues** — one bad plugin must not take the deployment
|
|
120
|
+
down.
|
|
121
|
+
|
|
122
|
+
> ⚠️ The cost of that choice: a broken plugin is a `warn` on stderr, not a crash.
|
|
123
|
+
> If a tool is missing, check the boot log and the `:plugin_loaded` events before
|
|
124
|
+
> suspecting the allowlist.
|
|
125
|
+
|
|
126
|
+
The same posture applies to config: if `config_schema` is present and `config`
|
|
127
|
+
fails it, the plugin is **skipped** (fail-closed) with the validation errors
|
|
128
|
+
printed. The validator is a deliberate subset of JSON Schema — `type`,
|
|
129
|
+
`properties`, `required`, `additionalProperties`, `enum` — and an unsupported
|
|
130
|
+
keyword is itself an error rather than being silently ignored.
|
|
131
|
+
|
|
132
|
+
### Discovery and enabling
|
|
133
|
+
|
|
134
|
+
Plugins come from three kinds of root, and they differ in **who has to say yes**:
|
|
135
|
+
|
|
136
|
+
| Root | How it is found | Enabled by default |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| **Gem** | the gem calls `Insika::Plugin.announce(__dir__)` when its `lib/` loads | **yes** — installing it is the consent |
|
|
139
|
+
| **Workspace** | a directory in the deployment's plugin roots | no — must be listed in `enabled:` |
|
|
140
|
+
| **Bundled** | `plugins/` in this repo | no — must be listed in `enabled:` |
|
|
141
|
+
|
|
142
|
+
`disabled:` is an absolute veto: an id listed there never loads, even if it is
|
|
143
|
+
also in `enabled:` (deny wins, the same rule as every allowlist in the engine).
|
|
144
|
+
|
|
145
|
+
A gem announces itself explicitly — Insika never scans the load path or your
|
|
146
|
+
installed gems:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# lib/insika-plugin-acme.rb
|
|
150
|
+
require "insika/plugin"
|
|
151
|
+
Insika::Plugin.announce(File.expand_path("../..", __dir__))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Discovery details worth knowing: the manifest may be named `insika.plugin.yml`
|
|
155
|
+
(preferred) or `plugin.yml` (deprecated — it warns); one manifest per directory,
|
|
156
|
+
with `insika.plugin.yml` winning if both exist; and if two roots ship the same
|
|
157
|
+
`id`, the **first root wins** and the second is skipped.
|
|
158
|
+
|
|
159
|
+
### Secrets
|
|
160
|
+
|
|
161
|
+
Put the *name* of the environment variable in the manifest, never the value:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
config:
|
|
165
|
+
api_key_env: ACME_API_KEY
|
|
166
|
+
config_schema:
|
|
167
|
+
type: object
|
|
168
|
+
required: [api_key_env]
|
|
169
|
+
properties:
|
|
170
|
+
api_key_env: { type: string }
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The manifest is committed; the secret is not. This mirrors how data tools handle
|
|
174
|
+
`{{secret.*}}`.
|
|
175
|
+
|
|
176
|
+
## Publishing a plugin
|
|
177
|
+
|
|
178
|
+
- **Name it `insika-plugin-<thing>`.** The convention *is* the registry for now:
|
|
179
|
+
a predictable RubyGems prefix plus a curated list here beats a hub nobody has
|
|
180
|
+
had a reason to build yet.
|
|
181
|
+
- **Announce in your gem's entry file** (above), so installing it is enough.
|
|
182
|
+
- **Treat `contracts` as your public API.** Renaming a tool, changing its
|
|
183
|
+
parameters, or adding a `required` config key are breaking changes for the
|
|
184
|
+
agents that allowlist them by name. Version accordingly.
|
|
185
|
+
- **Do not require `insika` at load time** if you can avoid it —
|
|
186
|
+
`insika/plugin` is deliberately dependency-free so a plugin gem can announce
|
|
187
|
+
itself before anything else of ours is loaded.
|
|
188
|
+
|
|
189
|
+
> **Compatibility, stated honestly:** Insika is pre-1.0 and nothing is tagged
|
|
190
|
+
> yet. The manifest keys and the `register(api)` surface above are what a plugin
|
|
191
|
+
> depends on; changes to them are listed in
|
|
192
|
+
> [`CHANGELOG.md`](https://github.com/guizaols/insika/blob/main/CHANGELOG.md).
|
|
193
|
+
> Until 1.0, pin the engine version you tested against.
|
|
194
|
+
|
|
195
|
+
## Choosing a tier
|
|
196
|
+
|
|
197
|
+
| You want to… | Tier |
|
|
198
|
+
|---|---|
|
|
199
|
+
| call a REST API the model can invoke | **1** — data tool |
|
|
200
|
+
| adopt an existing MCP server's tools | **1** — MCP import |
|
|
201
|
+
| add domain knowledge or a procedure | **1** — a skill |
|
|
202
|
+
| touch the filesystem, run a subprocess, hold state in-process | **2** |
|
|
203
|
+
| add a policy, a middleware, or a context provider | **2** |
|
|
204
|
+
| package the above for other deployments to install | **2**, as a gem |
|
|
205
|
+
|
|
206
|
+
## See also
|
|
207
|
+
|
|
208
|
+
- [Tools](TOOLS.md) — data tools, MCP ingestion, allowlists, and the egress guard.
|
|
209
|
+
- [Skills](SKILLS.md) — `SKILL.md`, progressive disclosure, and skill packs.
|
|
210
|
+
- [Sandbox](SANDBOX.md) — confining a code plugin that touches the filesystem or shell.
|
|
211
|
+
- [Architecture](ARCHITECTURE.md) — where plugins hook into the turn pipeline.
|