Skip to content

Commit 0c4abeb

Browse files
committed
Add task system with file-based CRUD and dependency DAG
1 parent e82c8e4 commit 0c4abeb

8 files changed

Lines changed: 1267 additions & 15 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ tests/results/
4545
.ralphex/progress/
4646
.ralphex/config
4747

48-
# Transcripts
48+
# Agent runtime data
4949
.transcripts/
50+
.tasks/

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Exploring the architecture of coding agents by rebuilding a Claude Code-style CLI from scratch in Swift.
44

5-
> **Current progress:** Stage 06 of 08 — context compaction with 3-layer strategy (micro, auto, manual)
5+
> **Current progress:** Stage 07 of 08 — persistent task system with file-based dependency DAG
66
77
![demo](demo.gif)
88

@@ -85,7 +85,7 @@ The features that make an agent feel like a usable product: context, memory mana
8585
| **04** | Subagents: recursive loop with fresh context | `04-subagents` |
8686
| **05** | Skill loading: `.md` files injected as tool results | `05-skill-loading` |
8787
| **06** | Context compaction: 3-layer strategy (micro, auto, manual) | `06-context-compaction` |
88-
| 07 | Task system: file-based CRUD with dependency DAG | |
88+
| **07** | Task system: file-based CRUD with dependency DAG | `07-task-system` |
8989
| 08 | Background tasks: `Task {}` + actor-based notification queue ||
9090

9191
## Architecture

Sources/Core/Agent.swift

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public enum Limits {
99
}
1010

1111
public final class Agent {
12-
public static let version = "0.6.0"
12+
public static let version = "0.7.0"
1313

1414
private static let todoReminderThreshold = 3
1515

@@ -21,7 +21,8 @@ public final class Agent {
2121
private let shellExecutor: ShellExecutor
2222
private let skillLoader: SkillLoader
2323
private let contextCompactor: ContextCompactor
24-
private let todoManager = TodoManager()
24+
private let todoManager: TodoManager
25+
private let taskManager: TaskManager
2526

2627
private var messages: [Message] = []
2728

@@ -42,6 +43,8 @@ public final class Agent {
4243
transcriptDirectory: "\(workingDirectory)/.transcripts",
4344
tokenThreshold: tokenThreshold
4445
)
46+
self.todoManager = TodoManager()
47+
self.taskManager = TaskManager(directory: "\(workingDirectory)/.tasks")
4548
self.systemPrompt =
4649
systemPrompt
4750
?? Self.buildSystemPrompt(
@@ -153,6 +156,8 @@ public final class Agent {
153156
- Prefer read_file/write_file/edit_file over bash for file operations
154157
- Always check tool results before proceeding
155158
- Use the todo tool to plan multi-step tasks. Mark in_progress before starting, completed when done.
159+
- Use task tools for persistent multi-step work with dependencies. \
160+
Tasks survive context compaction and process restarts.
156161
"""
157162

158163
if !skillDescriptions.isEmpty {
@@ -311,6 +316,76 @@ extension Agent {
311316
]),
312317
"required": .array([])
313318
])
319+
),
320+
ToolDefinition(
321+
name: "task_create",
322+
description: "Create a persistent task. Tasks survive context compaction and process restarts.",
323+
inputSchema: .object([
324+
"type": "object",
325+
"properties": .object([
326+
"subject": .object([
327+
"type": "string",
328+
"description": "Short title for the task"
329+
]),
330+
"description": .object([
331+
"type": "string",
332+
"description": "Detailed description of the task"
333+
])
334+
]),
335+
"required": .array(["subject"])
336+
])
337+
),
338+
ToolDefinition(
339+
name: "task_update",
340+
description: "Update a task's status or dependencies.",
341+
inputSchema: .object([
342+
"type": "object",
343+
"properties": .object([
344+
"task_id": .object([
345+
"type": "integer",
346+
"description": "The task ID to update"
347+
]),
348+
"status": .object([
349+
"type": "string",
350+
"enum": .array(["pending", "in_progress", "completed"]),
351+
"description": "New status for the task"
352+
]),
353+
"add_blocked_by": .object([
354+
"type": "array",
355+
"items": .object(["type": "integer"]),
356+
"description": "Task IDs that block this task"
357+
]),
358+
"add_blocks": .object([
359+
"type": "array",
360+
"items": .object(["type": "integer"]),
361+
"description": "Task IDs that this task blocks"
362+
])
363+
]),
364+
"required": .array(["task_id"])
365+
])
366+
),
367+
ToolDefinition(
368+
name: "task_list",
369+
description: "List all tasks with status markers and dependency info.",
370+
inputSchema: .object([
371+
"type": "object",
372+
"properties": .object([:]),
373+
"required": .array([])
374+
])
375+
),
376+
ToolDefinition(
377+
name: "task_get",
378+
description: "Get detailed info about a specific task.",
379+
inputSchema: .object([
380+
"type": "object",
381+
"properties": .object([
382+
"task_id": .object([
383+
"type": "integer",
384+
"description": "The task ID to retrieve"
385+
])
386+
]),
387+
"required": .array(["task_id"])
388+
])
314389
)
315390
]
316391

@@ -323,7 +398,11 @@ extension Agent {
323398
"todo": executeTodo,
324399
"agent": executeAgent,
325400
"load_skill": executeLoadSkill,
326-
"compact": executeCompact
401+
"compact": executeCompact,
402+
"task_create": executeTaskCreate,
403+
"task_update": executeTaskUpdate,
404+
"task_list": executeTaskList,
405+
"task_get": executeTaskGet
327406
]
328407

329408
guard let handler = handlers[name] else {
@@ -508,6 +587,60 @@ extension Agent {
508587

509588
private func executeCompact(_ input: JSONValue) async -> Result<String, ToolError> { .success("Compressing...") }
510589

590+
private func executeTaskCreate(_ input: JSONValue) async -> Result<String, ToolError> {
591+
guard let subject = input["subject"]?.stringValue else {
592+
return .failure(.missingParameter("subject"))
593+
}
594+
595+
let description = input["description"]?.stringValue ?? ""
596+
597+
do {
598+
let result = try taskManager.create(subject: subject, description: description)
599+
return .success(result)
600+
} catch {
601+
return .failure(.executionFailed("\(error)"))
602+
}
603+
}
604+
605+
private func executeTaskUpdate(_ input: JSONValue) async -> Result<String, ToolError> {
606+
guard let taskId = input["task_id"]?.intValue else {
607+
return .failure(.missingParameter("task_id"))
608+
}
609+
610+
let status = input["status"]?.stringValue
611+
let addBlockedBy = input["add_blocked_by"]?.arrayValue?.compactMap(\.intValue) ?? []
612+
let addBlocks = input["add_blocks"]?.arrayValue?.compactMap(\.intValue) ?? []
613+
614+
do {
615+
let result = try taskManager.update(
616+
taskId: taskId,
617+
status: status,
618+
addBlockedBy: addBlockedBy,
619+
addBlocks: addBlocks
620+
)
621+
return .success(result)
622+
} catch {
623+
return .failure(.executionFailed("\(error)"))
624+
}
625+
}
626+
627+
private func executeTaskList(_ input: JSONValue) async -> Result<String, ToolError> {
628+
.success(taskManager.listAll())
629+
}
630+
631+
private func executeTaskGet(_ input: JSONValue) async -> Result<String, ToolError> {
632+
guard let taskId = input["task_id"]?.intValue else {
633+
return .failure(.missingParameter("task_id"))
634+
}
635+
636+
do {
637+
let result = try taskManager.get(taskId: taskId)
638+
return .success(result)
639+
} catch {
640+
return .failure(.executionFailed("\(error)"))
641+
}
642+
}
643+
511644
// MARK: Helpers
512645

513646
struct ToolProcessingResult {
@@ -611,7 +744,7 @@ extension Agent {
611744

612745
static let subagent = LoopConfig(
613746
tools: Agent.toolDefinitions.filter {
614-
!Set(["agent", "todo", "compact"]).contains($0.name)
747+
!Set(["agent", "todo", "compact", "task_create", "task_update"]).contains($0.name)
615748
},
616749
maxIterations: 30,
617750
enableNag: false,

0 commit comments

Comments
 (0)