insika 0.2.0 → 0.7.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 +325 -17
- data/README.md +52 -11
- data/bin/insika +895 -8
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +229 -163
- data/docs/API.md +56 -0
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +111 -21
- data/docs/CONTEXT.md +132 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +75 -4
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +146 -2
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +116 -0
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +16 -10
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +65 -6
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +17 -12
- data/docs/RELEASING.md +27 -0
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +6 -6
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +79 -2
- data/docs/SKILLS.md +13 -4
- data/docs/SOAK.md +127 -0
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +220 -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 +115 -0
- data/docs/improve.md +20 -0
- data/docs/index.md +9 -6
- 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 +202 -10
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/budget_ledger.rb +34 -3
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +119 -14
- data/lib/insika/channels/relay.rb +77 -3
- data/lib/insika/channels/web.rb +2 -2
- data/lib/insika/chat_builder.rb +105 -2
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/cancel_followup.rb +49 -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 +107 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/send_message.rb +103 -6
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +21 -3
- data/lib/insika/context/fragment.rb +7 -3
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +57 -23
- data/lib/insika/context/providers/skill.rb +2 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +38 -2
- 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/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +889 -11
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +69 -82
- data/lib/insika/dsl/server_boot.rb +92 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +186 -5
- data/lib/insika/edge_limiter.rb +28 -7
- data/lib/insika/env_schema.rb +25 -3
- data/lib/insika/errors.rb +11 -0
- data/lib/insika/evals/assertions.rb +3 -2
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +29 -6
- 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 +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +869 -76
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +198 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +17 -2
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- 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 +382 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +7 -2
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +22 -1
- data/lib/insika/outbox_store.rb +42 -4
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +29 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/queue_policy.rb +4 -1
- data/lib/insika/refinement/proposer.rb +1 -1
- data/lib/insika/reliability.rb +32 -6
- data/lib/insika/retention.rb +316 -0
- 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/routing.rb +101 -0
- data/lib/insika/safety/config.rb +44 -4
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +32 -113
- data/lib/insika/safety/factory.rb +15 -2
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +1 -1
- data/lib/insika/safety/moderator.rb +1 -1
- data/lib/insika/safety/output_filter.rb +9 -5
- data/lib/insika/safety/output_validator.rb +11 -5
- 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/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +247 -17
- data/lib/insika/server/rack_app.rb +21 -1
- data/lib/insika/server/responses.rb +40 -1
- data/lib/insika/session_actor.rb +8 -4
- data/lib/insika/session_store.rb +65 -2
- data/lib/insika/settings_store.rb +10 -0
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/skill_catalog.rb +16 -0
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/store.rb +10 -1
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1437 -54
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +23 -23
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +308 -10
- data/lib/insika/studio/nav_icons.rb +19 -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 +62 -278
- data/lib/insika/studio/views/agents.erb +71 -54
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +6 -3
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +106 -66
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +15 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/session.erb +233 -124
- data/lib/insika/studio/views/settings.erb +41 -1
- data/lib/insika/studio/views/skills.erb +1 -2
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +24 -10
- data/lib/insika/task_store.rb +21 -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 +38 -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 +58 -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/testing/store_contract.rb +27 -6
- data/lib/insika/tick.rb +47 -11
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_definition.rb +17 -7
- data/lib/insika/tool_envelope.rb +69 -0
- data/lib/insika/tool_manifest.rb +5 -1
- data/lib/insika/tools/data_defined_tool.rb +10 -0
- data/lib/insika/tools/generate_image.rb +89 -0
- 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/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +49 -1
- data/lib/insika/turn_timing.rb +22 -2
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +472 -13
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +149 -3
- metadata +181 -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,14 +1,14 @@
|
|
|
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
|
|
|
8
8
|
# Running the Insika locally
|
|
9
9
|
|
|
10
10
|
Boots the engine single-process, serving `/studio` and `/v1/*` against a demo
|
|
11
|
-
agent (the `
|
|
11
|
+
agent (the `demo` agent on DeepSeek). Every message runs the **same**
|
|
12
12
|
`send_message` the API runs — real tools, skills, and memory.
|
|
13
13
|
|
|
14
14
|
## Boot
|
|
@@ -26,7 +26,7 @@ Open `http://localhost:9292`:
|
|
|
26
26
|
| URL | What |
|
|
27
27
|
|-----|------|
|
|
28
28
|
| `/studio` | management UI (log in with the token; default `local-demo`) |
|
|
29
|
-
| `/studio/chats` | chat with the demo agent (`agent:
|
|
29
|
+
| `/studio/chats` | chat with the demo agent (`agent: demo`, `session_id: web`, multi-turn ready) |
|
|
30
30
|
| `/studio/tasks` | tasks / approvals console |
|
|
31
31
|
| `/v1/responses` | OpenAI-Responses ingress (Bearer) — the drop-in API contract |
|
|
32
32
|
| `/v1/agents` | provisioning by definition/pack (Bearer) — `POST` imports, `DELETE /:id` removes |
|
|
@@ -82,7 +82,7 @@ the `model`:
|
|
|
82
82
|
curl -N http://localhost:9292/v1/responses \
|
|
83
83
|
-H "Authorization: Bearer local-demo" \
|
|
84
84
|
-H "Content-Type: application/json" \
|
|
85
|
-
-d '{ "model": "
|
|
85
|
+
-d '{ "model": "demo", "user": "web", "stream": true, "input": "hello" }'
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
`user` is the session id (any stable id for a multi-turn conversation).
|
|
@@ -122,7 +122,7 @@ 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):
|
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
|
@@ -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,6 +367,83 @@ 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)
|
|
371
|
+
|
|
372
|
+
Memory is scoped per **`(tenant, customer)`** — the cell `"memory:<tenant>:<customer>"`
|
|
373
|
+
is the isolation boundary. A query against one tenant never touches another's
|
|
374
|
+
cells. A turn with no customer falls back to the session's own **marked** cell
|
|
375
|
+
(`"memory:chat:<session id>"`) — the marker keeps the Studio drill and the doctor
|
|
376
|
+
from ever reading a conversation as a customer (a bare `memory:<id>` cell is
|
|
377
|
+
indistinguishable from a single-tenant customer ref). Cells written before the
|
|
378
|
+
marker (bare session ids) may still appear as customers in the drill until
|
|
379
|
+
forgotten or aged out. Three operations enforce the right to be forgotten:
|
|
380
|
+
|
|
381
|
+
- **Retention** — the `memory_ttl_days` setting (platform default per cell, or an
|
|
382
|
+
ops-authored per-tenant map) sweeps facts older than the window. A per-fact
|
|
383
|
+
`expires_at` override wins over the cell TTL. Both run on the daily retention
|
|
384
|
+
tick, **not** gated by `retention_days` (the conversation-footprint window).
|
|
385
|
+
Note the interaction: an explicit `expires_at` also exempts the fact from the
|
|
386
|
+
`retention_days` age-based sweep — an explicit date owns that fact's life, so a
|
|
387
|
+
far-future date makes the fact immune to *both* age passes. Export and forget
|
|
388
|
+
still see it; the only way to remove it early is the Customers drill.
|
|
389
|
+
- **Export** — `export_customer_memory` returns the full fact + note content to the
|
|
390
|
+
**operator** (the Studio turns it into a JSON download). The emitted event carries
|
|
391
|
+
counts only — the event stream, the audit store and every log stay content-free.
|
|
392
|
+
- **Forget** — `forget_customer` purges the customer's memory cell AND their
|
|
393
|
+
sessions (and everything those sessions left behind: traces, tasks, checkpoints,
|
|
394
|
+
outbox deliveries). The operator-mutation audit store records a digest-free line
|
|
395
|
+
("a purge happened, with N records") — content-free by construction.
|
|
396
|
+
|
|
397
|
+
### Distilled facts are personal data
|
|
398
|
+
|
|
399
|
+
The distillation loop ([Facts](FACTS.md)) writes **proposals** — a distilled
|
|
400
|
+
fact's name and value are personal data, and they are treated like the rest of
|
|
401
|
+
the memory footprint: `forget_customer` deletes the person's proposals (every
|
|
402
|
+
status), `delete_tenant_data` deletes a tenant's, and the `retention_days`
|
|
403
|
+
sweep ages them out with the transcripts they were distilled from. Provenance
|
|
404
|
+
holds: an approved fact is written with `origin: "distilled:<session_ref>"`
|
|
405
|
+
(the closed set gains one spelling, never an open string). Events and
|
|
406
|
+
audit carry ids and counts only — a fact value never enters the stream, the
|
|
407
|
+
ledger or a log; the evidence excerpt is a link read from the transcript at
|
|
408
|
+
request time, never a copy.
|
|
409
|
+
|
|
410
|
+
The doctor's `memory-scopes` check flags bare cells only in a `multi_tenant`
|
|
411
|
+
deployment (in `single_tenant` the bare cell is the designed customer shape), and
|
|
412
|
+
never the session-marked cells. See [Context](CONTEXT.md#memory) and
|
|
413
|
+
[Deploy](DEPLOY.md#strict-config-and-insika-doctor).
|
|
414
|
+
|
|
415
|
+
### Harvest candidates are derived data
|
|
416
|
+
|
|
417
|
+
The harvest loop ([Harvest](HARVEST.md)) writes **candidates** — a mined skill
|
|
418
|
+
proposal is behavior instructions, the same trust level as any skill content,
|
|
419
|
+
and it is store-scoped, never customer-scoped: `forget_customer` does NOT reach
|
|
420
|
+
it (a candidate's skill body is not customer content). Candidates reference
|
|
421
|
+
sessions by id and their evidence excerpt is the transcript rendered at request
|
|
422
|
+
time through the output filter — never a copy. `delete_tenant_data` purges a
|
|
423
|
+
tenant's candidates, promotion rows and markers; the `retention_days` sweep
|
|
424
|
+
ages them out with the transcripts they were derived from (they are
|
|
425
|
+
re-derivable — pruning is never data loss). **No auto-application**: nothing
|
|
426
|
+
reaches the catalog without a human click, and the grounding filter refuses any
|
|
427
|
+
product claim the origin sessions' evidence ledger did not see. Events and the
|
|
428
|
+
promotion log carry ids, refs and verdicts only — a skill body never enters
|
|
429
|
+
the stream.
|
|
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
|
+
|
|
370
447
|
## See also
|
|
371
448
|
|
|
372
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
|
|
@@ -219,8 +227,8 @@ Write one with `agent:`, and remove it the same way (which un-specializes, leavi
|
|
|
219
227
|
the shared skill in place):
|
|
220
228
|
|
|
221
229
|
```ruby
|
|
222
|
-
dispatch(:write_skill, { name: "escalation-to-human", agent: "store-
|
|
223
|
-
dispatch(:delete_skill, { name: "escalation-to-human", agent: "store-
|
|
230
|
+
dispatch(:write_skill, { name: "escalation-to-human", agent: "store-demo", content: md })
|
|
231
|
+
dispatch(:delete_skill, { name: "escalation-to-human", agent: "store-demo" })
|
|
224
232
|
```
|
|
225
233
|
|
|
226
234
|
In the Studio: **Skills → specialize for this agent**, which seeds the override from
|
|
@@ -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
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Soak
|
|
3
|
+
parent: Operate
|
|
4
|
+
nav_order: 5
|
|
5
|
+
permalink: /soak/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# SOAK — does it degrade over uptime?
|
|
9
|
+
|
|
10
|
+
The soak is the third question the measurement toolbox answers, and it is the
|
|
11
|
+
one the other two cannot:
|
|
12
|
+
|
|
13
|
+
| Tool | Question | Drives |
|
|
14
|
+
|------|----------|--------|
|
|
15
|
+
| [`scripts/bench.rb`](BENCHMARK.md) | How much overhead does the engine itself add? | publishable claims |
|
|
16
|
+
| [Load test](LOADTEST.md) | What does a burst look like, end to end? | topology + provider choice |
|
|
17
|
+
| **`insika soak`** | **Does the deploy degrade over 72 hours of steady load?** | the cut argument: "it does not tax uptime" |
|
|
18
|
+
|
|
19
|
+
A load test fires a wave, joins it, and reports. A soak **sustains a declared
|
|
20
|
+
arrival rate for days** and asks whether the *process itself* — resident memory,
|
|
21
|
+
per-turn local work, restarts — degrades with uptime. A leak that only shows at
|
|
22
|
+
hour 40 is invisible to a 10-minute bench.
|
|
23
|
+
|
|
24
|
+
## The protocol in one paragraph
|
|
25
|
+
|
|
26
|
+
1. **Freeze the envelope first.** A deployment-side envelope file declares the
|
|
27
|
+
load shape, the gated ceilings and the isolation contract — *before* the
|
|
28
|
+
first turn is fired. Its SHA-256 is stamped into every hourly snapshot;
|
|
29
|
+
editing it mid-run turns the run `invalid` instead of producing a verdict.
|
|
30
|
+
2. **Preflight.** `insika soak --preflight` refuses to start unless the target
|
|
31
|
+
answers `/v1/vitals` with a readable RSS, turns carry the `INSIKA_TURN_TIMING`
|
|
32
|
+
breakdown *and* a `usage` block, the pid is stable, and the target host
|
|
33
|
+
matches the envelope. Each check is a named refusal, not a runtime warning.
|
|
34
|
+
3. **Run.** `insika soak --run` fires the arrival process (Poisson, seeded,
|
|
35
|
+
concurrency-capped), polls vitals hourly, and appends every observation to
|
|
36
|
+
`<out>/<run>.jsonl` as it happens — a runner that dies at hour 60 must
|
|
37
|
+
leave 60 usable hours behind. No retry, no self-healing: a failed turn is
|
|
38
|
+
evidence.
|
|
39
|
+
4. **Read the verdict.** `insika soak --verify <file>.jsonl.gz` recomputes the
|
|
40
|
+
whole verdict offline from the raw records — no network, no target, no
|
|
41
|
+
clock. Two people running it get the same answer; so does CI.
|
|
42
|
+
|
|
43
|
+
## What is gated, and what is only reported
|
|
44
|
+
|
|
45
|
+
The gate is on the part **Insika owns**:
|
|
46
|
+
|
|
47
|
+
| Gated (binding) | Reported (never gated) |
|
|
48
|
+
|-----------------|------------------------|
|
|
49
|
+
| `rss_growth_ratio` — upper 95% bound of the RSS fit, hour 6 → hour 72 | `rss_slope_mb_per_day`, `rss_peak_mb` |
|
|
50
|
+
| `prep_p95_drift_ratio` — p95 of the local per-turn work (`prep_ms`: context build, policy, guardrails, chat assembly), last 6 h vs first measured 6 h | `ttft_p95_ms`, `total_p95` hourly series |
|
|
51
|
+
| `restarts_max` — any `boot_id` **or** `pid` change is a fail; there is no "declared deploy" exception | `heap_growth_ratio`, `db_growth_mb` |
|
|
52
|
+
| `error_rate_ceiling`, `no_usage_rate_ceiling` | `cost_usd`, `tokens_per_turn` |
|
|
53
|
+
| calibrated absolute ceilings (written from the E1 dry run, ×2) | — |
|
|
54
|
+
|
|
55
|
+
Time-to-first-token is **reported, never gated**: it was measured at ~720 ms and
|
|
56
|
+
shown to be ~100% provider. Gating on a number the engine cannot move would mean
|
|
57
|
+
failing a clean soak because the provider had a bad afternoon — and then
|
|
58
|
+
loosening the envelope, which the protocol forbids.
|
|
59
|
+
|
|
60
|
+
The **`no_usage_rate`** ceiling is the trap detector. The edge limiter answers a
|
|
61
|
+
breach with a canned reply, zero LLM calls and HTTP 200 — so a soak that ran
|
|
62
|
+
entirely into the limiter looks like thousands of successful turns with a
|
|
63
|
+
gorgeous 2 ms p95 and flat memory. A turn with no `usage` block called no model;
|
|
64
|
+
the ceiling keeps that from reading as a pass.
|
|
65
|
+
|
|
66
|
+
## The isolation contract
|
|
67
|
+
|
|
68
|
+
- **Staging or a dedicated tenant, never a production tenant.** The run writes
|
|
69
|
+
~4,000 sessions and tasks into the store; against production that is
|
|
70
|
+
synthetic data mixed into real customer history. The envelope names the
|
|
71
|
+
tenant (`soak`), so `delete_tenant_data` cleans the entire run in one
|
|
72
|
+
command.
|
|
73
|
+
- **Its own agent, with the edge limits off.** At a real ~48k tokens per turn,
|
|
74
|
+
the production token ceiling admits about 10 turns per hour — a 60 turns/h
|
|
75
|
+
soak against a production-configured agent measures its own limiter within
|
|
76
|
+
minutes. `chat_rate_limit: 0` and `agent_token_ceiling: 0` on the soak agent
|
|
77
|
+
are part of the frozen envelope, not an afternoon decision.
|
|
78
|
+
- **Deploys are frozen for the window.** Any restart is a fail, including a
|
|
79
|
+
deployment — a marker the operator writes after the fact would turn every
|
|
80
|
+
crash into a deploy.
|
|
81
|
+
|
|
82
|
+
## Running it
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# the plan + one sample request, no traffic
|
|
86
|
+
insika soak --dry-run --envelope soak-envelope.md
|
|
87
|
+
|
|
88
|
+
# every precondition, and nothing else
|
|
89
|
+
insika soak --preflight --envelope soak-envelope.md
|
|
90
|
+
|
|
91
|
+
# the run itself (INSIKA_URL + OPENCLAW_GATEWAY_TOKEN, like loadtest.rb)
|
|
92
|
+
INSIKA_URL=https://<target> insika soak --run --envelope soak-envelope.md --out soak-out/
|
|
93
|
+
|
|
94
|
+
# resume after a short outage (the gap is recorded and counts against the window)
|
|
95
|
+
insika soak --run --resume soak-out/staging-2026-08-20T09-00-00Z.jsonl
|
|
96
|
+
|
|
97
|
+
# the verdict, recomputed offline — the only step that needs no target
|
|
98
|
+
insika soak --verify soak-out/staging-2026-08-20T09-00-00Z.jsonl.gz
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Duration, rate and ceilings all come from the envelope — there is no
|
|
102
|
+
`--duration` override, so a short run cannot be passed off as a soak.
|
|
103
|
+
|
|
104
|
+
## Reading the verdict
|
|
105
|
+
|
|
106
|
+
`insika soak --verify` prints one of four, in this order of precedence:
|
|
107
|
+
|
|
108
|
+
- **`invalid`** — the snapshots carry more than one envelope hash, more than
|
|
109
|
+
one process id appeared under one boot generation, or the file is materially
|
|
110
|
+
truncated. **No metric is reported at all**; the file cannot be argued with.
|
|
111
|
+
- **`insufficient`** — coverage is short: a gap over 15 minutes, an hour under
|
|
112
|
+
the turn floor, or a runner that did not reach hour 72. Short coverage
|
|
113
|
+
restarts the window; it is never rounded up into a pass.
|
|
114
|
+
- **`fail`** — a gated ceiling was breached. The report names each one and
|
|
115
|
+
prints the leak-hunt starting point: whether the Ruby heap grew with RSS
|
|
116
|
+
(Ruby-side retention) or stayed flat while RSS climbed (allocator or a
|
|
117
|
+
native buffer).
|
|
118
|
+
- **`pass`** — none of the above.
|
|
119
|
+
|
|
120
|
+
A fail means *find the leak* — never cut, and never loosen the envelope.
|
|
121
|
+
|
|
122
|
+
## See also
|
|
123
|
+
|
|
124
|
+
- [Observability](OBSERVABILITY.md) — `GET /v1/vitals`, the process readings
|
|
125
|
+
the soak samples hourly.
|
|
126
|
+
- [Deploy](DEPLOY.md) — the process model; `WEB_CONCURRENCY=1` is a soak
|
|
127
|
+
precondition, not only a queue-semantics one.
|