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/DEPLOY.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Deploy
|
|
3
|
+
parent: Ship it
|
|
4
|
+
nav_order: 3
|
|
5
|
+
permalink: /deploy/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Deploy
|
|
9
|
+
|
|
10
|
+
How to run the engine in a container (Railway today, Kubernetes later) and how to
|
|
11
|
+
measure performance and load.
|
|
12
|
+
|
|
13
|
+
> **This page deploys the repo, not the gem.** The reference deployment is a
|
|
14
|
+
> checkout of this same repository — `config/` is its composition root, and the
|
|
15
|
+
> `Gemfile` consumes the engine through the gemspec. An adopter deploys
|
|
16
|
+
> `gem install insika` (or a `Gemfile` line) instead.
|
|
17
|
+
|
|
18
|
+
## Image (Docker)
|
|
19
|
+
|
|
20
|
+
The `Dockerfile` (multi-stage, YJIT on) serves `config.ru` under Falcon. The Studio
|
|
21
|
+
ships with its `dist/` built and vendored — **no Node in the build**. The backend
|
|
22
|
+
is durable SQLite (WAL) at `INSIKA_DB`; mount a volume and point it inside.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
docker build -t insika .
|
|
26
|
+
docker run -p 9292:9292 -v insika-data:/data \
|
|
27
|
+
-e DEEPSEEK_API_KEY=sk-... \
|
|
28
|
+
-e OPENCLAW_GATEWAY_TOKEN=change-me \
|
|
29
|
+
insika
|
|
30
|
+
curl localhost:9292/up # {"status":"ok"}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## The process model
|
|
34
|
+
|
|
35
|
+
The image boots **N Falcon worker processes over one SQLite file**
|
|
36
|
+
(`WEB_CONCURRENCY`, default 1). That number is a **contract input, not a tuning
|
|
37
|
+
knob**: it decides which engine semantics hold cluster-wide and which are
|
|
38
|
+
per-worker. The default is 1 because the per-worker semantics are the product
|
|
39
|
+
— the queue modes (`collect`/`steer`) and FIFO ordering need one
|
|
40
|
+
session actor; raise N only with sticky routing per session in front. The
|
|
41
|
+
contract:
|
|
42
|
+
|
|
43
|
+
> Everything here describes N workers of **one** deployment — one graph, replicated.
|
|
44
|
+
> N *graphs* inside one process is a different contract, and it is
|
|
45
|
+
> [Embedding](EMBEDDING.md): there each graph owns its own store and credentials,
|
|
46
|
+
> and the host — not the engine — installs the drain.
|
|
47
|
+
|
|
48
|
+
1. **N workers share one SQLite store.** Everything durable — sessions, tasks,
|
|
49
|
+
checkpoints, outbox, delegations — is cross-process state. Any status
|
|
50
|
+
transition that hands work to "whoever gets there first" goes through a
|
|
51
|
+
transactional claim (`Store#transaction`); a bare read-check-write on a
|
|
52
|
+
shared status field is a bug by definition.
|
|
53
|
+
2. **A session's live semantics are per-worker.** Per-session FIFO ordering,
|
|
54
|
+
`steer`, `interrupt`, `pause`/`cancel` and the SSE watch operate on the
|
|
55
|
+
worker that holds the session's actor. The engine does **not** promise them
|
|
56
|
+
across workers. A deploy that needs those semantics for a session must route
|
|
57
|
+
that session's traffic to one worker (sticky routing) — or accept per-worker
|
|
58
|
+
best-effort.
|
|
59
|
+
3. **Recovery is part of boot, in every wiring.** Every worker boots through
|
|
60
|
+
`Server::Boot`, which runs recovery **before the listen**. The per-record
|
|
61
|
+
sweeps (undelivered outbox records, undelivered delegation results) run in
|
|
62
|
+
every worker — each record carries its own transactional claim (item 1), so
|
|
63
|
+
at-most-once holds however many workers sweep. The **task sweep** runs
|
|
64
|
+
**once per boot generation**: the sweep's "orphaned `:running`" test cannot
|
|
65
|
+
see a fiber living in a *sibling* process, so the first worker to claim
|
|
66
|
+
`INSIKA_BOOT_ID` (one id per container start, exported by
|
|
67
|
+
`deploy/entrypoint.sh`) sweeps and the rest skip. A worker respawned
|
|
68
|
+
mid-generation skips too — sweeping then would steal its siblings' live
|
|
69
|
+
turns; its own orphans wait for the next generation (the next deploy).
|
|
70
|
+
Without `INSIKA_BOOT_ID` (single-process runs) every boot sweeps.
|
|
71
|
+
4. **Shutdown is a drain, not a kill.** On SIGTERM (or SIGINT) a worker stops
|
|
72
|
+
accepting new turns — a turn that arrives mid-drain stays `:queued` and the
|
|
73
|
+
next boot's recovery replays it — and waits up to `INSIKA_DRAIN_TIMEOUT`
|
|
74
|
+
(default **20s**) for the in-flight ones. A second signal skips the wait.
|
|
75
|
+
Whatever the deadline abandons dies `:running`, and item 3 picks it up at the
|
|
76
|
+
next boot. The layers above must grant the time: `deploy/entrypoint.sh`
|
|
77
|
+
passes Falcon `--graceful-stop` = drain + 5 (Falcon's own default is 1s),
|
|
78
|
+
and the platform's SIGTERM→SIGKILL buffer must be ≥ drain + 10. **On Railway
|
|
79
|
+
that buffer defaults to 0** — SIGKILL right after SIGTERM, which cancels the
|
|
80
|
+
whole drain — so set `RAILWAY_DEPLOYMENT_DRAINING_SECONDS=30` on the
|
|
81
|
+
service.
|
|
82
|
+
5. **A periodic tick closes the gap between boots.** Serving workers run a tick
|
|
83
|
+
every `INSIKA_TICK_INTERVAL` (default **60s**, `0` disables) as a child of
|
|
84
|
+
the turn supervisor: it re-drives any outbox record left `:pending` (each
|
|
85
|
+
carries its own claim — every worker may drain), and sweeps orphaned tasks
|
|
86
|
+
**without waiting for a deploy** — which is what recovers the orphans of a
|
|
87
|
+
worker respawned mid-generation (item 3's hole). One worker per window
|
|
88
|
+
sweeps (a single transactional claim), and only `:queued`/`:running` tasks
|
|
89
|
+
untouched for `INSIKA_TICK_STALE_AFTER` (default **900s**) are candidates:
|
|
90
|
+
a live turn is bounded by `turn_timeout`, so anything older cannot be alive.
|
|
91
|
+
**If you raise `turn_timeout` past it, raise `INSIKA_TICK_STALE_AFTER` too** —
|
|
92
|
+
the threshold must exceed the largest `turn_timeout` of the deployment.
|
|
93
|
+
`:waiting`/`:paused` tasks are idle by nature and stay boot recovery's.
|
|
94
|
+
|
|
95
|
+
`deploy/entrypoint.sh` sets `WEB_CONCURRENCY` next to a pointer to this section;
|
|
96
|
+
this section is the single source of truth for what changing it means.
|
|
97
|
+
|
|
98
|
+
## Environment variables
|
|
99
|
+
|
|
100
|
+
| Env | Default | Effect |
|
|
101
|
+
|-----|---------|--------|
|
|
102
|
+
| `INSIKA_DB` | `/data/insika.db` (in the image) | durable SQLite path (**mount a volume!**) |
|
|
103
|
+
| `PORT` | `9292` | HTTP bind port |
|
|
104
|
+
| `WEB_CONCURRENCY` | `1` | number of Falcon worker processes — a contract input, see [The process model](#the-process-model) |
|
|
105
|
+
| `INSIKA_BOOT_ID` | set by `deploy/entrypoint.sh` | boot generation id; the recovery **task sweep** runs once per id (process model, item 3). Unset = every boot sweeps (single-process default) |
|
|
106
|
+
| `INSIKA_DRAIN_TIMEOUT` | `20` | seconds a stopping worker waits for in-flight turns before abandoning them to the next boot's recovery (process model, item 4). The entrypoint sizes Falcon's `--graceful-stop` from it; on Railway also set `RAILWAY_DEPLOYMENT_DRAINING_SECONDS` ≥ drain + 10 |
|
|
107
|
+
| `INSIKA_TICK_INTERVAL` | `60` | seconds between tick passes — outbox drain + stale recovery sweep (process model, item 5). `0` disables |
|
|
108
|
+
| `INSIKA_TICK_STALE_AFTER` | `900` | seconds a `:queued`/`:running` task must sit untouched before the tick sweeps it. Must exceed the largest `turn_timeout` of the deployment |
|
|
109
|
+
| `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for `/v1/responses` and `/v1/agents` (the API contract) |
|
|
110
|
+
| `ADMIN_TOKEN` | `local-demo` | login token for `/studio` (**change in production**) |
|
|
111
|
+
| `DEEPSEEK_API_KEY` | — | provider key. **Without it the engine still boots** (`/up` green), but turns fail until it is configured (env or Studio → LLM providers) — cloud resilience |
|
|
112
|
+
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model |
|
|
113
|
+
| `CONSUMER_INTERNAL_URL` | — | base URL for data-tools calling back a consumer's internal API (see below) |
|
|
114
|
+
| `INSIKA_EGRESS_HOSTS` | — | outbound host allowlist (SSRF guard) |
|
|
115
|
+
| `INSIKA_EGRESS_ALLOW_HTTP` / `_ALLOW_PRIVATE` | off | for `http`/loopback callbacks only (**never in cloud**) |
|
|
116
|
+
| `INSIKA_RELAY_TOKEN` | — | **mounts the relay channel** at `POST /channels/relay/events`, and is the Bearer it requires. Empty = the route does not exist (`404`). See [Channels](CHANNELS.md) |
|
|
117
|
+
| `INSIKA_RELAY_DELIVER_URL` | — | your callback; the engine POSTs each reply there. Goes through the egress guard |
|
|
118
|
+
| `INSIKA_RELAY_DELIVER_TOKEN` | — | Bearer the engine sends **to** your callback (optional) |
|
|
119
|
+
| `INSIKA_WIDGET_ORIGINS` | — | exact-match origins allowed to embed the [web widget](CHANNELS.md#the-web-widget), comma-separated. No wildcards. **Half the switch**: with `INSIKA_WIDGET_AGENTS` unset, nothing is mounted (`404`) |
|
|
120
|
+
| `INSIKA_WIDGET_AGENTS` | — | agent ids a widget visitor may address, comma-separated. The other half of the switch. **A chat rate limit is also required** or the widget answers `503` |
|
|
121
|
+
| `LITESTREAM_REPLICA_URL` | — | **enables Litestream** (backup/DR). Empty = disabled (default). See below |
|
|
122
|
+
| `LITESTREAM_ENDPOINT` | — | S3-compatible endpoint (R2/MinIO). Empty = AWS S3 |
|
|
123
|
+
| `LITESTREAM_REGION` | — | bucket region (AWS: `us-east-1`; R2: `auto`) |
|
|
124
|
+
| `LITESTREAM_ACCESS_KEY_ID` / `LITESTREAM_SECRET_ACCESS_KEY` | — | bucket credentials (read natively by Litestream) |
|
|
125
|
+
|
|
126
|
+
> **Renamed from `HARNESS_*` → `INSIKA_*`.** Every engine variable now uses the
|
|
127
|
+
> `INSIKA_` prefix. The old `HARNESS_*` names are still honored as deprecated aliases
|
|
128
|
+
> — set either one and the engine reads it, logging a one-line deprecation notice at
|
|
129
|
+
> boot (`insika doctor` reports it too). Migrate at your convenience; the legacy names
|
|
130
|
+
> will be dropped in a future release.
|
|
131
|
+
|
|
132
|
+
> **The database filename changed too** — the image now defaults to
|
|
133
|
+
> `INSIKA_DB=/data/insika.db` (it was `/data/harness.db`). **Existing volumes are
|
|
134
|
+
> adopted automatically:** the container entrypoint renames the old file — with its
|
|
135
|
+
> `-wal`/`-shm` siblings, before anything opens it — when the configured path does
|
|
136
|
+
> not exist yet. Nothing to run by hand, no data lost, and a no-op from the second
|
|
137
|
+
> boot on. To keep the old filename instead, point `INSIKA_DB` at it: the variable
|
|
138
|
+
> is the knob, the image only picks a default. (The adoption lives in
|
|
139
|
+
> `deploy/entrypoint.sh`, not in the engine — it is deploy baggage, not a runtime
|
|
140
|
+
> behavior.)
|
|
141
|
+
|
|
142
|
+
### Tokens & rotation (keep the two separate!)
|
|
143
|
+
|
|
144
|
+
There are **two** secrets with distinct purposes — in production use **different**
|
|
145
|
+
values (the API token falling back to `ADMIN_TOKEN` is a dev convenience only):
|
|
146
|
+
|
|
147
|
+
- **`ADMIN_TOKEN`** — the `/studio` login (cookie auth). This is the **operator**
|
|
148
|
+
surface (just you). Rotating it is **safe and independent**: change it, redeploy,
|
|
149
|
+
log in with the new value. It does not affect any API consumer.
|
|
150
|
+
- **`OPENCLAW_GATEWAY_TOKEN`** — the Bearer for `/v1/responses` and `/v1/agents`.
|
|
151
|
+
This is the **contract with your API consumers**. Rotating it means **changing
|
|
152
|
+
both sides together** (or the integration breaks): update the runtime var **and**
|
|
153
|
+
each consumer's token in the same step.
|
|
154
|
+
|
|
155
|
+
Generate a strong token: `ruby -rsecurerandom -e 'puts SecureRandom.hex(24)'`.
|
|
156
|
+
|
|
157
|
+
### Strict config and `insika doctor`
|
|
158
|
+
|
|
159
|
+
Config discipline that **rejects unknown keys — no silent schema tolerance**. Two
|
|
160
|
+
parts:
|
|
161
|
+
|
|
162
|
+
**1. Boot gate.** On boot, the engine validates the environment against a schema of
|
|
163
|
+
known keys (`Insika::EnvSchema`): a wrong type (`INSIKA_PORT=abc`) or an
|
|
164
|
+
**unknown key in the `INSIKA_` namespace** (a typo like `INSIKA_EGRES_ALLOW_HTTP`
|
|
165
|
+
the runtime would silently ignore). By **default it only warns** and boots anyway
|
|
166
|
+
(*last-known-good* — a rotated key or a typo never takes the whole service down).
|
|
167
|
+
To **refuse boot** on any finding, set `INSIKA_CONFIG_STRICT=1`. Unknown-key
|
|
168
|
+
detection is scoped to the `INSIKA_` prefix; the shared `OPENCLAW_`, `LITESTREAM_`,
|
|
169
|
+
and `OTEL_` namespaces are never flagged.
|
|
170
|
+
|
|
171
|
+
**2. `bin/insika doctor` — on-demand diagnostics.** Reads the **same** durable
|
|
172
|
+
backend the server uses (`INSIKA_DB`) without booting the whole app (no provider,
|
|
173
|
+
no seed) — safe to run against a production volume:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
insika doctor # colored report; exits != 0 on any error
|
|
177
|
+
insika doctor --json # machine-readable (CI / monitoring)
|
|
178
|
+
insika doctor --fix # applies the safe autofixes and re-diagnoses
|
|
179
|
+
insika env # lists known keys + current values (secrets masked)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Checks: env (the schema above), settings schema version (a pending migration →
|
|
183
|
+
`--fix` applies it), a missing platform `default_model` (`--fix` seeds it from
|
|
184
|
+
`DEEPSEEK_MODEL`), durable vs ephemeral backend, LLM provider configured,
|
|
185
|
+
`ADMIN_TOKEN` set, data-tool definitions still valid, **prompt files that hold
|
|
186
|
+
text rather than a serialized object** (a file whose content is a stringified Hash
|
|
187
|
+
serves a mangled prompt on every turn while looking perfectly healthy — present,
|
|
188
|
+
non-empty, and the agent still answers), and **skill drift** — a shared skill whose
|
|
189
|
+
body names one store, a prompt file routing to a skill the agent cannot load, a broken
|
|
190
|
+
companion pair, a stale `eager:` key (see
|
|
191
|
+
[Skills](SKILLS.md#drift-guards)). Settings-schema migrations are **explicit**
|
|
192
|
+
— no Studio save silently reinterprets old-shape data.
|
|
193
|
+
|
|
194
|
+
### Data-tool callbacks to a backend — via a tunnel
|
|
195
|
+
|
|
196
|
+
Data-tools call back a consumer's internal HTTP API. With the engine **in the
|
|
197
|
+
cloud** and your backend **on your machine** (`:3000`), expose it over a public
|
|
198
|
+
`https` tunnel and point the engine at it:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# in the tool/manifest: base_url = {{env.CONSUMER_INTERNAL_URL}}
|
|
202
|
+
CONSUMER_INTERNAL_URL=https://your-tunnel.example.dev
|
|
203
|
+
INSIKA_EGRESS_HOSTS=your-tunnel.example.dev
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Because the tunnel is **public `https`**, the strict egress guard (the default)
|
|
207
|
+
**already allows it** — you do **not** need `ALLOW_HTTP`/`ALLOW_PRIVATE` (those are
|
|
208
|
+
only for a fully-local loop). Restricting `INSIKA_EGRESS_HOSTS` to the tunnel host
|
|
209
|
+
is the secure posture. See [Security](SECURITY.md#egress-the-ssrf-boundary).
|
|
210
|
+
|
|
211
|
+
## Railway
|
|
212
|
+
|
|
213
|
+
`railway.json` already configures the Dockerfile builder, `startCommand`, the `/up`
|
|
214
|
+
healthcheck, and a restart policy.
|
|
215
|
+
|
|
216
|
+
1. Create the project/service from this repo (builder = Dockerfile).
|
|
217
|
+
2. **Volume**: mount it at `/data` (the default `INSIKA_DB` points there) —
|
|
218
|
+
without a volume, SQLite is ephemeral and recovery resumes nothing after a
|
|
219
|
+
redeploy.
|
|
220
|
+
3. **Vars**: `DEEPSEEK_API_KEY`, `OPENCLAW_GATEWAY_TOKEN`, `CONSUMER_INTERNAL_URL`,
|
|
221
|
+
`INSIKA_EGRESS_HOSTS` (and `WEB_CONCURRENCY` to match your plan/CPU).
|
|
222
|
+
4. The healthcheck hits `/up`.
|
|
223
|
+
5. Point your consumer at the service's public URL, with a matching API token
|
|
224
|
+
(see [RUNNING-LOCAL.md](RUNNING-LOCAL.md)).
|
|
225
|
+
|
|
226
|
+
## Backup / DR — Litestream (opt-in, configurable)
|
|
227
|
+
|
|
228
|
+
A single volume is the **one point of total loss** between a pilot and production
|
|
229
|
+
(disk corruption/loss = goodbye conversations + config). Litestream does
|
|
230
|
+
**continuous replication** of `insika.db` (its WAL) to an S3/R2 bucket, without
|
|
231
|
+
changing databases and **without a line of Ruby**.
|
|
232
|
+
|
|
233
|
+
It is **off by default** and turns on by env — a single-box ephemeral deploy pays
|
|
234
|
+
nothing; a durable deploy enables it by pointing at a bucket. The trigger is one
|
|
235
|
+
variable, `LITESTREAM_REPLICA_URL`:
|
|
236
|
+
|
|
237
|
+
- **empty (default):** the entrypoint `exec`s Falcon directly. The Litestream binary
|
|
238
|
+
is never invoked — behavior identical to not having it.
|
|
239
|
+
- **set:** on a fresh box the entrypoint **restores** `insika.db` from the replica
|
|
240
|
+
*before* the app opens it (`litestream restore -if-replica-exists`; a no-op if the
|
|
241
|
+
bucket is still empty), then **supervises** the app (`litestream replicate -exec`),
|
|
242
|
+
replicating the WAL continuously and doing a final sync on shutdown (Railway's
|
|
243
|
+
SIGTERM).
|
|
244
|
+
|
|
245
|
+
### Enable in production (Railway)
|
|
246
|
+
|
|
247
|
+
Add the vars (keep the volume at `/data`):
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# AWS S3
|
|
251
|
+
LITESTREAM_REPLICA_URL=s3://my-bucket/insika
|
|
252
|
+
LITESTREAM_REGION=us-east-1
|
|
253
|
+
LITESTREAM_ACCESS_KEY_ID=AKIA...
|
|
254
|
+
LITESTREAM_SECRET_ACCESS_KEY=...
|
|
255
|
+
|
|
256
|
+
# Cloudflare R2 (S3-compatible): same, + endpoint and region=auto
|
|
257
|
+
LITESTREAM_REPLICA_URL=s3://my-bucket/insika
|
|
258
|
+
LITESTREAM_ENDPOINT=https://<accountid>.r2.cloudflarestorage.com
|
|
259
|
+
LITESTREAM_REGION=auto
|
|
260
|
+
LITESTREAM_ACCESS_KEY_ID=...
|
|
261
|
+
LITESTREAM_SECRET_ACCESS_KEY=...
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Credentials are read natively by Litestream (they are not in `deploy/litestream.yml`,
|
|
265
|
+
which only references URL/endpoint/region via `${VAR}`).
|
|
266
|
+
|
|
267
|
+
### Restore drill (the real "done" — an untested backup does not count)
|
|
268
|
+
|
|
269
|
+
The pilot→production gap only closes once a restore has been **exercised**. Two ways:
|
|
270
|
+
|
|
271
|
+
**1. Local, automated (proves the mechanism, zero credentials):** uses the real
|
|
272
|
+
image + a `file://` replica; boots → replicates → deletes the volume → boots a new
|
|
273
|
+
box → restores → confirms the marker row survived and `/up` is green.
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
scripts/litestream-restore-drill.sh # needs docker, sqlite3, curl
|
|
277
|
+
# → [drill] PASS — marker … restored from replica; /up green on the new box.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**2. Production (the drill that counts for go-live):** against the real bucket.
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
# a. with the service live and replicating, generate some config/conversation and
|
|
284
|
+
# confirm the replica has generations:
|
|
285
|
+
litestream snapshots -config deploy/litestream.yml "$INSIKA_DB"
|
|
286
|
+
|
|
287
|
+
# b. boot a NEW box (empty volume) with the same LITESTREAM_* vars → the entrypoint
|
|
288
|
+
# restores on boot. Verify manually in /studio that conversations and config came
|
|
289
|
+
# back. Manual restore alternative:
|
|
290
|
+
litestream restore -config deploy/litestream.yml -o /tmp/restored.db "$INSIKA_DB"
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Kubernetes (evolution)
|
|
294
|
+
|
|
295
|
+
SQLite does not share one file across nodes. Paths forward: a StatefulSet + a PVC
|
|
296
|
+
per pod + **sticky-by-agent** routing (shard by tenant), or **LiteFS**, or an
|
|
297
|
+
optional **Postgres** adapter. **Litestream** (above) for backup/DR from day one —
|
|
298
|
+
orthogonal to topology.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Measuring performance / load
|
|
303
|
+
|
|
304
|
+
### 1. SQLite write ceiling (no provider) — `bench_store.rb`
|
|
305
|
+
|
|
306
|
+
Isolates "can SQLite take multi-process writes?" from LLM noise: N processes
|
|
307
|
+
hammering writes on the **same** file (WAL + busy_timeout — the real config).
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
bundle exec ruby scripts/bench_store.rb 1,2,4,8 3000
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Measured (mid-2026, laptop, ~481B payload):**
|
|
314
|
+
|
|
315
|
+
| procs | writes/s (aggregate) | p50 | p95 | max | locked |
|
|
316
|
+
|------:|--------------------:|----:|----:|----:|-------:|
|
|
317
|
+
| 1 | ~29.6k | 0.02ms | 0.04ms | 2.4ms | **0** |
|
|
318
|
+
| 2 | ~29.9k | 0.02ms | 0.04ms | 59ms | **0** |
|
|
319
|
+
| 4 | ~23.7k | 0.02ms | 0.05ms | 336ms | **0** |
|
|
320
|
+
| 8 | ~28.4k | 0.03ms | 0.05ms | 539ms | **0** |
|
|
321
|
+
|
|
322
|
+
**Reading:** aggregate throughput stays ~25–30k writes/s regardless of process
|
|
323
|
+
count (the WAL's one-writer-at-a-time ceiling), with **zero "database is locked"**
|
|
324
|
+
(the `busy_timeout` absorbs contention into tail latency, not errors), and a
|
|
325
|
+
microscopic p95. A real turn is **provider-bound (seconds)** and does a handful of
|
|
326
|
+
writes → the workload sits ~100× under the ceiling. **Empirically, SQLite is not
|
|
327
|
+
the bottleneck on a single box.**
|
|
328
|
+
|
|
329
|
+
### 2. End-to-end load (with provider) — `loadtest.rb`
|
|
330
|
+
|
|
331
|
+
Hits `POST /v1/responses` (SSE), the production path. Measures TTFB, total, tokens,
|
|
332
|
+
cache hits, P50/P95, error rate. Runs against local or a remote deployment. See
|
|
333
|
+
[LOADTEST.md](LOADTEST.md).
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
INSIKA_URL=http://localhost:9292 OPENCLAW_GATEWAY_TOKEN=xxx \
|
|
337
|
+
bundle exec ruby scripts/loadtest.rb --agents assistant --concurrency 16 --iterations 3
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### 3. Baseline vs multi-worker on one box — `loadtest-local.sh`
|
|
341
|
+
|
|
342
|
+
Boots Falcon with 1 worker, then N, over the **same** SQLite, and counts "database
|
|
343
|
+
is locked" in the logs.
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
DEEPSEEK_API_KEY=sk-... ./scripts/loadtest-local.sh 4 24
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## See also
|
|
350
|
+
|
|
351
|
+
- [RUNNING-LOCAL.md](RUNNING-LOCAL.md) — run the engine locally, single-process.
|
|
352
|
+
- [Security](SECURITY.md) — tokens, egress, strict config.
|
|
353
|
+
- [BENCHMARK.md](BENCHMARK.md) — the neutral, key-free engine benchmark.
|
|
354
|
+
- [OBSERVABILITY.md](OBSERVABILITY.md) — OpenTelemetry traces + metrics (opt-in).
|
data/docs/EMBEDDING.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Embedding
|
|
3
|
+
parent: Ship it
|
|
4
|
+
nav_order: 4
|
|
5
|
+
permalink: /embedding/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Embedding
|
|
9
|
+
|
|
10
|
+
Insika does not have to be a service you stand up next to your app. It can be an
|
|
11
|
+
object inside the app you already have: a graph you build in an initializer and a
|
|
12
|
+
Rack app you mount in your router. This page is the contract for doing that —
|
|
13
|
+
what an embedded graph owns, what it still shares with the process, and what that
|
|
14
|
+
makes your responsibility.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The short version
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# config/initializers/insika.rb
|
|
22
|
+
INSIKA = Insika.embed(backend: Insika::Stores::SQLite.new(path: Rails.root.join("storage/insika.sqlite3").to_s)) do
|
|
23
|
+
agent "support" do
|
|
24
|
+
model "deepseek-v4-flash"
|
|
25
|
+
provider :deepseek
|
|
26
|
+
api_key ENV.fetch("DEEPSEEK_API_KEY")
|
|
27
|
+
instructions "You answer questions about orders. Be brief."
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# turns are born as children of a long-lived supervisor, not of the request
|
|
32
|
+
INSIKA.runtime.graph.executor.supervised = true
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
# config/routes.rb — the transport is lazy: `require "insika"` never loads it
|
|
37
|
+
require "insika/server/rack_app"
|
|
38
|
+
|
|
39
|
+
mount Insika::Server.rack_app(INSIKA, token: ENV.fetch("INSIKA_TOKEN")), at: "/ai"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
# anywhere in your app — no HTTP involved
|
|
44
|
+
INSIKA.reply("support", "where is order 8123?", session: current_user.id)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`Insika.embed` takes the same block as [`Insika.system`](AGENTS.md) and adds one
|
|
48
|
+
obligation: **you name the store**. That single argument is what turns "one
|
|
49
|
+
program" into "one object" — see [why](#why-the-store-is-an-argument) below.
|
|
50
|
+
|
|
51
|
+
`Insika::Server.rack_app` returns a plain Rack app. It routes on `PATH_INFO`, so
|
|
52
|
+
Rails' `mount` (and `Rack::URLMap`, and anything else that moves the prefix into
|
|
53
|
+
`SCRIPT_NAME`) leaves every route intact: mounted at `/ai`, the drop-in API is at
|
|
54
|
+
`/ai/v1/responses`.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Why the store is an argument
|
|
59
|
+
|
|
60
|
+
The engine was not always one process — it was one *program*. Two graphs
|
|
61
|
+
built in the same Ruby process shared things they never declared:
|
|
62
|
+
|
|
63
|
+
| What | What actually happened |
|
|
64
|
+
|---|---|
|
|
65
|
+
| LLM credentials | one process-wide slot **per provider**: two graphs on `deepseek` with different keys, and the second one won **for both**. No error, no warning — agent A simply called the provider with tenant B's key. |
|
|
66
|
+
| The store | `INSIKA_DB` unset → each graph got its own in-memory store and nothing persisted; `INSIKA_DB` set → every graph opened the **same file** and read the others' agents, sessions and tasks. |
|
|
67
|
+
|
|
68
|
+
Neither failure raised. That is the whole reason the embed contract is written
|
|
69
|
+
down rather than left to good sense.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## The contract
|
|
74
|
+
|
|
75
|
+
### 1. A graph owns its store
|
|
76
|
+
|
|
77
|
+
Every durable thing — sessions, tasks, checkpoints, the outbox, delegations,
|
|
78
|
+
memory — belongs to the backend the graph was built with. Two graphs given the
|
|
79
|
+
**same** backend share all of it, by your choice. Two graphs given **different**
|
|
80
|
+
backends share nothing.
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
support = Insika.embed(backend: Insika::Stores::SQLite.new(path: "storage/support.sqlite3")) { … }
|
|
84
|
+
ops = Insika.embed(backend: Insika::Stores::SQLite.new(path: "storage/ops.sqlite3")) { … }
|
|
85
|
+
# a session created in `support` does not exist in `ops`
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. A graph owns its LLM credentials
|
|
89
|
+
|
|
90
|
+
Provider keys and bases resolve through the graph's own
|
|
91
|
+
[`RubyLLM.context`](https://rubyllm.com) — an isolated dup of the configuration.
|
|
92
|
+
An embedded graph never mutates the global `RubyLLM.config`, and never reads
|
|
93
|
+
another graph's credentials. That holds for the turn itself *and* for everything
|
|
94
|
+
else in the graph that asks a model: the content-safety moderator and the output
|
|
95
|
+
validator ask on the same context the turn does.
|
|
96
|
+
|
|
97
|
+
It also holds for runtime reconfiguration. Editing a provider key in the Studio
|
|
98
|
+
(or dispatching `:upsert_llm_provider`) applies to **that graph only** — the
|
|
99
|
+
change is real, takes effect without a restart, and stops at the graph boundary.
|
|
100
|
+
|
|
101
|
+
### 3. The process still owns signals and the reactor
|
|
102
|
+
|
|
103
|
+
Draining in-flight turns on SIGTERM ([the process model](DEPLOY.md)) is a process
|
|
104
|
+
concern, not a graph concern: `Signal.trap` keeps only the last handler, so a
|
|
105
|
+
shutdown installed once per graph would drain the last graph and let the others
|
|
106
|
+
die mid-turn. An embedded graph therefore installs **nothing** behind your back.
|
|
107
|
+
You install it once, naming every graph:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
Insika::Shutdown.install(executors: [SUPPORT, OPS].map { |g| g.runtime.graph.executor })
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
One signal closes every graph's intake first, then waits — up to
|
|
114
|
+
`INSIKA_DRAIN_TIMEOUT` (default 20s) — for the in-flight turns of all of them.
|
|
115
|
+
Whatever outlives the deadline stays `:running` and is replayed by the next
|
|
116
|
+
boot's recovery sweep.
|
|
117
|
+
|
|
118
|
+
The reactor is yours too, and this one has teeth:
|
|
119
|
+
|
|
120
|
+
> **The mounted app needs an async server.** `rack_app` is a value — nothing in it
|
|
121
|
+
> starts a server or a reactor. A turn is a fiber, so the routes that start one
|
|
122
|
+
> (`POST /v1/responses`, `/v1/messages`, `/v1/commands`, `POST /channels/:id/events`)
|
|
123
|
+
> must be called from inside a running reactor. Under **Falcon** you already are.
|
|
124
|
+
> Under Puma or Unicorn they answer `500 {"error":{"message":"No async task
|
|
125
|
+
> available!"}}` — the read-only routes (`GET /v1/agents/:id`, `/v1/tasks/:id`, `/up`)
|
|
126
|
+
> are fine either way. Serve an embedded Insika from Falcon, or reach the graph
|
|
127
|
+
> in-process with `INSIKA.reply(...)`, which builds its own reactor.
|
|
128
|
+
|
|
129
|
+
And once you are inside one, set `supervised = true` (as in the quickstart): it
|
|
130
|
+
makes a turn a child of a long-lived supervisor instead of a child of the
|
|
131
|
+
request, so the turn survives a client disconnect. Without it the runtime cancels
|
|
132
|
+
the turn when the connection drops.
|
|
133
|
+
|
|
134
|
+
### 4. The Studio is not part of the embeddable surface
|
|
135
|
+
|
|
136
|
+
`rack_app` returns the `/v1` transport and nothing else. `Studio::App.configure`
|
|
137
|
+
freezes its collaborators on the **class**, which makes it one operator UI per
|
|
138
|
+
process: a second graph configuring the Studio replaces the first's wiring, for
|
|
139
|
+
both. So a host that wants the UI does a plain `require "insika/studio/app"`,
|
|
140
|
+
calls `Studio::App.configure` with exactly one graph's collaborators, and mounts
|
|
141
|
+
the class — accepting that the UI shows that graph and no other.
|
|
142
|
+
|
|
143
|
+
This is a stated limitation, not an oversight. Making the Studio instantiable is
|
|
144
|
+
its own change, and it needs its own reason.
|
|
145
|
+
|
|
146
|
+
### 5. ENV is a default, never a requirement
|
|
147
|
+
|
|
148
|
+
Anything an embedded graph reads from the environment has an injectable
|
|
149
|
+
equivalent, and the injected value wins. `INSIKA_DB` still works — it is the
|
|
150
|
+
default for the DSL quickstart and for `config.ru` — but an embedded graph that
|
|
151
|
+
was given a backend never looks at it.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## What you are responsible for
|
|
156
|
+
|
|
157
|
+
| Concern | Who |
|
|
158
|
+
|---|---|
|
|
159
|
+
| Which store each graph gets | You, via `backend:` |
|
|
160
|
+
| Which credentials each graph gets | You, via the agent's `api_key`/`provider` |
|
|
161
|
+
| Who may call the mounted app | You. `token:` is one shared Bearer for the whole mount — see below |
|
|
162
|
+
| Signals and the drain | You, once per process (`Shutdown.install(executors:)`) |
|
|
163
|
+
| The reactor / `supervised` | You, matching your server |
|
|
164
|
+
| Recovery of orphaned turns at boot | You, if you want it — the sweep is `Insika::Recovery`, wired by `Insika::Server::Boot` for the standalone deployment, not by `embed` |
|
|
165
|
+
|
|
166
|
+
### Embedding is not the multi-tenant server
|
|
167
|
+
|
|
168
|
+
Two graphs stop corrupting each other. That is all this contract says. **Who is
|
|
169
|
+
allowed to talk to which graph** is authorization, and it is not here: `token:` is
|
|
170
|
+
a single Bearer gating the whole mounted app, exactly as it does for the
|
|
171
|
+
standalone server in the default mode. Multi-tenancy is a property of the
|
|
172
|
+
*standalone* server, not of an embed: with `INSIKA_TENANCY=multi_tenant` the
|
|
173
|
+
Bearer resolves to a principal before the routes — per-tenant tokens scoped to
|
|
174
|
+
their own sessions, tasks and events, operator tokens with the run of the
|
|
175
|
+
deployment (see [Security](SECURITY.md#the-bearer-gate)). An embedded graph keeps
|
|
176
|
+
no token store, so an embed's tenancy belongs to the host: put the mounted app
|
|
177
|
+
behind your own authentication and pass `session:` yourself — do not hand the
|
|
178
|
+
mount point to the browser.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## What this is not
|
|
183
|
+
|
|
184
|
+
- **Not a Rails engine.** `insika-rails` would be a separate gem; this is the Rack
|
|
185
|
+
app such a gem would mount. The core has no Rails knowledge and takes no Rails
|
|
186
|
+
dependency.
|
|
187
|
+
- **Not a second assembly path.** `Insika.embed` is a front door over the same
|
|
188
|
+
pipeline `Insika.agent`/`Insika.system` use — the profile it produces is
|
|
189
|
+
identical, and there is a spec that holds it to that.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## See also
|
|
194
|
+
|
|
195
|
+
- [Architecture](ARCHITECTURE.md) — how a turn actually runs.
|
|
196
|
+
- [Deploy](DEPLOY.md) — the standalone process model (N workers of *one*
|
|
197
|
+
deployment), which is a different question from N graphs in one process.
|
|
198
|
+
- [Agents](AGENTS.md) — the DSL block `embed` takes.
|