language-operator 0.1.52 → 0.1.54

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 11e9d5df0ee7675d0e0f2bccbc2fcb9eaee376e5ad9f7d11a8d8f0000c994bb2
4
- data.tar.gz: 0d8ac88a87ac9037934084d3e616ab1107e4e4fa26ae138809e7c64ca6924ca1
3
+ metadata.gz: cc9f8e717ca6bd8f00386094494b88f2f2e3050239b9d476ba9519868d82df58
4
+ data.tar.gz: b15364fd16e4a9793263a8d861999d3c0a9188c2db3c6122176462a6b934c753
5
5
  SHA512:
6
- metadata.gz: 452afc411ecdf258e31636750e63305b682a89dd07039071e5b43d68b2de2d37ecde1a5d2b362a2a96d28bc3f934dfd5fd90df54c483e0321ba46f5bff3aa037
7
- data.tar.gz: 0fd49b16dc6b2db57ee1be9a165bfbe9e5eecccb6051cde1ed8d417caf2ca0949160ad357f516603a9f8afdfc300e8432fd5e9c96b88bf3f4fcca8fd149d00b0
6
+ metadata.gz: 62729e36597f476b960c64b589a2e426ef5fe54f51e7db91d0ab91cdbc05a916fc000bc94c1e828e9839e2f6bff95a31f42f3c330006a008bc9c1e2a2fd63e03
7
+ data.tar.gz: a5472ea444b2d6bf58316c638b03b22365bd51e6b635a3eb3b2cb2d458a540e802def9e7d86243ec72bf958924b1c4441485ea8cf9121c7d96647a9bf046cec9
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- language-operator (0.1.52)
4
+ language-operator (0.1.54)
5
5
  faraday (~> 2.0)
6
6
  k8s-ruby (~> 0.17)
7
7
  mcp (~> 0.4)
@@ -17,7 +17,6 @@ PATH
17
17
  rouge (~> 4.0)
18
18
  ruby_llm (~> 1.8)
19
19
  ruby_llm-mcp (~> 0.1)
20
- rufus-scheduler (~> 3.9)
21
20
  thor (~> 1.3)
22
21
  tty-prompt (~> 0.23)
23
22
  tty-spinner (~> 0.9)
@@ -62,8 +61,6 @@ GEM
62
61
  dry-inflector (~> 1.0)
63
62
  dry-logic (~> 1.4)
64
63
  zeitwerk (~> 2.6)
65
- et-orbi (1.4.0)
66
- tzinfo
67
64
  event_stream_parser (1.0.0)
68
65
  excon (0.112.0)
69
66
  faraday (2.14.0)
@@ -76,9 +73,6 @@ GEM
76
73
  net-http (>= 0.5.0)
77
74
  faraday-retry (2.3.2)
78
75
  faraday (~> 2.0)
79
- fugit (1.12.1)
80
- et-orbi (~> 1.4)
81
- raabro (~> 1.4)
82
76
  google-protobuf (4.33.0)
83
77
  bigdecimal
84
78
  rake (>= 13)
@@ -164,7 +158,6 @@ GEM
164
158
  public_suffix (6.0.2)
165
159
  puma (6.6.1)
166
160
  nio4r (~> 2.0)
167
- raabro (1.4.0)
168
161
  racc (1.8.1)
169
162
  rack (3.2.4)
170
163
  rack-test (2.2.0)
@@ -226,8 +219,6 @@ GEM
226
219
  ruby_llm (~> 1.9)
227
220
  zeitwerk (~> 2)
228
221
  ruby_llm-schema (0.2.1)
229
- rufus-scheduler (3.9.2)
230
- fugit (~> 1.1, >= 1.11.1)
231
222
  strings (0.2.1)
232
223
  strings-ansi (~> 0.2)
233
224
  unicode-display_width (>= 1.5, < 3.0)
@@ -250,8 +241,6 @@ GEM
250
241
  pastel (~> 0.8)
251
242
  strings (~> 0.2.0)
252
243
  tty-screen (~> 0.8)
253
- tzinfo (2.0.6)
254
- concurrent-ruby (~> 1.0)
255
244
  unicode-display_width (2.6.0)
256
245
  unicode_utils (1.4.0)
257
246
  uri (1.1.1)
@@ -37,7 +37,6 @@ module LanguageOperator
37
37
  @workspace_path = ENV.fetch('WORKSPACE_PATH', '/workspace')
38
38
  @mode = ENV.fetch('AGENT_MODE', 'autonomous')
39
39
  @executor = nil
40
- @scheduler = nil
41
40
  end
42
41
 
43
42
  # Run the agent in its configured mode
@@ -93,12 +92,16 @@ module LanguageOperator
93
92
  @executor.run_loop
94
93
  end
95
94
 
96
- # Run in scheduled mode
95
+ # Run in scheduled mode (execute once - Kubernetes CronJob handles scheduling)
97
96
  #
98
97
  # @return [void]
99
98
  def run_scheduled
100
- @scheduler = Scheduler.new(self)
101
- @scheduler.start
99
+ logger.info('Agent running in scheduled mode without definition - executing goal once')
100
+
101
+ goal = ENV.fetch('AGENT_INSTRUCTIONS', 'Complete the assigned task')
102
+ execute_goal(goal)
103
+
104
+ logger.info('Scheduled execution completed - exiting')
102
105
  end
103
106
 
104
107
  # Run in reactive mode (HTTP server)
@@ -3,7 +3,6 @@
3
3
  require_relative 'agent/base'
4
4
  require_relative 'agent/executor'
5
5
  require_relative 'agent/task_executor'
6
- require_relative 'agent/scheduler'
7
6
  require_relative 'agent/web_server'
8
7
  require_relative 'dsl'
9
8
  require_relative 'logger'
@@ -147,6 +146,7 @@ module LanguageOperator
147
146
  # @param agent [LanguageOperator::Agent::Base] The agent instance
148
147
  # @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
149
148
  # @return [void]
149
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
150
150
  def self.run_with_definition(agent, agent_def)
151
151
  agent.connect!
152
152
 
@@ -167,17 +167,24 @@ module LanguageOperator
167
167
  raise 'Agent definition must have either main block (DSL v1) or workflow (DSL v0)'
168
168
  end
169
169
  when 'scheduled', 'event-driven'
170
+ # Scheduled mode: Execute once and exit (Kubernetes CronJob handles scheduling)
171
+ logger.info('Agent running in scheduled mode - executing once',
172
+ agent_name: agent_def.name,
173
+ dsl_version: uses_dsl_v1 ? 'v1' : 'v0')
174
+
170
175
  if uses_dsl_v1
171
- # DSL v1: Schedule main block execution
172
- scheduler = LanguageOperator::Agent::Scheduler.new(agent)
173
- scheduler.start_with_main(agent_def)
176
+ # DSL v1: Execute main block once
177
+ execute_main_block(agent, agent_def)
174
178
  elsif uses_dsl_v0
175
- # DSL v0: Schedule workflow execution
176
- scheduler = LanguageOperator::Agent::Scheduler.new(agent)
177
- scheduler.start_with_workflow(agent_def)
179
+ # DSL v0: Execute workflow once
180
+ executor = LanguageOperator::Agent::Executor.new(agent)
181
+ executor.execute_workflow(agent_def)
178
182
  else
179
183
  raise 'Agent definition must have either main block (DSL v1) or workflow (DSL v0)'
180
184
  end
185
+
186
+ logger.info('Scheduled execution completed - exiting',
187
+ agent_name: agent_def.name)
181
188
  when 'reactive', 'http', 'webhook'
182
189
  # Start web server with webhooks, MCP tools, and chat endpoint
183
190
  web_server = LanguageOperator::Agent::WebServer.new(agent)
@@ -189,6 +196,7 @@ module LanguageOperator
189
196
  raise "Unknown agent mode: #{agent.mode}"
190
197
  end
191
198
  end
199
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/PerceivedComplexity
192
200
 
193
201
  # Execute main block (DSL v1) in autonomous mode
194
202
  #
@@ -357,24 +357,6 @@ module LanguageOperator
357
357
  "Agent:#{@name}"
358
358
  end
359
359
 
360
- def run_scheduled
361
- require 'rufus-scheduler'
362
-
363
- scheduler = Rufus::Scheduler.new
364
-
365
- logger.info('Scheduling agent',
366
- name: @name,
367
- cron: @schedule)
368
-
369
- scheduler.cron(@schedule) do
370
- logger.timed('Scheduled execution') do
371
- execute_objectives
372
- end
373
- end
374
-
375
- scheduler.join
376
- end
377
-
378
360
  def run_autonomous
379
361
  logger.info('Running agent in autonomous mode', name: @name)
380
362
  execute_objectives
@@ -77,8 +77,8 @@ This is attempt {{.AttemptNumber}} of {{.MaxAttempts}}. The user is counting on
77
77
 
78
78
  **Runtime Context:**
79
79
  - All agent messages and output are automatically logged to stdout
80
- - Agents have access to a workspace directory for file operations
81
80
  - LLM responses are captured and available in agent execution context
81
+ - File operations should use neural tasks that delegate to the workspace tool (see examples below)
82
82
 
83
83
  ## DSL v1 Reference Examples
84
84
 
@@ -174,16 +174,72 @@ agent "data-pipeline" do
174
174
 
175
175
  main do |inputs|
176
176
  extracted = execute_task(:extract_data, inputs: inputs)
177
- transformed = execute_task(:transform_data, inputs: extracted)
177
+ transformed = execute_task(:transform_data, inputs: transformed)
178
178
  result = execute_task(:load_data, inputs: transformed)
179
179
  result
180
180
  end
181
181
  end
182
182
  ```
183
183
 
184
+ ### Example 4: Stateful Agent with Workspace File Operations
185
+ ```ruby
186
+ require 'language_operator'
187
+
188
+ agent "story-builder" do
189
+ description "Build a story one sentence at a time"
190
+ mode :scheduled
191
+ schedule "0 * * * *"
192
+
193
+ # Neural task - LLM reads file using workspace tool
194
+ task :read_existing_story,
195
+ instructions: "Read the story.txt file from workspace. If it doesn't exist, return empty string. Return the content and count of sentences.",
196
+ inputs: {},
197
+ outputs: { content: 'string', sentence_count: 'integer' }
198
+
199
+ # Neural task - LLM generates creative continuation
200
+ task :generate_next_sentence,
201
+ instructions: "Generate exactly one new sentence to continue this story. Maintain consistent tone and style. Only output the new sentence.",
202
+ inputs: { existing_content: 'string' },
203
+ outputs: { sentence: 'string' }
204
+
205
+ # Neural task - LLM writes file using workspace tool
206
+ task :append_to_story,
207
+ instructions: "Append the new sentence to story.txt in workspace. If the file has existing content, add a newline first.",
208
+ inputs: { sentence: 'string' },
209
+ outputs: { success: 'boolean', total_sentences: 'integer' }
210
+
211
+ main do |inputs|
212
+ # Read current state from workspace
213
+ story_data = execute_task(:read_existing_story)
214
+
215
+ # Generate new content based on what exists
216
+ new_sentence = execute_task(:generate_next_sentence,
217
+ inputs: { existing_content: story_data[:content] })
218
+
219
+ # Persist to workspace for next run
220
+ result = execute_task(:append_to_story,
221
+ inputs: { sentence: new_sentence[:sentence] })
222
+
223
+ { sentence: new_sentence[:sentence], total: result[:total_sentences] }
224
+ end
225
+
226
+ output do |outputs|
227
+ puts "Added sentence: #{outputs[:sentence]}"
228
+ puts "Story now has #{outputs[:total]} sentences"
229
+ end
230
+ end
231
+ ```
232
+
233
+ **File Operations Best Practices:**
234
+ - **NEVER** use direct Ruby file operations (`File.read`, `File.write`, `File.open`, `Dir.pwd`, etc.) in agent code
235
+ - **ALWAYS** delegate file operations to neural tasks with clear natural language instructions
236
+ - File operations require the LLM to reason about paths, content, and state - use the workspace tool via neural tasks
237
+ - The workspace tool provides: `read_file`, `write_file`, `list_directory`, `create_directory`, `get_file_info`, `search_files`
238
+ - Example pattern: `task :read_data, instructions: "read data.json from workspace and parse it", inputs: {}, outputs: { data: 'hash' }`
239
+
184
240
  ## Your Task: Generate DSL v1 Agent
185
241
 
186
- Using the THREE CONCRETE EXAMPLES above (daily-report, code-reviewer, data-pipeline) as reference patterns, generate WORKING Ruby DSL code for the agent described in the user instructions.
242
+ Using the FOUR CONCRETE EXAMPLES above (daily-report, code-reviewer, data-pipeline, story-builder) as reference patterns, generate WORKING Ruby DSL code for the agent described in the user instructions.
187
243
 
188
244
  **CRITICAL REQUIREMENTS:**
189
245
  - DO NOT output placeholder text like "Brief description extracted from instructions" or "CRON_EXPRESSION"
@@ -250,6 +306,9 @@ end
250
306
  - ✓ Are all task names, descriptions, and logic SPECIFIC to the user's request?
251
307
  - ✓ Did you AVOID outputting placeholders like "task_name" or "CRON_EXPRESSION"?
252
308
  - ✓ Does the code actually DO what the user asked for?
309
+ - ✓ Did you use NEURAL TASKS (not symbolic code blocks) for all file operations?
310
+ - ✓ Did you NEVER use File.read, File.write, Dir.pwd, or other direct file APIs?
311
+ - ✓ For workspace operations, did you write clear instructions for the LLM to use the workspace tool?
253
312
 
254
313
  If you cannot answer YES to all of the above, re-read the user instructions and generate FUNCTIONAL code.
255
314
 
@@ -2,7 +2,7 @@
2
2
  :openapi: 3.0.3
3
3
  :info:
4
4
  :title: Language Operator Agent API
5
- :version: 0.1.52
5
+ :version: 0.1.54
6
6
  :description: HTTP API endpoints exposed by Language Operator reactive agents
7
7
  :contact:
8
8
  :name: Language Operator
@@ -3,7 +3,7 @@
3
3
  "$id": "https://github.com/language-operator/language-operator-gem/schema/agent-dsl.json",
4
4
  "title": "Language Operator Agent DSL",
5
5
  "description": "Schema for defining autonomous AI agents using the Language Operator DSL",
6
- "version": "0.1.52",
6
+ "version": "0.1.54",
7
7
  "type": "object",
8
8
  "properties": {
9
9
  "name": {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LanguageOperator
4
- VERSION = '0.1.52'
4
+ VERSION = '0.1.54'
5
5
  end
data/synth/002/README.md CHANGED
@@ -36,16 +36,24 @@ mode :scheduled
36
36
  schedule "*/10 * * * *" # Every 10 minutes
37
37
  ```
38
38
 
39
- Validates that agents can run on a schedule using cron syntax:
39
+ Validates that agents can run on a schedule using Kubernetes CronJobs:
40
40
  - ✅ **Mode dispatch** - Runtime recognizes `:scheduled` mode
41
- - ✅ **Cron parsing** - Schedule expression is parsed correctly
42
- - ✅ **Scheduler integration** - `rufus-scheduler` integration works
43
- - ✅ **Repeated execution** - Agent runs multiple times automatically
41
+ - ✅ **Cron parsing** - Schedule expression is used by Kubernetes CronJob
42
+ - ✅ **Kubernetes-native** - CronJob creates pods on schedule
43
+ - ✅ **Execute once and exit** - Each pod runs the task once, then terminates
44
+ - ✅ **Repeated execution** - Kubernetes creates new pods per schedule
44
45
 
45
46
  ### 3. Complete Neural Execution Flow
46
47
  ```
47
48
  ┌─────────────────────────────────────────────────────────┐
48
- Scheduler Triggers (every 10 minutes)
49
+ Kubernetes CronJob Triggers (every 10 minutes)
50
+ │ Creates new pod for this execution │
51
+ └────────────────────┬────────────────────────────────────┘
52
+
53
+
54
+ ┌─────────────────────────────────────────────────────────┐
55
+ │ Pod Starts → Agent Runtime Loads │
56
+ │ Mode: scheduled → Execute once and exit │
49
57
  └────────────────────┬────────────────────────────────────┘
50
58
 
51
59
 
@@ -75,6 +83,12 @@ Validates that agents can run on a schedule using cron syntax:
75
83
  ┌─────────────────────────────────────────────────────────┐
76
84
  │ Output Block Processes Result │
77
85
  │ puts outputs[:fortune] │
86
+ └────────────────────┬────────────────────────────────────┘
87
+
88
+
89
+ ┌─────────────────────────────────────────────────────────┐
90
+ │ Agent Exits → Pod Terminates │
91
+ │ Kubernetes waits for next cron schedule │
78
92
  └─────────────────────────────────────────────────────────┘
79
93
  ```
80
94
 
@@ -133,100 +147,6 @@ This test proves the foundation for learning:
133
147
 
134
148
  **The critical insight**: Because `execute_task(:generate_fortune)` works the same whether the task is neural or symbolic, we can replace implementations without breaking the `main` block.
135
149
 
136
- ### No Other Framework Can Do This
137
-
138
- | Framework | Neural Execution | Symbolic Execution | Transparent Evolution |
139
- |-----------|-----------------|-------------------|---------------------|
140
- | **Language Operator** | ✅ Instructions-based tasks | ✅ Code blocks | ✅ Contract abstraction |
141
- | LangChain | ❌ Chains are static | ✅ Python code | ❌ No abstraction |
142
- | AutoGen | ✅ Conversational | ❌ No symbolic optimization | ❌ No contracts |
143
- | CrewAI | ✅ Agents with prompts | ❌ No learning | ❌ No abstraction |
144
-
145
- ## What It Doesn't Test
146
-
147
- This test intentionally **does not** validate:
148
- - ❌ Learning/re-synthesis (future tests)
149
- - ❌ MCP tool integration in neural tasks (future tests)
150
- - ❌ Complex multi-task workflows (future tests)
151
- - ❌ Error recovery and re-synthesis (future tests)
152
- - ❌ Hybrid neural-symbolic agents (future tests)
153
-
154
- ## Success Criteria
155
-
156
- ✅ **Agent synthesizes with neural task** - Instructions-based task definition works
157
- ✅ **Scheduled mode activates** - Agent runs on cron schedule
158
- ✅ **Neural task executes** - LLM is invoked with instructions
159
- ✅ **Output schema validated** - LLM response matches `{ fortune: 'string' }`
160
- ✅ **Output appears** - Fortune is logged to stdout
161
- ✅ **Repeated execution** - Agent runs multiple times (every 10 minutes)
162
-
163
- ## Connection to DSL v1 Proposal
164
-
165
- From [dsl-v1.md](../requirements/proposals/dsl-v1.md):
166
-
167
- > **Critical Property:** The caller cannot tell which implementation is used. The contract is the interface.
168
-
169
- This test proves that property works in practice:
170
-
171
- **Contract (Stable):**
172
- ```ruby
173
- task :generate_fortune,
174
- inputs: {},
175
- outputs: { fortune: 'string' }
176
- ```
177
-
178
- **Implementation (Neural - for now):**
179
- ```ruby
180
- instructions: "Generate a random fortune for the user"
181
- ```
182
-
183
- **Caller (Unaware):**
184
- ```ruby
185
- main do |inputs|
186
- fortune_data = execute_task(:generate_fortune) # Works regardless of implementation
187
- { fortune: fortune_data[:fortune] }
188
- end
189
- ```
190
-
191
- The `main` block doesn't know (and doesn't care) whether `:generate_fortune` is neural or symbolic. This is the **organic function abstraction** that enables real-time synthesis and learning.
192
-
193
- ## Running the Test
194
-
195
- ```bash
196
- # Execute the synthesized agent
197
- ruby synth/002/agent.rb
198
-
199
- # Expected behavior:
200
- # - Agent starts in scheduled mode
201
- # - Every 10 minutes, generates and prints a fortune
202
- # - Runs continuously until stopped
203
- ```
204
-
205
- ## What Success Looks Like
206
-
207
- ```
208
- [INFO] Loading agent: test-agent
209
- [INFO] Mode: scheduled (*/10 * * * *)
210
- [INFO] Scheduler started
211
- [INFO] Executing main block (scheduled trigger)
212
- [INFO] Executing task: generate_fortune (neural)
213
- [INFO] Calling LLM with instructions: "Generate a random fortune for the user"
214
- [INFO] LLM returned: {:fortune=>"A journey of a thousand miles begins with a single step."}
215
- [INFO] Validating output schema: { fortune: 'string' } ✓
216
- [INFO] Task returned: {:fortune=>"A journey of a thousand miles begins with a single step."}
217
- [INFO] Processing output
218
- A journey of a thousand miles begins with a single step.
219
- [INFO] Agent execution complete, waiting for next schedule
220
- [INFO] Next run: 2025-11-16 14:20:00
221
- ...
222
- [INFO] Executing main block (scheduled trigger)
223
- [INFO] Executing task: generate_fortune (neural)
224
- [INFO] Calling LLM with instructions: "Generate a random fortune for the user"
225
- [INFO] LLM returned: {:fortune=>"Fortune favors the bold."}
226
- [INFO] Validating output schema: { fortune: 'string' } ✓
227
- Fortune favors the bold.
228
- ```
229
-
230
150
  ## The Organic Function In Action
231
151
 
232
152
  **What makes this revolutionary:**
@@ -237,51 +157,4 @@ Fortune favors the bold.
237
157
  4. **Learning Ready**: After N runs, system can observe patterns and synthesize symbolic implementation
238
158
  5. **Zero Breaking Changes**: When re-synthesized, `main` block never changes
239
159
 
240
- **This is what "living code" means**: Code that starts neural (flexible, works immediately) and becomes symbolic (fast, cheap) through observation, all while maintaining a stable contract.
241
-
242
- ---
243
-
244
- **Status**: ✅ VALIDATED - Neural organic functions work
245
-
246
- **Next**: Test 003+ will validate learning, re-synthesis, and progressive neural→symbolic evolution
247
-
248
- ---
249
-
250
- ## Technical Deep Dive
251
-
252
- ### How Neural Execution Works
253
-
254
- When `execute_task(:generate_fortune)` is called:
255
-
256
- 1. **Task Lookup**: Runtime finds task definition in agent
257
- 2. **Type Check**: Task has `instructions`, no code block → Neural execution
258
- 3. **Prompt Construction**:
259
- ```
260
- You are an AI agent executing a task.
261
-
262
- Task: generate_fortune
263
- Instructions: Generate a random fortune for the user
264
-
265
- Inputs: {}
266
-
267
- You must return a response matching this schema:
268
- { fortune: 'string' }
269
-
270
- [Available tools if any MCP servers connected]
271
- ```
272
- 4. **LLM Invocation**: Send prompt to configured LLM (via `ruby_llm`)
273
- 5. **Response Parsing**: Extract structured output from LLM response
274
- 6. **Schema Validation**: Ensure response matches `{ fortune: 'string' }`
275
- 7. **Return**: Validated output returned to caller
276
-
277
- ### What This Enables Later
278
-
279
- Once this works, the learning system can:
280
-
281
- 1. **Observe Execution**: Collect OpenTelemetry traces showing what the LLM did
282
- 2. **Detect Patterns**: Analyze if LLM behavior is deterministic
283
- 3. **Synthesize Code**: Generate symbolic implementation from observed pattern
284
- 4. **Re-Deploy**: Update ConfigMap with learned code
285
- 5. **Transparent Evolution**: `main` block continues working identically
286
-
287
- **This test proves step 1 works** (neural execution). Future tests prove steps 2-5.
160
+ **This is what "living code" means**: Code that starts neural (flexible, works immediately) and becomes symbolic (fast, cheap) through observation, all while maintaining a stable contract.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: language-operator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.52
4
+ version: 0.1.54
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Ryan
@@ -121,20 +121,6 @@ dependencies:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
123
  version: '1.26'
124
- - !ruby/object:Gem::Dependency
125
- name: rufus-scheduler
126
- requirement: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - "~>"
129
- - !ruby/object:Gem::Version
130
- version: '3.9'
131
- type: :runtime
132
- prerelease: false
133
- version_requirements: !ruby/object:Gem::Requirement
134
- requirements:
135
- - - "~>"
136
- - !ruby/object:Gem::Version
137
- version: '3.9'
138
124
  - !ruby/object:Gem::Dependency
139
125
  name: faraday
140
126
  requirement: !ruby/object:Gem::Requirement
@@ -468,7 +454,6 @@ files:
468
454
  - lib/language_operator/agent/safety/manager.rb
469
455
  - lib/language_operator/agent/safety/rate_limiter.rb
470
456
  - lib/language_operator/agent/safety/safe_executor.rb
471
- - lib/language_operator/agent/scheduler.rb
472
457
  - lib/language_operator/agent/task_executor.rb
473
458
  - lib/language_operator/agent/telemetry.rb
474
459
  - lib/language_operator/agent/web_server.rb
@@ -1,253 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rufus-scheduler'
4
- require_relative 'executor'
5
- require_relative 'instrumentation'
6
- require_relative '../logger'
7
- require_relative '../loggable'
8
-
9
- module LanguageOperator
10
- module Agent
11
- # Task Scheduler
12
- #
13
- # Handles scheduled and event-driven task execution using rufus-scheduler.
14
- #
15
- # @example
16
- # scheduler = Scheduler.new(agent)
17
- # scheduler.start
18
- class Scheduler
19
- include LanguageOperator::Loggable
20
- include LanguageOperator::Agent::Instrumentation
21
-
22
- attr_reader :agent, :rufus_scheduler
23
-
24
- # Initialize the scheduler
25
- #
26
- # @param agent [LanguageOperator::Agent::Base] The agent instance
27
- def initialize(agent)
28
- @agent = agent
29
- @rufus_scheduler = Rufus::Scheduler.new
30
- @executor = Executor.new(agent)
31
-
32
- logger.debug('Scheduler initialized',
33
- workspace: @agent.workspace_path,
34
- servers: @agent.servers_info.length)
35
- end
36
-
37
- # Start the scheduler
38
- #
39
- # @return [void]
40
- def start
41
- logger.info('Agent starting in scheduled mode')
42
- logger.info("Workspace: #{@agent.workspace_path}")
43
- logger.info("Connected to #{@agent.servers_info.length} MCP server(s)")
44
-
45
- setup_schedules
46
- logger.info('Scheduler started, waiting for scheduled tasks')
47
- @rufus_scheduler.join
48
- end
49
-
50
- # Start the scheduler with a workflow definition
51
- #
52
- # @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition with workflow
53
- # @return [void]
54
- def start_with_workflow(agent_def)
55
- logger.info('Agent starting in scheduled mode with workflow',
56
- agent_name: agent_def.name,
57
- has_workflow: !agent_def.workflow.nil?)
58
- logger.info("Workspace: #{@agent.workspace_path}")
59
- logger.info("Connected to #{@agent.servers_info.length} MCP server(s)")
60
-
61
- # Extract schedule from agent definition and validate
62
- cron_schedule = agent_def.schedule
63
- if cron_schedule.nil? || cron_schedule.empty?
64
- raise ArgumentError,
65
- "Schedule required for scheduled mode agent '#{agent_def.name}'. " \
66
- 'Use schedule() method to provide a cron expression.'
67
- end
68
-
69
- logger.info('Scheduling workflow', cron: cron_schedule, agent: agent_def.name)
70
-
71
- @rufus_scheduler.cron(cron_schedule) do
72
- with_span('agent.scheduler.execute', attributes: {
73
- 'scheduler.cron_expression' => cron_schedule,
74
- 'agent.name' => agent_def.name,
75
- 'scheduler.task_type' => 'workflow'
76
- }) do
77
- logger.timed('Scheduled workflow execution') do
78
- logger.info('Executing scheduled workflow', agent: agent_def.name)
79
- result = @executor.execute_workflow(agent_def)
80
- result_text = result.is_a?(String) ? result : result.content
81
- preview = result_text[0..200]
82
- preview += '...' if result_text.length > 200
83
- logger.info('Workflow completed', result_preview: preview)
84
- end
85
- end
86
- end
87
-
88
- logger.info('Scheduler started, waiting for scheduled tasks')
89
- @rufus_scheduler.join
90
- end
91
-
92
- # Start the scheduler with a main block (DSL v1)
93
- #
94
- # @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition with main block
95
- # @return [void]
96
- def start_with_main(agent_def)
97
- logger.info('Agent starting in scheduled mode with main block',
98
- agent_name: agent_def.name,
99
- task_count: agent_def.tasks.size)
100
- logger.info("Workspace: #{@agent.workspace_path}")
101
- logger.info("Connected to #{@agent.servers_info.length} MCP server(s)")
102
-
103
- # Extract schedule from agent definition and validate
104
- cron_schedule = agent_def.schedule
105
- if cron_schedule.nil? || cron_schedule.empty?
106
- raise ArgumentError,
107
- "Schedule required for scheduled mode agent '#{agent_def.name}'. " \
108
- 'Use schedule() method to provide a cron expression.'
109
- end
110
-
111
- logger.info('Scheduling main block execution', cron: cron_schedule, agent: agent_def.name)
112
-
113
- # Create task executor with constraints config
114
- require_relative 'task_executor'
115
- config = build_executor_config(agent_def)
116
- task_executor = TaskExecutor.new(@agent, agent_def.tasks, config)
117
-
118
- @rufus_scheduler.cron(cron_schedule) do
119
- with_span('agent.scheduler.execute', attributes: {
120
- 'scheduler.cron_expression' => cron_schedule,
121
- 'agent.name' => agent_def.name,
122
- 'scheduler.task_type' => 'main_block'
123
- }) do
124
- logger.timed('Scheduled main block execution') do
125
- logger.info('Executing scheduled main block', agent: agent_def.name)
126
-
127
- # Get inputs from environment or default to empty hash
128
- inputs = {}
129
-
130
- # Execute main block
131
- result = agent_def.main.call(inputs, task_executor)
132
-
133
- logger.info('Main block completed', result: result)
134
- end
135
- end
136
- end
137
-
138
- logger.info('Scheduler started, waiting for scheduled tasks')
139
- @rufus_scheduler.join
140
- end
141
-
142
- # Stop the scheduler
143
- #
144
- # @return [void]
145
- def stop
146
- logger.info('Shutting down scheduler')
147
- @rufus_scheduler.shutdown
148
- logger.info('Scheduler stopped')
149
- end
150
-
151
- private
152
-
153
- def logger_component
154
- 'Agent::Scheduler'
155
- end
156
-
157
- # Setup schedules from config
158
- #
159
- # @return [void]
160
- def setup_schedules
161
- schedules = @agent.config.dig('agent', 'schedules') || []
162
-
163
- logger.debug('Loading schedules from config', count: schedules.length)
164
-
165
- if schedules.empty?
166
- logger.warn('No schedules configured, using default daily schedule')
167
- setup_default_schedule
168
- return
169
- end
170
-
171
- schedules.each do |schedule|
172
- add_schedule(schedule)
173
- end
174
-
175
- logger.info("#{schedules.length} schedule(s) configured")
176
- end
177
-
178
- # Add a single schedule
179
- #
180
- # @param schedule [Hash] Schedule configuration
181
- # @return [void]
182
- def add_schedule(schedule)
183
- cron = schedule['cron']
184
- task = schedule['task']
185
- agent_name = @agent.config.dig('agent', 'name')
186
-
187
- logger.info('Scheduling task', cron: cron, task: task[0..100])
188
-
189
- @rufus_scheduler.cron(cron) do
190
- with_span('agent.scheduler.execute', attributes: {
191
- 'scheduler.cron_expression' => cron,
192
- 'agent.name' => agent_name,
193
- 'scheduler.task_type' => 'scheduled'
194
- }) do
195
- logger.timed('Scheduled task execution') do
196
- logger.info('Executing scheduled task', task: task[0..100])
197
- result = @executor.execute(task)
198
- preview = result[0..200]
199
- preview += '...' if result.length > 200
200
- logger.info('Task completed', result_preview: preview)
201
- end
202
- end
203
- end
204
- end
205
-
206
- # Setup default daily schedule
207
- #
208
- # @return [void]
209
- def setup_default_schedule
210
- instructions = @agent.config.dig('agent', 'instructions') ||
211
- 'Check for updates and report status'
212
- agent_name = @agent.config.dig('agent', 'name')
213
- cron = '0 6 * * *'
214
-
215
- logger.info('Setting up default schedule', cron: cron,
216
- instructions: instructions[0..100])
217
-
218
- @rufus_scheduler.cron(cron) do
219
- with_span('agent.scheduler.execute', attributes: {
220
- 'scheduler.cron_expression' => cron,
221
- 'agent.name' => agent_name,
222
- 'scheduler.task_type' => 'default'
223
- }) do
224
- logger.timed('Daily task execution') do
225
- logger.info('Executing daily task')
226
- result = @executor.execute(instructions)
227
- preview = result[0..200]
228
- preview += '...' if result.length > 200
229
- logger.info('Daily task completed', result_preview: preview)
230
- end
231
- end
232
- end
233
-
234
- logger.info('Scheduled: Daily at 6:00 AM')
235
- end
236
-
237
- # Build executor configuration from agent definition constraints
238
- #
239
- # @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
240
- # @return [Hash] Executor configuration
241
- def build_executor_config(agent_def)
242
- config = {}
243
-
244
- if agent_def.constraints
245
- config[:timeout] = agent_def.constraints[:timeout] if agent_def.constraints[:timeout]
246
- config[:max_retries] = agent_def.constraints[:max_retries] if agent_def.constraints[:max_retries]
247
- end
248
-
249
- config
250
- end
251
- end
252
- end
253
- end