insika 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +361 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +366 -0
- data/docs/AGENTS.md +618 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +117 -0
- data/docs/DEPLOY.md +354 -0
- data/docs/EMBEDDING.md +198 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +232 -0
- data/docs/OBSERVABILITY.md +374 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +375 -0
- data/docs/SKILLS.md +284 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +255 -0
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +153 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +303 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +58 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +43 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_refinement.rb +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +112 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +60 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +89 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +43 -0
- data/lib/insika/context/priority.rb +30 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +123 -0
- data/lib/insika/context/providers/skill.rb +24 -0
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +539 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +382 -0
- data/lib/insika/dsl/server_boot.rb +98 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +364 -0
- data/lib/insika/edge_limiter.rb +268 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +249 -0
- data/lib/insika/errors.rb +201 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +132 -0
- data/lib/insika/executor.rb +1995 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +115 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +102 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +123 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +143 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +168 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +102 -0
- data/lib/insika/safety/moderator.rb +94 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/app.rb +1022 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +118 -0
- data/lib/insika/server/responses.rb +165 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +220 -0
- data/lib/insika/skill_store.rb +127 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1693 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +70 -0
- data/lib/insika/studio/forms.rb +335 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +137 -0
- data/lib/insika/studio/views/settings.erb +168 -0
- data/lib/insika/studio/views/skills.erb +141 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +219 -0
- data/lib/insika/tools/load_skill.rb +99 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +162 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +249 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +220 -4
- metadata +412 -8
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "protocol"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "message"
|
|
7
|
+
require_relative "task_projection"
|
|
8
|
+
require_relative "agent_card"
|
|
9
|
+
|
|
10
|
+
module Insika
|
|
11
|
+
module Server
|
|
12
|
+
module A2A
|
|
13
|
+
# A2A edge handler: injected sub-app mounted by Server::App. A2A is
|
|
14
|
+
# TRANSPORT — translates JSON-RPC↔Command on the SAME bus, projects
|
|
15
|
+
# Task→A2A, and NEVER leaks an exception (always an error object).
|
|
16
|
+
class App
|
|
17
|
+
def initialize(command_bus:, task_store:, session_store:, profiles:, skill_catalog:, config:)
|
|
18
|
+
@command_bus = command_bus
|
|
19
|
+
@task_store = task_store
|
|
20
|
+
@session_store = session_store
|
|
21
|
+
@profiles = Insika::ProfileSource.coerce(profiles)
|
|
22
|
+
@skill_catalog = skill_catalog
|
|
23
|
+
@config = config # { a2a_agent:, base_url: }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# POST /a2a — `body` is already the deserialized Hash. -> JSON-RPC envelope.
|
|
27
|
+
def rpc(body)
|
|
28
|
+
kind, parsed = Protocol.parse(body)
|
|
29
|
+
return Protocol.error(parsed[:id], parsed[:code], parsed[:message]) if kind == :error
|
|
30
|
+
|
|
31
|
+
dispatch_method(parsed[:id], parsed[:method], parsed[:params])
|
|
32
|
+
rescue StandardError => e # safety net: parse ok but something slipped through
|
|
33
|
+
code, message = Errors.from_exception(e)
|
|
34
|
+
Protocol.error(nil, code, message)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# GET /.well-known/agent-card.json
|
|
38
|
+
def agent_card
|
|
39
|
+
agent = @profiles[@config[:a2a_agent]]
|
|
40
|
+
skills = @skill_catalog.effective(agent.skills)
|
|
41
|
+
AgentCard.build(agent: agent, base_url: @config[:base_url], skills: skills)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def dispatch_method(id, method, params)
|
|
47
|
+
case method
|
|
48
|
+
when "message/send" then message_send(id, params)
|
|
49
|
+
when "tasks/get" then tasks_get(id, params)
|
|
50
|
+
when "tasks/cancel" then tasks_cancel(id, params)
|
|
51
|
+
else Protocol.error(id, Errors::METHOD_NOT_FOUND, "method '#{method}' not supported")
|
|
52
|
+
end
|
|
53
|
+
rescue StandardError => e # map core error -> A2A code
|
|
54
|
+
code, message = Errors.from_exception(e)
|
|
55
|
+
Protocol.error(id, code, message)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# message/send returns the Task; missing contextId -> creates a session (the
|
|
59
|
+
# server assigns the contextId; ensures a transcript for tasks/get).
|
|
60
|
+
def message_send(id, params)
|
|
61
|
+
message = params["message"] || {}
|
|
62
|
+
session_id = message["contextId"] || params["contextId"]
|
|
63
|
+
session_id ||= @command_bus.dispatch(build(:create_session, {})).id
|
|
64
|
+
text = Message.text_from(message)
|
|
65
|
+
result = @command_bus.dispatch(build(:send_message,
|
|
66
|
+
agent: @config[:a2a_agent], message: text, session_id: session_id))
|
|
67
|
+
task = @task_store.find(result[:task_id])
|
|
68
|
+
Protocol.result(id, TaskProjection.call(task, at: now))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def tasks_get(id, params)
|
|
72
|
+
task = @task_store.find(params["id"])
|
|
73
|
+
return Protocol.error(id, Errors::TASK_NOT_FOUND, "task not found: #{params['id']}") if task.nil?
|
|
74
|
+
|
|
75
|
+
Protocol.result(id, TaskProjection.call(task, at: now,
|
|
76
|
+
content: terminal_content(task),
|
|
77
|
+
error: terminal_error(task)))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def tasks_cancel(id, params)
|
|
81
|
+
task_id = params["id"]
|
|
82
|
+
return Protocol.error(id, Errors::TASK_NOT_FOUND, "task not found: #{task_id}") if @task_store.find(task_id).nil?
|
|
83
|
+
|
|
84
|
+
@command_bus.dispatch(build(:cancel_task, task_id: task_id))
|
|
85
|
+
Protocol.result(id, TaskProjection.call(@task_store.find(task_id), at: now))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Final content = last `assistant` message in the session transcript.
|
|
89
|
+
def terminal_content(task)
|
|
90
|
+
return nil unless task.session_id
|
|
91
|
+
|
|
92
|
+
session = @session_store.find(task.session_id)
|
|
93
|
+
return nil unless session
|
|
94
|
+
|
|
95
|
+
msg = session.messages.reverse.find { |m| (m["role"] || m[:role]).to_s == "assistant" }
|
|
96
|
+
msg && (msg["content"] || msg[:content])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Error message from the last :failed Execution.
|
|
100
|
+
def terminal_error(task)
|
|
101
|
+
exec = task.executions.last
|
|
102
|
+
return nil unless exec && exec.outcome.to_s == "failed"
|
|
103
|
+
|
|
104
|
+
exec.error && (exec.error["message"] || exec.error[:message])
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def build(type, payload) = Insika::Command.build(type, payload, transport: :a2a)
|
|
108
|
+
def now = Time.now.utc.iso8601
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "message"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Server
|
|
7
|
+
module A2A
|
|
8
|
+
# JSON-RPC response envelope carrying `error` (the remote refused/failed).
|
|
9
|
+
class RemoteError < StandardError
|
|
10
|
+
attr_reader :code
|
|
11
|
+
|
|
12
|
+
def initialize(code, message)
|
|
13
|
+
@code = code
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Outbound A2A client: calls a remote A2A agent. PURE — the
|
|
19
|
+
# `http` (post_json(url, body) -> Hash) is injected; the smoke test uses loopback.
|
|
20
|
+
class Client
|
|
21
|
+
TERMINAL = %w[completed failed canceled rejected input-required].freeze
|
|
22
|
+
POLL_DELAY = 0.02
|
|
23
|
+
|
|
24
|
+
def initialize(http:, poll_max: 30, sleeper: nil)
|
|
25
|
+
@http = http
|
|
26
|
+
@poll_max = poll_max
|
|
27
|
+
@sleeper = sleeper || ->(seconds) { Async::Task.current&.sleep(seconds) }
|
|
28
|
+
@id_seq = 0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# -> remote A2A Task (Hash) | raise RemoteError.
|
|
32
|
+
def send_message(url, text, context_id: nil)
|
|
33
|
+
message = { "role" => "user", "parts" => [{ "kind" => "text", "text" => text.to_s }] }
|
|
34
|
+
message["contextId"] = context_id if context_id
|
|
35
|
+
parse_envelope(@http.post_json(url, request("message/send", { "message" => message })))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# -> remote A2A Task (Hash) | raise RemoteError.
|
|
39
|
+
def get_task(url, task_id)
|
|
40
|
+
parse_envelope(@http.post_json(url, request("tasks/get", { "id" => task_id })))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# High level: send + poll until terminal. ALWAYS does ≥1 get_task — the
|
|
44
|
+
# inbound message/send projects the Task WITHOUT `status.message` (the content
|
|
45
|
+
# only comes from tasks/get). -> { text:, state:, id: } | { error:, state:, id: }.
|
|
46
|
+
# NEVER raises (wraps RemoteError).
|
|
47
|
+
def call(url, text, context_id: nil)
|
|
48
|
+
sent = send_message(url, text, context_id: context_id)
|
|
49
|
+
id = remote_id(sent)
|
|
50
|
+
task = get_task(url, id)
|
|
51
|
+
attempts = 0
|
|
52
|
+
until TERMINAL.include?(remote_state(task)) || attempts >= @poll_max
|
|
53
|
+
@sleeper.call(POLL_DELAY)
|
|
54
|
+
attempts += 1
|
|
55
|
+
task = get_task(url, id)
|
|
56
|
+
end
|
|
57
|
+
project(task, id)
|
|
58
|
+
rescue RemoteError => e
|
|
59
|
+
{ error: e.message, state: "failed", id: nil }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def request(method, params)
|
|
65
|
+
@id_seq += 1
|
|
66
|
+
{ "jsonrpc" => "2.0", "id" => @id_seq, "method" => method, "params" => params }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_envelope(response)
|
|
70
|
+
raise RemoteError.new(nil, "invalid A2A response") unless response.is_a?(Hash)
|
|
71
|
+
return response["result"] if response.key?("result")
|
|
72
|
+
|
|
73
|
+
err = response["error"]
|
|
74
|
+
raise RemoteError.new(err && err["code"], (err && err["message"]).to_s) if err
|
|
75
|
+
|
|
76
|
+
raise RemoteError.new(nil, "A2A response has neither result nor error")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def remote_state(task) = task.is_a?(Hash) ? task.dig("status", "state").to_s : ""
|
|
80
|
+
def remote_text(task) = Message.text_from(task.is_a?(Hash) ? task.dig("status", "message") : nil)
|
|
81
|
+
def remote_id(task) = task.is_a?(Hash) ? task["id"] : nil
|
|
82
|
+
|
|
83
|
+
# completed/input-required -> text; failed/canceled/rejected -> error
|
|
84
|
+
# (text if present, else the state itself — avoids the "".truthy footgun);
|
|
85
|
+
# exhausted poll (non-terminal) -> error.
|
|
86
|
+
def project(task, id)
|
|
87
|
+
state = remote_state(task)
|
|
88
|
+
text = remote_text(task)
|
|
89
|
+
case state
|
|
90
|
+
when "completed", "input-required"
|
|
91
|
+
{ text: text, state: state, id: id }
|
|
92
|
+
when "failed", "canceled", "rejected"
|
|
93
|
+
{ error: text.empty? ? state : text, state: state, id: id }
|
|
94
|
+
else
|
|
95
|
+
{ error: "remote task did not complete (state: #{state})", state: state, id: id }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Server
|
|
5
|
+
module A2A
|
|
6
|
+
# A2A adapter error map: standard JSON-RPC 2.0 codes +
|
|
7
|
+
# A2A extensions. The `App` NEVER leaks an exception — always an error object.
|
|
8
|
+
module Errors
|
|
9
|
+
PARSE_ERROR = -32_700
|
|
10
|
+
INVALID_REQUEST = -32_600
|
|
11
|
+
METHOD_NOT_FOUND = -32_601
|
|
12
|
+
INVALID_PARAMS = -32_602
|
|
13
|
+
INTERNAL_ERROR = -32_603
|
|
14
|
+
TASK_NOT_FOUND = -32_001 # A2A TaskNotFoundError
|
|
15
|
+
TASK_NOT_CANCELABLE = -32_002 # A2A TaskNotCancelableError
|
|
16
|
+
|
|
17
|
+
# Core exception -> [code, message]. A task `NotFoundError` is handled
|
|
18
|
+
# EXPLICITLY in the App (tasks/get returns TASK_NOT_FOUND directly), so
|
|
19
|
+
# here `NotFoundError` falls into INVALID_PARAMS (missing agent/session in the
|
|
20
|
+
# request). Anything else -> INTERNAL_ERROR (generic message, no stack).
|
|
21
|
+
def self.from_exception(error)
|
|
22
|
+
case error
|
|
23
|
+
when Insika::ValidationError, Insika::NotFoundError
|
|
24
|
+
[INVALID_PARAMS, error.message]
|
|
25
|
+
else
|
|
26
|
+
[INTERNAL_ERROR, "internal error"]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Server
|
|
7
|
+
module A2A
|
|
8
|
+
# Production HTTP adapter: implements the duck-type
|
|
9
|
+
# `post_json(url, body) -> Hash` over async-http (runs on the turn's reactor).
|
|
10
|
+
# BOUNDARY — the lib require lives here, never in lib/insika.rb/wiring-load.
|
|
11
|
+
# The Client (the logic) is tested with a fake; this adapter, with a light test.
|
|
12
|
+
class Http
|
|
13
|
+
HEADERS = [["content-type", "application/json"], ["accept", "application/json"]].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(internet: nil)
|
|
16
|
+
@internet = internet # lazy: Async::HTTP::Internet.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# POST JSON -> Hash (parsed JSON-RPC, string keys).
|
|
20
|
+
def post_json(url, body)
|
|
21
|
+
response = internet.post(url, HEADERS, JSON.generate(body))
|
|
22
|
+
JSON.parse(response.read.to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def close
|
|
26
|
+
@internet&.close
|
|
27
|
+
rescue StandardError
|
|
28
|
+
nil # best-effort
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def internet
|
|
34
|
+
@internet ||= begin
|
|
35
|
+
require "async/http/internet"
|
|
36
|
+
Async::HTTP::Internet.new
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Server
|
|
5
|
+
module A2A
|
|
6
|
+
# Message parts translation: TextPart only in this slice.
|
|
7
|
+
# Tolerates `kind` (A2A ~v0.2+) and `type` (older spec) at the boundary.
|
|
8
|
+
module Message
|
|
9
|
+
# A2A Message (Hash) -> String (concatenates the TextParts). "" if none.
|
|
10
|
+
def self.text_from(a2a_message)
|
|
11
|
+
parts = a2a_message.is_a?(Hash) ? Array(a2a_message["parts"] || a2a_message[:parts]) : []
|
|
12
|
+
parts.filter_map do |part|
|
|
13
|
+
next unless part.is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
kind = part["kind"] || part["type"] || part[:kind] || part[:type]
|
|
16
|
+
(part["text"] || part[:text]).to_s if kind.to_s == "text"
|
|
17
|
+
end.join
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# String -> A2A Message (role agent, 1 TextPart).
|
|
21
|
+
def self.agent_message(text)
|
|
22
|
+
{ role: "agent", parts: [{ kind: "text", text: text.to_s }] }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Server
|
|
7
|
+
module A2A
|
|
8
|
+
# JSON-RPC 2.0 envelope. PURE — receives the already
|
|
9
|
+
# deserialized Hash (raw JSON parsing belongs to Server::App). NEVER raises.
|
|
10
|
+
module Protocol
|
|
11
|
+
VERSION = "2.0"
|
|
12
|
+
|
|
13
|
+
# -> [:ok, { id:, method:, params: }] | [:error, { id:, code:, message: }]
|
|
14
|
+
def self.parse(body)
|
|
15
|
+
return err(nil, Errors::INVALID_REQUEST, "request must be a JSON-RPC object") unless body.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
id = body["id"] # missing -> nil (we always respond; no notifications in this slice)
|
|
18
|
+
return err(id, Errors::INVALID_REQUEST, "jsonrpc must be '2.0'") unless body["jsonrpc"] == VERSION
|
|
19
|
+
|
|
20
|
+
method = body["method"]
|
|
21
|
+
return err(id, Errors::INVALID_REQUEST, "method missing") unless method.is_a?(String) && !method.empty?
|
|
22
|
+
|
|
23
|
+
[:ok, { id: id, method: method, params: body["params"] || {} }]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# { jsonrpc:, id:, result: }
|
|
27
|
+
def self.result(id, result)
|
|
28
|
+
{ jsonrpc: VERSION, id: id, result: result }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# { jsonrpc:, id:, error: { code:, message:, data? } }
|
|
32
|
+
def self.error(id, code, message, data: nil)
|
|
33
|
+
err = { code: code, message: message }
|
|
34
|
+
err[:data] = data unless data.nil?
|
|
35
|
+
{ jsonrpc: VERSION, id: id, error: err }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.err(id, code, message)
|
|
39
|
+
[:error, { id: id, code: code, message: message }]
|
|
40
|
+
end
|
|
41
|
+
private_class_method :err
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Insika
|
|
4
|
+
module Server
|
|
5
|
+
module A2A
|
|
6
|
+
# Remote A2A agents config. Parses "id=url,id2=url2".
|
|
7
|
+
module Remotes
|
|
8
|
+
Remote = Data.define(:id, :url, :description)
|
|
9
|
+
|
|
10
|
+
# -> [Remote]. Malformed entries (no '=', empty id/url) are ignored
|
|
11
|
+
# with a warn. env nil/"" -> [].
|
|
12
|
+
def self.parse(env_string, descriptions: {})
|
|
13
|
+
env_string.to_s.split(",").filter_map do |entry|
|
|
14
|
+
id, url = entry.split("=", 2).map { |s| s.to_s.strip }
|
|
15
|
+
if id.to_s.empty? || url.to_s.empty?
|
|
16
|
+
warn "[a2a] malformed remote ignored: #{entry.inspect} (use id=url)"
|
|
17
|
+
next
|
|
18
|
+
end
|
|
19
|
+
Remote.new(id: id, url: url, description: descriptions[id])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "message"
|
|
4
|
+
|
|
5
|
+
module Insika
|
|
6
|
+
module Server
|
|
7
|
+
module A2A
|
|
8
|
+
# Task (core) -> A2A Task projection. PURE.
|
|
9
|
+
module TaskProjection
|
|
10
|
+
# State map. Unknown state -> "unknown".
|
|
11
|
+
STATE = {
|
|
12
|
+
queued: "submitted", running: "working", waiting: "input-required",
|
|
13
|
+
paused: "working", completed: "completed", failed: "failed", cancelled: "canceled"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
# task (TaskStore::Task) + content/error (String|nil) + at (ISO8601) -> Hash A2A Task.
|
|
17
|
+
def self.call(task, at:, content: nil, error: nil)
|
|
18
|
+
state = STATE.fetch(task.status.to_sym, "unknown")
|
|
19
|
+
status = { state: state, timestamp: at }
|
|
20
|
+
msg = status_message(state, content, error)
|
|
21
|
+
status[:message] = msg if msg
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
id: task.id, contextId: task.session_id, kind: "task",
|
|
25
|
+
status: status, artifacts: [], history: []
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# completed -> content; failed -> error; other states have no message.
|
|
30
|
+
def self.status_message(state, content, error)
|
|
31
|
+
case state
|
|
32
|
+
when "completed" then content && Message.agent_message(content)
|
|
33
|
+
when "failed" then error && Message.agent_message(error)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
private_class_method :status_message
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|