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/bin/insika-router
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# `insika-router` — session-sticky router. A standalone Async/
|
|
5
|
+
# Falcon process that sits in front of N engine backends and routes each
|
|
6
|
+
# request to the SAME backend every time its session id resolves to (a
|
|
7
|
+
# ketama-style consistent hash ring), so `WEB_CONCURRENCY=1`-per-worker's
|
|
8
|
+
# session guarantees (FIFO/collect/steer, docs/DEPLOY.md "The process model")
|
|
9
|
+
# hold at N>1 capacity without touching the engine itself.
|
|
10
|
+
#
|
|
11
|
+
# Entirely OPT-IN: an operator happy with one worker never runs this. Two
|
|
12
|
+
# backend-discovery modes, picked by which env var is set (never both):
|
|
13
|
+
#
|
|
14
|
+
# INSIKA_ROUTER_BACKENDS=http://host-a:9292,http://host-b:9292
|
|
15
|
+
# static list — the Railway shape (N local Falcon workers on known ports).
|
|
16
|
+
# INSIKA_ROUTER_BACKENDS_DNS=insika-headless.default.svc.cluster.local
|
|
17
|
+
# one hostname, re-resolved on INSIKA_ROUTER_DNS_INTERVAL (default 15s) —
|
|
18
|
+
# the Kubernetes shape (a headless Service, one record per ready pod).
|
|
19
|
+
# Requires INSIKA_ROUTER_BACKEND_PORT (the engine port on every pod).
|
|
20
|
+
#
|
|
21
|
+
# See docs/ROUTER.md for both deploy shapes end to end.
|
|
22
|
+
|
|
23
|
+
require "async"
|
|
24
|
+
require "async/http/server"
|
|
25
|
+
require "async/http/endpoint"
|
|
26
|
+
require "protocol/rack"
|
|
27
|
+
|
|
28
|
+
require_relative "../lib/insika/env_schema"
|
|
29
|
+
require_relative "../lib/insika/router"
|
|
30
|
+
|
|
31
|
+
module Insika
|
|
32
|
+
module CLI
|
|
33
|
+
module Router
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def main
|
|
37
|
+
Insika::EnvSchema.reconcile_legacy!
|
|
38
|
+
pool = build_pool
|
|
39
|
+
app = Insika::Router::App.new(
|
|
40
|
+
pool: pool,
|
|
41
|
+
body_max_bytes: (Insika::EnvSchema.read("INSIKA_ROUTER_BODY_MAX_BYTES") || Insika::Router::App::DEFAULT_BODY_MAX_BYTES).to_i,
|
|
42
|
+
backend_timeout: (Insika::EnvSchema.read("INSIKA_ROUTER_BACKEND_TIMEOUT") || 10).to_i
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
host = Insika::EnvSchema.read("INSIKA_ROUTER_HOST") || "0.0.0.0"
|
|
46
|
+
port = (Insika::EnvSchema.read("INSIKA_ROUTER_PORT") || 9090).to_i
|
|
47
|
+
endpoint = Async::HTTP::Endpoint.parse("http://#{host}:#{port}")
|
|
48
|
+
|
|
49
|
+
banner(pool, host, port)
|
|
50
|
+
Async do |task|
|
|
51
|
+
pool.start_polling(task)
|
|
52
|
+
Async::HTTP::Server.new(Protocol::Rack::Adapter.new(app), endpoint).run
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def build_pool
|
|
57
|
+
static = Insika::EnvSchema.read("INSIKA_ROUTER_BACKENDS")
|
|
58
|
+
dns = Insika::EnvSchema.read("INSIKA_ROUTER_BACKENDS_DNS")
|
|
59
|
+
if static.nil? == dns.nil?
|
|
60
|
+
abort "insika-router: set exactly one of INSIKA_ROUTER_BACKENDS or INSIKA_ROUTER_BACKENDS_DNS"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if static
|
|
64
|
+
Insika::Router::BackendPool.new(static: static.split(",").map(&:strip))
|
|
65
|
+
else
|
|
66
|
+
port = Insika::EnvSchema.read("INSIKA_ROUTER_BACKEND_PORT")
|
|
67
|
+
abort "insika-router: INSIKA_ROUTER_BACKEND_PORT is required with INSIKA_ROUTER_BACKENDS_DNS" if port.nil?
|
|
68
|
+
|
|
69
|
+
Insika::Router::BackendPool.new(
|
|
70
|
+
dns: dns, dns_port: port.to_i,
|
|
71
|
+
dns_interval: (Insika::EnvSchema.read("INSIKA_ROUTER_DNS_INTERVAL") || 15).to_i
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def banner(pool, host, port)
|
|
77
|
+
puts "\e[1mInsika Router\e[0m — session-sticky proxy"
|
|
78
|
+
puts " http://#{host}:#{port}/up → health (also this process's own liveness)"
|
|
79
|
+
mode = pool.dns? ? "dns" : "static"
|
|
80
|
+
puts " backends (#{mode}): #{pool.backends.join(', ')}"
|
|
81
|
+
puts " Ctrl-C to stop."
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Insika::CLI::Router.main
|
data/docs/AGENTS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Agents
|
|
3
|
-
parent:
|
|
3
|
+
parent: Core concepts
|
|
4
4
|
nav_order: 1
|
|
5
5
|
permalink: /agents/
|
|
6
6
|
---
|
|
@@ -41,7 +41,7 @@ ergonomics, not in what they produce.
|
|
|
41
41
|
re-import that drops a tool or skill also removes it. `DELETE /v1/agents/:id`
|
|
42
42
|
removes an agent.
|
|
43
43
|
- **Studio** — create and edit an agent by hand in the control UI (Config /
|
|
44
|
-
Prompts / Skills / Memory / History tabs), backed by the same commands.
|
|
44
|
+
Prompts / Skills / Memory / Outcomes / Cache / History / **loops** tabs), backed by the same commands.
|
|
45
45
|
|
|
46
46
|
Creating an agent validates its id (required, must be unique) and its subagent
|
|
47
47
|
graph (cycle/depth — see [subagents](#delegation-subagents)) **before**
|
|
@@ -70,6 +70,23 @@ injected text as the customer repeating themselves. See
|
|
|
70
70
|
## The AgentProfile
|
|
71
71
|
|
|
72
72
|
A profile is built through one front door — `AgentProfile.build(id:, model: nil, …)`.
|
|
73
|
+
`model`/`provider` are a straight pass-through to [RubyLLM](https://rubyllm.com) — Insika
|
|
74
|
+
keeps no allowlist of "supported" models. Whatever RubyLLM's installed version
|
|
75
|
+
reaches, an agent can name: today that's 13 provider adapters (Anthropic, OpenAI,
|
|
76
|
+
Gemini, DeepSeek, Bedrock, Vertex AI, Azure, Mistral, xAI, OpenRouter,
|
|
77
|
+
Perplexity, Ollama, GPUStack) and every model each one exposes — see
|
|
78
|
+
[rubyllm.com/available-models](https://rubyllm.com/available-models/) for the
|
|
79
|
+
current, live catalog. Upgrading the `ruby_llm` gem is the only thing that ever
|
|
80
|
+
widens this list; no Insika code changes with it.
|
|
81
|
+
|
|
82
|
+
Wire a provider's credentials once in the Studio's **Settings → LLM providers**
|
|
83
|
+
(or `POST /v1/settings`) — `api` is any slug RubyLLM recognizes, and
|
|
84
|
+
`LLMConfigurator` applies it by reflection (`<api>_api_key=`, `<api>_api_base=`),
|
|
85
|
+
so a provider RubyLLM doesn't expose an accessor for is skipped, never a hard
|
|
86
|
+
error. To *restrict* which models an agent may use — the opposite direction —
|
|
87
|
+
declare `model_policy: { allow: [refs] }` on its profile (exact `"provider/model"`
|
|
88
|
+
refs or a `"provider/*"` wildcard); absent means no fence, every configured
|
|
89
|
+
model is fair game.
|
|
73
90
|
A prompt file is **text**. Passing a structured value where the markdown belongs —
|
|
74
91
|
a `{"content": …}` wrapper in a pack, or a store entry read and written back — is
|
|
75
92
|
rejected, not coerced: `to_s` on a Hash produces Ruby's `#inspect`, and a prompt made
|
|
@@ -97,6 +114,31 @@ the next tool-batch boundary: "you already ran this, answer with what you
|
|
|
97
114
|
have"). A repeat after the warning aborts the turn like `max_tool_calls` does.
|
|
98
115
|
Set it below 2 to switch it off.
|
|
99
116
|
|
|
117
|
+
### `tool_persistence` — don't give up on the first empty result
|
|
118
|
+
|
|
119
|
+
The loop guard's mirror image. `max_tool_repeat` stops the model from repeating
|
|
120
|
+
the *same* call; `tool_persistence` stops it from giving up after *one* weak
|
|
121
|
+
call. When ON (the default), the engine appends a short **"Tool discipline"**
|
|
122
|
+
block after the agent's identity in the system prompt: a weak or empty tool
|
|
123
|
+
result means *try again with a different approach — a rephrased query, a
|
|
124
|
+
synonym, a broader term — before telling the user you found nothing* (and don't
|
|
125
|
+
narrate the retries); a tool error means *read it and fix the arguments*, never
|
|
126
|
+
repeat the exact same call. Without it, a search that returns 0 results reads as
|
|
127
|
+
final and the model answers "I couldn't find it" when a synonym one call away
|
|
128
|
+
would have.
|
|
129
|
+
|
|
130
|
+
This is the **one default-ON profile flag** — every field above is opt-in, this
|
|
131
|
+
one is opt-out, because the behavior is the proven default and the exception is
|
|
132
|
+
the thing worth declaring:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
tool_persistence false # remove the block for this agent
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The block is a byte-stable constant, so `prompt_caching` stays effective: the
|
|
139
|
+
deploy that introduces it costs one cache write per agent, and every turn after
|
|
140
|
+
that hits as before.
|
|
141
|
+
|
|
100
142
|
### Why some limits are missing from that list
|
|
101
143
|
|
|
102
144
|
`chat_rate_limit`, `agent_token_ceiling`, `queue_mode`, `debounce_ms`,
|
|
@@ -219,6 +261,12 @@ separately lives in one event, emitted when the window closes:
|
|
|
219
261
|
Times and counts, never content. That is what answers "the customer says they
|
|
220
262
|
sent the order number" without keeping a throwaway task per fragment.
|
|
221
263
|
|
|
264
|
+
> **`steer` also collects at the door.** The two windows are the same policy's
|
|
265
|
+
> halves, not two modes: a `steer` agent that set a `debounce_ms` also merges the
|
|
266
|
+
> fragments that land before the turn starts. The window value is what
|
|
267
|
+
> an operator replaces the legacy pre-batch buffer with — `steer` catches anything
|
|
268
|
+
> that arrives after the turn started, the door window the fragments before it.
|
|
269
|
+
|
|
222
270
|
#### `steer` — the message arrives while the turn is already running
|
|
223
271
|
|
|
224
272
|
`collect` only ever touches a turn that has **not started**. Once the agent is
|
|
@@ -339,7 +387,9 @@ Three capabilities invert the default — `nil`/absent means **OFF**, not "all":
|
|
|
339
387
|
setting, never "everything on"). `tool_output_compression` is a fourth: opt-in
|
|
340
388
|
mechanical dedupe of repeated tool results in the history (see
|
|
341
389
|
[Context](CONTEXT.md#compaction-is-not-wired--except-the-mechanical-dedupe)),
|
|
342
|
-
off by default because it changes what the model sees.
|
|
390
|
+
off by default because it changes what the model sees. And one flag inverts the
|
|
391
|
+
other way: `tool_persistence` is **ON unless you set it to `false`** (see
|
|
392
|
+
[`tool_persistence`](#tool_persistence--dont-give-up-on-the-first-empty-result)).
|
|
343
393
|
|
|
344
394
|
### Declaring what this deployment has
|
|
345
395
|
|
|
@@ -351,167 +401,9 @@ has promotions" from data is how a test suite starts lying.
|
|
|
351
401
|
|
|
352
402
|
## The five access layers
|
|
353
403
|
|
|
354
|
-
What an agent may do is layered
|
|
355
|
-
matters, and editable hot.
|
|
356
|
-
|
|
357
|
-
### Layer 1: Tools (what it can call)
|
|
358
|
-
|
|
359
|
-
`tools_allow` / `tools_deny` / `tools_allow_groups` decide which tools enter the
|
|
360
|
-
turn's tool-loop, enforced by the tool-allowlist policy. See [Tools](TOOLS.md)
|
|
361
|
-
for how tools are defined and registered, and [`examples/data-tool/`](https://github.com/guizaols/insika/tree/main/examples/data-tool/).
|
|
362
|
-
|
|
363
|
-
### Layer 2: Policies and approvals
|
|
364
|
-
|
|
365
|
-
Policies are named entries evaluated before the turn runs. Builtins cover
|
|
366
|
-
tool-, skill-, and workflow-allowlisting, plus **`ApprovalRequired`** — which
|
|
367
|
-
does not allow or deny but *tags* a tool as needing human approval. Set
|
|
368
|
-
`approvals_required: [tool names]`; the gate then fires when the model tries to
|
|
369
|
-
call that tool, suspending the turn until an operator approves it in the Studio.
|
|
370
|
-
See [Security](SECURITY.md#human-approval).
|
|
371
|
-
|
|
372
|
-
### Layer 3: Guardrails (content safety)
|
|
373
|
-
|
|
374
|
-
`guardrails` configures input/output content safety per agent — **opt-in**, so an
|
|
375
|
-
agent that says nothing gets a conservative default (deterministic detectors on,
|
|
376
|
-
LLM moderator off). See [Security](SECURITY.md#guardrails) and
|
|
377
|
-
[`examples/guardrails/`](https://github.com/guizaols/insika/tree/main/examples/guardrails/).
|
|
378
|
-
|
|
379
|
-
### Layer 4: Edge limits (flood and spend control)
|
|
380
|
-
|
|
381
|
-
Two independent, opt-in ceilings, enforced *before* the model is ever called —
|
|
382
|
-
opt-in everywhere except on a public channel, where `chat_rate_limit` is
|
|
383
|
-
[required](CHANNELS.md#a-rate-limit-is-required-not-suggested) and the
|
|
384
|
-
[web widget](CHANNELS.md#the-web-widget) refuses to serve without one:
|
|
385
|
-
|
|
386
|
-
- **`chat_rate_limit`** — turn attempts per session per `chat_rate_window`.
|
|
387
|
-
- **`agent_token_ceiling`** — total tokens per agent per `agent_token_window`.
|
|
388
|
-
|
|
389
|
-
On breach the turn halts gracefully with a configurable `limit_response` and
|
|
390
|
-
**zero LLM calls**. Windows are set at the platform level; the ceilings can be set
|
|
391
|
-
per agent (blank inherits the platform value, `0` explicitly disables it).
|
|
392
|
-
|
|
393
|
-
> ⚠️ The token window default is **86400 (daily)**. To express "500k tokens per
|
|
394
|
-
> **hour**", set `agent_token_window = 3600` explicitly. A per-agent key that is
|
|
395
|
-
> *present but nil* reads as OFF for that agent — leave the key **absent** to
|
|
396
|
-
> inherit. See [Security](SECURITY.md#edge-limits).
|
|
397
|
-
|
|
398
|
-
#### Calendar budgets — the daily/monthly cost wall
|
|
399
|
-
|
|
400
|
-
A third, opt-in ceiling for the *billing* shape the windows above cannot express:
|
|
401
|
-
a spend cap over a CALENDAR day or month, per `(tenant, agent)` when
|
|
402
|
-
multi-tenant. Data on the profile (DSL `budget` or the pack's `budget` key):
|
|
403
|
-
|
|
404
|
-
```ruby
|
|
405
|
-
budget daily: 100_000, monthly: 2_000_000, soft: false # or soft: true
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
- Tokens count the **billed** spend — `input + output + cached + cache_creation`
|
|
409
|
-
(the cached prefix is the bulk of the bill, not an afterthought).
|
|
410
|
-
- **Hard** (the default, `soft` absent/false): a turn that arrives with the
|
|
411
|
-
window's spend already at/over the cap **fails** with the typed
|
|
412
|
-
`Insika::BudgetExceeded` — the envelope reads `budget_exceeded` +
|
|
413
|
-
`retry_after` (seconds until the window rolls). It is NOT a customer reply; it
|
|
414
|
-
is an operator signal.
|
|
415
|
-
- **Soft** (`soft: true`): the same turn RUNS — crossing the cap emits one
|
|
416
|
-
`budget_warning` event per window and injects a note into the context (the
|
|
417
|
-
model sees it, the transcript does not).
|
|
418
|
-
- Either way, crossing `alert_at` (default `0.8` of the cap) fires the same
|
|
419
|
-
warning **before** the wall, once per window.
|
|
420
|
-
|
|
421
|
-
> ⚠️ Unlike the ceilings above, the cap that counts is per **calendar** window —
|
|
422
|
-
> a `daily` budget rolls at UTC midnight, a `monthly` one on the 1st, whatever
|
|
423
|
-
> the sun. `agent_token_window` is a fixed seconds window and cannot express
|
|
424
|
-
> "the day resets at midnight".
|
|
425
|
-
|
|
426
|
-
#### Reliability — retries, fallback, circuit breaker (WS3)
|
|
427
|
-
|
|
428
|
-
The provider interaction is a single attempt by default (RubyLLM's own 2
|
|
429
|
-
transport retries aside). For a store that cannot have a dead model take the
|
|
430
|
-
chat down, the reliability policy is DATA on the profile:
|
|
431
|
-
|
|
432
|
-
```ruby
|
|
433
|
-
reliability retries: 2, backoff: "exponential",
|
|
434
|
-
fallback: ["openai/gpt-4o-mini"],
|
|
435
|
-
circuit_breaker: { after: 10, within: 60, cooldown: 300 }
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
- **Retries** — transient failures (`:retryable` / `:rate_limited_*` per the
|
|
439
|
-
error classification) retry with exponential backoff, up to `retries`.
|
|
440
|
-
A `:fatal` (auth, billing, bad request) is NEVER retried or rotated. Each
|
|
441
|
-
attempt runs on a fresh chat — the customer-visible answer comes only from
|
|
442
|
-
the attempt that returns.
|
|
443
|
-
- **Fallback** — after a node's retries, the turn ROTATES to the next model in
|
|
444
|
-
the chain: the profile's `fallback` refs first, then the platform
|
|
445
|
-
`fallback_models`. The turn's usage is attributed to the model that actually
|
|
446
|
-
spoke (`model_source: "fallback"`).
|
|
447
|
-
- **Circuit breaker** — per `(tenant, provider/model)`: `after` failures within
|
|
448
|
-
`within` seconds open the circuit; while open, the turn fail-fasts with the
|
|
449
|
-
typed `circuit_open` + `retry_after` (remaining cooldown) and the provider is
|
|
450
|
-
never touched. After `cooldown` a half-open trial closes the circuit on
|
|
451
|
-
success or reopens it on failure.
|
|
452
|
-
- **`timeout`** — per-attempt ceiling (default 30s), counted as a retryable
|
|
453
|
-
failure.
|
|
454
|
-
|
|
455
|
-
Absent `reliability` = the plain single attempt, byte-for-byte today's
|
|
456
|
-
behavior.
|
|
457
|
-
|
|
458
|
-
#### Operator alerts — the webhook (WS6)
|
|
459
|
-
|
|
460
|
-
Three operational events — `budget_warning`, `breaker_open`, `delivery_failed` —
|
|
461
|
-
can be answered per agent with a webhook:
|
|
462
|
-
|
|
463
|
-
```ruby
|
|
464
|
-
alerts webhook: "https://ops.example.com/insika-alerts"
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
When present, each such event is POSTed to the URL as JSON (the event's
|
|
468
|
-
type/data/meta, plus the agent). Delivery rides the same outbox + claim +
|
|
469
|
-
bounded-retry pipeline as channel answers — at-most-once, crashed deliveries
|
|
470
|
-
recovered at boot. The engine transports the event and does not interpret it: a
|
|
471
|
-
Slack/CRM adapter is the consumer's. Absent `alerts` = nothing is sent.
|
|
472
|
-
|
|
473
|
-
Separately, with `INSIKA_TURN_TIMING`, the provider's **live TTFB** is carried in
|
|
474
|
-
the streaming envelope: the first content chunk emits an `insika.ttft` frame
|
|
475
|
-
(`ttft_ms`) on `/v1/responses`, alongside the per-turn `timing` breakdown on the
|
|
476
|
-
final `response.completed`. Additive and opt-in — absent by default.
|
|
477
|
-
|
|
478
|
-
### Layer 5: Reasoning (thinking)
|
|
479
|
-
|
|
480
|
-
Controls the model's thinking budget, resolved by precedence
|
|
481
|
-
**Chat > Agent > Model > Global** (first non-blank wins):
|
|
482
|
-
|
|
483
|
-
| Scope | Where |
|
|
484
|
-
|-------|-------|
|
|
485
|
-
| Chat | session var `__llm__.thinking` |
|
|
486
|
-
| Agent | `profile.params["thinking"]` |
|
|
487
|
-
| Model | platform `model_params[<ref>].thinking` |
|
|
488
|
-
| Global | platform `thinking` |
|
|
404
|
+
What an agent may do is layered: which tools it may call, which calls need a human, content safety, edge limits on flood and spend, and how much it may think. See [Limits and policy](POLICY.md).
|
|
489
405
|
|
|
490
|
-
|
|
491
|
-
levels map to the provider's thinking-effort parameter. This is a control
|
|
492
|
-
primitive, not a latency lever — turning reasoning off does not necessarily speed
|
|
493
|
-
up a turn, because most of a turn's latency is the provider itself, not thinking.
|
|
494
|
-
|
|
495
|
-
Whether the reasoning ever reaches the **customer** is a separate switch, off by
|
|
496
|
-
default:
|
|
497
|
-
|
|
498
|
-
```ruby
|
|
499
|
-
edge_stream thinking: true, intermediate: false
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
`thinking` is the provider's reasoning; `intermediate` is the model narrating its
|
|
503
|
-
own tool loop ("let me look that up"). Both are always on the event stream for the
|
|
504
|
-
Studio and the trace — this decides only whether `/v1/responses` translates them,
|
|
505
|
-
and each opted-in channel gets its own frame type, never the answer's. See
|
|
506
|
-
[Architecture](ARCHITECTURE.md#what-crosses-the-edge).
|
|
507
|
-
|
|
508
|
-
> ⚠️ Turn it on knowing your consumer. One that concatenates every text delta into
|
|
509
|
-
> a single message — a WhatsApp adapter — will only be affected once it learns to
|
|
510
|
-
> read the new frames, and when it does, the deliberation is what the customer
|
|
511
|
-
> reads. That is the operator's call, which is why it is neither a default nor a
|
|
512
|
-
> global.
|
|
513
|
-
|
|
514
|
-
## The stuck signal — "I cannot proceed" (WS5)
|
|
406
|
+
## The stuck signal — "I cannot proceed"
|
|
515
407
|
|
|
516
408
|
The engine doesn't decide what "I can't help you" means — the consumer does. What
|
|
517
409
|
the engine provides is the deterministic signal, so that a product wanting
|
|
@@ -562,6 +454,81 @@ allowlist, one or more `proposers`, a token `budget`, and a gate that replays th
|
|
|
562
454
|
golden set before anything reaches a human. All of it is in
|
|
563
455
|
[Refinement](REFINEMENT.md); none of it is on until you name it.
|
|
564
456
|
|
|
457
|
+
## Distillation of customer facts
|
|
458
|
+
|
|
459
|
+
`distill` configures how finished, idle customer conversations are read back as
|
|
460
|
+
proposed facts — the human-gated loop documented in [Facts](FACTS.md). Pack
|
|
461
|
+
data, `refinement:`'s shape, and absent = off for that agent:
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
distill enabled: true,
|
|
465
|
+
idle_hours: 6, # how idle a session must be before it distills
|
|
466
|
+
min_messages: 3, # a shorter session distills noise
|
|
467
|
+
max_proposals: 10 # cap per session pass
|
|
468
|
+
# prompt: "<what counts as a fact for THIS store>" (the forge's half)
|
|
469
|
+
# model: "<ref — absent = the platform utility_model>"
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Nothing is ever applied automatically: the engine writes **proposals**, the
|
|
473
|
+
operator approves/rejects/dismisses them on the Studio **Facts** page (the
|
|
474
|
+
latch: a dismissed or rejected tuple is never proposed again), and an approval
|
|
475
|
+
writes the fact to the customer's memory cell stamped
|
|
476
|
+
`distilled:<session_ref>` through an optimistic CAS — an approval never
|
|
477
|
+
silently overwrites an operator edit. Sessions are the only candidates, and
|
|
478
|
+
the distiller rides the platform `utility_model`, never a new model slot.
|
|
479
|
+
|
|
480
|
+
## Harvest of skills from real traffic
|
|
481
|
+
|
|
482
|
+
`harvest` configures how finished, idle conversations are read back as
|
|
483
|
+
proposed **SKILLS** for the agent's playbook — the human-gated loop documented
|
|
484
|
+
in [Harvest](HARVEST.md). Pack data, `distill:`'s shape, and absent = off for
|
|
485
|
+
that agent:
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
harvest enabled: true,
|
|
489
|
+
negative_list: [ { rule: "no-competitor-prices", pattern: "concorrente" } ],
|
|
490
|
+
miner: { model: "deepseek-v4-flash", # absent = the platform utility_model
|
|
491
|
+
window: { last_sessions: 200 } },
|
|
492
|
+
idle_hours: 24,
|
|
493
|
+
min_messages: 3
|
|
494
|
+
# prompt: "<what a harvestable skill is for THIS store>" (the forge's half)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
The loop reads only finished traffic (the fork is structural — the mining
|
|
498
|
+
writes nothing to the sessions it read), filters every proposal through the
|
|
499
|
+
negative list and the evidence ledger (product claims must reference IDs the
|
|
500
|
+
origin sessions actually saw — an agent without `grounding.matcher.sku` does
|
|
501
|
+
not mine at all), scores survivors with a double gate (the eval replay against
|
|
502
|
+
the clone's golden set, judges mandatory in three shapes; the
|
|
503
|
+
conversion "not worse" check against the frozen funnel baseline), and lands
|
|
504
|
+
a skill **only after a human approves** — snapshot-first, append-only
|
|
505
|
+
promotion log, deterministic rollback. Nothing is ever applied automatically.
|
|
506
|
+
|
|
507
|
+
## Knowledge from finished conversations
|
|
508
|
+
|
|
509
|
+
`knowledge` configures the post-turn learning loop documented in
|
|
510
|
+
[Knowledge](KNOWLEDGE.md): after a turn completes, the engine extracts durable
|
|
511
|
+
**concepts** — facts, procedures, policies, objections — and persists them for
|
|
512
|
+
the agent, separate from any one customer's memory. Pack data, `distill:`'s
|
|
513
|
+
shape, and absent = off for that agent:
|
|
514
|
+
|
|
515
|
+
```ruby
|
|
516
|
+
knowledge extract: true,
|
|
517
|
+
types: %w[fact policy objection] # what the extractor may emit
|
|
518
|
+
# prompt: "<what counts as a concept for THIS store>" (the forge's half)
|
|
519
|
+
# model: "<ref — absent = the platform utility_model>"
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
The model proposes `name`/`description`/`type`/`body`; the engine stamps
|
|
523
|
+
`provenance`/`confidence`/`sources`/timestamps itself (a model-authored one
|
|
524
|
+
of those is dropped, never trusted) and redacts the body for PII. Writing a
|
|
525
|
+
concept name that already exists never blindly overwrites: the engine
|
|
526
|
+
decides same claim (bump the evidence), related claim (merge, one extra
|
|
527
|
+
model call), or contradicting claim (never merged — appended under a
|
|
528
|
+
heading, confidence drops, a human resolves it in the Studio's Knowledge
|
|
529
|
+
page). Nothing is injected into a turn's prompt yet — see
|
|
530
|
+
[Knowledge](KNOWLEDGE.md#whats-not-here-yet) for what's not here.
|
|
531
|
+
|
|
565
532
|
## Delegation (subagents)
|
|
566
533
|
|
|
567
534
|
An agent can delegate to **subagents**: named child agents it may invoke as a
|
|
@@ -608,9 +575,108 @@ next turn with no restart. See [Deploy](DEPLOY.md) for the durable-volume setup
|
|
|
608
575
|
[Context](CONTEXT.md#the-volume) for why editing a committed file does *not* change
|
|
609
576
|
a running agent.
|
|
610
577
|
|
|
578
|
+
## Media
|
|
579
|
+
|
|
580
|
+
Photos, voice notes and documents travel through the message contract as additive content parts, and the engine can hand generated images back the same way. See [Media](MEDIA.md).
|
|
581
|
+
|
|
582
|
+
## Customer-scoped memory and the right to be forgotten
|
|
583
|
+
|
|
584
|
+
Memory is naturally **per customer, not per tenant**. A message that carries a
|
|
585
|
+
`customer` key moves the engine's memory scope to that person:
|
|
586
|
+
|
|
587
|
+
```bash
|
|
588
|
+
curl -X POST /v1/messages?stream=false -H "Authorization: Bearer $TOKEN" \
|
|
589
|
+
-d '{ "agent": "store-support", "session_id": "chat-7",
|
|
590
|
+
"customer": "c-123", "message": "cadê meu pedido" }'
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
- **Scope** — with `customer` present, the `remember` tool and the `<memory>`
|
|
594
|
+
block read/write the `[tenant:]customer` cell: two customers under the same
|
|
595
|
+
tenant never see each other, and the `<request_context>` tenant label (the
|
|
596
|
+
merchant) is untouched. Absent `customer` = today's per-tenant/per-chat
|
|
597
|
+
behavior.
|
|
598
|
+
- **Right to be forgotten** — `POST /v1/commands/forget_customer` (operator)
|
|
599
|
+
purges the customer's memory cell, their sessions and everything those
|
|
600
|
+
sessions left behind — per-session traces, the tasks (the message text lives
|
|
601
|
+
in the persisted command), their checkpoints (the transcript) and the outbox
|
|
602
|
+
deliveries (the answer as it was handed to the channel) — and nothing else's:
|
|
603
|
+
`{ "customer": "c-123", "tenant": "acme" }`. **Name the tenant**: the
|
|
604
|
+
operator credential carries none, and without one the purge means the whole
|
|
605
|
+
deployment (the untagged memory cell, plus that customer's sessions in every
|
|
606
|
+
tenant) — right for a single-tenant deployment, never what a multi-tenant
|
|
607
|
+
operator means. Facts also support an optimistic CAS write
|
|
608
|
+
(`replace_if_revision`) for an integration that must not clobber a concurrent
|
|
609
|
+
edit.
|
|
610
|
+
- **Tenant deletion** — `POST /v1/commands/delete_tenant_data` (operator)
|
|
611
|
+
purges EVERYTHING the engine holds about one tenant: its sessions and their
|
|
612
|
+
whole footprint (traces, tasks, checkpoints, outbox deliveries), every memory
|
|
613
|
+
cell under the tenant (its own + the customer cells — enumerated from the
|
|
614
|
+
store, so even a cell whose session was already deleted goes), its outcome
|
|
615
|
+
records and its artifacts (a report is content, never kept behind an
|
|
616
|
+
offboarded tenant): `{ "tenant": "acme" }`. Its API tokens are **revoked
|
|
617
|
+
first** (before the sweep): an offboarded tenant whose credentials still
|
|
618
|
+
resolved kept authenticating and could open a new session over the erasure.
|
|
619
|
+
The tenant string is the isolation boundary; a neighbour is untouched.
|
|
620
|
+
- **Retention** — the age-based counterpart, as data: the settings key
|
|
621
|
+
`retention_days` (Integer days; absent/0 = OFF, the engine never sweeps by
|
|
622
|
+
default). The tick's daily sweep (at most once per 24 h, behind the same
|
|
623
|
+
single-key claim the stale-task sweep uses) purges sessions (+traces),
|
|
624
|
+
terminal tasks (+checkpoints), delivered/failed outbox records, memory
|
|
625
|
+
facts/notes and outcomes older than the window. A non-terminal task is never
|
|
626
|
+
touched — the Recovery sweep owns those lives — and neither is a delivery
|
|
627
|
+
still owed to somebody. One thing the same daily pass sweeps **regardless of
|
|
628
|
+
`retention_days`**: the budget counter cells whose window already rolled over
|
|
629
|
+
(and their once-per-window alert markers). Those are engine bookkeeping, not
|
|
630
|
+
customer content, and nothing else ever collected them. Artifacts (reports)
|
|
631
|
+
likewise expire on their **own** knob, `artifact_ttl_days` (settings; absent
|
|
632
|
+
= OFF) — the guarantee that PII inside a report dies even in a deployment
|
|
633
|
+
that keeps its conversations forever. See [Artifacts](ARTIFACTS.md).
|
|
634
|
+
|
|
635
|
+
## Outcomes and follow-ups
|
|
636
|
+
|
|
637
|
+
What the agent's traffic was worth in business terms, and the tool that lets it come back later on a promise it made. See [Outcomes and follow-ups](OUTCOMES.md).
|
|
638
|
+
|
|
639
|
+
## Schedules — recurring turns the engine fires
|
|
640
|
+
|
|
641
|
+
The operator's counterpart to follow-ups: a turn **nobody sends** — the daily
|
|
642
|
+
report at 22:00, the eval sweep every night. Declared per agent as pack data
|
|
643
|
+
(`schedule "daily_report", cron: …, tz: …, message: …` or `every: N`), edited
|
|
644
|
+
hot in the Studio's Schedules config group, fired by the engine's own tick
|
|
645
|
+
one turn per claim window, never queued, no catch-up after a downtime (each
|
|
646
|
+
missed window is a recorded skip, visible in the Studio):
|
|
647
|
+
|
|
648
|
+
```ruby
|
|
649
|
+
agent = Insika.agent("reporter") do
|
|
650
|
+
schedule "daily_report", cron: "0 22 * * *", tz: "America/Sao_Paulo",
|
|
651
|
+
message: "Run the daily report now.",
|
|
652
|
+
overrides: { turn_timeout: 900, max_tool_calls: 200 }
|
|
653
|
+
end
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
The run is a first-class turn stamped `origin: "scheduled"`; `session_mode:
|
|
657
|
+
"new"` gives it a fresh session per run (the report shape), `"fixed"` a
|
|
658
|
+
standing one; per-run `overrides` raise the chat-time ceilings a report needs.
|
|
659
|
+
A hard calendar budget at its cap skips instead of burning the store's tokens.
|
|
660
|
+
Distinct by shape and by law from the follow-up tool. See
|
|
661
|
+
[Schedules](SCHEDULING.md).
|
|
662
|
+
|
|
663
|
+
## Artifacts — a report the agent can hand you a URL to
|
|
664
|
+
|
|
665
|
+
A scheduled report's natural output is not a message but a page — tables,
|
|
666
|
+
sections, inline charts. The `save_artifact` tool (a registry tool, in the
|
|
667
|
+
agent's `tools_allow`) gives the agent a durable destination: it hands in
|
|
668
|
+
title + content and gets a URL back, which it can include in a channel message
|
|
669
|
+
("today's report: <url>"). The report is stored (one per run, the listing is
|
|
670
|
+
the history), served under `/studio/artifacts/…` inside a sandboxed iframe,
|
|
671
|
+
optionally shared outside the Studio via an expiring signed link
|
|
672
|
+
(`INSIKA_ARTIFACT_SIGNING_KEY`). The tenant binding is inherited from the
|
|
673
|
+
saving agent — never a parameter the model types. Artifact content is LLM
|
|
674
|
+
output and is served as untrusted. See [Artifacts](ARTIFACTS.md).
|
|
675
|
+
|
|
611
676
|
## See also
|
|
612
677
|
|
|
613
678
|
- [Tools](TOOLS.md) — define, register, and troubleshoot tools.
|
|
679
|
+
- [Artifacts](ARTIFACTS.md) — the report destination: the tool, the routes, the signed link.
|
|
614
680
|
- [Skills](SKILLS.md) — progressive playbooks an agent loads on demand.
|
|
615
681
|
- [Context](CONTEXT.md) — what fills a turn's prompt, and memory.
|
|
616
682
|
- [Security](SECURITY.md) — guardrails, sandbox, approvals, edge limits.
|
data/docs/API.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: The /v1 API
|
|
3
|
+
parent: Integrate
|
|
4
|
+
nav_order: 1
|
|
5
|
+
permalink: /api/
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The /v1 API — the frozen compatibility contract
|
|
9
|
+
|
|
10
|
+
The HTTP surface is the drop-in OpenAI-Responses-compatible contract: a client
|
|
11
|
+
that speaks `/v1` today speaks it tomorrow. This page is the WRITTEN promise
|
|
12
|
+
— the mechanical half is the version gate in the server, and the
|
|
13
|
+
two cannot drift (a spec pins the gate's version to the date below).
|
|
14
|
+
|
|
15
|
+
## The surface
|
|
16
|
+
|
|
17
|
+
| Route | What it is |
|
|
18
|
+
|-------|------------|
|
|
19
|
+
| `POST /v1/responses` | the OpenAI-Responses-compatible turn ingress (streaming SSE) |
|
|
20
|
+
| `POST /v1/messages` | the channel-style message ingress (one turn per message, ack-fast) |
|
|
21
|
+
| `POST /v1/agents` | provisioning by definition/pack (idempotent, authoritative) |
|
|
22
|
+
| `GET/DELETE /v1/agents` | list / remove agents |
|
|
23
|
+
| `POST /v1/sessions` | create a session explicitly |
|
|
24
|
+
| `POST /v1/outcomes` | record a business outcome (`conversion`, `escalation`, any label) |
|
|
25
|
+
| `GET /v1/outcomes` | last outcome per agent + the per-day series |
|
|
26
|
+
| `POST /v1/commands/...` | operator commands (`forget_customer`, `delete_tenant_data`, the follow-up mutations) |
|
|
27
|
+
| `GET /v1/vitals` | process health/uptime vitals |
|
|
28
|
+
| `GET /v1/...` | the onboarding surface (`start.md`, `models.json`, `/docs/<name>.md`) |
|
|
29
|
+
|
|
30
|
+
The turn endpoints speak the OpenAI `response.completed` wire format; the
|
|
31
|
+
`Insika-Version` header declares the compatibility vintage of a request.
|
|
32
|
+
|
|
33
|
+
## The compatibility rule
|
|
34
|
+
|
|
35
|
+
**Request and response shapes are stable; changes are additive-only.** A new
|
|
36
|
+
field on a request or response is a release. Removing, renaming or re-typing a
|
|
37
|
+
field is a BREAKING change and requires, together, all three:
|
|
38
|
+
|
|
39
|
+
1. a new `Insika-Version` entry in the server's known-versions gate (an
|
|
40
|
+
unknown value is refused with 400 before auth — fail-closed);
|
|
41
|
+
2. a route branch that serves both vintages (a compatibility branch), so
|
|
42
|
+
an old client keeps working while the new shape rolls;
|
|
43
|
+
3. a new `**Frozen as of:**` line below.
|
|
44
|
+
|
|
45
|
+
Additive changes need none of the three.
|
|
46
|
+
|
|
47
|
+
**Frozen as of: 2026-08-08** — the date that is the 1.0 promise. The gate's
|
|
48
|
+
first known version, pinned by `spec/insika/server/version_gate_spec.rb`; the
|
|
49
|
+
release writes the 1.0 freeze date here at release time.
|
|
50
|
+
|
|
51
|
+
## The 400-on-unknown rule
|
|
52
|
+
|
|
53
|
+
A request carrying an `Insika-Version` the gate does not know is refused with
|
|
54
|
+
`400 Bad Request` BEFORE authentication — a client that speaks a future or
|
|
55
|
+
misspelled vintage learns immediately, and an unauthenticated probe cannot
|
|
56
|
+
distinguish the gate from auth (fail-closed, constant-time).
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Architecture
|
|
3
|
-
parent:
|
|
4
|
-
nav_order:
|
|
3
|
+
parent: Start here
|
|
4
|
+
nav_order: 3
|
|
5
5
|
permalink: /architecture/
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -319,6 +319,7 @@ validator as the after-task hook, so both roots enforce content safety identical
|
|
|
319
319
|
| Tools (data/manifest/MCP) | `lib/insika/tool_definition.rb`, `tool_manifest.rb`, `mcp_tool_ingestor.rb` |
|
|
320
320
|
| Plugin loading (boot) | `lib/insika/plugin.rb`, `lib/insika/plugin/loader.rb` |
|
|
321
321
|
| Refinement (traffic → report) | `lib/insika/refinement/*`, `lib/insika/refinement_store.rb` |
|
|
322
|
+
| Post-turn learning (facts, skills, knowledge — extracted from finished conversations) | `lib/insika/distill.rb`, `lib/insika/harvest.rb`, `lib/insika/knowledge.rb`, `lib/insika/knowledge_store.rb`; the per-turn hook lives in `Executor#persist_turn`, next to `finalize_delegation` |
|
|
322
323
|
| Evals (cases, judges, gate) | `lib/insika/evals/*`, `lib/insika/golden_store.rb`; `evals/run.rb` is the CLI |
|
|
323
324
|
| HTTP/SSE surface | `lib/insika/server/*` |
|
|
324
325
|
|