insika 0.3.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 +180 -0
- data/README.md +45 -10
- data/bin/insika +684 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +94 -403
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +9 -7
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +71 -3
- 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 +2 -2
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +15 -10
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +3 -3
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +22 -6
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +2 -2
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +152 -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 +73 -16
- 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 +22 -2
- 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/context/priority.rb +2 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- 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 +176 -8
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +60 -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 +16 -1
- 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 +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +231 -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/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 +8 -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 +1 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +567 -45
- 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 +46 -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 +39 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- 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/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -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_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 +57 -0
- metadata +105 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
data/docs/PLUGINS.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Plugins
|
|
3
|
-
parent:
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Integrate
|
|
4
|
+
nav_order: 5
|
|
5
5
|
permalink: /plugins/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -131,16 +131,34 @@ keyword is itself an error rather than being silently ignored.
|
|
|
131
131
|
|
|
132
132
|
### Discovery and enabling
|
|
133
133
|
|
|
134
|
+
The loader runs **at boot, in every composition root** — the server wirings and
|
|
135
|
+
a DSL-run agent alike — single-threaded, before the first request is accepted.
|
|
136
|
+
A plugin therefore either exists for the whole life of the process or not at
|
|
137
|
+
all; there is no half-loaded state a turn can observe.
|
|
138
|
+
|
|
134
139
|
Plugins come from three kinds of root, and they differ in **who has to say yes**:
|
|
135
140
|
|
|
136
141
|
| Root | How it is found | Enabled by default |
|
|
137
142
|
|---|---|---|
|
|
138
143
|
| **Gem** | the gem calls `Insika::Plugin.announce(__dir__)` when its `lib/` loads | **yes** — installing it is the consent |
|
|
139
|
-
| **Workspace** |
|
|
140
|
-
| **Bundled** | `plugins/` in this repo | no —
|
|
144
|
+
| **Workspace** | the directory named by `INSIKA_PLUGIN_DIR` | no — list the id in `INSIKA_PLUGINS` |
|
|
145
|
+
| **Bundled** | `plugins/` in this repo | no — list the id in `INSIKA_PLUGINS` |
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
INSIKA_PLUGIN_DIR=/srv/insika/plugins # workspace root to scan
|
|
149
|
+
INSIKA_PLUGINS=weather,acme # ids to enable from workspace/bundled roots
|
|
150
|
+
INSIKA_PLUGINS_DISABLED=insika-code # ids that never load, no matter what
|
|
151
|
+
```
|
|
141
152
|
|
|
142
|
-
`
|
|
143
|
-
|
|
153
|
+
`INSIKA_PLUGINS_DISABLED` is an absolute veto: an id listed there never loads,
|
|
154
|
+
even if it is enabled or shipped by an installed gem (deny wins, the same rule
|
|
155
|
+
as every allowlist in the engine).
|
|
156
|
+
|
|
157
|
+
When two roots ship the same `id`, precedence is workspace → gems → bundled:
|
|
158
|
+
the first root wins, so an operator's local copy always overrides an installed
|
|
159
|
+
one. Skills and prompts a plugin ships join the catalogs at the **lowest**
|
|
160
|
+
precedence for the same reason — a workspace or Studio-authored skill beats a
|
|
161
|
+
plugin's same-named one.
|
|
144
162
|
|
|
145
163
|
A gem announces itself explicitly — Insika never scans the load path or your
|
|
146
164
|
installed gems:
|
|
@@ -173,6 +191,33 @@ config_schema:
|
|
|
173
191
|
The manifest is committed; the secret is not. This mirrors how data tools handle
|
|
174
192
|
`{{secret.*}}`.
|
|
175
193
|
|
|
194
|
+
## Why it is built this way
|
|
195
|
+
|
|
196
|
+
Five decisions carry the whole design; knowing them explains every behavior
|
|
197
|
+
above.
|
|
198
|
+
|
|
199
|
+
1. **Data before code.** Most agent runtimes make code the unit of extension
|
|
200
|
+
and a restart the price of every integration. Insika inverts that: tier 1
|
|
201
|
+
(data tools, MCP imports, skills) covers most integrations hot, with no
|
|
202
|
+
deploy, and tier 2 exists only for what genuinely needs to run in-process.
|
|
203
|
+
A smaller code-plugin surface is a feature — less to audit, less to break.
|
|
204
|
+
2. **Manifest before code.** Discovery never executes anything: the manifest is
|
|
205
|
+
read, validated, and gated first, and only then is the entry `require`d.
|
|
206
|
+
You can inventory, enable, and veto plugins without running them.
|
|
207
|
+
3. **Contracts are the public API.** Anything addressable by name — tools,
|
|
208
|
+
workflows, capabilities, channels — must be declared in `contracts`, and an
|
|
209
|
+
undeclared registration is ignored with a warning. A plugin cannot quietly
|
|
210
|
+
widen its surface between versions, and a channel cannot mount a route
|
|
211
|
+
nobody asked for.
|
|
212
|
+
4. **Announce, never scan.** A gem opts in with one explicit
|
|
213
|
+
`Insika::Plugin.announce` call; Insika never walks the load path or your
|
|
214
|
+
installed gems looking for candidates. What loads is exactly what was
|
|
215
|
+
announced or configured — auditable from the boot log alone.
|
|
216
|
+
5. **One bad plugin never takes the deployment down.** Registration is staged
|
|
217
|
+
and committed atomically; a raise inside `register(api)` rolls back
|
|
218
|
+
everything that plugin staged and boot continues. Enablement is explicit,
|
|
219
|
+
and deny wins.
|
|
220
|
+
|
|
176
221
|
## Publishing a plugin
|
|
177
222
|
|
|
178
223
|
- **Name it `insika-plugin-<thing>`.** The convention *is* the registry for now:
|
data/docs/POLICY.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Limits and policy
|
|
3
|
+
parent: Core concepts
|
|
4
|
+
nav_order: 2
|
|
5
|
+
permalink: /policy/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Limits and policy
|
|
9
|
+
|
|
10
|
+
What an agent may do is layered. Each layer is independent, opt-in where it
|
|
11
|
+
matters, and editable hot — the same profile keys covered in [Agents](AGENTS.md),
|
|
12
|
+
grouped here because they answer one question: what is this agent allowed to do,
|
|
13
|
+
and what stops it when it tries to do more.
|
|
14
|
+
|
|
15
|
+
What an agent may do is layered. Each layer is independent, opt-in where it
|
|
16
|
+
matters, and editable hot.
|
|
17
|
+
|
|
18
|
+
## Layer 1: Tools (what it can call)
|
|
19
|
+
|
|
20
|
+
`tools_allow` / `tools_deny` / `tools_allow_groups` decide which tools enter the
|
|
21
|
+
turn's tool-loop, enforced by the tool-allowlist policy. See [Tools](TOOLS.md)
|
|
22
|
+
for how tools are defined and registered, and [`examples/data-tool/`](https://github.com/guizaols/insika/tree/main/examples/data-tool/).
|
|
23
|
+
|
|
24
|
+
## Layer 2: Policies and approvals
|
|
25
|
+
|
|
26
|
+
Policies are named entries evaluated before the turn runs. Builtins cover
|
|
27
|
+
tool-, skill-, and workflow-allowlisting, plus **`ApprovalRequired`** — which
|
|
28
|
+
does not allow or deny but *tags* a tool as needing human approval. Set
|
|
29
|
+
`approvals_required: [tool names]`; the gate then fires when the model tries to
|
|
30
|
+
call that tool, suspending the turn until an operator approves it in the Studio.
|
|
31
|
+
See [Security](SECURITY.md#human-approval).
|
|
32
|
+
|
|
33
|
+
## Layer 3: Guardrails (content safety)
|
|
34
|
+
|
|
35
|
+
`guardrails` configures input/output content safety per agent — **opt-in**, so an
|
|
36
|
+
agent that says nothing gets a conservative default (deterministic detectors on,
|
|
37
|
+
LLM moderator off). See [Security](SECURITY.md#guardrails) and
|
|
38
|
+
[`examples/guardrails/`](https://github.com/guizaols/insika/tree/main/examples/guardrails/).
|
|
39
|
+
|
|
40
|
+
## Layer 4: Edge limits (flood and spend control)
|
|
41
|
+
|
|
42
|
+
Two independent, opt-in ceilings, enforced *before* the model is ever called —
|
|
43
|
+
opt-in everywhere except on a public channel, where `chat_rate_limit` is
|
|
44
|
+
[required](CHANNELS.md#a-rate-limit-is-required-not-suggested) and the
|
|
45
|
+
[web widget](CHANNELS.md#the-web-widget) refuses to serve without one:
|
|
46
|
+
|
|
47
|
+
- **`chat_rate_limit`** — turn attempts per session per `chat_rate_window`.
|
|
48
|
+
- **`agent_token_ceiling`** — total tokens per agent per `agent_token_window`.
|
|
49
|
+
|
|
50
|
+
On breach the turn halts gracefully with a configurable `limit_response` and
|
|
51
|
+
**zero LLM calls**. Windows are set at the platform level; the ceilings can be set
|
|
52
|
+
per agent (blank inherits the platform value, `0` explicitly disables it).
|
|
53
|
+
|
|
54
|
+
> ⚠️ The token window default is **86400 (daily)**. To express "500k tokens per
|
|
55
|
+
> **hour**", set `agent_token_window = 3600` explicitly. A per-agent key that is
|
|
56
|
+
> *present but nil* reads as OFF for that agent — leave the key **absent** to
|
|
57
|
+
> inherit. See [Security](SECURITY.md#edge-limits).
|
|
58
|
+
|
|
59
|
+
### Calendar budgets — the daily/monthly cost wall
|
|
60
|
+
|
|
61
|
+
A third, opt-in ceiling for the *billing* shape the windows above cannot express:
|
|
62
|
+
a spend cap over a CALENDAR day or month, per `(tenant, agent)` when
|
|
63
|
+
multi-tenant. Data on the profile (DSL `budget` or the pack's `budget` key):
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
budget daily: 100_000, monthly: 2_000_000, soft: false # or soft: true
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- Tokens count the **billed** spend — `input + output + cached + cache_creation`
|
|
70
|
+
(the cached prefix is the bulk of the bill, not an afterthought).
|
|
71
|
+
- **Hard** (the default, `soft` absent/false): a turn that arrives with the
|
|
72
|
+
window's spend already at/over the cap **fails** with the typed
|
|
73
|
+
`Insika::BudgetExceeded` — the envelope reads `budget_exceeded` +
|
|
74
|
+
`retry_after` (seconds until the window rolls). It is NOT a customer reply; it
|
|
75
|
+
is an operator signal.
|
|
76
|
+
- **Soft** (`soft: true`): the same turn RUNS — crossing the cap emits one
|
|
77
|
+
`budget_warning` event per window and injects a note into the context (the
|
|
78
|
+
model sees it, the transcript does not).
|
|
79
|
+
- Either way, crossing `alert_at` (default `0.8` of the cap) fires the same
|
|
80
|
+
warning **before** the wall, once per window.
|
|
81
|
+
|
|
82
|
+
> ⚠️ Unlike the ceilings above, the cap that counts is per **calendar** window —
|
|
83
|
+
> a `daily` budget rolls at UTC midnight, a `monthly` one on the 1st, whatever
|
|
84
|
+
> the sun. `agent_token_window` is a fixed seconds window and cannot express
|
|
85
|
+
> "the day resets at midnight".
|
|
86
|
+
|
|
87
|
+
### Reliability — retries, fallback, circuit breaker
|
|
88
|
+
|
|
89
|
+
The provider interaction is a single attempt by default (RubyLLM's own 2
|
|
90
|
+
transport retries aside). For a store that cannot have a dead model take the
|
|
91
|
+
chat down, the reliability policy is DATA on the profile:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
reliability retries: 2, backoff: "exponential",
|
|
95
|
+
fallback: ["openai/gpt-4o-mini"],
|
|
96
|
+
circuit_breaker: { after: 10, within: 60, cooldown: 300 }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- **Retries** — transient failures (`:retryable` / `:rate_limited_*` per the
|
|
100
|
+
error classification) retry with exponential backoff, up to `retries`.
|
|
101
|
+
A `:fatal` (auth, billing, bad request) is NEVER retried or rotated. Each
|
|
102
|
+
attempt runs on a fresh chat — the customer-visible answer comes only from
|
|
103
|
+
the attempt that returns.
|
|
104
|
+
- **Fallback** — after a node's retries, the turn ROTATES to the next model in
|
|
105
|
+
the chain: the profile's `fallback` refs first, then the platform
|
|
106
|
+
`fallback_models`. The turn's usage is attributed to the model that actually
|
|
107
|
+
spoke (`model_source: "fallback"`).
|
|
108
|
+
- **Circuit breaker** — per `(tenant, provider/model)`: `after` failures within
|
|
109
|
+
`within` seconds open the circuit; while open, the turn fail-fasts with the
|
|
110
|
+
typed `circuit_open` + `retry_after` (remaining cooldown) and the provider is
|
|
111
|
+
never touched. After `cooldown` a half-open trial closes the circuit on
|
|
112
|
+
success or reopens it on failure.
|
|
113
|
+
- **`timeout`** — per-attempt ceiling (default 30s), counted as a retryable
|
|
114
|
+
failure.
|
|
115
|
+
|
|
116
|
+
Absent `reliability` = the plain single attempt, byte-for-byte today's
|
|
117
|
+
behavior.
|
|
118
|
+
|
|
119
|
+
### Intent routing — classify before you answer
|
|
120
|
+
|
|
121
|
+
For a store that must tell "shopping" from "order" from "human" up front,
|
|
122
|
+
routing is data on the profile:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
routes "shopping" => "the customer wants to browse products",
|
|
126
|
+
"order" => { "description" => "asks about an existing order",
|
|
127
|
+
"delegate" => "order-agent" },
|
|
128
|
+
"human" => { "description" => "the customer asks for a person",
|
|
129
|
+
"stuck" => true, "message" => "A person will help you." },
|
|
130
|
+
"default" => "shopping",
|
|
131
|
+
"model" => "deepseek-v4-flash" # the cheap classifier (absent = the agent's own)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
- **Classification** — when `routes:` is present, the message is classified into
|
|
135
|
+
one route with the configured model BEFORE the agent chat is assembled, from
|
|
136
|
+
a prompt auto-generated out of the descriptions (no per-route prompt file).
|
|
137
|
+
The route rides the turn: `state.route`, the `:route_classified` event, and
|
|
138
|
+
the terminal event additively.
|
|
139
|
+
- **Deterministic default** — the model's answer must be a route name; prose,
|
|
140
|
+
an unknown name, or an empty answer falls back to `default`, never invents.
|
|
141
|
+
A classifier call that FAILS leaves the turn unrouted (routing is additive —
|
|
142
|
+
it must not break the turn).
|
|
143
|
+
- **Cost** — the classification is an extra provider call, counted in the
|
|
144
|
+
turn's usage (the trace, the token ceiling and the budget all see it).
|
|
145
|
+
- **Actions** — a route value may be a description string, or a Hash:
|
|
146
|
+
`delegate: "<agent-id>"` hands the turn to that existing agent and its
|
|
147
|
+
answer becomes the parent's; `stuck: true` ends the turn with the [stuck
|
|
148
|
+
outcome](AGENTS.md#the-stuck-signal--i-cannot-proceed) and the route's `message`
|
|
149
|
+
(or description) as the lead-in — the consumer interprets it. A route with
|
|
150
|
+
neither is just a label. A delegation counts against the same delegation
|
|
151
|
+
depth cap as a subagent (`INSIKA_SUBAGENT_DEPTH_CAP`, default 5), so a pair
|
|
152
|
+
of agents routing to each other stops instead of looping.
|
|
153
|
+
|
|
154
|
+
Absent `routes` = no classification, no extra call, byte-identical turn.
|
|
155
|
+
|
|
156
|
+
### Operator alerts — the webhook
|
|
157
|
+
|
|
158
|
+
Three operational events — `budget_warning`, `breaker_open`, `delivery_failed` —
|
|
159
|
+
can be answered per agent with a webhook:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
alerts webhook: "https://ops.example.com/insika-alerts"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
When present, each such event is POSTed to the URL as JSON (the event's
|
|
166
|
+
type/data/meta, plus the agent). Delivery rides the same outbox + claim +
|
|
167
|
+
bounded-retry pipeline as channel answers — at-most-once, crashed deliveries
|
|
168
|
+
recovered at boot. The engine transports the event and does not interpret it: a
|
|
169
|
+
Slack/CRM adapter is the consumer's. Absent `alerts` = nothing is sent.
|
|
170
|
+
|
|
171
|
+
Separately, with `INSIKA_TURN_TIMING`, the provider's **live TTFB** is carried in
|
|
172
|
+
the streaming envelope: the first content chunk emits an `insika.ttft` frame
|
|
173
|
+
(`ttft_ms`) on `/v1/responses`, alongside the per-turn `timing` breakdown on the
|
|
174
|
+
final `response.completed`. Additive and opt-in — absent by default.
|
|
175
|
+
|
|
176
|
+
## Layer 5: Reasoning (thinking)
|
|
177
|
+
|
|
178
|
+
Controls the model's thinking budget, resolved by precedence
|
|
179
|
+
**Chat > Agent > Model > Global** (first non-blank wins):
|
|
180
|
+
|
|
181
|
+
| Scope | Where |
|
|
182
|
+
|-------|-------|
|
|
183
|
+
| Chat | session var `__llm__.thinking` |
|
|
184
|
+
| Agent | `profile.params["thinking"]` |
|
|
185
|
+
| Model | platform `model_params[<ref>].thinking` |
|
|
186
|
+
| Global | platform `thinking` |
|
|
187
|
+
|
|
188
|
+
Values: `off | on | low | medium | high`. `off`/`on` toggle thinking; the effort
|
|
189
|
+
levels map to the provider's thinking-effort parameter. This is a control
|
|
190
|
+
primitive, not a latency lever — turning reasoning off does not necessarily speed
|
|
191
|
+
up a turn, because most of a turn's latency is the provider itself, not thinking.
|
|
192
|
+
|
|
193
|
+
Whether the reasoning ever reaches the **customer** is a separate switch, off by
|
|
194
|
+
default:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
edge_stream thinking: true, intermediate: false
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`thinking` is the provider's reasoning; `intermediate` is the model narrating its
|
|
201
|
+
own tool loop ("let me look that up"). Both are always on the event stream for the
|
|
202
|
+
Studio and the trace — this decides only whether `/v1/responses` translates them,
|
|
203
|
+
and each opted-in channel gets its own frame type, never the answer's. See
|
|
204
|
+
[Architecture](ARCHITECTURE.md#what-crosses-the-edge).
|
|
205
|
+
|
|
206
|
+
> ⚠️ Turn it on knowing your consumer. One that concatenates every text delta into
|
|
207
|
+
> a single message — a WhatsApp adapter — will only be affected once it learns to
|
|
208
|
+
> read the new frames, and when it does, the deliberation is what the customer
|
|
209
|
+
> reads. That is the operator's call, which is why it is neither a default nor a
|
|
210
|
+
> global.
|
|
211
|
+
|
|
212
|
+
## See also
|
|
213
|
+
|
|
214
|
+
- [Agents](AGENTS.md) — the profile these keys live on.
|
|
215
|
+
- [Tools](TOOLS.md) — how a tool gets defined, registered and allowed.
|
|
216
|
+
- [Security](SECURITY.md) — the deployment-side counterpart: sandbox, egress, secrets.
|
data/docs/REFINEMENT.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Refinement
|
|
3
|
-
parent:
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Improve
|
|
4
|
+
nav_order: 2
|
|
5
5
|
permalink: /refinement/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -51,11 +51,14 @@ Or from **Studio → Refinement**: pick the agent, press Run, and the same repor
|
|
|
51
51
|
renders with each session id linking to its transcript. Every finding is a claim
|
|
52
52
|
you can go and check.
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
A run is one command (`run_refinement`), and the CLI and the button are the
|
|
55
|
+
two ways to fire it. There is still no built-in timer *for the report itself*,
|
|
56
|
+
but there is one for the *agent*: fine-grained recurring turns live in
|
|
57
|
+
[Schedules](SCHEDULING.md), and a refinement run scheduled like any other turn
|
|
58
|
+
is just a message the agent's schedule sends it. If you prefer to stay outside
|
|
59
|
+
the engine, pointing your own cron at the authenticated route the button uses
|
|
60
|
+
works just the same — both paths are supported, the engine's trigger is the
|
|
61
|
+
built-in one.
|
|
59
62
|
|
|
60
63
|
```
|
|
61
64
|
insika refine ─┐
|
|
@@ -466,8 +469,10 @@ prompt edits have real leverage, and it is also where they do damage.
|
|
|
466
469
|
|
|
467
470
|
## What this is not
|
|
468
471
|
|
|
469
|
-
|
|
470
|
-
|
|
472
|
+
A run happens because a person or a cron asked for one — the refinement
|
|
473
|
+
pipeline has no timer of its own, and if you want one, a schedule
|
|
474
|
+
([Schedules](SCHEDULING.md)) makes a run a turn any agent can be scheduled to
|
|
475
|
+
send. A proposal is written when you ask for
|
|
471
476
|
one, and — unless you turned on [`auto_apply`](#applying-without-a-human), which is
|
|
472
477
|
off until you do and bounded when you do — applied when you approve it. And it cannot
|
|
473
478
|
touch your guardrails, tools, policies, model pins or limits, and not because a prompt
|
data/docs/RELEASING.md
CHANGED
|
@@ -59,10 +59,10 @@ GEM_HOME="$T/gemhome" GEM_PATH="$T/gemhome" ruby -e '
|
|
|
59
59
|
puts "clean"'
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
### The 1.0 release gate — the
|
|
62
|
+
### The 1.0 release gate — the clean-install proof
|
|
63
63
|
|
|
64
64
|
For the 1.0 release the proof above is scripted and its **installed-bytes**
|
|
65
|
-
half is asserted by the domain-boundary suite
|
|
65
|
+
half is asserted by the domain-boundary suite on the artifact, not the
|
|
66
66
|
repo. Run the runbook, with a key (the smoke turn is one `reply` through the
|
|
67
67
|
installed gem):
|
|
68
68
|
|
|
@@ -77,12 +77,12 @@ only the public docs. Archive the PASS output with the release notes — it is
|
|
|
77
77
|
the 1.0 exit criterion "install proof by the docs alone".
|
|
78
78
|
|
|
79
79
|
The same gate writes the freeze date: a breaking `/v1` change needs a new
|
|
80
|
-
`Insika-Version` entry (server/app.rb), a compatibility branch
|
|
80
|
+
`Insika-Version` entry (server/app.rb), a compatibility branch
|
|
81
81
|
and a rewritten `**Frozen as of:**` line in `docs/API.md` — the version-gate
|
|
82
82
|
spec pins the two together, and the 1.0 release writes the date at release
|
|
83
83
|
time.
|
|
84
84
|
|
|
85
|
-
Catalog submission checklist
|
|
85
|
+
Catalog submission checklist: verify the best-of-Agent-Harnesses
|
|
86
86
|
catalog size at submission time (161 vs 154 — the counts diverge across the
|
|
87
87
|
catalog's own pages) and cite the conformance suite as the `durable` evidence.
|
|
88
88
|
|
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
|
|
|
@@ -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):
|