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 +4 -4
- data/Gemfile.lock +1 -12
- data/lib/language_operator/agent/base.rb +7 -4
- data/lib/language_operator/agent.rb +15 -7
- data/lib/language_operator/dsl/agent_definition.rb +0 -18
- data/lib/language_operator/templates/agent_synthesis.tmpl +62 -3
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/version.rb +1 -1
- data/synth/002/README.md +20 -147
- metadata +1 -16
- data/lib/language_operator/agent/scheduler.rb +0 -253
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc9f8e717ca6bd8f00386094494b88f2f2e3050239b9d476ba9519868d82df58
|
|
4
|
+
data.tar.gz: b15364fd16e4a9793263a8d861999d3c0a9188c2db3c6122176462a6b934c753
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
101
|
-
|
|
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:
|
|
172
|
-
|
|
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:
|
|
176
|
-
|
|
177
|
-
|
|
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:
|
|
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
|
|
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
|
|
|
@@ -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.
|
|
6
|
+
"version": "0.1.54",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"properties": {
|
|
9
9
|
"name": {
|
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
|
|
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
|
|
42
|
-
- ✅ **
|
|
43
|
-
- ✅ **
|
|
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
|
-
│
|
|
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.
|
|
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
|