Skip to content

Commit 6154aaa

Browse files
committed
refactor: unify BFS/DFS explorers behind Exploring protocol
1 parent 2b996b1 commit 6154aaa

4 files changed

Lines changed: 90 additions & 20 deletions

File tree

Sources/mirroir-mcp/DFSExplorer.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,4 +490,24 @@ final class DFSExplorer: @unchecked Sendable {
490490
snapshot: data.graphSnapshot, allScreens: data.screens
491491
)
492492
}
493+
494+
/// Generate a summary report of the DFS exploration.
495+
func generateReport() -> String {
496+
let s = stats
497+
lock.lock()
498+
let depth = backtrackStack.count - 1
499+
lock.unlock()
500+
var lines = [
501+
"## DFS Exploration Report",
502+
"- Screens: \(s.nodeCount), Edges: \(s.edgeCount)",
503+
"- Actions: \(s.actionCount), Duration: \(s.elapsedSeconds)s",
504+
"- Max depth reached: \(depth)",
505+
]
506+
let events = graph.finalize().recoveryEvents
507+
if !events.isEmpty {
508+
lines.append("\n### Recovery Events")
509+
for event in events { lines.append("- [\(event.category)] \(event.description)") }
510+
}
511+
return lines.joined(separator: "\n")
512+
}
493513
}

Sources/mirroir-mcp/ExplorationResultFormatter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ enum ExplorationResultFormatter {
2525
}
2626

2727
/// Format the final exploration result with stats, skill content, and detailed report.
28-
static func formatExploreResult(bundle: SkillBundle, explorer: BFSExplorer) -> String {
28+
static func formatExploreResult(bundle: SkillBundle, explorer: any Exploring) -> String {
2929
let stats = explorer.stats
3030
let statLine = "(\(stats.nodeCount) screens, \(stats.actionCount) actions, \(stats.elapsedSeconds)s)"
3131
guard !bundle.skills.isEmpty else {

Sources/mirroir-mcp/GenerateSkillTools.swift

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ extension MirroirMCP {
2121
(1) action="start" \u{2014} launch app + OCR. \
2222
(2) Navigate with tap/swipe/type_text, then action="capture" per screen. \
2323
(3) action="finish" \u{2014} emit SKILL.md. \
24-
Use action="explore" for autonomous BFS exploration. \
24+
Use action="explore" for autonomous exploration (BFS default, or DFS). \
2525
Set fresh=true to discard persisted graph and explore from scratch. \
2626
WARNING: Exploration steals Mac keyboard focus (global HID events). \
2727
SECURITY: May navigate into sensitive screens. Do not run unattended.
@@ -35,7 +35,7 @@ extension MirroirMCP {
3535
"Session action: \"start\" to launch app and begin, " +
3636
"\"capture\" to OCR current screen and append, " +
3737
"\"finish\" to generate SKILL.md from all captures, " +
38-
"\"explore\" for autonomous BFS exploration."),
38+
"\"explore\" for autonomous exploration (BFS or DFS)."),
3939
"enum": .array([
4040
.string("start"),
4141
.string("capture"),
@@ -121,6 +121,16 @@ extension MirroirMCP {
121121
"Full-page scrolling still runs to discover below-fold elements. " +
122122
"Useful with vision describers that produce clean semantic elements. Default: false."),
123123
]),
124+
"explorer": .object([
125+
"type": .string("string"),
126+
"description": .string(
127+
"Exploration algorithm: \"bfs\" (breadth-first, default) " +
128+
"or \"dfs\" (depth-first)."),
129+
"enum": .array([
130+
.string("bfs"),
131+
.string("dfs"),
132+
]),
133+
]),
124134
]),
125135
"required": .array([.string("action")]),
126136
],
@@ -402,6 +412,7 @@ extension MirroirMCP {
402412
let fresh = args["fresh"]?.asBool() ?? false
403413
let seed = args["seed"]?.asInt().map { UInt64($0) }
404414
let skipCalibration = args["skip_calibration"]?.asBool() ?? false
415+
let explorerChoice = args["explorer"]?.asString() ?? "bfs"
405416
let explicitStrategy = args["strategy"]?.asString()
406417
let strategyChoice = StrategyDetector.detect(
407418
targetType: ctx.targetType,
@@ -427,30 +438,37 @@ extension MirroirMCP {
427438
screenshotBase64: firstResult.screenshotBase64
428439
)
429440

430-
// Create BFS explorer and run exploration loop
441+
// Create explorer (BFS or DFS) and run exploration loop
431442
let windowSize = ctx.bridge.getWindowInfo()?.size ?? CGSize(width: 410, height: 890)
432-
let componentDefinitions = ComponentLoader.loadAll()
433-
let detectionMode = ComponentDetectionMode(rawValue: EnvConfig.componentDetection) ?? .llmFirstScreen
434-
let classifier = detectionMode.buildClassifier(server: server)
435-
let advisor: any ExplorationAdvising = EmbacleFFI.isAvailable
436-
? VisionExplorationAdvisor() : HeuristicExplorationAdvisor()
437-
let explorer = BFSExplorer(
438-
session: session, budget: budget, windowSize: windowSize,
439-
componentDefinitions: componentDefinitions,
440-
classifier: classifier,
441-
bridge: ctx.bridge,
442-
seed: seed,
443-
skipCalibration: skipCalibration,
444-
advisor: advisor
445-
)
443+
let explorer: any Exploring
444+
if explorerChoice == "dfs" {
445+
explorer = DFSExplorer(
446+
session: session, budget: budget, windowSize: windowSize
447+
)
448+
} else {
449+
let componentDefinitions = ComponentLoader.loadAll()
450+
let detectionMode = ComponentDetectionMode(rawValue: EnvConfig.componentDetection) ?? .llmFirstScreen
451+
let classifier = detectionMode.buildClassifier(server: server)
452+
let advisor: any ExplorationAdvising = EmbacleFFI.isAvailable
453+
? VisionExplorationAdvisor() : HeuristicExplorationAdvisor()
454+
explorer = BFSExplorer(
455+
session: session, budget: budget, windowSize: windowSize,
456+
componentDefinitions: componentDefinitions,
457+
classifier: classifier,
458+
bridge: ctx.bridge,
459+
seed: seed,
460+
skipCalibration: skipCalibration,
461+
advisor: advisor
462+
)
463+
}
446464
explorer.markStarted()
447465

448466
var stepResults: [String] = [
449-
"Autonomous exploration started for '\(appName)'.",
467+
"Autonomous \(explorerChoice.uppercased()) exploration started for '\(appName)'.",
450468
"Budget: depth=\(maxDepth), screens=\(maxScreens), time=\(maxTime)s",
451469
]
452470

453-
// Run BFS loop using detected strategy
471+
// Run exploration loop using detected strategy
454472
while !explorer.completed {
455473
let result: ExploreStepResult
456474
switch strategyChoice {

Sources/mirroir-mcp/Protocols.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,34 @@ protocol ComponentClassifying: Sendable {
191191
) -> [ScreenComponent]?
192192
}
193193

194+
/// Common interface for app exploration algorithms (BFS, DFS).
195+
/// Both explorers follow the Session Accumulator pattern: `markStarted()` begins the
196+
/// lifecycle, `step()` advances one action, and `generateBundle()` produces the final output.
197+
protocol Exploring: AnyObject, Sendable {
198+
/// Perform one exploration step using the given strategy.
199+
func step<S: ExplorationStrategy>(
200+
describer: ScreenDescribing, input: InputProviding, strategy: S.Type
201+
) -> ExploreStepResult
202+
203+
/// Record the exploration start time. Call once after the initial screen capture.
204+
func markStarted()
205+
206+
/// Whether the exploration has completed (budget exhausted or all reachable screens visited).
207+
var completed: Bool { get }
208+
209+
/// Current exploration statistics: screens discovered, edges, actions performed, elapsed time.
210+
var stats: (nodeCount: Int, edgeCount: Int, actionCount: Int, elapsedSeconds: Int) { get }
211+
212+
/// The navigation graph tracking screen transitions and visited elements.
213+
var graph: NavigationGraph { get }
214+
215+
/// Generate the final skill bundle from the exploration session.
216+
func generateBundle() -> SkillBundle
217+
218+
/// Generate a human-readable exploration report summarizing what was explored.
219+
func generateReport() -> String
220+
}
221+
194222
// MARK: - Conformances
195223

196224
extension MirroringBridge: MenuActionCapable {}
@@ -210,3 +238,7 @@ extension CompositeTextRecognizer: TextRecognizing {}
210238
extension ScreenDescriber: ScreenDescribing {}
211239

212240
extension VisionScreenDescriber: ScreenDescribing {}
241+
242+
extension BFSExplorer: Exploring {}
243+
244+
extension DFSExplorer: Exploring {}

0 commit comments

Comments
 (0)