insika 0.1.0 → 0.3.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 +199 -5
- data/README.md +8 -2
- data/bin/insika +231 -13
- data/docs/AGENTS.md +505 -6
- data/docs/API.md +56 -0
- data/docs/CHANNELS.md +100 -10
- data/docs/CONTEXT.md +147 -19
- data/docs/DEPLOY.md +34 -11
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +20 -1
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +117 -0
- data/docs/LOADTEST.md +17 -10
- data/docs/OBSERVABILITY.md +65 -2
- data/docs/REFINEMENT.md +9 -9
- data/docs/RELEASING.md +34 -7
- data/docs/RUNNING-LOCAL.md +4 -4
- data/docs/SECURITY.md +85 -11
- data/docs/SKILLS.md +189 -3
- data/docs/SOAK.md +127 -0
- data/docs/TOOLS.md +70 -2
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/domain.md +115 -0
- data/docs/index.md +2 -2
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +228 -26
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +166 -0
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +132 -24
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +80 -6
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +9 -9
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +145 -13
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +6 -4
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/create_agent.rb +2 -2
- data/lib/insika/commands/create_session.rb +1 -1
- data/lib/insika/commands/delete_llm_provider.rb +1 -1
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_tenant_data.rb +95 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +12 -12
- data/lib/insika/commands/import_mcp_tools.rb +1 -1
- data/lib/insika/commands/import_tools.rb +4 -4
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/resolve_refinement.rb +1 -1
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/run_refinement.rb +5 -5
- data/lib/insika/commands/send_message.rb +112 -15
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/set_agent_tools.rb +1 -1
- data/lib/insika/commands/set_skill_agents.rb +60 -19
- data/lib/insika/commands/trigger_workflow.rb +1 -1
- data/lib/insika/commands/update_agent.rb +1 -1
- data/lib/insika/commands/write_data_tool.rb +1 -1
- data/lib/insika/commands/write_golden.rb +1 -1
- data/lib/insika/commands/write_skill.rb +19 -9
- data/lib/insika/config_store.rb +8 -4
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +23 -5
- data/lib/insika/context/fragment.rb +31 -3
- data/lib/insika/context/priority.rb +6 -2
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +30 -2
- data/lib/insika/context/providers/request.rb +1 -1
- data/lib/insika/context/providers/session.rb +17 -2
- data/lib/insika/context/providers/skill.rb +7 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +128 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +962 -7
- data/lib/insika/dsl/runtime.rb +20 -11
- data/lib/insika/dsl/server_boot.rb +74 -4
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +152 -15
- data/lib/insika/edge_limiter.rb +167 -8
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +22 -12
- data/lib/insika/errors.rb +72 -5
- data/lib/insika/evals/assertions.rb +15 -14
- data/lib/insika/evals/baseline.rb +3 -3
- data/lib/insika/evals/golden.rb +8 -8
- data/lib/insika/evals/judge.rb +7 -7
- data/lib/insika/evals/pairwise.rb +21 -9
- data/lib/insika/evals/report.rb +2 -2
- data/lib/insika/evals/runner.rb +6 -6
- data/lib/insika/evals/transport.rb +2 -2
- data/lib/insika/event_stream.rb +23 -5
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +1092 -160
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +221 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +3 -3
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/http_client.rb +3 -3
- data/lib/insika/inbound_log.rb +1 -1
- data/lib/insika/llm_configurator.rb +3 -3
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +4 -4
- data/lib/insika/mcp_tool_ingestor.rb +6 -6
- data/lib/insika/media.rb +298 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +8 -3
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +8 -3
- data/lib/insika/outbox_store.rb +44 -6
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +3 -4
- data/lib/insika/pack.rb +3 -3
- data/lib/insika/pack_importer.rb +17 -15
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/pending_action_store.rb +1 -1
- data/lib/insika/plugin/loader.rb +2 -2
- data/lib/insika/policy/policy.rb +1 -1
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +34 -7
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +6 -3
- data/lib/insika/recovery.rb +47 -6
- data/lib/insika/refinement/candidate.rb +4 -4
- data/lib/insika/refinement/evidence_collector.rb +6 -6
- data/lib/insika/refinement/gate.rb +7 -7
- data/lib/insika/refinement/panel.rb +7 -7
- data/lib/insika/refinement/proposer.rb +10 -10
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +211 -0
- data/lib/insika/retention.rb +281 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +46 -6
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +34 -115
- data/lib/insika/safety/factory.rb +18 -5
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +20 -5
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +10 -6
- data/lib/insika/safety/output_validator.rb +13 -7
- data/lib/insika/safety/safe_responses.rb +1 -1
- data/lib/insika/sandbox/boundary.rb +2 -2
- data/lib/insika/sandbox.rb +1 -1
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +366 -54
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +31 -7
- data/lib/insika/server/responses.rb +58 -9
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +11 -7
- data/lib/insika/session_store.rb +66 -3
- data/lib/insika/settings_store.rb +15 -5
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +131 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +11 -2
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1058 -75
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +27 -26
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +274 -22
- data/lib/insika/studio/nav_icons.rb +7 -2
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +629 -86
- data/lib/insika/studio/views/agents.erb +11 -7
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +4 -1
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +2 -1
- data/lib/insika/studio/views/layout.erb +1 -0
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +133 -3
- data/lib/insika/studio/views/settings.erb +9 -12
- data/lib/insika/studio/views/skills.erb +66 -12
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +0 -1
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +22 -2
- data/lib/insika/telemetry/pricing.rb +3 -3
- data/lib/insika/telemetry/recorder.rb +1 -1
- data/lib/insika/telemetry.rb +2 -2
- data/lib/insika/testing/store_contract.rb +54 -33
- data/lib/insika/tick.rb +146 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +25 -15
- data/lib/insika/tool_envelope.rb +70 -1
- data/lib/insika/tool_manifest.rb +11 -7
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_store.rb +1 -1
- data/lib/insika/tool_trace_store.rb +1 -1
- data/lib/insika/tools/concurrency.rb +2 -2
- data/lib/insika/tools/data_defined_tool.rb +14 -5
- data/lib/insika/tools/generate_image.rb +44 -0
- data/lib/insika/tools/load_skill.rb +61 -3
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +4 -4
- data/lib/insika/tools/subagents.rb +1 -1
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +54 -13
- data/lib/insika/turn_timing.rb +24 -4
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +372 -34
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +122 -16
- metadata +95 -2
- data/lib/insika/server/admin_auth.rb +0 -29
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require_relative "agent_enum"
|
|
5
|
+
|
|
6
|
+
module Insika
|
|
7
|
+
module Tools
|
|
8
|
+
# Shared write-edge plumbing for the two briefing tools: the
|
|
9
|
+
# session id always comes from the turn's task — never from the model's
|
|
10
|
+
# arguments — and the :briefing_updated event carries task/session
|
|
11
|
+
# correlation for the trace.
|
|
12
|
+
module BriefingWrite
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# The turn's session, from the task (turn_state.rb:7) — never the model's args.
|
|
16
|
+
def session_id
|
|
17
|
+
@state.respond_to?(:task) ? @state.task&.session_id : nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def no_session_error = { error: "no session to brief" }
|
|
21
|
+
|
|
22
|
+
def emit(kind, field, value)
|
|
23
|
+
@event_stream&.emit(Insika::Event.new(
|
|
24
|
+
type: :briefing_updated,
|
|
25
|
+
data: { kind: kind, field: field, value: value },
|
|
26
|
+
meta: { task_id: @state.task&.id, session_id: @state.task&.session_id }
|
|
27
|
+
))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Write edge of the session briefing: the agent records what it
|
|
32
|
+
# learned in THIS conversation — the working state it is accountable for
|
|
33
|
+
# (tool-written, never engine-inferred). System builtin (like remember):
|
|
34
|
+
# `require "ruby_llm"` stays in THIS file, loaded lazily by the Executor in
|
|
35
|
+
# create_chat. Wired only when the pack declared briefing_fields AND the
|
|
36
|
+
# builder has a session_store (double gate, like remember). Never enveloped:
|
|
37
|
+
# deterministic in-process writes.
|
|
38
|
+
class UpdateBriefing < RubyLLM::Tool
|
|
39
|
+
include BriefingWrite
|
|
40
|
+
|
|
41
|
+
# The class-level text carries no field list, so it can never render the
|
|
42
|
+
# empty "field must be one of: ." placeholder; the instance adds the
|
|
43
|
+
# DECLARED names (D3) to the description AND as the `field` enum (the
|
|
44
|
+
# model sees the valid set in the schema instead of inventing one —
|
|
45
|
+
# the set is instance-built. Per-instance, like the subagent tools' dynamic schemas.
|
|
46
|
+
BASE_DESCRIPTION = "Records a fact learned in this conversation into the session briefing. " \
|
|
47
|
+
"Call it as soon as the customer gives one of these, and when they correct one."
|
|
48
|
+
|
|
49
|
+
description BASE_DESCRIPTION
|
|
50
|
+
|
|
51
|
+
def self.description_for(fields)
|
|
52
|
+
names = Array(fields).map(&:to_s)
|
|
53
|
+
names.empty? ? BASE_DESCRIPTION : "#{BASE_DESCRIPTION} field must be one of: #{names.join(', ')}."
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
param :field, desc: "The briefing field name (one of the declared list)"
|
|
57
|
+
param :value, desc: "The value learned. Blank clears the field."
|
|
58
|
+
|
|
59
|
+
def name = "update_briefing"
|
|
60
|
+
|
|
61
|
+
def initialize(session_store:, fields:, event_stream:, state:)
|
|
62
|
+
@session_store = session_store
|
|
63
|
+
@fields = Array(fields).map(&:to_s)
|
|
64
|
+
@event_stream = event_stream
|
|
65
|
+
@state = state
|
|
66
|
+
super()
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def description
|
|
70
|
+
self.class.description_for(@fields)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The declared names become the `field` enum in the schema the provider
|
|
74
|
+
# sees — per-turn data, like the subagent allowlist (Tools::AgentEnum).
|
|
75
|
+
def params_schema
|
|
76
|
+
@field_enum_schema ||= Insika::Tools::AgentEnum.inject(super, @fields, path: %i[field])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# E2: an undeclared name returns an ENVELOPE error to the model and never
|
|
80
|
+
# persists. A missing session is the same shape (the model can retry later).
|
|
81
|
+
def execute(field:, value:)
|
|
82
|
+
sid = session_id
|
|
83
|
+
return no_session_error if sid.to_s.empty?
|
|
84
|
+
unless @fields.include?(field.to_s)
|
|
85
|
+
return { error: "unknown field '#{field}'; declared: #{@fields.join(', ')}" }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
session = @session_store.update_briefing(sid, field: field.to_s, value: value.to_s)
|
|
89
|
+
emit("field", field.to_s, value.to_s)
|
|
90
|
+
{ updated: field.to_s, briefing: session.briefing }
|
|
91
|
+
rescue Insika::NotFoundError
|
|
92
|
+
{ error: "session not found" }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The agreed next step: same engine-owned briefing object,
|
|
96
|
+
# sibling writer. Blank clears to nil.
|
|
97
|
+
class SetNextStep < RubyLLM::Tool
|
|
98
|
+
include BriefingWrite
|
|
99
|
+
|
|
100
|
+
description "Records the next step agreed with the customer " \
|
|
101
|
+
"(e.g. 'send the payment link tomorrow at 10'). Blank clears it."
|
|
102
|
+
param :text, desc: "The agreed next step, in one sentence"
|
|
103
|
+
|
|
104
|
+
def name = "set_next_step"
|
|
105
|
+
|
|
106
|
+
def initialize(session_store:, event_stream:, state:)
|
|
107
|
+
@session_store = session_store
|
|
108
|
+
@event_stream = event_stream
|
|
109
|
+
@state = state
|
|
110
|
+
super()
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def execute(text:)
|
|
114
|
+
sid = session_id
|
|
115
|
+
return no_session_error if sid.to_s.empty?
|
|
116
|
+
|
|
117
|
+
session = @session_store.set_next_step(sid, text: text.to_s)
|
|
118
|
+
emit("next_step", nil, text.to_s)
|
|
119
|
+
{ next_step: session.briefing["next_step"] }
|
|
120
|
+
rescue Insika::NotFoundError
|
|
121
|
+
{ error: "session not found" }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
data/lib/insika/turn_output.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Insika
|
|
|
25
25
|
# deliberate:
|
|
26
26
|
#
|
|
27
27
|
# · The customer-visible stream is per MESSAGE, not per token. `ttft_ms` still
|
|
28
|
-
# measures the provider's first token, so
|
|
28
|
+
# measures the provider's first token, so's baselines stay comparable;
|
|
29
29
|
# what moved is when the customer can read it. For the WhatsApp edge this
|
|
30
30
|
# changes nothing — the dispatcher accumulated the deltas into one message
|
|
31
31
|
# anyway — and the Studio keeps its live typing off `:intermediate`.
|
|
@@ -44,7 +44,7 @@ module Insika
|
|
|
44
44
|
# decides and publishes once, at the end of the stage.
|
|
45
45
|
attr_reader :candidate
|
|
46
46
|
|
|
47
|
-
# filter: Safety::OutputFilter | nil (
|
|
47
|
+
# filter: Safety::OutputFilter | nil (nil = stream untouched).
|
|
48
48
|
# emit: ->(type, data) — the Executor's emitter, already bound to the task.
|
|
49
49
|
# public_intermediate: the agent opted this channel in (`edge_stream`), so the
|
|
50
50
|
# narration is TAGGED and `/v1/responses` gives it its own frame. Default false:
|
data/lib/insika/turn_state.rb
CHANGED
|
@@ -11,11 +11,11 @@ module Insika
|
|
|
11
11
|
:allowed_skills,
|
|
12
12
|
:chat, # the turn's RubyLLM::Chat instance
|
|
13
13
|
:session, # the turn's SessionStore::Session | nil (set at stage 2;
|
|
14
|
-
# read by create_chat for the per-chat model pin
|
|
15
|
-
:model_selection, # resolved ModelSelection
|
|
14
|
+
# read by create_chat for the per-chat model pin)
|
|
15
|
+
:model_selection, # resolved ModelSelection: model/provider/source/
|
|
16
16
|
# pinned/params/fallbacks. Set at stage 5; surfaced in usage.
|
|
17
17
|
:halt_reason, # set by Middleware when short-circuiting (halt-as-FAILURE)
|
|
18
|
-
:halt_response, # set by a Middleware for the GRACEFUL halt
|
|
18
|
+
:halt_response, # set by a Middleware for the GRACEFUL halt:
|
|
19
19
|
# the safe reply the turn completes with, WITHOUT touching the
|
|
20
20
|
# LLM. Distinct from halt_reason — a completion, not a failure.
|
|
21
21
|
:guardrail_block, # audit metadata the guardrail sets alongside halt_response
|
|
@@ -25,7 +25,45 @@ module Insika
|
|
|
25
25
|
# (after_task); the Executor emits one :guardrail_flagged each.
|
|
26
26
|
:response_content, # the turn's final assistant text, set at stage 6/on halt so the
|
|
27
27
|
# after_task validator can inspect it.
|
|
28
|
-
:
|
|
28
|
+
:route, # WS4: the intent route the classifier chose (Symbol | nil).
|
|
29
|
+
# Set before the ask; rides the :route_classified event and the
|
|
30
|
+
# terminal event additively. nil = no routing on this turn.
|
|
31
|
+
:message_source, # WS9: how the turn's message entered (:voice when it came
|
|
32
|
+
# from a transcribed audio part; nil = typed). Rides the
|
|
33
|
+
# terminal event additively.
|
|
34
|
+
:media_attachments, # WS9: the ask's attachments (image parts); nil = none.
|
|
35
|
+
# The provider bills them; usage flows like any ask.
|
|
36
|
+
# The first image URL also lands on turn_context[:image_url]
|
|
37
|
+
# for data/HTTP tools (`{{ctx.image_url}}`).
|
|
38
|
+
:output_parts, # WS9 (saída): media the turn GENERATED, as additive
|
|
39
|
+
# parts ({ type:, mime_type:, base64:, model: }). Rides
|
|
40
|
+
# the terminal event (output_parts) and the /v1/responses
|
|
41
|
+
# envelope; the answer text stays text on purpose — the
|
|
42
|
+
# media is a separate sibling. nil = nothing generated.
|
|
43
|
+
:channel_capabilities, # WS9 (saída): the CHANNEL's declared OUTPUT media
|
|
44
|
+
# kinds (Media::OUTPUT_CAPABILITIES — image_output /
|
|
45
|
+
# audio_output). Half of the gate that decides whether
|
|
46
|
+
# generate_image/tts are wired at all (the other half is
|
|
47
|
+
# profile.outputs). [] = the channel declared nothing —
|
|
48
|
+
# no media can leak to it.
|
|
49
|
+
:output_filter, # per-turn Safety::OutputFilter (nil = off); redacts the stream.
|
|
50
|
+
:stuck_outcome, # set by the signal_stuck system tool (WS5):
|
|
51
|
+
# { reason:, message: } when the agent declared it cannot
|
|
52
|
+
# proceed. The Executor tags the terminal event with
|
|
53
|
+
# outcome: "stuck" and emits :turn_stuck. nil = normal turn.
|
|
54
|
+
:evidence_ledger, # Insika::EvidenceLedger | nil — the
|
|
55
|
+
# session-scoped set of product ids that entered the
|
|
56
|
+
# context via an evidence-declared tool (the envelope
|
|
57
|
+
# appends; the validator/enforcer read it). Built per
|
|
58
|
+
# turn by the Executor; nil = no session/no evidence.
|
|
59
|
+
:evidence_attachments, # [ {type, url, caption} ] hoarded by the
|
|
60
|
+
# envelope this turn; read by the Executor at stage 8
|
|
61
|
+
# for the channel delivery. Reset per turn.
|
|
62
|
+
:context_trace_entry # the sanitized trace entry parked at
|
|
63
|
+
# prepare_turn (fingerprints + invalidation_reason);
|
|
64
|
+
# the stage-8 stamp merges the cache-hit fields into
|
|
65
|
+
# it and re-records the same (task_id, turn) key.
|
|
66
|
+
# nil = no store wired / no session.
|
|
29
67
|
|
|
30
68
|
# Internal (not part of the contract): per-CALL correlation between RubyLLM's
|
|
31
69
|
# tool callbacks and the tool decorators — `current_tool_call` keys the
|
|
@@ -34,7 +72,7 @@ module Insika
|
|
|
34
72
|
#
|
|
35
73
|
# They live in FIBER STORAGE, not in ivars, and that is the whole point:
|
|
36
74
|
# `before_tool_call` → `tool.call` → `after_tool_result` all run in the SAME
|
|
37
|
-
# fiber, and with `ToolConcurrency`
|
|
75
|
+
# fiber, and with `ToolConcurrency` there is one fiber PER CALL. A
|
|
38
76
|
# single slot on this shared object would let one in-flight call overwrite
|
|
39
77
|
# another's — a side-effect recorded under the wrong id (so a resume skips the
|
|
40
78
|
# wrong tool, or re-runs a non-idempotent one) and a mislabelled event. Both
|
|
@@ -56,7 +94,7 @@ module Insika
|
|
|
56
94
|
Fiber[NAME_KEY] = name
|
|
57
95
|
end
|
|
58
96
|
|
|
59
|
-
# Internal (
|
|
97
|
+
# Internal (R1): the chat's message count RIGHT AFTER `assemble` (seeded
|
|
60
98
|
# history) and BEFORE `ask`. persist_turn slices `chat.messages.drop(baseline)`
|
|
61
99
|
# to serialize the turn's real exchange — user + assistant(tool_calls) + tool
|
|
62
100
|
# results + final assistant — into the transcript. nil = no chat recorded
|
|
@@ -70,13 +108,13 @@ module Insika
|
|
|
70
108
|
# capability_registry or empty profile.capabilities (parity).
|
|
71
109
|
attr_accessor :capability_names
|
|
72
110
|
|
|
73
|
-
# Internal
|
|
111
|
+
# Internal: the turn's resolved QueuePolicy. Read at stage 6 to decide
|
|
74
112
|
# whether this run accepts steered messages, and how they are worded. Resolved once
|
|
75
113
|
# per turn, in build_turn_state — an edit to the agent mid-run does not change the
|
|
76
114
|
# rules the run started under.
|
|
77
115
|
attr_accessor :queue_policy
|
|
78
116
|
|
|
79
|
-
# Internal
|
|
117
|
+
# Internal: true when this turn re-enters the pipeline via
|
|
80
118
|
# resume_task/recovery. The EdgeLimiter reads it to NEVER re-count or block a
|
|
81
119
|
# turn that was already admitted — a crash/pause under a saturated window must
|
|
82
120
|
# not swallow a legitimate message with the rate-limit reply.
|
|
@@ -86,13 +124,13 @@ module Insika
|
|
|
86
124
|
# (`remember` tool). Set in run_pipeline; nil = DEFAULT_TENANT in the MemoryStore.
|
|
87
125
|
attr_accessor :tenant
|
|
88
126
|
|
|
89
|
-
# Internal
|
|
127
|
+
# Internal: turn context deposited into the data-tools to
|
|
90
128
|
# resolve {{ctx.*}} (chat_id/agent_id/tenant/store_id) and emit
|
|
91
129
|
# X-Chat-Id/X-Store-Id/X-Agent-Id. A Hash of symbols, set in run_pipeline.
|
|
92
130
|
# Comes from the TURN, never from the model's args (R2). Distinct from `tenant` (memory).
|
|
93
131
|
attr_accessor :turn_context
|
|
94
132
|
|
|
95
|
-
# Internal (
|
|
133
|
+
# Internal (observability): the turn's token usage (input/output/
|
|
96
134
|
# total/cached + model), captured from the provider's response at stage 6. Goes
|
|
97
135
|
# to the terminal event (:task_completed) — feeds the usage of
|
|
98
136
|
# /v1/responses and the Telemetry (OTEL). nil = turn with no model response
|
|
@@ -111,19 +149,19 @@ module Insika
|
|
|
111
149
|
# by the coordinator for await(:approval)).
|
|
112
150
|
attr_accessor :requires_approval, :approval_coordinator, :actor
|
|
113
151
|
|
|
114
|
-
# Internal
|
|
152
|
+
# Internal: the turn's shared in-flight cap for tool calls —
|
|
115
153
|
# ONE Async::Semaphore(tool_concurrency), installed by ToolAssembly#wrap_tools
|
|
116
154
|
# and acquired by every ToolEnvelope, INCLUDING the ones tool_search promotes
|
|
117
155
|
# mid-turn (they read it off the state, so the cap survives promotion).
|
|
118
156
|
# nil = concurrency off: no gate, no overhead, serial execution unchanged.
|
|
119
157
|
attr_accessor :tool_gate
|
|
120
158
|
|
|
121
|
-
#
|
|
159
|
+
# parallel tool calls, resolved PER TURN and read by ChatBuilder
|
|
122
160
|
# (whether to hand the gem `concurrency:`) and ToolAssembly (the gate's size).
|
|
123
161
|
#
|
|
124
162
|
# `requested_tool_concurrency` is what the operator configured;
|
|
125
163
|
# `tool_concurrency` is what this turn actually gets. They differ for exactly
|
|
126
|
-
# one reason
|
|
164
|
+
# one reason —: `Executor#request_approval` blocks on `actor.await(approval)`,
|
|
127
165
|
# and the mailbox is one queue per TASK. Two fibers waiting there share it,
|
|
128
166
|
# `dequeue` wakes exactly one, the message is consumed, and the other fiber
|
|
129
167
|
# hangs until `approval_timeout` (~1h). So a turn that can suspend for a human
|
|
@@ -147,6 +185,9 @@ module Insika
|
|
|
147
185
|
@turn = turn
|
|
148
186
|
@message = message
|
|
149
187
|
@capability_names = {}
|
|
188
|
+
@output_parts = []
|
|
189
|
+
@channel_capabilities = []
|
|
190
|
+
@evidence_attachments = []
|
|
150
191
|
# Fiber storage is INHERITED by fibers created later, so a turn spawned from
|
|
151
192
|
# inside a tool call (a subagent child) would start out carrying its
|
|
152
193
|
# parent's correlation. Clearing at turn start keeps a child from keying its
|
data/lib/insika/turn_timing.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
|
-
# Opt-in per-turn latency breakdown (
|
|
4
|
+
# Opt-in per-turn latency breakdown (locate the
|
|
5
5
|
# TTFB cost with real-turn data). OFF unless INSIKA_TURN_TIMING is set — when
|
|
6
6
|
# off the Executor never allocates one and the hot path pays only `nil&.mark`.
|
|
7
7
|
#
|
|
@@ -16,7 +16,7 @@ module Insika
|
|
|
16
16
|
#
|
|
17
17
|
# `ttft_ms` is the PROVIDER's first token, not the first byte the customer can
|
|
18
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 —
|
|
19
|
+
# answer lands inside `gen_ms`. Measuring the provider is the point —'s
|
|
20
20
|
# baselines (~720 ms, provider-bound) stay comparable across that change.
|
|
21
21
|
class TurnTiming
|
|
22
22
|
# EnvSchema owns "is this flag on?" (1/true/yes/on) — the same predicate that
|
|
@@ -26,22 +26,42 @@ module Insika
|
|
|
26
26
|
Insika::EnvSchema.truthy?(Insika::EnvSchema.read("INSIKA_TURN_TIMING", env))
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# The marks that only mean something under INSIKA_TURN_TIMING. A `breakdown:
|
|
30
|
+
# false` clock (a channel turn with the flag off) ignores them and measures
|
|
31
|
+
# only first_balloon_ms.
|
|
32
|
+
BREAKDOWN_MARKS = %i[prep_start ask first_token done].freeze
|
|
33
|
+
private_constant :BREAKDOWN_MARKS
|
|
34
|
+
|
|
35
|
+
# breakdown: true (the default) is the flag-on clock: prep/ttft/gen/total.
|
|
36
|
+
# false is the channel clock: only :inbound -> :first_balloon,
|
|
37
|
+
# so a channel turn with the flag off still answers H-latência.
|
|
38
|
+
def initialize(breakdown: true)
|
|
30
39
|
@marks = {}
|
|
40
|
+
@breakdown = breakdown
|
|
31
41
|
end
|
|
32
42
|
|
|
33
43
|
def mark(name)
|
|
44
|
+
return unless @breakdown || !BREAKDOWN_MARKS.include?(name)
|
|
45
|
+
|
|
34
46
|
@marks[name] ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
35
47
|
end
|
|
36
48
|
|
|
49
|
+
# Has this mark fired? Used by `SendMessage` to prove the channel clock was
|
|
50
|
+
# stamped at 202 acceptance (`:inbound` before the debounce window), and by
|
|
51
|
+
# the pipeline to know a threaded clock already started.
|
|
52
|
+
def marked?(name) = @marks.key?(name)
|
|
53
|
+
|
|
37
54
|
# -> Hash of phase deltas in ms (only the windows whose endpoints both fired;
|
|
38
55
|
# a workflow turn has no ask/first_token, so those are simply absent).
|
|
56
|
+
# first_balloon_ms is the inbound -> first outbox flush window.
|
|
57
|
+
# A missing number is never a zero — no mark, no key.
|
|
39
58
|
def to_h
|
|
40
59
|
{
|
|
41
60
|
prep_ms: delta(:prep_start, :ask),
|
|
42
61
|
ttft_ms: delta(:ask, :first_token),
|
|
43
62
|
gen_ms: delta(:first_token, :done),
|
|
44
|
-
total_ms: delta(:prep_start, :done)
|
|
63
|
+
total_ms: delta(:prep_start, :done),
|
|
64
|
+
first_balloon_ms: delta(:inbound, :first_balloon)
|
|
45
65
|
}.compact
|
|
46
66
|
end
|
|
47
67
|
|
data/lib/insika/usage_ledger.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Insika
|
|
4
|
-
# Fixed-window counters on the KV Store
|
|
4
|
+
# Fixed-window counters on the KV Store: the durable side of
|
|
5
5
|
# the edge limits. One scope, keys shaped "kind:id:window_start" — the window
|
|
6
6
|
# start bucketed on the epoch keeps every process/worker on the SAME bucket
|
|
7
7
|
# without coordination (the Store's transaction serializes the read-modify-write,
|
data/lib/insika/version.rb
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
# Process vitals. One cheap read-only answer to "which process
|
|
5
|
+
# is this, how long has it been up, how much memory does it hold, and what is
|
|
6
|
+
# the Ruby heap doing?" It touches NO store, allocates nothing meaningful,
|
|
7
|
+
# and knows nothing about soaks — a general operator surface the soak
|
|
8
|
+
# happens to be the first consumer of. Exposed at GET /v1/vitals (operator
|
|
9
|
+
# only).
|
|
10
|
+
module Vitals
|
|
11
|
+
# Stamped when this file is first required: the process's own start, which
|
|
12
|
+
# is what "uptime" has to mean (Falcon's controller start is not this
|
|
13
|
+
# worker's). Per-process, not per-App, so a worker respawn under a live
|
|
14
|
+
# container keeps its real age and a new pid reads as a new clock.
|
|
15
|
+
STARTED_AT = Time.now.utc
|
|
16
|
+
|
|
17
|
+
# The subset of GC.stat that answers "is the Ruby heap growing, or is this
|
|
18
|
+
# the allocator?" — the difference between a leak and fragmentation, which
|
|
19
|
+
# is the first fork of any leak hunt .
|
|
20
|
+
GC_KEYS = %i[heap_live_slots heap_free_slots heap_allocated_pages
|
|
21
|
+
total_allocated_objects total_freed_objects
|
|
22
|
+
major_gc_count minor_gc_count
|
|
23
|
+
malloc_increase_bytes oldmalloc_increase_bytes].freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# -> Hash with STRING keys (it is a JSON body, not an internal value
|
|
28
|
+
# object). `executor:`/`db_path:` are optional — absent, the body simply
|
|
29
|
+
# omits those readings. `env:` is injected for specs.
|
|
30
|
+
def snapshot(executor: nil, db_path: nil, env: ENV)
|
|
31
|
+
{
|
|
32
|
+
"boot_id" => EnvSchema.read("INSIKA_BOOT_ID", env).to_s,
|
|
33
|
+
"pid" => Process.pid,
|
|
34
|
+
"started_at" => STARTED_AT.iso8601,
|
|
35
|
+
"uptime_s" => (Time.now.utc - STARTED_AT).round,
|
|
36
|
+
"version" => Insika::VERSION,
|
|
37
|
+
"ruby" => RUBY_DESCRIPTION,
|
|
38
|
+
"yjit" => (defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?),
|
|
39
|
+
"rss_bytes" => rss_bytes,
|
|
40
|
+
"gc" => gc_stat,
|
|
41
|
+
"threads" => Thread.list.size,
|
|
42
|
+
"in_flight" => executor&.in_flight&.size,
|
|
43
|
+
"db_bytes" => db_bytes(db_path),
|
|
44
|
+
"at" => Time.now.utc.iso8601
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Resident set size in bytes. Linux first (/proc/self/status VmRSS) —
|
|
49
|
+
# production is a Linux container; macOS/dev fall back to `ps`. Neither
|
|
50
|
+
# readable -> nil, NEVER a guess: a fabricated RSS would silently pass an
|
|
51
|
+
# envelope.
|
|
52
|
+
def rss_bytes
|
|
53
|
+
if File.file?("/proc/self/status")
|
|
54
|
+
match = File.read("/proc/self/status", encoding: "ASCII-8BIT")[/^VmRSS:\s+(\d+)\s*kB/, 1]
|
|
55
|
+
return Integer(match) * 1024 if match
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
out = `ps -o rss= -p #{Process.pid} 2>/dev/null`.strip
|
|
59
|
+
return Integer(out) * 1024 unless out.empty?
|
|
60
|
+
|
|
61
|
+
nil
|
|
62
|
+
rescue StandardError
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def gc_stat
|
|
67
|
+
stat = GC.stat
|
|
68
|
+
GC_KEYS.each_with_object({}) { |key, acc| acc[key.to_s] = stat[key] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# SQLite file + WAL + shm bytes. -> { "db" =>, "wal" =>, "shm" => } | nil.
|
|
72
|
+
def db_bytes(path)
|
|
73
|
+
return nil if path.nil? || path.to_s.empty?
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
"db" => File.size(path.to_s),
|
|
77
|
+
"wal" => File.exist?("#{path}-wal") ? File.size("#{path}-wal") : 0,
|
|
78
|
+
"shm" => File.exist?("#{path}-shm") ? File.size("#{path}-shm") : 0
|
|
79
|
+
}
|
|
80
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|