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/HARVEST.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Harvest
|
|
3
|
-
parent:
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Improve
|
|
4
|
+
nav_order: 6
|
|
5
5
|
permalink: /harvest/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -44,7 +44,7 @@ filters, each drop counted and logged:
|
|
|
44
44
|
regex; phrases match case/accent-folded at word boundaries. Every rejected
|
|
45
45
|
candidate is logged with the rule id.
|
|
46
46
|
2. **The grounding filter** — every product reference in a proposal must be
|
|
47
|
-
in the union of the origin sessions' evidence ids (
|
|
47
|
+
in the union of the origin sessions' evidence ids (the evidence ledger).
|
|
48
48
|
A store without `grounding.matcher.sku` does not mine at all: product
|
|
49
49
|
claims that cannot be verified are blocked by refusal, not by prompt.
|
|
50
50
|
3. **Dedup** — an open `(agent, name)` tuple or a skill the store already has.
|
|
@@ -59,11 +59,10 @@ regression disqualifies** — the gate is a veto, never a score to argue with.
|
|
|
59
59
|
Judges are mandatory in exactly the shapes the refinement gate already
|
|
60
60
|
refuses: no recorded baseline, an all-red baseline, and a judged baseline
|
|
61
61
|
replayed with no judge (a rubric'd case with no verdict would count as a
|
|
62
|
-
pass
|
|
62
|
+
pass, see [Evals](EVALS.md)).
|
|
63
63
|
|
|
64
64
|
The conversion gate is the second ruler: the store's funnel metric over the
|
|
65
|
-
criterion's window, compared to the **frozen baseline** (the
|
|
66
|
-
`freeze_funnel_baseline`). Outcome is evidence — this gate can only say "the
|
|
65
|
+
criterion's window, compared to the **frozen baseline** (`freeze_funnel_baseline`). Outcome is evidence — this gate can only say "the
|
|
67
66
|
store is measurably worse than the accepted state" or "there is nothing to
|
|
68
67
|
compare against". It refuses on missing data, never passes: no frozen
|
|
69
68
|
baseline, no criterion, no funnel store, a fold that has not converged — each
|
data/docs/KNOWLEDGE.md
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Knowledge
|
|
3
|
+
parent: Improve
|
|
4
|
+
nav_order: 4
|
|
5
|
+
permalink: /knowledge/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Knowledge — learning from conversations
|
|
9
|
+
|
|
10
|
+
An agent is amnesiac in exactly one dimension. It has history (the session),
|
|
11
|
+
memory (facts about one customer, written by the `remember` tool), skills
|
|
12
|
+
(procedures a human curated), and tool traces — but nothing turns a finished
|
|
13
|
+
conversation into knowledge the agent can use on someone else's turn. Every
|
|
14
|
+
conversation teaches a store things — which product customers actually ask
|
|
15
|
+
about, which objection keeps coming up, which CEP maps to which distribution
|
|
16
|
+
center — and today all of it is write-once trace data nobody reads back.
|
|
17
|
+
|
|
18
|
+
Knowledge is that loop. After a turn completes, the engine can extract durable
|
|
19
|
+
**concepts** from it — facts, procedures, policies, objections — and persist
|
|
20
|
+
them for the agent (never a customer, never a session) to build on.
|
|
21
|
+
|
|
22
|
+
**What ships today: extraction, consolidation, retrieval, export, and a
|
|
23
|
+
Studio page.** The engine writes what it learns, decides whether a repeat
|
|
24
|
+
sighting confirms, merges with, or contradicts what it already knew,
|
|
25
|
+
retrieves the concepts relevant to a turn's message back into the prompt,
|
|
26
|
+
and an operator can see, edit and resolve all of it in the Studio. Only the
|
|
27
|
+
optional FTS5 index remains, deferred with a measured trigger — see
|
|
28
|
+
[What's not here yet](#whats-not-here-yet).
|
|
29
|
+
|
|
30
|
+
## The concept format
|
|
31
|
+
|
|
32
|
+
One concept is one record — a markdown document with a YAML frontmatter
|
|
33
|
+
block, the same shape a `SKILL.md` uses:
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
---
|
|
37
|
+
name: cep-sudeste-cd-campinas
|
|
38
|
+
description: CEPs 13xxx-13999 ship from the Campinas DC, 1-2 business days.
|
|
39
|
+
type: fact # fact | entity | procedure | policy | objection
|
|
40
|
+
provenance: observed # policy (curated) | observed (learned from conversations)
|
|
41
|
+
confidence: 0.6 # 0..1, evidence-weighted
|
|
42
|
+
sources: ["sess_8f3c"] # session ids, never message content
|
|
43
|
+
occurrences: 1
|
|
44
|
+
created_at: 2026-08-24T18:02:11Z
|
|
45
|
+
updated_at: 2026-08-24T18:02:11Z
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
Orders to CEP range 13000-13999 are fulfilled by the Campinas DC. Quoted
|
|
49
|
+
delivery is 1-2 business days. Related: [[frete-gratis-acima-199]].
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The model only ever writes `name`, `description`, `type` and `body`. Every
|
|
53
|
+
other field — `provenance`, `confidence`, `sources`, `occurrences`, the
|
|
54
|
+
timestamps — is stamped by the engine; an extraction answer that tries to
|
|
55
|
+
supply one of those is rejected outright (the same discipline Facts applies
|
|
56
|
+
to a model-authored scope). `[[links]]` inside the body are plain text stored
|
|
57
|
+
as-is; retrieval resolves them lazily by name at read time (below) — that one
|
|
58
|
+
hop is the entire "graph," never a stored structure of its own.
|
|
59
|
+
|
|
60
|
+
**`provenance` is not decoration.** Everything the extractor writes is
|
|
61
|
+
`provenance: observed` — a claim learned from what people said in
|
|
62
|
+
conversations, not official policy. A promise an agent made ("we'll get back
|
|
63
|
+
to you in 48h") is observed practice, not a guarantee, and the field exists so
|
|
64
|
+
nothing downstream states it to a customer as a commitment. `provenance:
|
|
65
|
+
policy` is reserved for a concept a human authored or promoted by hand — the
|
|
66
|
+
engine never sets it.
|
|
67
|
+
|
|
68
|
+
## Enabling it — the `knowledge:` block
|
|
69
|
+
|
|
70
|
+
Knowledge is pack data on the agent, exactly like `distill:` or `harvest:` —
|
|
71
|
+
absent = the feature is off for that agent, byte-identical engine:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
agent = Insika.agent("store-support") do
|
|
75
|
+
instructions "…"
|
|
76
|
+
knowledge extract: true, retrieve: true,
|
|
77
|
+
types: %w[fact policy objection], # what the extractor may emit
|
|
78
|
+
top_k: 5 # concepts injected per turn (default 5)
|
|
79
|
+
# prompt: "<what counts as a concept for THIS store>" — the pack-authored
|
|
80
|
+
# half; absent = the engine's generic prompt. `model:` (absent = the
|
|
81
|
+
# platform utility_model) names the extractor AND the consolidator.
|
|
82
|
+
# index: "scan" (default, the only one built) | "fts5" (accepted, falls
|
|
83
|
+
# back to "scan" — the optional SQLite index isn't built yet).
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`extract` and `retrieve` are independent switches: an agent can learn without
|
|
88
|
+
recalling (write-only, inspected by hand), or recall without learning
|
|
89
|
+
(curate every concept by hand in the Studio, `provenance: policy`).
|
|
90
|
+
|
|
91
|
+
## The write path
|
|
92
|
+
|
|
93
|
+
After a turn completes, off the turn's critical path (the customer already has
|
|
94
|
+
the answer), the engine sends the turn's transcript slice to the platform
|
|
95
|
+
`utility_model` and asks for a JSON array of candidate concepts. The answer is
|
|
96
|
+
schema-validated, filtered against the agent's configured `types`, and any key
|
|
97
|
+
the model should not be writing is dropped and counted, never trusted. The
|
|
98
|
+
survivors are redacted for PII, stamped with provenance and a first-sighting
|
|
99
|
+
confidence, and written to the store — one `:knowledge_learned` event per
|
|
100
|
+
concept (name, type, agent — never content).
|
|
101
|
+
|
|
102
|
+
**Durability, honestly.** Extraction is best-effort: a crash between a turn's
|
|
103
|
+
terminal and the write loses that turn's concepts, not the conversation (which
|
|
104
|
+
is durable) and not a previously learned concept. The recovery path is a
|
|
105
|
+
re-scan, not a queue:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
insika knowledge:backfill --agent store-support --since 2026-08-01T00:00:00Z
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
replays the agent's stored sessions through the same extractor a live turn
|
|
112
|
+
uses.
|
|
113
|
+
|
|
114
|
+
## Consolidation — what happens on a repeat sighting
|
|
115
|
+
|
|
116
|
+
Writing a concept name that already exists is never a blind overwrite. The
|
|
117
|
+
engine compares the new sighting against what is on record and picks one of
|
|
118
|
+
three outcomes:
|
|
119
|
+
|
|
120
|
+
- **Same claim, reworded or reconfirmed** — a cheap, deterministic check
|
|
121
|
+
(no model call): occurrences go up, the new session id joins `sources`,
|
|
122
|
+
confidence climbs (`min(0.95, 0.5 + 0.1 × distinct sources)` — more
|
|
123
|
+
independent sightings, more confidence, never certainty). The body itself
|
|
124
|
+
is untouched, so an operator's edit is never silently discarded by a
|
|
125
|
+
repeat sighting.
|
|
126
|
+
- **Related claim** — the two bodies say compatible things that combine into
|
|
127
|
+
one coherent statement. A second model call (the only place this feature
|
|
128
|
+
spends a second call, and only when a name already exists) merges them;
|
|
129
|
+
the result bumps occurrences/sources/confidence the same way a same-claim
|
|
130
|
+
sighting does.
|
|
131
|
+
- **Contradicting claim** — the two bodies say genuinely different things.
|
|
132
|
+
**Never merged, never silently overwritten.** The new claim is appended
|
|
133
|
+
under a `## Contradiction` heading, confidence drops to a flat `0.4`, and
|
|
134
|
+
a `:knowledge_conflict` event fires. A human resolves it in the Studio by
|
|
135
|
+
editing the concept directly — there is no separate "resolve" action,
|
|
136
|
+
because resolving IS editing the markdown to say what's actually true.
|
|
137
|
+
|
|
138
|
+
When no model is configured for consolidation (or its answer is unusable),
|
|
139
|
+
the engine defaults to the conservative outcome — contradicting. A concept
|
|
140
|
+
this feature is unsure about becomes a human's problem, never a guess that
|
|
141
|
+
looks confident and might be wrong.
|
|
142
|
+
|
|
143
|
+
## The Studio page
|
|
144
|
+
|
|
145
|
+
`/studio/knowledge` — single-agent-scoped like Harvest (`?agent=`), not
|
|
146
|
+
shared like Skills, because a concept only ever belongs to one store. A
|
|
147
|
+
drill-down list (name, type, confidence, occurrences, updated_at) with a
|
|
148
|
+
conflict filter (`?status=conflict`) and the same CodeMirror markdown editor
|
|
149
|
+
Skills uses, version history and restore, and delete. Editing the raw
|
|
150
|
+
markdown is also how an operator promotes `provenance: observed` to `policy`
|
|
151
|
+
— there is no separate "promote" button, the field is just another line in
|
|
152
|
+
the file.
|
|
153
|
+
|
|
154
|
+
## Retrieval — what reaches a turn's prompt
|
|
155
|
+
|
|
156
|
+
With `knowledge.retrieve` on, every turn the engine searches the agent's
|
|
157
|
+
concepts for the ones relevant to the customer's message (pure term overlap —
|
|
158
|
+
no embeddings, no network call) and injects the top few as a level-1
|
|
159
|
+
`<knowledge>` block, the same progressive-disclosure shape Skills uses:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
<knowledge>
|
|
163
|
+
<concept name="cep-13-campinas" confidence="0.60" provenance="observed">CEPs 13xxx-13999 ship from the Campinas DC, 1-2 business days.</concept>
|
|
164
|
+
</knowledge>
|
|
165
|
+
|
|
166
|
+
If the customer's question needs more than the summary above, call
|
|
167
|
+
`load_knowledge("name")` FIRST — before any other lookup for that topic.
|
|
168
|
+
This is learned from past conversations, not official policy: never state
|
|
169
|
+
a `provenance="observed"` concept to the customer as a guarantee.
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Only `name`/`description`/`confidence`/`provenance` are shown — the same
|
|
173
|
+
"summary now, body on demand" shape a skill's level-1 list uses. A `load_knowledge`
|
|
174
|
+
tool (outside `tools_allow`, wired only when `retrieve` is on — same as
|
|
175
|
+
`load_skill`) fetches one concept's complete body. Calling it fires
|
|
176
|
+
`:knowledge_retrieved` — this, not the injection itself, is what the
|
|
177
|
+
adoption metric tracks (see [the honest limits](#the-honest-limits)): a
|
|
178
|
+
concept sitting unread in the prompt taught the agent nothing.
|
|
179
|
+
|
|
180
|
+
**One hop through `[[links]]`.** A matched concept's body may reference
|
|
181
|
+
`[[other-concept-name]]`; retrieval resolves those names against the store
|
|
182
|
+
and injects them too (capped at `top_k` again), so a concept that names its
|
|
183
|
+
neighbor arrives with it. No transitive walk — one level, deliberately, the
|
|
184
|
+
same reasoning a skill's declared `companions:` uses.
|
|
185
|
+
|
|
186
|
+
**Where it sits, and what gets cut first.** `<knowledge>` sits at priority 77
|
|
187
|
+
— below curated skills (80), above a single conversation's memory (75): a
|
|
188
|
+
human's playbook always outranks what the engine inferred, and what the
|
|
189
|
+
engine inferred outranks one customer's chat facts. Never pinned; under
|
|
190
|
+
budget pressure it goes before skills but survives longer than memory,
|
|
191
|
+
briefing, history and request context — see [Context](CONTEXT.md).
|
|
192
|
+
|
|
193
|
+
## External knowledge over MCP
|
|
194
|
+
|
|
195
|
+
A native knowledge base is not the only shape this can take. Mounting a
|
|
196
|
+
third-party knowledge or memory server as MCP tools on an agent is a
|
|
197
|
+
supported pattern, complementary to the native loop above — a deployment can
|
|
198
|
+
run both: this loop learns concepts from ITS OWN traffic automatically, while
|
|
199
|
+
an MCP-mounted server can serve as a synthesis/consultant tool an agent calls
|
|
200
|
+
explicitly.
|
|
201
|
+
|
|
202
|
+
The one adoption lesson worth carrying over regardless of which shape is used:
|
|
203
|
+
a model does not reliably call a recall/lookup tool just because it exists,
|
|
204
|
+
even when it clearly should. A polite "use this when relevant" instruction
|
|
205
|
+
measured close to zero calls under pressure; what worked was an explicit,
|
|
206
|
+
ordered rule naming the competing tools directly — "call the recall tool
|
|
207
|
+
FIRST, before any data lookup, on topics X" — not a sentiment about when it's
|
|
208
|
+
a good idea. Tool *adoption* is a prompt-ordering problem, not a tool-quality
|
|
209
|
+
one, and it is worth measuring (calls per conversation), not assuming.
|
|
210
|
+
|
|
211
|
+
## The honest limits
|
|
212
|
+
|
|
213
|
+
- **Best-effort extraction, re-scan recovery.** Same discipline as Facts: no
|
|
214
|
+
queue, no exactly-once claim. A concept is re-derivable from the session
|
|
215
|
+
transcript, so a missed extraction is recoverable, never lost.
|
|
216
|
+
- **Consolidation trusts the same model that extracts.** The "related vs.
|
|
217
|
+
contradicting" call is a model judgment, not a proof — the conservative
|
|
218
|
+
default (contradicting, when unsure) bounds the failure mode to "a human
|
|
219
|
+
looks at it," never "two different claims silently became one wrong one."
|
|
220
|
+
- **Precision is a forge audit.** The engine guarantees the gates (schema,
|
|
221
|
+
key-stripping, PII redaction, type allowlist); it cannot guarantee the
|
|
222
|
+
model's judgment about what is worth remembering. That is tuned per store,
|
|
223
|
+
the same way a Harvest or Facts prompt is.
|
|
224
|
+
- **Retrieval quality is not adoption.** Injecting the right concept proves
|
|
225
|
+
nothing if the model never reads it — measure `:knowledge_retrieved`
|
|
226
|
+
(retrieval calls per conversation), not just whether the block appeared.
|
|
227
|
+
The explicit, ordered instruction in the block exists because a softer
|
|
228
|
+
"when to use" wording measured close to zero calls in practice.
|
|
229
|
+
- **Term overlap, not understanding.** `Index::Scan` matches words, not
|
|
230
|
+
meaning — a concept phrased very differently from the customer's words
|
|
231
|
+
will not surface even if it answers the question. No embeddings by
|
|
232
|
+
design: revisit only with evidence that retrieval, not
|
|
233
|
+
extraction, is the bottleneck.
|
|
234
|
+
|
|
235
|
+
## Index::Scan's performance, measured not assumed
|
|
236
|
+
|
|
237
|
+
`Index::Scan` keeps a read cache per instance (the context provider holds
|
|
238
|
+
one for the whole process, never rebuilt per turn): a concept's YAML
|
|
239
|
+
frontmatter is parsed once and reused until that concept's own record
|
|
240
|
+
`updated_at` changes, so a write invalidates itself for free. Reproduce
|
|
241
|
+
these numbers with:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
bundle exec ruby scripts/bench_knowledge_index.rb
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
| concepts/agent | p50 | p95 |
|
|
248
|
+
|---|---|---|
|
|
249
|
+
| 50 | 0.28 ms | 0.47 ms |
|
|
250
|
+
| 200 | 1.17 ms | 1.56 ms |
|
|
251
|
+
| 1000 | 6.4 ms | 8.0 ms |
|
|
252
|
+
| 5000 | 35.6 ms | 41.3 ms |
|
|
253
|
+
|
|
254
|
+
At the scale this feature targets for the first year — hundreds of concepts
|
|
255
|
+
per agent — a warm-cache search costs a bit over a millisecond, close to
|
|
256
|
+
the engine's own documented per-turn overhead (see [Benchmark](BENCHMARK.md)).
|
|
257
|
+
Past roughly a thousand concepts it becomes a real, measurable cost again —
|
|
258
|
+
that specific, numeric point is the trigger for building `Index::FTS5`, not
|
|
259
|
+
a guess made in advance.
|
|
260
|
+
|
|
261
|
+
## Export
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
insika knowledge:export --agent store-support --out ./export [--tenant loja-a]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Writes one `<name>.md` per concept — the storage format IS the export
|
|
268
|
+
format, so this is a dump, not a converter (same discipline as Facts/
|
|
269
|
+
Harvest's own append-only records): each file is the concept's markdown,
|
|
270
|
+
byte for byte, directly consumable by okf-gem (`OKF::Bundle`) or graphify.
|
|
271
|
+
Re-running it is safe — nothing here is lossy, so there is no `--force` to
|
|
272
|
+
reason about.
|
|
273
|
+
|
|
274
|
+
`--format graphml` writes one combined `knowledge.graphml` instead: a node
|
|
275
|
+
per concept (`name`/`type`/`description`/`confidence`/`provenance` as node
|
|
276
|
+
data) and an edge per `[[link]]` that resolves to another concept in the
|
|
277
|
+
same export — a link to a concept outside the scope is dropped, never a
|
|
278
|
+
dangling edge. Hand-built, hand-escaped XML (no new dependency), directly
|
|
279
|
+
openable in Gephi, yEd, or graphify.
|
|
280
|
+
|
|
281
|
+
## What's not here yet
|
|
282
|
+
|
|
283
|
+
- **The optional FTS5 index** — `knowledge.index: "fts5"` is accepted but
|
|
284
|
+
falls back to `Index::Scan`. Deliberately not built yet: `Scan` was
|
|
285
|
+
measured (above), not assumed, and it comfortably meets this feature's
|
|
286
|
+
target scale. A deployment whose concept count is heading past ~1000 per
|
|
287
|
+
agent is the evidence that would justify building the SQLite
|
|
288
|
+
`MATCH`/`bm25()` adapter — not before.
|
|
289
|
+
- **Decay** — recency is a ranking tiebreak today; a real confidence decay
|
|
290
|
+
curve is a later, evidence-driven addition, not a default.
|
data/docs/LOADTEST.md
CHANGED
data/docs/MEDIA.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Media
|
|
3
|
+
parent: Integrate
|
|
4
|
+
nav_order: 3
|
|
5
|
+
permalink: /media/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Media
|
|
9
|
+
|
|
10
|
+
The engine transports media, it never means it. Photos, voice notes and documents
|
|
11
|
+
travel through the message contract as additive content parts; what a picture
|
|
12
|
+
*means* — a fitting room, a product mockup — stays a skill on top.
|
|
13
|
+
|
|
14
|
+
## In — the message contract
|
|
15
|
+
|
|
16
|
+
The engine transports media, it never means it. The message accepts additive
|
|
17
|
+
**content parts** alongside the text — voice notes and photos travel, and any
|
|
18
|
+
skill (a fitting room, an image QA) stays a consumer layer on top:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
curl -X POST /v1/messages?stream=false -H "Authorization: Bearer $TOKEN" \
|
|
22
|
+
-d '{ "agent": "store-support", "session_id": "chat-7",
|
|
23
|
+
"message": "", "parts": [
|
|
24
|
+
{ "type": "audio", "url": "https://cdn.example.com/voz.ogg" },
|
|
25
|
+
{ "type": "image", "url": "https://cdn.example.com/sofa.jpg" },
|
|
26
|
+
{ "type": "document", "url": "https://cdn.example.com/receita.pdf" }
|
|
27
|
+
] }'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **Audio** is transcribed (RubyLLM STT; model via `INSIKA_STT_MODEL`) and the
|
|
31
|
+
text enters the turn marked `source: "voice"` on the terminal event — the
|
|
32
|
+
consumer's signal the person spoke. A consumer that transcribes itself can
|
|
33
|
+
send the text with `"source": "voice"` directly. A domain vocabulary hint
|
|
34
|
+
(product names, brand terms) rides the transcription as `prompt:` —
|
|
35
|
+
per-agent `stt_prompt` (the DSL setter, or the Studio config form) beats the
|
|
36
|
+
deployment-wide `INSIKA_STT_PROMPT` env, which beats nothing. OPERATOR
|
|
37
|
+
config, never customer input.
|
|
38
|
+
- **Images** attach to the model's ask (vision); the provider bills them and
|
|
39
|
+
the usage flows like any ask. The first image URL is also
|
|
40
|
+
`{{ctx.image_url}}` for data tools — photo analysis outside the prompt, the
|
|
41
|
+
tool's own egress applying when it fetches.
|
|
42
|
+
- **Documents** (a prescription, a recipe, an invoice — most often a PDF)
|
|
43
|
+
attach the same way images do; the first document URL is
|
|
44
|
+
`{{ctx.document_url}}`. A model without document support fails the ask at
|
|
45
|
+
`:ruby_llm` with the provider's own error — the transport does not preflight
|
|
46
|
+
capability. Capped at 10 MB (`MAX_DOCUMENT_BYTES` — "a prescription, not an
|
|
47
|
+
archive"), separately from the 5 MB image cap.
|
|
48
|
+
- Media URLs (audio, image AND document) are fetched by the engine through
|
|
49
|
+
the same egress guard (a private/metadata target is refused — SSRF) and a
|
|
50
|
+
size ceiling per kind (1 MB audio, 5 MB image, 10 MB document: the bytes
|
|
51
|
+
land in this process). A refused, oversized or unreadable part fails the turn
|
|
52
|
+
loudly at the `:media` stage, never a silent drop.
|
|
53
|
+
- **Media alone is a turn.** A voice note with no caption is `parts` and an
|
|
54
|
+
empty `message` — the transcription becomes the message at the `:media`
|
|
55
|
+
stage. A media message never joins another turn (`collect`/`steer` move text
|
|
56
|
+
only, and the parts would be left behind), and a transcription that comes
|
|
57
|
+
back empty fails the turn instead of asking the model about nothing.
|
|
58
|
+
- **Parts are contract at the edge** — a malformed part (unknown type, an
|
|
59
|
+
image/audio/document without `url`, a text without `text`) is a 422 before
|
|
60
|
+
dispatch on `/v1/messages` and `/v1/responses`. `document` is an ADDITIVE
|
|
61
|
+
part type (the compatibility rule in [the /v1 API](API.md) — no
|
|
62
|
+
`Insika-Version` bump needed).
|
|
63
|
+
- `/v1/responses` accepts the OpenAI multimodal shape: `input` as an array of
|
|
64
|
+
text/image/audio/document parts.
|
|
65
|
+
|
|
66
|
+
### Image editing
|
|
67
|
+
|
|
68
|
+
`generate_image` (below) doesn't only generate — it can EDIT an existing
|
|
69
|
+
image, using `RubyLLM.paint`'s `with:`/`mask:`. The tool exposes
|
|
70
|
+
`source_image_urls` (an array — up to 4) and `mask_url`; when the model omits
|
|
71
|
+
`source_image_urls` AND the turn carries an inbound photo, that photo is
|
|
72
|
+
edited by default — no URL round-trip needed for "edit the photo the customer
|
|
73
|
+
just sent". Explicit URLs always win over the default. A text-to-image call
|
|
74
|
+
(no sources at all) is byte-identical to before this feature existed.
|
|
75
|
+
|
|
76
|
+
What the edit MEANS — a virtual try-on, a product mockup on the customer's
|
|
77
|
+
wall — is the calling skill's business; the tool only transports the bytes.
|
|
78
|
+
Not every image model can edit (`dall-e-3` cannot); a call against a
|
|
79
|
+
non-editing model fails at the provider, surfaced verbatim.
|
|
80
|
+
|
|
81
|
+
## Out — generated media
|
|
82
|
+
|
|
83
|
+
The turn can **produce** an image or a voice clip — but only when both sides of
|
|
84
|
+
the gate agree, because nothing leaks by default. The agent declares it may
|
|
85
|
+
generate media (`outputs` on the profile), and the **channel** declares it can
|
|
86
|
+
receive it (`channel.capabilities` on the request):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
curl -X POST /v1/responses -H "Authorization: Bearer $TOKEN" -d '{
|
|
90
|
+
"model": "openclaw:store-support", "user": "chat-7",
|
|
91
|
+
"input": "manda a foto do sofá da promoção",
|
|
92
|
+
"channel": { "capabilities": ["image_output", "audio_output"] }
|
|
93
|
+
}'
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
agent = Insika.agent("store-support") do
|
|
98
|
+
instructions "…"
|
|
99
|
+
outputs image: { model: "gpt-image-1", size: "1024x1024" }, # the AGENT's half
|
|
100
|
+
tts: { model: "tts-1", voice: "alloy" }
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- **Both gates** must pass for the model to even see the `generate_image` /
|
|
105
|
+
`tts` tools: the agent opted in (`outputs`) and the request declared the
|
|
106
|
+
matching capability (`image_output` / `audio_output` — an unknown value is a
|
|
107
|
+
422, never a silent ignore). The "abstraction admits only what leaks" rule.
|
|
108
|
+
- **The media rides the envelope, never the answer text.** The terminal event
|
|
109
|
+
and the `/v1/responses` completed frame carry an additive `output_parts`
|
|
110
|
+
array — `{ type: "image", mime_type:, base64:, model: }` /
|
|
111
|
+
`{ type: "audio", mime_type:, base64:, model: }`. The model's prose stays
|
|
112
|
+
the `:content` answer; the channel consumes the bytes next to it.
|
|
113
|
+
- **Generation is billed and counted.** Image tokens join the turn's usage
|
|
114
|
+
(like any ask). The speech API reports no token counts, so a TTS call adds
|
|
115
|
+
an honest `usage.media` counter and the part carries the `model` for
|
|
116
|
+
consumer-side pricing.
|
|
117
|
+
- **Seams, not magic.** The generator is injectable per kind (specs stub it);
|
|
118
|
+
the defaults are lazy: images via RubyLLM (paint), speech via a thin POST to
|
|
119
|
+
the OpenAI-compatible `/audio/speech` endpoint using the same provider
|
|
120
|
+
config the chat uses — RubyLLM as of 1.16.0 has no speech API. A generated
|
|
121
|
+
part over 8 MB refuses loudly, never silently truncates.
|
|
122
|
+
- **Not here:** what the generated image *means* — a fitting room, a product
|
|
123
|
+
mockup — is a skill on top. The engine transports bytes and cost.
|
|
124
|
+
|
|
125
|
+
## See also
|
|
126
|
+
|
|
127
|
+
- [The /v1 API](API.md) — the full message contract.
|
|
128
|
+
- [Channels](CHANNELS.md) — how WhatsApp and the web widget carry these parts.
|
data/docs/OBSERVABILITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Observability
|
|
3
|
-
parent: Operate
|
|
3
|
+
parent: Operate
|
|
4
4
|
nav_order: 1
|
|
5
5
|
permalink: /observability/
|
|
6
6
|
---
|
|
@@ -36,23 +36,28 @@ something that left no task of its own behind:
|
|
|
36
36
|
| `:turn_steered` | `task_id`, `count`, `total` | a message arrived mid-run and was appended to the turn in flight |
|
|
37
37
|
| `:turn_steer_released` | `task_id`, `released_as`, `count` | the run could not absorb it, so it became the turn `released_as` |
|
|
38
38
|
| `:turn_interrupted` | `task_id`, `replaced_by` | the turn was abandoned mid-run, and which turn replaced it |
|
|
39
|
-
| `:turn_stuck` | `task_id`, `agent`, `reason`, `message` | the agent declared it could not proceed (`signal_stuck
|
|
39
|
+
| `:turn_stuck` | `task_id`, `agent`, `reason`, `message` | the agent declared it could not proceed (`signal_stuck`) — the deterministic signal a consumer escalates on |
|
|
40
40
|
| `:channel_delivered` | `channel`, `outbox_id`, `status`, `attempts`, `error` | the answer reached the platform (or did not) — the turn completing says nothing about that |
|
|
41
|
-
| `:delivery_failed` | `channel`, `outbox_id`, `status`, `attempts`, `error` | a delivery exhausted its bounded retries — the alert face of the row above
|
|
42
|
-
| `:budget_warning` | `agent`, `tenant`, `window`, `spent`, `cap` | a calendar budget crossed its threshold (`alert_at` or a soft cap) — once per window
|
|
43
|
-
| `:breaker_open` | `agent`, `ref`, `tenant` | the reliability circuit breaker tripped for a `(tenant, provider/model)` — further turns fail fast until the cooldown
|
|
44
|
-
| `:provider_failure` | `agent`, `ref`, `error`, `kind` | one attempt against `ref` failed and spent a retry — emitted with or without a circuit breaker
|
|
45
|
-
| `:provider_fallback` | `agent`, `from`, `to`, `error`, `kind` | the turn ROTATED mid-flight to the next node of the fallback chain, and the error that caused it
|
|
46
|
-
| `:ttft` | `task_id`, `session_id`, `ttft_ms` | the provider's time-to-first-token on the streaming envelope — only under `INSIKA_TURN_TIMING`, once per turn
|
|
47
|
-
| `:distillation_completed` | `session_ref`, `agent`, `proposals`, `dropped{}`, `deduped`, `cost{}` | a session's traffic was distilled into N proposals
|
|
41
|
+
| `:delivery_failed` | `channel`, `outbox_id`, `status`, `attempts`, `error` | a delivery exhausted its bounded retries — the alert face of the row above |
|
|
42
|
+
| `:budget_warning` | `agent`, `tenant`, `window`, `spent`, `cap` | a calendar budget crossed its threshold (`alert_at` or a soft cap) — once per window |
|
|
43
|
+
| `:breaker_open` | `agent`, `ref`, `tenant` | the reliability circuit breaker tripped for a `(tenant, provider/model)` — further turns fail fast until the cooldown |
|
|
44
|
+
| `:provider_failure` | `agent`, `ref`, `error`, `kind` | one attempt against `ref` failed and spent a retry — emitted with or without a circuit breaker |
|
|
45
|
+
| `:provider_fallback` | `agent`, `from`, `to`, `error`, `kind` | the turn ROTATED mid-flight to the next node of the fallback chain, and the error that caused it |
|
|
46
|
+
| `:ttft` | `task_id`, `session_id`, `ttft_ms` | the provider's time-to-first-token on the streaming envelope — only under `INSIKA_TURN_TIMING`, once per turn |
|
|
47
|
+
| `:distillation_completed` | `session_ref`, `agent`, `proposals`, `dropped{}`, `deduped`, `cost{}` | a session's traffic was distilled into N proposals — counts and ids only, never a fact value |
|
|
48
48
|
| `:proposal_approved` | `proposal_id`, `status`, `operator` | a human approved a distilled fact on the Facts page — written to memory via CAS |
|
|
49
49
|
| `:proposal_rejected` | `proposal_id`, `status`, `operator` | a human rejected it (the reason is on the proposal record, not in the event) |
|
|
50
50
|
| `:proposal_dismissed` | `proposal_id`, `status`, `operator` | a human dismissed it — the tuple is latched, never proposed again |
|
|
51
51
|
| `:proposal_stale` | `proposal_id`, `status`, `operator` | the CAS lost: the fact moved after distillation — re-presented with both values on the wiki, never silently overwritten |
|
|
52
|
-
| `:harvest_mined` | `agent`, `run_id`, `candidates`, `rejected{}`, `cost{}` | a mining pass finished
|
|
52
|
+
| `:harvest_mined` | `agent`, `run_id`, `candidates`, `rejected{}`, `cost{}` | a mining pass finished — counts and rule ids only, never a skill body |
|
|
53
53
|
| `:harvest_gated` | `run_id`, `candidate_id`, `agent`, `eval_passed`, `conversion_passed`, `reason` | the double gate's verdict on one candidate — ids and verdicts only |
|
|
54
54
|
| `:skill_promoted` | `agent`, `skill`, `candidate_id`, `snapshot_ref`, `promotion_ref`, `approver` | a human approved a mined skill — it is live for the store, with the snapshot for rollback |
|
|
55
55
|
| `:skill_rolled_back` | `snapshot_ref`, `skill`, `agent`, `operator` | the snapshot was restored — the promotion row carries the `rolled_back_at` stamp |
|
|
56
|
+
| `:knowledge_learned` | `name`, `type`, `agent` | a turn's transcript taught the agent a new concept, or a related sighting merged into an existing one — name and type only, never the body |
|
|
57
|
+
| `:knowledge_conflict` | `name`, `agent` | a repeat sighting contradicted what's on record — never merged, appended under a heading, needs a human in the Studio |
|
|
58
|
+
| `:knowledge_deleted` | `name`, `agent` | an operator removed a concept from the Studio |
|
|
59
|
+
| `:knowledge_retrieved` | `name`, `agent` | the model called `load_knowledge` — the adoption signal (retrieval calls per conversation), not the `<knowledge>` injection itself |
|
|
60
|
+
| `:knowledge_backfilled` | `agent`, `sessions`, `concepts`, `conflicts`, `dropped{}` | the recovery re-scan (`insika knowledge:backfill`) finished replaying an agent's stored sessions |
|
|
56
61
|
|
|
57
62
|
`delivery_failed` and `breaker_open` are the two the operator config is pointed at
|
|
58
63
|
(`alerts.webhook` on the profile): each only fires when something durable did
|
data/docs/OUTCOMES.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Outcomes and follow-ups
|
|
3
|
+
parent: Improve
|
|
4
|
+
nav_order: 3
|
|
5
|
+
permalink: /outcomes/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Outcomes and follow-ups
|
|
9
|
+
|
|
10
|
+
## Outcomes — business results over real traffic
|
|
11
|
+
|
|
12
|
+
The engine measures what it is told to measure. The operator or the integration
|
|
13
|
+
records a conversation's business outcome after the fact — `conversion`,
|
|
14
|
+
`escalation`, `deflected`, anything, optionally with a monetary `value`:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
curl -X POST /v1/outcomes -H "Authorization: Bearer $TOKEN" \
|
|
18
|
+
-d '{ "agent": "store-support", "session_id": "chat-7",
|
|
19
|
+
"outcome": "conversion", "value": 129.9 }'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The endpoint is **additive and outside the response contract** — the turn never
|
|
23
|
+
knows or cares; the engine transports the outcome and never interprets it (what
|
|
24
|
+
"conversion" means is yours). Records are tenant-stamped (a tenant principal
|
|
25
|
+
writes and reads only its own), and `GET /v1/outcomes?agent=` serves the last
|
|
26
|
+
outcome per agent plus the per-day series — the last-outcome pill on the Studio
|
|
27
|
+
agent grid, and the per-day series on the agent detail.
|
|
28
|
+
|
|
29
|
+
### The outcome funnel
|
|
30
|
+
|
|
31
|
+
A store's funnel is pack data on the agent — the engine folds outcomes into
|
|
32
|
+
the **declared** stages, and never hard-codes one itself (the stage vocabulary
|
|
33
|
+
is the forge's):
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
agent = Insika.agent("store-support") do
|
|
37
|
+
instructions "…"
|
|
38
|
+
funnel stages: %w[greeted qualified cart paid],
|
|
39
|
+
advance_on: { "abandoned_cart" => "cart", "pix_paid" => "paid" },
|
|
40
|
+
primary: "paid", attribution_window: "72h"
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The fold contract:
|
|
45
|
+
|
|
46
|
+
- **Tick-driven, cumulative event counts on the declared order.** An outcome of
|
|
47
|
+
kind K means the session *reached* `advance_on[K]`; the fold increments
|
|
48
|
+
`stages[0..index]` for the reached stage. A per-stage-complete integration
|
|
49
|
+
and a terminal-event integration therefore produce identical counts — a
|
|
50
|
+
session that paid also emitted the earlier events. A duplicate event
|
|
51
|
+
double-counts (the integration's defect, not the engine's); **do not declare
|
|
52
|
+
a stage off the linear path** (a "handoff" stage would be inflated by every
|
|
53
|
+
later event). Counts are **event counts, not distinct sessions** — the
|
|
54
|
+
baseline is events-based.
|
|
55
|
+
- **Idempotent**: a per-pair `{at, ids}` cursor inside one transaction; a crash
|
|
56
|
+
mid-fold never double counts, and a second pass folds only what is new.
|
|
57
|
+
- **The attribution window is carried data, never computed** — `72h` is
|
|
58
|
+
validated, rendered, and copied into the baseline snapshot; causal
|
|
59
|
+
attribution stays human.
|
|
60
|
+
- **The baseline freeze** (Studio > Funnel, or `:freeze_funnel_baseline` on the
|
|
61
|
+
bus) sums the folded cells over a span of **≥ 28 days** (shorter spans are
|
|
62
|
+
refused) into one current snapshot per `(tenant, agent)` — the number
|
|
63
|
+
the follow-up A/B and the harvest promotion gate compare against.
|
|
64
|
+
- **Malformed declarations never crash the tick**: the fold skips them, the
|
|
65
|
+
doctor names the defect, the Studio shows nothing until it is fixed.
|
|
66
|
+
- Vocabulary note: in the gem this is the **outcome funnel** — the stage names
|
|
67
|
+
are the forge's, and a bare install (no `funnel:` on any agent) shows no
|
|
68
|
+
funnel and no stage names at all.
|
|
69
|
+
|
|
70
|
+
## Follow-ups — the seller who comes back
|
|
71
|
+
|
|
72
|
+
The agent can book a follow-up with a customer at a future time — "te chamo
|
|
73
|
+
amanhã se o PIX não cair" said in-conversation and meant. The engine fires the
|
|
74
|
+
synthetic turn on its own tick, with consent and without spam. Everything is
|
|
75
|
+
pack data on the profile:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
agent = Insika.agent("store-support") do
|
|
79
|
+
instructions "…"
|
|
80
|
+
followup arm: "schedule",
|
|
81
|
+
policy: { quiet_hours: { timezone: "America/Sao_Paulo",
|
|
82
|
+
start: "21:30", end: "09:00" },
|
|
83
|
+
max_frequency: "2/24h", # N outbound per window, per customer
|
|
84
|
+
cancel_keywords: ["não quero mais contato"],
|
|
85
|
+
silence_after_sends: 3 } # N fires without a reply -> :unavailable
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The pieces:
|
|
90
|
+
|
|
91
|
+
- **`schedule(at:, reason:)`** — a built-in tool the agent calls when the
|
|
92
|
+
customer agrees to be contacted again (a product, a cart, a pending payment).
|
|
93
|
+
The call itself IS the consent record — recorded without ever lifting
|
|
94
|
+
`:unavailable` or resetting the silence counter (ONLY a customer message
|
|
95
|
+
reopens, so a re-booking inside a follow-up turn cannot clear the silence
|
|
96
|
+
protection). `cancel_followup(id:)` is the sibling. A customer who opted out
|
|
97
|
+
can never be rescheduled.
|
|
98
|
+
- **Contact state per customer** — `granted | revoked | unavailable` in a
|
|
99
|
+
durable cell per `(tenant, customer)`. Only `granted` may be messaged;
|
|
100
|
+
`revoked` is immediate and permanent until the customer speaks again;
|
|
101
|
+
`unavailable` means silence ≠ refusal — the engine stops firing after
|
|
102
|
+
`silence_after_sends` unanswered sends, and ANY customer message reopens.
|
|
103
|
+
The policy's `cancel_keywords` are matched on every inbound message: a
|
|
104
|
+
match revokes the contact and cancels its pending follow-ups in one
|
|
105
|
+
transaction.
|
|
106
|
+
- **Firing is the tick's third duty** — the engine claims the due records
|
|
107
|
+
(one per claim window, at-most-once across workers), applies the policy in
|
|
108
|
+
force AT FIRE TIME (contact state, quiet hours, dedup per
|
|
109
|
+
`(customer, reason)`, frequency ceiling) and either enqueues the synthetic
|
|
110
|
+
turn or marks the record `blocked` with the failing rule — auditable, never
|
|
111
|
+
silent. Blocking happens at fire time, never at schedule time: the schedule
|
|
112
|
+
is a promise made in-conversation, and only the policy in force then may
|
|
113
|
+
revoke it.
|
|
114
|
+
- **The synthetic turn** — a first-class inbound turn stamped
|
|
115
|
+
`origin: "scheduled"` (a refinement read can never mistake the engine's
|
|
116
|
+
kick for the customer repeating themselves), delivered through the full
|
|
117
|
+
pipeline on the channel the conversation came in on. It skips the edge's
|
|
118
|
+
ENTRY rate/token checks like a resume does — a follow-up she agreed to must
|
|
119
|
+
not receive the rate-limit reply; its usage still lands on the ledger.
|
|
120
|
+
- **The Follow-ups page** (Studio) — per agent: the pending/fired/cancelled/
|
|
121
|
+
blocked records (blocked rows carry the reason), the read-only policy
|
|
122
|
+
summary and the A/B card: per arm, `sent` vs `conversions` (against the
|
|
123
|
+
frozen baseline) vs `opt-outs`. The only mutations — cancel a pending
|
|
124
|
+
record, force-revoke a contact — go through bus commands.
|
|
125
|
+
- **LGPD** — the records and cells die with the customer (`forget_customer`),
|
|
126
|
+
the tenant (`delete_tenant_data`) and age out under the same
|
|
127
|
+
`retention_days` sweep as the rest of the footprint.
|
|
128
|
+
|
|
129
|
+
Absent `followup:` = the feature is off for that agent — no tools wired, no
|
|
130
|
+
records, byte-identical turns. The A/B against an existing cron is an
|
|
131
|
+
operator experiment: the engine only keeps the records and the read card (the
|
|
132
|
+
cron arm writes through the same store class with its own `arm` label).
|
|
133
|
+
|
|
134
|
+
## See also
|
|
135
|
+
|
|
136
|
+
- [Evals](EVALS.md) — the cases that grade an agent before traffic does.
|
|
137
|
+
- [Refinement](REFINEMENT.md) — reading an agent's own traffic back as a report.
|