insika 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +361 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +366 -0
- data/docs/AGENTS.md +618 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +117 -0
- data/docs/DEPLOY.md +354 -0
- data/docs/EMBEDDING.md +198 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +232 -0
- data/docs/OBSERVABILITY.md +374 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +375 -0
- data/docs/SKILLS.md +284 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +255 -0
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +153 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +303 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +58 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +43 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_refinement.rb +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +112 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +60 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +89 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +43 -0
- data/lib/insika/context/priority.rb +30 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +123 -0
- data/lib/insika/context/providers/skill.rb +24 -0
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +539 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +382 -0
- data/lib/insika/dsl/server_boot.rb +98 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +364 -0
- data/lib/insika/edge_limiter.rb +268 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +249 -0
- data/lib/insika/errors.rb +201 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +132 -0
- data/lib/insika/executor.rb +1995 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +115 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +102 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +123 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +143 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +168 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +102 -0
- data/lib/insika/safety/moderator.rb +94 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/app.rb +1022 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +118 -0
- data/lib/insika/server/responses.rb +165 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +220 -0
- data/lib/insika/skill_store.rb +127 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1693 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +70 -0
- data/lib/insika/studio/forms.rb +335 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +137 -0
- data/lib/insika/studio/views/settings.erb +168 -0
- data/lib/insika/studio/views/skills.erb +141 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +219 -0
- data/lib/insika/tools/load_skill.rb +99 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +162 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +249 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +220 -4
- metadata +412 -8
data/docs/CHANNELS.md
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Channels
|
|
3
|
+
parent: Build an agent
|
|
4
|
+
nav_order: 6
|
|
5
|
+
permalink: /channels/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Channels
|
|
9
|
+
|
|
10
|
+
An agent is only useful where the people are. A **channel** is the seam between a
|
|
11
|
+
place people already talk — WhatsApp, Slack, a widget on your site, your own app —
|
|
12
|
+
and a turn of the engine.
|
|
13
|
+
|
|
14
|
+
A channel does exactly two things:
|
|
15
|
+
|
|
16
|
+
- **inbound**: normalize whatever the platform sent into a message for an agent;
|
|
17
|
+
- **outbound**: render the turn's answer back in that platform's shape.
|
|
18
|
+
|
|
19
|
+
Nothing else. A channel never decides which tools an agent may call, never edits a
|
|
20
|
+
prompt, never grants a capability. It may *refuse* a request (a bad signature); it
|
|
21
|
+
may not *widen* one. Everything a channel learns about the caller is data, and it
|
|
22
|
+
enters the prompt at the most cuttable priority, like any other untrusted input.
|
|
23
|
+
|
|
24
|
+
## Which one is yours?
|
|
25
|
+
|
|
26
|
+
Two channels ship with the engine, and the choice between them is **where the
|
|
27
|
+
conversation happens**.
|
|
28
|
+
|
|
29
|
+
| | [**web widget**](#the-web-widget) | [**relay**](#the-relay-contract) |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| Where people talk to you | a panel on your own site | WhatsApp, Slack, your app — wherever you already are |
|
|
32
|
+
| Who talks to that platform | — (there is none) | **you**, with the integration you already have |
|
|
33
|
+
| Who runs the turn | the engine | the engine |
|
|
34
|
+
| What you have to build | nothing: one `<script>` tag | two HTTP calls |
|
|
35
|
+
|
|
36
|
+
**The web widget** is for a team with no messaging stack at all. Paste one tag on
|
|
37
|
+
your site and you have an agent — no backend of yours, no build step, no npm.
|
|
38
|
+
|
|
39
|
+
**The relay** is for a team that already owns its messaging stack — a WhatsApp BSP,
|
|
40
|
+
a Zendesk, a legacy Rails app with years of tuning in it. You keep every bit of
|
|
41
|
+
that. You POST the customer's message to the engine, the engine POSTs the answer
|
|
42
|
+
back to a URL you control, and your existing code does what it already does well.
|
|
43
|
+
Nothing about your platform integration has to move for you to adopt Insika.
|
|
44
|
+
|
|
45
|
+
They are not stages of the same path. Plenty of teams run both.
|
|
46
|
+
|
|
47
|
+
> **On Slack and WhatsApp "natively".** The engine does not speak either platform,
|
|
48
|
+
> and that is a decision rather than a gap. If your customers are on WhatsApp or
|
|
49
|
+
> Slack, the relay is the answer: you own the platform integration — the 24-hour
|
|
50
|
+
> window, templates, media, read receipts, formatting — and the engine owns the
|
|
51
|
+
> turn. That boundary is [the promise, not a limitation](#what-the-relay-does-not-do--on-purpose),
|
|
52
|
+
> and it is why the relay is permanent rather than a stepping stone.
|
|
53
|
+
>
|
|
54
|
+
> Adapters that make the engine speak a platform directly are specified and
|
|
55
|
+
> deliberately unbuilt. The seam they would plug into is real and proven — the two
|
|
56
|
+
> channels above are its opposite shapes, and a third would add `deliver` and
|
|
57
|
+
> nothing else — so this is a question of demand, not of design. If you need one,
|
|
58
|
+
> [open an issue](https://github.com/guizaols/insika/issues); you can also
|
|
59
|
+
> [write it yourself](#writing-your-own-channel) as a plugin, without forking.
|
|
60
|
+
|
|
61
|
+
## The web widget
|
|
62
|
+
|
|
63
|
+
For a team with no messaging stack at all: **one `<script>` tag on your site and
|
|
64
|
+
you have an agent**. No backend of yours, no build step, no npm.
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<script src="https://agents.example.com/channels/web/asset/widget.js"
|
|
68
|
+
data-agent="support" data-title="Ask us anything" defer></script>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It renders a bubble in the corner, opens a panel, and streams the answer token by
|
|
72
|
+
token on the same connection it sent the message on. Everything it needs it reads
|
|
73
|
+
off its own tag — including the engine's address, derived from the script's own
|
|
74
|
+
`src`, so you never configure a host twice.
|
|
75
|
+
|
|
76
|
+
| Attribute | Required | What it does |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `data-agent` | **yes** | which agent answers. Must be on `INSIKA_WIDGET_AGENTS` |
|
|
79
|
+
| `data-title` | — | the panel header (default `Chat`) |
|
|
80
|
+
| `data-placeholder` | — | the input's placeholder |
|
|
81
|
+
| `data-greeting` | — | a first message from the agent, shown without running a turn |
|
|
82
|
+
|
|
83
|
+
Theming is a block of CSS custom properties. Set them anywhere on the page:
|
|
84
|
+
|
|
85
|
+
```css
|
|
86
|
+
:root {
|
|
87
|
+
--insika-accent: #0f766e; --insika-on-accent: #fff;
|
|
88
|
+
--insika-bg: #fff; --insika-fg: #111827;
|
|
89
|
+
--insika-muted: #f0fdfa; --insika-border: rgba(0,0,0,.12);
|
|
90
|
+
--insika-font: "Inter", system-ui, sans-serif;
|
|
91
|
+
--insika-offset: 20px; /* distance from the corner */
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A runnable page lives in
|
|
96
|
+
[`examples/web-widget/`](https://github.com/guizaols/insika/tree/main/examples/web-widget).
|
|
97
|
+
|
|
98
|
+
### Setting up the widget
|
|
99
|
+
|
|
100
|
+
Two environment variables, and **both are the switch** — with either one missing the
|
|
101
|
+
channel is not mounted and every `/channels/web/*` route answers `404`:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
INSIKA_WIDGET_ORIGINS=https://shop.example,https://www.shop.example
|
|
105
|
+
INSIKA_WIDGET_AGENTS=support
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`INSIKA_WIDGET_ORIGINS` is an **exact-match** list of the origins allowed to embed
|
|
109
|
+
the widget. No wildcards, no subdomain matching, and no value that means "anyone" —
|
|
110
|
+
`https://shop.example` does not admit `https://a.shop.example`. Include every origin
|
|
111
|
+
your site actually serves from, `www` included.
|
|
112
|
+
|
|
113
|
+
`INSIKA_WIDGET_AGENTS` is the list of agents a visitor may address. An anonymous
|
|
114
|
+
browser addresses these and nothing else, so an internal agent in the same
|
|
115
|
+
deployment stays out of reach even if someone edits the `data-agent` in devtools.
|
|
116
|
+
|
|
117
|
+
### A rate limit is required, not suggested
|
|
118
|
+
|
|
119
|
+
**The widget answers `503` until a chat rate limit is configured.** This is the one
|
|
120
|
+
place the engine refuses to run rather than warn, because a public endpoint with an
|
|
121
|
+
LLM behind it and no ceiling is an unmetered bill that arrives before anybody
|
|
122
|
+
notices.
|
|
123
|
+
|
|
124
|
+
Either source satisfies it — the platform default, in Studio → Settings:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{ "edge": { "chat_rate_limit": 6, "chat_rate_window": 60 } }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
or a per-agent `limits.chat_rate_limit` on **every** agent in
|
|
131
|
+
`INSIKA_WIDGET_AGENTS`. The bucket is the minted session id, so one visitor cannot
|
|
132
|
+
spend another's allowance. `insika doctor` tells you which half is missing.
|
|
133
|
+
|
|
134
|
+
### The three routes
|
|
135
|
+
|
|
136
|
+
You never call these yourself — the widget does — but they are the contract, and
|
|
137
|
+
anything can speak them.
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
POST /channels/web/sessions -> 201 {"session_id": "web:8f3c…"}
|
|
141
|
+
POST /channels/web/messages -> 200 text/event-stream
|
|
142
|
+
GET /channels/web/asset/widget.js -> the script
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```jsonc
|
|
146
|
+
POST /channels/web/messages
|
|
147
|
+
Content-Type: application/json
|
|
148
|
+
|
|
149
|
+
{ "agent": "support", "session_id": "web:8f3c…", "message": "cadê meu pedido?" }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The reply is SSE on that same connection — four frame types, and an unknown one is
|
|
153
|
+
safe to ignore:
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
event: delta data: {"delta":"Seu pedido "} the answer, token by token
|
|
157
|
+
event: working data: {"name":"order_status"} a tool is running
|
|
158
|
+
event: done data: {} the turn ended
|
|
159
|
+
event: error data: {"message":"…"} it ended badly
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**The engine issues the session id and the client never proposes one.** `POST
|
|
163
|
+
/messages` with an id nobody minted is a `404`, never a new conversation: on an
|
|
164
|
+
anonymous endpoint, create-on-write means anyone who guesses an id can read someone
|
|
165
|
+
else's chat. The widget keeps the id it was given in `localStorage`, so a returning
|
|
166
|
+
visitor continues the same conversation.
|
|
167
|
+
|
|
168
|
+
### What the widget does not do
|
|
169
|
+
|
|
170
|
+
File upload, typing indicators, history across devices, and i18n of its own chrome
|
|
171
|
+
(the four words on the buttons). It also never retries a message POST: that request
|
|
172
|
+
runs a turn, so re-sending it costs a second LLM call and can put a second answer in
|
|
173
|
+
front of the customer. A dropped stream shows what arrived and lets the person ask
|
|
174
|
+
again.
|
|
175
|
+
|
|
176
|
+
## Relay or the drop-in API?
|
|
177
|
+
|
|
178
|
+
If you already own your messaging platform, you can reach the engine two ways: the
|
|
179
|
+
drop-in [`POST /v1/responses`](/architecture/) — you hold an SSE connection for the
|
|
180
|
+
whole turn and read the answer off it — or the relay, where the engine acks in
|
|
181
|
+
milliseconds and POSTs the answer to you when it exists.
|
|
182
|
+
|
|
183
|
+
The instinct is that streaming gets the customer their reply sooner, and that the
|
|
184
|
+
relay trades that away. **It does not, and the reason is structural:** the engine
|
|
185
|
+
publishes `:content` as the ANSWER, whole, after the turn's hooks
|
|
186
|
+
([what crosses the edge](/architecture/#what-crosses-the-edge)). During the turn the
|
|
187
|
+
stream carries tool activity; the text arrives in one piece at the end. Measured on
|
|
188
|
+
a real store agent, the text frames span **0 ms** — there is nothing to deliver
|
|
189
|
+
progressively, on either path.
|
|
190
|
+
|
|
191
|
+
| | drop-in `/v1/responses` | relay |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| What the customer receives | one message, at the end | one message, at the end |
|
|
194
|
+
| Your app's request | held open for the whole turn (seconds) | acked in **milliseconds** |
|
|
195
|
+
| A turn that outlives your HTTP timeout | your problem | already handled — the answer arrives later |
|
|
196
|
+
| Retry on a failed handover | yours to build | the engine's outbox, bounded, at-most-once |
|
|
197
|
+
| Three fragments typed in a row | three turns, three replies | **one turn, one reply** (with `queue_mode`) |
|
|
198
|
+
| Your platform code | unchanged | unchanged |
|
|
199
|
+
|
|
200
|
+
That last row is the one that cannot be had the other way. `/v1/responses` answers
|
|
201
|
+
the request it was given, so a message that arrives while a turn is running is a
|
|
202
|
+
second turn — the engine has no way to tell you "this joined the previous one". The
|
|
203
|
+
relay's `merged` / `steered` acks exist precisely to say that, which is why
|
|
204
|
+
[the inbound queue](/agents/#queue_mode--when-a-message-arrives-while-the-agent-is-busy)
|
|
205
|
+
is only reachable from here.
|
|
206
|
+
|
|
207
|
+
**Measured, so you can judge it rather than take our word:** same agent, same
|
|
208
|
+
conversations, one local deployment.
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
drop-in relay
|
|
212
|
+
greeting 2.3s 2.9s (ack 11ms)
|
|
213
|
+
catalog (4 tool calls) 6.8–12.3s 9.4s (ack 2ms)
|
|
214
|
+
3 fragments 3 turns/3 replies 1 turn/1 reply, with queue_mode
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The spread on the catalog turn is the tool retrying, not the transport — the relay
|
|
218
|
+
adds one HTTP POST, not seconds. Your numbers will differ; the *shapes* are what
|
|
219
|
+
transfer.
|
|
220
|
+
|
|
221
|
+
**Pick the drop-in** if you already have it working and none of the rows above bite.
|
|
222
|
+
**Pick the relay** if you want the queue, or if holding a connection for the length
|
|
223
|
+
of a turn is awkward where your app runs.
|
|
224
|
+
|
|
225
|
+
## What the relay does not do — on purpose
|
|
226
|
+
|
|
227
|
+
Platform semantics stay with you:
|
|
228
|
+
|
|
229
|
+
- the 24-hour customer-service window and template fallback,
|
|
230
|
+
- template approval and template sending,
|
|
231
|
+
- media upload and download,
|
|
232
|
+
- typing indicators and read receipts,
|
|
233
|
+
- how a markdown reply becomes WhatsApp formatting.
|
|
234
|
+
|
|
235
|
+
The engine sends **text and identity**. That is the promise, not the limitation:
|
|
236
|
+
it is what lets you keep an integration you have already tuned, and it is why
|
|
237
|
+
nothing here expires. A relay that starts growing template logic has stopped being
|
|
238
|
+
a relay; the place for platform semantics is the code that already has them —
|
|
239
|
+
yours — or a [channel of your own](#writing-your-own-channel).
|
|
240
|
+
|
|
241
|
+
## The relay contract
|
|
242
|
+
|
|
243
|
+
Two routes and an envelope.
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
you ──POST /channels/relay/events──▶ engine acked now, never the reply
|
|
247
|
+
you ◀──POST <your deliver_url>───── engine the reply, when there is one
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Inbound
|
|
251
|
+
|
|
252
|
+
```jsonc
|
|
253
|
+
POST /channels/relay/events
|
|
254
|
+
Authorization: Bearer <INSIKA_RELAY_TOKEN>
|
|
255
|
+
Content-Type: application/json
|
|
256
|
+
|
|
257
|
+
{
|
|
258
|
+
"agent": "support", // required — which agent answers
|
|
259
|
+
"external_id": "5511999998888", // required — YOUR key for this conversation
|
|
260
|
+
"message": "queria saber do pedido", // required
|
|
261
|
+
"event_id": "wamid.HBg…", // optional but strongly recommended (dedup)
|
|
262
|
+
"vars": { "store": "demo-store" } // optional — session vars on first contact
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The engine acks immediately and never streams the answer here. Four possible
|
|
267
|
+
answers, and **they are four different facts**:
|
|
268
|
+
|
|
269
|
+
| Status | Body | What happened | What you must do |
|
|
270
|
+
|---|---|---|---|
|
|
271
|
+
| `202` | `{"task_id": "…"}` | a turn is running | wait for the delivery |
|
|
272
|
+
| `200` | `{"task_id": "…", "duplicate": true}` | we already ran this `event_id` | nothing — the original reply is on its way |
|
|
273
|
+
| `200` | `{"task_id": "…", "merged": true}` | it joined a turn still at the door | nothing — the answer belongs to `task_id` |
|
|
274
|
+
| `200` | `{"task_id": "…", "steered": true}` | it was appended to a turn already running | nothing — same |
|
|
275
|
+
| `422` | `{"error": …}` | the envelope is malformed | fix and resend |
|
|
276
|
+
| `401` / `503` | `{"error": …}` | bad token / channel not configured | fix the credential |
|
|
277
|
+
|
|
278
|
+
**A consumer that treats the three `200`s like a `202` sends the customer the same
|
|
279
|
+
answer two or three times.** That is the one contract mistake that is visible to
|
|
280
|
+
the end user, so it is worth a line of code: only deliver for a `202`.
|
|
281
|
+
|
|
282
|
+
`merged` and `steered` come from the [inbound queue](/agents/#queue_mode--when-a-message-arrives-while-the-agent-is-busy)
|
|
283
|
+
(`limits[:queue_mode]`). They only ever occur if you turned that on for the agent;
|
|
284
|
+
with the default `followup` you will only see `202` and `duplicate`.
|
|
285
|
+
|
|
286
|
+
### Outbound
|
|
287
|
+
|
|
288
|
+
One POST per reply, to the URL you configured:
|
|
289
|
+
|
|
290
|
+
```jsonc
|
|
291
|
+
POST <INSIKA_RELAY_DELIVER_URL>
|
|
292
|
+
Authorization: Bearer <INSIKA_RELAY_DELIVER_TOKEN> // omitted if unset
|
|
293
|
+
X-Insika-Delivery: 0f2c… // stable idempotency key
|
|
294
|
+
Content-Type: application/json
|
|
295
|
+
|
|
296
|
+
{
|
|
297
|
+
"external_id": "5511999998888",
|
|
298
|
+
"session_id": "relay:5511999998888",
|
|
299
|
+
"task_id": "…",
|
|
300
|
+
"content": "Seu pedido saiu para entrega hoje 😊"
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Any `2xx` means delivered. Anything else is retried, up to three attempts with a
|
|
305
|
+
short backoff, and then the delivery is marked `failed` and stops.
|
|
306
|
+
|
|
307
|
+
`content` is the turn's **answer** — one message, whole. The model's narration on
|
|
308
|
+
the way to an answer ("vou verificar o cardápio…") does not come through here; it
|
|
309
|
+
stays internal unless the agent opts in. That contract is
|
|
310
|
+
[the edge contract](/architecture/#what-crosses-the-edge), and it is why you can
|
|
311
|
+
forward `content` straight to the customer.
|
|
312
|
+
|
|
313
|
+
### Deduplication
|
|
314
|
+
|
|
315
|
+
Send `event_id` and a retried webhook costs you nothing: the engine recognizes the
|
|
316
|
+
id inside a 24-hour window, answers `duplicate: true`, and runs no turn. Without
|
|
317
|
+
it, every retry on your side is a second LLM turn (that you pay for) and a second
|
|
318
|
+
message (that the customer reads).
|
|
319
|
+
|
|
320
|
+
The engine will not invent dedup from a content hash — two customers typing "oi"
|
|
321
|
+
a second apart are not a duplicate. No `event_id` means at-least-once turns, and
|
|
322
|
+
that is stated rather than hidden.
|
|
323
|
+
|
|
324
|
+
### Delivery guarantee: at-most-once
|
|
325
|
+
|
|
326
|
+
The reply is written durably when the turn commits, then **claimed** before the
|
|
327
|
+
HTTP call. If the process dies between the claim and the POST, that delivery is
|
|
328
|
+
lost; it is never duplicated. On boot, replies that were recorded but never
|
|
329
|
+
claimed are re-dispatched.
|
|
330
|
+
|
|
331
|
+
This is deliberate and it is the same guarantee the engine's async-subagent
|
|
332
|
+
delivery has. If you need at-least-once instead, the honest place to build it is
|
|
333
|
+
your side: you already have the customer's conversation, and `GET /v1/tasks/:id`
|
|
334
|
+
tells you the turn's terminal state.
|
|
335
|
+
|
|
336
|
+
A turn that **failed** delivers nothing — an error string is not an answer. Watch
|
|
337
|
+
`GET /v1/tasks/:id` or the [event stream](/observability/) for those.
|
|
338
|
+
|
|
339
|
+
## Setting up the relay
|
|
340
|
+
|
|
341
|
+
Three environment variables on the engine:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
INSIKA_RELAY_TOKEN=<a long random secret> # the switch AND the credential
|
|
345
|
+
INSIKA_RELAY_DELIVER_URL=https://you.example/insika/deliver
|
|
346
|
+
INSIKA_RELAY_DELIVER_TOKEN=<another secret> # optional; what we send to you
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
`INSIKA_RELAY_TOKEN` is the switch: without it the channel is not mounted and
|
|
350
|
+
`/channels/relay/events` answers `404`. There is no way to expose this route
|
|
351
|
+
without a credential — a public inbound route with an LLM behind it is a money
|
|
352
|
+
faucet, so it fails closed by construction.
|
|
353
|
+
|
|
354
|
+
The delivery POST goes through the same [egress guard](/security/#egress-the-ssrf-boundary) as
|
|
355
|
+
data-tools: **https only**, and private/loopback destinations blocked. For local
|
|
356
|
+
development, where your consumer is on `localhost`:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
INSIKA_EGRESS_ALLOW_HTTP=1
|
|
360
|
+
INSIKA_EGRESS_ALLOW_PRIVATE=1
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Run `insika doctor` after configuring — it refuses to let you have half of it
|
|
364
|
+
(a token with no URL accepts turns it can never answer; a URL with no token mounts
|
|
365
|
+
nothing at all).
|
|
366
|
+
|
|
367
|
+
A runnable consumer in ~40 lines lives in
|
|
368
|
+
[`examples/relay-channel/`](https://github.com/guizaols/insika/tree/main/examples/relay-channel).
|
|
369
|
+
|
|
370
|
+
## Sessions
|
|
371
|
+
|
|
372
|
+
The engine mints the session id: `relay:<your external_id>`. Namespacing is not
|
|
373
|
+
decoration — it stops a Slack channel id from colliding with a phone number, and
|
|
374
|
+
it means an id from one channel can never read another channel's conversation.
|
|
375
|
+
|
|
376
|
+
Your `vars` ride along on first contact and become session vars, except for
|
|
377
|
+
`channel` and `external_id`, which the engine always owns. A caller cannot
|
|
378
|
+
redirect its own conversation.
|
|
379
|
+
|
|
380
|
+
## Writing your own channel
|
|
381
|
+
|
|
382
|
+
A channel is a plain object — no base class. Register it from a plugin
|
|
383
|
+
(see [Plugins](/plugins/)) with `contracts: { channels: [<id>] }` in the manifest,
|
|
384
|
+
and it mounts under `/channels/<id>/`.
|
|
385
|
+
|
|
386
|
+
Two members are always there; the rest of the object decides which shape you get.
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
class MyChannel
|
|
390
|
+
def id = "mine"
|
|
391
|
+
|
|
392
|
+
# -> :ok | :unauthorized | :disabled. Never open by omission — a channel with
|
|
393
|
+
# no `authenticate` at all is refused with 503 rather than defaulted open.
|
|
394
|
+
def authenticate(req) = ...
|
|
395
|
+
|
|
396
|
+
# Rack request + parsed body -> the fields the engine turns into a turn.
|
|
397
|
+
def parse(req, body:) = ...
|
|
398
|
+
end
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Shape B** (the platform calls you back later) adds `deliver`, and that alone
|
|
402
|
+
mounts `POST /channels/<id>/events`:
|
|
403
|
+
|
|
404
|
+
```ruby
|
|
405
|
+
def parse(req, body:)
|
|
406
|
+
{ agent: …, external_id: …, message: …, event_id: …, vars: {} }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def session_id_for(external_id) = "#{id}:#{external_id}"
|
|
410
|
+
|
|
411
|
+
# Hand ONE reply to the platform. -> HTTP status.
|
|
412
|
+
# Raise Insika::DeliveryError when the request could not be made at all.
|
|
413
|
+
def deliver(payload, to:, delivery_id: nil) = ...
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Shape A** (the reply rides the request's own connection) adds `frame_for`, which
|
|
417
|
+
mounts `POST /channels/<id>/messages`, plus whichever of these it wants:
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
def parse(req, body:) = { agent: …, session_id: …, message: … }
|
|
421
|
+
|
|
422
|
+
# Turn Event -> an SSE frame | nil for an event with no counterpart.
|
|
423
|
+
def frame_for(event) = ...
|
|
424
|
+
|
|
425
|
+
# Optional: mounts POST /channels/<id>/sessions. The engine issues the id.
|
|
426
|
+
def mint_session_id = "#{id}:#{SecureRandom.hex(16)}"
|
|
427
|
+
|
|
428
|
+
# Optional: mounts GET /channels/<id>/asset/:f. A CLOSED map of names, never a
|
|
429
|
+
# path — anything resolving a filesystem path from a URL is a traversal.
|
|
430
|
+
def asset(name) = { content_type: …, body: …, etag: …, cache_control: … }
|
|
431
|
+
|
|
432
|
+
# Optional: the CORS headers for this origin, or none.
|
|
433
|
+
def cors_headers(origin) = ...
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Everything else is shared — the outbox, the at-most-once claim, the bounded retry,
|
|
437
|
+
the inbound dedup and the queue. A channel that needs to branch anywhere but
|
|
438
|
+
`deliver` is a sign the seam is wrong, not that the channel is special.
|
|
439
|
+
|
|
440
|
+
## Observability
|
|
441
|
+
|
|
442
|
+
Each delivery emits `:channel_delivered` on the event stream —
|
|
443
|
+
`{channel, outbox_id, status, attempts, error}` — so a failed handover is visible
|
|
444
|
+
without reading the store. Inbound is already visible: the turn's persisted
|
|
445
|
+
command carries `transport: "channel:<id>"`.
|
|
446
|
+
|
|
447
|
+
## See also
|
|
448
|
+
|
|
449
|
+
- [Security](/security/) — the tokens, the egress guard, and why the rate limit
|
|
450
|
+
matters for anything public.
|
|
451
|
+
- [Agents](/agents/) — `limits[:queue_mode]`, which is what produces `merged` and
|
|
452
|
+
`steered`.
|
|
453
|
+
- [Observability](/observability/) — the event stream and OpenTelemetry.
|
data/docs/CONTEXT.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Context
|
|
3
|
+
parent: Build an agent
|
|
4
|
+
nav_order: 4
|
|
5
|
+
permalink: /context/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Context
|
|
9
|
+
|
|
10
|
+
Every turn, the **Context Builder** assembles the model's prompt from a fixed set
|
|
11
|
+
of **providers**, estimates its token cost, and evicts the lowest-priority
|
|
12
|
+
non-pinned fragments to fit the agent's `context_budget`. The Executor never
|
|
13
|
+
builds the prompt itself — it receives a finished context package. Providers run
|
|
14
|
+
in parallel, each with a short per-provider timeout, and an *optional* provider
|
|
15
|
+
that fails or times out is dropped with a warning rather than failing the turn.
|
|
16
|
+
|
|
17
|
+
## What enters a turn's prompt
|
|
18
|
+
|
|
19
|
+
Providers are chosen by a double gate (the provider opts in for the profile **and**
|
|
20
|
+
the agent's `context_providers` allowlist permits it), then assembled by priority
|
|
21
|
+
into a deterministic prompt:
|
|
22
|
+
|
|
23
|
+
| Provider | Block | Priority | Notes |
|
|
24
|
+
|----------|-------|:--------:|-------|
|
|
25
|
+
| **Identity** | system | **100 — pinned** | The agent's prompt files (global system files first). Never cut. |
|
|
26
|
+
| **Skill trigger** | `<active_skill>` | 85 | Level-2 bodies: the agent's `skills_eager` set, plus the ones whose `triggers:` match the message — see [Skills](SKILLS.md). |
|
|
27
|
+
| **Skills** | `<available_skills>` | 80 | Level-1 skill list, minus whatever is already eager — see [Skills](SKILLS.md). |
|
|
28
|
+
| **Memory** | `<memory>` | 75 | Durable facts + recent notes, only if `memory` is on. Cuttable. |
|
|
29
|
+
| **Tool search** | `<available_tools>` | 70 | Level-1 list of deferred tools — see [Tools](TOOLS.md). |
|
|
30
|
+
| **Session** | history | 60–79 | The running transcript; priority scales with recency. |
|
|
31
|
+
| **Request** | `<request_context>` | 40 | Turn variables + tenant. Most cuttable; sits last. |
|
|
32
|
+
|
|
33
|
+
The ordering is deliberate: the **stable identity sits first**, the **volatile
|
|
34
|
+
request context sits last**. That keeps the cacheable prefix byte-stable (see the
|
|
35
|
+
prefix cache below).
|
|
36
|
+
|
|
37
|
+
## Budget and eviction — the actual "compaction"
|
|
38
|
+
|
|
39
|
+
- The cap is `profile.limits[:context_budget]`, **default 8000 tokens**.
|
|
40
|
+
- To fit the budget, the builder cuts **non-pinned** fragments
|
|
41
|
+
lowest-priority-first (ties broken by oldest history first). Under pressure you
|
|
42
|
+
lose old history, then the memory block, then request context — **the pinned
|
|
43
|
+
identity is never truncated**.
|
|
44
|
+
- A **pinned** fragment (the identity) that *alone* exceeds the budget raises an
|
|
45
|
+
error — the turn fails rather than shipping a truncated identity.
|
|
46
|
+
|
|
47
|
+
> ⚠️ **The `context_budget` gotcha.** A rich system prompt can run to tens of
|
|
48
|
+
> thousands of tokens — far past the 8000 default. If a newly created agent
|
|
49
|
+
> returns empty turns, raise `context_budget` (e.g. to `60000`) before looking
|
|
50
|
+
> anywhere else. See [Agents](AGENTS.md#default-limits).
|
|
51
|
+
|
|
52
|
+
### Compaction is not wired — except the mechanical dedupe
|
|
53
|
+
|
|
54
|
+
There is a settings stub for LLM-summarization compaction (`enabled: false`,
|
|
55
|
+
`keep_last`, a reserved utility-model slot), but **nothing consumes it today**
|
|
56
|
+
— and the Studio no longer shows a form for it, so the setting cannot be
|
|
57
|
+
switched on by accident. Size is managed purely by hard budget eviction.
|
|
58
|
+
Do not rely on compaction to shrink a bloated agent: tune `context_budget` and
|
|
59
|
+
keep the identity lean.
|
|
60
|
+
|
|
61
|
+
One cheap half **is** wired, opt-in per agent: `tool_output_compression` (DSL
|
|
62
|
+
`tool_output_compression`, or `"tool_output_compression": true` in the pack).
|
|
63
|
+
When on, byte-identical repeated **tool results** in the replayed history
|
|
64
|
+
collapse to a compact back-reference (the first occurrence stays full, with a
|
|
65
|
+
one-line summary) — no LLM involved. It changes what the model sees, so it is
|
|
66
|
+
never a default: an older detail is only in the first occurrence, and a model
|
|
67
|
+
that wants it re-calls the tool. Reach for it when a tool keeps returning the
|
|
68
|
+
same body (a catalog page, a status) and history is the fragment blowing the
|
|
69
|
+
budget first.
|
|
70
|
+
|
|
71
|
+
> The **Studio session screen** shows what the builder assembled per turn —
|
|
72
|
+
> tokens per category (identity, history, memory, …), the tools-schema estimate
|
|
73
|
+
> and the budget verdict (`used / cap`, evicted sources). Counts only, never
|
|
74
|
+
> fragment content.
|
|
75
|
+
|
|
76
|
+
## Memory
|
|
77
|
+
|
|
78
|
+
With `memory` enabled, an agent gains a built-in `remember` tool for durable
|
|
79
|
+
facts, and those facts (plus recent notes) are injected back into the prompt on
|
|
80
|
+
later turns — **including turns in a different session**. Memory is scoped per
|
|
81
|
+
agent. This is distinct from *session history*, which is the transcript of one
|
|
82
|
+
conversation; memory is the small set of facts that should outlive any single
|
|
83
|
+
conversation. Facts and notes are editable from the Studio agent page. See
|
|
84
|
+
[`examples/memory/`](https://github.com/guizaols/insika/tree/main/examples/memory/) for a runnable cross-session example.
|
|
85
|
+
|
|
86
|
+
## The provider prefix cache
|
|
87
|
+
|
|
88
|
+
Two distinct caching mechanisms — don't conflate them:
|
|
89
|
+
|
|
90
|
+
- **Automatic server-side prefix cache.** Some providers prefix-cache a stable
|
|
91
|
+
system prefix automatically, at no cost to configure. This works **only because**
|
|
92
|
+
the identity is at the top of the system block and the volatile
|
|
93
|
+
`<request_context>` is at the bottom, keeping the cacheable prefix byte-stable.
|
|
94
|
+
Anything that injects volatile content high in the system block breaks the cache.
|
|
95
|
+
- **Manual cache breakpoints (opt-in).** With `prompt_caching` on **and** a
|
|
96
|
+
provider that supports explicit cache control, the builder sets one cache
|
|
97
|
+
breakpoint at the end of the system block. Only enable this for a byte-stable
|
|
98
|
+
system — a volatile system turns every turn into a paid cache *write*.
|
|
99
|
+
|
|
100
|
+
Cache accounting surfaces as `cached_tokens` (reads) and `cache_creation_tokens`
|
|
101
|
+
(writes), visible in telemetry and the Studio tokens chip.
|
|
102
|
+
|
|
103
|
+
## The volume
|
|
104
|
+
|
|
105
|
+
Agents, prompts, skills, and tools are **data in SQLite**, not files on a volume.
|
|
106
|
+
A deploy swaps the image but does **not** rewrite the database — so changing a
|
|
107
|
+
committed file changes nothing on a running box. Updates happen at runtime through
|
|
108
|
+
the Studio, the API, or the DSL. The durable volume persists only the database
|
|
109
|
+
(with optional continuous replication — see [Deploy](DEPLOY.md)). This is why the
|
|
110
|
+
instinct to "commit a file and redeploy" fails for agents, skills, and tools alike.
|
|
111
|
+
|
|
112
|
+
## See also
|
|
113
|
+
|
|
114
|
+
- [Agents](AGENTS.md) — `context_budget` and the other limits.
|
|
115
|
+
- [Skills](SKILLS.md) — how the `<available_skills>` list is built and budgeted.
|
|
116
|
+
- [Tools](TOOLS.md) — deferred tools and `tool_search`.
|
|
117
|
+
- [Architecture](ARCHITECTURE.md) — where the Context Builder sits in a turn.
|