insika 0.2.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +325 -17
- data/README.md +52 -11
- data/bin/insika +895 -8
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +229 -163
- data/docs/API.md +56 -0
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +111 -21
- data/docs/CONTEXT.md +132 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +75 -4
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +146 -2
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +116 -0
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +16 -10
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +65 -6
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +17 -12
- data/docs/RELEASING.md +27 -0
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +6 -6
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +79 -2
- data/docs/SKILLS.md +13 -4
- data/docs/SOAK.md +127 -0
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +220 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +115 -0
- data/docs/improve.md +20 -0
- data/docs/index.md +9 -6
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +202 -10
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/budget_ledger.rb +34 -3
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +119 -14
- data/lib/insika/channels/relay.rb +77 -3
- data/lib/insika/channels/web.rb +2 -2
- data/lib/insika/chat_builder.rb +105 -2
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +107 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/send_message.rb +103 -6
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +21 -3
- data/lib/insika/context/fragment.rb +7 -3
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +57 -23
- data/lib/insika/context/providers/skill.rb +2 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +38 -2
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +889 -11
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +69 -82
- data/lib/insika/dsl/server_boot.rb +92 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +186 -5
- data/lib/insika/edge_limiter.rb +28 -7
- data/lib/insika/env_schema.rb +25 -3
- data/lib/insika/errors.rb +11 -0
- data/lib/insika/evals/assertions.rb +3 -2
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +29 -6
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +869 -76
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +198 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +17 -2
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +382 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +7 -2
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +22 -1
- data/lib/insika/outbox_store.rb +42 -4
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +29 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/queue_policy.rb +4 -1
- data/lib/insika/refinement/proposer.rb +1 -1
- data/lib/insika/reliability.rb +32 -6
- data/lib/insika/retention.rb +316 -0
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +44 -4
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +32 -113
- data/lib/insika/safety/factory.rb +15 -2
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +1 -1
- data/lib/insika/safety/moderator.rb +1 -1
- data/lib/insika/safety/output_filter.rb +9 -5
- data/lib/insika/safety/output_validator.rb +11 -5
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +247 -17
- data/lib/insika/server/rack_app.rb +21 -1
- data/lib/insika/server/responses.rb +40 -1
- data/lib/insika/session_actor.rb +8 -4
- data/lib/insika/session_store.rb +65 -2
- data/lib/insika/settings_store.rb +10 -0
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/skill_catalog.rb +16 -0
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/store.rb +10 -1
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1437 -54
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +23 -23
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +308 -10
- data/lib/insika/studio/nav_icons.rb +19 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +62 -278
- data/lib/insika/studio/views/agents.erb +71 -54
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +6 -3
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +106 -66
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +15 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/session.erb +233 -124
- data/lib/insika/studio/views/settings.erb +41 -1
- data/lib/insika/studio/views/skills.erb +1 -2
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +24 -10
- data/lib/insika/task_store.rb +21 -1
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +38 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +58 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/testing/store_contract.rb +27 -6
- data/lib/insika/tick.rb +47 -11
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_definition.rb +17 -7
- data/lib/insika/tool_envelope.rb +69 -0
- data/lib/insika/tool_manifest.rb +5 -1
- data/lib/insika/tools/data_defined_tool.rb +10 -0
- data/lib/insika/tools/generate_image.rb +89 -0
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +49 -1
- data/lib/insika/turn_timing.rb +22 -2
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +472 -13
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +149 -3
- metadata +181 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
data/docs/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Load test
|
|
3
|
-
parent: Operate
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Operate
|
|
4
|
+
nav_order: 4
|
|
5
5
|
permalink: /loadtest/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -16,7 +16,7 @@ The whole point: the engine exposes `POST /v1/responses` as an **SSE drop-in** o
|
|
|
16
16
|
the OpenClaw gateway. Same contract → the same load tools work against either side,
|
|
17
17
|
so you can measure the engine you are about to ship against the gateway it replaces.
|
|
18
18
|
|
|
19
|
-
There are
|
|
19
|
+
There are four scripts, each answering a different question:
|
|
20
20
|
|
|
21
21
|
| Script | Question it answers | Needs a provider? |
|
|
22
22
|
|--------|---------------------|-------------------|
|
|
@@ -24,8 +24,13 @@ There are three scripts, each answering a different question:
|
|
|
24
24
|
| `scripts/loadtest.rb` | End-to-end: TTFB/total/tokens/cache/error against `/v1/responses` | Yes |
|
|
25
25
|
| `scripts/loadtest-local.sh` | Single-proc baseline vs N-worker multi-proc on one box | Yes |
|
|
26
26
|
| `scripts/loadtest_session.rb` | A full multi-message session (CEP, searches, FAQ) under C concurrent sessions — direct to the engine (`--surface engine`, stream vs steer) or through the consumer's real ingress (`--surface web`, the consumer's widget API) | Yes |
|
|
27
|
+
| `insika soak` | Does the deploy degrade over 72 h of steady load? | Yes |
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
The first four take `--help` / `-h`; the soak is a shipped command (`insika soak --help`) rather
|
|
30
|
+
than a repo script. Bursts and uptime are different questions: a wave driver
|
|
31
|
+
measures a burst, and the soak's arrival process measures *degradation over
|
|
32
|
+
uptime* — the load-test table above deliberately stops where
|
|
33
|
+
[Soak](SOAK.md) begins.
|
|
29
34
|
|
|
30
35
|
---
|
|
31
36
|
|
|
@@ -64,7 +69,7 @@ frame that carries it.
|
|
|
64
69
|
INSIKA_URL=http://localhost:9292 \
|
|
65
70
|
OPENCLAW_GATEWAY_TOKEN=xxx \
|
|
66
71
|
bundle exec ruby scripts/loadtest.rb \
|
|
67
|
-
--agents
|
|
72
|
+
--agents demo,my-store --concurrency 16 --iterations 3 \
|
|
68
73
|
--message "hi, how are you?"
|
|
69
74
|
```
|
|
70
75
|
|
|
@@ -75,7 +80,7 @@ Runs against a local server **or** a remote one (e.g. Railway) — just point
|
|
|
75
80
|
|
|
76
81
|
| Flag | Default | Meaning |
|
|
77
82
|
|------|---------|---------|
|
|
78
|
-
| `--agents a,b,c` | `
|
|
83
|
+
| `--agents a,b,c` | `demo` | comma-separated agent ids (mapped to `model: openclaw:<agent>`) |
|
|
79
84
|
| `--concurrency N` | `8` | concurrent turns per wave |
|
|
80
85
|
| `--iterations N` | `1` | number of waves per agent |
|
|
81
86
|
| `--message TEXT` | greeting | user message sent every turn |
|
|
@@ -98,7 +103,7 @@ Use `--dry-run` to sanity-check your flags/URL/token before firing real traffic
|
|
|
98
103
|
|
|
99
104
|
```bash
|
|
100
105
|
INSIKA_URL=http://localhost:9292 OPENCLAW_GATEWAY_TOKEN=xxx \
|
|
101
|
-
bundle exec ruby scripts/loadtest.rb --agents
|
|
106
|
+
bundle exec ruby scripts/loadtest.rb --agents demo --concurrency 16 --dry-run
|
|
102
107
|
```
|
|
103
108
|
|
|
104
109
|
### `--same-user` and the cache
|
|
@@ -130,7 +135,7 @@ DEEPSEEK_API_KEY=sk-... ./scripts/loadtest-local.sh [WORKERS] [CONCURRENCY]
|
|
|
130
135
|
| `DEEPSEEK_API_KEY` | — (required) | real turns hit the provider; also auto-sourced from `.env.local` |
|
|
131
136
|
| `OPENCLAW_GATEWAY_TOKEN` | falls back to `ADMIN_TOKEN`, then `local-demo` | Bearer for the sweep |
|
|
132
137
|
| `PORT` | `9299` | bind port for the local Falcon |
|
|
133
|
-
| `AGENT` | `
|
|
138
|
+
| `AGENT` | `demo` | agent id to load |
|
|
134
139
|
|
|
135
140
|
The final block prints the lock counts; the expected reading is `0` for both:
|
|
136
141
|
|
|
@@ -163,7 +168,7 @@ drop-in for the gateway, you only change **where it points**:
|
|
|
163
168
|
# In the OpenClaw checkout, run its gateway loadtest against the HARNESS:
|
|
164
169
|
OPENCLAW_GATEWAY_URL=http://localhost:9292 \
|
|
165
170
|
OPENCLAW_GATEWAY_TOKEN=<same bearer the engine accepts> \
|
|
166
|
-
node scripts/loadtest-gateway.mjs --agents
|
|
171
|
+
node scripts/loadtest-gateway.mjs --agents demo --concurrency 16 --iterations 3
|
|
167
172
|
```
|
|
168
173
|
|
|
169
174
|
Then run the exact same command with `OPENCLAW_GATEWAY_URL` pointing at the real
|
|
@@ -174,7 +179,7 @@ gateway, and diff the two reports. This is the shadow comparison the pilot needs
|
|
|
174
179
|
- The OpenClaw checkout containing `scripts/loadtest-gateway.mjs` and Node installed.
|
|
175
180
|
- A **bearer token accepted by both** sides. For the engine that is
|
|
176
181
|
`OPENCLAW_GATEWAY_TOKEN` (see DEPLOY.md); point the gateway run at its own token.
|
|
177
|
-
- **The same agent id provisioned on both** sides (e.g. `
|
|
182
|
+
- **The same agent id provisioned on both** sides (e.g. `demo`) so `model:
|
|
178
183
|
openclaw:<agent>` resolves on each. On the engine, provision via
|
|
179
184
|
`scripts/import_pack.rb`.
|
|
180
185
|
- The **same provider** (or an equivalent-latency one) behind each, otherwise you
|
|
@@ -224,6 +229,7 @@ Work top-down and **measure before assuming** — avoid premature topology optim
|
|
|
224
229
|
| 4 | Insika vs gateway, identical knobs | §4 (either method) | Is the engine at parity with the engine it replaces before cut-over? |
|
|
225
230
|
| 5 | Cold vs hot conversation (cache) | `loadtest.rb` with/without `--same-user 1` | Expected steady-state cost/latency once conversations warm up. |
|
|
226
231
|
| 6 | Remote (Railway) vs local | `loadtest.rb` with `INSIKA_URL` remote | Network/deploy overhead of the real environment. |
|
|
232
|
+
| 7 | Degradation over uptime (72 h) | `insika soak` (see [Soak](SOAK.md)) | The cut argument: does latency or memory grow with uptime? Run it last — it needs the topology settled first. |
|
|
227
233
|
|
|
228
234
|
**Reaching for horizontal scale is only justified after 1–3 show the single box is
|
|
229
235
|
the limit.** If it is, the paths are: sharding-by-tenant +
|
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,12 +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
|
-
| `:
|
|
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
|
+
| `:proposal_approved` | `proposal_id`, `status`, `operator` | a human approved a distilled fact on the Facts page — written to memory via CAS |
|
|
49
|
+
| `:proposal_rejected` | `proposal_id`, `status`, `operator` | a human rejected it (the reason is on the proposal record, not in the event) |
|
|
50
|
+
| `:proposal_dismissed` | `proposal_id`, `status`, `operator` | a human dismissed it — the tuple is latched, never proposed again |
|
|
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 — counts and rule ids only, never a skill body |
|
|
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
|
+
| `: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
|
+
| `: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 |
|
|
45
61
|
|
|
46
62
|
`delivery_failed` and `breaker_open` are the two the operator config is pointed at
|
|
47
63
|
(`alerts.webhook` on the profile): each only fires when something durable did
|
|
@@ -366,6 +382,49 @@ consumer that fell far behind would have its subscription closed (telemetry stop
|
|
|
366
382
|
the turn does not). Span and instrument operations are cheap, so there's ample
|
|
367
383
|
headroom.
|
|
368
384
|
|
|
385
|
+
## Process vitals — `GET /v1/vitals`
|
|
386
|
+
|
|
387
|
+
OTel carries turn/tool telemetry; it says nothing about the **process**. For the
|
|
388
|
+
questions a soak (or any operator) asks — *which process is this, how long has it
|
|
389
|
+
been up, how much memory does it hold, and what is the Ruby heap doing?* — there
|
|
390
|
+
is one read-only route:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
curl -H "Authorization: Bearer $TOKEN" https://<target>/v1/vitals
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
```jsonc
|
|
397
|
+
{
|
|
398
|
+
"boot_id": "20260820T09-…", // one per container start; a change = a restart
|
|
399
|
+
"pid": 42,
|
|
400
|
+
"started_at": "2026-08-20T09:00:00Z",
|
|
401
|
+
"uptime_s": 259200,
|
|
402
|
+
"version": "0.2.0",
|
|
403
|
+
"ruby": "ruby 3.4.1 …",
|
|
404
|
+
"yjit": true,
|
|
405
|
+
"rss_bytes": 536870912, // nil when unreadable — never a guess
|
|
406
|
+
"gc": { "heap_live_slots": …, "major_gc_count": …, "malloc_increase_bytes": … },
|
|
407
|
+
"threads": 8,
|
|
408
|
+
"in_flight": 1, // the executor's in-flight turns
|
|
409
|
+
"db_bytes": { "db": …, "wal": …, "shm": … },
|
|
410
|
+
"at": "2026-08-20T09:00:00Z"
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
The two fields that make it a **restart detector**: `boot_id` (one per container
|
|
415
|
+
start, exported by the entrypoint and shared by every worker) and `pid`. A
|
|
416
|
+
`boot_id` change is a container restart; a `pid` change under the same `boot_id`
|
|
417
|
+
is a worker respawn — the event a platform metrics API cannot see.
|
|
418
|
+
|
|
419
|
+
- **Operator-only.** The route is not in the public allowlist (no bearer →
|
|
420
|
+
unauthorized) and not on the tenant surface, so only an operator reads
|
|
421
|
+
process internals. `/up` stays the public health probe and carries no
|
|
422
|
+
process data.
|
|
423
|
+
- **Reads no store.** Pure OS/VM readings — safe to poll at any rate, and it
|
|
424
|
+
cannot contend with turns.
|
|
425
|
+
- **The soak's sampler.** [Soak](SOAK.md) polls it hourly; a `nil` RSS reads as
|
|
426
|
+
missing coverage, never as zero.
|
|
427
|
+
|
|
369
428
|
---
|
|
370
429
|
|
|
371
430
|
*Packaging note.* Today the bridge lives in the Insika repo as an opt-in core
|