src / tools / taskTool.ts
src / tools / taskTool.ts
import { tool, type Tool } from "@lmstudio/sdk";
import { z } from "zod";
import { AgenticError } from "../core/errors";
import { errorResult, okResult } from "../core/result";
import type { PluginRuntime } from "../runtime";
import type {
NewTaskItem,
TaskBoard,
TaskItemStatus,
TaskPriority,
} from "../tasks/taskStore";
const statusSchema = z.enum([
"pending",
"in_progress",
"completed",
"blocked",
"canceled",
]);
const prioritySchema = z.enum(["low", "normal", "high", "critical"]);
const newItemSchema = z.object({
text: z.string(),
status: statusSchema.optional(),
priority: prioritySchema.optional(),
details: z.string().optional(),
depends_on: z.array(z.string()).optional(),
});
function boardData(runtime: PluginRuntime, board: TaskBoard) {
const counts = {
pending: 0,
in_progress: 0,
completed: 0,
blocked: 0,
canceled: 0,
};
for (const item of board.items) counts[item.status]++;
return {
id: board.id,
title: board.title,
objective: board.objective,
status: board.status,
counts,
items: board.items,
notes: board.notes,
decisions: board.decisions,
checkpoints: board.checkpoints,
nextActionable: runtime.tasks.nextActionable(board).slice(0, 20),
createdAt: board.createdAt,
updatedAt: board.updatedAt,
completedAt: board.completedAt,
archivedAt: board.archivedAt,
statePath: runtime.tasks.stateRelativePath(board.id),
};
}
function summaryData(runtime: PluginRuntime, board: TaskBoard) {
const data = boardData(runtime, board);
return {
id: data.id,
title: data.title,
objective: data.objective?.slice(0, 500),
status: data.status,
counts: data.counts,
nextActionable: data.nextActionable.slice(0, 5).map((item) => ({
id: item.id,
text: item.text,
status: item.status,
priority: item.priority,
})),
updatedAt: data.updatedAt,
statePath: data.statePath,
};
}
function boardFacts(board: TaskBoard): string[] {
const outstanding = board.items.filter(
(item) => item.status !== "completed" && item.status !== "canceled",
);
return [
`task board ${board.id} ${board.status}: ${board.title}`,
...outstanding.slice(0, 20).map((item) => `${item.id} ${item.status}: ${item.text}`),
];
}
export function createTaskTool(runtime: PluginRuntime): Tool {
return tool({
name: "workspace_tasks",
description:
"Durable To-Do boards under .agentic/tasks. Actions: create, list, show, add, update, remove, note, checkpoint, next, archive, reopen. For any multi-step job create a board first, then update each item with evidence and checkpoint after every verified step, so progress survives context compression. depends_on: item_ ids or 1-based positions in the same create/add call. No approval needed.",
parameters: {
action: z.enum([
"create",
"list",
"show",
"add",
"update",
"remove",
"note",
"checkpoint",
"next",
"archive",
"reopen",
]),
board_id: z.string().optional(),
title: z.string().optional(),
objective: z.string().optional(),
items: z.array(newItemSchema).optional(),
item_id: z.string().optional(),
text: z.string().optional(),
status: statusSchema.optional(),
priority: prioritySchema.optional(),
details: z.string().optional(),
depends_on: z.array(z.string()).optional(),
evidence: z.array(z.string()).optional(),
note: z.string().optional(),
decision: z.string().optional(),
summary: z.string().optional(),
decisions: z.array(z.string()).optional(),
blockers: z.array(z.string()).optional(),
next: z.array(z.string()).optional(),
include_archived: z.boolean().optional(),
limit: z.number().int().min(1).max(100).optional(),
},
implementation: async (input: {
action:
| "create"
| "list"
| "show"
| "add"
| "update"
| "remove"
| "note"
| "checkpoint"
| "next"
| "archive"
| "reopen";
board_id?: string;
title?: string;
objective?: string;
items?: Array<{
text: string;
status?: TaskItemStatus;
priority?: TaskPriority;
details?: string;
depends_on?: string[];
}>;
item_id?: string;
text?: string;
status?: TaskItemStatus;
priority?: TaskPriority;
details?: string;
depends_on?: string[];
evidence?: string[];
note?: string;
decision?: string;
summary?: string;
decisions?: string[];
blockers?: string[];
next?: string[];
include_archived?: boolean;
limit?: number;
}) => {
try {
if (input.action === "create") {
if (!input.title) {
throw new AgenticError("INVALID_INPUT", "workspace_tasks create requires title.");
}
const items: NewTaskItem[] = (input.items ?? []).map((item) => ({
text: item.text,
status: item.status,
priority: item.priority,
details: item.details,
dependsOn: item.depends_on,
}));
const board = await runtime.tasks.create({
title: input.title,
objective: input.objective,
items,
});
return okResult({
operation: "tasks.create",
summary: `Created task board ${board.id} with ${board.items.length} item(s).`,
data: boardData(runtime, board),
importance: "critical",
facts: boardFacts(board),
omit: ["data.notes", "data.decisions", "data.checkpoints"],
});
}
if (input.action === "list") {
const boards = await runtime.tasks.list(
input.limit ?? 30,
input.include_archived ?? false,
);
return okResult({
operation: "tasks.list",
summary: `Listed ${boards.length} task board(s).`,
data: boards.map((board) => summaryData(runtime, board)),
importance: "high",
facts: boards.slice(0, 20).map((board) => `task board ${board.id} ${board.status}: ${board.title}`),
});
}
if (!input.board_id) {
throw new AgenticError(
"INVALID_INPUT",
`workspace_tasks ${input.action} requires board_id.`,
);
}
if (input.action === "show" || input.action === "next") {
const board = await runtime.tasks.load(input.board_id);
const data = boardData(runtime, board);
return okResult({
operation: `tasks.${input.action}`,
summary:
input.action === "next"
? `${data.nextActionable.length} actionable item(s) on ${board.id}.`
: `${board.id}: ${board.title} (${board.status}).`,
data:
input.action === "next"
? {
board: summaryData(runtime, board),
items: data.nextActionable,
}
: data,
importance: "high",
facts: boardFacts(board),
omit:
input.action === "show"
? ["data.notes", "data.decisions", "data.checkpoints"]
: undefined,
});
}
let board: TaskBoard;
switch (input.action) {
case "add": {
const items: NewTaskItem[] = (input.items ?? []).map((item) => ({
text: item.text,
status: item.status,
priority: item.priority,
details: item.details,
dependsOn: item.depends_on,
}));
board = await runtime.tasks.addItems(input.board_id, items);
break;
}
case "update":
if (!input.item_id) {
throw new AgenticError("INVALID_INPUT", "workspace_tasks update requires item_id.");
}
board = await runtime.tasks.updateItem(input.board_id, input.item_id, {
text: input.text,
status: input.status,
priority: input.priority,
details: input.details,
dependsOn: input.depends_on,
evidence: input.evidence,
});
break;
case "remove":
if (!input.item_id) {
throw new AgenticError("INVALID_INPUT", "workspace_tasks remove requires item_id.");
}
board = await runtime.tasks.removeItem(input.board_id, input.item_id);
break;
case "note":
board = await runtime.tasks.addNote(input.board_id, {
note: input.note,
decision: input.decision,
});
break;
case "checkpoint":
if (!input.summary) {
throw new AgenticError(
"INVALID_INPUT",
"workspace_tasks checkpoint requires summary.",
);
}
board = await runtime.tasks.checkpoint(input.board_id, {
summary: input.summary,
decisions: input.decisions,
blockers: input.blockers,
next: input.next,
});
break;
case "archive":
board = await runtime.tasks.archive(input.board_id);
break;
case "reopen":
board = await runtime.tasks.reopen(input.board_id);
break;
default:
throw new AgenticError("INVALID_INPUT", `Unsupported task action: ${input.action}`);
}
return okResult({
operation: `tasks.${input.action}`,
summary: `${board.id} updated; ${runtime.tasks.nextActionable(board).length} actionable item(s) remain.`,
data: boardData(runtime, board),
importance: "critical",
facts: boardFacts(board),
omit: ["data.notes", "data.decisions", "data.checkpoints"],
});
} catch (error) {
return errorResult(`tasks.${input.action}`, error, "high");
}
},
});
}
import { tool, type Tool } from "@lmstudio/sdk";
import { z } from "zod";
import { AgenticError } from "../core/errors";
import { errorResult, okResult } from "../core/result";
import type { PluginRuntime } from "../runtime";
import type {
NewTaskItem,
TaskBoard,
TaskItemStatus,
TaskPriority,
} from "../tasks/taskStore";
const statusSchema = z.enum([
"pending",
"in_progress",
"completed",
"blocked",
"canceled",
]);
const prioritySchema = z.enum(["low", "normal", "high", "critical"]);
const newItemSchema = z.object({
text: z.string(),
status: statusSchema.optional(),
priority: prioritySchema.optional(),
details: z.string().optional(),
depends_on: z.array(z.string()).optional(),
});
function boardData(runtime: PluginRuntime, board: TaskBoard) {
const counts = {
pending: 0,
in_progress: 0,
completed: 0,
blocked: 0,
canceled: 0,
};
for (const item of board.items) counts[item.status]++;
return {
id: board.id,
title: board.title,
objective: board.objective,
status: board.status,
counts,
items: board.items,
notes: board.notes,
decisions: board.decisions,
checkpoints: board.checkpoints,
nextActionable: runtime.tasks.nextActionable(board).slice(0, 20),
createdAt: board.createdAt,
updatedAt: board.updatedAt,
completedAt: board.completedAt,
archivedAt: board.archivedAt,
statePath: runtime.tasks.stateRelativePath(board.id),
};
}
function summaryData(runtime: PluginRuntime, board: TaskBoard) {
const data = boardData(runtime, board);
return {
id: data.id,
title: data.title,
objective: data.objective?.slice(0, 500),
status: data.status,
counts: data.counts,
nextActionable: data.nextActionable.slice(0, 5).map((item) => ({
id: item.id,
text: item.text,
status: item.status,
priority: item.priority,
})),
updatedAt: data.updatedAt,
statePath: data.statePath,
};
}
function boardFacts(board: TaskBoard): string[] {
const outstanding = board.items.filter(
(item) => item.status !== "completed" && item.status !== "canceled",
);
return [
`task board ${board.id} ${board.status}: ${board.title}`,
...outstanding.slice(0, 20).map((item) => `${item.id} ${item.status}: ${item.text}`),
];
}
export function createTaskTool(runtime: PluginRuntime): Tool {
return tool({
name: "workspace_tasks",
description:
"Durable To-Do boards under .agentic/tasks. Actions: create, list, show, add, update, remove, note, checkpoint, next, archive, reopen. For any multi-step job create a board first, then update each item with evidence and checkpoint after every verified step, so progress survives context compression. depends_on: item_ ids or 1-based positions in the same create/add call. No approval needed.",
parameters: {
action: z.enum([
"create",
"list",
"show",
"add",
"update",
"remove",
"note",
"checkpoint",
"next",
"archive",
"reopen",
]),
board_id: z.string().optional(),
title: z.string().optional(),
objective: z.string().optional(),
items: z.array(newItemSchema).optional(),
item_id: z.string().optional(),
text: z.string().optional(),
status: statusSchema.optional(),
priority: prioritySchema.optional(),
details: z.string().optional(),
depends_on: z.array(z.string()).optional(),
evidence: z.array(z.string()).optional(),
note: z.string().optional(),
decision: z.string().optional(),
summary: z.string().optional(),
decisions: z.array(z.string()).optional(),
blockers: z.array(z.string()).optional(),
next: z.array(z.string()).optional(),
include_archived: z.boolean().optional(),
limit: z.number().int().min(1).max(100).optional(),
},
implementation: async (input: {
action:
| "create"
| "list"
| "show"
| "add"
| "update"
| "remove"
| "note"
| "checkpoint"
| "next"
| "archive"
| "reopen";
board_id?: string;
title?: string;
objective?: string;
items?: Array<{
text: string;
status?: TaskItemStatus;
priority?: TaskPriority;
details?: string;
depends_on?: string[];
}>;
item_id?: string;
text?: string;
status?: TaskItemStatus;
priority?: TaskPriority;
details?: string;
depends_on?: string[];
evidence?: string[];
note?: string;
decision?: string;
summary?: string;
decisions?: string[];
blockers?: string[];
next?: string[];
include_archived?: boolean;
limit?: number;
}) => {
try {
if (input.action === "create") {
if (!input.title) {
throw new AgenticError("INVALID_INPUT", "workspace_tasks create requires title.");
}
const items: NewTaskItem[] = (input.items ?? []).map((item) => ({
text: item.text,
status: item.status,
priority: item.priority,
details: item.details,
dependsOn: item.depends_on,
}));
const board = await runtime.tasks.create({
title: input.title,
objective: input.objective,
items,
});
return okResult({
operation: "tasks.create",
summary: `Created task board ${board.id} with ${board.items.length} item(s).`,
data: boardData(runtime, board),
importance: "critical",
facts: boardFacts(board),
omit: ["data.notes", "data.decisions", "data.checkpoints"],
});
}
if (input.action === "list") {
const boards = await runtime.tasks.list(
input.limit ?? 30,
input.include_archived ?? false,
);
return okResult({
operation: "tasks.list",
summary: `Listed ${boards.length} task board(s).`,
data: boards.map((board) => summaryData(runtime, board)),
importance: "high",
facts: boards.slice(0, 20).map((board) => `task board ${board.id} ${board.status}: ${board.title}`),
});
}
if (!input.board_id) {
throw new AgenticError(
"INVALID_INPUT",
`workspace_tasks ${input.action} requires board_id.`,
);
}
if (input.action === "show" || input.action === "next") {
const board = await runtime.tasks.load(input.board_id);
const data = boardData(runtime, board);
return okResult({
operation: `tasks.${input.action}`,
summary:
input.action === "next"
? `${data.nextActionable.length} actionable item(s) on ${board.id}.`
: `${board.id}: ${board.title} (${board.status}).`,
data:
input.action === "next"
? {
board: summaryData(runtime, board),
items: data.nextActionable,
}
: data,
importance: "high",
facts: boardFacts(board),
omit:
input.action === "show"
? ["data.notes", "data.decisions", "data.checkpoints"]
: undefined,
});
}
let board: TaskBoard;
switch (input.action) {
case "add": {
const items: NewTaskItem[] = (input.items ?? []).map((item) => ({
text: item.text,
status: item.status,
priority: item.priority,
details: item.details,
dependsOn: item.depends_on,
}));
board = await runtime.tasks.addItems(input.board_id, items);
break;
}
case "update":
if (!input.item_id) {
throw new AgenticError("INVALID_INPUT", "workspace_tasks update requires item_id.");
}
board = await runtime.tasks.updateItem(input.board_id, input.item_id, {
text: input.text,
status: input.status,
priority: input.priority,
details: input.details,
dependsOn: input.depends_on,
evidence: input.evidence,
});
break;
case "remove":
if (!input.item_id) {
throw new AgenticError("INVALID_INPUT", "workspace_tasks remove requires item_id.");
}
board = await runtime.tasks.removeItem(input.board_id, input.item_id);
break;
case "note":
board = await runtime.tasks.addNote(input.board_id, {
note: input.note,
decision: input.decision,
});
break;
case "checkpoint":
if (!input.summary) {
throw new AgenticError(
"INVALID_INPUT",
"workspace_tasks checkpoint requires summary.",
);
}
board = await runtime.tasks.checkpoint(input.board_id, {
summary: input.summary,
decisions: input.decisions,
blockers: input.blockers,
next: input.next,
});
break;
case "archive":
board = await runtime.tasks.archive(input.board_id);
break;
case "reopen":
board = await runtime.tasks.reopen(input.board_id);
break;
default:
throw new AgenticError("INVALID_INPUT", `Unsupported task action: ${input.action}`);
}
return okResult({
operation: `tasks.${input.action}`,
summary: `${board.id} updated; ${runtime.tasks.nextActionable(board).length} actionable item(s) remain.`,
data: boardData(runtime, board),
importance: "critical",
facts: boardFacts(board),
omit: ["data.notes", "data.decisions", "data.checkpoints"],
});
} catch (error) {
return errorResult(`tasks.${input.action}`, error, "high");
}
},
});
}