agentf 0.4.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/agentf/agents/architect.rb +4 -0
- data/lib/agentf/agents/base.rb +29 -1
- data/lib/agentf/agents/debugger.rb +33 -10
- data/lib/agentf/agents/designer.rb +19 -8
- data/lib/agentf/agents/documenter.rb +6 -0
- data/lib/agentf/agents/explorer.rb +31 -12
- data/lib/agentf/agents/reviewer.rb +5 -0
- data/lib/agentf/agents/security.rb +26 -16
- data/lib/agentf/agents/specialist.rb +32 -18
- data/lib/agentf/agents/tester.rb +47 -8
- data/lib/agentf/cli/agent.rb +95 -0
- data/lib/agentf/cli/eval.rb +203 -0
- data/lib/agentf/cli/install.rb +7 -0
- data/lib/agentf/cli/memory.rb +82 -30
- data/lib/agentf/cli/router.rb +15 -3
- data/lib/agentf/cli/update.rb +9 -2
- data/lib/agentf/commands/memory_reviewer.rb +10 -2
- data/lib/agentf/commands/metrics.rb +16 -14
- data/lib/agentf/commands/registry.rb +28 -0
- data/lib/agentf/evals/report.rb +134 -0
- data/lib/agentf/evals/runner.rb +771 -0
- data/lib/agentf/evals/scenario.rb +211 -0
- data/lib/agentf/installer.rb +486 -348
- data/lib/agentf/mcp/server.rb +291 -49
- data/lib/agentf/memory.rb +97 -19
- data/lib/agentf/service/providers.rb +10 -62
- data/lib/agentf/version.rb +1 -1
- data/lib/agentf/workflow_engine.rb +204 -73
- data/lib/agentf.rb +9 -3
- metadata +8 -3
- data/lib/agentf/packs.rb +0 -74
data/lib/agentf/mcp/server.rb
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
begin
|
|
4
4
|
require "mcp"
|
|
5
5
|
rescue LoadError
|
|
6
|
-
require_relative "stub"
|
|
7
6
|
end
|
|
8
7
|
require "json"
|
|
8
|
+
require "set"
|
|
9
9
|
|
|
10
10
|
module Agentf
|
|
11
11
|
module MCP
|
|
@@ -20,6 +20,161 @@ module Agentf
|
|
|
20
20
|
# AGENTF_MCP_ALLOW_WRITES - true/false, controls memory write tools
|
|
21
21
|
# AGENTF_MCP_MAX_ARG_LENGTH - max length per string argument
|
|
22
22
|
class Server
|
|
23
|
+
ToolDefinition = Struct.new(:name, :description, :arguments, :handler, keyword_init: true)
|
|
24
|
+
|
|
25
|
+
class ToolBuilder
|
|
26
|
+
attr_reader :arguments, :handler
|
|
27
|
+
|
|
28
|
+
def initialize(name)
|
|
29
|
+
@name = name
|
|
30
|
+
@arguments = {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def description(value = nil)
|
|
34
|
+
@description = value unless value.nil?
|
|
35
|
+
@description
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def argument(name, type, required: false, description:, items: nil, **_opts)
|
|
39
|
+
@arguments[name] = {
|
|
40
|
+
type: type,
|
|
41
|
+
items: items,
|
|
42
|
+
required: required,
|
|
43
|
+
schema: {
|
|
44
|
+
type: schema_type(type),
|
|
45
|
+
description: description
|
|
46
|
+
}.tap do |schema|
|
|
47
|
+
schema[:items] = { type: schema_type(items) } if type == Array && items
|
|
48
|
+
end
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def call(&block)
|
|
53
|
+
@handler = block
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def schema_type(type)
|
|
59
|
+
case type&.name
|
|
60
|
+
when "String" then "string"
|
|
61
|
+
when "Integer" then "integer"
|
|
62
|
+
when "Float" then "number"
|
|
63
|
+
when "TrueClass", "FalseClass" then "boolean"
|
|
64
|
+
when "Array" then "array"
|
|
65
|
+
when "Hash" then "object"
|
|
66
|
+
else
|
|
67
|
+
"string"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class RegistryAdapter
|
|
73
|
+
def initialize(name:, version:)
|
|
74
|
+
@name = name
|
|
75
|
+
@version = version
|
|
76
|
+
@tools = {}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def tool(name, &block)
|
|
80
|
+
builder = ToolBuilder.new(name)
|
|
81
|
+
builder.instance_eval(&block)
|
|
82
|
+
@tools[name] = ToolDefinition.new(
|
|
83
|
+
name: name,
|
|
84
|
+
description: builder.description,
|
|
85
|
+
arguments: builder.arguments,
|
|
86
|
+
handler: builder.handler
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def list_tools
|
|
91
|
+
@tools.values.map do |tool|
|
|
92
|
+
{
|
|
93
|
+
name: tool.name,
|
|
94
|
+
description: tool.description.to_s,
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: tool.arguments.transform_values { |arg| arg[:schema] },
|
|
98
|
+
required: tool.arguments.select { |_k, arg| arg[:required] }.keys.map(&:to_s)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def call_tool(name, **args)
|
|
105
|
+
tool = @tools[name]
|
|
106
|
+
raise "Unknown tool: #{name}" unless tool
|
|
107
|
+
|
|
108
|
+
tool.handler.call(args)
|
|
109
|
+
rescue StandardError => e
|
|
110
|
+
e.message
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def run
|
|
114
|
+
runtime_server = build_runtime_server
|
|
115
|
+
transport = ::MCP::Server::Transports::StdioTransport.new(runtime_server)
|
|
116
|
+
runtime_server.transport = transport if runtime_server.respond_to?(:transport=)
|
|
117
|
+
transport.open
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def build_runtime_server
|
|
123
|
+
ensure_runtime_support!
|
|
124
|
+
|
|
125
|
+
runtime_server = ::MCP::Server.new(name: @name, version: @version)
|
|
126
|
+
adapter = self
|
|
127
|
+
@tools.each_value do |tool|
|
|
128
|
+
runtime_server.define_tool(
|
|
129
|
+
name: tool.name,
|
|
130
|
+
description: tool.description,
|
|
131
|
+
input_schema: runtime_input_schema(tool.arguments)
|
|
132
|
+
) do |**kwargs|
|
|
133
|
+
result = tool.handler.call(kwargs)
|
|
134
|
+
adapter.send(:build_response, result)
|
|
135
|
+
rescue StandardError => e
|
|
136
|
+
::MCP::Tool::Response.new([::MCP::Content::Text.new(e.message).to_h], error: true)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
runtime_server
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def ensure_runtime_support!
|
|
143
|
+
return if defined?(::MCP::Server::Transports::StdioTransport) && defined?(::MCP::Tool::Response) && defined?(::MCP::Content::Text)
|
|
144
|
+
|
|
145
|
+
raise "Installed MCP runtime does not support stdio server transport"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def runtime_input_schema(arguments)
|
|
149
|
+
schema = {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: arguments.transform_keys(&:to_s).transform_values { |arg| arg[:schema].transform_keys(&:to_s) }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
required = arguments.select { |_k, arg| arg[:required] }.keys.map(&:to_s)
|
|
155
|
+
schema[:required] = required unless required.empty?
|
|
156
|
+
schema
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def build_response(result)
|
|
160
|
+
return result if result.is_a?(::MCP::Tool::Response)
|
|
161
|
+
|
|
162
|
+
text = result.is_a?(String) ? result : JSON.generate(result)
|
|
163
|
+
structured = parse_structured_content(result)
|
|
164
|
+
::MCP::Tool::Response.new([::MCP::Content::Text.new(text).to_h], structured_content: structured)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_structured_content(result)
|
|
168
|
+
return result if result.is_a?(Hash)
|
|
169
|
+
return JSON.parse(result) if result.is_a?(String)
|
|
170
|
+
return result.as_json if result.respond_to?(:as_json)
|
|
171
|
+
|
|
172
|
+
nil
|
|
173
|
+
rescue JSON::ParserError
|
|
174
|
+
nil
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
23
178
|
KNOWN_TOOLS = %w[
|
|
24
179
|
agentf-code-glob
|
|
25
180
|
agentf-code-grep
|
|
@@ -66,6 +221,25 @@ module Agentf
|
|
|
66
221
|
@server = build_server
|
|
67
222
|
end
|
|
68
223
|
|
|
224
|
+
# Helper to centralize confirmation handling for MCP server write tools.
|
|
225
|
+
# Yields the block that performs the memory write and returns either the
|
|
226
|
+
# block result or the normalized confirmation hash produced by
|
|
227
|
+
# Agentf::Agents::Base#safe_memory_write.
|
|
228
|
+
def safe_mcp_memory_write(memory, attempted: {})
|
|
229
|
+
begin
|
|
230
|
+
yield
|
|
231
|
+
nil
|
|
232
|
+
rescue Agentf::Memory::RedisMemory::ConfirmationRequired => e
|
|
233
|
+
{
|
|
234
|
+
"confirmation_required" => true,
|
|
235
|
+
"confirmation_details" => e.details,
|
|
236
|
+
"attempted" => attempted,
|
|
237
|
+
"confirmed_write_token" => "confirmed",
|
|
238
|
+
"confirmation_prompt" => "Ask the user whether to save this memory. If they approve, call the same tool again after confirmation. If they decline, do not retry."
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
69
243
|
# Start the stdio read loop (blocks until stdin closes).
|
|
70
244
|
def run
|
|
71
245
|
@server.run
|
|
@@ -139,7 +313,7 @@ module Agentf
|
|
|
139
313
|
# ── Server construction ─────────────────────────────────────
|
|
140
314
|
|
|
141
315
|
def build_server
|
|
142
|
-
s =
|
|
316
|
+
s = RegistryAdapter.new(name: "agentf", version: Agentf::VERSION)
|
|
143
317
|
register_code_tools(s)
|
|
144
318
|
register_memory_tools(s)
|
|
145
319
|
register_architecture_tools(s)
|
|
@@ -349,14 +523,24 @@ module Agentf
|
|
|
349
523
|
argument :priority, Integer, required: false, description: "Priority"
|
|
350
524
|
call do |args|
|
|
351
525
|
mcp_server.send(:guard!, "agentf-memory-add-business-intent", **args)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
526
|
+
begin
|
|
527
|
+
id = nil
|
|
528
|
+
res = mcp_server.send(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-business-intent", args: args }) do
|
|
529
|
+
id = memory.store_business_intent(
|
|
530
|
+
title: args[:title],
|
|
531
|
+
description: args[:description],
|
|
532
|
+
tags: args[:tags] || [],
|
|
533
|
+
constraints: args[:constraints] || [],
|
|
534
|
+
priority: args[:priority] || 1
|
|
535
|
+
)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
if res.is_a?(Hash) && res["confirmation_required"]
|
|
539
|
+
JSON.generate(confirmation_required: true, confirmation_details: res["confirmation_details"], attempted: res["attempted"])
|
|
540
|
+
else
|
|
541
|
+
JSON.generate(id: id, type: "business_intent", status: "stored")
|
|
542
|
+
end
|
|
543
|
+
end
|
|
360
544
|
end
|
|
361
545
|
end
|
|
362
546
|
|
|
@@ -370,15 +554,25 @@ module Agentf
|
|
|
370
554
|
argument :related_task_id, String, required: false, description: "Related task id"
|
|
371
555
|
call do |args|
|
|
372
556
|
mcp_server.send(:guard!, "agentf-memory-add-feature-intent", **args)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
557
|
+
begin
|
|
558
|
+
id = nil
|
|
559
|
+
res = mcp_server.send(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-feature-intent", args: args }) do
|
|
560
|
+
id = memory.store_feature_intent(
|
|
561
|
+
title: args[:title],
|
|
562
|
+
description: args[:description],
|
|
563
|
+
tags: args[:tags] || [],
|
|
564
|
+
acceptance_criteria: args[:acceptance] || [],
|
|
565
|
+
non_goals: args[:non_goals] || [],
|
|
566
|
+
related_task_id: args[:related_task_id]
|
|
567
|
+
)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
if res.is_a?(Hash) && res["confirmation_required"]
|
|
571
|
+
JSON.generate(confirmation_required: true, confirmation_details: res["confirmation_details"], attempted: res["attempted"])
|
|
572
|
+
else
|
|
573
|
+
JSON.generate(id: id, type: "feature_intent", status: "stored")
|
|
574
|
+
end
|
|
575
|
+
end
|
|
382
576
|
end
|
|
383
577
|
end
|
|
384
578
|
|
|
@@ -427,16 +621,32 @@ module Agentf
|
|
|
427
621
|
argument :context, String, required: false, description: "Context"
|
|
428
622
|
call do |args|
|
|
429
623
|
mcp_server.send(:guard!, "agentf-memory-add-lesson", **args)
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
624
|
+
begin
|
|
625
|
+
id = nil
|
|
626
|
+
res = mcp_server.send(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-lesson", args: args }) do
|
|
627
|
+
id = memory.store_episode(
|
|
628
|
+
type: "lesson",
|
|
629
|
+
title: args[:title],
|
|
630
|
+
description: args[:description],
|
|
631
|
+
agent: args[:agent] || Agentf::AgentRoles::ENGINEER,
|
|
632
|
+
tags: args[:tags] || [],
|
|
633
|
+
context: args[:context].to_s,
|
|
634
|
+
code_snippet: ""
|
|
635
|
+
)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
if res.is_a?(Hash) && res["confirmation_required"]
|
|
639
|
+
JSON.generate(
|
|
640
|
+
confirmation_required: true,
|
|
641
|
+
confirmation_details: res["confirmation_details"],
|
|
642
|
+
attempted: res["attempted"],
|
|
643
|
+
confirmed_write_token: res["confirmed_write_token"],
|
|
644
|
+
confirmation_prompt: res["confirmation_prompt"]
|
|
645
|
+
)
|
|
646
|
+
else
|
|
647
|
+
JSON.generate(id: id, type: "lesson", status: "stored")
|
|
648
|
+
end
|
|
649
|
+
end
|
|
440
650
|
end
|
|
441
651
|
end
|
|
442
652
|
|
|
@@ -449,16 +659,32 @@ module Agentf
|
|
|
449
659
|
argument :context, String, required: false, description: "Context"
|
|
450
660
|
call do |args|
|
|
451
661
|
mcp_server.send(:guard!, "agentf-memory-add-success", **args)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
662
|
+
begin
|
|
663
|
+
id = nil
|
|
664
|
+
res = mcp_server.send(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-success", args: args }) do
|
|
665
|
+
id = memory.store_episode(
|
|
666
|
+
type: "success",
|
|
667
|
+
title: args[:title],
|
|
668
|
+
description: args[:description],
|
|
669
|
+
agent: args[:agent] || Agentf::AgentRoles::ENGINEER,
|
|
670
|
+
tags: args[:tags] || [],
|
|
671
|
+
context: args[:context].to_s,
|
|
672
|
+
code_snippet: ""
|
|
673
|
+
)
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
if res.is_a?(Hash) && res["confirmation_required"]
|
|
677
|
+
JSON.generate(
|
|
678
|
+
confirmation_required: true,
|
|
679
|
+
confirmation_details: res["confirmation_details"],
|
|
680
|
+
attempted: res["attempted"],
|
|
681
|
+
confirmed_write_token: res["confirmed_write_token"],
|
|
682
|
+
confirmation_prompt: res["confirmation_prompt"]
|
|
683
|
+
)
|
|
684
|
+
else
|
|
685
|
+
JSON.generate(id: id, type: "success", status: "stored")
|
|
686
|
+
end
|
|
687
|
+
end
|
|
462
688
|
end
|
|
463
689
|
end
|
|
464
690
|
|
|
@@ -471,16 +697,32 @@ module Agentf
|
|
|
471
697
|
argument :context, String, required: false, description: "Context"
|
|
472
698
|
call do |args|
|
|
473
699
|
mcp_server.send(:guard!, "agentf-memory-add-pitfall", **args)
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
700
|
+
begin
|
|
701
|
+
id = nil
|
|
702
|
+
res = mcp_server.send(:safe_mcp_memory_write, memory, attempted: { tool: "agentf-memory-add-pitfall", args: args }) do
|
|
703
|
+
id = memory.store_episode(
|
|
704
|
+
type: "pitfall",
|
|
705
|
+
title: args[:title],
|
|
706
|
+
description: args[:description],
|
|
707
|
+
agent: args[:agent] || Agentf::AgentRoles::ENGINEER,
|
|
708
|
+
tags: args[:tags] || [],
|
|
709
|
+
context: args[:context].to_s,
|
|
710
|
+
code_snippet: ""
|
|
711
|
+
)
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
if res.is_a?(Hash) && res["confirmation_required"]
|
|
715
|
+
JSON.generate(
|
|
716
|
+
confirmation_required: true,
|
|
717
|
+
confirmation_details: res["confirmation_details"],
|
|
718
|
+
attempted: res["attempted"],
|
|
719
|
+
confirmed_write_token: res["confirmed_write_token"],
|
|
720
|
+
confirmation_prompt: res["confirmation_prompt"]
|
|
721
|
+
)
|
|
722
|
+
else
|
|
723
|
+
JSON.generate(id: id, type: "pitfall", status: "stored")
|
|
724
|
+
end
|
|
725
|
+
end
|
|
484
726
|
end
|
|
485
727
|
end
|
|
486
728
|
end
|
data/lib/agentf/memory.rb
CHANGED
|
@@ -23,6 +23,16 @@ module Agentf
|
|
|
23
23
|
ensure_indexes if @search_supported
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Raised when a write requires explicit user confirmation (ask_first).
|
|
27
|
+
class ConfirmationRequired < StandardError
|
|
28
|
+
attr_reader :details
|
|
29
|
+
|
|
30
|
+
def initialize(message = "confirmation required to persist memory", details = {})
|
|
31
|
+
super(message)
|
|
32
|
+
@details = details
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
def store_task(content:, embedding: [], language: nil, task_type: nil, success: true, agent: Agentf::AgentRoles::PLANNER)
|
|
27
37
|
task_id = "task_#{SecureRandom.hex(4)}"
|
|
28
38
|
|
|
@@ -44,8 +54,34 @@ module Agentf
|
|
|
44
54
|
task_id
|
|
45
55
|
end
|
|
46
56
|
|
|
47
|
-
def store_episode(type:, title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::
|
|
48
|
-
related_task_id: nil, metadata: {}, entity_ids: [], relationships: [], parent_episode_id: nil, causal_from: nil)
|
|
57
|
+
def store_episode(type:, title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR,
|
|
58
|
+
related_task_id: nil, metadata: {}, entity_ids: [], relationships: [], parent_episode_id: nil, causal_from: nil, confirm: nil)
|
|
59
|
+
# Determine persistence preference from the agent's policy boundaries.
|
|
60
|
+
# Precedence: never > ask_first > always > none.
|
|
61
|
+
# For local/dev testing we may bypass interactive confirmation when
|
|
62
|
+
# AGENTF_AUTO_CONFIRM_MEMORIES=true. Otherwise, when an agent declares
|
|
63
|
+
# an "ask_first" persistence preference we raise ConfirmationRequired
|
|
64
|
+
# so higher-level code (MCP server / workflow engine / CLI) can prompt
|
|
65
|
+
# the user and retry the write with confirm: true.
|
|
66
|
+
auto_confirm = ENV['AGENTF_AUTO_CONFIRM_MEMORIES'] == 'true'
|
|
67
|
+
pref = persistence_preference_for(agent)
|
|
68
|
+
|
|
69
|
+
case pref
|
|
70
|
+
when :never
|
|
71
|
+
begin
|
|
72
|
+
puts "[MEMORY] Skipping persistence for #{agent}: policy forbids persisting memories"
|
|
73
|
+
rescue StandardError
|
|
74
|
+
end
|
|
75
|
+
return nil
|
|
76
|
+
when :ask_first
|
|
77
|
+
# If the agent's policy requires asking first, and we do not have
|
|
78
|
+
# an explicit confirmation (confirm: true) and auto_confirm is not
|
|
79
|
+
# enabled, raise ConfirmationRequired so callers can handle prompting.
|
|
80
|
+
unless auto_confirm || confirm == true
|
|
81
|
+
details = { "reason" => "ask_first", "agent" => agent.to_s, "attempted" => { "type" => type, "title" => title } }
|
|
82
|
+
raise ConfirmationRequired.new("confirm", details)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
49
85
|
episode_id = "episode_#{SecureRandom.hex(4)}"
|
|
50
86
|
normalized_metadata = enrich_metadata(
|
|
51
87
|
metadata: metadata,
|
|
@@ -107,52 +143,56 @@ module Agentf
|
|
|
107
143
|
episode_id
|
|
108
144
|
end
|
|
109
145
|
|
|
110
|
-
|
|
111
|
-
|
|
146
|
+
def store_success(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, confirm: nil)
|
|
147
|
+
store_episode(
|
|
112
148
|
type: "success",
|
|
113
149
|
title: title,
|
|
114
150
|
description: description,
|
|
115
151
|
context: context,
|
|
116
152
|
code_snippet: code_snippet,
|
|
117
153
|
tags: tags,
|
|
118
|
-
|
|
154
|
+
agent: agent,
|
|
155
|
+
confirm: confirm
|
|
119
156
|
)
|
|
120
157
|
end
|
|
121
158
|
|
|
122
|
-
|
|
123
|
-
|
|
159
|
+
def store_pitfall(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, confirm: nil)
|
|
160
|
+
store_episode(
|
|
124
161
|
type: "pitfall",
|
|
125
162
|
title: title,
|
|
126
163
|
description: description,
|
|
127
164
|
context: context,
|
|
128
165
|
code_snippet: code_snippet,
|
|
129
166
|
tags: tags,
|
|
130
|
-
|
|
167
|
+
agent: agent,
|
|
168
|
+
confirm: confirm
|
|
131
169
|
)
|
|
132
170
|
end
|
|
133
171
|
|
|
134
|
-
|
|
135
|
-
|
|
172
|
+
def store_lesson(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, confirm: nil)
|
|
173
|
+
store_episode(
|
|
136
174
|
type: "lesson",
|
|
137
175
|
title: title,
|
|
138
176
|
description: description,
|
|
139
177
|
context: context,
|
|
140
178
|
code_snippet: code_snippet,
|
|
141
179
|
tags: tags,
|
|
142
|
-
|
|
180
|
+
agent: agent,
|
|
181
|
+
confirm: confirm
|
|
143
182
|
)
|
|
144
183
|
end
|
|
145
184
|
|
|
146
|
-
def store_business_intent(title:, description:, constraints: [], tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, priority: 1)
|
|
185
|
+
def store_business_intent(title:, description:, constraints: [], tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, priority: 1, confirm: nil)
|
|
147
186
|
context = constraints.any? ? "Constraints: #{constraints.join('; ')}" : ""
|
|
148
187
|
|
|
149
|
-
|
|
188
|
+
store_episode(
|
|
150
189
|
type: "business_intent",
|
|
151
190
|
title: title,
|
|
152
191
|
description: description,
|
|
153
192
|
context: context,
|
|
154
193
|
tags: tags,
|
|
155
|
-
|
|
194
|
+
agent: agent,
|
|
195
|
+
confirm: confirm,
|
|
156
196
|
metadata: {
|
|
157
197
|
"intent_kind" => "business",
|
|
158
198
|
"constraints" => constraints,
|
|
@@ -162,18 +202,19 @@ module Agentf
|
|
|
162
202
|
end
|
|
163
203
|
|
|
164
204
|
def store_feature_intent(title:, description:, acceptance_criteria: [], non_goals: [], tags: [], agent: Agentf::AgentRoles::PLANNER,
|
|
165
|
-
|
|
205
|
+
related_task_id: nil, confirm: nil)
|
|
166
206
|
context_parts = []
|
|
167
207
|
context_parts << "Acceptance: #{acceptance_criteria.join('; ')}" if acceptance_criteria.any?
|
|
168
208
|
context_parts << "Non-goals: #{non_goals.join('; ')}" if non_goals.any?
|
|
169
209
|
|
|
170
|
-
|
|
210
|
+
store_episode(
|
|
171
211
|
type: "feature_intent",
|
|
172
212
|
title: title,
|
|
173
213
|
description: description,
|
|
174
214
|
context: context_parts.join(" | "),
|
|
175
215
|
tags: tags,
|
|
176
216
|
agent: agent,
|
|
217
|
+
confirm: confirm,
|
|
177
218
|
related_task_id: related_task_id,
|
|
178
219
|
metadata: {
|
|
179
220
|
"intent_kind" => "feature",
|
|
@@ -184,7 +225,7 @@ module Agentf
|
|
|
184
225
|
end
|
|
185
226
|
|
|
186
227
|
def store_incident(title:, description:, root_cause: "", resolution: "", tags: [], agent: Agentf::AgentRoles::INCIDENT_RESPONDER,
|
|
187
|
-
business_capability: nil)
|
|
228
|
+
business_capability: nil, confirm: nil)
|
|
188
229
|
store_episode(
|
|
189
230
|
type: "incident",
|
|
190
231
|
title: title,
|
|
@@ -192,6 +233,7 @@ module Agentf
|
|
|
192
233
|
context: ["Root cause: #{root_cause}", "Resolution: #{resolution}"].reject { |entry| entry.end_with?(": ") }.join(" | "),
|
|
193
234
|
tags: tags,
|
|
194
235
|
agent: agent,
|
|
236
|
+
confirm: confirm,
|
|
195
237
|
metadata: {
|
|
196
238
|
"root_cause" => root_cause,
|
|
197
239
|
"resolution" => resolution,
|
|
@@ -201,7 +243,7 @@ module Agentf
|
|
|
201
243
|
)
|
|
202
244
|
end
|
|
203
245
|
|
|
204
|
-
def store_playbook(title:, description:, steps: [], tags: [], agent: Agentf::AgentRoles::PLANNER, feature_area: nil)
|
|
246
|
+
def store_playbook(title:, description:, steps: [], tags: [], agent: Agentf::AgentRoles::PLANNER, feature_area: nil, confirm: nil)
|
|
205
247
|
store_episode(
|
|
206
248
|
type: "playbook",
|
|
207
249
|
title: title,
|
|
@@ -209,6 +251,7 @@ module Agentf
|
|
|
209
251
|
context: steps.any? ? "Steps: #{steps.join('; ')}" : "",
|
|
210
252
|
tags: tags,
|
|
211
253
|
agent: agent,
|
|
254
|
+
confirm: confirm,
|
|
212
255
|
metadata: {
|
|
213
256
|
"steps" => steps,
|
|
214
257
|
"feature_area" => feature_area,
|
|
@@ -580,7 +623,7 @@ module Agentf
|
|
|
580
623
|
cursor, batch = @client.scan(cursor, match: "episodic:*", count: 100)
|
|
581
624
|
batch.each do |key|
|
|
582
625
|
memory = load_episode(key)
|
|
583
|
-
memories << memory if memory
|
|
626
|
+
memories << memory if memory && memory["project"].to_s == @project.to_s
|
|
584
627
|
end
|
|
585
628
|
break if cursor == "0"
|
|
586
629
|
end
|
|
@@ -858,6 +901,41 @@ module Agentf
|
|
|
858
901
|
base
|
|
859
902
|
end
|
|
860
903
|
|
|
904
|
+
# NOTE: previous implementations exposed an `agent_requires_confirmation?`
|
|
905
|
+
# helper here. That functionality is superseded by
|
|
906
|
+
# `persistence_preference_for(agent)` which returns explicit preferences
|
|
907
|
+
# (:always, :ask_first, :never) and is used by callers to decide whether
|
|
908
|
+
# interactive confirmation is required. Keep this file lean and avoid
|
|
909
|
+
# duplicate helpers.
|
|
910
|
+
|
|
911
|
+
# Inspect loaded agent classes for explicit persistence preference.
|
|
912
|
+
# Returns one of: :always, :ask_first, :never, or nil when unknown.
|
|
913
|
+
def persistence_preference_for(agent)
|
|
914
|
+
begin
|
|
915
|
+
candidate = Agentf::Agents.constants
|
|
916
|
+
.map { |c| Agentf::Agents.const_get(c) }
|
|
917
|
+
.find do |klass|
|
|
918
|
+
klass.is_a?(Class) && klass.respond_to?(:policy_boundaries) && klass.typed_name == agent
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
return nil unless candidate
|
|
922
|
+
|
|
923
|
+
boundaries = candidate.policy_boundaries
|
|
924
|
+
persist_pattern = /persist|store|save/i
|
|
925
|
+
|
|
926
|
+
never_matches = Array(boundaries["never"]).select { |s| s =~ persist_pattern }
|
|
927
|
+
ask_matches = Array(boundaries["ask_first"]).select { |s| s =~ persist_pattern }
|
|
928
|
+
always_matches = Array(boundaries["always"]).select { |s| s =~ persist_pattern }
|
|
929
|
+
|
|
930
|
+
return :never if never_matches.any?
|
|
931
|
+
return :ask_first if ask_matches.any?
|
|
932
|
+
return :always if always_matches.any? && ask_matches.empty?
|
|
933
|
+
nil
|
|
934
|
+
rescue StandardError
|
|
935
|
+
nil
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
|
|
861
939
|
def infer_division(agent)
|
|
862
940
|
case agent
|
|
863
941
|
when Agentf::AgentRoles::PLANNER, Agentf::AgentRoles::ORCHESTRATOR, Agentf::AgentRoles::KNOWLEDGE_MANAGER
|