insika 0.3.0 → 0.8.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 +296 -0
- data/README.md +48 -12
- data/bin/insika +725 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +116 -406
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +137 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +63 -19
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +87 -10
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +128 -3
- data/docs/FACTS.md +3 -3
- data/docs/HARVEST.md +5 -6
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +17 -29
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +46 -12
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +222 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +5 -5
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +23 -7
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +3 -3
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +176 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +4 -4
- data/docs/improve.md +20 -0
- data/docs/index.md +8 -5
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/channel_delivery.rb +1 -1
- data/lib/insika/chat_builder.rb +50 -19
- data/lib/insika/commands/agent_payload.rb +2 -2
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +15 -3
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/compaction.rb +196 -0
- data/lib/insika/context/builder.rb +6 -2
- data/lib/insika/context/fragment.rb +4 -1
- data/lib/insika/context/priority.rb +8 -0
- data/lib/insika/context/providers/briefing.rb +53 -24
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- data/lib/insika/context/providers/session.rb +46 -10
- data/lib/insika/context_trace_store.rb +11 -1
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/doctor.rb +280 -17
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +64 -79
- data/lib/insika/dsl/server_boot.rb +23 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +103 -2
- data/lib/insika/env_schema.rb +21 -7
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +11 -0
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +84 -2
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +295 -55
- data/lib/insika/followup_policy.rb +2 -25
- data/lib/insika/golden_store.rb +16 -1
- data/lib/insika/grounding/matcher.rb +1 -1
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/loop_detector.rb +5 -34
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +115 -31
- data/lib/insika/message_origin.rb +1 -1
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/onboarding.rb +17 -1
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +2 -2
- data/lib/insika/profile_source.rb +15 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/retention.rb +36 -1
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/server/app.rb +105 -15
- data/lib/insika/server/rack_app.rb +5 -1
- data/lib/insika/server/responses.rb +5 -5
- data/lib/insika/session_store.rb +34 -4
- data/lib/insika/settings_store.rb +8 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/soak/runner.rb +4 -4
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +591 -47
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +21 -21
- data/lib/insika/studio/forms.rb +57 -5
- data/lib/insika/studio/nav_icons.rb +14 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +61 -820
- data/lib/insika/studio/views/agents.erb +70 -57
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/evals.erb +2 -2
- data/lib/insika/studio/views/facts.erb +1 -1
- data/lib/insika/studio/views/funnel.erb +1 -1
- data/lib/insika/studio/views/home.erb +106 -67
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +14 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/session.erb +231 -177
- data/lib/insika/studio/views/settings.erb +50 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- data/lib/insika/telemetry/recorder.rb +49 -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 +47 -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 +68 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tool_batch.rb +67 -0
- data/lib/insika/tool_usage_report.rb +162 -0
- data/lib/insika/tools/generate_image.rb +52 -7
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/turn_budget.rb +91 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +15 -4
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +184 -12
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +64 -0
- metadata +109 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
data/lib/insika/server/app.rb
CHANGED
|
@@ -56,7 +56,7 @@ module Insika
|
|
|
56
56
|
config:, pending_action_store: nil, a2a: nil, provisioner: nil,
|
|
57
57
|
workflow_registry: nil, onboarding: nil, profiles: nil,
|
|
58
58
|
channels: nil, logger: nil, token_store: nil, outcome_store: nil,
|
|
59
|
-
executor: nil, db_path: nil)
|
|
59
|
+
executor: nil, db_path: nil, tool_registry: nil, mcp_store: nil)
|
|
60
60
|
@command_bus = command_bus
|
|
61
61
|
@event_stream = event_stream
|
|
62
62
|
@session_store = session_store
|
|
@@ -104,6 +104,17 @@ module Insika
|
|
|
104
104
|
# Both optional — nil means the body simply omits those readings.
|
|
105
105
|
@executor = executor
|
|
106
106
|
@db_path = db_path
|
|
107
|
+
# The deployment's EFFECTIVE tool registry, so the capability view can
|
|
108
|
+
# answer what the agent can reach that can WRITE (the derived
|
|
109
|
+
# eval-profile swap list). Read-only use (names/side_effect?) — same
|
|
110
|
+
# constitutional footing as the profile source. nil = the view omits it.
|
|
111
|
+
@tool_registry = tool_registry
|
|
112
|
+
# The durable MCP instance CRUD (GET/PUT/DELETE
|
|
113
|
+
# /v1/mcp[/:name]) — a direct store read/Command surface, same footing
|
|
114
|
+
# as @profiles/@outcome_store. nil = the routes 404 (parity — a
|
|
115
|
+
# deployment that never wires an McpStore never exposes MCP config
|
|
116
|
+
# over HTTP, same as one that skips provisioning).
|
|
117
|
+
@mcp_store = mcp_store
|
|
107
118
|
@heartbeat = config.fetch(:heartbeat, 15)
|
|
108
119
|
@sync_timeout = config.fetch(:sync_timeout, 10) # synchronous control
|
|
109
120
|
end
|
|
@@ -181,6 +192,14 @@ module Insika
|
|
|
181
192
|
handle_list_outcomes(req)
|
|
182
193
|
in ["POST", ["v1", "tools", "manifest"]]
|
|
183
194
|
handle_import_tools(req)
|
|
195
|
+
in ["GET", ["v1", "mcp"]] if @mcp_store
|
|
196
|
+
handle_list_mcp
|
|
197
|
+
in ["GET", ["v1", "mcp", name]] if @mcp_store
|
|
198
|
+
handle_read_mcp(name)
|
|
199
|
+
in ["PUT", ["v1", "mcp"]] if @mcp_store
|
|
200
|
+
handle_upsert_mcp(req)
|
|
201
|
+
in ["DELETE", ["v1", "mcp", name]] if @mcp_store
|
|
202
|
+
handle_delete_mcp(req, name)
|
|
184
203
|
in ["POST", ["v1", "mcp", name, "import"]]
|
|
185
204
|
handle_import_mcp_tools(req, name)
|
|
186
205
|
in ["POST", ["v1", "agents"]] if @provisioner
|
|
@@ -438,18 +457,61 @@ module Insika
|
|
|
438
457
|
json_response(200, dispatch_with_timeout(command))
|
|
439
458
|
end
|
|
440
459
|
|
|
441
|
-
#
|
|
460
|
+
# GET /v1/mcp — every configured MCP instance, MASKED (@mcp_store.all
|
|
461
|
+
# already masks — never plaintext over the wire). A direct store read,
|
|
462
|
+
# not a Command (constitutional rule at the top of this file); the
|
|
463
|
+
# generic route gate already confined this to the operator (not a
|
|
464
|
+
# TENANT_SURFACES entry), same footing as GET /v1/agents/:id.
|
|
465
|
+
def handle_list_mcp
|
|
466
|
+
json_response(200, { instances: @mcp_store.all })
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# GET /v1/mcp/:name — one instance, MASKED. Missing -> 404.
|
|
470
|
+
def handle_read_mcp(name)
|
|
471
|
+
record = @mcp_store.get(name)
|
|
472
|
+
raise Insika::NotFoundError, "MCP instance '#{name}' not found" if record.nil?
|
|
473
|
+
|
|
474
|
+
json_response(200, record)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# PUT /v1/mcp — create/edit an instance (the CLI/DSL/Studio
|
|
478
|
+
# config surfaces share this ONE seam: the `:upsert_mcp` Command already
|
|
479
|
+
# registered on the bus). Per-key secret reconciliation (the `__OCULTO__`
|
|
480
|
+
# sentinel round-trips: preserves; a new string replaces; "" clears) —
|
|
481
|
+
# same contract as `Insika::McpJson`. 200 { the MASKED upserted record }.
|
|
482
|
+
def handle_upsert_mcp(req)
|
|
483
|
+
gate = gateway_gate(req)
|
|
484
|
+
return gate if gate
|
|
485
|
+
|
|
486
|
+
command = Insika::Command.build(:upsert_mcp, parse_body(req), transport: :http)
|
|
487
|
+
json_response(200, dispatch_with_timeout(command))
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# DELETE /v1/mcp/:name — removes an instance (`:delete_mcp`). Idempotent
|
|
491
|
+
# (200 { existed: false } on a name that was never there — never a 404;
|
|
492
|
+
# deleting a thing that is already gone is not an error). The name comes
|
|
493
|
+
# from the ROUTE (data), not the body.
|
|
494
|
+
def handle_delete_mcp(req, name)
|
|
495
|
+
gate = gateway_gate(req)
|
|
496
|
+
return gate if gate
|
|
497
|
+
|
|
498
|
+
command = Insika::Command.build(:delete_mcp, { name: name }, transport: :http)
|
|
499
|
+
json_response(200, dispatch_with_timeout(command))
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# POST /v1/mcp/:name/import — kept as the alias for the
|
|
503
|
+
# "Refresh tools" action (the route predates the live registry; the name
|
|
504
|
+
# stuck). Dispatches :refresh_mcp_tools — LIVE connect + tools/list +
|
|
505
|
+
# write McpStore#tools_cache, never the retired ingestor snapshot. Same
|
|
442
506
|
# Bearer as provisioning (gateway_token, fail-closed): it's an authoring
|
|
443
|
-
# surface.
|
|
444
|
-
#
|
|
445
|
-
#
|
|
446
|
-
# + instance }. Missing instance -> 404; disabled/no-url -> 422; per-tool
|
|
447
|
-
# failure isolated in `errors[]` (R4). The name comes from the ROUTE (data), not the body.
|
|
507
|
+
# surface. Missing instance -> 404; disabled -> 422; a transport failure
|
|
508
|
+
# surfaces as whatever #call's rescue maps it to. The name comes from the
|
|
509
|
+
# ROUTE (data), not the body.
|
|
448
510
|
def handle_import_mcp_tools(req, name)
|
|
449
511
|
gate = gateway_gate(req)
|
|
450
512
|
return gate if gate
|
|
451
513
|
|
|
452
|
-
command = Insika::Command.build(:
|
|
514
|
+
command = Insika::Command.build(:refresh_mcp_tools, { name: name }, transport: :http)
|
|
453
515
|
json_response(200, dispatch_with_timeout(command))
|
|
454
516
|
end
|
|
455
517
|
|
|
@@ -466,22 +528,50 @@ module Insika
|
|
|
466
528
|
# (a client: it never reads a store) can tell "this case cannot run here" from
|
|
467
529
|
# "this case failed". Deliberately NOT the profile: the prompt,
|
|
468
530
|
# the model and the guardrail config are none of the caller's business. Just the
|
|
469
|
-
#
|
|
531
|
+
# three facts a case declares `requires` against — plus the side-effect set,
|
|
532
|
+
# which is what an eval profile must swap.
|
|
470
533
|
#
|
|
471
534
|
# `tools` is the DECLARED allowlist, and `null` means the agent has an open one
|
|
472
535
|
# (every registered tool) — the client reads that as "cannot rule anything out"
|
|
473
536
|
# and runs the case rather than skipping it.
|
|
537
|
+
#
|
|
538
|
+
# `side_effect_tools` is DERIVED here from the deployment's own registry (the
|
|
539
|
+
# effective one the Executor uses): the agent's reachable tools the registry
|
|
540
|
+
# marks `side_effect` (a data-tool's non-GET method, the MCP ingestor's
|
|
541
|
+
# `tools/call`). It is the list a simulated run's eval profile must cover —
|
|
542
|
+
# and it is a fact of the deployment, never a hand-maintained client list.
|
|
543
|
+
# Absent (no registry wired) -> the key is omitted, and the view behaves as
|
|
544
|
+
# before.
|
|
474
545
|
def handle_read_agent(id)
|
|
475
546
|
profile = @profiles.fetch(id)
|
|
476
547
|
raise Insika::NotFoundError, "agent not found: #{id}" if profile.nil?
|
|
477
548
|
|
|
478
549
|
allow = profile.tools_allow
|
|
479
550
|
deny = Array(profile.tools_deny).map(&:to_s)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
551
|
+
payload = {
|
|
552
|
+
id: profile.id,
|
|
553
|
+
# `tools: null` is MEANINGFUL (an open allowlist — the client reads it
|
|
554
|
+
# as "cannot rule anything out"), so it must survive, unlike the new
|
|
555
|
+
# field below.
|
|
556
|
+
tools: allow.nil? ? nil : (Array(allow).map(&:to_s) - deny),
|
|
557
|
+
capabilities: Array(profile.capabilities_declared).map(&:to_s)
|
|
558
|
+
}
|
|
559
|
+
derived = side_effect_tools(profile)
|
|
560
|
+
payload[:side_effect_tools] = derived unless derived.nil?
|
|
561
|
+
json_response(200, payload)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# The derived side-effect set for an agent: its reachable tools (the same
|
|
565
|
+
# allowlist − deny resolution the `tools` field uses) that the registry marks
|
|
566
|
+
# `side_effect`, sorted for a stable answer. -> [names] | nil (no registry).
|
|
567
|
+
def side_effect_tools(profile)
|
|
568
|
+
return nil unless @tool_registry
|
|
569
|
+
|
|
570
|
+
allowed = if profile.tools_allow.nil? then Array(@tool_registry.names)
|
|
571
|
+
else Array(profile.tools_allow).map(&:to_s)
|
|
572
|
+
end
|
|
573
|
+
denied = Array(profile.tools_deny).map(&:to_s)
|
|
574
|
+
(allowed - denied).select { |name| @tool_registry.side_effect?(name) }.sort
|
|
485
575
|
end
|
|
486
576
|
|
|
487
577
|
# `/v1` only — `/a2a` is versioned by its own JSON-RPC spec and a channel's
|
|
@@ -887,7 +977,7 @@ TENANT_SURFACES = [
|
|
|
887
977
|
parts = payload[:parts] || payload["parts"]
|
|
888
978
|
if parts && !Insika::Media.well_formed?(parts)
|
|
889
979
|
raise Insika::ValidationError,
|
|
890
|
-
"malformed content part — each part must be {type: text|image|audio} with text/url"
|
|
980
|
+
"malformed content part — each part must be {type: text|image|audio|document} with text/url"
|
|
891
981
|
end
|
|
892
982
|
# WS9: the only declared message source is "voice" (pre-transcribed
|
|
893
983
|
# audio) — a consumer that writes prose here is told so, not silently.
|
|
@@ -59,8 +59,10 @@ module Insika
|
|
|
59
59
|
pending_action_store: @graph.pending_action_store,
|
|
60
60
|
provisioner: Insika::PackImporter.new(bus: @graph.bus, profiles: @graph.profiles),
|
|
61
61
|
# GET /v1/agents/:id — the read-only capability view a case's `requires`
|
|
62
|
-
# resolves against
|
|
62
|
+
# resolves against (and the derived side-effect set an eval profile must
|
|
63
|
+
# cover).
|
|
63
64
|
profiles: @graph.profiles,
|
|
65
|
+
tool_registry: @graph.tool_registry,
|
|
64
66
|
# the OSS onboarding surface (start.md + models.json + docs).
|
|
65
67
|
# This is the primary "build my first agent" target — models.json reports the
|
|
66
68
|
# DSL's stores + the agents this process serves (each id IS the `model`).
|
|
@@ -76,6 +78,8 @@ module Insika
|
|
|
76
78
|
config: { gateway_token: @token, tenancy: tenancy }.merge(@config),
|
|
77
79
|
token_store: store,
|
|
78
80
|
outcome_store: @graph.outcome_store,
|
|
81
|
+
# GET/PUT/DELETE /v1/mcp[/:name] — the config surface.
|
|
82
|
+
mcp_store: @rt.component(:mcp_store),
|
|
79
83
|
# a 500's error_ref must be findable in the process log.
|
|
80
84
|
logger: $stdout
|
|
81
85
|
)
|
|
@@ -12,9 +12,9 @@ module Insika
|
|
|
12
12
|
# OpenAI Responses SSE frame (or nil for events with no counterpart). Follows the
|
|
13
13
|
# constitutional rule: no business logic, no store access here.
|
|
14
14
|
#
|
|
15
|
-
# Request: { model: "
|
|
15
|
+
# Request: { model: "insika:<agent>", user: "<chat.id>", stream: true,
|
|
16
16
|
# input: "<string with already-composed blocks>" } + header
|
|
17
|
-
# X-
|
|
17
|
+
# X-Insika-Agent (agent fallback). The `input` enters VERBATIM as the
|
|
18
18
|
# turn's message — the blocks (<memoria>/<dados_conhecidos>/directives) already come
|
|
19
19
|
# composed by the consumer (the engine does not interpret them).
|
|
20
20
|
module Responses
|
|
@@ -32,8 +32,8 @@ module Insika
|
|
|
32
32
|
# gets that filtered structurally instead of by a regex on the leading tag.
|
|
33
33
|
# Omitted = a customer typed it, which is what every turn meant before.
|
|
34
34
|
def parse_request(body, req)
|
|
35
|
-
agent = body[:model].to_s.sub(/\
|
|
36
|
-
agent = req.get_header("
|
|
35
|
+
agent = body[:model].to_s.sub(/\Ainsika:/, "")
|
|
36
|
+
agent = req.get_header("HTTP_X_INSIKA_AGENT").to_s if agent.empty?
|
|
37
37
|
raise Insika::ValidationError, "model/agent missing" if agent.strip.empty?
|
|
38
38
|
|
|
39
39
|
user = body[:user].to_s
|
|
@@ -53,7 +53,7 @@ module Insika
|
|
|
53
53
|
if raw.is_a?(Array)
|
|
54
54
|
unless Insika::Media.well_formed?(raw)
|
|
55
55
|
raise Insika::ValidationError,
|
|
56
|
-
"malformed content part — each part must be {type: text|image|audio} with text/url"
|
|
56
|
+
"malformed content part — each part must be {type: text|image|audio|document} with text/url"
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
normalized = Insika::Media.parts(raw).map do |p|
|
data/lib/insika/session_store.rb
CHANGED
|
@@ -23,11 +23,11 @@ module Insika
|
|
|
23
23
|
KEY_PREFIX = "session:"
|
|
24
24
|
|
|
25
25
|
Session = Data.define(:id, :messages, :vars, :memory_refs,
|
|
26
|
-
:created_at, :updated_at, :briefing, :evidence) do
|
|
26
|
+
:created_at, :updated_at, :briefing, :evidence, :compaction) do
|
|
27
27
|
# Trailing members with defaults: an old record without the "briefing" /
|
|
28
|
-
# "evidence" keys reads as empty/nil without a migration.
|
|
28
|
+
# "evidence" / "compaction" keys reads as empty/nil without a migration.
|
|
29
29
|
def initialize(id:, messages:, vars:, memory_refs:, created_at:, updated_at:,
|
|
30
|
-
briefing: nil, evidence: nil)
|
|
30
|
+
briefing: nil, evidence: nil, compaction: nil)
|
|
31
31
|
super
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -150,6 +150,35 @@ module Insika
|
|
|
150
150
|
to_session(record)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
+
# -> Session. Persists the in-session compaction state (RFC-0044): the
|
|
154
|
+
# summary of messages[0...upto] plus the boundary. `upto` is MONOTONIC —
|
|
155
|
+
# a write that does not move the boundary forward is a no-op (a stale
|
|
156
|
+
# double-write from a racing worker can never move it backwards). `runs`
|
|
157
|
+
# counts compactions over the session's lifetime (the trace reports it).
|
|
158
|
+
#
|
|
159
|
+
# CONCURRENCY NOTE: an unlocked RMW (read -> mutate -> set), like
|
|
160
|
+
# update_briefing — and safe for the same reason: nothing in the
|
|
161
|
+
# read/mutate/set path suspends, so no other writer can interleave
|
|
162
|
+
# mid-RMW. The LLM call that produced the summary happened BEFORE this
|
|
163
|
+
# method; only the plain write lives here.
|
|
164
|
+
def set_compaction(id, summary:, upto:, model: nil)
|
|
165
|
+
record = fetch!(id)
|
|
166
|
+
current = record["compaction"]
|
|
167
|
+
upto = Integer(upto)
|
|
168
|
+
return to_session(record) if current && upto <= current["upto"].to_i
|
|
169
|
+
|
|
170
|
+
record["compaction"] = {
|
|
171
|
+
"summary" => Coercion.utf8(summary.to_s),
|
|
172
|
+
"upto" => upto,
|
|
173
|
+
"runs" => (current ? current["runs"].to_i : 0) + 1,
|
|
174
|
+
"model" => Coercion.presence(model.to_s),
|
|
175
|
+
"at" => timestamp
|
|
176
|
+
}.compact
|
|
177
|
+
record["updated_at"] = timestamp
|
|
178
|
+
@store.set(SCOPE, key_for(id), record)
|
|
179
|
+
to_session(record)
|
|
180
|
+
end
|
|
181
|
+
|
|
153
182
|
# -> bool (delegates to the backend: false for a nonexistent id)
|
|
154
183
|
def delete(id)
|
|
155
184
|
@store.delete(SCOPE, key_for(id))
|
|
@@ -189,7 +218,8 @@ module Insika
|
|
|
189
218
|
created_at: record["created_at"],
|
|
190
219
|
updated_at: record["updated_at"],
|
|
191
220
|
briefing: record["briefing"] || { "fields" => {}, "next_step" => nil },
|
|
192
|
-
evidence: record["evidence"]
|
|
221
|
+
evidence: record["evidence"],
|
|
222
|
+
compaction: record["compaction"]
|
|
193
223
|
)
|
|
194
224
|
end
|
|
195
225
|
|
|
@@ -30,7 +30,14 @@ module Insika
|
|
|
30
30
|
"max_retries" => 2,
|
|
31
31
|
"turn_timeout" => 120,
|
|
32
32
|
"tool_timeout" => 30,
|
|
33
|
-
|
|
33
|
+
# In-session compaction (RFC-0044): when a session's UNCOMPACTED
|
|
34
|
+
# transcript grows past `compact_after` messages, everything but the
|
|
35
|
+
# last `keep_last` is summarized (model -> compaction.model, else the
|
|
36
|
+
# platform utility_model) into one history fragment. `prompt` replaces
|
|
37
|
+
# the engine default wholesale (the distill convention). enabled: false
|
|
38
|
+
# = parity (nothing runs). Additive keys — reads overlay DEFAULTS.
|
|
39
|
+
"compaction" => { "enabled" => false, "keep_last" => 20,
|
|
40
|
+
"compact_after" => 40, "model" => nil },
|
|
34
41
|
# Data lifecycle (WS8, phase 2): the RETENTION window in days. The
|
|
35
42
|
# tick's Retention sweep purges sessions (+traces), terminal tasks
|
|
36
43
|
# (+checkpoints), memory cells and outcomes older than this. nil/0 =
|
data/lib/insika/skill_catalog.rb
CHANGED
|
@@ -68,6 +68,18 @@ module Insika
|
|
|
68
68
|
self
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Appends roots at the END — lowest precedence, so a workspace skill always
|
|
72
|
+
# overrides a same-named one shipped by a plugin. This is how
|
|
73
|
+
# the plugin Loader's `skill_dirs` reach the catalog at boot; reloads only
|
|
74
|
+
# when something new actually arrived.
|
|
75
|
+
def add_roots(dirs)
|
|
76
|
+
added = Array(dirs).map { |d| File.expand_path(d.to_s) } - @roots.map { |r| File.expand_path(r) }
|
|
77
|
+
return self if added.empty?
|
|
78
|
+
|
|
79
|
+
@roots.concat(added)
|
|
80
|
+
reload
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
# Per-agent allowlist: nil -> all | [] -> none | [names] -> subset. `agent`
|
|
72
84
|
# selects WHICH body each allowed name resolves to (see #find); the allowlist is
|
|
73
85
|
# by NAME either way, so specializing a skill never touches the allowlist.
|
data/lib/insika/soak/runner.rb
CHANGED
|
@@ -50,7 +50,7 @@ module Insika
|
|
|
50
50
|
|
|
51
51
|
Environment:
|
|
52
52
|
INSIKA_URL base URL of the engine (default: http://localhost:9292)
|
|
53
|
-
|
|
53
|
+
INSIKA_GATEWAY_TOKEN Bearer; falls back to ADMIN_TOKEN, then "local-demo"
|
|
54
54
|
TXT
|
|
55
55
|
|
|
56
56
|
# Poisson inter-arrival seconds: `-mean * Math.log(1.0 - rand)`. Seeded,
|
|
@@ -245,7 +245,7 @@ module Insika
|
|
|
245
245
|
" agent: #{@agent}",
|
|
246
246
|
" shape: #{@envelope[:turns_per_hour]} turns/h poisson, #{@envelope[:session_turns]}-turn sessions, " \
|
|
247
247
|
"cap #{@envelope[:concurrency_cap]}, #{@envelope[:duration_hours]}h (#{@envelope[:warmup_hours]}h warmup)",
|
|
248
|
-
" sample: POST #{URI.join(target_url + '/', 'v1/responses')} model=
|
|
248
|
+
" sample: POST #{URI.join(target_url + '/', 'v1/responses')} model=insika:#{@agent} user=soak-1",
|
|
249
249
|
" out: #{@out}/"
|
|
250
250
|
]
|
|
251
251
|
unless @envelope.calibrated?
|
|
@@ -345,7 +345,7 @@ module Insika
|
|
|
345
345
|
|
|
346
346
|
private
|
|
347
347
|
|
|
348
|
-
def resolve_token(env) = env["
|
|
348
|
+
def resolve_token(env) = env["INSIKA_GATEWAY_TOKEN"] || env["ADMIN_TOKEN"] || "local-demo"
|
|
349
349
|
|
|
350
350
|
def install_traps
|
|
351
351
|
%w[INT TERM].each { |sig| Signal.trap(sig) { @stop_reason = "interrupted" } }
|
|
@@ -489,7 +489,7 @@ module Insika
|
|
|
489
489
|
req["Authorization"] = "Bearer #{@token}"
|
|
490
490
|
req["Content-Type"] = "application/json"
|
|
491
491
|
req["Accept"] = "text/event-stream"
|
|
492
|
-
req.body = JSON.generate(model: "
|
|
492
|
+
req.body = JSON.generate(model: "insika:#{agent}", user: user, stream: true, input: message)
|
|
493
493
|
|
|
494
494
|
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
495
495
|
ttfb = nil
|
|
@@ -69,14 +69,32 @@ module Insika
|
|
|
69
69
|
inject! if @seen >= @expected
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Tail-appends whatever is in the mailbox right now and reports how many. Called
|
|
73
|
+
# at a batch boundary (#inject!) and ONCE MORE by the Executor when the run ended
|
|
74
|
+
# with a message no boundary ever arrived for (a text-only turn closes no batch) —
|
|
75
|
+
# the append, the running count and the `:turn_steered` event belong in one place
|
|
76
|
+
# either way.
|
|
77
|
+
def absorb_pending!
|
|
78
|
+
texts = @actor.take_user_messages!
|
|
79
|
+
return 0 if texts.empty?
|
|
80
|
+
|
|
81
|
+
texts.each { |text| @chat.add_message(role: :user, content: @policy.frame(text)) }
|
|
82
|
+
@injected += texts.size
|
|
83
|
+
# Counts only, never content — the text is already in the transcript, which is the
|
|
84
|
+
# surface that is allowed to carry it. `task_id`/`session_id` are the event's meta.
|
|
85
|
+
@emit.call(:turn_steered, { count: texts.size, total: @injected })
|
|
86
|
+
texts.size
|
|
87
|
+
end
|
|
88
|
+
|
|
72
89
|
private
|
|
73
90
|
|
|
74
91
|
def open_batch(message)
|
|
75
92
|
calls = field(message, :tool_calls)
|
|
76
93
|
size = calls.respond_to?(:size) ? calls.size : 0
|
|
77
94
|
# A message with no tool call is the model talking, not a batch: leave any
|
|
78
|
-
# pending message where it is
|
|
79
|
-
#
|
|
95
|
+
# pending message where it is. The turn is about to end, and the Executor
|
|
96
|
+
# absorbs it there in ONE extra round (#absorb_pending!) — a text-only turn
|
|
97
|
+
# closes no batch, and answering it half-way is not an option.
|
|
80
98
|
return @expected = nil if size.zero?
|
|
81
99
|
|
|
82
100
|
@expected = size
|
|
@@ -88,14 +106,7 @@ module Insika
|
|
|
88
106
|
@expected = nil
|
|
89
107
|
return if @halted # nothing will read it: leave it in the mailbox
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
return if texts.empty?
|
|
93
|
-
|
|
94
|
-
texts.each { |text| @chat.add_message(role: :user, content: @policy.frame(text)) }
|
|
95
|
-
@injected += texts.size
|
|
96
|
-
# Counts only, never content — the text is already in the transcript, which is the
|
|
97
|
-
# surface that is allowed to carry it. `task_id`/`session_id` are the event's meta.
|
|
98
|
-
@emit.call(:turn_steered, { count: texts.size, total: @injected })
|
|
109
|
+
absorb_pending!
|
|
99
110
|
end
|
|
100
111
|
|
|
101
112
|
def halt?(result) = defined?(RubyLLM::Tool::Halt) && result.is_a?(RubyLLM::Tool::Halt)
|