agentf 0.4.7 → 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 +31 -8
- data/lib/agentf/agents/designer.rb +18 -7
- data/lib/agentf/agents/documenter.rb +6 -0
- data/lib/agentf/agents/explorer.rb +30 -11
- data/lib/agentf/agents/reviewer.rb +5 -0
- data/lib/agentf/agents/security.rb +24 -14
- data/lib/agentf/agents/specialist.rb +31 -17
- data/lib/agentf/agents/tester.rb +46 -7
- 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 +46 -53
- 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,10 +54,15 @@ 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::
|
|
57
|
+
def store_episode(type:, title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR,
|
|
48
58
|
related_task_id: nil, metadata: {}, entity_ids: [], relationships: [], parent_episode_id: nil, causal_from: nil, confirm: nil)
|
|
49
59
|
# Determine persistence preference from the agent's policy boundaries.
|
|
50
|
-
# Precedence: never >
|
|
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.
|
|
51
66
|
auto_confirm = ENV['AGENTF_AUTO_CONFIRM_MEMORIES'] == 'true'
|
|
52
67
|
pref = persistence_preference_for(agent)
|
|
53
68
|
|
|
@@ -58,24 +73,13 @@ module Agentf
|
|
|
58
73
|
rescue StandardError
|
|
59
74
|
end
|
|
60
75
|
return nil
|
|
61
|
-
when :always
|
|
62
|
-
# proceed without requiring explicit confirm
|
|
63
76
|
when :ask_first
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
end
|
|
71
|
-
else
|
|
72
|
-
# default conservative behavior: require explicit confirm (or env opt-in)
|
|
73
|
-
unless agent == Agentf::AgentRoles::ORCHESTRATOR || confirm == true || auto_confirm
|
|
74
|
-
begin
|
|
75
|
-
puts "[MEMORY] Skipping persistence for #{agent}: confirmation required"
|
|
76
|
-
rescue StandardError
|
|
77
|
-
end
|
|
78
|
-
return nil
|
|
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)
|
|
79
83
|
end
|
|
80
84
|
end
|
|
81
85
|
episode_id = "episode_#{SecureRandom.hex(4)}"
|
|
@@ -139,7 +143,7 @@ module Agentf
|
|
|
139
143
|
episode_id
|
|
140
144
|
end
|
|
141
145
|
|
|
142
|
-
|
|
146
|
+
def store_success(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, confirm: nil)
|
|
143
147
|
store_episode(
|
|
144
148
|
type: "success",
|
|
145
149
|
title: title,
|
|
@@ -152,7 +156,7 @@ module Agentf
|
|
|
152
156
|
)
|
|
153
157
|
end
|
|
154
158
|
|
|
155
|
-
|
|
159
|
+
def store_pitfall(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, confirm: nil)
|
|
156
160
|
store_episode(
|
|
157
161
|
type: "pitfall",
|
|
158
162
|
title: title,
|
|
@@ -165,7 +169,7 @@ module Agentf
|
|
|
165
169
|
)
|
|
166
170
|
end
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
def store_lesson(title:, description:, context: "", code_snippet: "", tags: [], agent: Agentf::AgentRoles::ORCHESTRATOR, confirm: nil)
|
|
169
173
|
store_episode(
|
|
170
174
|
type: "lesson",
|
|
171
175
|
title: title,
|
|
@@ -221,7 +225,7 @@ module Agentf
|
|
|
221
225
|
end
|
|
222
226
|
|
|
223
227
|
def store_incident(title:, description:, root_cause: "", resolution: "", tags: [], agent: Agentf::AgentRoles::INCIDENT_RESPONDER,
|
|
224
|
-
business_capability: nil)
|
|
228
|
+
business_capability: nil, confirm: nil)
|
|
225
229
|
store_episode(
|
|
226
230
|
type: "incident",
|
|
227
231
|
title: title,
|
|
@@ -229,6 +233,7 @@ module Agentf
|
|
|
229
233
|
context: ["Root cause: #{root_cause}", "Resolution: #{resolution}"].reject { |entry| entry.end_with?(": ") }.join(" | "),
|
|
230
234
|
tags: tags,
|
|
231
235
|
agent: agent,
|
|
236
|
+
confirm: confirm,
|
|
232
237
|
metadata: {
|
|
233
238
|
"root_cause" => root_cause,
|
|
234
239
|
"resolution" => resolution,
|
|
@@ -238,7 +243,7 @@ module Agentf
|
|
|
238
243
|
)
|
|
239
244
|
end
|
|
240
245
|
|
|
241
|
-
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)
|
|
242
247
|
store_episode(
|
|
243
248
|
type: "playbook",
|
|
244
249
|
title: title,
|
|
@@ -246,6 +251,7 @@ module Agentf
|
|
|
246
251
|
context: steps.any? ? "Steps: #{steps.join('; ')}" : "",
|
|
247
252
|
tags: tags,
|
|
248
253
|
agent: agent,
|
|
254
|
+
confirm: confirm,
|
|
249
255
|
metadata: {
|
|
250
256
|
"steps" => steps,
|
|
251
257
|
"feature_area" => feature_area,
|
|
@@ -617,7 +623,7 @@ module Agentf
|
|
|
617
623
|
cursor, batch = @client.scan(cursor, match: "episodic:*", count: 100)
|
|
618
624
|
batch.each do |key|
|
|
619
625
|
memory = load_episode(key)
|
|
620
|
-
memories << memory if memory
|
|
626
|
+
memories << memory if memory && memory["project"].to_s == @project.to_s
|
|
621
627
|
end
|
|
622
628
|
break if cursor == "0"
|
|
623
629
|
end
|
|
@@ -895,31 +901,12 @@ module Agentf
|
|
|
895
901
|
base
|
|
896
902
|
end
|
|
897
903
|
|
|
898
|
-
#
|
|
899
|
-
#
|
|
900
|
-
#
|
|
901
|
-
#
|
|
902
|
-
#
|
|
903
|
-
#
|
|
904
|
-
def agent_requires_confirmation?(agent)
|
|
905
|
-
begin
|
|
906
|
-
# If a developer passed an agent class-like string, try to map it to
|
|
907
|
-
# the loaded agent class and inspect its policy_boundaries.
|
|
908
|
-
candidate = Agentf::Agents.constants
|
|
909
|
-
.map { |c| Agentf::Agents.const_get(c) }
|
|
910
|
-
.find do |klass|
|
|
911
|
-
klass.is_a?(Class) && klass.respond_to?(:policy_boundaries) && klass.typed_name == agent
|
|
912
|
-
end
|
|
913
|
-
|
|
914
|
-
return false unless candidate
|
|
915
|
-
|
|
916
|
-
boundaries = candidate.policy_boundaries
|
|
917
|
-
ask_first = Array(boundaries["ask_first"]) rescue []
|
|
918
|
-
!ask_first.empty?
|
|
919
|
-
rescue StandardError
|
|
920
|
-
false
|
|
921
|
-
end
|
|
922
|
-
end
|
|
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.
|
|
923
910
|
|
|
924
911
|
# Inspect loaded agent classes for explicit persistence preference.
|
|
925
912
|
# Returns one of: :always, :ask_first, :never, or nil when unknown.
|
|
@@ -934,9 +921,15 @@ module Agentf
|
|
|
934
921
|
return nil unless candidate
|
|
935
922
|
|
|
936
923
|
boundaries = candidate.policy_boundaries
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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?
|
|
940
933
|
nil
|
|
941
934
|
rescue StandardError
|
|
942
935
|
nil
|