insika 0.1.0 → 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.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -3
  3. data/README.md +1 -1
  4. data/bin/insika +22 -7
  5. data/docs/AGENTS.md +129 -5
  6. data/docs/CHANNELS.md +1 -1
  7. data/docs/CONTEXT.md +22 -5
  8. data/docs/DEPLOY.md +30 -10
  9. data/docs/EMBEDDING.md +11 -7
  10. data/docs/EVALS.md +1 -1
  11. data/docs/LOADTEST.md +3 -2
  12. data/docs/OBSERVABILITY.md +11 -2
  13. data/docs/REFINEMENT.md +6 -6
  14. data/docs/RELEASING.md +7 -7
  15. data/docs/RUNNING-LOCAL.md +1 -1
  16. data/docs/SECURITY.md +24 -11
  17. data/docs/SKILLS.md +189 -3
  18. data/docs/WHY.md +1 -1
  19. data/docs/WORKFLOWS.md +2 -2
  20. data/docs/index.md +1 -1
  21. data/docs/onboarding/start.md +1 -1
  22. data/lib/insika/agent_profile.rb +89 -22
  23. data/lib/insika/alert_dispatcher.rb +139 -0
  24. data/lib/insika/baseline_store.rb +2 -2
  25. data/lib/insika/budget_ledger.rb +135 -0
  26. data/lib/insika/channel_delivery.rb +14 -11
  27. data/lib/insika/channel_registry.rb +1 -1
  28. data/lib/insika/channels/relay.rb +3 -3
  29. data/lib/insika/channels/web/widget.js +2 -2
  30. data/lib/insika/channels/web.rb +7 -7
  31. data/lib/insika/channels/webhook.rb +58 -0
  32. data/lib/insika/chat_builder.rb +62 -13
  33. data/lib/insika/circuit_state.rb +114 -0
  34. data/lib/insika/coercion.rb +8 -0
  35. data/lib/insika/commands/agent_payload.rb +5 -3
  36. data/lib/insika/commands/create_agent.rb +2 -2
  37. data/lib/insika/commands/create_session.rb +1 -1
  38. data/lib/insika/commands/delete_llm_provider.rb +1 -1
  39. data/lib/insika/commands/delete_skill.rb +43 -0
  40. data/lib/insika/commands/gate_refinement.rb +12 -12
  41. data/lib/insika/commands/import_mcp_tools.rb +1 -1
  42. data/lib/insika/commands/import_tools.rb +4 -4
  43. data/lib/insika/commands/issue_tenant_token.rb +41 -0
  44. data/lib/insika/commands/resolve_refinement.rb +1 -1
  45. data/lib/insika/commands/revoke_token.rb +39 -0
  46. data/lib/insika/commands/rotate_tenant_token.rb +43 -0
  47. data/lib/insika/commands/run_refinement.rb +5 -5
  48. data/lib/insika/commands/send_message.rb +9 -9
  49. data/lib/insika/commands/set_agent_tools.rb +1 -1
  50. data/lib/insika/commands/set_skill_agents.rb +60 -19
  51. data/lib/insika/commands/trigger_workflow.rb +1 -1
  52. data/lib/insika/commands/update_agent.rb +1 -1
  53. data/lib/insika/commands/write_data_tool.rb +1 -1
  54. data/lib/insika/commands/write_golden.rb +1 -1
  55. data/lib/insika/commands/write_skill.rb +19 -9
  56. data/lib/insika/config_store.rb +8 -4
  57. data/lib/insika/context/builder.rb +2 -2
  58. data/lib/insika/context/fragment.rb +27 -3
  59. data/lib/insika/context/priority.rb +3 -2
  60. data/lib/insika/context/providers/memory.rb +1 -1
  61. data/lib/insika/context/providers/request.rb +1 -1
  62. data/lib/insika/context/providers/session.rb +17 -2
  63. data/lib/insika/context/providers/skill.rb +5 -1
  64. data/lib/insika/context/providers/skill_trigger.rb +128 -0
  65. data/lib/insika/context_trace_store.rb +92 -0
  66. data/lib/insika/delegation_store.rb +2 -2
  67. data/lib/insika/doctor.rb +250 -5
  68. data/lib/insika/dsl/runtime.rb +12 -9
  69. data/lib/insika/dsl/server_boot.rb +4 -3
  70. data/lib/insika/dsl/system.rb +1 -1
  71. data/lib/insika/dsl.rb +72 -15
  72. data/lib/insika/edge_limiter.rb +144 -6
  73. data/lib/insika/egress_guard.rb +3 -3
  74. data/lib/insika/env_schema.rb +13 -10
  75. data/lib/insika/errors.rb +61 -5
  76. data/lib/insika/evals/assertions.rb +12 -12
  77. data/lib/insika/evals/baseline.rb +3 -3
  78. data/lib/insika/evals/golden.rb +8 -8
  79. data/lib/insika/evals/judge.rb +7 -7
  80. data/lib/insika/evals/pairwise.rb +3 -3
  81. data/lib/insika/evals/report.rb +2 -2
  82. data/lib/insika/evals/runner.rb +6 -6
  83. data/lib/insika/evals/transport.rb +2 -2
  84. data/lib/insika/event_stream.rb +23 -5
  85. data/lib/insika/executor.rb +423 -108
  86. data/lib/insika/frontmatter.rb +1 -1
  87. data/lib/insika/golden_store.rb +2 -2
  88. data/lib/insika/http_client.rb +3 -3
  89. data/lib/insika/inbound_log.rb +1 -1
  90. data/lib/insika/llm_configurator.rb +3 -3
  91. data/lib/insika/loop_detector.rb +143 -0
  92. data/lib/insika/mcp_http_client.rb +4 -4
  93. data/lib/insika/mcp_tool_ingestor.rb +6 -6
  94. data/lib/insika/message_origin.rb +2 -2
  95. data/lib/insika/model_resolver.rb +1 -1
  96. data/lib/insika/model_selection.rb +5 -4
  97. data/lib/insika/onboarding.rb +2 -2
  98. data/lib/insika/outbox_store.rb +2 -2
  99. data/lib/insika/overlay_tool_registry.rb +3 -4
  100. data/lib/insika/pack.rb +3 -3
  101. data/lib/insika/pack_importer.rb +17 -15
  102. data/lib/insika/pending_action_store.rb +1 -1
  103. data/lib/insika/plugin/loader.rb +2 -2
  104. data/lib/insika/policy/policy.rb +1 -1
  105. data/lib/insika/profile_source.rb +12 -6
  106. data/lib/insika/provider_error_classifier.rb +160 -0
  107. data/lib/insika/queue_policy.rb +2 -2
  108. data/lib/insika/recovery.rb +47 -6
  109. data/lib/insika/refinement/candidate.rb +4 -4
  110. data/lib/insika/refinement/evidence_collector.rb +6 -6
  111. data/lib/insika/refinement/gate.rb +7 -7
  112. data/lib/insika/refinement/panel.rb +7 -7
  113. data/lib/insika/refinement/proposer.rb +9 -9
  114. data/lib/insika/refinement_store.rb +12 -12
  115. data/lib/insika/reliability.rb +185 -0
  116. data/lib/insika/safety/config.rb +2 -2
  117. data/lib/insika/safety/detectors.rb +5 -5
  118. data/lib/insika/safety/factory.rb +3 -3
  119. data/lib/insika/safety/input_guardrail.rb +19 -4
  120. data/lib/insika/safety/moderator.rb +19 -11
  121. data/lib/insika/safety/output_filter.rb +2 -2
  122. data/lib/insika/safety/output_validator.rb +2 -2
  123. data/lib/insika/safety/safe_responses.rb +1 -1
  124. data/lib/insika/sandbox/boundary.rb +2 -2
  125. data/lib/insika/sandbox.rb +1 -1
  126. data/lib/insika/server/app.rb +223 -51
  127. data/lib/insika/server/boot.rb +4 -4
  128. data/lib/insika/server/rack_app.rb +15 -7
  129. data/lib/insika/server/responses.rb +18 -8
  130. data/lib/insika/server/tenant_auth.rb +61 -0
  131. data/lib/insika/session_actor.rb +3 -3
  132. data/lib/insika/session_store.rb +1 -1
  133. data/lib/insika/settings_store.rb +5 -5
  134. data/lib/insika/shutdown.rb +4 -4
  135. data/lib/insika/skill_catalog.rb +127 -20
  136. data/lib/insika/skill_store.rb +70 -22
  137. data/lib/insika/steer_injector.rb +1 -1
  138. data/lib/insika/store.rb +1 -1
  139. data/lib/insika/studio/app.rb +183 -61
  140. data/lib/insika/studio/assets/dist/application.js +25 -24
  141. data/lib/insika/studio/forms.rb +13 -18
  142. data/lib/insika/studio/nav_icons.rb +1 -1
  143. data/lib/insika/studio/views/_message.erb +2 -2
  144. data/lib/insika/studio/views/agent_detail.erb +2 -2
  145. data/lib/insika/studio/views/agents.erb +1 -1
  146. data/lib/insika/studio/views/refinement.erb +4 -4
  147. data/lib/insika/studio/views/session.erb +78 -3
  148. data/lib/insika/studio/views/settings.erb +7 -12
  149. data/lib/insika/studio/views/skills.erb +67 -12
  150. data/lib/insika/subagent_graph.rb +3 -3
  151. data/lib/insika/task_actor.rb +3 -3
  152. data/lib/insika/task_store.rb +1 -1
  153. data/lib/insika/telemetry/pricing.rb +3 -3
  154. data/lib/insika/telemetry/recorder.rb +1 -1
  155. data/lib/insika/telemetry.rb +2 -2
  156. data/lib/insika/testing/store_contract.rb +27 -27
  157. data/lib/insika/tick.rb +122 -0
  158. data/lib/insika/token_store.rb +168 -0
  159. data/lib/insika/tool_assembly.rb +5 -5
  160. data/lib/insika/tool_definition.rb +8 -8
  161. data/lib/insika/tool_envelope.rb +1 -1
  162. data/lib/insika/tool_manifest.rb +6 -6
  163. data/lib/insika/tool_output_compressor.rb +100 -0
  164. data/lib/insika/tool_store.rb +1 -1
  165. data/lib/insika/tool_trace_store.rb +1 -1
  166. data/lib/insika/tools/concurrency.rb +2 -2
  167. data/lib/insika/tools/data_defined_tool.rb +4 -5
  168. data/lib/insika/tools/load_skill.rb +61 -3
  169. data/lib/insika/tools/stuck_signal.rb +44 -0
  170. data/lib/insika/tools/subagent.rb +4 -4
  171. data/lib/insika/tools/subagents.rb +1 -1
  172. data/lib/insika/turn_output.rb +2 -2
  173. data/lib/insika/turn_state.rb +17 -13
  174. data/lib/insika/turn_timing.rb +2 -2
  175. data/lib/insika/usage_ledger.rb +1 -1
  176. data/lib/insika/version.rb +1 -1
  177. data/lib/insika/wiring/graph.rb +77 -26
  178. data/lib/insika/workflow.rb +1 -1
  179. data/lib/insika/workflow_registry.rb +1 -1
  180. data/lib/insika.rb +32 -15
  181. metadata +19 -2
  182. data/lib/insika/server/admin_auth.rb +0 -29
@@ -7,7 +7,7 @@ module Insika
7
7
  # The convention is YAML, but real packs carry PROSE in `description` — with `: `
8
8
  # (colon + space), quotes, parentheses — which STRICT YAML rejects
9
9
  # ("mapping values are not allowed in this context"). The OpenClaw gateway
10
- # tolerates it; the insika has to tolerate it too (NF2: the same pack must hold).
10
+ # tolerates it; the insika has to tolerate it too (the same pack must hold).
11
11
  #
12
12
  # Strategy: try YAML (respects quoted / multi-line / lists); if the YAML
13
13
  # fails OR doesn't yield a Hash, fall back to a LINE-BY-LINE parse that splits on
@@ -5,7 +5,7 @@ require "time"
5
5
  require "yaml"
6
6
 
7
7
  module Insika
8
- # AUTHORED eval cases (RFC-0008 §3.1, promoted to a store by RFC-0013 §3.7).
8
+ # AUTHORED eval cases (promoted to a store by).
9
9
  #
10
10
  # A golden case used to be a YAML file in `evals/golden/`, which means only someone
11
11
  # with a checkout and a text editor could add one. The rubric is the part of an eval
@@ -133,7 +133,7 @@ module Insika
133
133
  def case_hash(golden)
134
134
  h = { "id" => golden.id, "agent" => golden.agent, "turns" => golden.turns }
135
135
  h["requires"] = golden.requires unless golden.requires.empty?
136
- # Same rule for `reference` (RFC-0014 §3.4): omitted when absent, never dropped
136
+ # Same rule for `reference`: omitted when absent, never dropped
137
137
  # when present. A case that lost its reference in a round-trip would stop being
138
138
  # compared against the incumbent and the report would look identical.
139
139
  h["reference"] = golden.reference unless golden.reference.empty?
@@ -5,11 +5,11 @@ require "uri"
5
5
  require_relative "coercion"
6
6
 
7
7
  module Insika
8
- # Default HTTP client for data-tools. Net::HTTP (stdlib, zero-dep — spec §10)
8
+ # Default HTTP client for data-tools. Net::HTTP (stdlib, zero-dep — spec)
9
9
  # with its own socket timeouts (mitigates reactor blocking even if the
10
- # envelope timer doesn't fire) and a response-size CAP via streaming (NF2,
10
+ # envelope timer doesn't fire) and a response-size CAP via streaming (
11
11
  # avoids OOM). It is INJECTABLE: tests pass a double (none hit the network);
12
- # Stage C can swap in async-http without touching DataDefinedTool.
12
+ # can swap in async-http without touching DataDefinedTool.
13
13
  #
14
14
  # Contract: request(method:, url:, headers:, body:, timeout:) -> { status:, body: }
15
15
  # (+ `location:` on a 3xx). It does NOT follow redirects: the destination is
@@ -4,7 +4,7 @@ require "time"
4
4
 
5
5
  module Insika
6
6
  # Short-lived memory of inbound event ids, so a platform's retry does not become
7
- # a second LLM turn and a second reply (RFC-0011 §6.4).
7
+ # a second LLM turn and a second reply.
8
8
  #
9
9
  # Every messaging platform retries a webhook it did not see acked in time, and a
10
10
  # relay consumer that hands us its own queue does the same. Without this, the
@@ -12,7 +12,7 @@ module Insika
12
12
  # matching accessor) does NOT blow up: it goes into `skipped` (degrades to "restart
13
13
  # recommended", like OpenClaw), the rest applies.
14
14
  #
15
- # ⚠️ GOTCHA — the DEFAULT target is a global singleton (RubyLLM research, §10).
15
+ # ⚠️ GOTCHA — the DEFAULT target is a global singleton (RubyLLM research).
16
16
  # With no `configure:`, `apply`/`unapply` mutate the PROCESS-WIDE config
17
17
  # (`RubyLLM.config`). They are therefore admin operations (rare, operator-driven:
18
18
  # a provider key/base edit in the Studio), NOT a per-request/per-turn path — a
@@ -22,7 +22,7 @@ module Insika
22
22
  # MODEL per turn — the model is chosen at chat build time (ModelResolver ->
23
23
  # chat(model:)), never by mutating the global.
24
24
  #
25
- # PER-GRAPH credentials are no longer hypothetical (RFC-0017 A2): the DSL runtime
25
+ # PER-GRAPH credentials are no longer hypothetical: the DSL runtime
26
26
  # passes `configure:` targeting its own `RubyLLM.context` — an isolated config dup
27
27
  # — so an operator's key edit in an EMBEDDED graph applies to that graph and stops
28
28
  # there. See `DSL::Runtime#llm_configure` and docs/EMBEDDING.md.
@@ -62,7 +62,7 @@ module Insika
62
62
  end
63
63
 
64
64
  # UNDOES a provider's config in RubyLLM at runtime (delete without a restart,
65
- # §9.5): clears `<api>_api_key`/`<api>_api_base`. A provider that RubyLLM
65
+ # clears `<api>_api_key`/`<api>_api_base`. A provider that RubyLLM
66
66
  # doesn't recognize (no accessor) -> unapplied: false (nothing applied, nothing to undo).
67
67
  # -> { unapplied: bool }.
68
68
  def unapply(api)
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Insika
4
+ # loop detection by (tool, args) hash, with a ONE-SHOT intervention.
5
+ #
6
+ # `max_tool_calls` bounds how MANY tool calls a turn makes, not how useful they
7
+ # are: a model retrying the exact same call — same tool, identical arguments —
8
+ # after an empty or error result burns the whole budget doing something that
9
+ # was settled on the first repeat. This detector is the engine saying so, once.
10
+ #
11
+ # The streak is CONSECUTIVE and turn-scoped, like the max_tool_calls counter it
12
+ # sits next to in ChatBuilder#wire_callbacks: a call revisited much later in a
13
+ # long turn is not the pathology being caught, and semantic ("nearly the same")
14
+ # matching is how a guard-rail starts eating legitimate retries.
15
+ #
16
+ # Two invariants, both borrowed from SteerInjector, because the
17
+ # intervention is a `user` message appended mid-loop:
18
+ #
19
+ # · **Batch boundary only.** The append happens after the LAST tool result of a
20
+ # batch closes — a `user` message between two tool results is rejected by
21
+ # Anthropic outright. Same arithmetic: an assistant message opens a batch of
22
+ # N, the Nth `role: tool` message closes it.
23
+ # · **A halted batch receives nothing.** With `halt_when` there is no next
24
+ # model step; a warning appended there would sit unanswered forever.
25
+ #
26
+ # The repeated call itself STILL RUNS — fabricating a synthetic result would
27
+ # teach the model that tools lie (the failure refuses). The
28
+ # warning rides after the truth; only a repeat that arrives AFTER the warning
29
+ # was spent aborts, through the existing TimeoutError(stage: :tool_limit) path.
30
+ class LoopDetector
31
+ # The one intervention text, verbatim — a fixed engine sentence, so a report
32
+ # can identify it without an origin stamp (chat messages carry none).
33
+ def self.intervention(name, streak)
34
+ "You have called `#{name}` with identical arguments #{streak} times in a row and " \
35
+ "received the same result every time. Repeating it will not produce new information. " \
36
+ "Do not call it again with the same arguments — answer with what you already have, " \
37
+ "or change your approach."
38
+ end
39
+
40
+ # chat: the turn's chat — must answer #add_message (the boundary append).
41
+ # limit: the streak that triggers the intervention (profile's
42
+ # max_tool_repeat). Values < 2 mean OFF: a "streak of 1" is every
43
+ # call, which is meaningless.
44
+ # emit: ->(type, data) — the Executor's emitter, bound to the task.
45
+ def initialize(chat:, limit:, emit:)
46
+ @chat = chat
47
+ @limit = limit
48
+ @emit = emit
49
+ @last = nil # fingerprint of the previous call (nil = none yet)
50
+ @streak = 0
51
+ @intervened = false # the ONE warning of this turn has been delivered
52
+ @pending = false # detection fired; waiting for the batch boundary
53
+ @expected = nil # tool calls announced by the batch in flight
54
+ @seen = 0
55
+ @halted = false
56
+ end
57
+
58
+ # From ChatBuilder's before_tool_call. Raises BEFORE the call executes once
59
+ # the warning is spent — bounded spend is the point of aborting here.
60
+ def tool_call(name, arguments)
61
+ fingerprint = [name.to_s, canonical(arguments)]
62
+ if fingerprint == @last
63
+ @streak += 1
64
+ else
65
+ # A different call broke the run: the loop resolved itself, so a warning
66
+ # armed earlier is moot — it must not fire later naming the WRONG call.
67
+ @streak = 1
68
+ @pending = false
69
+ end
70
+ @last = fingerprint
71
+ return if @streak < @limit
72
+
73
+ if @intervened
74
+ raise Insika::TimeoutError.new(
75
+ "tool loop detected (#{name} repeated with identical arguments after a warning)",
76
+ stage: :tool_limit)
77
+ end
78
+ @pending = true
79
+ end
80
+
81
+ # From ChatBuilder's after_tool_result, with the RAW result — the only place
82
+ # a Tool::Halt is still recognizable (SteerInjector's comment applies here).
83
+ def tool_result(result)
84
+ @halted = true if defined?(RubyLLM::Tool::Halt) && result.is_a?(RubyLLM::Tool::Halt)
85
+ end
86
+
87
+ # RubyLLM after_message. An assistant message carrying tool calls OPENS a
88
+ # batch; the Nth tool result CLOSES it — the one boundary where appending
89
+ # is valid.
90
+ def message_ended(message)
91
+ role = field(message, :role).to_s
92
+ return open_batch(message) if role == "assistant"
93
+ return unless role == "tool" && @expected
94
+
95
+ @seen += 1
96
+ intervene! if @seen >= @expected
97
+ end
98
+
99
+ private
100
+
101
+ def open_batch(message)
102
+ calls = field(message, :tool_calls)
103
+ size = calls.respond_to?(:size) ? calls.size : 0
104
+ # No tool call = the model talking; the turn is ending and a pending
105
+ # warning is moot — the loop resolved itself.
106
+ return @expected = nil if size.zero?
107
+
108
+ @expected = size
109
+ @seen = 0
110
+ @halted = false
111
+ end
112
+
113
+ def intervene!
114
+ @expected = nil
115
+ return unless @pending
116
+ @pending = false
117
+ return if @halted # nothing will read it (halt_when): drop, never deliver
118
+
119
+ @intervened = true
120
+ name, = @last
121
+ @chat.add_message(role: :user, content: self.class.intervention(name, @streak))
122
+ # Counts and the tool name, never the arguments — order numbers are PII.
123
+ @emit.call(:tool_loop_intervened, { name: name, streak: @streak })
124
+ end
125
+
126
+ # (name, args) hash: symbols vs strings and key order must not split an
127
+ # identical call into two fingerprints. Compared with ==, never hashed.
128
+ def canonical(value)
129
+ case value
130
+ when Hash then value.map { |k, v| [k.to_s, canonical(v)] }.sort_by(&:first)
131
+ when Array then value.map { |v| canonical(v) }
132
+ else value
133
+ end
134
+ end
135
+
136
+ def field(message, name)
137
+ return message.public_send(name) if message.respond_to?(name)
138
+ return message[name] || message[name.to_s] if message.respond_to?(:[])
139
+
140
+ nil
141
+ end
142
+ end
143
+ end
@@ -3,19 +3,19 @@
3
3
  require "json"
4
4
 
5
5
  module Insika
6
- # MINIMAL MCP client over HTTP JSON-RPC (Phase 7, Stage E). Discovers the tools
6
+ # MINIMAL MCP client over HTTP JSON-RPC. Discovers the tools
7
7
  # of an MCP instance with HTTP transport by making a JSON-RPC 2.0 `tools/list`
8
8
  # POST to the instance endpoint, behind the EgressGuard (SSRF — the url comes
9
- # from editable config, NF4). It is the DEFAULT client injected into the
9
+ # from editable config). It is the DEFAULT client injected into the
10
10
  # McpToolIngestor; tests pass a Fake (duck-typed) in its place.
11
11
  #
12
12
  # Contract (MCP client duck-type): `#list_tools -> [{name, description,
13
13
  # inputSchema}]` — the same MCP envelope that the ToolManifest adapter normalizes.
14
14
  #
15
- # SCOPE (bounded, D8): only the minimal handshake of ONE stateless `tools/list`
15
+ # SCOPE (bounded): only the minimal handshake of ONE stateless `tools/list`
16
16
  # POST. Does NOT implement the full MCP session lifecycle (initialize/protocol
17
17
  # negotiation/session-id/notifications) nor the stdio transport — that is the
18
- # "real MCP transport", later work (out-of-scope, see spec §4 D8). It serves
18
+ # "real MCP transport", later work (out-of-scope, see spec). It serves
19
19
  # simple HTTP MCP servers (direct JSON-RPC) and proves the ingestion seam.
20
20
  class McpHttpClient
21
21
  JSONRPC_VERSION = "2.0"
@@ -3,16 +3,16 @@
3
3
  require "json"
4
4
 
5
5
  module Insika
6
- # LIVE MCP ingestion (Phase 7, Stage E / spec §4 D8): discovers the tools of an
6
+ # LIVE MCP ingestion (/ spec): discovers the tools of an
7
7
  # MCP instance at RUNTIME (no hand-written manifest) and ingests them as
8
8
  # data-tools. Given an McpStore instance + an INJECTABLE MCP client
9
9
  # (duck-typed: `#list_tools -> [{name, description, inputSchema}]`), it builds a
10
- # ToolManifest and REUSES the Stage B ingestion path (the :import_tools Command:
10
+ # ToolManifest and REUSES the ingestion path (the:import_tools Command:
11
11
  # batch upsert into the ToolStore + hot reload + per-tool report + partial-
12
12
  # failure isolation R4). The ToolManifest MCP adapter (`inputSchema`) is reused
13
13
  # — no schema parsing here.
14
14
  #
15
- # GENERIC (NF1): nothing here mentions achei/openclaw. The MCP instance is DATA in the store.
15
+ # GENERIC: nothing here mentions a consumer/gateway. The MCP instance is DATA in the store.
16
16
  #
17
17
  # BINDING STRATEGY (this stage's choice, bounded):
18
18
  # Each discovered tool becomes an HTTP data-tool that makes a JSON-RPC 2.0
@@ -23,10 +23,10 @@ module Insika
23
23
  # runs through the SAME HTTP path as the other data-tools (egress guard, secret
24
24
  # headers, hot reload) — no new execution code.
25
25
  #
26
- # Each tool gets `group: "mcp:<instance>"` so the Stage C per-group gating
26
+ # Each tool gets `group: "mcp:<instance>"` so the per-group gating
27
27
  # (tools_allow_groups) works for free.
28
28
  #
29
- # DEFERRED / OUT-OF-SCOPE (documented — spec §4 D8):
29
+ # DEFERRED / OUT-OF-SCOPE (documented — spec):
30
30
  # - Real MCP transport: only instances with a `url` (http transport) are ingestible;
31
31
  # stdio has no HTTP endpoint -> raises a clear error (later work).
32
32
  # - MCP session lifecycle (initialize/negotiation/session-id/notifications) and the
@@ -65,7 +65,7 @@ module Insika
65
65
  if url.nil?
66
66
  raise Insika::ValidationError,
67
67
  "MCP instance '#{name}' has no url: live ingestion requires HTTP transport " \
68
- "(stdio is later work — D8)"
68
+ "(stdio is later work)"
69
69
  end
70
70
 
71
71
  tools = Array((client || @client_factory.call(record)).list_tools)
@@ -7,11 +7,11 @@ module Insika
7
7
  # two come apart constantly:
8
8
  #
9
9
  # · The engine delivers an async subagent's result to the parent as a NEW turn
10
- # (RFC-0010) — a `user` message the engine wrote.
10
+ # a `user` message the engine wrote.
11
11
  # · A guardrail short-circuits with a safe reply — an `assistant` message produced
12
12
  # with zero LLM calls.
13
13
  # · A consumer composes context blocks into the input it sends (`<memoria> …`,
14
- # `<cacau_cep_obrigatorio> …`) — a `user` message the customer never typed.
14
+ # `<store_cep_required> …`) — a `user` message the customer never typed.
15
15
  # · In an imported transcript, a human operator types after a handoff — an
16
16
  # `assistant` message no model produced.
17
17
  #
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Insika
4
- # LLM config v2 resolution (FOLLOWUP §10). Turns the three config layers into a
4
+ # LLM config v2 resolution. Turns the three config layers into a
5
5
  # single ModelSelection at turn start:
6
6
  #
7
7
  # Chat (per-session pin) > Agent (profile.model) > Platform default (Settings)
@@ -14,8 +14,9 @@ module Insika
14
14
  # model / platform default is NOT pinned -> fallbacks apply.
15
15
  # params -> generation params (Hash of symbols: temperature/max_tokens/thinking)
16
16
  # fallbacks -> ordered [{ model:, provider: }] to try when NOT pinned. The
17
- # mid-turn ROTATION across this chain is a follow-up; today the
18
- # chain is resolved + surfaced (source/pinned) for telemetry.
17
+ # mid-turn ROTATION across this chain (plus the profile's own
18
+ # reliability["fallback"] refs) is WS3's Reliability coordinator;
19
+ # here the chain is resolved + surfaced (source/pinned) for it.
19
20
  ModelSelection = Data.define(:model, :provider, :source, :pinned, :params, :fallbacks) do
20
21
  def initialize(model:, provider: nil, source: :platform_default, pinned: false,
21
22
  params: {}, fallbacks: [])
@@ -70,7 +71,7 @@ module Insika
70
71
  chat.with_params(**payload) unless payload.empty?
71
72
  end
72
73
 
73
- # The resolved reasoning control (§10, 4-layer). Two axes folded into one field:
74
+ # The resolved reasoning control (4-layer). Two axes folded into one field:
74
75
  # off -> reasoning DISABLED (thinking:{type:disabled})
75
76
  # on -> reasoning ENABLED, provider-default effort
76
77
  # low|medium|high -> reasoning enabled at that effort (reasoning_effort)
@@ -105,7 +106,7 @@ module Insika
105
106
  def present?(v) = Insika::Coercion.present?(v)
106
107
  end
107
108
 
108
- # The selectable reasoning values (§10, 4-layer). Blank/absent = inherit the
109
+ # The selectable reasoning values (4-layer). Blank/absent = inherit the
109
110
  # broader layer; these are the explicit choices. Shared with the Studio forms.
110
111
  # Defined on the class OUTSIDE the Data.define block on purpose: a constant
111
112
  # assigned inside the block would land in the enclosing lexical scope (Insika),
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Insika
4
- # LLM-first onboarding surface (item 20 / §5.6). The "Flue trick": the insika
4
+ # LLM-first onboarding surface. The "Flue trick": the insika
5
5
  # serves, from itself, a `start.md` addressed to the DEVELOPER'S OWN coding agent
6
6
  # ("Read <base>/start.md then help me build my first agent") plus a machine-readable
7
7
  # `/models.json` and the public docs mirrored as raw markdown. It is `rails new`
@@ -31,7 +31,7 @@ module Insika
31
31
  MODELS_SCHEMA_VERSION = 1
32
32
 
33
33
  # The PUBLIC docs allowlist, repo-relative: slug => path. Explicit on purpose —
34
- # the gitignored internal docs (FOLLOWUP / techspec / TRANSLATION-TRACKER / …)
34
+ # the gitignored internal docs (/ TRANSLATION-TRACKER / …)
35
35
  # are NEVER here, so /docs can only ever serve OSS material. Kept in sync with the
36
36
  # tracked `.md` prose (README + docs/*.md).
37
37
  PUBLIC_DOCS = {
@@ -4,11 +4,11 @@ require "securerandom"
4
4
  require "time"
5
5
 
6
6
  module Insika
7
- # Durable record of one pending OUTBOUND reply (RFC-0011 §6.5). A Shape B
7
+ # Durable record of one pending OUTBOUND reply. A Shape B
8
8
  # channel answers out of band — the turn ends and the reply is POSTed to the
9
9
  # platform (or, for a relay, to the consumer's own callback) afterwards — so
10
10
  # "the answer exists but the recipient is not on this connection" is exactly
11
- # the problem RFC-0010 Phase 2 already solved for async delegation. This store
11
+ # the problem already solved for async delegation. This store
12
12
  # is DelegationStore's shape with a different recipient, deliberately: a second
13
13
  # invention here would be a second thing to get wrong.
14
14
  #
@@ -3,17 +3,16 @@
3
3
  module Insika
4
4
  # DYNAMIC tool registry: composes the CODE registry (base, built at boot,
5
5
  # immutable) with the DATA-DEFINED tools from the ToolStore. Drop-in for ToolRegistry —
6
- # the Executor/ToolCatalog/ToolEnvelope only use entries/resolve/side_effect?. Phase 5,
7
- # Step B / D2.
6
+ # the Executor/ToolCatalog/ToolEnvelope only use entries/resolve/side_effect?.,
8
7
  #
9
8
  # Rules:
10
9
  # - COLLISION: the base (code) ALWAYS wins — a data-tool cannot hijack
11
10
  # the name of a code tool (security, R3). The authoring Command also
12
11
  # refuses to create with a colliding name (code_tool?), but the defense stays here.
13
12
  # - HOT: `reload` re-reads the store and swaps the dynamic index atomically — a
14
- # new/edited data-tool takes effect on the next turn without a restart (F5), mirroring
13
+ # new/edited data-tool takes effect on the next turn without a restart, mirroring
15
14
  # SkillCatalog.reload. An in-flight turn has already captured the index.
16
- # - PARITY (NF1): empty ToolStore ⇒ entries/resolve/side_effect? identical to the
15
+ # PARITY: empty ToolStore ⇒ entries/resolve/side_effect? identical to the
17
16
  # pure base. The base (config/wiring.rb) does not even use the overlay — zero regression.
18
17
  #
19
18
  # The data-tools enter as NORMAL Registry::Entry (optional: false) — they obey
data/lib/insika/pack.rb CHANGED
@@ -3,11 +3,11 @@
3
3
  require "json"
4
4
 
5
5
  module Insika
6
- # Provisioning pack (Phase 6/D4/F6): the PORTABLE form of an agent — a
6
+ # Provisioning pack: the PORTABLE form of an agent — a
7
7
  # manifest + the prompt files + the skills + the data-tool defs. It's what
8
8
  # `docs/prompt-base/06` describes as a workspace, here in a value object
9
9
  # consumable by the PackImporter (which emits the authoring Commands). GENERIC
10
- # per project (NF1): the engine doesn't know achei-b2b — the pack is the contract.
10
+ # per project: the engine doesn't know any consumer — the pack is the contract.
11
11
  #
12
12
  # config: Hash — manifest (AgentProfile.build attrs: id/model/provider/
13
13
  # limits/metadata/tools_deferred/…). `id`/`model` required there.
@@ -16,7 +16,7 @@ module Insika
16
16
  # skills: { "escalation-to-human" => "<SKILL.md>", ... } — 1 per skill.
17
17
  # tools: [ { ToolDefinition hash }, ... ] — the pack's data-tool defs.
18
18
  #
19
- # Two sources: `from_h` (GatewayClient JSON — provisioning API, task 8) and
19
+ # Two sources: `from_h` (GatewayClient JSON — provisioning API) and
20
20
  # `from_dir` (a folder on disk per docs/prompt-base/06 — authoring/CLI).
21
21
  Pack = Data.define(:config, :files, :skills, :tools) do
22
22
  # Raw Hash (string|symbol keys) -> Pack. Tolerates both key conventions
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Insika
4
- # Pack importer (Phase 6/D4/F6, task 7): reads a Pack and emits the ALREADY
4
+ # Pack importer: reads a Pack and emits the ALREADY
5
5
  # existing authoring Commands — create_agent/update_agent + write_agent_file +
6
6
  # write_skill + write_data_tool — making an agent provisionable at runtime from
7
- # a standardized pack. It's the piece that achei-b2b's `GatewayClient`/
8
- # `ProvisionStore` triggers (via the provisioning API, task 8).
7
+ # a standardized pack. It's the piece a consumer's provisioning client
8
+ # triggers (via the provisioning API).
9
9
  #
10
- # GENERIC (NF1): nothing here mentions achei-b2b — the pack is the contract. Tool
10
+ # GENERIC: nothing here mentions a consumer — the pack is the contract. Tool
11
11
  # names come from the PACK, so prompts<->tools stay consistent by construction
12
- # (NF2). It doesn't write to the store directly: it only dispatches Commands on
12
+ # It doesn't write to the store directly: it only dispatches Commands on
13
13
  # the bus (the same transport discipline) + READS the ProfileSource to decide
14
14
  # create vs update.
15
15
  #
@@ -58,13 +58,15 @@ module Insika
58
58
  # its OWN pack (per-store isolation) and re-provisioning removes what left.
59
59
  # - prompt_files = the pack's .md files (write_agent_file also registers;
60
60
  # union is a no-op). Setting here makes the list authoritative (removes the ones that left).
61
- # - skills = the pack's skills/ dirs (explicit allowlist; [] if none in the
62
- # pack and config didn't declare any — never nil=all, which would leak
63
- # skills from other stores).
61
+ # - skills = the pack's skills/ dirs (explicit allowlist; [] when the pack
62
+ # has none — never nil=all, which would leak skills from other stores).
64
63
  # - tools_allow = (config.tools_allow) ∪ (the pack's tool names) — guarantees
65
- # the agent can call its own data-tools (NF2).
64
+ # the agent can call its own data-tools. [] when neither exists —
65
+ # never nil=all: the ToolStore is GLOBAL, so a tool-less pack would
66
+ # otherwise see every other store's tools (store A's pack calling
67
+ # store B's set_location is exactly that leak).
66
68
  # - tools_allow_groups = groups enabled by FLAG in the pack (see
67
- # #enabled_groups) — the per-flag schema CUT (Phase 7, Stage E / D5): only
69
+ # #enabled_groups) — the per-flag schema CUT: only
68
70
  # the tools of the enabled groups (union with tools_allow) go to the model;
69
71
  # those of disabled groups are cut BEFORE the turn (resolves the OpenClaw
70
72
  # tool-call waste, where the flag only exists in Rails).
@@ -72,24 +74,24 @@ module Insika
72
74
  attrs = pack.config.dup
73
75
  attrs[:id] = id
74
76
  attrs[:prompt_files] = pack.files.keys unless pack.files.empty?
75
- attrs[:skills] = pack.skills.keys if pack.skills.any? || pack.config.key?(:skills)
77
+ attrs[:skills] = pack.skills.keys
76
78
 
77
79
  pack_tools = pack.tools.map { |t| tool_name(t) }
78
80
  allow = Array(pack.config[:tools_allow]).map(&:to_s) | pack_tools
79
- attrs[:tools_allow] = allow unless allow.empty? && !pack.config.key?(:tools_allow)
81
+ attrs[:tools_allow] = allow
80
82
 
81
83
  groups = enabled_groups(pack.config)
82
84
  attrs[:tools_allow_groups] = groups unless groups.nil?
83
85
  attrs
84
86
  end
85
87
 
86
- # PER-FLAG CUT (Phase 7, Stage E / D5, STATIC pilot): derives the agent's
88
+ # PER-FLAG CUT (/, STATIC pilot): derives the agent's
87
89
  # per-group allowlist from FLAGS declared in the pack config — DATA, never a
88
- # core convention (NF1). The engine doesn't know "groceries_v2"/"b2b": the pack
90
+ # core convention. The engine doesn't know "groceries_v2"/"b2b": the pack
89
91
  # declares which GROUPS are enabled; the flag->group mapping is the
90
92
  # responsibility of provisioning/the pack, not the insika. Two forms (union):
91
93
  # - `enabled_groups: ["default", "b2b"]` — explicit list of ON groups.
92
- # - `flags: { "b2b" => true, "natura" => false }` — the flag key IS the group
94
+ # - `flags: { "wholesale" => true, "outlet" => false }` — the flag key IS the group
93
95
  # name; only the truthy ones enter (the false ones CUT the group).
94
96
  # Neither declared -> nil (no per-group cut; old behavior).
95
97
  # Explicit `[]` (empty enabled_groups, or all flags false) -> no group.
@@ -64,7 +64,7 @@ module Insika
64
64
  end
65
65
 
66
66
  # -> [PendingAction] every :pending across all tasks — the approvals inbox
67
- # (§12 G5, Studio). Single O(n) scan (vs. open_for per task = O(n·m)); the
67
+ # (Studio). Single O(n) scan (vs. open_for per task = O(n·m)); the
68
68
  # UI resolves task context afterwards via TaskStore#find.
69
69
  def all_open
70
70
  @store.list(SCOPE, KEY_PREFIX).filter_map do |key|
@@ -209,7 +209,7 @@ module Insika
209
209
  @registries[:workflows].register(name, callable, plugin: @plugin_id, &block)
210
210
  end
211
211
 
212
- # RFC-0011 §4.2 — a channel is an INSTANCE (it holds its credentials and its
212
+ # a channel is an INSTANCE (it holds its credentials and its
213
213
  # HTTP client), so unlike a tool there is no factory to defer. Declared-or-
214
214
  # ignored like the others: the id is a URL segment, and a plugin quietly
215
215
  # mounting a route nobody declared is exactly what the contract list prevents.
@@ -254,7 +254,7 @@ module Insika
254
254
 
255
255
  if kind == :workflow
256
256
  warn "[plugin #{@plugin_id}] capability '#{name}' (kind: workflow) registered without a consumer " \
257
- "in this slice — agent exposure is follow-up (P2B-01 L5)"
257
+ "in this slice — agent exposure is follow-up"
258
258
  end
259
259
 
260
260
  @staged_capabilities << { capability: name, impl_name: impl_name, kind: kind,
@@ -47,7 +47,7 @@ module Insika
47
47
  # Absorbs ToolRegistry#resolve as a policy: optional without
48
48
  # opt-in -> deny; tools_deny -> deny ("deny always wins"); tools_allow
49
49
  # with the semantics (nil = all; [] = ∅; [names] = final set).
50
- # Phase 7/D4/F5 (Step C): `tools_allow_groups` UNIONS in the groups' tools
50
+ # `tools_allow_groups` UNIONS in the groups' tools
51
51
  # (the group is GIVEN in the Entry metadata). Both allowlists nil = all.
52
52
  class ToolAllowlist < Base
53
53
  def decide(request)
@@ -90,6 +90,7 @@ module Insika
90
90
  tools_allow: h[:tools_allow], tools_deny: h[:tools_deny] || [],
91
91
  tools_allow_groups: h[:tools_allow_groups],
92
92
  skills: h[:skills],
93
+ skills_eager: h[:skills_eager],
93
94
  context_providers: h[:context_providers],
94
95
  workflows_allow: h[:workflows_allow],
95
96
  policies: Array(h[:policies]).map(&:to_sym),
@@ -97,27 +98,32 @@ module Insika
97
98
  limits: symbolize_limits(h[:limits]),
98
99
  approvals_required: h[:approvals_required],
99
100
  capabilities: h[:capabilities],
100
- # subagents (RFC-0010): allowlist of child ids; build re-normalizes to
101
+ # subagents: allowlist of child ids; build re-normalizes to
101
102
  # [String]. nil round-trips as nil (opt-in: NONE).
102
103
  subagents: h[:subagents],
103
104
  tools_deferred: h[:tools_deferred],
104
105
  memory: h[:memory],
105
106
  prompt_caching: h[:prompt_caching],
106
- # params/model_policy (v2, §10): the resolver tolerates string keys from
107
+ tool_output_compression: h[:tool_output_compression],
108
+ # params/model_policy: the resolver tolerates string keys from
107
109
  # the JSON round-trip (ModelResolver#normalize_params / ModelPolicy), so no
108
110
  # re-symbolization needed here.
109
111
  params: h[:params] || {},
110
112
  model_policy: h[:model_policy],
111
- # guardrails (RFC-0009): a plain Hash; Safety::Config tolerates the JSON
113
+ budget: h[:budget],
114
+ reliability: h[:reliability],
115
+ alerts: h[:alerts],
116
+ stuck_signal: h[:stuck_signal],
117
+ # guardrails: a plain Hash; Safety::Config tolerates the JSON
112
118
  # round-trip (string keys/values), so no re-symbolization here.
113
119
  guardrails: h[:guardrails],
114
- # sandbox (item 35): a plain config Hash; Sandbox.build tolerates the JSON
120
+ # sandbox: a plain config Hash; Sandbox.build tolerates the JSON
115
121
  # round-trip (string keys), so no re-symbolization here. nil = absent.
116
122
  sandbox: h[:sandbox],
117
- # refinement (RFC-0013): a plain config Hash read with string keys by the
123
+ # refinement: a plain config Hash read with string keys by the
118
124
  # RunRefinement handler; nil round-trips as nil (= report-only).
119
125
  refinement: h[:refinement],
120
- # capabilities_declared (RFC-0014 §3.5): flat [String]; build re-normalizes.
126
+ # capabilities_declared: flat [String]; build re-normalizes.
121
127
  capabilities_declared: h[:capabilities_declared],
122
128
  # edge_stream: which internal channels may cross to the customer. {} = neither.
123
129
  edge_stream: h[:edge_stream],