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,382 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require_relative "workflow_adapter"
|
|
5
|
+
|
|
6
|
+
module Insika
|
|
7
|
+
module DSL
|
|
8
|
+
# The live graph behind a Definition: it assembles the SAME authoring graph a
|
|
9
|
+
# concrete deployment does (Insika::Wiring::Graph + the runtime-authoring
|
|
10
|
+
# command surface), configures RubyLLM, and imports the definition's pack via
|
|
11
|
+
# the standard PackImporter. From there the agent is indistinguishable from one
|
|
12
|
+
# provisioned by any other pack — chat and serve just drive the normal path.
|
|
13
|
+
#
|
|
14
|
+
# Built lazily by Definition#runtime, so `require "insika"` never pays for
|
|
15
|
+
# ruby_llm or the HTTP server.
|
|
16
|
+
class Runtime
|
|
17
|
+
TERMINAL = %i[task_completed task_failed task_cancelled].freeze
|
|
18
|
+
|
|
19
|
+
# graph: the assembled Wiring::Graph::Result; pack: the PRIMARY imported
|
|
20
|
+
# Pack; packs: every imported Pack (one per agent — a System has N, a
|
|
21
|
+
# Definition has one). All read by the server boot and the specs.
|
|
22
|
+
attr_reader :graph, :pack, :packs
|
|
23
|
+
|
|
24
|
+
# `definition` is a Definition (one agent) or a System (N agents). Both
|
|
25
|
+
# answer #pack/#id; a System also answers #packs. Duck-typed on purpose:
|
|
26
|
+
# the runtime does not care how many agents it hosts, only that each one
|
|
27
|
+
# arrives as an ordinary Pack.
|
|
28
|
+
#
|
|
29
|
+
# `backend`: the store this graph owns. nil = the historic
|
|
30
|
+
# path, `INSIKA_DB` or memory — ENV is a default, never a requirement (the
|
|
31
|
+
# embed contract). An injected backend wins and is never widened
|
|
32
|
+
# back to the environment.
|
|
33
|
+
def initialize(definition, backend: nil)
|
|
34
|
+
@definition = definition
|
|
35
|
+
@injected_backend = backend
|
|
36
|
+
@packs = (definition.respond_to?(:packs) ? Array(definition.packs) : [definition.pack]).freeze
|
|
37
|
+
@pack = @packs.first
|
|
38
|
+
# the graph's OWN RubyLLM config, built before the graph so
|
|
39
|
+
# every collaborator that talks to a provider is handed it at wiring time
|
|
40
|
+
# rather than reading a process-wide singleton at call time.
|
|
41
|
+
@llm = build_llm_context
|
|
42
|
+
@graph = build_graph
|
|
43
|
+
seed_default_model
|
|
44
|
+
import_packs
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The graph's RubyLLM context — an isolated config dup that
|
|
48
|
+
# answers #chat. nil only under the test stub, where the fallback is the
|
|
49
|
+
# RubyLLM constant itself. Read by the specs and by anything a host wires
|
|
50
|
+
# alongside this graph.
|
|
51
|
+
attr_reader :llm
|
|
52
|
+
|
|
53
|
+
# An imported profile, read back from the store (config-over-code
|
|
54
|
+
# round-trip). Defaults to the primary agent.
|
|
55
|
+
def profile(agent_id = nil)
|
|
56
|
+
id = (agent_id || @definition.id).to_s
|
|
57
|
+
@graph.profiles.fetch(id) ||
|
|
58
|
+
(raise Insika::Error, "agent '#{id}' was not imported")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# One turn, in-process → the assistant's text. session_id nil = stateless
|
|
62
|
+
# one-shot; a value threads multi-turn memory (created on first use).
|
|
63
|
+
# `agent:` targets one agent of a system (default: the primary).
|
|
64
|
+
def chat(message, session_id: nil, timeout: nil, agent: nil)
|
|
65
|
+
command = Insika::Command.build(
|
|
66
|
+
:send_message,
|
|
67
|
+
{ agent: (agent || @definition.id).to_s, message: message, session_id: session_id },
|
|
68
|
+
transport: :cli
|
|
69
|
+
)
|
|
70
|
+
run_command(command, session_id: session_id, timeout: timeout)[:text]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Triggers a declared workflow and returns its OUTPUT (the typed value the
|
|
74
|
+
# workflow returned), not the turn text. A schema violation on the input
|
|
75
|
+
# raises here, synchronously, with no run created — same contract as
|
|
76
|
+
# POST /v1/workflows/:name.
|
|
77
|
+
def run_workflow(name, input:, agent:, timeout: nil)
|
|
78
|
+
command = Insika::Command.build(
|
|
79
|
+
:trigger_workflow, { workflow: name, agent: agent, input: input }, transport: :cli
|
|
80
|
+
)
|
|
81
|
+
outcome = run_command(command, session_id: nil, timeout: timeout)
|
|
82
|
+
outcome.key?(:output) ? outcome[:output] : outcome[:text]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Boot the control UI (/studio) + drop-in API (/v1). Blocks on the reactor.
|
|
86
|
+
def serve(port:, host:, token:, **_opts)
|
|
87
|
+
require_relative "server_boot"
|
|
88
|
+
ServerBoot.new(self, port: port, host: host, token: token).run
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A named collaborator (settings_store, tool_store, …) the server boot reads.
|
|
92
|
+
def component(name) = @components.fetch(name)
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Dispatch + drain, shared by a turn and a workflow run: subscribe BEFORE
|
|
97
|
+
# dispatching (the fiber may emit eagerly), bind to the task, read to the
|
|
98
|
+
# terminal event. A synchronous handler error (unknown agent/workflow, bad
|
|
99
|
+
# input against the schema) propagates from `dispatch` untouched — the
|
|
100
|
+
# caller sees the real ValidationError/NotFoundError, not a turn failure.
|
|
101
|
+
def run_command(command, session_id:, timeout:)
|
|
102
|
+
require "async"
|
|
103
|
+
outcome = {}
|
|
104
|
+
rejected = nil
|
|
105
|
+
Async do |task|
|
|
106
|
+
serving = !session_id.nil?
|
|
107
|
+
@graph.executor.supervised = true if serving
|
|
108
|
+
ensure_session(session_id) if session_id
|
|
109
|
+
sub = @graph.event_stream.subscribe
|
|
110
|
+
res = begin
|
|
111
|
+
@graph.bus.dispatch(command)
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
# CAPTURED, not re-raised inside the reactor: letting it escape the
|
|
114
|
+
# Async block logs "Task may have ended with unhandled exception" —
|
|
115
|
+
# alarming noise for a DOCUMENTED rejection (a schema violation, an
|
|
116
|
+
# unknown agent). Re-raised verbatim below, outside the reactor.
|
|
117
|
+
sub.close
|
|
118
|
+
rejected = e
|
|
119
|
+
next
|
|
120
|
+
end
|
|
121
|
+
sub.bind(task_id: res[:task_id])
|
|
122
|
+
drain(sub, outcome, task, timeout)
|
|
123
|
+
teardown_serving if serving
|
|
124
|
+
end.wait
|
|
125
|
+
raise rejected if rejected
|
|
126
|
+
raise Insika::Error, "turn #{outcome[:error]}" if outcome[:error]
|
|
127
|
+
|
|
128
|
+
outcome
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Reads the stream until this turn reaches a terminal event, then stops.
|
|
132
|
+
# A workflow run also carries its typed OUTPUT, which is not the turn text.
|
|
133
|
+
def drain(sub, outcome, task, timeout)
|
|
134
|
+
reader = task.async do
|
|
135
|
+
sub.each do |ev|
|
|
136
|
+
case ev.type
|
|
137
|
+
when :task_completed then outcome[:text] = ev.data[:content].to_s
|
|
138
|
+
when :workflow_completed then outcome[:output] = ev.data[:output]
|
|
139
|
+
when :task_failed then outcome[:error] = "failed: #{ev.data[:message] || ev.data[:error]}"
|
|
140
|
+
when :task_cancelled then outcome[:error] = "cancelled"
|
|
141
|
+
end
|
|
142
|
+
sub.close if TERMINAL.include?(ev.type)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
timeout ? task.with_timeout(timeout) { reader.wait } : reader.wait
|
|
146
|
+
rescue Async::TimeoutError
|
|
147
|
+
outcome[:error] = "timed out after #{timeout}s"
|
|
148
|
+
sub.close
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def ensure_session(session_id)
|
|
152
|
+
@graph.session_store.find(session_id) || @graph.session_store.create(id: session_id, vars: {})
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def teardown_serving
|
|
156
|
+
@graph.executor.stop_session_actors
|
|
157
|
+
@graph.executor.instance_variable_get(:@supervisor)&.stop
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# --- graph assembly (mirrors config/deployment.rb, generically) ------
|
|
161
|
+
|
|
162
|
+
def build_graph
|
|
163
|
+
backend = @injected_backend || Insika::Wiring::Graph.backend_from_env
|
|
164
|
+
# :workflow_allowlist joins the builtins here for the same reason the
|
|
165
|
+
# minimal wiring registers it: this root CAN expose workflows. An agent
|
|
166
|
+
# that never names one keeps `workflows_allow` nil = all (parity).
|
|
167
|
+
spine = Insika::Wiring::Graph.spine(
|
|
168
|
+
backend: backend,
|
|
169
|
+
extra_policy_builtins: { workflow_allowlist: Insika::Policy::Builtin::WorkflowAllowlist }
|
|
170
|
+
)
|
|
171
|
+
c = build_components(backend, spine)
|
|
172
|
+
@components = c
|
|
173
|
+
|
|
174
|
+
graph = Insika::Wiring::Graph.build(
|
|
175
|
+
spine: spine, profiles: c[:profile_source],
|
|
176
|
+
tool_registry: c[:tool_registry], tool_catalog: c[:tool_catalog],
|
|
177
|
+
skill_catalog: c[:skill_catalog], prompt_catalog: c[:prompt_catalog],
|
|
178
|
+
guardrails: c[:guardrails], context_providers: context_providers(spine, c),
|
|
179
|
+
edge_limiter: c[:edge_limiter],
|
|
180
|
+
executor_extra: { settings_store: c[:settings_store], tool_trace_store: c[:tool_trace_store],
|
|
181
|
+
context_trace_store: c[:context_trace_store], llm: @llm }
|
|
182
|
+
)
|
|
183
|
+
register_authoring_commands(graph, c)
|
|
184
|
+
register_workflows(graph)
|
|
185
|
+
graph
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Declared workflows land in the SAME registry a deployment uses, so a DSL
|
|
189
|
+
# workflow is discoverable (`GET /v1/workflows`), schema-validated and
|
|
190
|
+
# durable (its run IS a Task) exactly like a hand-wired one. The
|
|
191
|
+
# :trigger_workflow command is registered only when there is something to
|
|
192
|
+
# trigger — parity: a system without workflows behaves as before.
|
|
193
|
+
def register_workflows(graph)
|
|
194
|
+
declared = @definition.respond_to?(:workflows) ? Array(@definition.workflows) : []
|
|
195
|
+
return if declared.empty?
|
|
196
|
+
|
|
197
|
+
declared.each do |w|
|
|
198
|
+
callable = WorkflowAdapter.new(block: w[:block], runtime: self)
|
|
199
|
+
graph.workflow_registry.register(
|
|
200
|
+
w[:name], callable,
|
|
201
|
+
description: w[:description], input_schema: w[:input_schema], output_schema: w[:output_schema]
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
graph.bus.register(:trigger_workflow, Insika::Commands::TriggerWorkflow.new(
|
|
206
|
+
profiles: graph.profiles, session_store: graph.session_store,
|
|
207
|
+
task_store: graph.task_store, executor: graph.executor,
|
|
208
|
+
workflow_registry: graph.workflow_registry
|
|
209
|
+
))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def build_components(backend, spine)
|
|
213
|
+
config_store = Insika::ConfigStore.new(store: backend)
|
|
214
|
+
settings_store = Insika::SettingsStore.new(config_store: config_store)
|
|
215
|
+
provider_store = Insika::LLMProviderStore.new(config_store: config_store)
|
|
216
|
+
tool_store = Insika::ToolStore.new(config_store: config_store)
|
|
217
|
+
skill_store = Insika::SkillStore.new(config_store: config_store)
|
|
218
|
+
tool_registry = Insika::OverlayToolRegistry.new(
|
|
219
|
+
base: spine.code_tool_registry, tool_store: tool_store,
|
|
220
|
+
http: Insika::HttpClient.new, event_stream: spine.event_stream
|
|
221
|
+
)
|
|
222
|
+
{
|
|
223
|
+
backend: backend, config_store: config_store,
|
|
224
|
+
settings_store: settings_store, provider_store: provider_store,
|
|
225
|
+
configurator: Insika::LLMConfigurator.new(provider_store: provider_store, configure: llm_configure),
|
|
226
|
+
agent_file_store: Insika::AgentFileStore.new(config_store: config_store),
|
|
227
|
+
skill_store: skill_store, tool_store: tool_store,
|
|
228
|
+
system_file_store: Insika::SystemFileStore.new(config_store: config_store),
|
|
229
|
+
mcp_store: Insika::McpStore.new(config_store: config_store),
|
|
230
|
+
tool_trace_store: Insika::ToolTraceStore.new(store: backend),
|
|
231
|
+
context_trace_store: Insika::ContextTraceStore.new(store: backend),
|
|
232
|
+
tool_registry: tool_registry,
|
|
233
|
+
tool_catalog: Insika::ToolCatalog.new(tool_registry: tool_registry),
|
|
234
|
+
skill_catalog: Insika::SkillCatalog.new([], store: skill_store),
|
|
235
|
+
prompt_catalog: Insika::PromptCatalog.new([]),
|
|
236
|
+
profile_source: Insika::StoredProfileSource.new(config_store: config_store),
|
|
237
|
+
# the moderator/validator tiers ask a model too. A graph reading its
|
|
238
|
+
# own key for the turn but the global one for a guardrail would be a
|
|
239
|
+
# credential leak wearing the look of isolation.
|
|
240
|
+
guardrails: Insika::Safety::Factory.new(settings_store: settings_store, llm: @llm),
|
|
241
|
+
edge_limiter: Insika::EdgeLimiter.new(
|
|
242
|
+
ledger: Insika::UsageLedger.new(store: backend), settings_store: settings_store,
|
|
243
|
+
budget_ledger: spine.budget_ledger, event_stream: spine.event_stream
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def context_providers(spine, c)
|
|
249
|
+
[
|
|
250
|
+
Insika::Context::Providers::Request.new,
|
|
251
|
+
Insika::Context::Providers::Prompt.new(
|
|
252
|
+
base: "", files: [], catalog: c[:prompt_catalog],
|
|
253
|
+
agent_files: c[:agent_file_store], system_files: c[:system_file_store]
|
|
254
|
+
),
|
|
255
|
+
Insika::Context::Providers::Skill.new(catalog: c[:skill_catalog]),
|
|
256
|
+
Insika::Context::Providers::SkillTrigger.new(catalog: c[:skill_catalog]),
|
|
257
|
+
Insika::Context::Providers::ToolSearch.new(catalog: c[:tool_catalog]),
|
|
258
|
+
Insika::Context::Providers::Memory.new(store: spine.memory_store),
|
|
259
|
+
Insika::Context::Providers::Session.new(session_store: spine.session_store)
|
|
260
|
+
].freeze
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# The runtime-authoring surface a pack import needs (create_agent +
|
|
264
|
+
# write_agent_file/skill/data_tool), plus the rest of the CRUD so /studio is
|
|
265
|
+
# fully functional under #serve. Same handlers as config/deployment.rb.
|
|
266
|
+
def register_authoring_commands(graph, c)
|
|
267
|
+
bus = graph.bus
|
|
268
|
+
es = graph.event_stream
|
|
269
|
+
mem = graph.memory_store
|
|
270
|
+
bus.register(:create_agent, Insika::Commands::CreateAgent.new(profile_source: c[:profile_source], event_stream: es))
|
|
271
|
+
bus.register(:update_agent, Insika::Commands::UpdateAgent.new(profile_source: c[:profile_source], event_stream: es))
|
|
272
|
+
bus.register(:delete_agent, Insika::Commands::DeleteAgent.new(profile_source: c[:profile_source], event_stream: es))
|
|
273
|
+
bus.register(:set_agent_tools, Insika::Commands::SetAgentTools.new(profile_source: c[:profile_source], event_stream: es))
|
|
274
|
+
bus.register(:write_agent_file, Insika::Commands::WriteAgentFile.new(profile_source: c[:profile_source], agent_file_store: c[:agent_file_store], event_stream: es))
|
|
275
|
+
bus.register(:delete_agent_file, Insika::Commands::DeleteAgentFile.new(profile_source: c[:profile_source], agent_file_store: c[:agent_file_store], event_stream: es))
|
|
276
|
+
bus.register(:restore_agent_file, Insika::Commands::RestoreAgentFile.new(profile_source: c[:profile_source], agent_file_store: c[:agent_file_store], event_stream: es))
|
|
277
|
+
bus.register(:write_skill, Insika::Commands::WriteSkill.new(skill_store: c[:skill_store], skill_catalog: c[:skill_catalog], event_stream: es))
|
|
278
|
+
bus.register(:set_skill_agents, Insika::Commands::SetSkillAgents.new(profile_source: c[:profile_source], event_stream: es))
|
|
279
|
+
bus.register(:memory_put_fact, Insika::Commands::MemoryPutFact.new(memory_store: mem, event_stream: es))
|
|
280
|
+
bus.register(:memory_forget_fact, Insika::Commands::MemoryForgetFact.new(memory_store: mem, event_stream: es))
|
|
281
|
+
bus.register(:memory_add_note, Insika::Commands::MemoryAddNote.new(memory_store: mem, event_stream: es))
|
|
282
|
+
bus.register(:update_settings, Insika::Commands::UpdateSettings.new(settings_store: c[:settings_store], event_stream: es))
|
|
283
|
+
bus.register(:upsert_llm_provider, Insika::Commands::UpsertLLMProvider.new(provider_store: c[:provider_store], configurator: c[:configurator], event_stream: es))
|
|
284
|
+
bus.register(:delete_llm_provider, Insika::Commands::DeleteLLMProvider.new(provider_store: c[:provider_store], configurator: c[:configurator], event_stream: es))
|
|
285
|
+
bus.register(:upsert_mcp, Insika::Commands::UpsertMcp.new(mcp_store: c[:mcp_store], event_stream: es))
|
|
286
|
+
bus.register(:delete_mcp, Insika::Commands::DeleteMcp.new(mcp_store: c[:mcp_store], event_stream: es))
|
|
287
|
+
bus.register(:write_system_file, Insika::Commands::WriteSystemFile.new(system_file_store: c[:system_file_store], event_stream: es))
|
|
288
|
+
bus.register(:delete_system_file, Insika::Commands::DeleteSystemFile.new(system_file_store: c[:system_file_store], event_stream: es))
|
|
289
|
+
bus.register(:restore_system_file, Insika::Commands::RestoreSystemFile.new(system_file_store: c[:system_file_store], event_stream: es))
|
|
290
|
+
bus.register(:write_data_tool, Insika::Commands::WriteDataTool.new(tool_store: c[:tool_store], registry: c[:tool_registry], tool_catalog: c[:tool_catalog], event_stream: es))
|
|
291
|
+
bus.register(:delete_data_tool, Insika::Commands::DeleteDataTool.new(tool_store: c[:tool_store], registry: c[:tool_registry], tool_catalog: c[:tool_catalog], event_stream: es))
|
|
292
|
+
bus.register(:restore_data_tool, Insika::Commands::RestoreDataTool.new(tool_store: c[:tool_store], registry: c[:tool_registry], tool_catalog: c[:tool_catalog], event_stream: es))
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# --- RubyLLM configuration ------------------------------------------
|
|
296
|
+
|
|
297
|
+
def provider_name
|
|
298
|
+
(@definition.runtime_options[:provider] || @pack.config[:provider] || "deepseek").to_s
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Every distinct provider the system uses, primary first. A fan-out system
|
|
302
|
+
# routinely mixes providers (one specialist on Anthropic, another on
|
|
303
|
+
# OpenAI), and configuring only the primary would leave the others without
|
|
304
|
+
# a key — a 401 at the first delegation, far from its cause.
|
|
305
|
+
def provider_names
|
|
306
|
+
([provider_name] + @packs.filter_map { |p| Insika::Coercion.presence(p.config[:provider])&.to_s }).uniq
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# the graph's credentials belong to the graph. `RubyLLM.context`
|
|
310
|
+
# is an isolated dup of the config that answers #chat, so two graphs in one
|
|
311
|
+
# process no longer overwrite each other's keys: the global
|
|
312
|
+
# `RubyLLM.configure` is a single slot PER PROVIDER, and the loser of that
|
|
313
|
+
# race got no error, just the other tenant's key.
|
|
314
|
+
#
|
|
315
|
+
# Returns nil under the test stub (no #context) — every consumer falls back
|
|
316
|
+
# to the RubyLLM constant, which is exactly the old behavior.
|
|
317
|
+
def build_llm_context
|
|
318
|
+
return nil unless RubyLLM.respond_to?(:context)
|
|
319
|
+
|
|
320
|
+
primary = provider_name
|
|
321
|
+
RubyLLM.context do |cfg|
|
|
322
|
+
provider_names.each do |provider|
|
|
323
|
+
# The explicit api_key/api_base belong to the PRIMARY provider; the
|
|
324
|
+
# others resolve from the conventional <PROVIDER>_API_KEY.
|
|
325
|
+
explicit = provider == primary
|
|
326
|
+
key = (explicit ? @definition.runtime_options[:api_key] : nil) || ENV["#{provider.upcase}_API_KEY"]
|
|
327
|
+
base = (explicit ? @definition.runtime_options[:api_base] : nil) || default_base_for(provider)
|
|
328
|
+
apply_llm(cfg, "#{provider}_api_key", key)
|
|
329
|
+
apply_llm(cfg, "#{provider}_api_base", base)
|
|
330
|
+
end
|
|
331
|
+
cfg.request_timeout = 120
|
|
332
|
+
cfg.max_retries = 2
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# The LLMConfigurator's target (Studio: edit a provider key, no restart).
|
|
337
|
+
# Scoped to THIS graph's context — a `RubyLLM.configure` here would reach
|
|
338
|
+
# into every other graph in the process, which is the leak exists to
|
|
339
|
+
# close. nil = no context (test stub) -> the configurator's own global
|
|
340
|
+
# default, unchanged.
|
|
341
|
+
def llm_configure
|
|
342
|
+
return nil if @llm.nil?
|
|
343
|
+
|
|
344
|
+
context = @llm
|
|
345
|
+
->(&blk) { blk.call(context.config) }
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def apply_llm(cfg, setter, value)
|
|
349
|
+
return if value.nil? || value.to_s.empty?
|
|
350
|
+
|
|
351
|
+
writer = "#{setter}="
|
|
352
|
+
cfg.public_send(writer, value) if cfg.respond_to?(writer)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Known convenience default (DeepSeek's OpenAI-compatible base); other
|
|
356
|
+
# providers use the gem's default unless api_base is given.
|
|
357
|
+
def default_base_for(provider)
|
|
358
|
+
provider == "deepseek" ? "https://api.deepseek.com/v1" : nil
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Seed the platform default model/provider so a modelless agent works too.
|
|
362
|
+
def seed_default_model
|
|
363
|
+
c = @components
|
|
364
|
+
return unless Insika::Coercion.presence(c[:settings_store].get["default_model"]).nil?
|
|
365
|
+
|
|
366
|
+
model = @pack.config[:model]
|
|
367
|
+
return if model.nil?
|
|
368
|
+
|
|
369
|
+
c[:settings_store].update("default_model" => model.to_s, "default_provider" => provider_name)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# One import per agent, in declaration order. Order is not significant for
|
|
373
|
+
# delegation: `SubagentGraph.validate!` treats an unknown child id as a
|
|
374
|
+
# LEAF, so a parent declared before its children imports cleanly and the
|
|
375
|
+
# reference resolves once they land.
|
|
376
|
+
def import_packs
|
|
377
|
+
importer = Insika::PackImporter.new(bus: @graph.bus, profiles: @components[:profile_source])
|
|
378
|
+
@packs.each { |pack| importer.import(pack) }
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Boots the single-process HTTP server for Definition#serve: /studio (control UI,
|
|
4
|
+
# cookie-auth) + /v1 (drop-in OpenAI-Responses API), the same topology as
|
|
5
|
+
# scripts/serve_real.rb, but wired from a DSL Runtime instead of a deployment.
|
|
6
|
+
# Required lazily (pulls the server + Studio + Async) — never on `require "insika"`.
|
|
7
|
+
|
|
8
|
+
require "async"
|
|
9
|
+
require "async/http/server"
|
|
10
|
+
require "async/http/endpoint"
|
|
11
|
+
require "protocol/rack"
|
|
12
|
+
require "rack/urlmap"
|
|
13
|
+
|
|
14
|
+
require_relative "../server/rack_app"
|
|
15
|
+
require_relative "../studio/app"
|
|
16
|
+
|
|
17
|
+
module Insika
|
|
18
|
+
module DSL
|
|
19
|
+
class ServerBoot
|
|
20
|
+
def initialize(runtime, port:, host:, token:)
|
|
21
|
+
@rt = runtime
|
|
22
|
+
@graph = runtime.graph
|
|
23
|
+
@port = port
|
|
24
|
+
@host = host
|
|
25
|
+
# Fixed local token: logs into the Studio (cookie) and gates /v1 (Bearer).
|
|
26
|
+
# Never a real secret — override with `token:` or ADMIN_TOKEN.
|
|
27
|
+
@token = token || ENV.fetch("ADMIN_TOKEN", "local-demo")
|
|
28
|
+
# the /v1 app is a value now — the same one a host mounts.
|
|
29
|
+
# This boot adds the Studio and a reactor around it, nothing else.
|
|
30
|
+
@builder = Insika::Server::AppBuilder.new(runtime, token: @token)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
configure_studio
|
|
35
|
+
dispatch = Rack::URLMap.new("/studio" => Studio::App, "/" => @builder.app)
|
|
36
|
+
endpoint = Async::HTTP::Endpoint.parse("http://#{@host}:#{@port}")
|
|
37
|
+
middleware = Protocol::Rack::Adapter.new(dispatch)
|
|
38
|
+
# the OTEL bridge is opt-in but must be reachable from the DSL
|
|
39
|
+
# front door too — the convention is worthless if only config.ru can export.
|
|
40
|
+
# nil (the default) -> attach is a no-op and no gem is loaded.
|
|
41
|
+
telemetry = Insika::Telemetry.setup(service_name: ENV.fetch("OTEL_SERVICE_NAME", "insika"))
|
|
42
|
+
|
|
43
|
+
banner(telemetry)
|
|
44
|
+
# first Ctrl-C/SIGTERM closes the intake and drains in-flight
|
|
45
|
+
# turns (INSIKA_DRAIN_TIMEOUT, default 20s); a second signal skips the wait.
|
|
46
|
+
Insika::Shutdown.install(executor: @graph.executor)
|
|
47
|
+
Async do
|
|
48
|
+
@graph.executor.supervised = true # serving mode: turns survive disconnects
|
|
49
|
+
Insika::Telemetry.attach(event_stream: @graph.event_stream, recorder: telemetry)
|
|
50
|
+
Async::HTTP::Server.new(middleware, endpoint).run
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def workflows? = @builder.workflows?
|
|
57
|
+
def channels? = @builder.channels?
|
|
58
|
+
|
|
59
|
+
def configure_studio
|
|
60
|
+
Studio::App.configure(
|
|
61
|
+
command_bus: @graph.bus, profile_source: @graph.profiles,
|
|
62
|
+
event_stream: @graph.event_stream,
|
|
63
|
+
config: { admin_token: @token, persistence: persistence },
|
|
64
|
+
agent_file_store: @rt.component(:agent_file_store), skill_store: @rt.component(:skill_store),
|
|
65
|
+
skill_catalog: @graph.skill_catalog, tool_catalog: @graph.tool_catalog,
|
|
66
|
+
tool_store: @rt.component(:tool_store), memory_store: @graph.memory_store,
|
|
67
|
+
session_store: @graph.session_store,
|
|
68
|
+
settings_store: @rt.component(:settings_store), llm_provider_store: @rt.component(:provider_store),
|
|
69
|
+
mcp_store: @rt.component(:mcp_store), system_file_store: @rt.component(:system_file_store),
|
|
70
|
+
tool_trace_store: @rt.component(:tool_trace_store),
|
|
71
|
+
context_trace_store: @rt.component(:context_trace_store),
|
|
72
|
+
task_store: @graph.task_store, checkpoint_store: @graph.checkpoint_store,
|
|
73
|
+
pending_action_store: @graph.pending_action_store
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def persistence
|
|
78
|
+
@graph.durable? ? "durable (sqlite)" : "ephemeral (memory)"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def banner(telemetry = nil)
|
|
82
|
+
base = "http://#{@host}:#{@port}"
|
|
83
|
+
ids = @rt.packs.map { |p| p.config[:id].to_s }
|
|
84
|
+
headline = ids.one? ? "agent \"#{ids.first}\"" : "#{ids.size} agents: #{ids.map(&:inspect).join(', ')}"
|
|
85
|
+
models = ids.one? ? "model: \"#{ids.first}\"" : "model: one of #{ids.map(&:inspect).join(', ')}"
|
|
86
|
+
puts "\e[1mInsika — serving #{headline}\e[0m"
|
|
87
|
+
puts " #{base}/studio → control UI (login token: \"#{@token}\")"
|
|
88
|
+
puts " #{base}/v1/responses → drop-in API (Bearer \"#{@token}\", #{models})"
|
|
89
|
+
puts " #{base}/start.md → onboarding for your coding agent (+ /models.json, /docs)"
|
|
90
|
+
puts " #{base}/v1/workflows → #{@graph.workflow_registry.names.join(', ')}" if workflows?
|
|
91
|
+
puts " #{base}/channels/… → #{@graph.channel_registry.names.join(', ')}" if channels?
|
|
92
|
+
# Only when on: an off-by-default line in the OSS front door is noise.
|
|
93
|
+
puts " OTEL → #{Insika::Telemetry.metrics? ? "traces + metrics" : "traces"} to OTLP" if telemetry
|
|
94
|
+
puts " Ctrl-C to stop (drains in-flight turns; press twice to skip the wait)."
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module DSL
|
|
5
|
+
# The result of `Insika.system { … }`: N agents sharing ONE runtime graph.
|
|
6
|
+
#
|
|
7
|
+
# It is the multi-agent counterpart of Definition, and deliberately the same
|
|
8
|
+
# shape — `reply` for a turn, `serve` for the server, `to_packs` for the data.
|
|
9
|
+
# Nothing here is a new engine path: every agent is still an ordinary Pack
|
|
10
|
+
# imported by the standard PackImporter, so a system of three agents is
|
|
11
|
+
# indistinguishable from three hand-written packs provisioned into the same
|
|
12
|
+
# deployment.
|
|
13
|
+
#
|
|
14
|
+
# Why it exists: delegation (`subagents`), fan-out/fan-in and routing all
|
|
15
|
+
# require the child agents to be resolvable in the SAME ProfileSource. A
|
|
16
|
+
# Definition owns exactly one pack, so those patterns had no home in the DSL.
|
|
17
|
+
class System
|
|
18
|
+
attr_reader :definitions, :workflows, :runtime_options
|
|
19
|
+
|
|
20
|
+
# `backend`: the store this system's graph owns. nil = the
|
|
21
|
+
# historic path (INSIKA_DB, or memory when unset). Set by `Insika.embed`.
|
|
22
|
+
def initialize(definitions:, workflows: [], runtime: {}, backend: nil)
|
|
23
|
+
@definitions = definitions.freeze
|
|
24
|
+
@workflows = workflows.freeze
|
|
25
|
+
@backend = backend
|
|
26
|
+
# Agent-level runtime knobs merged in declaration order, then the
|
|
27
|
+
# system-level ones on top (an explicit `provider`/`api_key` in the
|
|
28
|
+
# system block is the shared default and wins the tie).
|
|
29
|
+
@runtime_options = definitions.map(&:runtime_options)
|
|
30
|
+
.reduce({}) { |acc, opts| acc.merge(opts) }
|
|
31
|
+
.merge(runtime)
|
|
32
|
+
.freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def ids = definitions.map(&:id)
|
|
36
|
+
|
|
37
|
+
# The primary agent: the first declared. Only used where a single id is
|
|
38
|
+
# structurally required (the default model seed, the banner) — never to
|
|
39
|
+
# guess the target of a turn, which is always explicit.
|
|
40
|
+
def id = definitions.first.id
|
|
41
|
+
|
|
42
|
+
# The portable artifacts — one Pack per agent. Hand them to any
|
|
43
|
+
# PackImporter and you get this same system.
|
|
44
|
+
def to_packs = definitions.map(&:to_pack)
|
|
45
|
+
def packs = to_packs
|
|
46
|
+
|
|
47
|
+
# Runtime reads it for the provider/default-model seed.
|
|
48
|
+
def pack = definitions.first.to_pack
|
|
49
|
+
|
|
50
|
+
def find(agent_id)
|
|
51
|
+
definitions.find { |d| d.id == agent_id.to_s } ||
|
|
52
|
+
(raise Insika::NotFoundError, "agent '#{agent_id}' is not in this system (have: #{ids.join(', ')})")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The AgentProfile the engine runs for one agent, read back from the store.
|
|
56
|
+
def profile(agent_id) = runtime.profile(find(agent_id).id)
|
|
57
|
+
|
|
58
|
+
# One turn against ONE agent of the system, in-process. The agent is always
|
|
59
|
+
# explicit: with several agents in the graph, inferring the target would be
|
|
60
|
+
# a guess, and a wrong guess is a silently wrong conversation.
|
|
61
|
+
def reply(agent_id, message, session: nil, timeout: nil)
|
|
62
|
+
runtime.chat(message, agent: find(agent_id).id, session_id: session, timeout: timeout)
|
|
63
|
+
end
|
|
64
|
+
alias_method :chat, :reply
|
|
65
|
+
|
|
66
|
+
# Runs a declared workflow and returns its output. `agent:` is the profile
|
|
67
|
+
# the run executes under (policy, limits, context) — it defaults to the
|
|
68
|
+
# primary agent, since a workflow's own steps pick their agents explicitly.
|
|
69
|
+
# A bad input raises before any run is created (schema is enforced at the
|
|
70
|
+
# edge, exactly as it is over HTTP).
|
|
71
|
+
def run(workflow_name, input: {}, agent: nil, timeout: nil)
|
|
72
|
+
runtime.run_workflow(workflow_name.to_s, input: input,
|
|
73
|
+
agent: (agent || id).to_s, timeout: timeout)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Boot the control UI (/studio) + the drop-in API (/v1) with EVERY agent of
|
|
77
|
+
# the system served — each agent's id is a `model` on /v1/responses.
|
|
78
|
+
# With workflows declared, `GET /v1/workflows` + `POST /v1/workflows/:name`
|
|
79
|
+
# are exposed too.
|
|
80
|
+
def serve(port: 9292, host: "localhost", token: nil, **opts)
|
|
81
|
+
runtime.serve(port: port, host: host, token: token, **opts)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The live runtime graph with every pack imported. Memoized.
|
|
85
|
+
def runtime
|
|
86
|
+
@runtime ||= begin
|
|
87
|
+
require_relative "runtime"
|
|
88
|
+
Runtime.new(self, backend: @backend)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module DSL
|
|
5
|
+
# Adapts a DSL `workflow "name" do |input, ctx| … end` block to the engine's
|
|
6
|
+
# canonical workflow callable, `#call(input, context:, tools:)`.
|
|
7
|
+
#
|
|
8
|
+
# The engine contract is unchanged — this only decides what the AUTHOR holds.
|
|
9
|
+
# A workflow's job is orchestrating agent turns, so the second argument is a
|
|
10
|
+
# small context whose main verb is `ask`; the raw `context`/`tools` the
|
|
11
|
+
# Executor passes stay reachable for the rare workflow that wants them.
|
|
12
|
+
class WorkflowAdapter
|
|
13
|
+
def initialize(block:, runtime:)
|
|
14
|
+
@block = block
|
|
15
|
+
@runtime = runtime
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(input, context:, tools:)
|
|
19
|
+
ctx = Context.new(runtime: @runtime, context: context, tools: tools)
|
|
20
|
+
@block.arity == 1 ? @block.call(input) : @block.call(input, ctx)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# What a workflow block receives. Deliberately tiny: the value of a workflow
|
|
24
|
+
# is deterministic Ruby AROUND model calls, not a framework.
|
|
25
|
+
class Context
|
|
26
|
+
# `context` is the turn's ContextPackage; `tools` are the resolved,
|
|
27
|
+
# enveloped tool instances (already filtered by the agent's policy).
|
|
28
|
+
attr_reader :context, :tools
|
|
29
|
+
|
|
30
|
+
def initialize(runtime:, context:, tools:)
|
|
31
|
+
@runtime = runtime
|
|
32
|
+
@context = context
|
|
33
|
+
@tools = tools
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# One turn against one agent of the system → its text.
|
|
37
|
+
#
|
|
38
|
+
# Stateless by design (no session): a workflow step is a unit of work, not
|
|
39
|
+
# a conversation, and threading a session here would also drag in the
|
|
40
|
+
# serving teardown path. Each step is its own Task, so every step of a
|
|
41
|
+
# workflow is separately visible in the Studio and the trace.
|
|
42
|
+
def ask(agent, message, timeout: nil)
|
|
43
|
+
@runtime.chat(message.to_s, agent: agent.to_s, timeout: timeout)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Runs blocks CONCURRENTLY on the turn's reactor and returns their values
|
|
47
|
+
# IN ORDER — the fan-out/fan-in step. Thin pass-through to the engine's
|
|
48
|
+
# own helper so a workflow does not hand-roll Async.
|
|
49
|
+
#
|
|
50
|
+
# sec, perf = ctx.gather(-> { ctx.ask("security", code) },
|
|
51
|
+
# -> { ctx.ask("performance", code) })
|
|
52
|
+
def gather(*blocks, max: 8)
|
|
53
|
+
require_relative "../tools/concurrency"
|
|
54
|
+
Insika::Tools::Concurrency.gather(*blocks, max: max)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|