insika 0.0.1 → 0.1.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 +295 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +351 -0
- data/docs/AGENTS.md +494 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +100 -0
- data/docs/DEPLOY.md +334 -0
- data/docs/EMBEDDING.md +194 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +231 -0
- data/docs/OBSERVABILITY.md +365 -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 +362 -0
- data/docs/SKILLS.md +98 -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 +188 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +150 -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/chat_builder.rb +254 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/coercion.rb +50 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +41 -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_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/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/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 +71 -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 +50 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +85 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +19 -0
- data/lib/insika/context/priority.rb +29 -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 +108 -0
- data/lib/insika/context/providers/skill.rb +20 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +294 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +379 -0
- data/lib/insika/dsl/server_boot.rb +97 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +307 -0
- data/lib/insika/edge_limiter.rb +130 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +246 -0
- data/lib/insika/errors.rb +145 -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 +114 -0
- data/lib/insika/executor.rb +1680 -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/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 +114 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +103 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +121 -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 +137 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +127 -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/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 +87 -0
- data/lib/insika/safety/moderator.rb +86 -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/admin_auth.rb +29 -0
- data/lib/insika/server/app.rb +850 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +110 -0
- data/lib/insika/server/responses.rb +155 -0
- data/lib/insika/server/sse_body.rb +96 -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 +113 -0
- data/lib/insika/skill_store.rb +79 -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 +1571 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +69 -0
- data/lib/insika/studio/forms.rb +340 -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 +62 -0
- data/lib/insika/studio/views/settings.erb +173 -0
- data/lib/insika/studio/views/skills.erb +86 -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/token_estimator.rb +16 -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_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 +220 -0
- data/lib/insika/tools/load_skill.rb +41 -0
- data/lib/insika/tools/remember.rb +53 -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 +158 -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 +198 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +203 -4
- metadata +395 -8
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# WHAT REACHES THE CUSTOMER — the turn's publishing rule.
|
|
5
|
+
#
|
|
6
|
+
# A turn is not one assistant message. Between the user's message and the answer
|
|
7
|
+
# the model may narrate the loop ("deixa eu buscar isso pra você"), apologise for
|
|
8
|
+
# a tool that failed, or — when it has no tool to call — reason in prose. All of
|
|
9
|
+
# it arrives as ordinary `content` chunks, indistinguishable at the token level
|
|
10
|
+
# from the answer, so streaming every chunk straight to `:content` published all
|
|
11
|
+
# of it. Running a real store's prompt proved what that costs: 132 deltas of an
|
|
12
|
+
# English monologue ("Let me check the tools I actually have… Actually, let me
|
|
13
|
+
# reconsider.") went out as `response.output_text.delta` — the bytes a WhatsApp
|
|
14
|
+
# customer would have read.
|
|
15
|
+
#
|
|
16
|
+
# The rule, enforced here: **`:content` carries the ANSWER — the text of the
|
|
17
|
+
# assistant message that ENDS the turn.** Everything else rides `:intermediate`,
|
|
18
|
+
# which the Studio and the trace render (that is the operator's window into what
|
|
19
|
+
# the model narrated) and which `/v1/responses` deliberately does not translate,
|
|
20
|
+
# exactly like `:thinking`.
|
|
21
|
+
#
|
|
22
|
+
# Which message is the last one is only knowable when it ends — a message that
|
|
23
|
+
# carries tool calls is never the answer — so text is buffered per message and
|
|
24
|
+
# published at the boundary (RubyLLM's `after_message`). Two consequences, both
|
|
25
|
+
# deliberate:
|
|
26
|
+
#
|
|
27
|
+
# · The customer-visible stream is per MESSAGE, not per token. `ttft_ms` still
|
|
28
|
+
# measures the provider's first token, so item 34's baselines stay comparable;
|
|
29
|
+
# what moved is when the customer can read it. For the WhatsApp edge this
|
|
30
|
+
# changes nothing — the dispatcher accumulated the deltas into one message
|
|
31
|
+
# anyway — and the Studio keeps its live typing off `:intermediate`.
|
|
32
|
+
# · A turn that dies mid-message publishes nothing. Half a sentence was never an
|
|
33
|
+
# answer; the fragment is still on the stream as `:intermediate` for whoever is
|
|
34
|
+
# debugging it.
|
|
35
|
+
#
|
|
36
|
+
# `halt_when` (PR #130) is the one case where narration IS the turn: the tool
|
|
37
|
+
# already answered the customer, and the model's lead-in ("vou te inscrever
|
|
38
|
+
# agora") is all the turn is worth. That text is retained and published by the
|
|
39
|
+
# Executor's halt branch — see #halt_text.
|
|
40
|
+
class TurnOutput
|
|
41
|
+
# The text of the message that ended the turn, or nil if no boundary said so.
|
|
42
|
+
# It is a CANDIDATE, not the published answer: the `:agent` after-hook runs
|
|
43
|
+
# after the message ends and may replace the whole response, so the Executor
|
|
44
|
+
# decides and publishes once, at the end of the stage.
|
|
45
|
+
attr_reader :candidate
|
|
46
|
+
|
|
47
|
+
# filter: Safety::OutputFilter | nil (RFC-0009 §3.2 — nil = stream untouched).
|
|
48
|
+
# emit: ->(type, data) — the Executor's emitter, already bound to the task.
|
|
49
|
+
# public_intermediate: the agent opted this channel in (`edge_stream`), so the
|
|
50
|
+
# narration is TAGGED and `/v1/responses` gives it its own frame. Default false:
|
|
51
|
+
# an internal event stays internal unless someone said otherwise.
|
|
52
|
+
def initialize(filter:, emit:, public_intermediate: false)
|
|
53
|
+
@filter = filter
|
|
54
|
+
@emit = emit
|
|
55
|
+
@public_intermediate = public_intermediate
|
|
56
|
+
@pending = +"" # text of the message currently streaming
|
|
57
|
+
@last_intermediate = +"" # text of the last message that turned out NOT to be the answer
|
|
58
|
+
@candidate = nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# One provider chunk. Publishes the redacted slice as `:intermediate` — live,
|
|
62
|
+
# chunk by chunk, because the operator surfaces want to watch it happen — and
|
|
63
|
+
# holds it until the message boundary decides what it was.
|
|
64
|
+
def push(text)
|
|
65
|
+
slice = @filter ? @filter.push(text) : text.to_s
|
|
66
|
+
return if slice.empty?
|
|
67
|
+
|
|
68
|
+
@pending << slice
|
|
69
|
+
emit_intermediate(slice)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# A message ended (RubyLLM `after_message`). Only an assistant message decides
|
|
73
|
+
# anything — a `role: tool` result is the gem's bookkeeping. A message carrying
|
|
74
|
+
# tool calls is intermediate by definition: the model asked for something, so
|
|
75
|
+
# it was not done talking.
|
|
76
|
+
def message_ended(message)
|
|
77
|
+
return unless assistant?(message)
|
|
78
|
+
|
|
79
|
+
flush
|
|
80
|
+
text = @pending
|
|
81
|
+
@pending = +""
|
|
82
|
+
tool_calls?(message) ? @last_intermediate = text : @candidate = text
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Releases the redactor's retained tail (the sliding buffer holds back a value
|
|
86
|
+
# that might still be growing into a match) into the current message.
|
|
87
|
+
def flush
|
|
88
|
+
return unless @filter
|
|
89
|
+
|
|
90
|
+
tail = @filter.flush.to_s
|
|
91
|
+
return if tail.empty?
|
|
92
|
+
|
|
93
|
+
@pending << tail
|
|
94
|
+
emit_intermediate(tail)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Emits the answer and returns it. Empty text emits no event — an empty turn is
|
|
98
|
+
# what the consumer suppresses.
|
|
99
|
+
def publish(text)
|
|
100
|
+
answer = text.to_s
|
|
101
|
+
@emit.call(:content, { delta: answer }) unless answer.empty?
|
|
102
|
+
answer
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The narration of the message that halted the turn. Normally the message
|
|
106
|
+
# boundary already moved it aside (`@last_intermediate`); a transport that does
|
|
107
|
+
# not report boundaries leaves it in `@pending`, and the lead-in is worth the
|
|
108
|
+
# same either way.
|
|
109
|
+
def halt_text = @last_intermediate.empty? ? @pending : @last_intermediate
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
# `public: true` is the whole difference between an event the edge drops and one
|
|
114
|
+
# it translates. The flag travels on the EVENT because `frame_for` is a pure
|
|
115
|
+
# static mapper with no agent in scope — the profile is read once, here.
|
|
116
|
+
def emit_intermediate(text)
|
|
117
|
+
data = { delta: text }
|
|
118
|
+
data[:public] = true if @public_intermediate
|
|
119
|
+
@emit.call(:intermediate, data)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def assistant?(message) = field(message, :role).to_s == "assistant"
|
|
123
|
+
|
|
124
|
+
# RubyLLM::Message answers `tool_call?`; a double may only carry the field.
|
|
125
|
+
def tool_calls?(message)
|
|
126
|
+
return !!message.tool_call? if message.respond_to?(:tool_call?)
|
|
127
|
+
|
|
128
|
+
calls = field(message, :tool_calls)
|
|
129
|
+
calls.respond_to?(:empty?) ? !calls.empty? : !calls.nil?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def field(message, name)
|
|
133
|
+
return message.public_send(name) if message.respond_to?(name)
|
|
134
|
+
return message[name] || message[name.to_s] if message.respond_to?(:[])
|
|
135
|
+
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# MUTABLE on purpose (the only exception to the Data types):
|
|
5
|
+
# the Middleware MODIFIES the execution — the links write into these fields.
|
|
6
|
+
class TurnState
|
|
7
|
+
attr_reader :task, :profile, :turn # turn identity (1-based)
|
|
8
|
+
attr_accessor :message, # input (Middleware may rewrite)
|
|
9
|
+
:context, # ContextPackage from the Builder
|
|
10
|
+
:allowed_tools, # Resolution from the Policy Engine
|
|
11
|
+
:allowed_skills,
|
|
12
|
+
:chat, # the turn's RubyLLM::Chat instance
|
|
13
|
+
:session, # the turn's SessionStore::Session | nil (set at stage 2;
|
|
14
|
+
# read by create_chat for the per-chat model pin, §10)
|
|
15
|
+
:model_selection, # resolved ModelSelection (v2, §10): model/provider/source/
|
|
16
|
+
# pinned/params/fallbacks. Set at stage 5; surfaced in usage.
|
|
17
|
+
:halt_reason, # set by Middleware when short-circuiting (halt-as-FAILURE)
|
|
18
|
+
:halt_response, # set by a Middleware for the GRACEFUL halt (RFC-0009 §3.1):
|
|
19
|
+
# the safe reply the turn completes with, WITHOUT touching the
|
|
20
|
+
# LLM. Distinct from halt_reason — a completion, not a failure.
|
|
21
|
+
:guardrail_block, # audit metadata the guardrail sets alongside halt_response
|
|
22
|
+
# ({category:, source:, action:, detail:}); the Executor (single
|
|
23
|
+
# emitter) turns it into :guardrail_blocked.
|
|
24
|
+
:guardrail_flags, # [{category:, source:, detail:}] appended by the OutputValidator
|
|
25
|
+
# (after_task); the Executor emits one :guardrail_flagged each.
|
|
26
|
+
:response_content, # the turn's final assistant text, set at stage 6/on halt so the
|
|
27
|
+
# after_task validator can inspect it.
|
|
28
|
+
:output_filter # per-turn Safety::OutputFilter (nil = off); redacts the stream.
|
|
29
|
+
|
|
30
|
+
# Internal (not part of the contract): per-CALL correlation between RubyLLM's
|
|
31
|
+
# tool callbacks and the tool decorators — `current_tool_call` keys the
|
|
32
|
+
# side-effect checkpoint / resume skip / trace, `current_tool_name` labels the
|
|
33
|
+
# :tool_result event.
|
|
34
|
+
#
|
|
35
|
+
# They live in FIBER STORAGE, not in ivars, and that is the whole point:
|
|
36
|
+
# `before_tool_call` → `tool.call` → `after_tool_result` all run in the SAME
|
|
37
|
+
# fiber, and with `ToolConcurrency` (item 30) there is one fiber PER CALL. A
|
|
38
|
+
# single slot on this shared object would let one in-flight call overwrite
|
|
39
|
+
# another's — a side-effect recorded under the wrong id (so a resume skips the
|
|
40
|
+
# wrong tool, or re-runs a non-idempotent one) and a mislabelled event. Both
|
|
41
|
+
# silent. One writer per fiber needs no lock; serial execution is unchanged,
|
|
42
|
+
# since a lone fiber writes and reads its own storage.
|
|
43
|
+
#
|
|
44
|
+
# Read/written ONLY through here so the rule has one home.
|
|
45
|
+
CALL_KEY = :insika_tool_call
|
|
46
|
+
NAME_KEY = :insika_tool_name
|
|
47
|
+
|
|
48
|
+
def current_tool_call = Fiber[CALL_KEY]
|
|
49
|
+
def current_tool_name = Fiber[NAME_KEY]
|
|
50
|
+
|
|
51
|
+
def current_tool_call=(call)
|
|
52
|
+
Fiber[CALL_KEY] = call
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def current_tool_name=(name)
|
|
56
|
+
Fiber[NAME_KEY] = name
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Internal (§11 R1): the chat's message count RIGHT AFTER `assemble` (seeded
|
|
60
|
+
# history) and BEFORE `ask`. persist_turn slices `chat.messages.drop(baseline)`
|
|
61
|
+
# to serialize the turn's real exchange — user + assistant(tool_calls) + tool
|
|
62
|
+
# results + final assistant — into the transcript. nil = no chat recorded
|
|
63
|
+
# (workflow/halt) → persist_turn falls back to the {user, assistant} pair.
|
|
64
|
+
attr_accessor :chat_baseline
|
|
65
|
+
|
|
66
|
+
# Internal: impl_name(String) -> STABLE name of the capability that
|
|
67
|
+
# resolved it, computed by resolve_capabilities BEFORE the policy_request and
|
|
68
|
+
# consulted AFTER @policy_engine.decide, at the post-Policy junction, to
|
|
69
|
+
# decide which impls enter as Capability::ResolvedTool. {} = no
|
|
70
|
+
# capability_registry or empty profile.capabilities (parity).
|
|
71
|
+
attr_accessor :capability_names
|
|
72
|
+
|
|
73
|
+
# Internal (RFC-0015): the turn's resolved QueuePolicy. Read at stage 6 to decide
|
|
74
|
+
# whether this run accepts steered messages, and how they are worded. Resolved once
|
|
75
|
+
# per turn, in build_turn_state — an edit to the agent mid-run does not change the
|
|
76
|
+
# rules the run started under.
|
|
77
|
+
attr_accessor :queue_policy
|
|
78
|
+
|
|
79
|
+
# Internal (item 33): true when this turn re-enters the pipeline via
|
|
80
|
+
# resume_task/recovery. The EdgeLimiter reads it to NEVER re-count or block a
|
|
81
|
+
# turn that was already admitted — a crash/pause under a saturated window must
|
|
82
|
+
# not swallow a legitimate message with the rate-limit reply.
|
|
83
|
+
attr_accessor :resumed
|
|
84
|
+
|
|
85
|
+
# Internal (memory): the turn's tenant (from the Command), scope of the write path
|
|
86
|
+
# (`remember` tool). Set in run_pipeline; nil = DEFAULT_TENANT in the MemoryStore.
|
|
87
|
+
attr_accessor :tenant
|
|
88
|
+
|
|
89
|
+
# Internal (Phase 6/D2/G4): turn context deposited into the data-tools to
|
|
90
|
+
# resolve {{ctx.*}} (chat_id/agent_id/tenant/store_id) and emit
|
|
91
|
+
# X-Chat-Id/X-Store-Id/X-Agent-Id. A Hash of symbols, set in run_pipeline.
|
|
92
|
+
# Comes from the TURN, never from the model's args (R2). Distinct from `tenant` (memory).
|
|
93
|
+
attr_accessor :turn_context
|
|
94
|
+
|
|
95
|
+
# Internal (Phase 6, observability): the turn's token usage (input/output/
|
|
96
|
+
# total/cached + model), captured from the provider's response at stage 6. Goes
|
|
97
|
+
# to the terminal event (:task_completed) — feeds the usage of
|
|
98
|
+
# /v1/responses and the Telemetry (OTEL). nil = turn with no model response
|
|
99
|
+
# (workflow) or provider without counts.
|
|
100
|
+
attr_accessor :usage
|
|
101
|
+
|
|
102
|
+
# Internal (Tool Search): ids of side-effects already completed in the
|
|
103
|
+
# interrupted turn, propagated to the tools PROMOTED by tool_search (the same `skip`
|
|
104
|
+
# that the eager tools' wrap_tools receives). Set in run_pipeline;
|
|
105
|
+
# nil = new turn (Array(nil) => []).
|
|
106
|
+
attr_accessor :skip_side_effects
|
|
107
|
+
|
|
108
|
+
# Approval gate. `requires_approval` = names of tools that require
|
|
109
|
+
# approval (Resolution); `approval_coordinator` = object (the Executor) that
|
|
110
|
+
# creates the PendingAction/suspends/waits; `actor` = the turn's mailbox (used
|
|
111
|
+
# by the coordinator for await(:approval)).
|
|
112
|
+
attr_accessor :requires_approval, :approval_coordinator, :actor
|
|
113
|
+
|
|
114
|
+
# Internal (item 30 / D4): the turn's shared in-flight cap for tool calls —
|
|
115
|
+
# ONE Async::Semaphore(tool_concurrency), installed by ToolAssembly#wrap_tools
|
|
116
|
+
# and acquired by every ToolEnvelope, INCLUDING the ones tool_search promotes
|
|
117
|
+
# mid-turn (they read it off the state, so the cap survives promotion).
|
|
118
|
+
# nil = concurrency off: no gate, no overhead, serial execution unchanged.
|
|
119
|
+
attr_accessor :tool_gate
|
|
120
|
+
|
|
121
|
+
# Item 30: parallel tool calls, resolved PER TURN and read by ChatBuilder
|
|
122
|
+
# (whether to hand the gem `concurrency:`) and ToolAssembly (the gate's size).
|
|
123
|
+
#
|
|
124
|
+
# `requested_tool_concurrency` is what the operator configured;
|
|
125
|
+
# `tool_concurrency` is what this turn actually gets. They differ for exactly
|
|
126
|
+
# one reason — D3: `Executor#request_approval` blocks on `actor.await(:approval)`,
|
|
127
|
+
# and the mailbox is one queue per TASK. Two fibers waiting there share it,
|
|
128
|
+
# `dequeue` wakes exactly one, the message is consumed, and the other fiber
|
|
129
|
+
# hangs until `approval_timeout` (~1h). So a turn that can suspend for a human
|
|
130
|
+
# runs its tools serially. Per-TURN and not per-profile because
|
|
131
|
+
# `requires_approval` comes from the Resolution: it can be empty on a turn
|
|
132
|
+
# whose profile does list approvals.
|
|
133
|
+
def requested_tool_concurrency
|
|
134
|
+
n = ((profile.respond_to?(:limits) && profile.limits) || {})[:tool_concurrency].to_i
|
|
135
|
+
n > 1 ? n : nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def tool_concurrency
|
|
139
|
+
return nil unless Array(requires_approval).empty?
|
|
140
|
+
|
|
141
|
+
requested_tool_concurrency
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def initialize(task:, profile:, turn:, message:)
|
|
145
|
+
@task = task
|
|
146
|
+
@profile = profile
|
|
147
|
+
@turn = turn
|
|
148
|
+
@message = message
|
|
149
|
+
@capability_names = {}
|
|
150
|
+
# Fiber storage is INHERITED by fibers created later, so a turn spawned from
|
|
151
|
+
# inside a tool call (a subagent child) would start out carrying its
|
|
152
|
+
# parent's correlation. Clearing at turn start keeps a child from keying its
|
|
153
|
+
# own side-effects under the parent's tool_call id.
|
|
154
|
+
self.current_tool_call = nil
|
|
155
|
+
self.current_tool_name = nil
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Opt-in per-turn latency breakdown (item 34 / §13.1, action 1: locate the
|
|
5
|
+
# TTFB cost with real-turn data). OFF unless INSIKA_TURN_TIMING is set — when
|
|
6
|
+
# off the Executor never allocates one and the hot path pays only `nil&.mark`.
|
|
7
|
+
#
|
|
8
|
+
# Splits a turn into the three windows that answer "is TTFB local or provider?":
|
|
9
|
+
# prep_ms — prep_start -> ask: ALL local work before the provider call
|
|
10
|
+
# (context build, policy, guardrail detectors, chat assembly).
|
|
11
|
+
# ttft_ms — ask -> first_token: the provider round-trip to the 1st token.
|
|
12
|
+
# gen_ms — first_token -> done: streaming the rest of the response.
|
|
13
|
+
#
|
|
14
|
+
# Marks are monotonic; `mark` is first-write-wins so `first_token` records the
|
|
15
|
+
# FIRST content chunk even though it is called on every chunk.
|
|
16
|
+
#
|
|
17
|
+
# `ttft_ms` is the PROVIDER's first token, not the first byte the customer can
|
|
18
|
+
# read: TurnOutput publishes a message once it ends, so the customer-visible
|
|
19
|
+
# answer lands inside `gen_ms`. Measuring the provider is the point — item 34's
|
|
20
|
+
# baselines (~720 ms, provider-bound) stay comparable across that change.
|
|
21
|
+
class TurnTiming
|
|
22
|
+
# EnvSchema owns "is this flag on?" (1/true/yes/on) — the same predicate that
|
|
23
|
+
# validates the :boolean keys, so a spelling `insika env` accepts is a spelling
|
|
24
|
+
# the reader honours.
|
|
25
|
+
def self.enabled?(env = ENV)
|
|
26
|
+
Insika::EnvSchema.truthy?(Insika::EnvSchema.read("INSIKA_TURN_TIMING", env))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize
|
|
30
|
+
@marks = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mark(name)
|
|
34
|
+
@marks[name] ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# -> Hash of phase deltas in ms (only the windows whose endpoints both fired;
|
|
38
|
+
# a workflow turn has no ask/first_token, so those are simply absent).
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
prep_ms: delta(:prep_start, :ask),
|
|
42
|
+
ttft_ms: delta(:ask, :first_token),
|
|
43
|
+
gen_ms: delta(:first_token, :done),
|
|
44
|
+
total_ms: delta(:prep_start, :done)
|
|
45
|
+
}.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def delta(from, to)
|
|
51
|
+
return nil unless @marks[from] && @marks[to]
|
|
52
|
+
|
|
53
|
+
((@marks[to] - @marks[from]) * 1000).round(2)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Fixed-window counters on the KV Store (item 33 / §12 G7): the durable side of
|
|
5
|
+
# the edge limits. One scope, keys shaped "kind:id:window_start" — the window
|
|
6
|
+
# start bucketed on the epoch keeps every process/worker on the SAME bucket
|
|
7
|
+
# without coordination (the Store's transaction serializes the read-modify-write,
|
|
8
|
+
# in-process via the fiber semaphore and cross-process via BEGIN IMMEDIATE).
|
|
9
|
+
#
|
|
10
|
+
# Growth is bounded: each `add` garbage-collects the (kind, id) pair's PREVIOUS
|
|
11
|
+
# window key, so an active entity holds at most 2 keys at any time and an idle
|
|
12
|
+
# one converges to 1.
|
|
13
|
+
class UsageLedger
|
|
14
|
+
SCOPE = "usage_counters"
|
|
15
|
+
|
|
16
|
+
def initialize(store:)
|
|
17
|
+
@store = store
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Adds `by` to the current window's counter of (kind, id) -> the NEW total.
|
|
21
|
+
def add(kind, id, window:, by: 1, now: Time.now)
|
|
22
|
+
start = window_start(now, window)
|
|
23
|
+
key = key_for(kind, id, start)
|
|
24
|
+
@store.transaction do
|
|
25
|
+
total = @store.get(SCOPE, key).to_i + by
|
|
26
|
+
@store.set(SCOPE, key, total)
|
|
27
|
+
@store.delete(SCOPE, key_for(kind, id, start - window))
|
|
28
|
+
total
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Current window's total for (kind, id). Missing/expired -> 0.
|
|
33
|
+
def count(kind, id, window:, now: Time.now)
|
|
34
|
+
@store.get(SCOPE, key_for(kind, id, window_start(now, window))).to_i
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def window_start(now, window)
|
|
40
|
+
(now.to_i / window) * window
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def key_for(kind, id, start)
|
|
44
|
+
"#{kind}:#{id}:#{start}"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/insika/version.rb
CHANGED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Wiring
|
|
5
|
+
# SHARED composition core for both roots (§12 G4 / §11.2 B4): the minimal wiring
|
|
6
|
+
# (config/wiring.rb) and the concrete deployment (config/deployment.rb) were two
|
|
7
|
+
# near-identical composition roots. The duplication — backend→stores, event
|
|
8
|
+
# stream, registries + policy builtins, capability registry, hooks, the Executor,
|
|
9
|
+
# and the core Command Bus — lives here now; each root only layers on what
|
|
10
|
+
# legitimately differs (profiles, plain-vs-overlay tool registry, catalogs,
|
|
11
|
+
# guardrails config, context providers, and the root-specific bus commands).
|
|
12
|
+
#
|
|
13
|
+
# Two phases, no magic (a block would hit dynamic-constant assignment in the
|
|
14
|
+
# roots, which promote everything to public constants):
|
|
15
|
+
# 1. `spine(backend:)` — the parameter-free infra spine.
|
|
16
|
+
# 2. `build(spine:, ...)` — assembles the Executor + Command Bus on top, given
|
|
17
|
+
# the root's divergent collaborators. This is where the 6 CORE commands
|
|
18
|
+
# (incl. pause_task/approve_action) are registered — which is what removes
|
|
19
|
+
# the config.ru / serve_real.rb patch that used to bolt them on afterwards.
|
|
20
|
+
module Graph
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Backend by config: INSIKA_DB set → durable SQLite (survives restart, the
|
|
24
|
+
# prerequisite for Recovery); missing → ephemeral Memory (dev/demo). The same
|
|
25
|
+
# rule lived verbatim in both roots. Dual-read honors the legacy HARNESS_DB alias.
|
|
26
|
+
def backend_from_env(env = ENV)
|
|
27
|
+
db = Insika::EnvSchema.read("INSIKA_DB", env)
|
|
28
|
+
db && !db.empty? ? Insika::Stores::SQLite.new(path: db) : Insika::Stores::Memory.new
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Phase 1 — the infra spine that is IDENTICAL across roots. `extra_policy_
|
|
32
|
+
# builtins` covers the one real divergence (the minimal wiring also registers
|
|
33
|
+
# :workflow_allowlist; the deployment does not expose workflows).
|
|
34
|
+
def spine(backend:, extra_policy_builtins: {})
|
|
35
|
+
session_store = Insika::SessionStore.new(store: backend)
|
|
36
|
+
task_store = Insika::TaskStore.new(store: backend)
|
|
37
|
+
checkpoint_store = Insika::CheckpointStore.new(store: backend)
|
|
38
|
+
pending_action_store = Insika::PendingActionStore.new(store: backend)
|
|
39
|
+
delegation_store = Insika::DelegationStore.new(store: backend)
|
|
40
|
+
memory_store = Insika::MemoryStore.new(store: backend)
|
|
41
|
+
# RFC-0011 §6.4/§6.5: the two durable halves of a Shape B channel — the
|
|
42
|
+
# replies still owed to a platform, and the retry window that stops a
|
|
43
|
+
# redelivered webhook from becoming a second turn. Built unconditionally
|
|
44
|
+
# (they are empty and free when no channel is registered) so a deployment
|
|
45
|
+
# that turns a channel on later finds its state already durable.
|
|
46
|
+
outbox_store = Insika::OutboxStore.new(store: backend)
|
|
47
|
+
inbound_log = Insika::InboundLog.new(store: backend)
|
|
48
|
+
# RFC-0013 phase A: refinement RUNS (reports over real traffic). Runtime data,
|
|
49
|
+
# same backend as sessions/tasks — the collector and the command that write it
|
|
50
|
+
# are the root's business (deployment-only, like the memory commands).
|
|
51
|
+
refinement_store = Insika::RefinementStore.new(store: backend)
|
|
52
|
+
|
|
53
|
+
code_tool_registry = Insika::ToolRegistry.new
|
|
54
|
+
workflow_registry = Insika::WorkflowRegistry.new
|
|
55
|
+
|
|
56
|
+
policy_registry = Insika::PolicyRegistry.new
|
|
57
|
+
policy_registry.register(:tool_allowlist, Insika::Policy::Builtin::ToolAllowlist)
|
|
58
|
+
policy_registry.register(:skill_allowlist, Insika::Policy::Builtin::SkillAllowlist)
|
|
59
|
+
policy_registry.register(:approval_required, Insika::Policy::Builtin::ApprovalRequired)
|
|
60
|
+
extra_policy_builtins.each { |name, klass| policy_registry.register(name, klass) }
|
|
61
|
+
|
|
62
|
+
Spine.new(
|
|
63
|
+
backend: backend, event_stream: Insika::EventStream.new,
|
|
64
|
+
session_store: session_store, task_store: task_store,
|
|
65
|
+
checkpoint_store: checkpoint_store, pending_action_store: pending_action_store,
|
|
66
|
+
delegation_store: delegation_store,
|
|
67
|
+
memory_store: memory_store, refinement_store: refinement_store,
|
|
68
|
+
outbox_store: outbox_store, inbound_log: inbound_log,
|
|
69
|
+
code_tool_registry: code_tool_registry,
|
|
70
|
+
workflow_registry: workflow_registry, policy_registry: policy_registry,
|
|
71
|
+
capability_registry: Insika::CapabilityRegistry.new, hooks: Insika::Hooks.new,
|
|
72
|
+
channel_registry: Insika::ChannelRegistry.new
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Phase 2 — assemble the graph on top of a spine.
|
|
77
|
+
#
|
|
78
|
+
# tool_registry: effective registry the Executor uses (plain REGISTRY at the
|
|
79
|
+
# base; OverlayToolRegistry in the deployment).
|
|
80
|
+
# guardrails: a Safety::Factory — its input_guardrail becomes the single
|
|
81
|
+
# middleware and its output_validator the after-task hook, so
|
|
82
|
+
# both wirings compose RFC-0009 identically.
|
|
83
|
+
# executor_extra: optional Executor kwargs a root adds (deployment passes
|
|
84
|
+
# settings_store + tool_trace_store; the base passes none).
|
|
85
|
+
# edge_limiter: optional EdgeLimiter (item 33 / §12 G7). It goes BEFORE
|
|
86
|
+
# the InputGuardrail so a flood can't spend the LLM
|
|
87
|
+
# moderator; nil = no edge (parity).
|
|
88
|
+
def build(spine:, profiles:, tool_registry:, tool_catalog:, skill_catalog:,
|
|
89
|
+
prompt_catalog:, guardrails:, context_providers:, edge_limiter: nil,
|
|
90
|
+
executor_extra: {})
|
|
91
|
+
spine.hooks.register(:task, after: guardrails.output_validator)
|
|
92
|
+
middleware = Insika::MiddlewareStack.new([edge_limiter, guardrails.input_guardrail].compact)
|
|
93
|
+
|
|
94
|
+
context_builder = Insika::ContextBuilder.new(
|
|
95
|
+
providers: context_providers, event_stream: spine.event_stream, hooks: spine.hooks
|
|
96
|
+
)
|
|
97
|
+
policy_engine = Insika::Policy::Engine.new(
|
|
98
|
+
policy_registry: spine.policy_registry, event_stream: spine.event_stream
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# RFC-0011 §6.5. Always built: the registry starts empty, so `record` finds no
|
|
102
|
+
# channel and returns nil on every turn — the cost of wiring it is one nil
|
|
103
|
+
# check per completed turn, and the alternative is a second wiring path that
|
|
104
|
+
# only production exercises.
|
|
105
|
+
channel_delivery = Insika::ChannelDelivery.new(
|
|
106
|
+
channels: spine.channel_registry, outbox: spine.outbox_store,
|
|
107
|
+
session_store: spine.session_store, event_stream: spine.event_stream
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
executor = Insika::Executor.new(
|
|
111
|
+
context_builder: context_builder, policy_engine: policy_engine,
|
|
112
|
+
middleware: middleware, hooks: spine.hooks,
|
|
113
|
+
tool_registry: tool_registry, skill_catalog: skill_catalog, profiles: profiles,
|
|
114
|
+
session_store: spine.session_store, task_store: spine.task_store,
|
|
115
|
+
checkpoint_store: spine.checkpoint_store, event_stream: spine.event_stream,
|
|
116
|
+
workflow_registry: spine.workflow_registry, pending_action_store: spine.pending_action_store,
|
|
117
|
+
capability_registry: spine.capability_registry, tool_catalog: tool_catalog,
|
|
118
|
+
memory_store: spine.memory_store,
|
|
119
|
+
content_filter_factory: guardrails.content_filter_factory, # RFC-0009: stream redaction
|
|
120
|
+
delegation_store: spine.delegation_store, # RFC-0010 Fase 2: async delegation durability
|
|
121
|
+
channel_delivery: channel_delivery, # RFC-0011 §6.5: out-of-band reply delivery
|
|
122
|
+
**executor_extra
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
bus = build_core_bus(spine: spine, profiles: profiles, executor: executor)
|
|
126
|
+
|
|
127
|
+
Graph::Result.new(
|
|
128
|
+
backend: spine.backend, event_stream: spine.event_stream,
|
|
129
|
+
session_store: spine.session_store, task_store: spine.task_store,
|
|
130
|
+
checkpoint_store: spine.checkpoint_store, pending_action_store: spine.pending_action_store,
|
|
131
|
+
delegation_store: spine.delegation_store,
|
|
132
|
+
memory_store: spine.memory_store, refinement_store: spine.refinement_store,
|
|
133
|
+
outbox_store: spine.outbox_store, inbound_log: spine.inbound_log,
|
|
134
|
+
channel_registry: spine.channel_registry, channel_delivery: channel_delivery,
|
|
135
|
+
code_tool_registry: spine.code_tool_registry,
|
|
136
|
+
tool_registry: tool_registry, workflow_registry: spine.workflow_registry,
|
|
137
|
+
policy_registry: spine.policy_registry, capability_registry: spine.capability_registry,
|
|
138
|
+
tool_catalog: tool_catalog, skill_catalog: skill_catalog, prompt_catalog: prompt_catalog,
|
|
139
|
+
hooks: spine.hooks, guardrails: guardrails, middleware: middleware,
|
|
140
|
+
context_providers: context_providers, context_builder: context_builder,
|
|
141
|
+
policy_engine: policy_engine, profiles: profiles, executor: executor, bus: bus
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The CORE command surface every root needs — turn essentials + the operator
|
|
146
|
+
# controls (pause/approve) the Studio dispatches. Registering pause_task/
|
|
147
|
+
# approve_action HERE is the crux of B4: it retires the config.ru:28-34 patch.
|
|
148
|
+
def build_core_bus(spine:, profiles:, executor:)
|
|
149
|
+
bus = Insika::CommandBus.new
|
|
150
|
+
bus.register(:create_session,
|
|
151
|
+
Insika::Commands::CreateSession.new(session_store: spine.session_store, event_stream: spine.event_stream))
|
|
152
|
+
bus.register(:cancel_task,
|
|
153
|
+
Insika::Commands::CancelTask.new(task_store: spine.task_store, executor: executor))
|
|
154
|
+
bus.register(:pause_task,
|
|
155
|
+
Insika::Commands::PauseTask.new(task_store: spine.task_store, executor: executor))
|
|
156
|
+
bus.register(:approve_action,
|
|
157
|
+
Insika::Commands::ApproveAction.new(pending_action_store: spine.pending_action_store,
|
|
158
|
+
executor: executor, event_stream: spine.event_stream))
|
|
159
|
+
bus.register(:send_message,
|
|
160
|
+
Insika::Commands::SendMessage.new(profiles: profiles, session_store: spine.session_store,
|
|
161
|
+
task_store: spine.task_store, executor: executor,
|
|
162
|
+
inbound_log: spine.inbound_log))
|
|
163
|
+
bus.register(:resume_task,
|
|
164
|
+
Insika::Commands::ResumeTask.new(profiles: profiles, task_store: spine.task_store,
|
|
165
|
+
checkpoint_store: spine.checkpoint_store, executor: executor))
|
|
166
|
+
bus
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Infra spine (phase 1 output). Value object — the roots read these to promote
|
|
170
|
+
# them to their historic public constants (SESSION_STORE, REGISTRY, ...).
|
|
171
|
+
Spine = Struct.new(
|
|
172
|
+
:backend, :event_stream, :session_store, :task_store, :checkpoint_store,
|
|
173
|
+
:pending_action_store, :delegation_store, :memory_store, :refinement_store,
|
|
174
|
+
:outbox_store, :inbound_log, :code_tool_registry,
|
|
175
|
+
:workflow_registry, :policy_registry, :capability_registry, :hooks,
|
|
176
|
+
:channel_registry, keyword_init: true
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Full graph (phase 2 output). `code_tool_registry` is the plain code registry
|
|
180
|
+
# (REGISTRY); `tool_registry` is the effective one the Executor uses (the
|
|
181
|
+
# deployment's overlay, or the same code registry at the base).
|
|
182
|
+
Result = Struct.new(
|
|
183
|
+
:backend, :event_stream,
|
|
184
|
+
:session_store, :task_store, :checkpoint_store, :pending_action_store, :delegation_store,
|
|
185
|
+
:memory_store, :refinement_store, :outbox_store, :inbound_log,
|
|
186
|
+
:channel_registry, :channel_delivery,
|
|
187
|
+
:code_tool_registry, :tool_registry, :workflow_registry, :policy_registry, :capability_registry,
|
|
188
|
+
:tool_catalog, :skill_catalog, :prompt_catalog,
|
|
189
|
+
:hooks, :guardrails, :middleware,
|
|
190
|
+
:context_providers, :context_builder, :policy_engine,
|
|
191
|
+
:profiles, :executor, :bus,
|
|
192
|
+
keyword_init: true
|
|
193
|
+
) do
|
|
194
|
+
def durable? = backend.is_a?(Insika::Stores::SQLite)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|