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,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "assertions"
|
|
7
|
+
|
|
8
|
+
module Insika
|
|
9
|
+
module Evals
|
|
10
|
+
# How the runner turns a (agent, conversation, message) into a TurnResult. The
|
|
11
|
+
# runner depends only on this seam, so it's unit-testable with a fake — the real
|
|
12
|
+
# HttpTransport is exercised against a running insika.
|
|
13
|
+
#
|
|
14
|
+
# One turn's outcome: the TurnResult the engine asserts on + perf timing (ttfb/
|
|
15
|
+
# total in ms) so `--mode perf` reports real latency over the real corpus (#6b).
|
|
16
|
+
#
|
|
17
|
+
# `usage` is the turn's token counts as the deployment reported them
|
|
18
|
+
# (`response.completed`), or nil when the provider sent none. It is carried, never
|
|
19
|
+
# asserted on: the consumer is's refinement budget, which has to bound the
|
|
20
|
+
# cost of a gate replay and cannot invent the number. nil is preserved as nil
|
|
21
|
+
# rather than zeroed — "the provider did not say" and "it cost nothing" are
|
|
22
|
+
# different facts, and a budget that confuses them stops being a budget.
|
|
23
|
+
TurnOutcome = Struct.new(:result, :ttfb, :total, :usage, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
# Pure reduction of the /v1/responses SSE stream. Kept separate from the HTTP so
|
|
26
|
+
# it's testable offline with canned frames (server/responses.rb is the producer).
|
|
27
|
+
module Sse
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# Raw SSE text -> [parsed JSON payload]. Skips `event:` lines, blanks, [DONE].
|
|
31
|
+
def payloads(text)
|
|
32
|
+
text.to_s.split("\n\n").flat_map do |frame|
|
|
33
|
+
frame.each_line.filter_map do |line|
|
|
34
|
+
next unless line.start_with?("data:")
|
|
35
|
+
|
|
36
|
+
p = line.sub(/^data:\s*/, "").strip
|
|
37
|
+
next if p.empty? || p == "[DONE]"
|
|
38
|
+
|
|
39
|
+
begin
|
|
40
|
+
JSON.parse(p)
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# [payload] -> { output_text:, tool_calls:, usage:, error: }. Maps the Responses
|
|
49
|
+
# frames (see server/responses.rb#frame_for): text deltas accumulate; each
|
|
50
|
+
# function_call item contributes a tool NAME (this stream carries no per-tool
|
|
51
|
+
# status — that lives in the ToolTraceStore, an in-process enrichment); a
|
|
52
|
+
# `response.failed` sets the turn error.
|
|
53
|
+
def reduce(payloads)
|
|
54
|
+
text = +""
|
|
55
|
+
tools = []
|
|
56
|
+
usage = nil
|
|
57
|
+
error = nil
|
|
58
|
+
payloads.each do |o|
|
|
59
|
+
case o["type"]
|
|
60
|
+
when "response.output_text.delta"
|
|
61
|
+
text << o["delta"].to_s
|
|
62
|
+
when "response.output_item.added"
|
|
63
|
+
item = o["item"] || {}
|
|
64
|
+
tools << { "name" => item["name"].to_s, "status" => nil } if item["type"] == "function_call"
|
|
65
|
+
when "response.completed"
|
|
66
|
+
usage = o.dig("response", "usage")
|
|
67
|
+
when "response.failed"
|
|
68
|
+
error = o.dig("response", "error", "message") || "response.failed"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
{ output_text: text, tool_calls: tools, usage: usage, error: error }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# What the deployment HAS, per agent, read over the same gated
|
|
76
|
+
# `/v1` the replay uses. The eval stays a client: it asks the engine instead of
|
|
77
|
+
# keeping its own idea of which tools exist.
|
|
78
|
+
#
|
|
79
|
+
# `#for(id)` -> { "tools" => [names] | nil, "capabilities" => [names] } | nil.
|
|
80
|
+
# nil means UNRESOLVED — the agent is not there, the route is not wired, the
|
|
81
|
+
# deployment is unreachable — and the Runner treats that as "run the case
|
|
82
|
+
# anyway". Silence must not shrink a suite. Answers are memoized: a corpus has
|
|
83
|
+
# many cases per agent and this is the same answer every time.
|
|
84
|
+
class HttpCapabilities
|
|
85
|
+
def initialize(base_url:, token:, timeout: 10)
|
|
86
|
+
@base = base_url
|
|
87
|
+
@token = token
|
|
88
|
+
@timeout = timeout
|
|
89
|
+
@cache = {}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def for(agent_id)
|
|
93
|
+
@cache.fetch(agent_id) { @cache[agent_id] = fetch(agent_id) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def fetch(agent_id)
|
|
99
|
+
uri = URI.join("#{@base}/", "v1/agents/#{URI.encode_www_form_component(agent_id)}")
|
|
100
|
+
req = Net::HTTP::Get.new(uri)
|
|
101
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
102
|
+
|
|
103
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
104
|
+
http.use_ssl = uri.scheme == "https"
|
|
105
|
+
http.read_timeout = @timeout
|
|
106
|
+
http.open_timeout = @timeout
|
|
107
|
+
res = http.start { http.request(req) }
|
|
108
|
+
return nil unless res.code.to_i == 200
|
|
109
|
+
|
|
110
|
+
body = JSON.parse(res.body)
|
|
111
|
+
{ "tools" => body["tools"], "capabilities" => Array(body["capabilities"]) }
|
|
112
|
+
rescue StandardError, JSON::ParserError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Drives real turns over POST /v1/responses — the SAME surface as
|
|
118
|
+
# scripts/loadtest.rb, so `--mode perf` is apples-to-apples with the loadtest and
|
|
119
|
+
# closes the real-traffic gap (#6b). A conversation is keyed by `user` (the
|
|
120
|
+
# adapter continues an existing chat on the same id), so multi-turn goldens replay
|
|
121
|
+
# in order under one `conv` id.
|
|
122
|
+
class HttpTransport
|
|
123
|
+
def initialize(base_url:, token:, timeout: 120)
|
|
124
|
+
@base = base_url
|
|
125
|
+
@token = token
|
|
126
|
+
@timeout = timeout
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def mono = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
130
|
+
|
|
131
|
+
def turn(agent:, conv:, message:)
|
|
132
|
+
uri = URI.join("#{@base}/", "v1/responses")
|
|
133
|
+
req = Net::HTTP::Post.new(uri)
|
|
134
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
135
|
+
req["Content-Type"] = "application/json"
|
|
136
|
+
req["Accept"] = "text/event-stream"
|
|
137
|
+
req.body = JSON.generate(model: "openclaw:#{agent}", user: conv, stream: true, input: message)
|
|
138
|
+
|
|
139
|
+
t0 = mono
|
|
140
|
+
ttfb = nil
|
|
141
|
+
buffer = +""
|
|
142
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
143
|
+
http.use_ssl = uri.scheme == "https"
|
|
144
|
+
http.read_timeout = @timeout
|
|
145
|
+
http.open_timeout = 10
|
|
146
|
+
|
|
147
|
+
status = nil
|
|
148
|
+
http.start do
|
|
149
|
+
http.request(req) do |res|
|
|
150
|
+
status = res.code.to_i
|
|
151
|
+
return failure("HTTP #{status}", t0) unless status.between?(200, 299)
|
|
152
|
+
|
|
153
|
+
res.read_body do |chunk|
|
|
154
|
+
ttfb ||= (mono - t0) * 1000.0
|
|
155
|
+
buffer << chunk
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
reduced = Sse.reduce(Sse.payloads(buffer))
|
|
161
|
+
TurnOutcome.new(
|
|
162
|
+
result: TurnResult.new(output_text: reduced[:output_text],
|
|
163
|
+
tool_calls: reduced[:tool_calls], error: reduced[:error]),
|
|
164
|
+
ttfb: ttfb, total: (mono - t0) * 1000.0, usage: reduced[:usage]
|
|
165
|
+
)
|
|
166
|
+
rescue StandardError => e
|
|
167
|
+
failure(e.class.to_s, t0)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def failure(msg, t0)
|
|
173
|
+
TurnOutcome.new(result: TurnResult.new(output_text: "", tool_calls: [], error: msg),
|
|
174
|
+
ttfb: nil, total: (mono - t0) * 1000.0)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
data/lib/insika/event.rb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Everything the runtime emits is an Event: a closed canonical catalog — new
|
|
5
|
+
# types require updating the catalog.
|
|
6
|
+
#
|
|
7
|
+
# meta: { task_id:, session_id:, seq:, at: } (seq monotonic per task).
|
|
8
|
+
# Terminal turn events are :task_completed / :task_failed / :task_cancelled.
|
|
9
|
+
# :error is NOT a terminal twin — it survives only as the subscription-overflow
|
|
10
|
+
# signal (EventStream) and the post-terminal failure report (fail_task).
|
|
11
|
+
Event = Data.define(:type, :data, :meta) do
|
|
12
|
+
def initialize(type:, data:, meta: {})
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h = { type:, **data, meta: meta.compact }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async"
|
|
4
|
+
require "async/queue"
|
|
5
|
+
|
|
6
|
+
module Insika
|
|
7
|
+
# In-process pub/sub. The stream is concurrent with the
|
|
8
|
+
# turn: a slow observer NEVER delays execution — each subscriber
|
|
9
|
+
# has its own queue and `emit` only enqueues. No mutex: one reactor, cooperative
|
|
10
|
+
# fibers — a plain Array is enough.
|
|
11
|
+
class EventStream
|
|
12
|
+
# One subscription = one queue. The consumer blocks on `each` (its own fiber),
|
|
13
|
+
# never the emitter.
|
|
14
|
+
class Subscription
|
|
15
|
+
CLOSED = Object.new # internal end-of-stream sentinel
|
|
16
|
+
private_constant :CLOSED
|
|
17
|
+
|
|
18
|
+
# Cap on events queued per subscriber. A slow consumer
|
|
19
|
+
# piles up in its OWN queue; on overflow, the subscription closes with a
|
|
20
|
+
# local :error event — the turn never waits on transport.
|
|
21
|
+
MAX_QUEUED = 1000
|
|
22
|
+
|
|
23
|
+
def initialize(task_id: nil, session_id: nil, tenant: nil, types: nil, on_close: nil)
|
|
24
|
+
@task_id = task_id
|
|
25
|
+
@session_id = session_id
|
|
26
|
+
@tenant = tenant
|
|
27
|
+
@types = types
|
|
28
|
+
@on_close = on_close
|
|
29
|
+
@queue = Async::Queue.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Binds the subscription to a task_id AFTER creation: the
|
|
33
|
+
# transport subscribes before the dispatch — when the task_id does not yet
|
|
34
|
+
# exist — and binds it as soon as the handler returns. This keeps the
|
|
35
|
+
# per-task cap honest (only events for THIS task enter the queue) and makes
|
|
36
|
+
# the overflow :error carry the correct task_id. Returns self to chain.
|
|
37
|
+
def bind(task_id:)
|
|
38
|
+
@task_id = task_id
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Meta filter: nil = matches any value. Events with no task_id in
|
|
43
|
+
# meta (e.g. :session_created) reach only subscribers with no task filter.
|
|
44
|
+
#
|
|
45
|
+
# A TENANT-scoped subscription is FAIL-CLOSED on the meta's tenant (WS1):
|
|
46
|
+
# an event that does not carry the tenant (control events, ignored turns)
|
|
47
|
+
# matches NO tenant subscription. The tenant only ever sees its own.
|
|
48
|
+
#
|
|
49
|
+
# `types:` (nil = any) keeps a subscriber's queue to the events it answers
|
|
50
|
+
# — an alert consumer must not sit behind a full-traffic stream's 1000-cap
|
|
51
|
+
# (WS6), and a filtered queue is the cheapest way to keep it there.
|
|
52
|
+
def matches?(event)
|
|
53
|
+
meta = event.meta || {}
|
|
54
|
+
owned = @tenant.nil? || meta[:tenant] == @tenant
|
|
55
|
+
|
|
56
|
+
owned &&
|
|
57
|
+
(@types.nil? || @types.include?(event.type)) &&
|
|
58
|
+
(@task_id.nil? || meta[:task_id] == @task_id) &&
|
|
59
|
+
(@session_id.nil? || meta[:session_id] == @session_id)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Enqueues without EVER blocking. The real queue depth is `@queue.size`
|
|
63
|
+
# (not a separate counter — avoids drift). On reaching the cap, it enqueues
|
|
64
|
+
# a local :error and closes; pushes after close are ignored (`@closed`).
|
|
65
|
+
def push(event)
|
|
66
|
+
return if @closed
|
|
67
|
+
|
|
68
|
+
if @queue.size >= MAX_QUEUED
|
|
69
|
+
@queue.enqueue(Insika::Event.new(
|
|
70
|
+
type: :error,
|
|
71
|
+
data: { message: "subscription overflow" },
|
|
72
|
+
meta: { task_id: @task_id, session_id: @session_id }
|
|
73
|
+
))
|
|
74
|
+
close
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
@queue.enqueue(event)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Blocks the CONSUMER's fiber until #close.
|
|
82
|
+
def each
|
|
83
|
+
while (event = @queue.dequeue) != CLOSED
|
|
84
|
+
yield event
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Idempotent: a second CLOSED is harmless (the `each` stops at the first).
|
|
89
|
+
# `@on_close` fires only once (avoids removing the subscription twice).
|
|
90
|
+
def close
|
|
91
|
+
return if @closed
|
|
92
|
+
|
|
93
|
+
@closed = true
|
|
94
|
+
@queue.enqueue(CLOSED)
|
|
95
|
+
@on_close&.call(self)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def initialize
|
|
100
|
+
@subscriptions = []
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# NEVER raises: an observer's exception is isolated — a
|
|
104
|
+
# broken observer does not bring down the turn. Synchronous and cheap.
|
|
105
|
+
#
|
|
106
|
+
# Iterates over a SNAPSHOT (`dup`): a subscription's cap may close it
|
|
107
|
+
# DURING the push (overflow -> close -> on_close removes from the array);
|
|
108
|
+
# mutating the array in the middle of a plain Array#each would skip the next
|
|
109
|
+
# subscriber.
|
|
110
|
+
def emit(event)
|
|
111
|
+
@subscriptions.dup.each do |sub|
|
|
112
|
+
sub.push(event) if sub.matches?(event)
|
|
113
|
+
rescue StandardError
|
|
114
|
+
# a broken observer does not bring down the turn; nothing to propagate
|
|
115
|
+
end
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# nil/nil = all events. Returns the Subscription (the caller iterates with
|
|
120
|
+
# `#each` on its own fiber). `tenant:` scopes the stream to one tenant's
|
|
121
|
+
# events (WS1) — fail-closed, see Subscription#matches?. `types:` (nil =
|
|
122
|
+
# any) filters by event type so a subscriber's queue only ever holds what
|
|
123
|
+
# its consumer answers (WS6).
|
|
124
|
+
def subscribe(task_id: nil, session_id: nil, tenant: nil, types: nil)
|
|
125
|
+
sub = Subscription.new(task_id: task_id, session_id: session_id, tenant: tenant,
|
|
126
|
+
types: types,
|
|
127
|
+
on_close: ->(s) { @subscriptions.delete(s) })
|
|
128
|
+
@subscriptions << sub
|
|
129
|
+
sub
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|