insika 0.0.1 → 0.2.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 +361 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +366 -0
- data/docs/AGENTS.md +618 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +117 -0
- data/docs/DEPLOY.md +354 -0
- data/docs/EMBEDDING.md +198 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +232 -0
- data/docs/OBSERVABILITY.md +374 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +375 -0
- data/docs/SKILLS.md +284 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +255 -0
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +153 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +303 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +58 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +43 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_refinement.rb +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +112 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +60 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +89 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +43 -0
- data/lib/insika/context/priority.rb +30 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +123 -0
- data/lib/insika/context/providers/skill.rb +24 -0
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +539 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +382 -0
- data/lib/insika/dsl/server_boot.rb +98 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +364 -0
- data/lib/insika/edge_limiter.rb +268 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +249 -0
- data/lib/insika/errors.rb +201 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +132 -0
- data/lib/insika/executor.rb +1995 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +115 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +102 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +123 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +143 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +168 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +102 -0
- data/lib/insika/safety/moderator.rb +94 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/app.rb +1022 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +118 -0
- data/lib/insika/server/responses.rb +165 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +220 -0
- data/lib/insika/skill_store.rb +127 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1693 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +70 -0
- data/lib/insika/studio/forms.rb +335 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +137 -0
- data/lib/insika/studio/views/settings.erb +168 -0
- data/lib/insika/studio/views/skills.erb +141 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +219 -0
- data/lib/insika/tools/load_skill.rb +99 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +162 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +249 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +220 -4
- metadata +412 -8
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
# CONFIGURATION DOMAIN store. Unlike the
|
|
7
|
+
# EXECUTION stores (session/task/checkpoint/pending/memory), this holds the
|
|
8
|
+
# configuration the Studio authors at runtime: agents (profiles), general
|
|
9
|
+
# settings, LLM providers and MCP instances. Scoped KV over any
|
|
10
|
+
# `Insika::Store` — durable when the backend is SQLite (survives a restart,
|
|
11
|
+
# like everything else).
|
|
12
|
+
#
|
|
13
|
+
# logical scope -> physical namespace "config:<scope>". Values are
|
|
14
|
+
# JSON-serializable Hashes (symbol becomes string on the round-trip, per the
|
|
15
|
+
# Store contract; the domain re-symbolizes at the edge — see StoredProfileSource).
|
|
16
|
+
class ConfigStore
|
|
17
|
+
SCOPE_PREFIX = "config"
|
|
18
|
+
# agent_files/skills: the CONTENT of per-agent prompts
|
|
19
|
+
# and shared skills lives in the Store (single source of truth,
|
|
20
|
+
# a SQLite backup), not on disk — disk becomes only seed/import.
|
|
21
|
+
# goldens: authored eval cases — config, like the
|
|
22
|
+
# prompts and skills: the corpus on disk is seed and export, the store is what a
|
|
23
|
+
# deployment runs and what the Studio edits.
|
|
24
|
+
# baselines: the ACCEPTED state of each agent's golden set.
|
|
25
|
+
# Config for the same reason: it is a curated decision ("this is the bar"), the
|
|
26
|
+
# file is its export, and the refinement gate reads it from inside a deployment
|
|
27
|
+
# that has no checkout.
|
|
28
|
+
# agent_skills: the per-agent SPECIALIZATIONS of a shared skill (and
|
|
29
|
+
# agent-private skills). A second scope rather than a composite key in `skills`,
|
|
30
|
+
# so the shared records are untouched by the agent dimension arriving — no
|
|
31
|
+
# migration, and a live deployment keeps serving exactly what it served.
|
|
32
|
+
SCOPES = %w[agents settings llm_providers mcp agent_files skills agent_skills
|
|
33
|
+
system_files tools goldens baselines].freeze
|
|
34
|
+
|
|
35
|
+
class UnknownScope < Insika::Error; end
|
|
36
|
+
|
|
37
|
+
def initialize(store:)
|
|
38
|
+
@store = store
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Upsert (last-write-wins). -> value (the same Hash that was passed)
|
|
42
|
+
def put(scope, key, value)
|
|
43
|
+
@store.set(ns(scope), key.to_s, stringify(value))
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# -> Hash | nil
|
|
48
|
+
def get(scope, key)
|
|
49
|
+
@store.get(ns(scope), key.to_s)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# -> bool (did it exist?)
|
|
53
|
+
def delete(scope, key)
|
|
54
|
+
@store.delete(ns(scope), key.to_s)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# -> [String] keys of the scope, sorted lexicographically
|
|
58
|
+
def keys(scope)
|
|
59
|
+
@store.list(ns(scope))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# -> [Hash] all records of the scope (lexicographic key order)
|
|
63
|
+
def all(scope)
|
|
64
|
+
s = ns(scope)
|
|
65
|
+
@store.list(s).filter_map { |k| @store.get(s, k) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def ns(scope)
|
|
71
|
+
key = scope.to_s
|
|
72
|
+
raise UnknownScope, "unknown config scope: #{scope.inspect}" unless SCOPES.include?(key)
|
|
73
|
+
|
|
74
|
+
"#{SCOPE_PREFIX}:#{key}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Normalizes symbol->string BEFORE writing (mirrors MemoryStore#stringify):
|
|
78
|
+
# the backend serializes JSON, and reading back would return strings anyway;
|
|
79
|
+
# normalizing on write keeps the record consistent across backends.
|
|
80
|
+
def stringify(obj)
|
|
81
|
+
case obj
|
|
82
|
+
when Hash then obj.each_with_object({}) { |(k, v), acc| acc[k.to_s] = stringify(v) }
|
|
83
|
+
when Array then obj.map { |v| stringify(v) }
|
|
84
|
+
when Symbol then obj.to_s
|
|
85
|
+
else obj
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Insika
|
|
7
|
+
# Builder output, consumed by the Executor in stage 5.
|
|
8
|
+
# system: String (final concatenation for with_instructions)
|
|
9
|
+
# history: [{role:, content:}] (for seeding the chat)
|
|
10
|
+
# tool_context: String | nil
|
|
11
|
+
# fragments: [ContextFragment] post-cut, in canonical order (audit)
|
|
12
|
+
# budget: { cap:, used:, evicted: [source] }
|
|
13
|
+
ContextPackage = Data.define(:system, :history, :tool_context, :fragments, :budget)
|
|
14
|
+
|
|
15
|
+
# Stage 2 of the pipeline: the Runtime NEVER builds the prompt — it asks the
|
|
16
|
+
# Builder for the package. Implements selection -> fan-out production ->
|
|
17
|
+
# collection/estimation -> budgeting with eviction -> canonical assembly.
|
|
18
|
+
class ContextBuilder
|
|
19
|
+
def initialize(providers:, event_stream:, hooks: Hooks.new, estimator: TokenEstimator)
|
|
20
|
+
@providers = providers
|
|
21
|
+
@event_stream = event_stream
|
|
22
|
+
@hooks = hooks # prompt pair; empty Hooks = no-op
|
|
23
|
+
@estimator = estimator
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The :prompt pair is wrapped HERE, not in the Executor: before_prompt
|
|
27
|
+
# can rewrite the ContextRequest (providers run with the modified one);
|
|
28
|
+
# after_prompt can rewrite the assembled ContextPackage. IMPORTANT: the
|
|
29
|
+
# Executor calls only `builder.call(request)` — do NOT wrap it with
|
|
30
|
+
# around(:prompt) again (double-wrapping would fire the hooks twice).
|
|
31
|
+
def call(request)
|
|
32
|
+
@hooks.around(:prompt, request) { |req| build_package(req) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def build_package(request)
|
|
38
|
+
selected = select_providers(request.profile)
|
|
39
|
+
fragments = estimate_tokens(produce(selected, request))
|
|
40
|
+
cap = request.profile.limits[:context_budget] || 8_000
|
|
41
|
+
fragments, evicted = apply_budget(fragments, cap)
|
|
42
|
+
unless evicted.empty?
|
|
43
|
+
emit_warning("ContextBuilder",
|
|
44
|
+
"budget: #{evicted.size} fragment(s) evicted from #{evicted.uniq}",
|
|
45
|
+
request)
|
|
46
|
+
end
|
|
47
|
+
assemble(fragments, cap, evicted)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Step 1: selection — enabled_for? AND the profile allowlist.
|
|
51
|
+
def select_providers(profile)
|
|
52
|
+
@providers.select do |p|
|
|
53
|
+
p.enabled_for?(profile) && Allowlist.allows?(profile.context_providers, p.id)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Step 2: fan-out production with a BARRIER and a per-provider timeout
|
|
58
|
+
# (with_timeout — never Timeout.timeout). Each provider is a CHILD fiber
|
|
59
|
+
# of the current fiber: cancelling the task cancels the in-flight production.
|
|
60
|
+
def produce(selected, request)
|
|
61
|
+
timeout = request.profile.limits[:provider_timeout] || 5
|
|
62
|
+
tasks = selected.map do |provider|
|
|
63
|
+
child = Async::Task.current.async { |t| t.with_timeout(timeout) { provider.call(request) } }
|
|
64
|
+
[provider, child]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
fragments = []
|
|
68
|
+
tasks.each do |provider, child|
|
|
69
|
+
fragments.concat(Array(child.wait))
|
|
70
|
+
rescue StandardError => e # Async::TimeoutError is a StandardError; Async::Stop is NOT (propagates)
|
|
71
|
+
handle_provider_failure(provider, e, request)
|
|
72
|
+
end
|
|
73
|
+
fragments
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A required provider failing -> ContextError
|
|
77
|
+
# (aborts the turn, mapped by the Executor); an optional one failing ->
|
|
78
|
+
# warning + graceful degradation (fragments omitted, turn proceeds).
|
|
79
|
+
def handle_provider_failure(provider, error, request)
|
|
80
|
+
if provider.required?
|
|
81
|
+
raise ContextError.new("required provider '#{provider.id}' failed: #{error.message}",
|
|
82
|
+
provider: provider.id)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
emit_warning(provider.id, error.message, request)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Step 3: estimate tokens only when the provider did not report them.
|
|
89
|
+
def estimate_tokens(fragments)
|
|
90
|
+
fragments.map { |f| f.tokens ? f : f.with(tokens: @estimator.estimate(estimable_text(f.content))) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# History fragments carry a Hash {role:, content:} as their content;
|
|
94
|
+
# estimating over the Hash would count the `#to_s` text (":role=>", quotes,
|
|
95
|
+
# symbols), inflating each message and biasing the eviction. Count the values,
|
|
96
|
+
# not the Ruby representation.
|
|
97
|
+
def estimable_text(content)
|
|
98
|
+
case content
|
|
99
|
+
when String then content
|
|
100
|
+
# An eviction unit (R1): a cycle of message Hashes -> sum their text.
|
|
101
|
+
when Array then content.map { |c| estimable_text(c) }.join(" ")
|
|
102
|
+
when Hash then content.values.map(&:to_s).join(" ")
|
|
103
|
+
else content.to_s
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Steps 4-5: GLOBAL budget. Cuts non-pinned fragments from lowest priority to
|
|
108
|
+
# highest; ties -> lowest production index first (stable cut: among
|
|
109
|
+
# histories, the oldest drops first). pinned is uncuttable; if pinned alone
|
|
110
|
+
# already exceeds -> ContextError (do not truncate identity).
|
|
111
|
+
def apply_budget(fragments, cap)
|
|
112
|
+
used = fragments.sum(&:tokens)
|
|
113
|
+
return [fragments, []] if used <= cap
|
|
114
|
+
|
|
115
|
+
indexed = fragments.each_with_index.to_a
|
|
116
|
+
cuttable = indexed.reject { |f, _i| f.pinned }.sort_by { |f, i| [f.priority, i] }
|
|
117
|
+
evicted_idx = []
|
|
118
|
+
evicted_sources = []
|
|
119
|
+
cuttable.each do |fragment, index|
|
|
120
|
+
break if used <= cap
|
|
121
|
+
|
|
122
|
+
used -= fragment.tokens
|
|
123
|
+
evicted_sources << fragment.source
|
|
124
|
+
evicted_idx << index
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if used > cap
|
|
128
|
+
raise ContextError.new(
|
|
129
|
+
"unsolvable budget: pinned fragments (#{used} tokens) exceed the cap (#{cap})",
|
|
130
|
+
provider: "ContextBuilder"
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
survivors = fragments.each_index.reject { |i| evicted_idx.include?(i) }.map { |i| fragments[i] }
|
|
135
|
+
[survivors, evicted_sources]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Step 6: assembly in DETERMINISTIC canonical order.
|
|
139
|
+
def assemble(fragments, cap, evicted)
|
|
140
|
+
system_frags = fragments.select { |f| f.placement == :system }
|
|
141
|
+
.sort_by.with_index { |f, i| [-f.priority, f.source.to_s, i] }
|
|
142
|
+
history_frags = fragments.select { |f| f.placement == :history } # production order (chronological)
|
|
143
|
+
tool_frags = fragments.select { |f| f.placement == :tool_context }
|
|
144
|
+
|
|
145
|
+
system = system_frags.map(&:content).join("\n\n")
|
|
146
|
+
history = history_frags.map(&:content)
|
|
147
|
+
tool_context = tool_frags.empty? ? nil : tool_frags.map(&:content).join("\n\n")
|
|
148
|
+
|
|
149
|
+
canonical = system_frags + history_frags + tool_frags
|
|
150
|
+
ContextPackage.new(
|
|
151
|
+
system: system, history: history, tool_context: tool_context,
|
|
152
|
+
fragments: canonical, budget: { cap: cap, used: canonical.sum(&:tokens), evicted: evicted }
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# :provider_warning. The Builder does not know task_id/seq (correlation is
|
|
157
|
+
# the Executor's job) — emits with what it has; Event#to_h does meta.compact.
|
|
158
|
+
def emit_warning(provider_id, message, request)
|
|
159
|
+
@event_stream.emit(Insika::Event.new(
|
|
160
|
+
type: :provider_warning,
|
|
161
|
+
data: { provider: provider_id, message: message },
|
|
162
|
+
meta: { session_id: request.session&.id, at: Time.now.utc.iso8601 }
|
|
163
|
+
))
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Context
|
|
5
|
+
# Base for providers that inject level 1 of a catalog (skills, deferred
|
|
6
|
+
# tools) as a :system fragment. The subclass says WHICH entries and the
|
|
7
|
+
# priority; the fragment assembly (format -> skip if empty -> a single
|
|
8
|
+
# fragment with source = the provider id) is shared.
|
|
9
|
+
class CatalogProvider < ContextProvider
|
|
10
|
+
def initialize(catalog:)
|
|
11
|
+
@catalog = catalog
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(request)
|
|
15
|
+
block = @catalog.format_for_prompt(entries(request))
|
|
16
|
+
return [] if block.empty?
|
|
17
|
+
|
|
18
|
+
[ContextFragment.build(content: block, placement: :system,
|
|
19
|
+
priority: priority, source: id)]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Unit of context produced by a provider.
|
|
5
|
+
# SHARED type (Insika::, not Insika::Context::).
|
|
6
|
+
# placement: :system | :history | :tool_context
|
|
7
|
+
# priority: Integer; higher = more important (survives cuts)
|
|
8
|
+
# tokens: Integer | nil; estimated by the Builder when nil
|
|
9
|
+
# source: String — provider id (audit)
|
|
10
|
+
# pinned: true -> uncuttable in the budget (e.g. identity)
|
|
11
|
+
# labels: [{ "name" =>, "reason" => }] — WHAT this fragment carries and WHY,
|
|
12
|
+
# as ids. Content-FREE by contract, so the context trace can report
|
|
13
|
+
# which skills a turn injected without storing a byte of the bodies.
|
|
14
|
+
# [] = nothing to name (the default for every provider that has no
|
|
15
|
+
# natural id, e.g. the identity prompt).
|
|
16
|
+
#
|
|
17
|
+
# The REASON is the point. A name alone answers "was something
|
|
18
|
+
# injected"; the operator's actual question is "which skill did I
|
|
19
|
+
# trigger, and why is it here" — `eager` (the agent always wants it),
|
|
20
|
+
# `trigger:<matched phrase>` (this message asked for it), or absent
|
|
21
|
+
# for a body a plugin supplied. String keys because these labels are
|
|
22
|
+
# written to the context trace and to events as JSON: the round-trip
|
|
23
|
+
# is then the identity, and no reader has to defend against both.
|
|
24
|
+
ContextFragment = Data.define(:content, :placement, :priority, :tokens,
|
|
25
|
+
:source, :pinned, :labels) do
|
|
26
|
+
def self.build(content:, placement:, source:, priority: 50, tokens: nil,
|
|
27
|
+
pinned: false, labels: [])
|
|
28
|
+
new(content: content, placement: placement, priority: priority,
|
|
29
|
+
tokens: tokens, source: source, pinned: pinned,
|
|
30
|
+
labels: Array(labels).map { |l| label(l) })
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A bare String is still a valid label (a provider that has an id but no reason
|
|
34
|
+
# to give) — it normalizes to a reason-less entry rather than being rejected.
|
|
35
|
+
def self.label(raw)
|
|
36
|
+
return { "name" => raw.to_s }.freeze unless raw.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
name = (raw[:name] || raw["name"]).to_s
|
|
39
|
+
reason = raw[:reason] || raw["reason"]
|
|
40
|
+
{ "name" => name, "reason" => reason&.to_s }.compact.freeze
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Context
|
|
5
|
+
# Precedence ladder for context fragments — SINGLE SOURCE of the order
|
|
6
|
+
# (trust boundary). Higher = more authority: appears earlier
|
|
7
|
+
# in the system prompt and survives budget cuts. Providers reference these
|
|
8
|
+
# constants instead of loose numbers, so the boundary is an auditable
|
|
9
|
+
# contract in a single place (trust_boundary_spec locks the order).
|
|
10
|
+
#
|
|
11
|
+
# Contract: identity and guardrails go PINNED at the top; TURN
|
|
12
|
+
# injections (request_context — the consumer's tenant/vars) sit at the
|
|
13
|
+
# BOTTOM and are sacrificed FIRST under budget. Identity (pinned) is NEVER
|
|
14
|
+
# truncated. A prompt injection riding in via turn data is DATA, not
|
|
15
|
+
# authority — it does not override IDENTITY/SOUL, and security decisions
|
|
16
|
+
# (tool allow/deny, egress, approvals) live in the engine/profile, never in
|
|
17
|
+
# the injected block.
|
|
18
|
+
module Priority
|
|
19
|
+
IDENTITY = 100 # IDENTITY/SOUL (Prompt) — pinned
|
|
20
|
+
PROMPT_REF = 90 # Prompt Catalog guardrails/refs (Prompt) — pinned
|
|
21
|
+
SKILL_BODY = 85 # <active_skill> trigger-matched body (SkillTrigger)
|
|
22
|
+
SKILL = 80 # <available_skills> level 1 (Skill)
|
|
23
|
+
MEMORY = 75 # <memory> read path (Memory)
|
|
24
|
+
TOOL_SEARCH = 70 # <available_tools> level 1 (ToolSearch)
|
|
25
|
+
HISTORY_MAX = 79 # history ceiling by recency (Session)
|
|
26
|
+
HISTORY_BASE = 60 # history base; +idx up to the ceiling (Session)
|
|
27
|
+
REQUEST = 40 # <request_context> — turn injection, the most cuttable
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Base class for a context provider. Concrete
|
|
5
|
+
# subclasses live in Insika::Context::Providers. The base is
|
|
6
|
+
# deliberately minimal: `required?` lives on the provider, not the wiring.
|
|
7
|
+
class ContextProvider
|
|
8
|
+
def id = self.class.name # override for a stable name
|
|
9
|
+
def required? = false # true -> failure aborts the turn
|
|
10
|
+
def enabled_for?(_profile) = true
|
|
11
|
+
def call(_request) = [] # -> [ContextFragment]; may do IO
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Input for the provider contract.
|
|
15
|
+
# session: SessionStore::Session | nil
|
|
16
|
+
# checkpoint: Checkpoint | nil (present on ResumeTask — history comes from it)
|
|
17
|
+
ContextRequest = Data.define(:session, :message, :profile, :tenant, :vars,
|
|
18
|
+
:checkpoint)
|
|
19
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Context
|
|
5
|
+
module Providers
|
|
6
|
+
# Read path for cross-session memory. Thin adapter over
|
|
7
|
+
# the MemoryStore — same pattern as the Skill/ToolSearch provider: one
|
|
8
|
+
# `:system` fragment with the tenant's facts + recent notes. Deterministic
|
|
9
|
+
# (no embeddings/ranking).
|
|
10
|
+
class Memory < ContextProvider
|
|
11
|
+
def initialize(store:, notes_limit: 10)
|
|
12
|
+
@store = store
|
|
13
|
+
@notes_limit = notes_limit
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Per-agent opt-in. The Builder still applies the `context_providers`
|
|
17
|
+
# allowlist on top (two gates, like the other providers).
|
|
18
|
+
def enabled_for?(profile) = !!profile.memory
|
|
19
|
+
|
|
20
|
+
# required? == false (default): a failure (store unavailable) becomes a
|
|
21
|
+
# :provider_warning + graceful degradation — never aborts the turn.
|
|
22
|
+
def call(request)
|
|
23
|
+
tenant = memory_tenant(request)
|
|
24
|
+
facts = @store.facts(tenant: tenant)
|
|
25
|
+
notes = @store.notes(tenant: tenant, limit: @notes_limit)
|
|
26
|
+
return [] if facts.empty? && notes.empty?
|
|
27
|
+
|
|
28
|
+
# priority MEMORY (75): between skills (80) and deferred tools (70) in
|
|
29
|
+
# the sacrifice order. pinned false (cuttable under a tight budget).
|
|
30
|
+
[ContextFragment.build(content: format_block(facts, notes),
|
|
31
|
+
placement: :system, priority: Context::Priority::MEMORY, source: id)]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Engine memory scope: an EXPLICIT tenant from the Command wins
|
|
37
|
+
# (multi-merchant override); otherwise the SESSION (=chat) — engine-owner
|
|
38
|
+
# memory is per-chat. No session (one-shot) and no tenant -> nil (MemoryStore
|
|
39
|
+
# applies _default). Symmetric to the write path (`state.tenant` in the Executor).
|
|
40
|
+
def memory_tenant(request)
|
|
41
|
+
explicit = request.respond_to?(:tenant) ? request.tenant : nil
|
|
42
|
+
return explicit if explicit
|
|
43
|
+
|
|
44
|
+
request.respond_to?(:session) ? request.session&.id : nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Passive <memory> (no instruction — the HOW of writing lives in the `remember` tool).
|
|
48
|
+
def format_block(facts, notes)
|
|
49
|
+
lines = facts.map { |f| %( <fact key="#{f.key}">#{f.value}</fact>) }
|
|
50
|
+
lines += notes.map { |n| " <note>#{n.text}</note>" }
|
|
51
|
+
<<~BLOCK.strip
|
|
52
|
+
<memory>
|
|
53
|
+
#{lines.join("\n")}
|
|
54
|
+
</memory>
|
|
55
|
+
BLOCK
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Context
|
|
5
|
+
module Providers
|
|
6
|
+
# Absorbs SystemPrompt + SOUL.md. Identity is
|
|
7
|
+
# PINNED, priority 100 — never cut. required?: an agent without identity is
|
|
8
|
+
# a WRONG agent, not a degraded one. prompt_refs: priority 90 pinned
|
|
9
|
+
# fragments, from the PromptCatalog (catalog defaults to nil).
|
|
10
|
+
#
|
|
11
|
+
# PER-AGENT identity. `profile.prompt_files` (file names)
|
|
12
|
+
# wins over the wiring's `files:` — this fixes the limitation of a new
|
|
13
|
+
# agent inheriting Bia's prompt. The content comes from `agent_files`
|
|
14
|
+
# (AgentFileStore, lives in the Store), with a File.read fallback
|
|
15
|
+
# for on-disk paths (compat/seed). Without prompt_files -> uses the wiring's
|
|
16
|
+
# `files:` (deployment default; byte-for-byte parity).
|
|
17
|
+
class Prompt < ContextProvider
|
|
18
|
+
def initialize(base: "", files: [], catalog: nil, agent_files: nil, system_files: nil)
|
|
19
|
+
@base = base
|
|
20
|
+
@files = Array(files)
|
|
21
|
+
@catalog = catalog
|
|
22
|
+
@agent_files = agent_files
|
|
23
|
+
@system_files = system_files
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def required? = true
|
|
27
|
+
|
|
28
|
+
def call(request)
|
|
29
|
+
fragments = []
|
|
30
|
+
identity = build_identity(request.profile)
|
|
31
|
+
unless identity.empty?
|
|
32
|
+
fragments << ContextFragment.build(content: identity, placement: :system,
|
|
33
|
+
priority: Context::Priority::IDENTITY, source: id, pinned: true)
|
|
34
|
+
end
|
|
35
|
+
fragments.concat(ref_fragments(request.profile))
|
|
36
|
+
fragments
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Migrates the SystemPrompt#build concatenation INTACT (without skills_block).
|
|
42
|
+
# A SINGLE fragment preserves the internal base->files order (sorting
|
|
43
|
+
# acts only BETWEEN fragments) and guarantees byte-for-byte parity.
|
|
44
|
+
#
|
|
45
|
+
# profile.prompt_files (names) wins over @files (wiring): an agent with
|
|
46
|
+
# its own identity does not inherit the deployment's. Each source resolves
|
|
47
|
+
# via AgentFileStore (per agent) OR File.read (on-disk path) — in that
|
|
48
|
+
# order. Without prompt_files, falls back to the wiring's @files.
|
|
49
|
+
def build_identity(profile)
|
|
50
|
+
parts = [@base]
|
|
51
|
+
parts.concat(system_parts) # global system files, for EVERY agent
|
|
52
|
+
# The agent's OWN inline identity (`base_prompt` — what the DSL's
|
|
53
|
+
# `instructions` and a pack's manifest set). It was stored, round-tripped
|
|
54
|
+
# and advertised on the A2A agent card while never reaching the model:
|
|
55
|
+
# every root wires `base: ""`, so an agent whose identity was inline had
|
|
56
|
+
# NO identity at all, and a chatty model answered plausibly enough to hide
|
|
57
|
+
# it. Additive to prompt_files, not exclusive: an agent may carry both.
|
|
58
|
+
parts << profile&.base_prompt.to_s
|
|
59
|
+
sources = Array(profile&.prompt_files)
|
|
60
|
+
if sources.empty?
|
|
61
|
+
@files.each { |f| parts << File.read(f, encoding: "UTF-8") if File.exist?(f) }
|
|
62
|
+
else
|
|
63
|
+
sources.each { |src| parts << read_source(profile&.id, src.to_s) }
|
|
64
|
+
end
|
|
65
|
+
parts.reject { |p| p.nil? || p.strip.empty? }.join("\n\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# GLOBAL system files: apply to every agent,
|
|
69
|
+
# injected BEFORE the individual identity. Empty/absent store -> [] ->
|
|
70
|
+
# a prompt byte-for-byte identical to before (the injection only exists if
|
|
71
|
+
# the operator authored something). Lexicographic order (SystemFileStore#list).
|
|
72
|
+
def system_parts
|
|
73
|
+
return [] unless @system_files
|
|
74
|
+
|
|
75
|
+
@system_files.list.map { |name| @system_files.read(name).to_s }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Per-agent store first, disk second (compat/seed).
|
|
79
|
+
def read_source(agent_id, src)
|
|
80
|
+
stored = agent_id && @agent_files&.read(agent_id, src)
|
|
81
|
+
return stored if stored
|
|
82
|
+
return File.read(src, encoding: "UTF-8") if File.exist?(src)
|
|
83
|
+
|
|
84
|
+
""
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ref_fragments(profile)
|
|
88
|
+
refs = Array(profile.prompt_refs)
|
|
89
|
+
return [] if refs.empty?
|
|
90
|
+
|
|
91
|
+
refs.map do |name|
|
|
92
|
+
entry = @catalog&.find(name.to_s)
|
|
93
|
+
unless entry
|
|
94
|
+
raise ContextError.new("prompt_ref '#{name}' not found in the Prompt Catalog",
|
|
95
|
+
provider: id)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
ContextFragment.build(content: entry.body, placement: :system,
|
|
99
|
+
priority: Context::Priority::PROMPT_REF, source: id, pinned: true)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Context
|
|
5
|
+
module Providers
|
|
6
|
+
# :system fragment with turn metadata (tenant, relevant vars).
|
|
7
|
+
# Nothing if there is no metadata. priority 40. Not required?
|
|
8
|
+
# (metadata may degrade).
|
|
9
|
+
class Request < ContextProvider
|
|
10
|
+
def call(request)
|
|
11
|
+
lines = []
|
|
12
|
+
lines << "tenant: #{request.tenant}" if request.tenant
|
|
13
|
+
# "history" is transcript (consumed by the Session provider), not turn
|
|
14
|
+
# metadata — it does not leak into the system's request_context. Keys
|
|
15
|
+
# prefixed with "__" are INTERNAL slots (e.g. the per-chat model pin
|
|
16
|
+
# "__llm__") — reserved, never rendered to the model.
|
|
17
|
+
request.vars.to_h.each do |k, v|
|
|
18
|
+
next if k.to_s == "history" || k.to_s.start_with?("__")
|
|
19
|
+
|
|
20
|
+
lines << "#{k}: #{v}"
|
|
21
|
+
end
|
|
22
|
+
return [] if lines.empty?
|
|
23
|
+
|
|
24
|
+
[ContextFragment.build(
|
|
25
|
+
content: "<request_context>\n#{lines.join("\n")}\n</request_context>",
|
|
26
|
+
placement: :system, priority: Context::Priority::REQUEST, source: id
|
|
27
|
+
)]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|