Skip to content

Commit 5866cf0

Browse files
wow-mileyclaude
andauthored
AMPR-163 #486: complete spark-based agent migration (#488)
I migrated every agent in the CLI to SparkBasedAgent<S> + role spark + declarative .spark.md guidance, finishing what AMPR-161 started. **Wave 0 — foundation:** generified SparkBasedAgent over the agent's state type so role-specific factories can carry CodeState / ProductState / ProjectState / QualityState without subclassing for behaviour. **Wave 1 — Code path:** authored `code-agent.spark.md` (language-neutral phase-loop guidance), added `SparkBasedAgent.Code(...)` factory, moved `CodeParams` strategies onto `ToolWriteCodeFile` / `ToolReadCodeFile`, introduced strict tool-id dispatch on `runLLMToExecuteTask` (no keyword-routing fallback), added a `ToolPlanSteps` tool that owns the plan-step JSON schema, added `GitParams.Commit` strategy on `ToolCommit`, deleted `CodeAgent.kt` (1348 LOC) and extracted its issue → PR workflow into `CodeIssueWorkflow` + generic `AutonomousWorkLoop<S>`. **Wave 2 — fan-out:** authored `product-agent.spark.md`, `project-agent.spark.md`, `quality-agent.spark.md`; added `SparkBasedAgent.Product`, `.Project`, `.Quality` factories; attached `ProjectParams.IssueCreation` / `ProjectParams.HumanEscalation` strategies onto `ToolCreateIssues` / `ToolAskHuman`; deleted `ProductAgent.kt`, `ProjectAgent.kt`, `QualityAgent.kt`; rewired `AgentFactory` and `Main.kt`. **Wave 3 — cleanup:** deleted the deprecated `ReasoningSettings` hooks (`perceptionContextBuilder` / `planningPromptBuilder` / `outcomeContextBuilder` / `knowledgeExtractor`) and their `perception {}` / `planning {}` / `evaluation {}` / `knowledge {}` builder DSLs; wired `DefaultPhaseSparkLibrary` into `AgentFactory` so every agent reaches the LLM with its `.spark.md` per-phase guidance active; expanded `LanguageSpark.Kotlin` with package-from-path conventions and PLAN/EXECUTE phase contributions so Kotlin specifics live in a composable spark rather than on the code agent's profile. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bc8765d commit 5866cf0

67 files changed

Lines changed: 2766 additions & 8267 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/AmpereCommand.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,11 @@ class AmpereCommand(
256256
jazzPane.setPhase(CognitiveProgressPane.Phase.INITIALIZING, "Finding issue #$issue...")
257257
agentScope.launch {
258258
try {
259-
val availableIssues = context.codeAgent.queryAvailableIssues()
259+
val availableIssues = context.codeIssueWorkflow.queryAvailableIssues()
260260
val targetIssue = availableIssues.find { it.number == issue }
261261
if (targetIssue != null) {
262262
jazzPane.setPhase(CognitiveProgressPane.Phase.PERCEIVE, "Working on issue #$issue...")
263-
val issueResult = context.codeAgent.workOnIssue(targetIssue)
263+
val issueResult = context.codeIssueWorkflow.workOnIssue(targetIssue, context.codeAgent)
264264
if (issueResult.isSuccess) {
265265
jazzPane.setPhase(CognitiveProgressPane.Phase.COMPLETED)
266266
} else {

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/AmpereContext.kt

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import kotlinx.coroutines.SupervisorJob
88
import kotlinx.coroutines.cancel
99
import kotlinx.serialization.json.Json
1010
import link.socket.ampere.agents.definition.AgentId
11-
import link.socket.ampere.agents.definition.CodeAgent
11+
import link.socket.ampere.agents.definition.SparkBasedAgent
12+
import link.socket.ampere.agents.definition.code.CodeState
1213
import link.socket.ampere.agents.domain.knowledge.KnowledgeRepository
1314
import link.socket.ampere.agents.domain.knowledge.KnowledgeRepositoryImpl
1415
import link.socket.ampere.agents.domain.outcome.OutcomeMemoryRepository
1516
import link.socket.ampere.agents.domain.event.Event
1617
import link.socket.ampere.agents.domain.memory.AgentMemoryService
1718
import link.socket.ampere.agents.execution.AutonomousWorkLoop
1819
import link.socket.ampere.agents.execution.WorkLoopConfig
20+
import link.socket.ampere.agents.execution.issue.CodeIssueWorkflow
21+
import link.socket.ampere.integrations.issues.IssueTrackerProvider
1922
import link.socket.ampere.agents.environment.EnvironmentService
2023
import link.socket.ampere.agents.environment.workspace.ExecutionWorkspace
2124
import link.socket.ampere.agents.environment.workspace.defaultWorkspace
@@ -225,29 +228,41 @@ class AmpereContext(
225228
}
226229

227230
/**
228-
* CodeAgent instance for autonomous work.
229-
* Set when createAutonomousWorkLoop() is called.
231+
* The code agent instance for autonomous work. Set when
232+
* [createAutonomousWorkLoop] is called.
230233
*/
231-
private var _codeAgent: CodeAgent? = null
234+
private var _codeAgent: SparkBasedAgent<CodeState>? = null
232235

233236
/**
234-
* Access the CodeAgent instance.
235-
* Throws an error if not initialized via createAutonomousWorkLoop().
237+
* Access the code agent instance.
238+
* Throws an error if not initialized via [createAutonomousWorkLoop].
236239
*/
237-
val codeAgent: CodeAgent
238-
get() = _codeAgent ?: error("CodeAgent not initialized. Call createAutonomousWorkLoop() first.")
240+
val codeAgent: SparkBasedAgent<CodeState>
241+
get() = _codeAgent ?: error("codeAgent not initialized. Call createAutonomousWorkLoop() first.")
239242

240243
/**
241-
* Autonomous work loop for CodeAgent.
242-
* Manages continuous polling and processing of GitHub issues.
244+
* The issue → task → PR workflow paired with the code agent. Owns the
245+
* claim/work-on/update lifecycle that used to live on the legacy
246+
* `CodeAgent`. Set when [createAutonomousWorkLoop] is called.
243247
*/
244-
private var _autonomousWorkLoop: AutonomousWorkLoop? = null
248+
private var _codeIssueWorkflow: CodeIssueWorkflow? = null
249+
250+
val codeIssueWorkflow: CodeIssueWorkflow
251+
get() = _codeIssueWorkflow ?: error(
252+
"codeIssueWorkflow not initialized. Call createAutonomousWorkLoop() first.",
253+
)
254+
255+
/**
256+
* Autonomous work loop for the code agent. Manages continuous polling
257+
* and processing of GitHub issues.
258+
*/
259+
private var _autonomousWorkLoop: AutonomousWorkLoop<CodeState>? = null
245260

246261
/**
247262
* Access the autonomous work loop.
248-
* Throws an error if not initialized via createAutonomousWorkLoop().
263+
* Throws an error if not initialized via [createAutonomousWorkLoop].
249264
*/
250-
val autonomousWorkLoop: AutonomousWorkLoop
265+
val autonomousWorkLoop: AutonomousWorkLoop<CodeState>
251266
get() = _autonomousWorkLoop ?: error("Autonomous work loop not initialized. Call createAutonomousWorkLoop() first.")
252267

253268
/**
@@ -316,23 +331,34 @@ class AmpereContext(
316331
}
317332

318333
/**
319-
* Create and initialize the autonomous work loop for a CodeAgent.
334+
* Create and initialize the autonomous work loop for a code agent.
320335
*
321336
* This must be called before attempting to start autonomous work.
322337
* The work loop is connected to the shared event bus so that work
323338
* events are visible in the dashboard.
324339
*
325-
* @param codeAgent The CodeAgent instance that will process issues
340+
* @param codeAgent The agent instance that will process issues
341+
* @param issueTrackerProvider Source of issues (typically GitHub)
342+
* @param repository Repository identifier the provider scopes its queries to
326343
* @param config Optional configuration for work loop behavior
327344
* @return The created AutonomousWorkLoop instance
328345
*/
329346
fun createAutonomousWorkLoop(
330-
codeAgent: CodeAgent,
347+
codeAgent: SparkBasedAgent<CodeState>,
348+
issueTrackerProvider: IssueTrackerProvider,
349+
repository: String,
331350
config: WorkLoopConfig = WorkLoopConfig(),
332-
): AutonomousWorkLoop {
351+
): AutonomousWorkLoop<CodeState> {
352+
val workflow = CodeIssueWorkflow(
353+
issueTrackerProvider = issueTrackerProvider,
354+
repository = repository,
355+
agentId = codeAgent.id,
356+
)
333357
_codeAgent = codeAgent
358+
_codeIssueWorkflow = workflow
334359
_autonomousWorkLoop = AutonomousWorkLoop(
335360
agent = codeAgent,
361+
workflow = workflow,
336362
config = config,
337363
scope = scope,
338364
eventApiFactory = { agentId -> environmentService.createEventApi(agentId) },

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/Main.kt

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import java.io.File
55
import kotlinx.coroutines.runBlocking
66
import link.socket.ampere.agents.definition.AgentFactory
77
import link.socket.ampere.agents.definition.AgentType
8-
import link.socket.ampere.agents.definition.CodeAgent
9-
import link.socket.ampere.agents.definition.ProductAgent
10-
import link.socket.ampere.agents.definition.QualityAgent
8+
import link.socket.ampere.agents.definition.SparkBasedAgent
9+
import link.socket.ampere.agents.definition.code.CodeState
10+
import link.socket.ampere.agents.definition.product.ProductState
11+
import link.socket.ampere.agents.definition.qa.QualityState
1112
import link.socket.ampere.config.AmpereConfig
1213
import link.socket.ampere.config.ConfigConverter
1314
import link.socket.ampere.config.ConfigParser
@@ -95,20 +96,27 @@ fun main(args: Array<String>) {
9596
// Create agents based on team configuration (or defaults if no config)
9697
val teamRoles = config?.team?.map { it.role } ?: listOf("engineer", "product-manager", "qa-tester")
9798

98-
val codeAgent: CodeAgent? = if (teamRoles.any { it == "engineer" || it == "code" }) {
99-
agentFactory.create<CodeAgent>(AgentType.CODE).also { it.initialize(context.scope) }
99+
val codeAgent: SparkBasedAgent<CodeState>? = if (teamRoles.any { it == "engineer" || it == "code" }) {
100+
agentFactory.create<SparkBasedAgent<CodeState>>(AgentType.CODE).also { it.initialize(context.scope) }
100101
} else null
101102

102-
val productAgent: ProductAgent? = if (teamRoles.any { it == "product-manager" || it == "product" }) {
103-
agentFactory.create<ProductAgent>(AgentType.PRODUCT).also { it.initialize(context.scope) }
103+
val productAgent: SparkBasedAgent<ProductState>? = if (teamRoles.any { it == "product-manager" || it == "product" }) {
104+
agentFactory.create<SparkBasedAgent<ProductState>>(AgentType.PRODUCT).also { it.initialize(context.scope) }
104105
} else null
105106

106-
val qualityAgent: QualityAgent? = if (teamRoles.any { it == "qa-tester" || it == "quality" }) {
107-
agentFactory.create<QualityAgent>(AgentType.QUALITY).also { it.initialize(context.scope) }
107+
val qualityAgent: SparkBasedAgent<QualityState>? = if (teamRoles.any { it == "qa-tester" || it == "quality" }) {
108+
agentFactory.create<SparkBasedAgent<QualityState>>(AgentType.QUALITY).also { it.initialize(context.scope) }
108109
} else null
109110

110-
// Initialize autonomous work loop for CodeAgent (if present in team)
111-
codeAgent?.let { context.createAutonomousWorkLoop(it) }
111+
// Initialize autonomous work loop for the code agent (if present in team
112+
// and a repository was detected for the issue tracker).
113+
if (codeAgent != null && repository != null) {
114+
context.createAutonomousWorkLoop(
115+
codeAgent = codeAgent,
116+
issueTrackerProvider = issueTrackerProvider,
117+
repository = repository,
118+
)
119+
}
112120

113121
try {
114122
// Start all orchestrator services

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/WorkCommand.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class WorkCommand(
108108
terminal.println()
109109

110110
// Show what would happen
111-
val issues = context.codeAgent.queryAvailableIssues()
111+
val issues = context.codeIssueWorkflow.queryAvailableIssues()
112112
terminal.println("Would work on ${issues.size} available issue(s)")
113113
issues.take(5).forEach { issue ->
114114
terminal.println(" #${issue.number}: ${issue.title}")
@@ -134,7 +134,9 @@ class WorkCommand(
134134
}
135135
} else {
136136
// Work on single issue
137-
val issues = context.codeAgent.queryAvailableIssues()
137+
val workflow = context.codeIssueWorkflow
138+
val agent = context.codeAgent
139+
val issues = workflow.queryAvailableIssues()
138140

139141
if (issues.isEmpty()) {
140142
terminal.println(yellow("No available issues found"))
@@ -147,13 +149,13 @@ class WorkCommand(
147149

148150
terminal.println("Working on issue #${issue.number}: ${issue.title}")
149151

150-
val claimed = context.codeAgent.claimIssue(issue.number)
152+
val claimed = workflow.claimIssue(issue.number)
151153
if (claimed.isFailure) {
152154
terminal.println(red("Failed to claim issue: ${claimed.exceptionOrNull()?.message}"))
153155
return@runBlocking
154156
}
155157

156-
val result = context.codeAgent.workOnIssue(issue)
158+
val result = workflow.workOnIssue(issue, agent)
157159
if (result.isSuccess) {
158160
terminal.println(green("✓ Successfully completed issue #${issue.number}"))
159161
} else {

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/cli/goal/GoalHandler.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import kotlinx.coroutines.withContext
88
import kotlinx.datetime.Clock
99
import link.socket.ampere.AmpereContext
1010
import link.socket.ampere.agents.config.AgentActionAutonomy
11-
import link.socket.ampere.agents.definition.CodeAgent
1211
import link.socket.ampere.agents.definition.AgentFactory
1312
import link.socket.ampere.agents.definition.AgentType
13+
import link.socket.ampere.agents.definition.SparkBasedAgent
14+
import link.socket.ampere.agents.definition.code.CodeState
1415
import link.socket.ampere.agents.domain.event.Event
1516
import link.socket.ampere.agents.domain.event.TicketEvent
1617
import link.socket.ampere.agents.domain.outcome.ExecutionOutcome
@@ -50,7 +51,7 @@ class GoalHandler(
5051
private val aiConfiguration: AIConfiguration? = null,
5152
) {
5253
private var currentActivation: GoalActivation? = null
53-
private var currentAgent: CodeAgent? = null
54+
private var currentAgent: SparkBasedAgent<CodeState>? = null
5455
private var eventApi: AgentEventApi? = null
5556

5657
/**
@@ -104,8 +105,8 @@ class GoalHandler(
104105
toolWriteCodeFileOverride = writeCodeTool,
105106
)
106107

107-
// Create CodeAgent
108-
val agent = agentFactory.create<CodeAgent>(AgentType.CODE)
108+
// Create spark-based code agent
109+
val agent = agentFactory.create<SparkBasedAgent<CodeState>>(AgentType.CODE)
109110
currentAgent = agent
110111

111112
// Create event API for this agent to publish events
@@ -170,7 +171,7 @@ class GoalHandler(
170171
* Handle ticket assignment by running the PROPEL cognitive cycle.
171172
*/
172173
private suspend fun handleTicketAssignment(
173-
agent: CodeAgent,
174+
agent: SparkBasedAgent<CodeState>,
174175
ticketId: String,
175176
) {
176177
val api = eventApi

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/demo/AgentTestRunner.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import kotlinx.datetime.Clock
1818
import link.socket.ampere.AmpereContext
1919
import link.socket.ampere.agents.definition.AgentFactory
2020
import link.socket.ampere.agents.definition.AgentType
21-
import link.socket.ampere.agents.definition.CodeAgent
21+
import link.socket.ampere.agents.definition.SparkBasedAgent
22+
import link.socket.ampere.agents.definition.code.CodeState
2223
import link.socket.ampere.agents.config.AgentActionAutonomy
2324
import link.socket.ampere.agents.domain.cognition.sparks.CognitivePhase
2425
import link.socket.ampere.agents.domain.cognition.sparks.PhaseSparkManager
@@ -108,7 +109,7 @@ fun main(escalation: Boolean = false) {
108109
)
109110

110111
// Create CodeWriterAgent
111-
val agent = agentFactory.create<CodeAgent>(AgentType.CODE)
112+
val agent = agentFactory.create<SparkBasedAgent<CodeState>>(AgentType.CODE)
112113

113114
println("🤖 CodeWriterAgent created")
114115
println(" Agent ID: ${agent.id}")
@@ -376,7 +377,7 @@ internal fun findGeneratedFiles(outputDir: File): GeneratedFiles? {
376377
* Handle ticket assignment by running the cognitive cycle.
377378
*/
378379
private suspend fun handleTicketAssignment(
379-
agent: CodeAgent,
380+
agent: SparkBasedAgent<CodeState>,
380381
ticketId: String,
381382
context: AmpereContext,
382383
eventApi: AgentEventApi,

ampere-cli/src/jvmMain/kotlin/link/socket/ampere/demo/MultiAgentDemoRunner.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import link.socket.ampere.AmpereContext
99
import link.socket.ampere.agents.config.AgentActionAutonomy
1010
import link.socket.ampere.agents.definition.AgentFactory
1111
import link.socket.ampere.agents.definition.AgentType
12-
import link.socket.ampere.agents.definition.CodeAgent
13-
import link.socket.ampere.agents.definition.ProjectAgent
12+
import link.socket.ampere.agents.definition.SparkBasedAgent
13+
import link.socket.ampere.agents.definition.code.CodeState
14+
import link.socket.ampere.agents.definition.project.ProjectState
1415
import link.socket.ampere.agents.domain.Urgency
1516
import link.socket.ampere.agents.domain.event.EventId
1617
import link.socket.ampere.agents.domain.event.EventSource
@@ -81,7 +82,7 @@ class MultiAgentDemoRunner(
8182
model = AIModel_Claude.Sonnet_4
8283
),
8384
)
84-
val coordinator = coordinatorFactory.create<ProjectAgent>(AgentType.PROJECT)
85+
val coordinator = coordinatorFactory.create<SparkBasedAgent<ProjectState>>(AgentType.PROJECT)
8586
val coordinatorEventApi = context.environmentService.createEventApi(coordinator.id)
8687

8788
// Create worker agent (CodeWriter role)
@@ -97,7 +98,7 @@ class MultiAgentDemoRunner(
9798
),
9899
toolWriteCodeFileOverride = writeCodeTool,
99100
)
100-
val worker = workerFactory.create<CodeAgent>(AgentType.CODE)
101+
val worker = workerFactory.create<SparkBasedAgent<CodeState>>(AgentType.CODE)
101102
val workerEventApi = context.environmentService.createEventApi(worker.id)
102103

103104
// Set coordinator and worker info on progress pane

ampere-core/src/androidUnitTest/kotlin/link/socket/ampere/agents/implementations/CodeWriterAgentTest.android.kt

Lines changed: 0 additions & 5 deletions
This file was deleted.

0 commit comments

Comments
 (0)