insika 0.9.0 → 1.0.1
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 +237 -2
- data/README.md +5 -3
- data/bin/insika +3 -0
- data/bin/insika-router +0 -1
- data/docs/AGENTS.md +90 -7
- data/docs/ARCHITECTURE.md +12 -7
- data/docs/ARTIFACTS.md +33 -3
- data/docs/BENCHMARK.md +50 -7
- data/docs/CONTEXT.md +63 -0
- data/docs/CROSS-HARNESS-BENCH.md +157 -0
- data/docs/DEPLOY.md +57 -11
- data/docs/EMBEDDING.md +10 -0
- data/docs/EVALS.md +33 -4
- data/docs/FACTS.md +8 -6
- data/docs/HARVEST.md +8 -3
- data/docs/KNOWLEDGE.md +7 -0
- data/docs/MEDIA.md +6 -2
- data/docs/OBSERVABILITY.md +143 -22
- data/docs/PERFORMANCE.md +110 -0
- data/docs/PLUGINS.md +4 -4
- data/docs/POLICY.md +22 -13
- data/docs/RELEASING.md +5 -0
- data/docs/ROUTER.md +7 -0
- data/docs/RUBYLLM_2_MIGRATION.md +307 -0
- data/docs/SECURITY.md +7 -0
- data/docs/SHARED-CONVERSATIONS.md +40 -0
- data/docs/SKILLS.md +5 -3
- data/docs/STUDIO.md +122 -0
- data/docs/TOOLS.md +111 -9
- data/docs/operate.md +3 -0
- data/lib/insika/agent_defaults.rb +140 -0
- data/lib/insika/agent_file_store.rb +12 -0
- data/lib/insika/agent_profile.rb +78 -8
- data/lib/insika/artifact_signing.rb +11 -0
- data/lib/insika/chat_builder.rb +79 -25
- data/lib/insika/checkpoint.rb +6 -1
- data/lib/insika/checkpoint_store.rb +59 -7
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/delete_kit.rb +26 -0
- data/lib/insika/commands/gate_harvest.rb +3 -0
- data/lib/insika/commands/review_guardrail_flag.rb +38 -0
- data/lib/insika/commands/run_harvest.rb +17 -1
- data/lib/insika/commands/seed_session.rb +11 -1
- data/lib/insika/commands/send_message.rb +9 -3
- data/lib/insika/commands/update_settings.rb +23 -1
- data/lib/insika/commands/write_kit.rb +27 -0
- data/lib/insika/compaction.rb +33 -7
- data/lib/insika/config_store.rb +87 -7
- data/lib/insika/context/priority.rb +3 -0
- data/lib/insika/context/provider.rb +2 -2
- data/lib/insika/context/providers/knowledge.rb +25 -2
- data/lib/insika/context/providers/memory.rb +53 -4
- data/lib/insika/context/providers/pending_confirmation.rb +52 -0
- data/lib/insika/context/providers/prompt.rb +17 -4
- data/lib/insika/context/providers/session.rb +39 -3
- data/lib/insika/context/reranker.rb +75 -0
- data/lib/insika/distill.rb +22 -13
- data/lib/insika/doctor.rb +34 -4
- data/lib/insika/dsl/runtime.rb +36 -4
- data/lib/insika/dsl/server_boot.rb +4 -12
- data/lib/insika/dsl.rb +13 -5
- data/lib/insika/edge_limiter.rb +23 -15
- data/lib/insika/egress_guard.rb +11 -3
- data/lib/insika/env_schema.rb +33 -73
- data/lib/insika/evals/assertions.rb +171 -16
- data/lib/insika/evals/golden.rb +141 -4
- data/lib/insika/evals/judge.rb +19 -10
- data/lib/insika/evals/moderator_compare.rb +47 -0
- data/lib/insika/evals/report.rb +61 -9
- data/lib/insika/evals/runner.rb +44 -4
- data/lib/insika/evals/simulator.rb +10 -1
- data/lib/insika/evals/transport.rb +200 -0
- data/lib/insika/event_stream.rb +63 -7
- data/lib/insika/evidence.rb +29 -8
- data/lib/insika/executor.rb +668 -141
- data/lib/insika/harvest.rb +26 -14
- data/lib/insika/harvest_engine.rb +1 -0
- data/lib/insika/harvest_store.rb +12 -0
- data/lib/insika/http_client.rb +87 -4
- data/lib/insika/kits.rb +54 -0
- data/lib/insika/knowledge.rb +34 -20
- data/lib/insika/llm_configurator.rb +46 -4
- data/lib/insika/llm_provider_store.rb +1 -3
- data/lib/insika/llm_trace_store.rb +54 -0
- data/lib/insika/mcp_client.rb +27 -63
- data/lib/insika/mcp_live_tool.rb +55 -9
- data/lib/insika/mcp_store.rb +32 -1
- data/lib/insika/mcp_tool_registry.rb +69 -26
- data/lib/insika/media.rb +38 -25
- data/lib/insika/memory_store.rb +4 -3
- data/lib/insika/memory_trim.rb +60 -0
- data/lib/insika/middleware.rb +12 -3
- data/lib/insika/model_metrics_store.rb +285 -0
- data/lib/insika/model_registry.rb +110 -0
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +14 -21
- data/lib/insika/model_visible.rb +17 -6
- data/lib/insika/onboarding.rb +1 -0
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +25 -2
- data/lib/insika/pack_importer.rb +54 -9
- data/lib/insika/packaging.rb +14 -5
- data/lib/insika/pending_action_store.rb +45 -18
- data/lib/insika/plugin/loader.rb +4 -2
- data/lib/insika/profile_source.rb +4 -0
- data/lib/insika/provider_error_classifier.rb +18 -3
- data/lib/insika/queue_policy.rb +7 -2
- data/lib/insika/refinement/proposer.rb +42 -17
- data/lib/insika/reliability.rb +12 -3
- data/lib/insika/router/app.rb +14 -5
- data/lib/insika/router/session_key.rb +35 -5
- data/lib/insika/safety/config.rb +20 -3
- data/lib/insika/safety/corpus.rb +10 -3
- data/lib/insika/safety/factory.rb +39 -1
- data/lib/insika/safety/input_guardrail.rb +21 -1
- data/lib/insika/safety/judge_moderator.rb +70 -0
- data/lib/insika/safety/output_validator.rb +29 -5
- data/lib/insika/safety/prompt_echo.rb +89 -0
- data/lib/insika/schema_guard.rb +53 -76
- data/lib/insika/server/app.rb +17 -8
- data/lib/insika/server/responses.rb +19 -1
- data/lib/insika/session_actor.rb +58 -28
- data/lib/insika/session_store.rb +159 -10
- data/lib/insika/settings_store.rb +47 -1
- data/lib/insika/shared_conversations.rb +305 -0
- data/lib/insika/skill_catalog.rb +28 -2
- data/lib/insika/skill_store.rb +6 -0
- data/lib/insika/soak/runner.rb +1 -1
- data/lib/insika/steer_injector.rb +7 -48
- data/lib/insika/store.rb +29 -1
- data/lib/insika/stores/json_value.rb +37 -0
- data/lib/insika/stores/memory.rb +22 -35
- data/lib/insika/stores/postgres.rb +295 -0
- data/lib/insika/stores/sqlite.rb +257 -43
- data/lib/insika/stores/turn_counter.rb +25 -0
- data/lib/insika/studio/app.rb +1453 -161
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +25 -25
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- data/lib/insika/studio/forms.rb +171 -9
- data/lib/insika/studio/nav_icons.rb +3 -0
- data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
- data/lib/insika/studio/views/_agent_picker.erb +15 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +78 -242
- data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
- data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
- data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
- data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
- data/lib/insika/studio/views/_agents_master.erb +8 -12
- data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
- data/lib/insika/studio/views/_cfg_budget.erb +20 -0
- data/lib/insika/studio/views/_cfg_distill.erb +26 -0
- data/lib/insika/studio/views/_cfg_followup.erb +30 -0
- data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
- data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
- data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
- data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
- data/lib/insika/studio/views/_chats_master.erb +66 -0
- data/lib/insika/studio/views/_customers_master.erb +37 -0
- data/lib/insika/studio/views/_empty.erb +15 -0
- data/lib/insika/studio/views/_facts_master.erb +42 -0
- data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
- data/lib/insika/studio/views/_identity.erb +16 -0
- data/lib/insika/studio/views/_kpi.erb +17 -0
- data/lib/insika/studio/views/_message.erb +40 -2
- data/lib/insika/studio/views/_model_chart.erb +82 -0
- data/lib/insika/studio/views/_refinement_master.erb +38 -0
- data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +32 -9
- data/lib/insika/studio/views/_tool_users.erb +17 -0
- data/lib/insika/studio/views/_why.erb +53 -0
- data/lib/insika/studio/views/agent_detail.erb +17 -10
- data/lib/insika/studio/views/agents.erb +28 -16
- data/lib/insika/studio/views/approvals.erb +16 -11
- data/lib/insika/studio/views/artifact.erb +20 -7
- data/lib/insika/studio/views/artifacts.erb +19 -14
- data/lib/insika/studio/views/chats.erb +22 -29
- data/lib/insika/studio/views/customer.erb +88 -43
- data/lib/insika/studio/views/customers.erb +16 -26
- data/lib/insika/studio/views/evals.erb +49 -31
- data/lib/insika/studio/views/fact_detail.erb +114 -0
- data/lib/insika/studio/views/facts.erb +15 -116
- data/lib/insika/studio/views/followups.erb +63 -52
- data/lib/insika/studio/views/funnel.erb +104 -43
- data/lib/insika/studio/views/harvest.erb +85 -59
- data/lib/insika/studio/views/home.erb +138 -23
- data/lib/insika/studio/views/inbox.erb +36 -0
- data/lib/insika/studio/views/kits.erb +176 -0
- data/lib/insika/studio/views/knowledge.erb +92 -80
- data/lib/insika/studio/views/layout.erb +42 -11
- data/lib/insika/studio/views/live.erb +45 -0
- data/lib/insika/studio/views/mcp.erb +43 -13
- data/lib/insika/studio/views/models.erb +187 -0
- data/lib/insika/studio/views/parity.erb +38 -32
- data/lib/insika/studio/views/playground.erb +13 -9
- data/lib/insika/studio/views/refinement.erb +15 -224
- data/lib/insika/studio/views/refinement_detail.erb +243 -0
- data/lib/insika/studio/views/session.erb +152 -117
- data/lib/insika/studio/views/settings.erb +86 -22
- data/lib/insika/studio/views/skills.erb +53 -19
- data/lib/insika/studio/views/system_files.erb +7 -7
- data/lib/insika/studio/views/task.erb +45 -15
- data/lib/insika/studio/views/tasks.erb +35 -18
- data/lib/insika/studio/views/tools.erb +62 -73
- data/lib/insika/task_actor.rb +4 -1
- data/lib/insika/task_store.rb +67 -1
- data/lib/insika/telemetry/pricing.rb +16 -14
- data/lib/insika/telemetry/recorder.rb +103 -9
- data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
- data/lib/insika/telemetry.rb +12 -2
- data/lib/insika/templates/daily-digest/README.md +13 -0
- data/lib/insika/templates/daily-digest/agent.rb +22 -11
- data/lib/insika/testing/store_contract.rb +65 -0
- data/lib/insika/text_tool_call.rb +66 -0
- data/lib/insika/tick.rb +7 -1
- data/lib/insika/token_store.rb +5 -0
- data/lib/insika/tool_batch.rb +1 -1
- data/lib/insika/tool_catalog.rb +12 -2
- data/lib/insika/tool_definition.rb +10 -4
- data/lib/insika/tool_envelope.rb +165 -28
- data/lib/insika/tool_manifest.rb +1 -0
- data/lib/insika/tool_store.rb +3 -0
- data/lib/insika/tool_trace_store.rb +15 -8
- data/lib/insika/tool_usage_report.rb +2 -2
- data/lib/insika/tools/a2a_remote.rb +1 -1
- data/lib/insika/tools/agent_enum.rb +1 -1
- data/lib/insika/tools/confirm_pending.rb +112 -0
- data/lib/insika/tools/data_defined_tool.rb +23 -15
- data/lib/insika/tools/generate_image.rb +2 -2
- data/lib/insika/tools/load_knowledge.rb +7 -3
- data/lib/insika/tools/load_skill.rb +3 -3
- data/lib/insika/tools/present.rb +1 -8
- data/lib/insika/tools/remember.rb +7 -2
- data/lib/insika/tools/run_persona_eval.rb +3 -5
- data/lib/insika/tools/save_artifact.rb +16 -6
- data/lib/insika/tools/schedule_followup.rb +3 -3
- data/lib/insika/tools/stuck_signal.rb +3 -3
- data/lib/insika/tools/subagent.rb +5 -5
- data/lib/insika/tools/subagents.rb +3 -3
- data/lib/insika/tools/tool_search.rb +7 -5
- data/lib/insika/tools/tts.rb +2 -2
- data/lib/insika/tools/update_briefing.rb +4 -4
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +18 -0
- data/lib/insika/turn_timing.rb +51 -14
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +81 -16
- data/lib/insika/workflow.rb +25 -64
- data/lib/insika.rb +19 -3
- metadata +69 -13
- data/lib/insika/commands/import_mcp_tools.rb +0 -48
- data/lib/insika/mcp_http_client.rb +0 -67
- data/lib/insika/mcp_tool_ingestor.rb +0 -165
data/docs/TOOLS.md
CHANGED
|
@@ -20,8 +20,8 @@ kinds, and the distinction that matters is **who can change one at runtime**:
|
|
|
20
20
|
**MCP tools are not data tools.** Configuring an enabled MCP **instance** (any
|
|
21
21
|
surface below) is enough — its tools appear automatically, tagged
|
|
22
22
|
`mcp:<instance>`, and each CALL goes straight to the server through a live,
|
|
23
|
-
held client (stdio
|
|
24
|
-
handshake) — never a frozen snapshot. See [MCP servers](#mcp-servers) below.
|
|
23
|
+
held native RubyLLM client (stdio / Streamable HTTP, with discovery and legacy
|
|
24
|
+
handshake fallback) — never a frozen snapshot. See [MCP servers](#mcp-servers) below.
|
|
25
25
|
|
|
26
26
|
Code tools **win name collisions** — you cannot register a data tool whose name
|
|
27
27
|
shadows a code tool.
|
|
@@ -55,6 +55,9 @@ write path (`POST /v1/tools/manifest`) — writing this same shape via the DSL
|
|
|
55
55
|
or Studio needs the literal header value instead; see
|
|
56
56
|
"[The one gotcha](#the-one-gotcha-envsecret-templating-is-manifest-only)" below.
|
|
57
57
|
|
|
58
|
+
`{{ctx.chat_id}}` is the external session ID. In multi-tenant mode, it excludes
|
|
59
|
+
the authenticated tenant prefix used by Insika to isolate stored sessions.
|
|
60
|
+
|
|
58
61
|
### Parameters: the schema is the contract
|
|
59
62
|
|
|
60
63
|
`parameters` is **JSON Schema**, and it reaches the provider verbatim — it is the only
|
|
@@ -281,6 +284,41 @@ backend rules such as quantity limits remain the backend's responsibility.
|
|
|
281
284
|
The per-tool timeout starts after both gates are acquired. Trace duration includes
|
|
282
285
|
queueing time, so it measures how long the model waited, not just backend execution.
|
|
283
286
|
|
|
287
|
+
### Customer confirmation: a write the conversation approves
|
|
288
|
+
|
|
289
|
+
Some writes should not happen on the strength of one ambiguous line — closing an
|
|
290
|
+
order, deleting a record, sending a message on someone's behalf. Name them on the
|
|
291
|
+
agent and the engine holds the call for the customer's word:
|
|
292
|
+
|
|
293
|
+
```ruby
|
|
294
|
+
customer_confirm "create_order" # DSL; the Studio field is "customer confirm"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
When the model calls a held tool, nothing runs. The engine records a pending action
|
|
298
|
+
for the session and returns a `pending_confirmation` result carrying the arguments
|
|
299
|
+
and an instruction: tell the customer exactly what will happen and ask. The turn ends
|
|
300
|
+
with that question. On the next message the open hold is rendered at the tail of the
|
|
301
|
+
context, and two system tools are available: `confirm_pending` runs the original call
|
|
302
|
+
with the arguments recorded at the hold (no path exists to confirm different ones),
|
|
303
|
+
`cancel_pending` drops it. A hold that the next message neither confirms nor cancels
|
|
304
|
+
expires on its own — a customer who changed the subject did not agree.
|
|
305
|
+
|
|
306
|
+
Three controls now sit on a write, and they compose in this order:
|
|
307
|
+
|
|
308
|
+
| Control | Who decides | What happens to the turn | Declared on |
|
|
309
|
+
| --- | --- | --- | --- |
|
|
310
|
+
| Provenance (`requires_evidence`) | the ledger | the call is refused, the model is told how to resolve the id | the tool |
|
|
311
|
+
| Customer confirmation (`customer_confirm`) | the customer, next message | the turn ends with a question | the agent |
|
|
312
|
+
| Operator approval (`approvals_required`) | a person in the Studio | the turn suspends and waits | the agent |
|
|
313
|
+
|
|
314
|
+
A tool is confirmed by the customer or approved by the operator, never both. The
|
|
315
|
+
confirmed run still passes provenance, fencing, the side-effect record and the trace;
|
|
316
|
+
the trace shows the hold as `gate: confirmation`, and the `:tool_result` event and the
|
|
317
|
+
Responses stream report it as `status: held`, which the evals treat as neither a
|
|
318
|
+
success nor an error. Whether the customer's "sim" means yes is the model's reading;
|
|
319
|
+
what the engine guarantees is that the write runs only after an explicit question, a
|
|
320
|
+
customer turn, and an explicit tool call.
|
|
321
|
+
|
|
284
322
|
### Grounding: policing claims against the ledger
|
|
285
323
|
|
|
286
324
|
With the ledger fed, the pack declares how claims are policed — data on the agent,
|
|
@@ -367,7 +405,7 @@ such as suggestion chips (same mechanism, when a channel asks for it).
|
|
|
367
405
|
A tool appears in the Studio panel and enters an agent's tool-loop when it is
|
|
368
406
|
**registered** in the catalog **and** allowed by the agent's policy allowlist.
|
|
369
407
|
Three ways to write a data tool into the store — all **hot** (registry and catalog
|
|
370
|
-
reload, no restart):
|
|
408
|
+
reload, no restart; with several workers, the others pick the change up within ~2 s):
|
|
371
409
|
|
|
372
410
|
1. **DSL** — `data_tool(name:, …)` in a `Insika.agent { … }` block.
|
|
373
411
|
2. **Studio** — the Tools panel editor.
|
|
@@ -390,6 +428,21 @@ any other way must ship literal values** — a real URL, and a real (masked on
|
|
|
390
428
|
read) header value. `{{ctx.*}}` and `{{param}}` work everywhere (they resolve
|
|
391
429
|
at turn time, not ingestion).
|
|
392
430
|
|
|
431
|
+
The Studio **Tools** screen groups permissions by origin: native tools, HTTP
|
|
432
|
+
tools, and MCP tools by server instance. Selection remains per tool; grouping
|
|
433
|
+
does not change tool names or grant permissions.
|
|
434
|
+
Each section can be collapsed. Filtering by tool or server name opens matching
|
|
435
|
+
sections without changing selections; clearing the filter restores their state.
|
|
436
|
+
Section selection buttons affect every tool in that section, including filtered
|
|
437
|
+
tools, but never override denied tools. Save tools applies the changes.
|
|
438
|
+
|
|
439
|
+
An MCP instance is a connection, not a single tool. Enable its individual tools
|
|
440
|
+
for the agent in Studio. **Test connection** validates discovery with that
|
|
441
|
+
instance's credentials; it does not validate a separate HTTP data tool or every
|
|
442
|
+
remote tool's execution permissions. When replacing an HTTP tool with MCP,
|
|
443
|
+
remove the old tool from the agent's permissions and update prompt references
|
|
444
|
+
to the actual MCP tool names. Keep the egress allowlist restricted to required hosts.
|
|
445
|
+
|
|
393
446
|
## MCP servers
|
|
394
447
|
|
|
395
448
|
An MCP **instance** is durable config — transport, target, credentials, an
|
|
@@ -399,19 +452,43 @@ instance is enabled, its tools appear in the catalog automatically (group
|
|
|
399
452
|
server through a live, held client — the runtime never converts an MCP tool
|
|
400
453
|
into a stored data tool.
|
|
401
454
|
|
|
402
|
-
|
|
455
|
+
Each tool is named after its instance: `<instance>__<tool>`, with the instance
|
|
456
|
+
lowercased and anything outside `a-z0-9` turned into `_` (instance
|
|
457
|
+
`shop-staging`, tool `list_products` → `shop_staging__list_products`). That is
|
|
458
|
+
the name the model calls and the one an agent's allowlist holds, so two
|
|
459
|
+
instances that offer the same tool (a store's production and staging) stay two
|
|
460
|
+
tools, each reaching its own server. Per-tool overrides (`tools:`) still use the
|
|
461
|
+
server's own tool name. A qualified name over 64 characters is skipped with a
|
|
462
|
+
warning, since providers reject it: shorten the instance name.
|
|
463
|
+
|
|
464
|
+
The deployment's legacy `import_mcp_tools` command now aliases
|
|
465
|
+
`refresh_mcp_tools`: it returns `{ instance:, tools: }` and emits
|
|
466
|
+
`mcp_tools_refreshed`, replacing the old created/updated import report.
|
|
467
|
+
Previously imported data tools remain unchanged and take precedence over live
|
|
468
|
+
tools with the same name. After refreshing and verifying the live instance,
|
|
469
|
+
delete those snapshots through the normal data-tool controls to use live calls.
|
|
470
|
+
`insika doctor` identifies them by their `mcp:<instance>` group.
|
|
471
|
+
|
|
472
|
+
**Two supported transports**, picked by `transport:`:
|
|
403
473
|
|
|
404
474
|
| Transport | Target | Notes |
|
|
405
475
|
|---|---|---|
|
|
406
476
|
| `stdio` | `command` + `args`, run as a child process, `env` is its process environment | requires `INSIKA_MCP_STDIO=1` — see below |
|
|
407
|
-
| `http` | `url` + `headers` (Streamable HTTP
|
|
408
|
-
|
|
477
|
+
| `http` | `url` + `headers` (Streamable HTTP) | egress-guarded; HTTPS required outside loopback |
|
|
478
|
+
|
|
479
|
+
Legacy `sse` records remain stored, but cannot connect through the native client.
|
|
480
|
+
Change the record to `http` with the server's Streamable HTTP endpoint. Event-stream
|
|
481
|
+
responses over Streamable HTTP remain supported; only the old SSE transport is removed.
|
|
409
482
|
|
|
410
483
|
**The stdio gate.** A stdio instance is arbitrary command execution by
|
|
411
484
|
config — it saves, but refuses to start ("stdio disabled by env") until the
|
|
412
485
|
operator sets `INSIKA_MCP_STDIO=1` (config-over-convention, the same pattern
|
|
413
|
-
as the egress envs). `http
|
|
414
|
-
|
|
486
|
+
as the egress envs). `http` needs no such gate; its URL goes through the
|
|
487
|
+
egress guard instead: HTTPS, and a host that resolves to a private address
|
|
488
|
+
needs `INSIKA_EGRESS_ALLOW_PRIVATE` and a place in `INSIKA_EGRESS_HOSTS`. A
|
|
489
|
+
public host is not pinned by that list, since the operator fixed the URL and
|
|
490
|
+
the model cannot change it. Native MCP additionally refuses plain HTTP
|
|
491
|
+
outside loopback, even when `INSIKA_EGRESS_ALLOW_HTTP` is enabled.
|
|
415
492
|
|
|
416
493
|
**Credentials are never visible in plaintext.** `env` (stdio) and `headers`
|
|
417
494
|
(http/sse) mask every value as `__OCULTO__` on read, everywhere (CLI, API,
|
|
@@ -443,6 +520,31 @@ held client, which does its own discovery on first use regardless of whether
|
|
|
443
520
|
`enabled` flag and its credentials are the **operator's** — a Studio/CLI/API
|
|
444
521
|
edit made after boot is never clobbered back by the next restart.
|
|
445
522
|
|
|
523
|
+
The **first** boot after a declaration also lists the server's tools, because
|
|
524
|
+
until they are listed the agent is offered none of them and answers out of its
|
|
525
|
+
own head instead. A server that cannot be listed (stdio behind a closed gate, a
|
|
526
|
+
host that is down) does not stop the boot — it is warned about by name, and the
|
|
527
|
+
agent runs without those tools until `insika mcp refresh <name>` succeeds.
|
|
528
|
+
|
|
529
|
+
**What the deployment trusts each tool with** — a server describes what its tools
|
|
530
|
+
*do*; only the deployment can say which of its answers are evidence and which
|
|
531
|
+
parameter may only ever carry an id something already returned:
|
|
532
|
+
|
|
533
|
+
```ruby
|
|
534
|
+
mcp "store", transport: :http, url: ENV.fetch("STORE_MCP"),
|
|
535
|
+
tools: {
|
|
536
|
+
# the store's own field names: no server renames them for us
|
|
537
|
+
"search_products" => { evidence: { kind: "products", items: "products",
|
|
538
|
+
id: "product_id", line: "line" } },
|
|
539
|
+
# a write that may only take an id the customer was actually shown
|
|
540
|
+
"add_to_cart" => { requires_evidence: ["product_id"] }
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Both are refused at ingestion, never at the turn: an `evidence:` typo that
|
|
545
|
+
survived would extract nothing and the gate below it would block every write.
|
|
546
|
+
A tool nobody names here behaves exactly as it did before.
|
|
547
|
+
|
|
446
548
|
2. **CLI** — `insika mcp list | add | remove | import <file.json> | test <name> |
|
|
447
549
|
refresh <name>`. `add` takes `--name`, `--transport`, `--command`/`--arg`
|
|
448
550
|
(repeatable) or `--url`/`--header "Name: value"` (repeatable)/`--env
|
|
@@ -464,7 +566,7 @@ held client, which does its own discovery on first use regardless of whether
|
|
|
464
566
|
|
|
465
567
|
`insika mcp import FILE.json` upserts every entry (a bare `command` implies
|
|
466
568
|
`stdio`; a bare `url` implies `http`; add `"transport": "sse"` explicitly
|
|
467
|
-
|
|
569
|
+
only to retain a legacy SSE record pending migration). The same parser
|
|
468
570
|
backs `PUT /v1/mcp` and Studio's "Import JSON" box; `export` produces the
|
|
469
571
|
document back with secrets masked as `__OCULTO__`, so round-tripping an
|
|
470
572
|
export never wipes a stored credential.
|
data/docs/operate.md
CHANGED
|
@@ -10,10 +10,13 @@ permalink: /operate/
|
|
|
10
10
|
Running it once it is live: seeing what happened, firing turns nobody sent,
|
|
11
11
|
handing back a report, and knowing what the box will take before it takes it.
|
|
12
12
|
|
|
13
|
+
- **[Studio](STUDIO.md)** — the operator UI: live conversations, why each reply, the review inbox, what a change did.
|
|
13
14
|
- **[Observability](OBSERVABILITY.md)** — the event stream, and turns as OpenTelemetry traces and metrics.
|
|
14
15
|
- **[Schedules](SCHEDULING.md)** — recurring turns the engine fires on its own tick.
|
|
15
16
|
- **[Artifacts](ARTIFACTS.md)** — a report the agent can hand you a URL to.
|
|
16
17
|
- **[Load test](LOADTEST.md)** — end-to-end load testing, and comparing topologies before you choose one.
|
|
17
18
|
- **[Soak](SOAK.md)** — the long run that finds what a short one cannot.
|
|
18
19
|
- **[Benchmark](BENCHMARK.md)** — the engine's measured overhead, reproducible without an API key.
|
|
20
|
+
- **[Cross-harness bench](CROSS-HARNESS-BENCH.md)** — the same store answered by six harnesses, graded on the store's own state.
|
|
21
|
+
- **[Performance](PERFORMANCE.md)** — what keeps a turn cheap, and what was tried and left out.
|
|
19
22
|
{: .card-grid }
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Platform defaults for agent fields (Settings["agent_defaults"]): an agent
|
|
5
|
+
# with no value of its own for an INHERITABLE field runs with the platform's.
|
|
6
|
+
# The agent's own value always wins, whole (no merge of the two).
|
|
7
|
+
#
|
|
8
|
+
# Applied only where turns read profiles (the graph, through ProfileSource
|
|
9
|
+
# below). Authoring (Studio, pack import) reads the agent's own record, so
|
|
10
|
+
# saving an agent never copies a default into it and it keeps inheriting.
|
|
11
|
+
#
|
|
12
|
+
# FIELDS are the ones where "absent" already means "not configured". Fields
|
|
13
|
+
# where absent means something else (tools_allow nil = every tool) or where
|
|
14
|
+
# absent and off read the same (the memory/fencing booleans) are left out.
|
|
15
|
+
module AgentDefaults
|
|
16
|
+
FIELDS = %w[reliability knowledge memory_retrieval budget alerts guardrails
|
|
17
|
+
grounding followup distill harvest].freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# -> the profile, rebuilt with the defaults it lacks (the same object when
|
|
22
|
+
# there is nothing to fill).
|
|
23
|
+
def apply(profile, defaults)
|
|
24
|
+
fill = FIELDS.each_with_object({}) do |field, acc|
|
|
25
|
+
value = defaults[field]
|
|
26
|
+
acc[field.to_sym] = value if !value.nil? && profile.public_send(field).nil?
|
|
27
|
+
end
|
|
28
|
+
return profile if fill.empty?
|
|
29
|
+
|
|
30
|
+
AgentProfile.build(**profile.to_h.merge(fill))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Like #apply, but a default the agent cannot be built with (saved before it
|
|
34
|
+
# was validated, or a rerank model the registry dropped since) leaves the
|
|
35
|
+
# agent on its own values: one bad default must not stop every turn.
|
|
36
|
+
def apply_safely(profile, defaults)
|
|
37
|
+
apply(profile, defaults)
|
|
38
|
+
rescue Insika::ValidationError => e
|
|
39
|
+
warn "[insika] agent defaults not applied to #{profile.id}: #{e.message}"
|
|
40
|
+
profile
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Raises ValidationError unless an agent can be built with these defaults.
|
|
44
|
+
def validate!(defaults)
|
|
45
|
+
fields = defaults.to_h { |field, value| [field.to_sym, value] }
|
|
46
|
+
AgentProfile.build(id: "agent-defaults", model: nil, **fields)
|
|
47
|
+
defaults
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The source as turns read it (defaults + kits applied), for turn-time readers
|
|
51
|
+
# wired outside the graph (workflow triggers, persona evals). No settings store
|
|
52
|
+
# = nothing to apply, the source as is.
|
|
53
|
+
def for_turns(source, settings_store)
|
|
54
|
+
return source if settings_store.nil?
|
|
55
|
+
|
|
56
|
+
ProfileSource.new(Insika::ProfileSource.coerce(source), settings_store: settings_store)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The agent's own records behind a profile source: what authoring reads and
|
|
60
|
+
# writes. A plain source is its own.
|
|
61
|
+
def own(source) = source.is_a?(ProfileSource) ? source.source : source
|
|
62
|
+
|
|
63
|
+
# A ProfileSource that hands out profiles with the platform defaults and the
|
|
64
|
+
# agent's kits (Insika::Kits) applied.
|
|
65
|
+
# Read-only: writing a profile read here would save the defaults into the
|
|
66
|
+
# agent, so put/delete refuse; writers use #source (AgentDefaults.own).
|
|
67
|
+
# Other reads (all_raw) go to the wrapped source untouched.
|
|
68
|
+
class ProfileSource
|
|
69
|
+
include Insika::ProfileSource
|
|
70
|
+
|
|
71
|
+
attr_reader :source
|
|
72
|
+
|
|
73
|
+
def initialize(source, settings_store:)
|
|
74
|
+
@source = source
|
|
75
|
+
@settings_store = settings_store
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def fetch(id)
|
|
79
|
+
profile = @source.fetch(id)
|
|
80
|
+
profile && Insika::Kits.apply(fill(profile, normalized), kits)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def all
|
|
84
|
+
values = normalized
|
|
85
|
+
by_name = kits
|
|
86
|
+
@source.all.map { |profile| Insika::Kits.apply(fill(profile, values), by_name) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def ids = @source.ids
|
|
90
|
+
|
|
91
|
+
def put(*) = raise(Insika::Error, "agent profiles are written through their own source (AgentDefaults.own)")
|
|
92
|
+
def delete(*) = put
|
|
93
|
+
|
|
94
|
+
def respond_to_missing?(name, include_private = false) = @source.respond_to?(name, include_private) || super
|
|
95
|
+
|
|
96
|
+
def method_missing(name, ...)
|
|
97
|
+
return super unless @source.respond_to?(name)
|
|
98
|
+
|
|
99
|
+
@source.public_send(name, ...)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
# Settings["kits"] — applied after the defaults (Insika::Kits).
|
|
105
|
+
def kits = @settings_store.get["kits"] || {}
|
|
106
|
+
|
|
107
|
+
# Every inheritable field is normalized on its own by AgentProfile.build,
|
|
108
|
+
# so a profile takes the normalized values as they are (Data#with) and is
|
|
109
|
+
# not rebuilt per read.
|
|
110
|
+
def fill(profile, values)
|
|
111
|
+
missing = values.select { |field, _| profile.public_send(field).nil? }
|
|
112
|
+
missing.empty? ? profile : profile.with(**missing)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The defaults as AgentProfile.build normalizes them, rebuilt only when the
|
|
116
|
+
# stored defaults change. Frozen: every profile shares them. A field that
|
|
117
|
+
# does not build is left out (warned once per change), so one bad default
|
|
118
|
+
# cannot stop every turn.
|
|
119
|
+
def normalized
|
|
120
|
+
raw = @settings_store.get["agent_defaults"] || {}
|
|
121
|
+
digest = raw.hash
|
|
122
|
+
return @normalized if digest == @normalized_digest
|
|
123
|
+
|
|
124
|
+
@normalized = build_values(raw.slice(*FIELDS).compact)
|
|
125
|
+
@normalized_digest = digest
|
|
126
|
+
@normalized
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def build_values(raw)
|
|
130
|
+
raw.filter_map do |field, value|
|
|
131
|
+
built = AgentProfile.build(id: "agent-defaults", model: nil, field.to_sym => value)
|
|
132
|
+
[field.to_sym, Ractor.make_shareable(built.public_send(field))]
|
|
133
|
+
rescue Insika::ValidationError => e
|
|
134
|
+
warn "[insika] agent defaults: #{field} not applied: #{e.message}"
|
|
135
|
+
nil
|
|
136
|
+
end.to_h
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -31,6 +31,13 @@ module Insika
|
|
|
31
31
|
entry(agent_id, filename.to_s)&.fetch("content", nil)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
# -> { name => content } every file of the agent, from ONE read of its record
|
|
35
|
+
# (which also carries the version history). Callers that need several files
|
|
36
|
+
# use this instead of `read` per file.
|
|
37
|
+
def contents(agent_id)
|
|
38
|
+
files(agent_id).transform_values { |entry| entry["content"] }
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
# -> [String] the agent's file names, lexicographic order.
|
|
35
42
|
def list(agent_id)
|
|
36
43
|
files(agent_id).keys.sort
|
|
@@ -88,6 +95,11 @@ module Insika
|
|
|
88
95
|
entry(agent_id, filename.to_s)&.fetch("history", []) || []
|
|
89
96
|
end
|
|
90
97
|
|
|
98
|
+
# -> String | nil: when the current content was written (iso8601).
|
|
99
|
+
def updated_at(agent_id, filename)
|
|
100
|
+
entry(agent_id, filename.to_s)&.fetch("updated_at", nil)
|
|
101
|
+
end
|
|
102
|
+
|
|
91
103
|
# Restores version `index` from history as the current content (a new write:
|
|
92
104
|
# the current one goes to the top of history). -> Hash (entry) | raises if index
|
|
93
105
|
# invalid / file nonexistent.
|
data/lib/insika/agent_profile.rb
CHANGED
|
@@ -35,12 +35,17 @@ module Insika
|
|
|
35
35
|
# a per-skill flag forced one decision onto every
|
|
36
36
|
# allowlist holding the skill. NOT `Allowlist`
|
|
37
37
|
# semantics — nil means NONE here (SkillCatalog#eager_for).
|
|
38
|
+
:kits, # kit names (Insika::Kits): bundles of skills/tools applied
|
|
39
|
+
# at READ time by AgentDefaults::ProfileSource — never
|
|
40
|
+
# copied into the record. Always a [String] ([] = none).
|
|
38
41
|
:context_providers, # provider allowlist
|
|
39
42
|
:workflows_allow, # applied by WorkflowAllowlist
|
|
40
43
|
:policies, # names in the Policy Registry
|
|
41
44
|
:prompt_refs, # names in the Prompt Catalog
|
|
42
45
|
:limits, # timeouts/budgets
|
|
43
46
|
:approvals_required, # tools that require approval (ApprovalRequired)
|
|
47
|
+
:customer_confirm, # tools the CUSTOMER confirms on the next message: the
|
|
48
|
+
# call is held, the turn ends with a question (nil = none)
|
|
44
49
|
:capabilities, # intents the agent can trigger.
|
|
45
50
|
# nil = NONE (opt-in, see above).
|
|
46
51
|
:subagents, # allowlist of child agent ids this agent MAY spawn
|
|
@@ -51,9 +56,11 @@ module Insika
|
|
|
51
56
|
:tools_deferred, # searchable-not-wired tools (Tool Search).
|
|
52
57
|
# nil = no deferred (all eager — parity);
|
|
53
58
|
# [names] ⊆ allowed_tools, exposed via tool_search.
|
|
59
|
+
:shared_conversations, # opt-in authoritative conversation persistence
|
|
54
60
|
:memory, # cross-session memory.
|
|
55
61
|
# nil/false = OFF (parity: provider []; the `remember`
|
|
56
62
|
# tool not wired); true = ON. Same opt-in as capabilities.
|
|
63
|
+
:memory_retrieval, # optional reranked context selection; nil = legacy memory block.
|
|
57
64
|
:prompt_caching, # Anthropic prompt caching (R3): nil/false = OFF
|
|
58
65
|
# (parity); true = ON. Same opt-in as `memory`. When ON
|
|
59
66
|
# AND the resolved provider is Anthropic, ChatBuilder sets
|
|
@@ -95,7 +102,10 @@ module Insika
|
|
|
95
102
|
# release: the goldens were baselined on unfenced bytes.
|
|
96
103
|
:params, # LLM generation params: a Hash with
|
|
97
104
|
# temperature/max_tokens/thinking, applied to the chat at
|
|
98
|
-
# stage 5. {} = provider defaults (parity).
|
|
105
|
+
# stage 5. {} = provider defaults (parity). Plus
|
|
106
|
+
# `provider_routing` — which UPSTREAM serves the model, for
|
|
107
|
+
# a gateway that has several; emitted under the wire key
|
|
108
|
+
# `provider` (OpenRouter's shape).
|
|
99
109
|
:budget, # spend caps per (tenant, agent) over
|
|
100
110
|
# CALENDAR windows (WS2): { "daily" => int,
|
|
101
111
|
# "monthly" => int, "soft" => bool, "alert_at" => 0.8 }.
|
|
@@ -330,9 +340,10 @@ module Insika
|
|
|
330
340
|
# platform `default_model` (Settings) at turn start via the ModelResolver.
|
|
331
341
|
def self.build(id:, model: nil, provider: nil, base_prompt: "", prompt_files: [],
|
|
332
342
|
tools_allow: nil, tools_deny: [], tools_allow_groups: nil, skills: nil,
|
|
333
|
-
skills_eager: nil, context_providers: nil, workflows_allow: nil,
|
|
343
|
+
skills_eager: nil, kits: nil, context_providers: nil, workflows_allow: nil,
|
|
334
344
|
policies: [], prompt_refs: [], limits: {}, approvals_required: nil,
|
|
335
|
-
|
|
345
|
+
customer_confirm: nil,
|
|
346
|
+
capabilities: nil, subagents: nil, tools_deferred: nil, memory: nil, memory_retrieval: nil, shared_conversations: false,
|
|
336
347
|
prompt_caching: nil, tool_persistence: nil, tool_output_compression: nil,
|
|
337
348
|
fencing: nil, params: {}, model_policy: nil, guardrails: nil, sandbox: nil,
|
|
338
349
|
refinement: nil, capabilities_declared: nil, edge_stream: nil, metadata: {},
|
|
@@ -344,16 +355,19 @@ outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil
|
|
|
344
355
|
prompt_files: Array(prompt_files), tools_allow: tools_allow,
|
|
345
356
|
tools_deny: Array(tools_deny), tools_allow_groups: tools_allow_groups, skills: skills,
|
|
346
357
|
skills_eager: skills_eager,
|
|
358
|
+
kits: Array(kits).map(&:to_s).uniq,
|
|
347
359
|
context_providers: context_providers, workflows_allow: workflows_allow,
|
|
348
360
|
policies: normalize_policies(policies, tools_allow: tools_allow, tools_deny: tools_deny,
|
|
349
361
|
tools_allow_groups: tools_allow_groups),
|
|
350
362
|
prompt_refs: Array(prompt_refs),
|
|
351
363
|
limits: DEFAULT_LIMITS.merge(limits), approvals_required: approvals_required,
|
|
364
|
+
customer_confirm: normalize_customer_confirm(customer_confirm, approvals_required),
|
|
352
365
|
capabilities: capabilities,
|
|
353
366
|
# opt-in like capabilities: nil => NONE. Array-normalize a present value so
|
|
354
367
|
# readers get a clean [] and the ChatBuilder gate (present? => wire) is stable.
|
|
355
368
|
subagents: subagents.nil? ? nil : Array(subagents).map(&:to_s),
|
|
356
|
-
tools_deferred: tools_deferred, memory: memory,
|
|
369
|
+
tools_deferred: tools_deferred, memory: memory, shared_conversations: shared_conversations == true,
|
|
370
|
+
memory_retrieval: normalize_memory_retrieval(memory_retrieval),
|
|
357
371
|
prompt_caching: prompt_caching, tool_persistence: tool_persistence,
|
|
358
372
|
tool_output_compression: tool_output_compression, fencing: fencing,
|
|
359
373
|
# The free-form hashes arrive with symbol keys (internal build) OR string
|
|
@@ -402,10 +416,9 @@ outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil
|
|
|
402
416
|
# free-form hashes; shape-validated by the command/engine/doctor
|
|
403
417
|
# (never here — the refinement precedent). nil = off (parity).
|
|
404
418
|
harvest: Coercion.deep_stringify(harvest),
|
|
405
|
-
# knowledge is profile DATA
|
|
406
|
-
#
|
|
407
|
-
|
|
408
|
-
knowledge: Coercion.deep_stringify(knowledge),
|
|
419
|
+
# knowledge is profile DATA. Legacy extraction remains free-form;
|
|
420
|
+
# an explicit rerank declaration is validated when loaded.
|
|
421
|
+
knowledge: normalize_knowledge(knowledge),
|
|
409
422
|
# schedules is profile DATA, deep-stringified like the other
|
|
410
423
|
# free-form hashes (an ARRAY of declarations); parsed into
|
|
411
424
|
# Insika::Schedule entries by the engine/doctor/Studio (shape-validated
|
|
@@ -414,6 +427,46 @@ outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil
|
|
|
414
427
|
)
|
|
415
428
|
end
|
|
416
429
|
|
|
430
|
+
def self.normalize_knowledge(value)
|
|
431
|
+
config = Coercion.deep_stringify(value)
|
|
432
|
+
return config unless config.is_a?(Hash) && config.key?("rerank")
|
|
433
|
+
|
|
434
|
+
validate_rerank(config, "knowledge")
|
|
435
|
+
config
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def self.normalize_memory_retrieval(value)
|
|
439
|
+
return nil if value.nil?
|
|
440
|
+
|
|
441
|
+
config = Coercion.deep_stringify(value)
|
|
442
|
+
raise ValidationError, "memory_retrieval must be an object" unless config.is_a?(Hash)
|
|
443
|
+
|
|
444
|
+
validate_rerank(config, "memory_retrieval", require_top_k: true)
|
|
445
|
+
config
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def self.validate_rerank(config, field, require_top_k: false)
|
|
449
|
+
rerank = config["rerank"]
|
|
450
|
+
raise ValidationError, "#{field}.rerank must be an object" unless rerank.is_a?(Hash)
|
|
451
|
+
|
|
452
|
+
top_k = require_top_k ? config["top_k"] : config.fetch("top_k", 5)
|
|
453
|
+
limit = rerank["candidate_limit"]
|
|
454
|
+
timeout = rerank["timeout_seconds"]
|
|
455
|
+
valid = top_k.is_a?(Integer) && top_k.positive? &&
|
|
456
|
+
limit.is_a?(Integer) && limit >= top_k && limit <= 100 &&
|
|
457
|
+
timeout.is_a?(Numeric) && timeout.finite? && timeout.positive? &&
|
|
458
|
+
%w[provider model].all? { |key| rerank[key].is_a?(String) && !rerank[key].strip.empty? }
|
|
459
|
+
raise ValidationError, "invalid #{field}.rerank configuration" unless valid
|
|
460
|
+
|
|
461
|
+
require "ruby_llm"
|
|
462
|
+
begin
|
|
463
|
+
model = RubyLLM.models.find(rerank["model"], provider: rerank["provider"])
|
|
464
|
+
rescue RubyLLM::ModelNotFoundError
|
|
465
|
+
raise ValidationError, "#{field}.rerank model is not registered for provider"
|
|
466
|
+
end
|
|
467
|
+
raise ValidationError, "#{field}.rerank model does not support reranking" unless model.type == :rerank
|
|
468
|
+
end
|
|
469
|
+
|
|
417
470
|
# Declaring a tool allow/deny list IS opting into it. The list is only ever
|
|
418
471
|
# applied by the builtin `tool_allowlist` policy, and the Policy::Engine runs
|
|
419
472
|
# ONLY the policies a profile names — so a profile with `tools_allow: [a, b]`
|
|
@@ -461,6 +514,23 @@ outputs: nil, stt_prompt: nil, briefing_fields: nil, grounding: nil, funnel: nil
|
|
|
461
514
|
names
|
|
462
515
|
end
|
|
463
516
|
|
|
517
|
+
# nil/empty -> nil (off); else [String], trimmed, uniq. A tool is confirmed by
|
|
518
|
+
# the customer OR approved by the operator, never both: the two hold a call in
|
|
519
|
+
# opposite ways (the turn ends with a question vs. the turn suspends), and one
|
|
520
|
+
# tool cannot do both at once.
|
|
521
|
+
def self.normalize_customer_confirm(list, approvals)
|
|
522
|
+
names = Array(list).map { |n| n.to_s.strip }.reject(&:empty?).uniq
|
|
523
|
+
return nil if names.empty?
|
|
524
|
+
|
|
525
|
+
both = names & Array(approvals).map(&:to_s)
|
|
526
|
+
unless both.empty?
|
|
527
|
+
raise Insika::ValidationError,
|
|
528
|
+
"customer_confirm and approvals_required overlap (#{both.join(', ')}): a tool is " \
|
|
529
|
+
"confirmed by the customer or approved by the operator, not both"
|
|
530
|
+
end
|
|
531
|
+
names
|
|
532
|
+
end
|
|
533
|
+
|
|
464
534
|
# opt-in for an optional tool = being in the agent's allow list.
|
|
465
535
|
def tool_opted_in?(name)
|
|
466
536
|
Array(tools_allow).include?(name)
|
|
@@ -22,6 +22,17 @@ module Insika
|
|
|
22
22
|
AUTHENTICATED_PATH = "/studio/artifacts/%{id}/content"
|
|
23
23
|
SIGNED_PATH = "/studio/artifacts/s/%{id}?exp=%{exp}&sig=%{sig}"
|
|
24
24
|
|
|
25
|
+
# The Studio's signing config, read from the environment.
|
|
26
|
+
# -> { key:, ttl:, base_url: } | nil (no signing key -> no signed surface).
|
|
27
|
+
def studio_config
|
|
28
|
+
key = Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_KEY")
|
|
29
|
+
return nil if key.to_s.empty?
|
|
30
|
+
|
|
31
|
+
{ key: key,
|
|
32
|
+
ttl: (Insika::EnvSchema.read("INSIKA_ARTIFACT_SIGNING_TTL") || 604_800).to_i,
|
|
33
|
+
base_url: Insika::Coercion.presence(Insika::EnvSchema.read("INSIKA_PUBLIC_URL")).to_s }
|
|
34
|
+
end
|
|
35
|
+
|
|
25
36
|
# -> hex token (64 chars) | nil when the key is blank (no signed surface).
|
|
26
37
|
def sign(id:, expires_at:, key:)
|
|
27
38
|
key = key.to_s
|