insika 0.3.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +180 -0
- data/README.md +45 -10
- data/bin/insika +684 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +94 -403
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +9 -7
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +71 -3
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +128 -3
- data/docs/FACTS.md +3 -3
- data/docs/HARVEST.md +5 -6
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +2 -2
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +15 -10
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +3 -3
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +22 -6
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +2 -2
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +152 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +4 -4
- data/docs/improve.md +20 -0
- data/docs/index.md +8 -5
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +73 -16
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/channel_delivery.rb +1 -1
- data/lib/insika/chat_builder.rb +22 -2
- data/lib/insika/commands/agent_payload.rb +2 -2
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +15 -3
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/context/priority.rb +2 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/doctor.rb +176 -8
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +60 -79
- data/lib/insika/dsl/server_boot.rb +23 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +103 -2
- data/lib/insika/env_schema.rb +16 -1
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +11 -0
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +231 -55
- data/lib/insika/followup_policy.rb +2 -25
- data/lib/insika/golden_store.rb +16 -1
- data/lib/insika/grounding/matcher.rb +1 -1
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +115 -31
- data/lib/insika/message_origin.rb +1 -1
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/onboarding.rb +17 -1
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +2 -2
- data/lib/insika/profile_source.rb +8 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/retention.rb +36 -1
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/server/app.rb +105 -15
- data/lib/insika/server/rack_app.rb +5 -1
- data/lib/insika/server/responses.rb +1 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +567 -45
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +21 -21
- data/lib/insika/studio/forms.rb +46 -5
- data/lib/insika/studio/nav_icons.rb +14 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +61 -820
- data/lib/insika/studio/views/agents.erb +70 -57
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/evals.erb +2 -2
- data/lib/insika/studio/views/facts.erb +1 -1
- data/lib/insika/studio/views/funnel.erb +1 -1
- data/lib/insika/studio/views/home.erb +106 -67
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +14 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/session.erb +231 -177
- data/lib/insika/studio/views/settings.erb +39 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +38 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +58 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tools/generate_image.rb +52 -7
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +15 -4
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +184 -12
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +57 -0
- metadata +105 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
data/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
|
)
|
|
@@ -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/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.
|
|
@@ -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)
|