klaus-agent 0.3.0 → 0.4.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.
@@ -25,7 +25,7 @@ import type { CompactionConfig } from "../compaction/types.js";
25
25
  import type { PlanningManager } from "../planning/planning-manager.js";
26
26
  import { PLANNING_TOOL_NAMES } from "../planning/types.js";
27
27
  import { executeToolCalls, type ToolCallResult } from "../tools/executor.js";
28
- import { estimateTokens, shouldCompact, findCutPoint } from "../compaction/compaction.js";
28
+ import { estimateTokens, shouldCompact, findCutPoint, microCompact } from "../compaction/compaction.js";
29
29
  import { normalizeHistory } from "../injection/history-normalizer.js";
30
30
  import { calculateCost } from "../providers/shared.js";
31
31
 
@@ -109,6 +109,7 @@ export async function runAgentLoop(
109
109
 
110
110
  // Mutable copies of config values that before_agent_start can override
111
111
  let { modelId, systemPrompt, thinkingLevel } = config;
112
+ const keepRecentToolResults = compaction?.keepRecentToolResults ?? 3;
112
113
 
113
114
  // --- before_agent_start: let extensions modify agent config ---
114
115
  if (extensionRunner) {
@@ -251,6 +252,9 @@ export async function runAgentLoop(
251
252
  llmMessages = stripImages(llmMessages);
252
253
  }
253
254
 
255
+ // --- Micro compaction: shorten old tool results ---
256
+ llmMessages = microCompact(llmMessages, keepRecentToolResults);
257
+
254
258
  // --- Phase-aware tool filtering ---
255
259
  let visibleTools = allTools;
256
260
  if (config.planningManager?.phase === "planning" && config.planningManager.allowedInPlanning.size > 2) {
package/src/core/agent.ts CHANGED
@@ -22,6 +22,7 @@ import type { SkillSource } from "../skills/types.js";
22
22
  import type { MCPServerConfig, MCPClient } from "../tools/mcp-adapter.js";
23
23
  import type { TaskFactory } from "../background/types.js";
24
24
  import type { PlanningConfig } from "../planning/types.js";
25
+ import type { TaskGraphConfig } from "../task-graph/types.js";
25
26
  import { SessionManager } from "../session/session-manager.js";
26
27
  import { CheckpointManager } from "../checkpoint/checkpoint-manager.js";
27
28
  import { InjectionManager } from "../injection/injection-manager.js";
@@ -39,6 +40,9 @@ import { createBackgroundTaskTools } from "../background/tools.js";
39
40
  import { PlanningManager } from "../planning/planning-manager.js";
40
41
  import { createPlanningTools } from "../planning/tools.js";
41
42
  import { PlanningNagProvider } from "../planning/nag-injection.js";
43
+ import { TaskGraph } from "../task-graph/task-graph.js";
44
+ import { createTaskGraphTools } from "../task-graph/tools.js";
45
+ import { TaskResultInjectionProvider } from "../task-graph/result-injection.js";
42
46
  import { runAgentLoop } from "./agent-loop.js";
43
47
 
44
48
  export interface AgentConfig {
@@ -64,6 +68,7 @@ export interface AgentConfig {
64
68
  mcp?: { servers: MCPServerConfig[]; clientFactory: (config: MCPServerConfig) => MCPClient };
65
69
  wire?: { bufferSize?: number };
66
70
  backgroundTasks?: { factories?: Record<string, TaskFactory> };
71
+ taskGraph?: TaskGraphConfig;
67
72
  planning?: PlanningConfig;
68
73
  }
69
74
 
@@ -89,6 +94,7 @@ export class Agent {
89
94
  private _wire: Wire;
90
95
  private _backgroundTaskManager: BackgroundTaskManager | undefined;
91
96
  private _planningManager: PlanningManager | undefined;
97
+ private _taskGraph: TaskGraph;
92
98
  private _initialized = false;
93
99
 
94
100
  constructor(config: AgentConfig) {
@@ -156,6 +162,9 @@ export class Agent {
156
162
  if (config.planning) {
157
163
  this._planningManager = new PlanningManager(config.planning);
158
164
  }
165
+
166
+ // Task graph
167
+ this._taskGraph = new TaskGraph(config.taskGraph ?? {});
159
168
  }
160
169
 
161
170
  // --- Public API ---
@@ -253,6 +262,10 @@ export class Agent {
253
262
  return this._planningManager;
254
263
  }
255
264
 
265
+ get taskGraph(): TaskGraph {
266
+ return this._taskGraph;
267
+ }
268
+
256
269
  setSystemPrompt(prompt: string): void {
257
270
  this._state.systemPrompt = prompt;
258
271
  }
@@ -282,6 +295,7 @@ export class Agent {
282
295
  this._followUpQueue = [];
283
296
  await this._mcpAdapter?.dispose();
284
297
  this._backgroundTaskManager?.dispose();
298
+ this._taskGraph.dispose();
285
299
  this._wire.dispose();
286
300
  }
287
301
 
@@ -373,12 +387,21 @@ export class Agent {
373
387
  this._state.tools = [...this._state.tools, ...createPlanningTools(this._planningManager)];
374
388
 
375
389
  // Register nag provider into injection manager (create one if needed)
376
- const nagProvider = new PlanningNagProvider(this._planningManager);
377
- if (this._injectionManager) {
378
- this._injectionManager.addProvider(nagProvider);
379
- } else {
380
- this._injectionManager = new InjectionManager([nagProvider]);
381
- }
390
+ this._addInjectionProvider(new PlanningNagProvider(this._planningManager));
391
+ }
392
+
393
+ // Task graph tools + result auto-injection
394
+ this._state.tools = [...this._state.tools, ...createTaskGraphTools(this._taskGraph)];
395
+ if (this._config.taskGraph?.autoInjectResults !== false) {
396
+ this._addInjectionProvider(new TaskResultInjectionProvider(this._taskGraph));
397
+ }
398
+ }
399
+
400
+ private _addInjectionProvider(provider: DynamicInjectionProvider): void {
401
+ if (this._injectionManager) {
402
+ this._injectionManager.addProvider(provider);
403
+ } else {
404
+ this._injectionManager = new InjectionManager([provider]);
382
405
  }
383
406
  }
384
407
 
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import type { SkillSource } from "./skills/types.js";
16
16
  import type { MCPServerConfig, MCPClient } from "./tools/mcp-adapter.js";
17
17
  import type { TaskFactory } from "./background/types.js";
18
18
  import type { PlanningConfig } from "./planning/types.js";
19
+ import type { TaskGraphConfig } from "./task-graph/types.js";
19
20
 
20
21
  export interface CreateAgentConfig {
21
22
  // Required
@@ -43,6 +44,7 @@ export interface CreateAgentConfig {
43
44
  wire?: { bufferSize?: number };
44
45
  backgroundTasks?: { factories?: Record<string, TaskFactory> };
45
46
  planning?: PlanningConfig;
47
+ taskGraph?: TaskGraphConfig;
46
48
 
47
49
  // Advanced: provide your own LLM provider
48
50
  provider?: LLMProvider;
@@ -74,6 +76,7 @@ export function createAgent(config: CreateAgentConfig): Agent {
74
76
  wire: config.wire,
75
77
  backgroundTasks: config.backgroundTasks,
76
78
  planning: config.planning,
79
+ taskGraph: config.taskGraph,
77
80
  });
78
81
  }
79
82
 
@@ -98,7 +101,7 @@ export { ExtensionRunner } from "./extensions/runner.js";
98
101
  export { discoverSkills } from "./skills/discovery.js";
99
102
  export { loadSkill, renderSkillTemplate } from "./skills/loader.js";
100
103
  export { MCPAdapter } from "./tools/mcp-adapter.js";
101
- export { estimateTokens, shouldCompact, findCutPoint } from "./compaction/compaction.js";
104
+ export { estimateTokens, shouldCompact, findCutPoint, microCompact } from "./compaction/compaction.js";
102
105
  export { calculateCost } from "./providers/shared.js";
103
106
  export { LLMSummarizer } from "./compaction/summarizer.js";
104
107
  export { Wire } from "./wire/wire.js";
@@ -107,6 +110,9 @@ export { createBackgroundTaskTools } from "./background/tools.js";
107
110
  export { PlanningManager } from "./planning/planning-manager.js";
108
111
  export { createPlanningTools } from "./planning/tools.js";
109
112
  export { PlanningNagProvider } from "./planning/nag-injection.js";
113
+ export { TaskGraph } from "./task-graph/task-graph.js";
114
+ export { createTaskGraphTools } from "./task-graph/tools.js";
115
+ export { TaskResultInjectionProvider } from "./task-graph/result-injection.js";
110
116
 
111
117
  // Core types
112
118
  export type {
@@ -243,3 +249,12 @@ export type {
243
249
  } from "./planning/types.js";
244
250
 
245
251
  export { PLANNING_TOOL_NAMES } from "./planning/types.js";
252
+ export { TASK_GRAPH_TOOL_NAMES } from "./task-graph/types.js";
253
+
254
+ // Task graph types
255
+ export type {
256
+ TaskGraphConfig,
257
+ TaskNode,
258
+ TaskStatus,
259
+ CompletedTaskResult,
260
+ } from "./task-graph/types.js";
@@ -0,0 +1,29 @@
1
+ // Auto-inject completed background task results before each LLM call
2
+
3
+ import type { DynamicInjectionProvider, DynamicInjection } from "../injection/types.js";
4
+ import type { AgentMessage } from "../types.js";
5
+ import type { TaskGraph } from "./task-graph.js";
6
+
7
+ export class TaskResultInjectionProvider implements DynamicInjectionProvider {
8
+ constructor(private _graph: TaskGraph) {}
9
+
10
+ async getInjections(_history: AgentMessage[]): Promise<DynamicInjection[]> {
11
+ const completed = this._graph.drainCompleted();
12
+ if (completed.length === 0) return [];
13
+
14
+ const lines = completed.map((c) => {
15
+ const status = c.status === "completed" ? "completed" : "FAILED";
16
+ const unblocked = c.unblockedTasks.length > 0
17
+ ? ` → unblocked: ${c.unblockedTasks.join(", ")}`
18
+ : "";
19
+ return `[task:${c.taskId}] ${c.subject} — ${status}: ${c.result}${unblocked}`;
20
+ });
21
+
22
+ return [
23
+ {
24
+ type: "task-results",
25
+ content: `<background-results>\n${lines.join("\n")}\n</background-results>`,
26
+ },
27
+ ];
28
+ }
29
+ }
@@ -0,0 +1,270 @@
1
+ // Task graph — dependency-aware DAG with background execution and auto-unlock
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "fs";
4
+ import { join } from "path";
5
+ import { generateId } from "../utils/id.js";
6
+ import type { TaskNode, TaskStatus, TaskGraphConfig, CompletedTaskResult } from "./types.js";
7
+
8
+ export class TaskGraph {
9
+ private _tasks = new Map<string, TaskNode>();
10
+ private _config: TaskGraphConfig;
11
+ private _completedQueue: CompletedTaskResult[] = [];
12
+ private _backgroundAborts = new Map<string, AbortController>();
13
+
14
+ constructor(config: TaskGraphConfig = {}) {
15
+ this._config = config;
16
+ if (config.persistDir) {
17
+ mkdirSync(config.persistDir, { recursive: true });
18
+ this._loadFromDisk();
19
+ }
20
+ }
21
+
22
+
23
+ get(id: string): TaskNode | undefined {
24
+ const task = this._tasks.get(id);
25
+ return task ? { ...task } : undefined;
26
+ }
27
+
28
+ listAll(): TaskNode[] {
29
+ return [...this._tasks.values()];
30
+ }
31
+
32
+ /** Tasks that are pending with no unfinished blockers. */
33
+ listReady(): TaskNode[] {
34
+ return this.listAll().filter(
35
+ (t) => t.status === "pending" && t.blockedBy.length === 0,
36
+ );
37
+ }
38
+
39
+ /** Tasks waiting on unfinished blockers. */
40
+ listBlocked(): TaskNode[] {
41
+ return this.listAll().filter(
42
+ (t) => t.status === "pending" && t.blockedBy.length > 0,
43
+ );
44
+ }
45
+
46
+
47
+ create(subject: string, description = ""): TaskNode {
48
+ const max = this._config.maxTasks ?? 100;
49
+ if (this._tasks.size >= max) {
50
+ throw new Error(`Task limit reached: ${max}.`);
51
+ }
52
+
53
+ const node: TaskNode = {
54
+ id: generateId(),
55
+ subject,
56
+ description,
57
+ status: "pending",
58
+ blockedBy: [],
59
+ blocks: [],
60
+ owner: "",
61
+ createdAt: Date.now(),
62
+ updatedAt: Date.now(),
63
+ };
64
+ this._tasks.set(node.id, node);
65
+ this._persist();
66
+ return { ...node };
67
+ }
68
+
69
+ /** Add a dependency: `taskId` is blocked by `blockedById`. */
70
+ addDependency(taskId: string, blockedById: string): void {
71
+ const task = this._require(taskId);
72
+ const blocker = this._require(blockedById);
73
+
74
+ if (task.blockedBy.includes(blockedById)) return;
75
+
76
+ // Cycle detection: if blocker is (transitively) blocked by task, adding this edge creates a cycle
77
+ if (this._isTransitivelyBlockedBy(blockedById, taskId)) {
78
+ throw new Error(`Adding dependency ${blockedById} → ${taskId} would create a cycle.`);
79
+ }
80
+
81
+ task.blockedBy.push(blockedById);
82
+ blocker.blocks.push(taskId);
83
+ task.updatedAt = Date.now();
84
+ blocker.updatedAt = Date.now();
85
+ this._persist();
86
+ }
87
+
88
+ update(taskId: string, fields: { status?: TaskStatus; owner?: string; result?: string }): TaskNode {
89
+ const task = this._require(taskId);
90
+
91
+ if (fields.status !== undefined && fields.status !== task.status) {
92
+ if (fields.status === "in_progress" && task.blockedBy.length > 0) {
93
+ throw new Error(`Task ${taskId} is blocked by: ${task.blockedBy.join(", ")}`);
94
+ }
95
+ task.status = fields.status;
96
+
97
+ if (fields.status === "completed" || fields.status === "failed") {
98
+ task.result = fields.result ?? task.result;
99
+ const unblocked = this._clearDependency(taskId);
100
+
101
+ this._completedQueue.push({
102
+ taskId,
103
+ subject: task.subject,
104
+ result: task.result ?? "",
105
+ status: fields.status,
106
+ unblockedTasks: unblocked,
107
+ });
108
+ }
109
+ }
110
+
111
+ if (fields.owner !== undefined) task.owner = fields.owner;
112
+ // result is already set inside the completion branch above; only apply here for non-completion updates
113
+ if (fields.result !== undefined && task.status !== "completed" && task.status !== "failed") {
114
+ task.result = fields.result;
115
+ }
116
+ task.updatedAt = Date.now();
117
+ this._persist();
118
+ return { ...task };
119
+ }
120
+
121
+
122
+ /**
123
+ * Run an async function in the background for a task.
124
+ * Auto-updates task status to in_progress/completed/failed.
125
+ */
126
+ runBackground(
127
+ taskId: string,
128
+ fn: (signal: AbortSignal) => Promise<string>,
129
+ ): void {
130
+ const task = this._require(taskId);
131
+ if (task.blockedBy.length > 0) {
132
+ throw new Error(`Task ${taskId} is blocked by: ${task.blockedBy.join(", ")}`);
133
+ }
134
+
135
+ const ac = new AbortController();
136
+ const bgId = generateId();
137
+ task.status = "in_progress";
138
+ task.backgroundId = bgId;
139
+ task.updatedAt = Date.now();
140
+ this._backgroundAborts.set(bgId, ac);
141
+ this._persist();
142
+
143
+ fn(ac.signal).then(
144
+ (result) => {
145
+ this._backgroundAborts.delete(bgId);
146
+ try { this.update(taskId, { status: "completed", result }); } catch { /* task may have been removed */ }
147
+ },
148
+ (err) => {
149
+ this._backgroundAborts.delete(bgId);
150
+ try {
151
+ this.update(taskId, {
152
+ status: "failed",
153
+ result: err instanceof Error ? err.message : String(err),
154
+ });
155
+ } catch { /* task may have been removed */ }
156
+ },
157
+ );
158
+ }
159
+
160
+ abortBackground(taskId: string): boolean {
161
+ const task = this._tasks.get(taskId);
162
+ if (!task?.backgroundId) return false;
163
+ const ac = this._backgroundAborts.get(task.backgroundId);
164
+ if (!ac) return false;
165
+ ac.abort();
166
+ return true;
167
+ }
168
+
169
+
170
+ drainCompleted(): CompletedTaskResult[] {
171
+ const results = [...this._completedQueue];
172
+ this._completedQueue = [];
173
+ return results;
174
+ }
175
+
176
+
177
+ render(): string {
178
+ const tasks = this.listAll();
179
+ if (tasks.length === 0) return "No tasks.";
180
+
181
+ const lines = tasks.map((t) => {
182
+ const icon = t.status === "completed" ? "[x]"
183
+ : t.status === "failed" ? "[!]"
184
+ : t.status === "in_progress" ? "[>]"
185
+ : t.blockedBy.length > 0 ? "[~]"
186
+ : "[ ]";
187
+ const deps = t.blockedBy.length > 0 ? ` (blocked by: ${t.blockedBy.join(", ")})` : "";
188
+ return `${icon} ${t.id}: ${t.subject}${deps}`;
189
+ });
190
+
191
+ const done = tasks.filter((t) => t.status === "completed").length;
192
+ const ready = tasks.filter((t) => t.status === "pending" && t.blockedBy.length === 0).length;
193
+ return `Tasks: ${done}/${tasks.length} done, ${ready} ready\n${lines.join("\n")}`;
194
+ }
195
+
196
+ dispose(): void {
197
+ for (const ac of this._backgroundAborts.values()) {
198
+ ac.abort();
199
+ }
200
+ this._backgroundAborts.clear();
201
+ }
202
+
203
+
204
+ private _require(id: string): TaskNode {
205
+ const task = this._tasks.get(id);
206
+ if (!task) throw new Error(`Task not found: ${id}`);
207
+ return task;
208
+ }
209
+
210
+ /** Remove completedId from all tasks' blockedBy. Returns IDs of newly unblocked tasks. */
211
+ private _clearDependency(completedId: string): string[] {
212
+ const completed = this._tasks.get(completedId);
213
+ if (!completed) return [];
214
+
215
+ const unblocked: string[] = [];
216
+ for (const dependentId of completed.blocks) {
217
+ const task = this._tasks.get(dependentId);
218
+ if (!task) continue;
219
+ const idx = task.blockedBy.indexOf(completedId);
220
+ if (idx !== -1) {
221
+ task.blockedBy.splice(idx, 1);
222
+ if (task.blockedBy.length === 0 && task.status === "pending") {
223
+ unblocked.push(task.id);
224
+ }
225
+ }
226
+ }
227
+ return unblocked;
228
+ }
229
+
230
+ /** Check if `taskId` is transitively blocked by `targetId`. */
231
+ private _isTransitivelyBlockedBy(taskId: string, targetId: string): boolean {
232
+ const visited = new Set<string>();
233
+ const stack = [taskId];
234
+ while (stack.length > 0) {
235
+ const current = stack.pop()!;
236
+ if (current === targetId) return true;
237
+ if (visited.has(current)) continue;
238
+ visited.add(current);
239
+ const node = this._tasks.get(current);
240
+ if (node) stack.push(...node.blockedBy);
241
+ }
242
+ return false;
243
+ }
244
+
245
+ private _persist(): void {
246
+ if (!this._config.persistDir) return;
247
+ const data = JSON.stringify([...this._tasks.values()], null, 2);
248
+ const target = join(this._config.persistDir, "tasks.json");
249
+ const tmp = target + ".tmp";
250
+ writeFileSync(tmp, data, "utf-8");
251
+ renameSync(tmp, target);
252
+ }
253
+
254
+ private _loadFromDisk(): void {
255
+ if (!this._config.persistDir) return;
256
+ const filePath = join(this._config.persistDir, "tasks.json");
257
+ if (!existsSync(filePath)) return;
258
+ try {
259
+ const raw = JSON.parse(readFileSync(filePath, "utf-8"));
260
+ if (!Array.isArray(raw)) return;
261
+ for (const entry of raw) {
262
+ if (entry && typeof entry === "object" && typeof entry.id === "string" && typeof entry.subject === "string") {
263
+ this._tasks.set(entry.id, entry as TaskNode);
264
+ }
265
+ }
266
+ } catch {
267
+ // Corrupted file — start fresh
268
+ }
269
+ }
270
+ }
@@ -0,0 +1,109 @@
1
+ // Task graph tools — CRUD + dependency management + background execution
2
+
3
+ import { Type } from "@sinclair/typebox";
4
+ import type { AgentTool, AgentToolResult } from "../tools/types.js";
5
+ import type { TaskGraph } from "./task-graph.js";
6
+ import { TASK_GRAPH_TOOL_NAMES } from "./types.js";
7
+ import type { TaskStatus, TaskNode } from "./types.js";
8
+
9
+ export function createTaskGraphTools(graph: TaskGraph): AgentTool[] {
10
+ return [
11
+ {
12
+ name: TASK_GRAPH_TOOL_NAMES.create,
13
+ label: "Create Task",
14
+ description:
15
+ "Create a new task in the task graph. Tasks start as pending. " +
16
+ "Use task_depend to set up dependency ordering.",
17
+ parameters: Type.Object({
18
+ subject: Type.String({ description: "Short title for the task." }),
19
+ description: Type.Optional(Type.String({ description: "Detailed description." })),
20
+ }),
21
+ async execute(_id, params: { subject: string; description?: string }): Promise<AgentToolResult> {
22
+ const task = graph.create(params.subject, params.description);
23
+ return text(`Created task ${task.id}: ${task.subject}\n\n${graph.render()}`);
24
+ },
25
+ },
26
+ {
27
+ name: TASK_GRAPH_TOOL_NAMES.depend,
28
+ label: "Add Task Dependency",
29
+ description:
30
+ "Add a dependency: task_id cannot start until blocked_by_id completes. " +
31
+ "Rejects if this would create a cycle.",
32
+ parameters: Type.Object({
33
+ task_id: Type.String({ description: "Task that is blocked." }),
34
+ blocked_by_id: Type.String({ description: "Task that must complete first." }),
35
+ }),
36
+ async execute(_id, params: { task_id: string; blocked_by_id: string }): Promise<AgentToolResult> {
37
+ graph.addDependency(params.task_id, params.blocked_by_id);
38
+ return text(`Dependency added: ${params.task_id} blocked by ${params.blocked_by_id}\n\n${graph.render()}`);
39
+ },
40
+ },
41
+ {
42
+ name: TASK_GRAPH_TOOL_NAMES.update,
43
+ label: "Update Task",
44
+ description:
45
+ "Update a task's status, owner, or result. " +
46
+ "Setting status to 'completed' auto-unblocks dependent tasks. " +
47
+ "Cannot start a task that has unfinished blockers.",
48
+ parameters: Type.Object({
49
+ task_id: Type.String({ description: "Task ID." }),
50
+ status: Type.Optional(Type.Union(
51
+ [Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed"), Type.Literal("failed")],
52
+ { description: "New status." },
53
+ )),
54
+ owner: Type.Optional(Type.String({ description: "Assign to an agent or user." })),
55
+ result: Type.Optional(Type.String({ description: "Result summary." })),
56
+ }),
57
+ async execute(_id, params: { task_id: string; status?: string; owner?: string; result?: string }): Promise<AgentToolResult> {
58
+ const task = graph.update(params.task_id, {
59
+ status: params.status as TaskStatus | undefined,
60
+ owner: params.owner,
61
+ result: params.result,
62
+ });
63
+ return text(`Updated task ${task.id}\n\n${graph.render()}`);
64
+ },
65
+ },
66
+ {
67
+ name: TASK_GRAPH_TOOL_NAMES.list,
68
+ label: "List Tasks",
69
+ description: "List all tasks with their status, dependencies, and progress.",
70
+ parameters: Type.Object({
71
+ filter: Type.Optional(Type.Union(
72
+ [Type.Literal("all"), Type.Literal("ready"), Type.Literal("blocked"), Type.Literal("in_progress"), Type.Literal("completed")],
73
+ { description: "Filter tasks by category. Default: all." },
74
+ )),
75
+ }),
76
+ async execute(_id, params: { filter?: string }): Promise<AgentToolResult> {
77
+ let tasks: TaskNode[];
78
+ switch (params.filter) {
79
+ case "ready": tasks = graph.listReady(); break;
80
+ case "blocked": tasks = graph.listBlocked(); break;
81
+ case "in_progress": tasks = graph.listAll().filter((t) => t.status === "in_progress"); break;
82
+ case "completed": tasks = graph.listAll().filter((t) => t.status === "completed"); break;
83
+ default: tasks = graph.listAll();
84
+ }
85
+ if (tasks.length === 0) return text(`No tasks matching filter: ${params.filter ?? "all"}`);
86
+ if (!params.filter || params.filter === "all") return text(graph.render());
87
+ const lines = tasks.map((t) => `${t.id}: ${t.subject} [${t.status}]`);
88
+ return text(`${params.filter}: ${tasks.length} task(s)\n${lines.join("\n")}`);
89
+ },
90
+ },
91
+ {
92
+ name: TASK_GRAPH_TOOL_NAMES.get,
93
+ label: "Get Task",
94
+ description: "Get detailed information about a specific task.",
95
+ parameters: Type.Object({
96
+ task_id: Type.String({ description: "Task ID." }),
97
+ }),
98
+ async execute(_id, params: { task_id: string }): Promise<AgentToolResult> {
99
+ const task = graph.get(params.task_id);
100
+ if (!task) return text(`Task not found: ${params.task_id}`);
101
+ return text(JSON.stringify(task, null, 2));
102
+ },
103
+ },
104
+ ];
105
+ }
106
+
107
+ function text(t: string): AgentToolResult {
108
+ return { content: [{ type: "text", text: t }] };
109
+ }
@@ -0,0 +1,52 @@
1
+ // Task graph types — dependency-aware task DAG with background execution
2
+
3
+ export const TASK_GRAPH_TOOL_NAMES = {
4
+ create: "task_create",
5
+ depend: "task_depend",
6
+ update: "task_update",
7
+ list: "task_list",
8
+ get: "task_get",
9
+ } as const;
10
+
11
+ export type TaskStatus = "pending" | "in_progress" | "completed" | "failed";
12
+
13
+ export interface TaskNode {
14
+ id: string;
15
+ subject: string;
16
+ description: string;
17
+ status: TaskStatus;
18
+ /** IDs of tasks that must complete before this one can start. */
19
+ blockedBy: string[];
20
+ /** IDs of tasks that this task blocks (reverse edges, maintained automatically). */
21
+ blocks: string[];
22
+ /** Agent or user assigned to this task. */
23
+ owner: string;
24
+ /** Result summary after completion/failure. */
25
+ result?: string;
26
+ /** Background execution handle ID, if running in background. */
27
+ backgroundId?: string;
28
+ createdAt: number;
29
+ updatedAt: number;
30
+ }
31
+
32
+ export interface TaskGraphConfig {
33
+ /** Directory for persisting task graph to disk. If omitted, in-memory only. */
34
+ persistDir?: string;
35
+
36
+ /** Maximum number of tasks. Default: 100. */
37
+ maxTasks?: number;
38
+
39
+ /**
40
+ * Auto-inject completed background task results before each LLM call.
41
+ * Default: true.
42
+ */
43
+ autoInjectResults?: boolean;
44
+ }
45
+
46
+ export interface CompletedTaskResult {
47
+ taskId: string;
48
+ subject: string;
49
+ result: string;
50
+ status: "completed" | "failed";
51
+ unblockedTasks: string[];
52
+ }