swarm_sdk 2.7.14 → 3.0.0.alpha2
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/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
- data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
- data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
- data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +213 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +88 -149
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -705
- data/lib/swarm_sdk/agent/chat.rb +0 -1438
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -588
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -558
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -368
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -285
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -248
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -241
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -446
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -973
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- data/lib/swarm_sdk.rb +0 -721
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# SubTask tool for spawning focused subtask agents
|
|
7
|
+
#
|
|
8
|
+
# Allows an agent to spawn a copy of itself for a focused subtask.
|
|
9
|
+
# The subtask agent shares the parent's memory (read-only), MCP
|
|
10
|
+
# connections, and skill access. It runs in the same process.
|
|
11
|
+
#
|
|
12
|
+
# SubTask is opt-in — agents must include :SubTask in their tools list.
|
|
13
|
+
# This prevents accidental cost explosion from unintended spawning.
|
|
14
|
+
#
|
|
15
|
+
# ## Depth control
|
|
16
|
+
#
|
|
17
|
+
# Subtasks track nesting depth. The maximum depth is controlled by
|
|
18
|
+
# {Configuration#max_subtask_depth} (default: 1, meaning the parent
|
|
19
|
+
# can spawn subtasks, but subtasks cannot spawn further subtasks).
|
|
20
|
+
#
|
|
21
|
+
# @example Agent definition with SubTask
|
|
22
|
+
# AgentDefinition.new(
|
|
23
|
+
# name: :researcher,
|
|
24
|
+
# description: "Research agent",
|
|
25
|
+
# tools: [:Read, :Grep, :Glob, :Think, :SubTask],
|
|
26
|
+
# )
|
|
27
|
+
class SubTask < Base
|
|
28
|
+
class << self
|
|
29
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
30
|
+
def creation_requirements
|
|
31
|
+
[:agent_definition, :memory_store, :directory, :subtask_depth]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param agent_definition [AgentDefinition] Parent agent's definition
|
|
36
|
+
# @param memory_store [Memory::Store, nil] Parent's memory store
|
|
37
|
+
# @param directory [String] Working directory
|
|
38
|
+
# @param subtask_depth [Integer] Current nesting depth
|
|
39
|
+
def initialize(agent_definition:, memory_store: nil, directory: ".", subtask_depth: 0)
|
|
40
|
+
super()
|
|
41
|
+
@agent_definition = agent_definition
|
|
42
|
+
@memory_store = memory_store
|
|
43
|
+
@directory = directory
|
|
44
|
+
@subtask_depth = subtask_depth
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
description <<~DESC
|
|
48
|
+
Spawn a focused subtask to handle a specific piece of work.
|
|
49
|
+
|
|
50
|
+
The subtask runs as a copy of you with the same tools, skills, and MCP
|
|
51
|
+
connections, but in an **isolated context window**. Results are returned
|
|
52
|
+
as a summary, keeping your main context clean.
|
|
53
|
+
|
|
54
|
+
## When to use SubTask
|
|
55
|
+
|
|
56
|
+
**Strongly prefer SubTask when:**
|
|
57
|
+
- Reading or analyzing multiple files (3+ files)
|
|
58
|
+
- Making multiple MCP tool calls (MCP operations are context-heavy)
|
|
59
|
+
- Searching across a codebase (grep/glob + read patterns)
|
|
60
|
+
- Performing any operation that would return large outputs
|
|
61
|
+
- Investigating tangential questions before returning to main task
|
|
62
|
+
|
|
63
|
+
**Use direct tools when:**
|
|
64
|
+
- Single, quick operations (one file read, one simple command)
|
|
65
|
+
- You need the raw output for immediate follow-up in the same turn
|
|
66
|
+
|
|
67
|
+
## Context efficiency
|
|
68
|
+
|
|
69
|
+
Each subtask gets a fresh context window. Instead of accumulating
|
|
70
|
+
50k+ tokens of tool outputs in your context, you receive a focused
|
|
71
|
+
summary. This keeps you effective over long conversations.
|
|
72
|
+
|
|
73
|
+
## Example patterns
|
|
74
|
+
|
|
75
|
+
Instead of: Read file1, Read file2, Read file3, Grep for X, analyze
|
|
76
|
+
Do: SubTask("Analyze auth system", "Read the auth files and grep for
|
|
77
|
+
login patterns, summarize the authentication flow")
|
|
78
|
+
|
|
79
|
+
Instead of: Multiple MCP tool calls to external services
|
|
80
|
+
Do: SubTask("Gather external data", "Use the MCP tools to fetch X, Y, Z
|
|
81
|
+
and return the relevant findings")
|
|
82
|
+
|
|
83
|
+
## Memory access
|
|
84
|
+
|
|
85
|
+
The subtask has read-only access to your memory — it can retrieve
|
|
86
|
+
context but cannot write new memories.
|
|
87
|
+
|
|
88
|
+
Be specific in your instructions. The subtask sees only what you
|
|
89
|
+
provide in the instructions field, plus any memory it retrieves.
|
|
90
|
+
DESC
|
|
91
|
+
|
|
92
|
+
param :title,
|
|
93
|
+
type: "string",
|
|
94
|
+
desc: "Short title for the subtask (used in logging)",
|
|
95
|
+
required: true
|
|
96
|
+
|
|
97
|
+
param :instructions,
|
|
98
|
+
type: "string",
|
|
99
|
+
desc: "Detailed instructions for the subtask. Be specific — the subtask only sees these instructions and its retrieved memory.",
|
|
100
|
+
required: true
|
|
101
|
+
|
|
102
|
+
# Execute the SubTask tool
|
|
103
|
+
#
|
|
104
|
+
# Spawns a SubTaskAgent, runs the instructions, and returns the result.
|
|
105
|
+
# The subtask agent is always cleaned up via ensure block.
|
|
106
|
+
#
|
|
107
|
+
# @param title [String] Short title for logging
|
|
108
|
+
# @param instructions [String] Detailed instructions for the subtask
|
|
109
|
+
# @return [String] Subtask result or error message
|
|
110
|
+
def execute(title:, instructions:, **_kwargs)
|
|
111
|
+
return validation_error("title is required") if title.to_s.strip.empty?
|
|
112
|
+
return validation_error("instructions are required") if instructions.to_s.strip.empty?
|
|
113
|
+
|
|
114
|
+
next_depth = @subtask_depth + 1
|
|
115
|
+
max_depth = Configuration.instance.max_subtask_depth
|
|
116
|
+
|
|
117
|
+
if next_depth > max_depth
|
|
118
|
+
return error(
|
|
119
|
+
"Maximum subtask depth (#{max_depth}) exceeded. " \
|
|
120
|
+
"Cannot spawn nested subtask at depth #{next_depth}.",
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
agent = SubTaskAgent.new(
|
|
125
|
+
@agent_definition,
|
|
126
|
+
parent_memory_store: @memory_store,
|
|
127
|
+
subtask_depth: next_depth,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
EventStream.emit(
|
|
131
|
+
type: "subtask_spawned",
|
|
132
|
+
agent: @agent_definition.name,
|
|
133
|
+
subtask_agent: agent.id,
|
|
134
|
+
title: title,
|
|
135
|
+
depth: next_depth,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
prompt = <<~PROMPT.strip
|
|
139
|
+
# SubTask: #{title}
|
|
140
|
+
|
|
141
|
+
#{instructions}
|
|
142
|
+
PROMPT
|
|
143
|
+
|
|
144
|
+
response = agent.ask(prompt)
|
|
145
|
+
format_result(title, response)
|
|
146
|
+
rescue StandardError => e
|
|
147
|
+
EventStream.emit(
|
|
148
|
+
type: "subtask_failed",
|
|
149
|
+
agent: @agent_definition.name,
|
|
150
|
+
subtask_agent: agent&.id,
|
|
151
|
+
title: title,
|
|
152
|
+
error: "#{e.class}: #{e.message}",
|
|
153
|
+
)
|
|
154
|
+
error("Subtask '#{title}' failed: #{e.class}: #{e.message}")
|
|
155
|
+
ensure
|
|
156
|
+
agent&.clear
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
# Format a successful subtask result
|
|
162
|
+
#
|
|
163
|
+
# @param title [String] Subtask title
|
|
164
|
+
# @param response [RubyLLM::Message] LLM response
|
|
165
|
+
# @return [String] Formatted result
|
|
166
|
+
def format_result(title, response)
|
|
167
|
+
EventStream.emit(
|
|
168
|
+
type: "subtask_completed",
|
|
169
|
+
agent: @agent_definition.name,
|
|
170
|
+
title: title,
|
|
171
|
+
success: true,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
<<~RESULT.strip
|
|
175
|
+
## SubTask Result: #{title}
|
|
176
|
+
|
|
177
|
+
#{response&.content || "(no response)"}
|
|
178
|
+
RESULT
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Think tool for explicit reasoning and planning
|
|
7
|
+
#
|
|
8
|
+
# Allows the agent to write down thoughts, plans, and intermediate
|
|
9
|
+
# calculations. These thoughts become part of the conversation context,
|
|
10
|
+
# enabling better reasoning through complex problems.
|
|
11
|
+
#
|
|
12
|
+
# When the agent has memory enabled, Think retrieves relevant memories
|
|
13
|
+
# for the thought content and returns them alongside "Thought noted."
|
|
14
|
+
# This gives the agent access to its long-term knowledge while reasoning.
|
|
15
|
+
# Without memory, Think behaves as a simple acknowledgment.
|
|
16
|
+
#
|
|
17
|
+
# Think does NOT write to memory — the existing turn-level async
|
|
18
|
+
# ingestion pipeline already captures tool call arguments (including
|
|
19
|
+
# Think's thoughts), so Think content is automatically persisted.
|
|
20
|
+
class Think < Base
|
|
21
|
+
class << self
|
|
22
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
23
|
+
def creation_requirements
|
|
24
|
+
[:memory_store]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param memory_store [Memory::Store, nil] Memory store for retrieval
|
|
29
|
+
def initialize(memory_store: nil)
|
|
30
|
+
super()
|
|
31
|
+
@memory_store = memory_store
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
description <<~DESC
|
|
35
|
+
Use this tool to think through problems step by step before acting.
|
|
36
|
+
|
|
37
|
+
This is your working memory — record your thoughts, plans, strategies,
|
|
38
|
+
and intermediate calculations. Using this tool leads to significantly
|
|
39
|
+
better outcomes and more accurate solutions.
|
|
40
|
+
|
|
41
|
+
Use it frequently:
|
|
42
|
+
- Before starting any task
|
|
43
|
+
- When doing arithmetic or counting
|
|
44
|
+
- After reading files to process what you learned
|
|
45
|
+
- Between steps to track progress
|
|
46
|
+
- When debugging to trace issues
|
|
47
|
+
|
|
48
|
+
Your thoughts persist throughout the session as conversation history.
|
|
49
|
+
DESC
|
|
50
|
+
|
|
51
|
+
param :thoughts,
|
|
52
|
+
type: "string",
|
|
53
|
+
desc: "Your thoughts, plans, calculations, or any notes you want to record",
|
|
54
|
+
required: true
|
|
55
|
+
|
|
56
|
+
# Execute the Think tool
|
|
57
|
+
#
|
|
58
|
+
# When memory is available, searches for cards relevant to the thought
|
|
59
|
+
# content and returns them as context for the agent's reasoning.
|
|
60
|
+
#
|
|
61
|
+
# @param thoughts [String] The agent's thoughts
|
|
62
|
+
# @return [String] Acknowledgment with optional related memories
|
|
63
|
+
def execute(thoughts:, **_kwargs)
|
|
64
|
+
return "Thought noted." unless @memory_store
|
|
65
|
+
|
|
66
|
+
cards = @memory_store.search(thoughts, top_k: 5)
|
|
67
|
+
format_response(cards)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Format the response with optional related memories
|
|
73
|
+
#
|
|
74
|
+
# @param cards [Array<Card>] Retrieved memory cards
|
|
75
|
+
# @return [String] Formatted response
|
|
76
|
+
def format_response(cards)
|
|
77
|
+
return "Thought noted." if cards.empty?
|
|
78
|
+
|
|
79
|
+
lines = ["Thought noted.", "", "Related memories:"]
|
|
80
|
+
cards.each do |card|
|
|
81
|
+
lines << "- [#{card.type.to_s.capitalize}] #{card.text}"
|
|
82
|
+
end
|
|
83
|
+
lines.join("\n")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module V3
|
|
5
|
+
module Tools
|
|
6
|
+
# Write tool for writing content to files
|
|
7
|
+
#
|
|
8
|
+
# Creates new files or overwrites existing files.
|
|
9
|
+
# Enforces read-before-write for existing files via the Read tool's tracking.
|
|
10
|
+
class Write < Base
|
|
11
|
+
class << self
|
|
12
|
+
# @return [Array<Symbol>] Constructor requirements
|
|
13
|
+
def creation_requirements
|
|
14
|
+
[:agent_name, :directory, :read_tracker]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
description <<~DESC
|
|
19
|
+
Writes a file to the local filesystem.
|
|
20
|
+
Overwrites existing files. You MUST use Read first on existing files.
|
|
21
|
+
ALWAYS prefer editing existing files. NEVER write new files unless required.
|
|
22
|
+
|
|
23
|
+
Path handling:
|
|
24
|
+
- Relative paths resolve against your working directory
|
|
25
|
+
- Absolute paths (starting with /) are used as-is
|
|
26
|
+
DESC
|
|
27
|
+
|
|
28
|
+
param :file_path,
|
|
29
|
+
type: "string",
|
|
30
|
+
desc: "Path to the file to write",
|
|
31
|
+
required: true
|
|
32
|
+
|
|
33
|
+
param :content,
|
|
34
|
+
type: "string",
|
|
35
|
+
desc: "The content to write to the file",
|
|
36
|
+
required: true
|
|
37
|
+
|
|
38
|
+
# @param agent_name [Symbol, String] Agent identifier
|
|
39
|
+
# @param directory [String] Agent's working directory
|
|
40
|
+
# @param read_tracker [ReadTracker] Shared read tracker for enforcement
|
|
41
|
+
def initialize(agent_name:, directory:, read_tracker:)
|
|
42
|
+
super()
|
|
43
|
+
@agent_name = agent_name.to_sym
|
|
44
|
+
@directory = File.expand_path(directory)
|
|
45
|
+
@read_tracker = read_tracker
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Execute file write
|
|
49
|
+
#
|
|
50
|
+
# @param file_path [String] Path to the file
|
|
51
|
+
# @param content [String] Content to write
|
|
52
|
+
# @return [String] Success or error message
|
|
53
|
+
def execute(file_path:, content:)
|
|
54
|
+
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
|
55
|
+
return validation_error("content is required") if content.nil?
|
|
56
|
+
|
|
57
|
+
resolved_path = resolve_path(file_path)
|
|
58
|
+
file_exists = File.exist?(resolved_path)
|
|
59
|
+
|
|
60
|
+
if file_exists && !@read_tracker.file_read?(@agent_name, resolved_path)
|
|
61
|
+
return validation_error(
|
|
62
|
+
"Cannot write to existing file without reading it first. " \
|
|
63
|
+
"Use the Read tool on '#{file_path}' before overwriting.",
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
parent_dir = File.dirname(resolved_path)
|
|
68
|
+
FileUtils.mkdir_p(parent_dir) unless File.directory?(parent_dir)
|
|
69
|
+
|
|
70
|
+
File.write(resolved_path, content, encoding: "UTF-8")
|
|
71
|
+
|
|
72
|
+
byte_size = content.bytesize
|
|
73
|
+
line_count = content.lines.count
|
|
74
|
+
action = file_exists ? "overwrote" : "created"
|
|
75
|
+
|
|
76
|
+
"Successfully #{action} file: #{file_path} (#{line_count} lines, #{byte_size} bytes)"
|
|
77
|
+
rescue Errno::EACCES
|
|
78
|
+
error("Permission denied: Cannot write to file '#{file_path}'")
|
|
79
|
+
rescue Errno::EISDIR
|
|
80
|
+
error("Path is a directory, not a file.")
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
error("Unexpected error writing file: #{e.class.name} - #{e.message}")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/swarm_sdk/v3.rb
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "digest"
|
|
6
|
+
require "English"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "json"
|
|
9
|
+
require "open3"
|
|
10
|
+
require "pathname"
|
|
11
|
+
require "securerandom"
|
|
12
|
+
require "set"
|
|
13
|
+
require "time"
|
|
14
|
+
require "timeout"
|
|
15
|
+
|
|
16
|
+
require "async"
|
|
17
|
+
require "async/barrier"
|
|
18
|
+
require "async/semaphore"
|
|
19
|
+
require "mcp"
|
|
20
|
+
require "ruby_llm"
|
|
21
|
+
|
|
22
|
+
# Load ruby_llm compatibility patches (shared gem infrastructure)
|
|
23
|
+
require_relative "ruby_llm_patches/init"
|
|
24
|
+
|
|
25
|
+
require "zeitwerk"
|
|
26
|
+
|
|
27
|
+
module SwarmSDK
|
|
28
|
+
# V3 is the next-generation agent primitive with built-in memory.
|
|
29
|
+
#
|
|
30
|
+
# Self-contained module with its own Zeitwerk loader and zero dependencies
|
|
31
|
+
# on V2 code. The core insight: the LLM's context window is a staging area,
|
|
32
|
+
# not the whole brain. Older turns get consolidated into memory cards, and
|
|
33
|
+
# retrieval brings relevant memory back into working context on demand.
|
|
34
|
+
#
|
|
35
|
+
# @example Basic usage
|
|
36
|
+
# require "swarm_sdk/v3"
|
|
37
|
+
#
|
|
38
|
+
# definition = SwarmSDK::V3::AgentDefinition.new(
|
|
39
|
+
# name: :assistant,
|
|
40
|
+
# description: "A helpful assistant",
|
|
41
|
+
# model: "claude-sonnet-4",
|
|
42
|
+
# tools: [:Read, :Write, :Edit, :Bash, :Grep, :Glob],
|
|
43
|
+
# memory_directory: ".swarm/memory",
|
|
44
|
+
# )
|
|
45
|
+
#
|
|
46
|
+
# agent = SwarmSDK::V3::Agent.new(definition)
|
|
47
|
+
# response = agent.ask("Build a login page")
|
|
48
|
+
#
|
|
49
|
+
# @example Without memory (pure conversation)
|
|
50
|
+
# definition = SwarmSDK::V3::AgentDefinition.new(
|
|
51
|
+
# name: :chat,
|
|
52
|
+
# description: "Simple chat agent",
|
|
53
|
+
# model: "claude-sonnet-4",
|
|
54
|
+
# )
|
|
55
|
+
#
|
|
56
|
+
# agent = SwarmSDK::V3::Agent.new(definition)
|
|
57
|
+
# response = agent.ask("Hello!")
|
|
58
|
+
module V3
|
|
59
|
+
class Error < StandardError; end
|
|
60
|
+
class ConfigurationError < Error; end
|
|
61
|
+
class ToolExecutionError < Error; end
|
|
62
|
+
class MemoryError < Error; end
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
# Build a single V3 Agent from a DSL block
|
|
66
|
+
#
|
|
67
|
+
# Convenience entry point that delegates to {AgentBuilder.build}.
|
|
68
|
+
# Returns an initialized Agent ready for use with {Agent#ask}.
|
|
69
|
+
#
|
|
70
|
+
# @yield DSL block evaluated on an {AgentBuilder} instance
|
|
71
|
+
# @return [Agent] Initialized agent
|
|
72
|
+
# @raise [ConfigurationError] If required fields are missing
|
|
73
|
+
#
|
|
74
|
+
# @example
|
|
75
|
+
# agent = SwarmSDK::V3.agent do
|
|
76
|
+
# name :assistant
|
|
77
|
+
# description "A helpful assistant"
|
|
78
|
+
# tools :Read, :Write, :Edit
|
|
79
|
+
# streaming true
|
|
80
|
+
#
|
|
81
|
+
# memory do
|
|
82
|
+
# directory ".swarm/memory"
|
|
83
|
+
# end
|
|
84
|
+
# end
|
|
85
|
+
#
|
|
86
|
+
# agent.ask("Hello!")
|
|
87
|
+
def agent(&block)
|
|
88
|
+
AgentBuilder.build(&block)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Configure V3 global settings
|
|
92
|
+
#
|
|
93
|
+
# Convenience wrapper around {Configuration.configure}. Yields the
|
|
94
|
+
# configuration instance and applies provider settings to RubyLLM.
|
|
95
|
+
#
|
|
96
|
+
# @yield [Configuration] The configuration instance
|
|
97
|
+
# @return [Configuration]
|
|
98
|
+
#
|
|
99
|
+
# @example
|
|
100
|
+
# SwarmSDK::V3.configure do |config|
|
|
101
|
+
# config.default_model = "claude-sonnet-4"
|
|
102
|
+
# config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
|
|
103
|
+
# config.default_tools = [:Think, :Clock]
|
|
104
|
+
# end
|
|
105
|
+
def configure(&block)
|
|
106
|
+
Configuration.configure(&block)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Access the global configuration instance
|
|
110
|
+
#
|
|
111
|
+
# @return [Configuration]
|
|
112
|
+
#
|
|
113
|
+
# @example
|
|
114
|
+
# model = SwarmSDK::V3.configuration.default_model
|
|
115
|
+
def configuration
|
|
116
|
+
Configuration.instance
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Reset configuration to defaults
|
|
120
|
+
#
|
|
121
|
+
# @return [void]
|
|
122
|
+
def reset_configuration!
|
|
123
|
+
Configuration.reset!
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# V3 Zeitwerk setup:
|
|
130
|
+
# When loaded via V2 (require "swarm_sdk"), the V2 loader already manages
|
|
131
|
+
# lib/swarm_sdk/ including v3/, so we skip to avoid conflicts.
|
|
132
|
+
# When loaded standalone (require "swarm_sdk/v3"), creates its own loader.
|
|
133
|
+
v3_dir = File.join(__dir__, "v3")
|
|
134
|
+
already_managed = false
|
|
135
|
+
Zeitwerk::Registry.loaders.each do |loader|
|
|
136
|
+
loader.dirs.each { |dir| already_managed = true if v3_dir.start_with?(dir) }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
unless already_managed
|
|
140
|
+
v3_loader = Zeitwerk::Loader.new
|
|
141
|
+
v3_loader.tag = "swarm_sdk_v3"
|
|
142
|
+
v3_loader.push_dir(v3_dir, namespace: SwarmSDK::V3)
|
|
143
|
+
v3_loader.inflector.inflect("mcp" => "MCP")
|
|
144
|
+
v3_loader.setup
|
|
145
|
+
end
|