anima-core 0.3.0 → 1.0.1
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/.reek.yml +27 -1
- data/CHANGELOG.md +4 -0
- data/README.md +219 -25
- data/agents/codebase-analyzer.md +88 -0
- data/agents/codebase-pattern-finder.md +83 -0
- data/agents/documentation-researcher.md +59 -0
- data/agents/thoughts-analyzer.md +102 -0
- data/agents/web-search-researcher.md +71 -0
- data/anima-core.gemspec +4 -1
- data/app/channels/session_channel.rb +76 -28
- data/app/jobs/agent_request_job.rb +24 -0
- data/app/jobs/analytical_brain_job.rb +33 -0
- data/app/jobs/count_event_tokens_job.rb +1 -1
- data/app/models/concerns/event/broadcasting.rb +20 -2
- data/app/models/event.rb +1 -1
- data/app/models/goal.rb +91 -0
- data/app/models/session.rb +347 -22
- data/config/application.rb +2 -0
- data/db/migrate/20260314075248_add_subagent_support_to_sessions.rb +6 -0
- data/db/migrate/20260314112417_add_granted_tools_to_sessions.rb +5 -0
- data/db/migrate/20260314140000_add_name_to_sessions.rb +7 -0
- data/db/migrate/20260314150000_add_viewport_event_ids_to_sessions.rb +7 -0
- data/db/migrate/20260315100000_add_active_skills_to_sessions.rb +7 -0
- data/db/migrate/20260315140843_create_goals.rb +16 -0
- data/db/migrate/20260315144837_add_completed_at_to_goals.rb +5 -0
- data/db/migrate/20260315191105_add_active_workflow_to_sessions.rb +5 -0
- data/lib/agent_loop.rb +65 -9
- data/lib/agents/definition.rb +116 -0
- data/lib/agents/registry.rb +106 -0
- data/lib/analytical_brain/runner.rb +276 -0
- data/lib/analytical_brain/tools/activate_skill.rb +52 -0
- data/lib/analytical_brain/tools/deactivate_skill.rb +43 -0
- data/lib/analytical_brain/tools/deactivate_workflow.rb +34 -0
- data/lib/analytical_brain/tools/everything_is_ready.rb +28 -0
- data/lib/analytical_brain/tools/finish_goal.rb +62 -0
- data/lib/analytical_brain/tools/read_workflow.rb +58 -0
- data/lib/analytical_brain/tools/rename_session.rb +63 -0
- data/lib/analytical_brain/tools/set_goal.rb +60 -0
- data/lib/analytical_brain/tools/update_goal.rb +60 -0
- data/lib/analytical_brain.rb +23 -0
- data/lib/anima/cli/mcp/secrets.rb +76 -0
- data/lib/anima/cli/mcp.rb +197 -0
- data/lib/anima/cli.rb +4 -0
- data/lib/anima/installer.rb +182 -6
- data/lib/anima/settings.rb +226 -0
- data/lib/anima/version.rb +1 -1
- data/lib/anima.rb +9 -0
- data/lib/credential_store.rb +103 -0
- data/lib/environment_probe.rb +232 -0
- data/lib/llm/client.rb +29 -10
- data/lib/mcp/client_manager.rb +86 -0
- data/lib/mcp/config.rb +213 -0
- data/lib/mcp/health_check.rb +77 -0
- data/lib/mcp/secrets.rb +73 -0
- data/lib/mcp/stdio_transport.rb +206 -0
- data/lib/providers/anthropic.rb +8 -7
- data/lib/shell_session.rb +11 -10
- data/lib/skills/definition.rb +97 -0
- data/lib/skills/registry.rb +105 -0
- data/lib/tools/edit.rb +3 -4
- data/lib/tools/mcp_tool.rb +114 -0
- data/lib/tools/read.rb +15 -16
- data/lib/tools/registry.rb +14 -12
- data/lib/tools/request_feature.rb +121 -0
- data/lib/tools/return_result.rb +81 -0
- data/lib/tools/spawn_specialist.rb +109 -0
- data/lib/tools/spawn_subagent.rb +111 -0
- data/lib/tools/subagent_prompts.rb +12 -0
- data/lib/tools/web_get.rb +8 -9
- data/lib/tui/app.rb +332 -43
- data/lib/tui/message_store.rb +20 -0
- data/lib/tui/screens/chat.rb +207 -20
- data/lib/workflows/definition.rb +97 -0
- data/lib/workflows/registry.rb +89 -0
- data/skills/activerecord/SKILL.md +255 -0
- data/skills/activerecord/examples/associations/association_extensions.rb +298 -0
- data/skills/activerecord/examples/associations/basic_associations.rb +118 -0
- data/skills/activerecord/examples/associations/counter_caches.rb +215 -0
- data/skills/activerecord/examples/associations/polymorphic_associations.rb +217 -0
- data/skills/activerecord/examples/associations/self_referential.rb +302 -0
- data/skills/activerecord/examples/associations/through_associations.rb +203 -0
- data/skills/activerecord/examples/basics/crud_operations.rb +209 -0
- data/skills/activerecord/examples/basics/dirty_tracking.rb +218 -0
- data/skills/activerecord/examples/basics/inheritance.rb +377 -0
- data/skills/activerecord/examples/basics/type_casting.rb +317 -0
- data/skills/activerecord/examples/callbacks/alternatives_to_callbacks.rb +447 -0
- data/skills/activerecord/examples/callbacks/conditional_callbacks.rb +353 -0
- data/skills/activerecord/examples/callbacks/lifecycle_callbacks.rb +280 -0
- data/skills/activerecord/examples/callbacks/transaction_callbacks.rb +340 -0
- data/skills/activerecord/examples/migrations/indexes_and_constraints.rb +337 -0
- data/skills/activerecord/examples/migrations/reversible_patterns.rb +403 -0
- data/skills/activerecord/examples/migrations/safe_patterns.rb +420 -0
- data/skills/activerecord/examples/migrations/schema_changes.rb +277 -0
- data/skills/activerecord/examples/querying/batch_processing.rb +226 -0
- data/skills/activerecord/examples/querying/eager_loading.rb +259 -0
- data/skills/activerecord/examples/querying/finder_methods.rb +170 -0
- data/skills/activerecord/examples/querying/optimization.rb +275 -0
- data/skills/activerecord/examples/querying/scopes.rb +260 -0
- data/skills/activerecord/examples/validations/built_in_validators.rb +277 -0
- data/skills/activerecord/examples/validations/conditional_validations.rb +288 -0
- data/skills/activerecord/examples/validations/custom_validators.rb +381 -0
- data/skills/activerecord/examples/validations/database_constraints.rb +432 -0
- data/skills/activerecord/examples/validations/validation_contexts.rb +367 -0
- data/skills/activerecord/references/associations.md +709 -0
- data/skills/activerecord/references/basics.md +622 -0
- data/skills/activerecord/references/callbacks.md +738 -0
- data/skills/activerecord/references/migrations.md +657 -0
- data/skills/activerecord/references/querying.md +655 -0
- data/skills/activerecord/references/validations.md +596 -0
- data/skills/dragonruby/SKILL.md +250 -0
- data/skills/dragonruby/examples/audio/audio_events.rb +55 -0
- data/skills/dragonruby/examples/audio/background_music.rb +29 -0
- data/skills/dragonruby/examples/audio/crossfade.rb +51 -0
- data/skills/dragonruby/examples/audio/music_controls.rb +51 -0
- data/skills/dragonruby/examples/audio/sound_effects.rb +30 -0
- data/skills/dragonruby/examples/core/coordinate_system.rb +27 -0
- data/skills/dragonruby/examples/core/hello_world.rb +24 -0
- data/skills/dragonruby/examples/core/labels.rb +22 -0
- data/skills/dragonruby/examples/core/sprites.rb +35 -0
- data/skills/dragonruby/examples/core/state_management.rb +29 -0
- data/skills/dragonruby/examples/distribution/background_pause.rb +42 -0
- data/skills/dragonruby/examples/distribution/build_workflow.sh +26 -0
- data/skills/dragonruby/examples/distribution/cvars_production.txt +16 -0
- data/skills/dragonruby/examples/distribution/game_metadata_hd.txt +23 -0
- data/skills/dragonruby/examples/distribution/game_metadata_minimal.txt +9 -0
- data/skills/dragonruby/examples/distribution/game_metadata_mobile.txt +31 -0
- data/skills/dragonruby/examples/distribution/platform_detection.rb +36 -0
- data/skills/dragonruby/examples/distribution/steam_metadata.txt +19 -0
- data/skills/dragonruby/examples/entities/collision_detection.rb +43 -0
- data/skills/dragonruby/examples/entities/entity_lifecycle.rb +68 -0
- data/skills/dragonruby/examples/entities/entity_storage.rb +38 -0
- data/skills/dragonruby/examples/entities/factory_methods.rb +45 -0
- data/skills/dragonruby/examples/entities/random_spawning.rb +50 -0
- data/skills/dragonruby/examples/game-logic/reset_patterns.rb +98 -0
- data/skills/dragonruby/examples/game-logic/save_load.rb +101 -0
- data/skills/dragonruby/examples/game-logic/scoring.rb +104 -0
- data/skills/dragonruby/examples/game-logic/state_transitions.rb +103 -0
- data/skills/dragonruby/examples/game-logic/timers.rb +87 -0
- data/skills/dragonruby/examples/input/action_triggers.rb +36 -0
- data/skills/dragonruby/examples/input/analog_movement.rb +28 -0
- data/skills/dragonruby/examples/input/controller_input.rb +28 -0
- data/skills/dragonruby/examples/input/directional_input.rb +24 -0
- data/skills/dragonruby/examples/input/keyboard_input.rb +28 -0
- data/skills/dragonruby/examples/input/mouse_click.rb +26 -0
- data/skills/dragonruby/examples/input/movement_with_bounds.rb +22 -0
- data/skills/dragonruby/examples/input/normalized_movement.rb +32 -0
- data/skills/dragonruby/examples/rendering/frame_animation.rb +32 -0
- data/skills/dragonruby/examples/rendering/labels.rb +32 -0
- data/skills/dragonruby/examples/rendering/layering.rb +51 -0
- data/skills/dragonruby/examples/rendering/solids.rb +61 -0
- data/skills/dragonruby/examples/rendering/sprites.rb +33 -0
- data/skills/dragonruby/examples/rendering/spritesheet_animation.rb +39 -0
- data/skills/dragonruby/examples/scenes/case_dispatch.rb +60 -0
- data/skills/dragonruby/examples/scenes/class_based.rb +150 -0
- data/skills/dragonruby/examples/scenes/pause_overlay.rb +100 -0
- data/skills/dragonruby/examples/scenes/safe_transitions.rb +68 -0
- data/skills/dragonruby/examples/scenes/scene_transitions.rb +98 -0
- data/skills/dragonruby/examples/scenes/send_dispatch.rb +88 -0
- data/skills/dragonruby/references/audio.md +396 -0
- data/skills/dragonruby/references/core.md +385 -0
- data/skills/dragonruby/references/distribution.md +434 -0
- data/skills/dragonruby/references/entities.md +516 -0
- data/skills/dragonruby/references/game-logic/persistence.md +386 -0
- data/skills/dragonruby/references/game-logic/state.md +389 -0
- data/skills/dragonruby/references/input.md +414 -0
- data/skills/dragonruby/references/rendering/animation.md +467 -0
- data/skills/dragonruby/references/rendering/primitives.md +403 -0
- data/skills/dragonruby/references/scenes.md +443 -0
- data/skills/draper-decorators/SKILL.md +344 -0
- data/skills/draper-decorators/examples/application_decorator.rb +61 -0
- data/skills/draper-decorators/examples/decorator_spec.rb +253 -0
- data/skills/draper-decorators/examples/model_decorator.rb +152 -0
- data/skills/draper-decorators/references/anti-patterns.md +640 -0
- data/skills/draper-decorators/references/patterns.md +507 -0
- data/skills/draper-decorators/references/testing.md +559 -0
- data/skills/gh-issue.md +182 -0
- data/skills/mcp-server/SKILL.md +177 -0
- data/skills/mcp-server/examples/dynamic_tools.rb +36 -0
- data/skills/mcp-server/examples/file_manager_tool.rb +85 -0
- data/skills/mcp-server/examples/http_client.rb +48 -0
- data/skills/mcp-server/examples/http_server.rb +97 -0
- data/skills/mcp-server/examples/rails_integration.rb +88 -0
- data/skills/mcp-server/examples/stdio_server.rb +108 -0
- data/skills/mcp-server/examples/streaming_client.rb +95 -0
- data/skills/mcp-server/references/gotchas.md +183 -0
- data/skills/mcp-server/references/prompts.md +98 -0
- data/skills/mcp-server/references/resources.md +53 -0
- data/skills/mcp-server/references/server.md +140 -0
- data/skills/mcp-server/references/tools.md +146 -0
- data/skills/mcp-server/references/transport.md +104 -0
- data/skills/ratatui-ruby/SKILL.md +315 -0
- data/skills/ratatui-ruby/references/core-concepts.md +340 -0
- data/skills/ratatui-ruby/references/events.md +387 -0
- data/skills/ratatui-ruby/references/frameworks.md +522 -0
- data/skills/ratatui-ruby/references/layout.md +423 -0
- data/skills/ratatui-ruby/references/styling.md +268 -0
- data/skills/ratatui-ruby/references/testing.md +433 -0
- data/skills/ratatui-ruby/references/widgets.md +532 -0
- data/skills/rspec/SKILL.md +340 -0
- data/skills/rspec/examples/core/basic_structure.rb +69 -0
- data/skills/rspec/examples/core/configuration.rb +126 -0
- data/skills/rspec/examples/core/hooks.rb +126 -0
- data/skills/rspec/examples/core/memoized_helpers.rb +139 -0
- data/skills/rspec/examples/core/metadata_filtering.rb +144 -0
- data/skills/rspec/examples/core/shared_examples.rb +145 -0
- data/skills/rspec/examples/factory_bot/associations.rb +314 -0
- data/skills/rspec/examples/factory_bot/build_strategies.rb +272 -0
- data/skills/rspec/examples/factory_bot/callbacks.rb +320 -0
- data/skills/rspec/examples/factory_bot/custom_construction.rb +328 -0
- data/skills/rspec/examples/factory_bot/factory_definition.rb +191 -0
- data/skills/rspec/examples/factory_bot/inheritance.rb +314 -0
- data/skills/rspec/examples/factory_bot/traits.rb +293 -0
- data/skills/rspec/examples/factory_bot/transients.rb +229 -0
- data/skills/rspec/examples/matchers/change.rb +115 -0
- data/skills/rspec/examples/matchers/collections.rb +154 -0
- data/skills/rspec/examples/matchers/comparisons.rb +79 -0
- data/skills/rspec/examples/matchers/composing.rb +155 -0
- data/skills/rspec/examples/matchers/custom_matchers.rb +197 -0
- data/skills/rspec/examples/matchers/equality.rb +58 -0
- data/skills/rspec/examples/matchers/errors.rb +136 -0
- data/skills/rspec/examples/matchers/output.rb +103 -0
- data/skills/rspec/examples/matchers/predicates.rb +87 -0
- data/skills/rspec/examples/matchers/truthiness.rb +101 -0
- data/skills/rspec/examples/matchers/types.rb +82 -0
- data/skills/rspec/examples/matchers/yield.rb +147 -0
- data/skills/rspec/examples/mocks/any_instance.rb +172 -0
- data/skills/rspec/examples/mocks/argument_matchers.rb +206 -0
- data/skills/rspec/examples/mocks/constants.rb +177 -0
- data/skills/rspec/examples/mocks/doubles.rb +139 -0
- data/skills/rspec/examples/mocks/expectations.rb +137 -0
- data/skills/rspec/examples/mocks/message_chains.rb +173 -0
- data/skills/rspec/examples/mocks/ordering.rb +144 -0
- data/skills/rspec/examples/mocks/receive_counts.rb +181 -0
- data/skills/rspec/examples/mocks/responses.rb +223 -0
- data/skills/rspec/examples/mocks/spies.rb +149 -0
- data/skills/rspec/examples/mocks/stubbing.rb +133 -0
- data/skills/rspec/examples/rails/channels.rb +250 -0
- data/skills/rspec/examples/rails/controller_specs.rb +302 -0
- data/skills/rspec/examples/rails/helper_specs.rb +245 -0
- data/skills/rspec/examples/rails/job_specs.rb +256 -0
- data/skills/rspec/examples/rails/mailer_specs.rb +228 -0
- data/skills/rspec/examples/rails/matchers.rb +374 -0
- data/skills/rspec/examples/rails/model_specs.rb +193 -0
- data/skills/rspec/examples/rails/request_specs.rb +275 -0
- data/skills/rspec/examples/rails/routing_specs.rb +276 -0
- data/skills/rspec/examples/rails/system_specs.rb +294 -0
- data/skills/rspec/examples/rails/transactions.rb +254 -0
- data/skills/rspec/examples/rails/view_specs.rb +252 -0
- data/skills/rspec/references/core.md +816 -0
- data/skills/rspec/references/factory_bot.md +641 -0
- data/skills/rspec/references/matchers.md +516 -0
- data/skills/rspec/references/mocks.md +381 -0
- data/skills/rspec/references/rails.md +528 -0
- data/templates/soul.md +40 -0
- data/workflows/commit.md +45 -0
- data/workflows/create_handoff.md +98 -0
- data/workflows/create_note.md +82 -0
- data/workflows/create_plan.md +457 -0
- data/workflows/decompose_ticket.md +109 -0
- data/workflows/feature.md +91 -0
- data/workflows/implement_plan.md +87 -0
- data/workflows/iterate_plan.md +247 -0
- data/workflows/research_codebase.md +210 -0
- data/workflows/resume_handoff.md +217 -0
- data/workflows/review_pr.md +320 -0
- data/workflows/thoughts_init.md +71 -0
- data/workflows/validate_plan.md +166 -0
- metadata +284 -2
- data/.mise.toml +0 -2
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tools
|
|
4
|
+
# Wraps a single MCP server tool for use with {Tools::Registry}.
|
|
5
|
+
# Registered as an instance (not a class) — the Registry calls
|
|
6
|
+
# +#execute+ directly without instantiation, since MCP tools
|
|
7
|
+
# carry their own client reference and are effectively stateless
|
|
8
|
+
# from the LLM's perspective.
|
|
9
|
+
#
|
|
10
|
+
# Implements the same duck-typed interface as {Tools::Base} subclasses:
|
|
11
|
+
# - +#tool_name+ — unique identifier
|
|
12
|
+
# - +#description+ — human-readable description
|
|
13
|
+
# - +#input_schema+ — JSON Schema for parameters
|
|
14
|
+
# - +#schema+ — Anthropic API tool definition
|
|
15
|
+
# - +#execute(input)+ — run the tool
|
|
16
|
+
#
|
|
17
|
+
# Tool names are namespaced as `<server_name>__<tool_name>` to prevent
|
|
18
|
+
# collisions between servers and with built-in tools.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# wrapper = Tools::McpTool.new(
|
|
22
|
+
# server_name: "mythonix",
|
|
23
|
+
# mcp_client: client,
|
|
24
|
+
# mcp_tool: client.tools.first
|
|
25
|
+
# )
|
|
26
|
+
# wrapper.tool_name # => "mythonix__create_image"
|
|
27
|
+
# wrapper.execute({"prompt" => "a red dragon"})
|
|
28
|
+
class McpTool
|
|
29
|
+
# Separator between server name and tool name in namespaced identifiers.
|
|
30
|
+
NAMESPACE_SEPARATOR = "__"
|
|
31
|
+
|
|
32
|
+
# @return [String] namespaced tool identifier (<server>__<tool>)
|
|
33
|
+
attr_reader :tool_name
|
|
34
|
+
|
|
35
|
+
# @param server_name [String] MCP server name from config
|
|
36
|
+
# @param mcp_client [MCP::Client] the client instance for this server
|
|
37
|
+
# @param mcp_tool [MCP::Client::Tool] tool metadata from the server
|
|
38
|
+
def initialize(server_name:, mcp_client:, mcp_tool:)
|
|
39
|
+
@tool_name = "#{server_name}#{NAMESPACE_SEPARATOR}#{mcp_tool.name}"
|
|
40
|
+
@mcp_client = mcp_client
|
|
41
|
+
@mcp_tool = mcp_tool
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [String] tool description from the MCP server
|
|
45
|
+
def description
|
|
46
|
+
@mcp_tool.description
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Hash] JSON Schema for tool input parameters
|
|
50
|
+
def input_schema
|
|
51
|
+
@mcp_tool.input_schema
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Builds the schema hash expected by the Anthropic tools API.
|
|
55
|
+
# @return [Hash] with :name, :description, and :input_schema keys
|
|
56
|
+
def schema
|
|
57
|
+
{name: tool_name, description: description, input_schema: input_schema}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Calls the MCP server tool and normalizes the response.
|
|
61
|
+
#
|
|
62
|
+
# @param input [Hash] tool input parameters from the LLM
|
|
63
|
+
# @return [String] normalized tool output
|
|
64
|
+
# @return [Hash] with :error key on failure
|
|
65
|
+
def execute(input)
|
|
66
|
+
response = @mcp_client.call_tool(tool: @mcp_tool, arguments: input)
|
|
67
|
+
normalize_response(response)
|
|
68
|
+
rescue MCP::Client::RequestHandlerError => error
|
|
69
|
+
{error: "#{tool_name}: #{error.message}"}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Extracts content from an MCP tool response (JSON-RPC envelope).
|
|
75
|
+
# Checks the `isError` flag and routes accordingly.
|
|
76
|
+
#
|
|
77
|
+
# @param response [Hash] full JSON-RPC response from MCP client
|
|
78
|
+
# @return [String] concatenated text content
|
|
79
|
+
# @return [Hash] with :error key if the response indicates an error
|
|
80
|
+
def normalize_response(response)
|
|
81
|
+
result = response["result"] || response
|
|
82
|
+
error = result["isError"]
|
|
83
|
+
|
|
84
|
+
text = extract_text(result)
|
|
85
|
+
error ? {error: "#{tool_name}: #{text}"} : text
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Extracts human-readable text from MCP content blocks.
|
|
89
|
+
# MCP responses contain an array of typed content blocks.
|
|
90
|
+
#
|
|
91
|
+
# @param result [Hash] MCP result containing "content" array
|
|
92
|
+
# @return [String] concatenated text from all content blocks
|
|
93
|
+
def extract_text(result)
|
|
94
|
+
content = result["content"]
|
|
95
|
+
|
|
96
|
+
return result.to_json unless content
|
|
97
|
+
|
|
98
|
+
case content
|
|
99
|
+
when Array
|
|
100
|
+
content.filter_map { |block|
|
|
101
|
+
case block["type"]
|
|
102
|
+
when "text" then block["text"]
|
|
103
|
+
when "image" then "[image: #{block["mimeType"]}]"
|
|
104
|
+
else block.to_json
|
|
105
|
+
end
|
|
106
|
+
}.join("\n")
|
|
107
|
+
when String
|
|
108
|
+
content
|
|
109
|
+
else
|
|
110
|
+
content.to_json
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/tools/read.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Tools
|
|
|
4
4
|
# Reads file contents with smart truncation and offset/limit paging.
|
|
5
5
|
# Returns plain text without line numbers, normalized to LF line endings.
|
|
6
6
|
#
|
|
7
|
-
# Truncation limits: `
|
|
7
|
+
# Truncation limits: `Anima::Settings.max_read_lines` lines or `Anima::Settings.max_read_bytes` bytes, whichever
|
|
8
8
|
# hits first. When truncated, appends a continuation hint with the next
|
|
9
9
|
# offset value so the agent can page through large files.
|
|
10
10
|
#
|
|
@@ -16,9 +16,6 @@ module Tools
|
|
|
16
16
|
# tool.execute("path" => "large.log", "offset" => 2001, "limit" => 500)
|
|
17
17
|
# # => "line 2001 content\n..."
|
|
18
18
|
class Read < Base
|
|
19
|
-
MAX_LINES = 2_000
|
|
20
|
-
MAX_BYTES = 50_000
|
|
21
|
-
|
|
22
19
|
def self.tool_name = "read"
|
|
23
20
|
|
|
24
21
|
def self.description = "Read file contents. Returns plain text with smart truncation. Use offset/limit to page through large files."
|
|
@@ -29,7 +26,7 @@ module Tools
|
|
|
29
26
|
properties: {
|
|
30
27
|
path: {type: "string", description: "Absolute or relative file path (relative resolved against working directory)"},
|
|
31
28
|
offset: {type: "integer", description: "1-indexed line number to start from (default: 1)"},
|
|
32
|
-
limit: {type: "integer", description: "Maximum
|
|
29
|
+
limit: {type: "integer", description: "Maximum lines to read (subject to line and byte caps from config)"}
|
|
33
30
|
},
|
|
34
31
|
required: ["path"]
|
|
35
32
|
}
|
|
@@ -61,7 +58,7 @@ module Tools
|
|
|
61
58
|
path = input["path"].to_s.strip
|
|
62
59
|
offset = [input["offset"].to_i, 1].max
|
|
63
60
|
raw_limit = input["limit"]
|
|
64
|
-
limit = raw_limit ? [raw_limit.to_i, 1].max :
|
|
61
|
+
limit = raw_limit ? [raw_limit.to_i, 1].max : Anima::Settings.max_read_lines
|
|
65
62
|
[path, offset, limit]
|
|
66
63
|
end
|
|
67
64
|
|
|
@@ -81,15 +78,15 @@ module Tools
|
|
|
81
78
|
|
|
82
79
|
# Reads the file, normalizes line endings, and applies truncation limits.
|
|
83
80
|
# Two limits are enforced as first-hit-wins: line count and byte size.
|
|
84
|
-
# A single line exceeding `
|
|
85
|
-
# Files larger than
|
|
86
|
-
MAX_READ_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
81
|
+
# A single line exceeding `Anima::Settings.max_read_bytes` is rejected outright (likely minified).
|
|
82
|
+
# Files larger than max_file_size are rejected to avoid memory exhaustion.
|
|
87
83
|
|
|
88
84
|
def read_file(path, offset, limit)
|
|
89
85
|
file_size = File.size(path)
|
|
90
|
-
|
|
86
|
+
max_size = Anima::Settings.max_file_size
|
|
87
|
+
if file_size > max_size
|
|
91
88
|
return {error: "File is #{file_size} bytes (#{file_size / 1_048_576} MB). " \
|
|
92
|
-
"Max readable size is #{
|
|
89
|
+
"Max readable size is #{max_size / 1_048_576} MB. " \
|
|
93
90
|
"Use bash tool with: head -n #{offset + limit} #{path} | tail -n +#{offset}"}
|
|
94
91
|
end
|
|
95
92
|
|
|
@@ -99,7 +96,7 @@ module Tools
|
|
|
99
96
|
start_index = offset - 1
|
|
100
97
|
return "[File has #{lines.size} lines. Offset #{offset} is beyond end of file.]" if start_index >= lines.size
|
|
101
98
|
|
|
102
|
-
window = lines[start_index, [limit,
|
|
99
|
+
window = lines[start_index, [limit, Anima::Settings.max_read_lines].min]
|
|
103
100
|
|
|
104
101
|
error = check_oversized_lines(window, offset, path)
|
|
105
102
|
return error if error
|
|
@@ -112,11 +109,12 @@ module Tools
|
|
|
112
109
|
end
|
|
113
110
|
|
|
114
111
|
def check_oversized_lines(window, offset, path)
|
|
115
|
-
|
|
112
|
+
max_bytes = Anima::Settings.max_read_bytes
|
|
113
|
+
index = window.index { |line| line.bytesize > max_bytes }
|
|
116
114
|
return unless index
|
|
117
115
|
|
|
118
116
|
line_num = offset + index
|
|
119
|
-
{error: "Line #{line_num} exceeds #{
|
|
117
|
+
{error: "Line #{line_num} exceeds #{max_bytes} bytes (likely minified). " \
|
|
120
118
|
"Use bash tool with: sed -n '#{line_num}p' #{path}"}
|
|
121
119
|
end
|
|
122
120
|
|
|
@@ -131,15 +129,16 @@ module Tools
|
|
|
131
129
|
end
|
|
132
130
|
end
|
|
133
131
|
|
|
134
|
-
# Accumulates lines until
|
|
132
|
+
# Accumulates lines until the byte cap would be exceeded.
|
|
135
133
|
# @return [Array(String, Integer)] accumulated text and number of lines included
|
|
136
134
|
def accumulate_lines(window)
|
|
135
|
+
max_bytes = Anima::Settings.max_read_bytes
|
|
137
136
|
output = +""
|
|
138
137
|
bytes = 0
|
|
139
138
|
count = 0
|
|
140
139
|
|
|
141
140
|
window.each_with_index do |line, index|
|
|
142
|
-
break if bytes + line.bytesize >
|
|
141
|
+
break if bytes + line.bytesize > max_bytes && index > 0
|
|
143
142
|
|
|
144
143
|
output << line
|
|
145
144
|
bytes += line.bytesize
|
data/lib/tools/registry.rb
CHANGED
|
@@ -4,16 +4,17 @@ module Tools
|
|
|
4
4
|
class UnknownToolError < StandardError; end
|
|
5
5
|
|
|
6
6
|
# Manages tool registration, schema export, and dispatch.
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Accepts both tool classes (e.g. {Tools::Base} subclasses) and tool
|
|
8
|
+
# instances (e.g. {Tools::McpTool}) via duck typing. Classes are
|
|
9
|
+
# instantiated with the registry's context on each execution; instances
|
|
10
|
+
# are called directly since they carry their own state.
|
|
10
11
|
#
|
|
11
12
|
# @example
|
|
12
13
|
# registry = Tools::Registry.new(context: {shell_session: my_shell})
|
|
13
14
|
# registry.register(Tools::Bash)
|
|
14
15
|
# registry.execute("bash", {"command" => "ls"})
|
|
15
16
|
class Registry
|
|
16
|
-
# @return [Hash{String => Class}] registered
|
|
17
|
+
# @return [Hash{String => Class, Object}] registered tools keyed by name
|
|
17
18
|
attr_reader :tools
|
|
18
19
|
|
|
19
20
|
# @param context [Hash] keyword arguments forwarded to every tool constructor
|
|
@@ -22,11 +23,11 @@ module Tools
|
|
|
22
23
|
@context = context
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
# Register a tool class
|
|
26
|
-
# @param
|
|
26
|
+
# Register a tool class or instance. Must respond to +tool_name+ and +schema+.
|
|
27
|
+
# @param tool [Class<Tools::Base>, #tool_name] tool class or duck-typed instance
|
|
27
28
|
# @return [void]
|
|
28
|
-
def register(
|
|
29
|
-
@tools[
|
|
29
|
+
def register(tool)
|
|
30
|
+
@tools[tool.tool_name] = tool
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# @return [Array<Hash>] schema array for the Anthropic tools API parameter
|
|
@@ -34,16 +35,17 @@ module Tools
|
|
|
34
35
|
@tools.values.map(&:schema)
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
38
|
+
# Execute a tool by name. Classes are instantiated with the registry's
|
|
39
|
+
# context; instances are called directly.
|
|
39
40
|
#
|
|
40
41
|
# @param name [String] registered tool name
|
|
41
42
|
# @param input [Hash] tool input parameters
|
|
42
43
|
# @return [String, Hash] tool execution result
|
|
43
44
|
# @raise [UnknownToolError] if no tool is registered with the given name
|
|
44
45
|
def execute(name, input)
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
tool = @tools.fetch(name) { raise UnknownToolError, "Unknown tool: #{name}" }
|
|
47
|
+
instance = tool.is_a?(Class) ? tool.new(**@context) : tool
|
|
48
|
+
instance.execute(input)
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
# @param name [String] tool name to check
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Tools
|
|
6
|
+
# Creates a GitHub issue via the +gh+ CLI, letting the agent request
|
|
7
|
+
# capabilities it discovers are missing during real work. Every issue
|
|
8
|
+
# is tagged with the label from +[github] label+ in +config.toml+ so
|
|
9
|
+
# the developer can filter agent-originated requests from human ones.
|
|
10
|
+
#
|
|
11
|
+
# The repository is read from +[github] repo+ in +config.toml+; when
|
|
12
|
+
# unset, the tool falls back to parsing the +origin+ remote URL.
|
|
13
|
+
#
|
|
14
|
+
# @see https://github.com/hoblin/anima/issues/103
|
|
15
|
+
class RequestFeature < Base
|
|
16
|
+
# @return [String] tool identifier used in the Anthropic API schema
|
|
17
|
+
def self.tool_name = "request_feature"
|
|
18
|
+
|
|
19
|
+
# @return [String] motivational description shown to the LLM
|
|
20
|
+
def self.description
|
|
21
|
+
"Don't have the right tool for this task? Request it! " \
|
|
22
|
+
"Creates a GitHub issue so the developer knows what you need."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @return [Hash] JSON Schema for the tool's input parameters
|
|
26
|
+
def self.input_schema
|
|
27
|
+
{
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
title: {type: "string", description: "Short, descriptive title for the feature request"},
|
|
31
|
+
description: {type: "string", description: "What you need and why — what were you trying to do, and what's missing?"}
|
|
32
|
+
},
|
|
33
|
+
required: %w[title description]
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param input [Hash<String, Object>] with +"title"+ and +"description"+ keys
|
|
38
|
+
# @return [String] formatted gh command output (stdout, stderr, and exit code if non-zero)
|
|
39
|
+
# @return [Hash{Symbol => String}] with +:error+ key on validation or repo resolution failure
|
|
40
|
+
def execute(input)
|
|
41
|
+
title = input["title"].to_s.strip
|
|
42
|
+
description = input["description"].to_s.strip
|
|
43
|
+
return {error: "Title cannot be blank"} if title.empty?
|
|
44
|
+
return {error: "Description cannot be blank"} if description.empty?
|
|
45
|
+
|
|
46
|
+
repo = resolve_repo
|
|
47
|
+
return repo if repo.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
run_gh(repo, title, description)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Resolves the target repository: config.toml setting first, then git remote origin.
|
|
55
|
+
# @return [String] owner/repo identifier
|
|
56
|
+
# @return [Hash{Symbol => String}] error hash when no repository can be determined
|
|
57
|
+
def resolve_repo
|
|
58
|
+
repo = settings_repo || git_remote_repo
|
|
59
|
+
return {error: "Cannot determine repository. Set [github] repo in config.toml or ensure a git remote origin exists."} unless repo
|
|
60
|
+
|
|
61
|
+
repo
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [String, nil] repo from config.toml, nil when not configured
|
|
65
|
+
def settings_repo
|
|
66
|
+
value = Anima::Settings.github_repo
|
|
67
|
+
value unless value.to_s.strip.empty?
|
|
68
|
+
rescue Anima::Settings::MissingSettingError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# @return [String, nil] owner/repo parsed from +git remote get-url origin+
|
|
73
|
+
def git_remote_repo
|
|
74
|
+
url, _status = Open3.capture2("git", "remote", "get-url", "origin")
|
|
75
|
+
parse_owner_repo(url.strip)
|
|
76
|
+
rescue Errno::ENOENT
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Extracts +owner/repo+ from common GitHub remote URL formats.
|
|
81
|
+
# @param url [String] SSH or HTTPS remote URL
|
|
82
|
+
# @return [String, nil] owner/repo or nil when the URL is not recognizable
|
|
83
|
+
def parse_owner_repo(url)
|
|
84
|
+
case url
|
|
85
|
+
when %r{github\.com[:/]([^/]+/[^/]+?)(?:\.git)?$}
|
|
86
|
+
Regexp.last_match(1)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Invokes +gh issue create+ and returns the formatted output.
|
|
91
|
+
# @param repo [String] owner/repo identifier
|
|
92
|
+
# @param title [String] issue title
|
|
93
|
+
# @param description [String] issue body
|
|
94
|
+
# @return [String] formatted command output
|
|
95
|
+
def run_gh(repo, title, description)
|
|
96
|
+
stdout, stderr, status = Open3.capture3(
|
|
97
|
+
"gh", "issue", "create",
|
|
98
|
+
"--repo", repo,
|
|
99
|
+
"--label", Anima::Settings.github_label,
|
|
100
|
+
"--title", title,
|
|
101
|
+
"--body", description
|
|
102
|
+
)
|
|
103
|
+
format_result(stdout, stderr, status.exitstatus)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Combines stdout, stderr, and exit code into a single string response.
|
|
107
|
+
# @param stdout [String] captured standard output
|
|
108
|
+
# @param stderr [String] captured standard error
|
|
109
|
+
# @param exit_code [Integer] process exit status
|
|
110
|
+
# @return [String] joined non-empty parts separated by blank lines
|
|
111
|
+
def format_result(stdout, stderr, exit_code)
|
|
112
|
+
out = stdout.strip
|
|
113
|
+
err = stderr.strip
|
|
114
|
+
parts = []
|
|
115
|
+
parts << out unless out.empty?
|
|
116
|
+
parts << err unless err.empty?
|
|
117
|
+
parts << "exit_code: #{exit_code}" unless exit_code.zero?
|
|
118
|
+
parts.join("\n\n")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tools
|
|
4
|
+
# Sub-agent-only tool that delivers a completed result back to the
|
|
5
|
+
# parent session as a tool_call/tool_response pair. The parent agent
|
|
6
|
+
# sees it as if it called a tool itself — no custom event types needed.
|
|
7
|
+
#
|
|
8
|
+
# Never registered for main sessions — only sub-agents see this tool.
|
|
9
|
+
class ReturnResult < Base
|
|
10
|
+
def self.tool_name = "return_result"
|
|
11
|
+
|
|
12
|
+
def self.description = "Return your completed result to the parent agent. " \
|
|
13
|
+
"Call this when you have fulfilled the assigned task."
|
|
14
|
+
|
|
15
|
+
def self.input_schema
|
|
16
|
+
{
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
result: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "The completed deliverable to send back to the parent agent"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
required: ["result"]
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param session [Session] the sub-agent session returning a result
|
|
29
|
+
def initialize(session:, **)
|
|
30
|
+
@session = session
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Emits a tool_call/tool_response pair in the parent session so the
|
|
34
|
+
# parent agent sees the sub-agent result as a regular tool interaction.
|
|
35
|
+
#
|
|
36
|
+
# @param input [Hash<String, Object>] with "result" key
|
|
37
|
+
# @return [String, Hash] confirmation message, or Hash with :error key on failure
|
|
38
|
+
def execute(input)
|
|
39
|
+
result = input["result"].to_s.strip
|
|
40
|
+
return {error: "Result cannot be blank"} if result.empty?
|
|
41
|
+
|
|
42
|
+
parent = @session.parent_session
|
|
43
|
+
return {error: "No parent session — only sub-agents can return results"} unless parent
|
|
44
|
+
|
|
45
|
+
tool_use_id = "toolu_subagent_#{@session.id}"
|
|
46
|
+
task = extract_task
|
|
47
|
+
# Specialists are spawned with a name from the registry; generic sub-agents have nil name.
|
|
48
|
+
origin_tool = @session.name ? SpawnSpecialist.tool_name : SpawnSubagent.tool_name
|
|
49
|
+
|
|
50
|
+
Events::Bus.emit(Events::ToolCall.new(
|
|
51
|
+
content: "Sub-agent result (session #{@session.id})",
|
|
52
|
+
tool_name: origin_tool,
|
|
53
|
+
tool_input: {"task" => task, "session_id" => @session.id},
|
|
54
|
+
tool_use_id: tool_use_id,
|
|
55
|
+
session_id: parent.id
|
|
56
|
+
))
|
|
57
|
+
|
|
58
|
+
Events::Bus.emit(Events::ToolResponse.new(
|
|
59
|
+
content: result,
|
|
60
|
+
tool_name: origin_tool,
|
|
61
|
+
tool_use_id: tool_use_id,
|
|
62
|
+
session_id: parent.id
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
"Result delivered to parent session #{parent.id}."
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Extracts the original task from the sub-agent's first user message.
|
|
71
|
+
# @return [String]
|
|
72
|
+
def extract_task
|
|
73
|
+
@session.events
|
|
74
|
+
.where(event_type: "user_message")
|
|
75
|
+
.order(:id)
|
|
76
|
+
.pick(:payload)
|
|
77
|
+
&.dig("content")
|
|
78
|
+
.to_s
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tools
|
|
4
|
+
# Spawns a named specialist sub-agent from the agent registry.
|
|
5
|
+
# The specialist has a predefined system prompt and tool set defined
|
|
6
|
+
# in its Markdown definition file under agents/.
|
|
7
|
+
#
|
|
8
|
+
# Results are delivered back through {Tools::ReturnResult}.
|
|
9
|
+
#
|
|
10
|
+
# @see Agents::Registry
|
|
11
|
+
# @see Agents::Definition
|
|
12
|
+
class SpawnSpecialist < Base
|
|
13
|
+
include SubagentPrompts
|
|
14
|
+
|
|
15
|
+
def self.tool_name = "spawn_specialist"
|
|
16
|
+
|
|
17
|
+
# Builds description dynamically to include available specialists.
|
|
18
|
+
def self.description
|
|
19
|
+
base = "Spawn a named specialist sub-agent to work on a task autonomously. " \
|
|
20
|
+
"The specialist has a predefined role, system prompt, and tool set."
|
|
21
|
+
|
|
22
|
+
registry = Agents::Registry.instance
|
|
23
|
+
return base unless registry.any?
|
|
24
|
+
|
|
25
|
+
specialist_list = registry.catalog.map { |name, desc| "- #{name}: #{desc}" }.join("\n")
|
|
26
|
+
"#{base}\n\nAvailable specialists:\n#{specialist_list}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Builds input schema dynamically to include named agent enum.
|
|
30
|
+
def self.input_schema
|
|
31
|
+
{
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
name: name_property,
|
|
35
|
+
task: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "What the specialist should do (emitted as its first user message)"
|
|
38
|
+
},
|
|
39
|
+
expected_output: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Description of the expected deliverable"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
required: %w[name task expected_output]
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Hash] JSON Schema property for the name parameter
|
|
49
|
+
def self.name_property
|
|
50
|
+
registry = Agents::Registry.instance
|
|
51
|
+
prop = {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "Named specialist agent to spawn from the registry."
|
|
54
|
+
}
|
|
55
|
+
prop[:enum] = registry.names if registry.any?
|
|
56
|
+
prop
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private_class_method :name_property
|
|
60
|
+
|
|
61
|
+
# @param session [Session] the parent session spawning the specialist
|
|
62
|
+
# @param agent_registry [Agents::Registry, nil] injectable for testing
|
|
63
|
+
def initialize(session:, agent_registry: nil, **)
|
|
64
|
+
@session = session
|
|
65
|
+
@agent_registry = agent_registry || Agents::Registry.instance
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Creates a child session with the specialist's predefined prompt and tools,
|
|
69
|
+
# emits the task as a user message, and queues background processing.
|
|
70
|
+
#
|
|
71
|
+
# @param input [Hash<String, Object>] with "name", "task", and "expected_output"
|
|
72
|
+
# @return [String] confirmation with child session ID
|
|
73
|
+
# @return [Hash{Symbol => String}] with :error key on validation failure
|
|
74
|
+
def execute(input)
|
|
75
|
+
task = input["task"].to_s.strip
|
|
76
|
+
expected_output = input["expected_output"].to_s.strip
|
|
77
|
+
name = input["name"].to_s.strip
|
|
78
|
+
|
|
79
|
+
return {error: "Name cannot be blank"} if name.empty?
|
|
80
|
+
return {error: "Task cannot be blank"} if task.empty?
|
|
81
|
+
return {error: "Expected output cannot be blank"} if expected_output.empty?
|
|
82
|
+
|
|
83
|
+
definition = @agent_registry.get(name)
|
|
84
|
+
return {error: "Unknown agent: #{name}"} unless definition
|
|
85
|
+
|
|
86
|
+
child = spawn_child(definition, task, expected_output)
|
|
87
|
+
"Specialist '#{name}' spawned (session #{child.id}). Result will arrive as a tool response."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def spawn_child(definition, task, expected_output)
|
|
93
|
+
prompt = build_prompt(definition, expected_output)
|
|
94
|
+
child = Session.create!(
|
|
95
|
+
parent_session_id: @session.id,
|
|
96
|
+
prompt: prompt,
|
|
97
|
+
granted_tools: definition.tools,
|
|
98
|
+
name: definition.name
|
|
99
|
+
)
|
|
100
|
+
Events::Bus.emit(Events::UserMessage.new(content: task, session_id: child.id))
|
|
101
|
+
AgentRequestJob.perform_later(child.id)
|
|
102
|
+
child
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_prompt(definition, expected_output)
|
|
106
|
+
"#{definition.prompt}\n\n#{RETURN_INSTRUCTION}\n\n#{EXPECTED_DELIVERABLE_PREFIX}#{expected_output}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|