feat(tasks): auto-link the originating agent session
When an agent creates or claims a task via the tool, record its session as a CONTEXT/SESSION link so the context bundle's session resolver has real edges to inline (status/intent/workflow). Shared linkOriginSession helper; reducer dedupes so create+claim from one session is a single edge. The REST create path stays unlinked (no agent session there). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
@@ -80,6 +80,9 @@ class TaskCreateTool(private val service: TaskService) : Tool, ToolExecutor {
|
|||||||
acceptanceCriteria = request.listParam("acceptance_criteria"),
|
acceptanceCriteria = request.listParam("acceptance_criteria"),
|
||||||
affectedPaths = request.listParam("affected_paths"),
|
affectedPaths = request.listParam("affected_paths"),
|
||||||
)
|
)
|
||||||
|
// Provenance: tie the new task to the session that spawned it so its context bundle
|
||||||
|
// can show the originating run.
|
||||||
|
service.linkOriginSession(task.taskId, request)
|
||||||
return ToolResult.Success(
|
return ToolResult.Success(
|
||||||
invocationId = request.invocationId,
|
invocationId = request.invocationId,
|
||||||
output = "Created ${task.taskId.value}: ${task.state.title}",
|
output = "Created ${task.taskId.value}: ${task.state.title}",
|
||||||
|
|||||||
+6
-1
@@ -127,7 +127,12 @@ class TaskUpdateTool(private val service: TaskService) : Tool, ToolExecutor {
|
|||||||
val claimant = request.stringParam("claimant") ?: request.sessionId.value
|
val claimant = request.stringParam("claimant") ?: request.sessionId.value
|
||||||
val reason = request.stringParam("reason")
|
val reason = request.stringParam("reason")
|
||||||
when (action) {
|
when (action) {
|
||||||
"claim" -> service.claim(taskId, claimant)
|
"claim" -> {
|
||||||
|
service.claim(taskId, claimant)
|
||||||
|
// Record the claiming session too, so a task picked up by a different run than
|
||||||
|
// the one that created it links both into its work graph.
|
||||||
|
service.linkOriginSession(taskId, request)
|
||||||
|
}
|
||||||
"release" -> service.release(taskId)
|
"release" -> service.release(taskId)
|
||||||
"block" -> service.block(taskId, reason ?: "blocked")
|
"block" -> service.block(taskId, reason ?: "blocked")
|
||||||
"unblock" -> service.unblock(taskId)
|
"unblock" -> service.unblock(taskId)
|
||||||
|
|||||||
+15
@@ -1,6 +1,10 @@
|
|||||||
package com.correx.infrastructure.tools.task
|
package com.correx.infrastructure.tools.task
|
||||||
|
|
||||||
import com.correx.core.events.events.ToolRequest
|
import com.correx.core.events.events.ToolRequest
|
||||||
|
import com.correx.core.events.types.TaskId
|
||||||
|
import com.correx.core.events.types.TaskLinkType
|
||||||
|
import com.correx.core.events.types.TaskTargetKind
|
||||||
|
import com.correx.core.tasks.TaskService
|
||||||
|
|
||||||
/** A non-blank string parameter, or null when absent/blank/not a string. */
|
/** A non-blank string parameter, or null when absent/blank/not a string. */
|
||||||
internal fun ToolRequest.stringParam(name: String): String? =
|
internal fun ToolRequest.stringParam(name: String): String? =
|
||||||
@@ -9,3 +13,14 @@ internal fun ToolRequest.stringParam(name: String): String? =
|
|||||||
/** A string-list parameter (JSON array), coerced element-wise; empty when absent. */
|
/** A string-list parameter (JSON array), coerced element-wise; empty when absent. */
|
||||||
internal fun ToolRequest.listParam(name: String): List<String> =
|
internal fun ToolRequest.listParam(name: String): List<String> =
|
||||||
(parameters[name] as? List<*>)?.mapNotNull { it?.toString()?.takeIf(String::isNotBlank) } ?: emptyList()
|
(parameters[name] as? List<*>)?.mapNotNull { it?.toString()?.takeIf(String::isNotBlank) } ?: emptyList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Records the agent's session on the task as a CONTEXT/SESSION link, so the context bundle can
|
||||||
|
* surface the run(s) that created or worked on it (the session resolver inlines status/intent).
|
||||||
|
* No-op for a blank session id; duplicate links are deduped by the reducer, so creating and then
|
||||||
|
* claiming from the same session yields a single edge.
|
||||||
|
*/
|
||||||
|
internal suspend fun TaskService.linkOriginSession(taskId: TaskId, request: ToolRequest) {
|
||||||
|
val session = request.sessionId.value.takeIf { it.isNotBlank() } ?: return
|
||||||
|
link(taskId, session, TaskLinkType.CONTEXT, TaskTargetKind.SESSION)
|
||||||
|
}
|
||||||
|
|||||||
+20
-1
@@ -6,6 +6,9 @@ import com.correx.core.events.events.ToolRequest
|
|||||||
import com.correx.core.events.stores.EventStore
|
import com.correx.core.events.stores.EventStore
|
||||||
import com.correx.core.events.types.SessionId
|
import com.correx.core.events.types.SessionId
|
||||||
import com.correx.core.events.types.StageId
|
import com.correx.core.events.types.StageId
|
||||||
|
import com.correx.core.events.types.TaskId
|
||||||
|
import com.correx.core.events.types.TaskLinkType
|
||||||
|
import com.correx.core.events.types.TaskTargetKind
|
||||||
import com.correx.core.events.types.ToolInvocationId
|
import com.correx.core.events.types.ToolInvocationId
|
||||||
import com.correx.core.tasks.TaskContextAssembler
|
import com.correx.core.tasks.TaskContextAssembler
|
||||||
import com.correx.core.tasks.TaskService
|
import com.correx.core.tasks.TaskService
|
||||||
@@ -82,10 +85,26 @@ class TaskToolsTest {
|
|||||||
assertEquals(TaskStatus.IN_PROGRESS, state.status)
|
assertEquals(TaskStatus.IN_PROGRESS, state.status)
|
||||||
assertEquals("claude-opus", state.claimant)
|
assertEquals("claude-opus", state.claimant)
|
||||||
assertEquals("JWT refresh flow", state.title)
|
assertEquals("JWT refresh flow", state.title)
|
||||||
assertEquals("adr-7", state.links.single().targetId)
|
assertTrue(state.links.any { it.targetId == "adr-7" && it.type == TaskLinkType.IMPLEMENTS })
|
||||||
assertEquals("starting", state.notes.single().body)
|
assertEquals("starting", state.notes.single().body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `task_create and claim auto-link the agent session as CONTEXT`() = runBlocking {
|
||||||
|
val id = createTask() // session s-1
|
||||||
|
val taskId = TaskId(id)
|
||||||
|
|
||||||
|
// Created from s-1 → one CONTEXT/SESSION edge.
|
||||||
|
val afterCreate = service.getTask(taskId)!!.state.links.single()
|
||||||
|
assertEquals("s-1", afterCreate.targetId)
|
||||||
|
assertEquals(TaskTargetKind.SESSION, afterCreate.targetKind)
|
||||||
|
assertEquals(TaskLinkType.CONTEXT, afterCreate.type)
|
||||||
|
|
||||||
|
// Claiming from the same session dedupes to the same single edge.
|
||||||
|
update.execute(request("task_update", mapOf("id" to id, "action" to "claim")))
|
||||||
|
assertEquals(1, service.getTask(taskId)!!.state.links.count { it.targetId == "s-1" })
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `task_update rejects an unknown action`() = runBlocking {
|
fun `task_update rejects an unknown action`() = runBlocking {
|
||||||
val id = createTask()
|
val id = createTask()
|
||||||
|
|||||||
Reference in New Issue
Block a user