insika 0.3.0 → 0.8.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 +296 -0
- data/README.md +48 -12
- data/bin/insika +725 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +116 -406
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +137 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +63 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +87 -10
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +128 -3
- data/docs/FACTS.md +3 -3
- data/docs/HARVEST.md +5 -6
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +17 -29
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +46 -12
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +222 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +5 -5
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +23 -7
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +3 -3
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +176 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +4 -4
- data/docs/improve.md +20 -0
- data/docs/index.md +8 -5
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/channel_delivery.rb +1 -1
- data/lib/insika/chat_builder.rb +50 -19
- data/lib/insika/commands/agent_payload.rb +2 -2
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +15 -3
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/compaction.rb +196 -0
- data/lib/insika/context/builder.rb +6 -2
- data/lib/insika/context/fragment.rb +4 -1
- data/lib/insika/context/priority.rb +8 -0
- data/lib/insika/context/providers/briefing.rb +53 -24
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- data/lib/insika/context/providers/session.rb +46 -10
- data/lib/insika/context_trace_store.rb +11 -1
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/doctor.rb +280 -17
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +64 -79
- data/lib/insika/dsl/server_boot.rb +23 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +103 -2
- data/lib/insika/env_schema.rb +21 -7
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +11 -0
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +84 -2
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +295 -55
- data/lib/insika/followup_policy.rb +2 -25
- data/lib/insika/golden_store.rb +16 -1
- data/lib/insika/grounding/matcher.rb +1 -1
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/loop_detector.rb +5 -34
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +115 -31
- data/lib/insika/message_origin.rb +1 -1
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/onboarding.rb +17 -1
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +2 -2
- data/lib/insika/profile_source.rb +15 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/retention.rb +36 -1
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/server/app.rb +105 -15
- data/lib/insika/server/rack_app.rb +5 -1
- data/lib/insika/server/responses.rb +5 -5
- data/lib/insika/session_store.rb +34 -4
- data/lib/insika/settings_store.rb +8 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/soak/runner.rb +4 -4
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +591 -47
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +21 -21
- data/lib/insika/studio/forms.rb +57 -5
- data/lib/insika/studio/nav_icons.rb +14 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +61 -820
- data/lib/insika/studio/views/agents.erb +70 -57
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/evals.erb +2 -2
- data/lib/insika/studio/views/facts.erb +1 -1
- data/lib/insika/studio/views/funnel.erb +1 -1
- data/lib/insika/studio/views/home.erb +106 -67
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +14 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/session.erb +231 -177
- data/lib/insika/studio/views/settings.erb +50 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- data/lib/insika/telemetry/recorder.rb +49 -1
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +47 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +68 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_batch.rb +67 -0
- data/lib/insika/tool_usage_report.rb +162 -0
- data/lib/insika/tools/generate_image.rb +52 -7
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/turn_budget.rb +91 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +15 -4
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +184 -12
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +64 -0
- metadata +109 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
data/docs/ROUTER.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Router
|
|
3
|
+
parent: Ship it
|
|
4
|
+
nav_order: 4
|
|
5
|
+
permalink: /router/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Session-sticky router
|
|
9
|
+
|
|
10
|
+
`insika-router` is a standalone proxy that lets you run **N engine backends**
|
|
11
|
+
(`WEB_CONCURRENCY=1` each) and get the same per-session guarantees a single
|
|
12
|
+
worker gives you today — FIFO ordering, `collect`/`steer`, the SSE watch (see
|
|
13
|
+
[DEPLOY.md "The process model"](DEPLOY.md#the-process-model)) — at N>1
|
|
14
|
+
capacity. It is **entirely opt-in**: if one worker is enough for you, ignore
|
|
15
|
+
this file, run the engine exactly as DEPLOY.md already describes, and nothing
|
|
16
|
+
changes. Reach for the router only when you outgrow one worker and want to
|
|
17
|
+
scale up.
|
|
18
|
+
|
|
19
|
+
It changes nothing about the engine itself — no code in `SessionActor` or
|
|
20
|
+
`Executor` is aware the router exists. It solves routing, and routing only: a
|
|
21
|
+
given session's requests always land on the same backend, so that backend's
|
|
22
|
+
in-memory session state is always the one being read and written.
|
|
23
|
+
|
|
24
|
+
## Why this exists
|
|
25
|
+
|
|
26
|
+
`WEB_CONCURRENCY>1` without sticky routing in front is not "reduced
|
|
27
|
+
guarantees" — it is a correctness bug (a reply from one session can leak into
|
|
28
|
+
another's transcript; `insika doctor`'s `web-concurrency` check exists because
|
|
29
|
+
this happened in staging). Sticky routing is the documented escape hatch, but
|
|
30
|
+
neither deploy target the engine ships for has it built in:
|
|
31
|
+
|
|
32
|
+
- **Railway** does not support sticky sessions at all — traffic is randomly
|
|
33
|
+
distributed across replicas, with no configuration that changes that.
|
|
34
|
+
- **Kubernetes** `Service` load-balances with no session notion, and
|
|
35
|
+
ingress-nginx's `upstream-hash-by` (the usual sticky mechanism) hashes on
|
|
36
|
+
nginx *variables* — headers, cookies, the URL — never a field parsed out of
|
|
37
|
+
a POST body. The session id here is exactly that: the `user` field inside
|
|
38
|
+
`POST /v1/responses`'s JSON body.
|
|
39
|
+
|
|
40
|
+
So this is a small piece of new infrastructure, not a config flag.
|
|
41
|
+
|
|
42
|
+
## How it decides where a request goes
|
|
43
|
+
|
|
44
|
+
Per request, in order:
|
|
45
|
+
|
|
46
|
+
1. `GET /up` → answered directly by the router (its own liveness), never
|
|
47
|
+
proxied.
|
|
48
|
+
2. `POST /v1/responses` or `POST /v1/messages` → the session key is the
|
|
49
|
+
`user` field of the JSON body.
|
|
50
|
+
3. `POST /channels/:id/messages` or `POST /channels/:id/events` (the web
|
|
51
|
+
widget and the relay channel) → the session key is the `session_id` field
|
|
52
|
+
of the JSON body.
|
|
53
|
+
4. Everything else (health checks, `/studio/*`, onboarding, minting a new
|
|
54
|
+
channel session) → no session key, plain round-robin. None of these depend
|
|
55
|
+
on a worker's in-memory `SessionActor` — a Studio read hits the durable
|
|
56
|
+
store, and minting a session has no existing state to be sticky about.
|
|
57
|
+
|
|
58
|
+
A request with a session key is routed by a ketama-style **consistent hash
|
|
59
|
+
ring** over the backend list: the same key always reaches the same backend,
|
|
60
|
+
and adding or removing one backend remaps only ~1/N of the key space, not the
|
|
61
|
+
whole ring — a rolling deploy does not bounce every live session to a new
|
|
62
|
+
owner at once. A request whose key isn't found (a corner-case route) or whose
|
|
63
|
+
key extraction is skipped (see body size cap below) round-robins across all
|
|
64
|
+
backends.
|
|
65
|
+
|
|
66
|
+
The whole request body is always read and forwarded byte-for-byte —
|
|
67
|
+
`INSIKA_ROUTER_BODY_MAX_BYTES` (default 256 KiB) only bounds how much of it
|
|
68
|
+
the router will attempt to parse as JSON while looking for a session key; a
|
|
69
|
+
body over that cap round-robins instead of erroring, and the router logs it.
|
|
70
|
+
|
|
71
|
+
**A request whose chosen backend is unreachable is never retried against a
|
|
72
|
+
different backend** — that backend may already hold a durable, at-most-once
|
|
73
|
+
claim on the task the request names, and retrying elsewhere could
|
|
74
|
+
double-process it. It answers the same retry envelope a single overloaded
|
|
75
|
+
backend would:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{"error": {"class": "Insika::Router::BackendUnavailable", "message": "no backend reachable",
|
|
79
|
+
"retryable": true, "retry_after": 1}}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
SSE responses stream through the router with no added buffering — a client
|
|
83
|
+
watching a long turn sees the same chunks, in the same order, as if it had
|
|
84
|
+
hit the backend directly.
|
|
85
|
+
|
|
86
|
+
## Deploy shape 1 — Railway (N local workers, one replica)
|
|
87
|
+
|
|
88
|
+
Railway's replica load balancer has no sticky option, full stop — this shape
|
|
89
|
+
does not attempt to fix that. What it fixes is the *unsafe* alternative
|
|
90
|
+
(`WEB_CONCURRENCY=N` Falcon workers behind Railway's own port, which is
|
|
91
|
+
exactly the leak `insika doctor` errors on). Instead, run N engine processes
|
|
92
|
+
on different local ports and put the router in front of them, all inside the
|
|
93
|
+
one container Railway load-balances to:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# three engine workers, WEB_CONCURRENCY=1 each (the entrypoint's own
|
|
97
|
+
# `falcon serve --count 1`), on different local ports — never `--count 3` on
|
|
98
|
+
# one port, which is exactly the unsafe fan-out this replaces
|
|
99
|
+
bundle exec falcon serve --bind http://127.0.0.1:9292 --count 1 config.ru &
|
|
100
|
+
bundle exec falcon serve --bind http://127.0.0.1:9293 --count 1 config.ru &
|
|
101
|
+
bundle exec falcon serve --bind http://127.0.0.1:9294 --count 1 config.ru &
|
|
102
|
+
|
|
103
|
+
# the router, bound to the port Railway actually forwards
|
|
104
|
+
INSIKA_ROUTER_PORT=$PORT \
|
|
105
|
+
INSIKA_ROUTER_BACKENDS=http://127.0.0.1:9292,http://127.0.0.1:9293,http://127.0.0.1:9294 \
|
|
106
|
+
bundle exec insika-router
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`insika doctor` treats `WEB_CONCURRENCY>1` as `ok` (not `error`/`warn`) once
|
|
110
|
+
it sees `INSIKA_ROUTER_BACKENDS` or `INSIKA_ROUTER_BACKENDS_DNS` set — it
|
|
111
|
+
cannot verify a router process is actually running at those addresses, only
|
|
112
|
+
that one was configured, same as every other env-based capability check in
|
|
113
|
+
`insika doctor`.
|
|
114
|
+
|
|
115
|
+
## Deploy shape 2 — Kubernetes (a headless Service)
|
|
116
|
+
|
|
117
|
+
Run N engine pods (`WEB_CONCURRENCY=1` each) behind a **headless** Service
|
|
118
|
+
(`clusterIP: None` — this is what makes DNS resolve to one A/AAAA record per
|
|
119
|
+
ready pod instead of a single virtual IP), and the router as its own
|
|
120
|
+
Deployment in front:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
apiVersion: v1
|
|
124
|
+
kind: Service
|
|
125
|
+
metadata:
|
|
126
|
+
name: insika-headless
|
|
127
|
+
spec:
|
|
128
|
+
clusterIP: None
|
|
129
|
+
selector: { app: insika }
|
|
130
|
+
ports: [{ port: 9292 }]
|
|
131
|
+
---
|
|
132
|
+
# insika-router Deployment env:
|
|
133
|
+
env:
|
|
134
|
+
- name: INSIKA_ROUTER_BACKENDS_DNS
|
|
135
|
+
value: insika-headless.default.svc.cluster.local
|
|
136
|
+
- name: INSIKA_ROUTER_BACKEND_PORT
|
|
137
|
+
value: "9292"
|
|
138
|
+
- name: INSIKA_ROUTER_DNS_INTERVAL
|
|
139
|
+
value: "15"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The router holds no session state itself, so it needs no sticky routing in
|
|
143
|
+
front of *itself* — it scales trivially (1-2 replicas behind an ordinary
|
|
144
|
+
`Service`). It re-resolves the headless Service on `INSIKA_ROUTER_DNS_INTERVAL`
|
|
145
|
+
(default 15s) and rebuilds its hash ring only when the resolved pod set
|
|
146
|
+
actually changed. A pod that just became ready is invisible to the router
|
|
147
|
+
until the next resolve — capacity added a few seconds late, never wrong.
|
|
148
|
+
|
|
149
|
+
## Environment variables
|
|
150
|
+
|
|
151
|
+
| Variable | Default | Meaning |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `INSIKA_ROUTER_BACKENDS` | — | Comma-separated backend URLs (static mode). Exactly one of this or the DNS var below. |
|
|
154
|
+
| `INSIKA_ROUTER_BACKENDS_DNS` | — | A headless-Service hostname to re-resolve (DNS mode). Requires `INSIKA_ROUTER_BACKEND_PORT`. |
|
|
155
|
+
| `INSIKA_ROUTER_BACKEND_PORT` | — | The engine port on every DNS-resolved pod. |
|
|
156
|
+
| `INSIKA_ROUTER_DNS_INTERVAL` | `15` | Seconds between DNS re-resolves. |
|
|
157
|
+
| `INSIKA_ROUTER_BODY_MAX_BYTES` | `262144` | Size cap on the session-key JSON peek (never on what is forwarded). |
|
|
158
|
+
| `INSIKA_ROUTER_BACKEND_TIMEOUT` | `10` | Connect/read timeout to a backend, in seconds. |
|
|
159
|
+
| `INSIKA_ROUTER_HOST` | `0.0.0.0` | Bind address for the router itself. |
|
|
160
|
+
| `INSIKA_ROUTER_PORT` | `9090` | Listen port for the router itself. |
|
|
161
|
+
|
|
162
|
+
## A runnable smoke test
|
|
163
|
+
|
|
164
|
+
The shape below is what the router's acceptance criteria were verified
|
|
165
|
+
against: two fake backends and the router in front, run entirely in-process.
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
require "async"; require "async/http/server"; require "async/http/client"
|
|
169
|
+
require "async/http/endpoint"; require "protocol/rack"; require "insika/router"
|
|
170
|
+
|
|
171
|
+
Async do |task|
|
|
172
|
+
echo = ->(name) { ->(env) { [200, {}, ["#{name} #{Rack::Request.new(env).path_info}"]] } }
|
|
173
|
+
%w[9292 9293].each_with_index do |port, i|
|
|
174
|
+
endpoint = Async::HTTP::Endpoint.parse("http://127.0.0.1:#{port}")
|
|
175
|
+
task.async { Async::HTTP::Server.new(Protocol::Rack::Adapter.new(echo["backend-#{i}"]), endpoint).run }
|
|
176
|
+
end
|
|
177
|
+
task.sleep(0.2)
|
|
178
|
+
|
|
179
|
+
pool = Insika::Router::BackendPool.new(static: %w[http://127.0.0.1:9292 http://127.0.0.1:9293])
|
|
180
|
+
app = Insika::Router::App.new(pool: pool)
|
|
181
|
+
endpoint = Async::HTTP::Endpoint.parse("http://127.0.0.1:9090")
|
|
182
|
+
task.async { Async::HTTP::Server.new(Protocol::Rack::Adapter.new(app), endpoint).run }
|
|
183
|
+
task.sleep(0.2)
|
|
184
|
+
|
|
185
|
+
client = Async::HTTP::Client.new(Async::HTTP::Endpoint.parse("http://127.0.0.1:9090"))
|
|
186
|
+
3.times { |i| puts client.post("/v1/responses", {}, [%({"user":"sess-1","i":#{i}})]).read }
|
|
187
|
+
# -> the same "backend-N" answers all three times, even though two backends are up.
|
|
188
|
+
ensure
|
|
189
|
+
task.stop
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## What this deliberately does not attempt
|
|
194
|
+
|
|
195
|
+
- **Railway cross-*replica* routing.** Railway's replica load balancer itself
|
|
196
|
+
has no sticky option and this router cannot sit in front of Railway's own
|
|
197
|
+
edge. Railway stays at one replica; this only raises the ceiling of that one
|
|
198
|
+
replica (N local workers instead of N=1).
|
|
199
|
+
- **A distributed `SessionActor`.** The alternative design — making any
|
|
200
|
+
worker able to safely pick up any session, removing the need for sticky
|
|
201
|
+
routing entirely — is a much larger rewrite (debounce windows, steer
|
|
202
|
+
mailboxes, and SSE fan-out would all have to move into the shared store with
|
|
203
|
+
lease semantics) for the same outcome this router reaches with an unchanged
|
|
204
|
+
engine. Worth revisiting only if this approach turns out not to scale far
|
|
205
|
+
enough.
|
|
206
|
+
- Native WhatsApp/Slack channel routing — those channels are shelved; the
|
|
207
|
+
relay channel rides the same `/v1/responses`-shaped call this router already
|
|
208
|
+
covers.
|
|
209
|
+
|
|
210
|
+
## See also
|
|
211
|
+
|
|
212
|
+
- [DEPLOY.md "The process model"](DEPLOY.md#the-process-model) — the contract
|
|
213
|
+
this router satisfies (FIFO/`collect`/`steer` guarantees, recovery, drain).
|
data/docs/RUNNING-LOCAL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Running locally
|
|
3
|
-
parent:
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Start here
|
|
4
|
+
nav_order: 2
|
|
5
5
|
permalink: /running-local/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -62,7 +62,7 @@ whole surface answers `503`, never open by omission.
|
|
|
62
62
|
| `INSIKA_DB` | — (ephemeral memory) | SQLite path → config + execution survive a restart |
|
|
63
63
|
| `BIND` | `http://localhost:9292` | host:port |
|
|
64
64
|
| `ADMIN_TOKEN` | `local-demo` | token for `/studio` |
|
|
65
|
-
| `
|
|
65
|
+
| `INSIKA_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN` | Bearer for the whole `/v1` + `/a2a` surface |
|
|
66
66
|
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | model |
|
|
67
67
|
|
|
68
68
|
With persistence:
|
|
@@ -122,13 +122,13 @@ prompt files, skills, and one data-tool per file:
|
|
|
122
122
|
> **Data-tool URLs must be literal on the pack path.** The pack import does not
|
|
123
123
|
> resolve `{{env.*}}` — bake the backend base URL into each `tools/*.json` at
|
|
124
124
|
> generation time. (Only the *manifest* path resolves `{{env.*}}`.) See
|
|
125
|
-
> [Tools](TOOLS.md#the-one-gotcha-
|
|
125
|
+
> [Tools](TOOLS.md#the-one-gotcha-envsecret-templating-is-manifest-only).
|
|
126
126
|
|
|
127
127
|
Provision it (runs as a client against the live server; the internal token comes
|
|
128
128
|
from the environment, never disk):
|
|
129
129
|
|
|
130
130
|
```bash
|
|
131
|
-
INSIKA_URL=http://localhost:9292
|
|
131
|
+
INSIKA_URL=http://localhost:9292 INSIKA_GATEWAY_TOKEN=local-demo \
|
|
132
132
|
bundle exec ruby scripts/import_pack.rb /path/to/pack
|
|
133
133
|
```
|
|
134
134
|
|
data/docs/SCHEDULING.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Schedules
|
|
3
|
+
parent: Operate
|
|
4
|
+
nav_order: 2
|
|
5
|
+
permalink: /schedules/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Schedules — recurring turns the engine fires
|
|
9
|
+
|
|
10
|
+
A schedule is a turn nobody has to remember to send: a daily report at 22:00,
|
|
11
|
+
an eval sweep every night, a heartbeat every hour. The engine fires it on its
|
|
12
|
+
own periodic tick — no cron on some other box pointing at an authenticated
|
|
13
|
+
route. (That route still works, if you want it; the built-in trigger just
|
|
14
|
+
removes the homework.)
|
|
15
|
+
|
|
16
|
+
A schedule is **declared on the agent** — pack data, like `followup:` or the
|
|
17
|
+
budget — in one of the same three places every profile field is edited: the
|
|
18
|
+
DSL at import, `POST /v1/agents` in the pack, or the Studio's config form
|
|
19
|
+
(the **Schedules** group on the agent page). Edits are hot: the next pass
|
|
20
|
+
sees them.
|
|
21
|
+
|
|
22
|
+
## The declaration
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
agent = Insika.agent("reporter") do
|
|
26
|
+
schedule "daily_report", cron: "0 22 * * *", tz: "America/Sao_Paulo",
|
|
27
|
+
message: "Run the daily report now.",
|
|
28
|
+
overrides: { turn_timeout: 900, max_tool_calls: 200 }
|
|
29
|
+
schedule "heartbeat", every: 3600, message: "Say you are alive."
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Key | Meaning |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `id` | the schedule's name (the argument). Lowercase, `[a-z][a-z0-9_-]*` |
|
|
36
|
+
| `cron` | a five-field expression (`minute hour day-of-month month day-of-week`), or |
|
|
37
|
+
| `every` | a plain interval in seconds — the two are **exclusive** |
|
|
38
|
+
| `tz` | IANA zone for **cron** materialization (default `Etc/UTC`). `every` never needs it — every comparison runs in UTC |
|
|
39
|
+
| `message` | the synthetic inbound that kicks each run — what the agent "hears" |
|
|
40
|
+
| `session_mode` | `"new"` (default) — a fresh session per run, the report shape; `"fixed"` — one standing session, the "standing assistant" shape |
|
|
41
|
+
| `session_id` | for `fixed` sessions: the standing session (created on first run when missing) |
|
|
42
|
+
| `overrides` | per-run ceilings: `turn_timeout`, `max_tool_calls`, `model` — a report needs a bigger ceiling than a chat turn; the base profile is untouched |
|
|
43
|
+
| `enabled` | `false` pauses the schedule (the Studio toggle / the JSON field) |
|
|
44
|
+
|
|
45
|
+
> **The cadence floor.** Firing rides one claim window per pass — a schedule
|
|
46
|
+
> fires **at most once per window**. That is the true cadence ceiling: an
|
|
47
|
+
> `every: 60` does not fire sixty times a minute, it fires once per window.
|
|
48
|
+
> `doctor` warns when a declared `every` is shorter than the claim window.
|
|
49
|
+
|
|
50
|
+
The Studio renders the schedules section as a JSON array of the same
|
|
51
|
+
declarations plus a read-only card: each schedule's next fire, last run and —
|
|
52
|
+
when a window was skipped — the skip reason. `doctor` parses every
|
|
53
|
+
declaration with the engine's own parser: an invalid cron, an unknown
|
|
54
|
+
runtime zone, a schedule with neither trigger, an unknown override key —
|
|
55
|
+
each is named, per agent, as an error finding.
|
|
56
|
+
|
|
57
|
+
## The engine's triggers: cron subset
|
|
58
|
+
|
|
59
|
+
Five fields, whitespace-separated. Per field: `*` (or `?`), a single value, a
|
|
60
|
+
range (`N-M`), a step (`*/N`, `N-M/N`, `N/N`), or a comma list of those.
|
|
61
|
+
Day-of-week is `0-7` with `7` = Sunday; when **both** day fields are
|
|
62
|
+
restricted the date matches on **either** (standard cron OR semantics).
|
|
63
|
+
`L`, `W`, `#` and month/day names are refused loudly at creation — the engine
|
|
64
|
+
will not silently ignore a cron that only some dates understand.
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
minute hour day-of-month month day-of-week
|
|
68
|
+
0 22 * * *
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Firing: one turn per window, no catch-up
|
|
72
|
+
|
|
73
|
+
Firing rides the tick, gated by its own claim window (one scheduler per
|
|
74
|
+
window across `N` workers — the same claim the outbox and recovery sweeps
|
|
75
|
+
use). Each due schedule is claimed transactionally: the task and the
|
|
76
|
+
schedule's state (last run, last task id, next fire) commit together, so two
|
|
77
|
+
workers racing serialize on the backend's lock and **exactly one fires per
|
|
78
|
+
window**.
|
|
79
|
+
|
|
80
|
+
Three skip rules are part of the contract, all recorded on the schedule and
|
|
81
|
+
shown in the Studio — never silent, never queued:
|
|
82
|
+
|
|
83
|
+
- **late** — the no-catch-up policy. A window more than one claim window in
|
|
84
|
+
the past is **missed, not replayed**: a deploy that was down over 22:00 does
|
|
85
|
+
not fire a 22:00 report at 06:00 the next morning. The schedule's lattice
|
|
86
|
+
advances to the next window and the skip is recorded.
|
|
87
|
+
- **overlap** — the previous run's task is still live (`queued`/`running`/
|
|
88
|
+
`waiting`/`paused`). The window is skipped, recorded, and the next one
|
|
89
|
+
fires. There is no queue buildup: one scheduled run at a time per schedule.
|
|
90
|
+
- **budget** — a **hard** calendar budget (`budget daily: …` on the profile)
|
|
91
|
+
already at/over its cap. The edge would fail the turn anyway; the engine
|
|
92
|
+
refuses to even queue it. A `soft` budget crosses and runs — the ledger
|
|
93
|
+
warns as usual.
|
|
94
|
+
|
|
95
|
+
A bounded run also costs what it costs: the run's usage lands on the same
|
|
96
|
+
`BudgetLedger` the edge enforces, and `billed = total + cached +
|
|
97
|
+
cache_creation` — a long report is cache-heavy, measure the caps against it.
|
|
98
|
+
|
|
99
|
+
## What a schedule run is
|
|
100
|
+
|
|
101
|
+
A first-class turn, stamped `origin: "scheduled"` so a refinement read can
|
|
102
|
+
never mistake the engine's kick for a customer speaking. It enters through the
|
|
103
|
+
pipeline directly (never through the message edge, so no rate-limit/token
|
|
104
|
+
ceiling gate applies — same as a resume), is charged to the ledger like any
|
|
105
|
+
turn, and its result is delivered wherever the agent's outputs go: a channel
|
|
106
|
+
answer, or – for the report shape – nothing at all, when the run publishes an
|
|
107
|
+
artifact instead. The Studio's schedule card links the last run's task.
|
|
108
|
+
|
|
109
|
+
No customer ever receives anything from a schedule unless your agent sends a
|
|
110
|
+
message in reply — the engine contacts no one.
|
|
111
|
+
|
|
112
|
+
## The boundaries
|
|
113
|
+
|
|
114
|
+
- **Not a job queue.** No priorities, no fan-out, no retries of a failed run
|
|
115
|
+
beyond what Recovery already does for any task.
|
|
116
|
+
- **Not the follow-up feature.** `schedule_followup` is a one-shot,
|
|
117
|
+
customer-facing, consent-gated contact from inside a conversation — and it
|
|
118
|
+
keeps being that. These schedules are operator-declared recurring internal
|
|
119
|
+
triggers: no contact policy, no consent, no customer.
|
|
120
|
+
- **Not a replacement for your cron.** The external route stays; this is the
|
|
121
|
+
built-in one.
|
data/docs/SECURITY.md
CHANGED
|
@@ -27,7 +27,7 @@ The layers, from the edge inward:
|
|
|
27
27
|
## The Bearer gate
|
|
28
28
|
|
|
29
29
|
The `/v1` and `/a2a` surface answers only with
|
|
30
|
-
`Authorization: Bearer <
|
|
30
|
+
`Authorization: Bearer <INSIKA_GATEWAY_TOKEN>` (which falls back to `ADMIN_TOKEN`).
|
|
31
31
|
The check runs in the router, **before** any dispatch, against an **allowlist** of
|
|
32
32
|
public routes — so a route added later is closed until someone deliberately publishes
|
|
33
33
|
it. Only these answer without a token:
|
|
@@ -171,7 +171,7 @@ LLM calls**. A *resumed* turn is never re-counted.
|
|
|
171
171
|
> `0` to explicitly disable. A malformed value in the Studio raises a validation
|
|
172
172
|
> error rather than silently disabling a production limit.
|
|
173
173
|
|
|
174
|
-
See [Agents §Layer 4](
|
|
174
|
+
See [Agents §Layer 4](POLICY.md#layer-4-edge-limits-flood-and-spend-control).
|
|
175
175
|
|
|
176
176
|
## Guardrails
|
|
177
177
|
|
|
@@ -203,7 +203,7 @@ Strictness selects the detector categories (`low` = injection only; `medium`
|
|
|
203
203
|
(default) and `high` add sexual and abuse). Safe-reply lookup falls back per
|
|
204
204
|
category: the agent's category reply → the agent's default → the builtin
|
|
205
205
|
category → the builtin default. All of it is editable in the Studio Configuration
|
|
206
|
-
form. See [Agents §Layer 3](
|
|
206
|
+
form. See [Agents §Layer 3](POLICY.md#layer-3-guardrails-content-safety).
|
|
207
207
|
|
|
208
208
|
## Human approval
|
|
209
209
|
|
|
@@ -367,7 +367,7 @@ rotated key or a typo never takes the whole service down. The `insika doctor`
|
|
|
367
367
|
command runs the same checks on demand against a live database. See
|
|
368
368
|
[Deploy](DEPLOY.md#strict-config-and-insika-doctor).
|
|
369
369
|
|
|
370
|
-
## Memory and the right to be forgotten (LGPD
|
|
370
|
+
## Memory and the right to be forgotten (LGPD)
|
|
371
371
|
|
|
372
372
|
Memory is scoped per **`(tenant, customer)`** — the cell `"memory:<tenant>:<customer>"`
|
|
373
373
|
is the isolation boundary. A query against one tenant never touches another's
|
|
@@ -394,7 +394,7 @@ forgotten or aged out. Three operations enforce the right to be forgotten:
|
|
|
394
394
|
outbox deliveries). The operator-mutation audit store records a digest-free line
|
|
395
395
|
("a purge happened, with N records") — content-free by construction.
|
|
396
396
|
|
|
397
|
-
### Distilled facts are personal data
|
|
397
|
+
### Distilled facts are personal data
|
|
398
398
|
|
|
399
399
|
The distillation loop ([Facts](FACTS.md)) writes **proposals** — a distilled
|
|
400
400
|
fact's name and value are personal data, and they are treated like the rest of
|
|
@@ -402,7 +402,7 @@ the memory footprint: `forget_customer` deletes the person's proposals (every
|
|
|
402
402
|
status), `delete_tenant_data` deletes a tenant's, and the `retention_days`
|
|
403
403
|
sweep ages them out with the transcripts they were distilled from. Provenance
|
|
404
404
|
holds: an approved fact is written with `origin: "distilled:<session_ref>"`
|
|
405
|
-
(the
|
|
405
|
+
(the closed set gains one spelling, never an open string). Events and
|
|
406
406
|
audit carry ids and counts only — a fact value never enters the stream, the
|
|
407
407
|
ledger or a log; the evidence excerpt is a link read from the transcript at
|
|
408
408
|
request time, never a copy.
|
|
@@ -412,7 +412,7 @@ deployment (in `single_tenant` the bare cell is the designed customer shape), an
|
|
|
412
412
|
never the session-marked cells. See [Context](CONTEXT.md#memory) and
|
|
413
413
|
[Deploy](DEPLOY.md#strict-config-and-insika-doctor).
|
|
414
414
|
|
|
415
|
-
### Harvest candidates are derived data
|
|
415
|
+
### Harvest candidates are derived data
|
|
416
416
|
|
|
417
417
|
The harvest loop ([Harvest](HARVEST.md)) writes **candidates** — a mined skill
|
|
418
418
|
proposal is behavior instructions, the same trust level as any skill content,
|
|
@@ -428,6 +428,22 @@ product claim the origin sessions' evidence ledger did not see. Events and the
|
|
|
428
428
|
promotion log carry ids, refs and verdicts only — a skill body never enters
|
|
429
429
|
the stream.
|
|
430
430
|
|
|
431
|
+
### Learned knowledge is redacted before it is stored
|
|
432
|
+
|
|
433
|
+
The [Knowledge](KNOWLEDGE.md) loop writes **concepts** extracted from finished
|
|
434
|
+
conversations — store-scoped, not customer-scoped, but written from real
|
|
435
|
+
transcript text, so the write path is deliberately conservative: a concept's
|
|
436
|
+
body goes through the same PII/secret redactor the output guardrail uses
|
|
437
|
+
before it is ever persisted, and `sources` holds session ids only — never
|
|
438
|
+
message content, never a customer identifier. This is the one write path in
|
|
439
|
+
the engine where a model-authored field is rejected outright rather than
|
|
440
|
+
merely validated: `provenance`, `confidence`, `sources` and the timestamps are
|
|
441
|
+
stamped by the engine, so a model cannot self-assign trust it did not earn or
|
|
442
|
+
smuggle a scope into a store-wide record. A repeat sighting that contradicts
|
|
443
|
+
what's on record is never silently merged — the conservative default when
|
|
444
|
+
the engine cannot tell is to flag it for a human, not to guess. Events carry
|
|
445
|
+
names and counts only — a concept's content never enters the stream.
|
|
446
|
+
|
|
431
447
|
## See also
|
|
432
448
|
|
|
433
449
|
- [Agents](AGENTS.md) — the five access layers per agent.
|
data/docs/SKILLS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Skills
|
|
3
|
-
parent:
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Core concepts
|
|
4
|
+
nav_order: 4
|
|
5
5
|
permalink: /skills/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -16,6 +16,14 @@ while paying for the text of only the ones it opens.
|
|
|
16
16
|
|
|
17
17
|
See [`examples/skills/`](https://github.com/guizaols/insika/tree/main/examples/skills/) for a runnable one.
|
|
18
18
|
|
|
19
|
+
**Skills vs. Knowledge.** They share a format — YAML frontmatter over a
|
|
20
|
+
Markdown body, progressive loading — which makes them easy to confuse. A
|
|
21
|
+
skill is **curated**: a human writes it, and it is canonical until a human
|
|
22
|
+
changes it. A [Knowledge](KNOWLEDGE.md) concept is **learned**: the engine
|
|
23
|
+
extracts it from finished conversations, it is `provenance: observed`, and it
|
|
24
|
+
sits below skills in the context priority ladder — earned trust, not
|
|
25
|
+
authored trust.
|
|
26
|
+
|
|
19
27
|
## Format
|
|
20
28
|
|
|
21
29
|
```markdown
|
|
@@ -281,4 +289,5 @@ reading the doctor:
|
|
|
281
289
|
- [Agents](AGENTS.md) — the skills allowlist.
|
|
282
290
|
- [Tools](TOOLS.md) — `load_skill` and deferred-tool progressive disclosure.
|
|
283
291
|
- [Plugins](PLUGINS.md) — shipping skills inside a plugin, and the two extension tiers.
|
|
292
|
+
- [Knowledge](KNOWLEDGE.md) — the learned counterpart: engine-extracted concepts, same format, earned trust.
|
|
284
293
|
- [`examples/skills/`](https://github.com/guizaols/insika/tree/main/examples/skills/) — progressive loading, runnable.
|
data/docs/SOAK.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Soak
|
|
3
|
-
parent: Operate
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Operate
|
|
4
|
+
nav_order: 5
|
|
5
5
|
permalink: /soak/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -88,7 +88,7 @@ insika soak --dry-run --envelope soak-envelope.md
|
|
|
88
88
|
# every precondition, and nothing else
|
|
89
89
|
insika soak --preflight --envelope soak-envelope.md
|
|
90
90
|
|
|
91
|
-
# the run itself (INSIKA_URL +
|
|
91
|
+
# the run itself (INSIKA_URL + INSIKA_GATEWAY_TOKEN, like loadtest.rb)
|
|
92
92
|
INSIKA_URL=https://<target> insika soak --run --envelope soak-envelope.md --out soak-out/
|
|
93
93
|
|
|
94
94
|
# resume after a short outage (the gap is recorded and counts against the window)
|
data/docs/TEMPLATES.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Templates
|
|
3
|
+
parent: Integrate
|
|
4
|
+
nav_order: 6
|
|
5
|
+
permalink: /templates/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Templates
|
|
9
|
+
|
|
10
|
+
Example agents shipped **inside the gem** — `lib/insika/templates/<name>/`,
|
|
11
|
+
one DSL file per template. `insika new <name>` copies it for you to run and
|
|
12
|
+
edit; the same file is what the Studio gallery evaluates to create the
|
|
13
|
+
agent from a click. One source of truth, two doors — never a parallel pack
|
|
14
|
+
format to drift.
|
|
15
|
+
|
|
16
|
+
## The gallery
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
insika new --list
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
travel-planner Starter Weather + currency data-tools against keyless public APIs …
|
|
24
|
+
research-analyst Advanced Insika.system fan-out — three specialist subagents research …
|
|
25
|
+
daily-digest Always-on A recurring schedule plus save_artifact build and publish …
|
|
26
|
+
review-panel Teams Two specialists reviewed in parallel by a synthesizing lead …
|
|
27
|
+
repo-explorer MCP Live MCP tool-loop over http — answers questions about any …
|
|
28
|
+
browser-agent MCP Live MCP tool-loop over stdio — navigates and summarizes …
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
insika new travel-planner # copies ./travel-planner/{agent.rb,README.md}
|
|
33
|
+
insika new travel-planner my-trip # ...into ./my-trip/ instead
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The CLI prints the exact run line, including any env the template needs
|
|
37
|
+
**set** (not just available as an override) — a stdio MCP template needs
|
|
38
|
+
`INSIKA_MCP_STDIO=1`, for instance. The generated script *is* the editing
|
|
39
|
+
surface: no Gemfile, no questionnaire, no placeholders to fill in.
|
|
40
|
+
|
|
41
|
+
The same roster appears as a "+ from template" gallery on the Studio
|
|
42
|
+
`/studio/agents` page — clicking **Create** dispatches the identical
|
|
43
|
+
`:create_agent` (and, for a system template, one per agent) the CLI-run
|
|
44
|
+
copy would produce. A template marked `studio: false` in its frontmatter
|
|
45
|
+
(none in wave 1) shows a "CLI-only for now" note instead of a button —
|
|
46
|
+
reserved for a template whose value is a durable workflow, until workflow
|
|
47
|
+
import into a running store exists.
|
|
48
|
+
|
|
49
|
+
## The MCP trail: point it at your own server
|
|
50
|
+
|
|
51
|
+
`repo-explorer` (http) and `browser-agent` (stdio) are not showcases for
|
|
52
|
+
one MCP vendor — they demonstrate exactly how to plug **any** MCP server
|
|
53
|
+
into an agent. Each ships with a working, keyless default so
|
|
54
|
+
`insika new` + the run line works with zero setup, but the server is a
|
|
55
|
+
config value:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
MCP_URL=https://your-mcp-server/mcp DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "..."
|
|
59
|
+
MCP_COMMAND=your-mcp-server INSIKA_MCP_STDIO=1 DEEPSEEK_API_KEY=sk-... ruby browser-agent/agent.rb "..."
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Swap the env var, rewrite the instructions for the new server's tools —
|
|
63
|
+
nothing else in `agent.rb` changes.
|
|
64
|
+
|
|
65
|
+
## Writing a template
|
|
66
|
+
|
|
67
|
+
A template is `lib/insika/templates/<name>/agent.rb` + `README.md`.
|
|
68
|
+
|
|
69
|
+
**The frontmatter contract** — a `# ---` … `# ---` comment block, YAML
|
|
70
|
+
inside, right after the standard `# frozen_string_literal: true` (that
|
|
71
|
+
magic comment is skipped automatically — a template doesn't have to break
|
|
72
|
+
the convention every other file in the gem follows):
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# frozen_string_literal: true
|
|
76
|
+
|
|
77
|
+
# ---
|
|
78
|
+
# title: My Template
|
|
79
|
+
# trail: Starter | Advanced | Always-on | Teams | MCP
|
|
80
|
+
# description: one line, shown in the CLI list and the Studio card.
|
|
81
|
+
# capabilities: comma, separated, tags
|
|
82
|
+
# studio: true # optional, default true
|
|
83
|
+
# env: SOME_REQUIRED_VAR # optional — env the run line must SET, not just may override
|
|
84
|
+
# requires: Node.js and npm # optional — a local dependency beyond the gem + a provider key
|
|
85
|
+
# ---
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**The two-doors mechanics**, in the file itself:
|
|
89
|
+
|
|
90
|
+
1. `require "insika"` — gem-style, never `require_relative` (the file gets
|
|
91
|
+
copied out of the gem into an arbitrary directory).
|
|
92
|
+
2. Build the agent/system as a normal top-level local: `travel = Insika.agent(...) { ... }`.
|
|
93
|
+
3. Guard the CLI demo footer: `if __FILE__ == $PROGRAM_NAME ... end`. False
|
|
94
|
+
whenever `Insika::Templates.evaluate` loads the file (never true from
|
|
95
|
+
inside the gem/Studio process), so the Studio door never makes a network
|
|
96
|
+
call, prints anything, or parses `ARGV`.
|
|
97
|
+
4. End the file with the **bare** built value (`travel`, `panel`, `team`,
|
|
98
|
+
…) as its last expression — `evaluate` runs the file in an isolated
|
|
99
|
+
`instance_eval` and returns whatever that last expression is. No
|
|
100
|
+
registration call, no second format.
|
|
101
|
+
5. **No top-level constants.** `instance_eval`'s isolation keeps local
|
|
102
|
+
variables and `def`s from leaking into the NEXT template evaluated in
|
|
103
|
+
the same process, but Ruby scopes a `CONST = ...` assignment lexically,
|
|
104
|
+
not by `self` — it would leak. Use a local variable (closures see it
|
|
105
|
+
fine from inside a `do...end` block) — every wave-1 template does.
|
|
106
|
+
|
|
107
|
+
**Engine-only rules** (enforced by the lint below):
|
|
108
|
+
provider-agnostic (one provider key), zero tenant/store data, external
|
|
109
|
+
calls only to keyless public APIs, every tool/mcp group covered by an
|
|
110
|
+
explicit allowlist.
|
|
111
|
+
|
|
112
|
+
**Declaring an `mcp` server** auto-adds `"mcp:<name>"` to that agent's
|
|
113
|
+
`tools_allow_groups` (`Insika::DSL::Builder#mcp`) — without it the agent
|
|
114
|
+
could never call the MCP tool it just declared, since `PackImporter`
|
|
115
|
+
forces `tools_allow: []` for a pack with no `data_tool`. A **system**-level
|
|
116
|
+
`mcp` (declared outside any member `agent { }` block) grants no agent
|
|
117
|
+
access by itself — declare it inside the specific agent that needs it.
|
|
118
|
+
|
|
119
|
+
## The E3 lint
|
|
120
|
+
|
|
121
|
+
`spec/insika/templates_spec.rb` iterates `Insika::Templates.all` for real —
|
|
122
|
+
one example per template name, so a broken new template fails by name, not
|
|
123
|
+
a generic loop assertion. It checks, per template:
|
|
124
|
+
|
|
125
|
+
- evaluates cleanly to schema-valid pack(s) (`id`/`model` present);
|
|
126
|
+
- every `data_tool` it declares is in that SAME pack's `tools_allow`;
|
|
127
|
+
- every `mcp` instance's group is granted by SOME agent in the pack(s);
|
|
128
|
+
- every referenced host (`data_tool` URL, http/sse `mcp` URL) passes
|
|
129
|
+
`Insika::EgressGuard.violation` — public HTTPS only, same guard a live
|
|
130
|
+
turn would apply;
|
|
131
|
+
- no hardcoded secret-shaped literal (`sk-...`, a long `Bearer ...` token)
|
|
132
|
+
in the source.
|
|
133
|
+
|
|
134
|
+
Run it before adding a template: `bundle exec rspec spec/insika/templates_spec.rb`.
|