This commit is contained in:
arvinxx
2026-03-23 21:51:15 +08:00
parent eb256c0c1c
commit a52669fee5
17 changed files with 649 additions and 324 deletions

View File

@@ -71,16 +71,13 @@ export function registerDocCommands(task: Command) {
if (!folder.startsWith('docs_')) {
// folder is a name, find or create it
const detail = await client.task.detail.query({ id });
const workspace = detail.data.documents as any;
const nodeMap = workspace?.nodeMap || {};
const folders = detail.data.workspace || [];
// Search for existing folder by name
const existing = Object.entries(nodeMap).find(
([, doc]: [string, any]) => doc.title === folder && doc.fileType === 'custom/folder',
);
const existingFolder = folders.find((f) => f.title === folder);
if (existing) {
folderId = existing[0];
if (existingFolder) {
folderId = existingFolder.documentId;
} else {
// Create folder and pin to task
const result = await client.document.createDocument.mutate({

View File

@@ -160,62 +160,40 @@ export function registerTaskCommand(program: Command) {
);
console.log(`${pc.dim('Instruction:')} ${t.instruction}`);
if (t.description) console.log(`${pc.dim('Description:')} ${t.description}`);
if (t.assigneeAgentId) console.log(`${pc.dim('Agent:')} ${t.assigneeAgentId}`);
if (t.assigneeUserId) console.log(`${pc.dim('User:')} ${t.assigneeUserId}`);
if (t.agentId) console.log(`${pc.dim('Agent:')} ${t.agentId}`);
if (t.userId) console.log(`${pc.dim('User:')} ${t.userId}`);
if (t.parent) {
const p = t.parent as any;
console.log(`${pc.dim('Parent:')} ${p.identifier} ${p.name || ''}`);
console.log(`${pc.dim('Parent:')} ${t.parent.identifier} ${t.parent.name || ''}`);
}
console.log(
`${pc.dim('Topics:')} ${t.totalTopics} ${pc.dim('Created:')} ${timeAgo(t.createdAt)}`,
);
if (t.heartbeatTimeout && t.lastHeartbeatAt) {
const hb = timeAgo(t.lastHeartbeatAt);
const interval = t.heartbeatInterval ? `${t.heartbeatInterval}s` : '-';
const elapsed = (Date.now() - new Date(t.lastHeartbeatAt).getTime()) / 1000;
const isStuck = t.status === 'running' && elapsed > t.heartbeatTimeout;
const topicInfo = t.topicCount ? `${t.topicCount}` : '0';
const createdInfo = t.createdAt ? timeAgo(t.createdAt) : '-';
console.log(`${pc.dim('Topics:')} ${topicInfo} ${pc.dim('Created:')} ${createdInfo}`);
if (t.heartbeat?.timeout && t.heartbeat.lastAt) {
const hb = timeAgo(t.heartbeat.lastAt);
const interval = t.heartbeat.interval ? `${t.heartbeat.interval}s` : '-';
const elapsed = (Date.now() - new Date(t.heartbeat.lastAt).getTime()) / 1000;
const isStuck = t.status === 'running' && elapsed > t.heartbeat.timeout;
console.log(
`${pc.dim('Heartbeat:')} ${isStuck ? pc.red(hb) : hb} ${pc.dim('interval:')} ${interval} ${pc.dim('timeout:')} ${t.heartbeatTimeout}s${isStuck ? pc.red(' ⚠ TIMEOUT') : ''}`,
`${pc.dim('Heartbeat:')} ${isStuck ? pc.red(hb) : hb} ${pc.dim('interval:')} ${interval} ${pc.dim('timeout:')} ${t.heartbeat.timeout}s${isStuck ? pc.red(' ⚠ TIMEOUT') : ''}`,
);
}
if (t.error) console.log(`${pc.red('Error:')} ${t.error}`);
// ── Subtasks ──
if (t.subtasks && t.subtasks.length > 0) {
// Build dependency lookup: taskId → [dependsOnIdentifier, ...]
const subtaskDeps = (t.subtaskDeps || []) as any[];
const idToIdentifier = new Map<string, string>();
for (const s of t.subtasks) idToIdentifier.set(s.id, s.identifier);
// Build lookup: which subtasks are completed
const completedIds = new Set(
t.subtasks.filter((s: any) => s.status === 'completed').map((s: any) => s.id),
const completedIdentifiers = new Set(
t.subtasks.filter((s) => s.status === 'completed').map((s) => s.identifier),
);
const blockedBy = new Map<string, string[]>();
const hasUnresolvedDep = new Set<string>();
for (const d of subtaskDeps) {
if (d.type === 'blocks') {
const list = blockedBy.get(d.taskId) || [];
const depIdentifier = idToIdentifier.get(d.dependsOnId) || d.dependsOnId;
list.push(depIdentifier);
blockedBy.set(d.taskId, list);
// Check if the dependency is NOT completed
if (!completedIds.has(d.dependsOnId)) {
hasUnresolvedDep.add(d.taskId);
}
}
}
console.log(`\n${pc.bold('Subtasks:')}`);
for (const s of t.subtasks) {
const deps = blockedBy.get(s.id);
const depInfo = deps ? pc.dim(` ← blocks: ${deps.join(', ')}`) : '';
const depInfo = s.blockedBy ? pc.dim(` ← blocks: ${s.blockedBy}`) : '';
// Show 'blocked' instead of 'backlog' if task has unresolved dependencies
const displayStatus =
s.status === 'backlog' && hasUnresolvedDep.has(s.id) ? 'blocked' : s.status;
const isBlocked = s.blockedBy && !completedIdentifiers.has(s.blockedBy);
const displayStatus = s.status === 'backlog' && isBlocked ? 'blocked' : s.status;
console.log(
` ${pc.dim(s.identifier)} ${statusBadge(displayStatus)} ${s.name || s.instruction}${depInfo}`,
` ${pc.dim(s.identifier)} ${statusBadge(displayStatus)} ${s.name || '(unnamed)'}${depInfo}`,
);
}
}
@@ -223,30 +201,31 @@ export function registerTaskCommand(program: Command) {
// ── Dependencies ──
if (t.dependencies && t.dependencies.length > 0) {
console.log(`\n${pc.bold('Dependencies:')}`);
for (const d of t.dependencies as any[]) {
console.log(` ${pc.dim(d.type)}: ${d.dependsOnId}`);
for (const d of t.dependencies) {
const depName = d.name ? ` ${d.name}` : '';
console.log(` ${pc.dim(d.type || 'blocks')}: ${d.dependsOn}${depName}`);
}
}
// ── Checkpoint ──
{
const cp = t.checkpoint as any;
const cp = t.checkpoint || {};
console.log(`\n${pc.bold('Checkpoint:')}`);
const hasConfig =
cp.onAgentRequest !== undefined ||
cp.topic?.before ||
cp.topic?.after ||
cp.tasks?.beforeIds?.length > 0 ||
cp.tasks?.afterIds?.length > 0;
cp.tasks?.beforeIds?.length ||
cp.tasks?.afterIds?.length;
if (hasConfig) {
if (cp.onAgentRequest !== undefined)
console.log(` onAgentRequest: ${cp.onAgentRequest}`);
if (cp.topic?.before) console.log(` topic.before: ${cp.topic.before}`);
if (cp.topic?.after) console.log(` topic.after: ${cp.topic.after}`);
if (cp.tasks?.beforeIds?.length > 0)
if (cp.tasks?.beforeIds?.length)
console.log(` tasks.before: ${cp.tasks.beforeIds.join(', ')}`);
if (cp.tasks?.afterIds?.length > 0)
if (cp.tasks?.afterIds?.length)
console.log(` tasks.after: ${cp.tasks.afterIds.join(', ')}`);
} else {
console.log(` ${pc.dim('(not configured, default: onAgentRequest=true)')}`);
@@ -280,108 +259,93 @@ export function registerTaskCommand(program: Command) {
}
}
// ── Activities (last section) ──
// ── Workspace ──
{
// ── Workspace ──
{
const workspace = t.documents as any;
const nodeMap = workspace?.nodeMap || {};
const tree = workspace?.tree || [];
const docCount = Object.keys(nodeMap).length;
const nodes = t.workspace || [];
if (nodes.length === 0) {
console.log(`\n${pc.bold('Workspace:')}`);
console.log(` ${pc.dim('No documents yet.')}`);
} else {
const countNodes = (list: typeof nodes): number =>
list.reduce((sum, n) => sum + 1 + (n.children ? countNodes(n.children) : 0), 0);
console.log(`\n${pc.bold(`Workspace (${countNodes(nodes)}):`)}`);
if (docCount === 0) {
console.log(`\n${pc.bold('Workspace:')}`);
console.log(` ${pc.dim('No documents yet.')}`);
} else {
console.log(`\n${pc.bold(`Workspace (${docCount}):`)}`);
const formatSize = (chars: number | null | undefined) => {
if (!chars) return '';
if (chars >= 10_000) return `${(chars / 1000).toFixed(1)}k`;
return `${chars}`;
};
const formatSize = (chars: number | null) => {
if (!chars) return '';
if (chars >= 10_000) return `${(chars / 1000).toFixed(1)}k`;
return `${chars}`;
};
const LEFT_COL = 56;
const FROM_WIDTH = 10;
const LEFT_COL = 56; // fixed display width from start to after title (including indent)
const FROM_WIDTH = 10;
const printNode = (node: any, prefix: string, isLast: boolean, isRoot: boolean) => {
const doc = nodeMap[node.id];
if (!doc) return;
const isFolder = doc.fileType === 'folder' || doc.fileType === 'custom/folder';
const renderNodes = (list: typeof nodes, indent: string) => {
for (let i = 0; i < list.length; i++) {
const node = list[i];
const isFolder = node.fileType === 'custom/folder';
const isLast = i === list.length - 1;
const icon = isFolder ? '📁' : '📄';
const connector = isRoot ? ' ' : isLast ? ' └── ' : ' ├── ';
// Prefix + connector + icon take variable width; pad title to fill LEFT_COL
const leftPrefix = `${prefix}${connector}${icon} `;
const prefixWidth = displayWidth(leftPrefix);
const maxTitle = Math.max(10, LEFT_COL - prefixWidth);
const titleStr = truncate(doc.title, maxTitle);
const titleWidth = displayWidth(titleStr);
const titlePad = ' '.repeat(Math.max(1, LEFT_COL - prefixWidth - titleWidth));
// Source column (fixed width)
const fromStr = doc.sourceTaskIdentifier ? `${doc.sourceTaskIdentifier}` : '';
const fromPad = ' '.repeat(Math.max(1, FROM_WIDTH - fromStr.length + 1));
// Right columns
const size =
!isFolder && doc.charCount
? formatSize(doc.charCount).padStart(6) + ' chars'
: ''.padStart(12);
const updated = doc.updatedAt ? timeAgo(doc.updatedAt).padStart(7) : '';
console.log(
`${leftPrefix}${titleStr}${titlePad}${pc.dim(`(${node.id})`)} ${fromStr}${fromPad}${pc.dim(`${size} ${updated}`)}`,
const prefix = `${indent}${icon} `;
const titleStr = truncate(node.title || 'Untitled', LEFT_COL - displayWidth(prefix));
const titlePad = ' '.repeat(
Math.max(1, LEFT_COL - displayWidth(prefix) - displayWidth(titleStr)),
);
const kids = node.children || [];
const childPrefix = isRoot ? '' : prefix + (isLast ? ' ' : ' │ ');
kids.forEach((child: any, i: number) => {
printNode(child, childPrefix, i === kids.length - 1, false);
});
};
const fromStr = node.sourceTaskIdentifier ? `${node.sourceTaskIdentifier}` : '';
const fromPad = fromStr
? ' '.repeat(Math.max(1, FROM_WIDTH - fromStr.length + 1))
: '';
const size =
!isFolder && node.size ? formatSize(node.size).padStart(6) + ' chars' : '';
tree.forEach((node: any, i: number) => {
printNode(node, '', i === tree.length - 1, true);
});
}
console.log(
`${prefix}${titleStr}${titlePad}${pc.dim(`(${node.documentId})`)} ${fromStr}${fromPad}${pc.dim(size)}`,
);
if (node.children && node.children.length > 0) {
const childIndent = indent + (isLast ? ' ' : ' ');
renderNodes(node.children, childIndent);
}
}
};
renderNodes(nodes, ' ');
}
}
const activities: {
data: any;
time: number;
type: 'topic' | 'brief' | 'comment' | 'review';
}[] = [];
// ── Activities ──
{
const tl = t.timeline;
const activities: { text: string; time: string }[] = [];
for (const tp of t.topics || []) {
for (const tp of tl?.topics || []) {
const sBadge = statusBadge(tp.status || 'running');
activities.push({
data: tp,
time: new Date(tp.createdAt).getTime(),
type: 'topic',
});
// Add review as separate activity if topic has been reviewed
if (tp.reviewedAt) {
activities.push({
data: tp,
time: new Date(tp.reviewedAt).getTime(),
type: 'review',
});
}
}
for (const b of t.briefs || []) {
activities.push({
data: b,
time: new Date(b.createdAt).getTime(),
type: 'brief',
text: ` 💬 ${pc.dim((tp.time || '').padStart(7))} Topic #${tp.seq || '?'} ${tp.title || 'Untitled'} ${sBadge} ${pc.dim(tp.id || '')}`,
time: tp.time || '',
});
}
for (const c of t.comments || []) {
for (const b of tl?.briefs || []) {
const icon = briefIcon(b.type);
const pri =
b.priority === 'urgent'
? pc.red(' [urgent]')
: b.priority === 'normal'
? pc.yellow(' [normal]')
: '';
const resolved = b.resolvedAction ? pc.green(` ✏️ ${b.resolvedAction}`) : '';
const typeLabel = pc.dim(`[${b.type}]`);
activities.push({
data: c,
time: new Date(c.createdAt).getTime(),
type: 'comment',
text: ` ${icon} ${pc.dim((b.time || '').padStart(7))} Brief ${typeLabel} ${b.title}${pri}${resolved} ${pc.dim(b.id || '')}`,
time: b.time || '',
});
}
for (const c of tl?.comments || []) {
const author = c.agentId ? `🤖 ${c.agentId}` : '👤 user';
activities.push({
text: ` 💭 ${pc.dim((c.time || '').padStart(7))} ${pc.cyan(author)} ${c.content}`,
time: c.time || '',
});
}
@@ -389,66 +353,9 @@ export function registerTaskCommand(program: Command) {
if (activities.length === 0) {
console.log(` ${pc.dim('No activities yet.')}`);
} else {
activities.sort((a, b) => a.time - b.time);
const pad = (s: string, w: number) => s.padStart(w);
// Activities are already sorted by the service
for (const act of activities) {
const ago = pad(
timeAgo(act.type === 'review' ? act.data.reviewedAt : act.data.createdAt),
7,
);
if (act.type === 'topic') {
const tp = act.data;
const sBadge = statusBadge(tp.status || 'running');
console.log(
` 💬 ${pc.dim(ago)} Topic #${tp.seq} ${tp.title || 'Untitled'} ${sBadge} ${pc.dim(tp.id)}`,
);
} else if (act.type === 'review') {
const tp = act.data;
const passed = tp.reviewPassed === 1;
const icon = passed ? pc.green('✓') : pc.red('✗');
const scoreText = ((tp.reviewScores as any[]) || [])
.map((s: any) => {
const pct = Math.round(s.score * 100);
const sIcon = s.passed ? pc.green('✓') : pc.red('✗');
return `${s.rubricId} ${pct}%${sIcon}`;
})
.join(' | ');
const iter =
tp.reviewIteration > 1 ? pc.dim(` (iteration ${tp.reviewIteration})`) : '';
console.log(
` 🔍 ${pc.dim(ago)} Review ${icon} ${passed ? 'passed' : 'failed'} (${tp.reviewScore}%) ${pc.dim(scoreText)}${iter}`,
);
} else if (act.type === 'comment') {
const c = act.data;
const author = c.agentId ? `🤖 ${c.agentId}` : '👤 user';
console.log(` 💭 ${pc.dim(ago)} ${pc.cyan(author)} ${c.content}`);
} else {
const b = act.data;
const icon = briefIcon(b.type);
const pri =
b.priority === 'urgent'
? pc.red(' [urgent]')
: b.priority === 'normal'
? pc.yellow(' [normal]')
: '';
let statusTag: string;
if (b.resolvedAt) {
const actions = (b.actions as any[]) || [];
const matched = actions.find((a: any) => a.key === b.resolvedAction);
statusTag = pc.green(` ${matched?.label || '✓'}`);
} else if (b.readAt) {
statusTag = pc.dim(' (read)');
} else {
statusTag = pc.yellow(' ●');
}
const typeLabel = pc.dim(`[${b.type}]`);
console.log(
` ${icon} ${pc.dim(ago)} Brief ${typeLabel} ${b.title}${pri}${statusTag} ${pc.dim(b.id)}`,
);
}
console.log(act.text);
}
}
}

View File

@@ -818,6 +818,22 @@ export function renderStepDetail(
}
}
// Default view: show tool errors even without -t flag
if (!hasSpecificFlag && step.toolsResult) {
const failedResults = step.toolsResult.filter((tr) => tr.isSuccess === false);
if (failedResults.length > 0) {
lines.push('');
lines.push(bold(red('Errors:')));
for (const tr of failedResults) {
lines.push(` ${red('✗')} ${cyan(tr.identifier || tr.apiName)}`);
if (tr.output) {
const output = tr.output.length > 500 ? tr.output.slice(0, 500) + '...' : tr.output;
lines.push(` ${red(output)}`);
}
}
}
}
if (options?.tools) {
if (step.toolsCalling && step.toolsCalling.length > 0) {
lines.push('');

View File

@@ -162,6 +162,27 @@ export const TaskManifest: BuiltinToolManifest = {
type: 'object',
},
},
{
description:
"Update a task's status. Use to mark tasks as completed, canceled, or change lifecycle state. Defaults to the current task if no identifier provided.",
name: TaskApiName.updateTaskStatus,
parameters: {
properties: {
identifier: {
description:
'The task identifier (e.g. "TASK-1"). Defaults to the current task if omitted.',
type: 'string',
},
status: {
description: 'New status for the task.',
enum: ['backlog', 'running', 'paused', 'completed', 'failed', 'canceled'],
type: 'string',
},
},
required: ['status'],
type: 'object',
},
},
{
description: 'Delete a task by identifier.',
name: TaskApiName.deleteTask,

View File

@@ -4,9 +4,11 @@ export const systemPrompt = `You have access to Task management tools. Use them
- **listTasks**: List tasks, optionally filtered by parent or status
- **viewTask**: View details of a specific task (defaults to your current task)
- **editTask**: Modify a task's name, instruction, priority, dependencies (addDependency/removeDependency), or review config
- **updateTaskStatus**: Change a task's status (e.g. mark as completed when done, or cancel if no longer needed)
- **deleteTask**: Delete a task
When planning work:
1. Create tasks for each major piece of work (use parentIdentifier to organize as subtasks)
2. Use editTask with addDependency to control execution order
3. Configure review criteria on tasks that need quality gates`;
3. Configure review criteria on tasks that need quality gates
4. Use updateTaskStatus to mark the current task as completed when you finish all work`;

View File

@@ -11,6 +11,9 @@ export const TaskApiName = {
/** List tasks with optional filters */
listTasks: 'listTasks',
/** Update a task's status (e.g. complete, cancel) */
updateTaskStatus: 'updateTaskStatus',
/** View details of a specific task */
viewTask: 'viewTask',
} as const;

View File

@@ -81,6 +81,14 @@ export class TaskModel {
return result[0] || null;
}
async findByIds(ids: string[]): Promise<TaskItem[]> {
if (ids.length === 0) return [];
return this.db
.select()
.from(tasks)
.where(and(inArray(tasks.id, ids), eq(tasks.createdByUserId, this.userId)));
}
// Resolve id or identifier (e.g. 'TASK-1') to a task
async resolve(idOrIdentifier: string): Promise<TaskItem | null> {
if (idOrIdentifier.startsWith('task_')) return this.findById(idOrIdentifier);

View File

@@ -32,5 +32,5 @@ export const resolveMaxTokens = async ({
const hasSmallContextWindow = smallContextWindowPatterns.some((pattern) => pattern.test(model));
return hasSmallContextWindow ? 4096 : 8192;
return hasSmallContextWindow ? 4096 : 16_384;
};

View File

@@ -1,3 +1,5 @@
import type { TaskDetailData, TaskDetailWorkspaceNode } from '@lobechat/types';
// ── Formatting helpers for Task tool responses ──
const priorityLabel = (p?: number | null): string => {
@@ -53,12 +55,15 @@ export interface TaskSummary {
status: string;
}
export interface TaskDetail extends TaskSummary {
dependencies?: Array<{ dependsOn: string; type: string }>;
instruction: string;
parentTaskId?: string | null;
subtasks?: TaskSummary[];
}
// Re-export shared types from @lobechat/types for backward compatibility
export type {
TaskDetailBrief,
TaskDetailComment,
TaskDetailData,
TaskDetailSubtask,
TaskDetailTopic,
TaskDetailWorkspaceNode,
} from '@lobechat/types';
/**
* Format a single task as a one-line summary
@@ -104,28 +109,131 @@ export const formatTaskList = (
/**
* Format viewTask response
*/
export const formatTaskDetail = (t: TaskDetail): string => {
export const formatTaskDetail = (t: TaskDetailData): string => {
const lines = [
`${t.identifier} "${t.name || '(unnamed)'}"`,
` Status: ${statusIcon(t.status)} ${t.status}`,
` Priority: ${priorityLabel(t.priority)}`,
` Instruction: ${t.instruction}`,
`${t.identifier} ${t.name || '(unnamed)'}`,
`Status: ${statusIcon(t.status)} ${t.status} Priority: ${priorityLabel(t.priority)}`,
`Instruction: ${t.instruction}`,
];
if (t.parentTaskId) lines.push(` Parent: ${t.parentTaskId}`);
if (t.agentId) lines.push(`Agent: ${t.agentId}`);
if (t.parent) lines.push(`Parent: ${t.parent.identifier}`);
if (t.topicCount) lines.push(`Topics: ${t.topicCount}`);
if (t.createdAt) lines.push(`Created: ${t.createdAt}`);
if (t.dependencies && t.dependencies.length > 0) {
lines.push(
`Dependencies: ${t.dependencies.map((d) => `${d.type}: ${d.dependsOn}`).join(', ')}`,
);
}
// Subtasks
if (t.subtasks && t.subtasks.length > 0) {
lines.push(` Subtasks (${t.subtasks.length}):`);
lines.push('');
lines.push('Subtasks:');
for (const s of t.subtasks) {
lines.push(` ${formatTaskLine(s)}`);
const dep = s.blockedBy ? ` ← blocks: ${s.blockedBy}` : '';
lines.push(
` ${s.identifier} ${statusIcon(s.status)} ${s.status} ${s.name || '(unnamed)'}${dep}`,
);
}
}
if (t.dependencies && t.dependencies.length > 0) {
lines.push(` Dependencies (${t.dependencies.length}):`);
for (const d of t.dependencies) {
lines.push(` ${d.type}: ${d.dependsOn}`);
// Checkpoint
lines.push('');
if (t.checkpoint && Object.keys(t.checkpoint).length > 0) {
lines.push(`Checkpoint: ${JSON.stringify(t.checkpoint)}`);
} else {
lines.push('Checkpoint: (not configured, default: onAgentRequest=true)');
}
// Review
lines.push('');
if (t.review && Object.keys(t.review).length > 0) {
const rubrics = (t.review as any).rubrics as
| Array<{ name: string; threshold?: number; type: string }>
| undefined;
lines.push(`Review (maxIterations: ${(t.review as any).maxIterations || 3}):`);
if (rubrics) {
for (const r of rubrics) {
lines.push(
` - ${r.name} [${r.type}]${r.threshold ? `${Math.round(r.threshold * 100)}%` : ''}`,
);
}
}
} else {
lines.push('Review: (not configured)');
}
// Workspace
if (t.workspace && t.workspace.length > 0) {
const countNodes = (nodes: TaskDetailWorkspaceNode[]): number =>
nodes.reduce((sum, n) => sum + 1 + (n.children ? countNodes(n.children) : 0), 0);
const total = countNodes(t.workspace);
lines.push('');
lines.push(`Workspace (${total}):`);
const renderNodes = (nodes: TaskDetailWorkspaceNode[], indent: string) => {
for (const node of nodes) {
const isFolder = node.fileType === 'custom/folder';
const icon = isFolder ? '📁' : '📄';
const source = node.sourceTaskIdentifier ? `${node.sourceTaskIdentifier}` : '';
const sizeStr = !isFolder && node.size ? ` ${node.size} chars` : '';
lines.push(
`${indent}${icon} ${node.title || 'Untitled'} (${node.documentId})${source}${sizeStr}`,
);
if (node.children) {
renderNodes(node.children, indent + ' ');
}
}
};
renderNodes(t.workspace, ' ');
}
// Activities
const timelineEntries: { text: string; time: number }[] = [];
if (t.timeline?.topics) {
for (const tp of t.timeline.topics) {
const status = tp.status || 'completed';
const idSuffix = tp.id ? ` ${tp.id}` : '';
timelineEntries.push({
text: ` 💬 ${tp.time || ''} Topic #${tp.seq || '?'} ${tp.title || 'Untitled'} ${statusIcon(status)} ${status}${idSuffix}`,
time: 0,
});
}
}
if (t.timeline?.briefs) {
for (const b of t.timeline.briefs) {
let resolved = '';
if (b.resolvedAction) {
resolved = ` ✏️ ${b.resolvedAction}`;
}
const priStr = b.priority ? ` [${b.priority}]` : '';
const idSuffix = b.id ? ` ${b.id}` : '';
timelineEntries.push({
text: ` ${briefIcon(b.type)} ${b.time || ''} Brief [${b.type}] ${b.title}${priStr}${resolved}${idSuffix}`,
time: 0,
});
}
}
if (t.timeline?.comments) {
for (const c of t.timeline.comments) {
const author = c.agentId ? '🤖 agent' : '👤 user';
const truncated = c.content.length > 80 ? c.content.slice(0, 80) + '...' : c.content;
timelineEntries.push({
text: ` 💭 ${c.time || ''} ${author} ${truncated}`,
time: 0,
});
}
}
if (timelineEntries.length > 0) {
lines.push('');
lines.push('Activities:');
lines.push(...timelineEntries.map((e) => e.text));
}
return lines.join('\n');
@@ -207,22 +315,16 @@ export interface TaskRunPromptSubtask {
status: string;
}
export interface TaskRunPromptDocument {
export interface TaskRunPromptWorkspaceNode {
children?: TaskRunPromptWorkspaceNode[];
createdAt?: string;
documentId: string;
/** Character count of the document content */
fileType?: string;
size?: number;
sourceTaskIdentifier?: string;
title?: string;
}
export interface TaskRunPromptFolder {
children: TaskRunPromptDocument[];
createdAt?: string;
documentId: string;
title?: string;
}
export interface TaskRunPromptInput {
/** Activity data (all optional) */
activities?: {
@@ -260,7 +362,7 @@ export interface TaskRunPromptInput {
subtasks?: Array<TaskSummary & { blockedBy?: string }>;
};
/** Pinned documents (workspace) */
workspace?: TaskRunPromptFolder[];
workspace?: TaskRunPromptWorkspaceNode[];
}
// ── Relative time helper ──
@@ -378,21 +480,28 @@ export const buildTaskRunPrompt = (input: TaskRunPromptInput, now?: Date): strin
// Workspace
if (workspace && workspace.length > 0) {
const totalDocs = workspace.reduce((sum, f) => sum + f.children.length, 0);
const countNodes = (nodes: TaskRunPromptWorkspaceNode[]): number =>
nodes.reduce((sum, n) => sum + 1 + (n.children ? countNodes(n.children) : 0), 0);
const total = countNodes(workspace);
taskLines.push('');
taskLines.push(`Workspace (${totalDocs + workspace.length}):`);
for (const folder of workspace) {
const folderAgo = folder.createdAt ? ` ${timeAgo(folder.createdAt, now)}` : '';
taskLines.push(` 📁 ${folder.title || 'Untitled'} (${folder.documentId})${folderAgo}`);
for (const d of folder.children) {
const source = d.sourceTaskIdentifier ? `${d.sourceTaskIdentifier}` : '';
const sizeStr = d.size ? ` ${d.size} chars` : '';
const docAgo = d.createdAt ? ` ${timeAgo(d.createdAt, now)}` : '';
taskLines.push(`Workspace (${total}):`);
const renderNodes = (nodes: TaskRunPromptWorkspaceNode[], indent: string) => {
for (const node of nodes) {
const isFolder = node.fileType === 'custom/folder';
const icon = isFolder ? '📁' : '📄';
const source = node.sourceTaskIdentifier ? ` ${node.sourceTaskIdentifier}` : '';
const sizeStr = !isFolder && node.size ? ` ${node.size} chars` : '';
const ago = node.createdAt ? ` ${timeAgo(node.createdAt, now)}` : '';
taskLines.push(
` └── 📄 ${d.title || 'Untitled'} (${d.documentId})${source}${sizeStr}${docAgo}`,
`${indent}${icon} ${node.title || 'Untitled'} (${node.documentId})${source}${sizeStr}${ago}`,
);
if (node.children) {
renderNodes(node.children, indent + ' ');
}
}
}
};
renderNodes(workspace, ' ');
}
// Activities (chronological, flat list)

View File

@@ -30,3 +30,76 @@ export interface WorkspaceData {
nodeMap: Record<string, WorkspaceDocNode>;
tree: WorkspaceTreeNode[];
}
// ── Task Detail (shared across CLI, viewTask tool, task.detail router) ──
export interface TaskDetailSubtask {
blockedBy?: string;
identifier: string;
name?: string | null;
priority?: number | null;
status: string;
}
export interface TaskDetailWorkspaceNode {
children?: TaskDetailWorkspaceNode[];
documentId: string;
fileType?: string;
size?: number | null;
sourceTaskIdentifier?: string | null;
title?: string;
}
export interface TaskDetailTopic {
id?: string;
seq?: number | null;
status?: string | null;
time?: string;
title?: string;
}
export interface TaskDetailBrief {
id?: string;
priority?: string | null;
resolvedAction?: string | null;
summary?: string;
time?: string;
title: string;
type: string;
}
export interface TaskDetailComment {
agentId?: string | null;
content: string;
time?: string;
}
export interface TaskDetailData {
agentId?: string | null;
checkpoint?: CheckpointConfig;
createdAt?: string;
dependencies?: Array<{ dependsOn: string; type: string }>;
description?: string | null;
error?: string | null;
heartbeat?: {
interval?: number | null;
lastAt?: string | null;
timeout?: number | null;
};
identifier: string;
instruction: string;
name?: string | null;
parent?: { identifier: string; name: string | null } | null;
priority?: number | null;
review?: Record<string, any> | null;
status: string;
subtasks?: TaskDetailSubtask[];
timeline?: {
briefs?: TaskDetailBrief[];
comments?: TaskDetailComment[];
topics?: TaskDetailTopic[];
};
topicCount?: number;
userId?: string | null;
workspace?: TaskDetailWorkspaceNode[];
}

View File

@@ -343,7 +343,16 @@ export const createRuntimeExecutors = (
// Construct ChatStreamPayload
const stream = ctx.stream ?? true;
const chatPayload = { messages: processedMessages, model, stream, tools };
// Resolve max_tokens from model bank to avoid truncation of long tool call arguments
const { LOBE_DEFAULT_MODEL_LIST: modelList } = await import('model-bank');
const modelInfo =
modelList.find(
(m: { id: string; providerId?: string }) => m.id === model && m.providerId === provider,
) || modelList.find((m: { id: string }) => m.id === model);
const max_tokens = modelInfo?.maxOutput;
const chatPayload = { max_tokens, messages: processedMessages, model, stream, tools };
log(
`${stagePrefix} calling model-runtime chat (model: %s, messages: %d, tools: %d)`,

View File

@@ -81,8 +81,7 @@ describe('Task Router Integration', () => {
const detail = await caller.detail({ id: 'TASK-1' });
expect(detail.data.identifier).toBe('TASK-1');
expect(detail.data.subtasks).toHaveLength(0);
expect(detail.data.topics).toHaveLength(0);
expect(detail.data.comments).toHaveLength(0);
expect(detail.data.timeline).toBeUndefined();
});
});
@@ -112,7 +111,9 @@ describe('Task Router Integration', () => {
const detail = await caller.detail({ id: parent.data.identifier });
expect(detail.data.subtasks).toHaveLength(2);
expect(detail.data.subtaskDeps).toHaveLength(1);
// ch2 should have blockedBy pointing to ch1's identifier
const ch2Sub = detail.data.subtasks!.find((s) => s.name === 'Chapter 2');
expect(ch2Sub?.blockedBy).toBeTruthy();
});
});
@@ -157,8 +158,8 @@ describe('Task Router Integration', () => {
});
const detail = await caller.detail({ id: task.data.identifier });
expect(detail.data.comments).toHaveLength(2);
expect(detail.data.comments[0].content).toBe('First comment');
expect(detail.data.timeline?.comments).toHaveLength(2);
expect(detail.data.timeline?.comments?.[0].content).toBe('First comment');
});
});
@@ -193,9 +194,9 @@ describe('Task Router Integration', () => {
});
const review = await caller.getReview({ id: task.data.id });
expect(review.data.enabled).toBe(true);
expect(review.data.rubrics).toHaveLength(2);
expect(review.data.rubrics[0].type).toBe('llm-rubric');
expect(review.data!.enabled).toBe(true);
expect(review.data!.rubrics).toHaveLength(2);
expect(review.data!.rubrics[0].type).toBe('llm-rubric');
});
});
@@ -319,9 +320,13 @@ describe('Task Router Integration', () => {
// Check detail workspace
const detail = await caller.detail({ id: task.data.identifier });
const workspace = detail.data.documents;
expect(Object.keys(workspace.nodeMap)).toHaveLength(1);
expect(workspace.nodeMap[doc.id].title).toBe('Test Doc');
expect(detail.data.workspace).toBeDefined();
// Document should appear somewhere in the workspace tree
const allDocs = detail.data.workspace!.flatMap((f) => [
{ documentId: f.documentId, title: f.title },
...f.children,
]);
expect(allDocs.find((d) => d.documentId === doc.id)?.title).toBe('Test Doc');
// Unpin
await caller.unpinDocument({
@@ -330,7 +335,7 @@ describe('Task Router Integration', () => {
});
const detail2 = await caller.detail({ id: task.data.identifier });
expect(Object.keys(detail2.data.documents.nodeMap)).toHaveLength(0);
expect(detail2.data.workspace).toBeUndefined();
});
});
@@ -352,10 +357,12 @@ describe('Task Router Integration', () => {
// Wait for timeout
await new Promise((r) => setTimeout(r, 1500));
// detail should auto-detect timeout
// detail should auto-detect timeout and pause
const detail = await caller.detail({ id: task.data.identifier });
expect(detail.data.status).toBe('paused');
expect(detail.data.error).toBeNull(); // stale timeout error gets cleared
// Verify stale timeout error gets cleared via find
const found = await caller.find({ id: task.data.id });
expect(found.data.error).toBeNull();
});
});
});

View File

@@ -2,6 +2,7 @@ import { BriefIdentifier } from '@lobechat/builtin-tool-brief';
import { NotebookIdentifier } from '@lobechat/builtin-tool-notebook';
import { TaskIdentifier } from '@lobechat/builtin-tool-task';
import { buildTaskRunPrompt } from '@lobechat/prompts';
import type { WorkspaceData } from '@lobechat/types';
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
@@ -12,6 +13,7 @@ import { TopicModel } from '@/database/models/topic';
import { authedProcedure, router } from '@/libs/trpc/lambda';
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
import { AiAgentService } from '@/server/services/aiAgent';
import { TaskService } from '@/server/services/task';
import { TaskLifecycleService } from '@/server/services/taskLifecycle';
import { TaskReviewService } from '@/server/services/taskReview';
@@ -22,6 +24,7 @@ const taskProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
briefModel: new BriefModel(ctx.serverDB, ctx.userId),
taskLifecycle: new TaskLifecycleService(ctx.serverDB, ctx.userId),
taskModel: new TaskModel(ctx.serverDB, ctx.userId),
taskService: new TaskService(ctx.serverDB, ctx.userId),
taskTopicModel: new TaskTopicModel(ctx.serverDB, ctx.userId),
topicModel: new TopicModel(ctx.serverDB, ctx.userId),
},
@@ -85,7 +88,9 @@ async function buildTaskPrompt(
taskModel.getComments(task.id).catch(() => []),
taskModel.findSubtasks(task.id).catch(() => []),
taskModel.getDependencies(task.id).catch(() => []),
taskModel.getTreePinnedDocuments(task.id).catch(() => []),
taskModel
.getTreePinnedDocuments(task.id)
.catch((): WorkspaceData => ({ nodeMap: {}, tree: [] })),
]);
// Batch-fetch dependencies for all subtasks to show blockedBy info
@@ -102,6 +107,11 @@ async function buildTaskPrompt(
if (depIdentifier) subtaskDepMap.set(dep.taskId, depIdentifier);
}
// Resolve dependency task identifiers
const depTaskIds = [...new Set(dependencies.map((d: any) => d.dependsOnId))];
const depTasks = await taskModel.findByIds(depTaskIds);
const depIdToIdentifier = new Map(depTasks.map((t: any) => [t.id, t.identifier]));
// Resolve parent task context (identifier + sibling subtasks)
let parentIdentifier: string | null = null;
let parentTaskContext:
@@ -198,7 +208,10 @@ async function buildTaskPrompt(
parentTask: parentTaskContext,
task: {
assigneeAgentId: task.assigneeAgentId,
dependencies: dependencies.map((d: any) => ({ dependsOn: d.dependsOnId, type: d.type })),
dependencies: dependencies.map((d: any) => ({
dependsOn: depIdToIdentifier.get(d.dependsOnId) ?? d.dependsOnId,
type: d.type,
})),
description: task.description,
id: task.id,
identifier: task.identifier,
@@ -486,10 +499,8 @@ export const taskRouter = router({
if (task.status === 'running' && task.heartbeatTimeout && task.lastHeartbeatAt) {
const elapsed = (Date.now() - new Date(task.lastHeartbeatAt).getTime()) / 1000;
if (elapsed > task.heartbeatTimeout) {
// Mark task as paused and running topics as timeout
await model.updateStatus(task.id, 'paused', { error: 'Heartbeat timeout' });
await ctx.taskTopicModel.timeoutRunning(task.id);
// Re-fetch updated task
task = await resolveOrThrow(model, input.id);
}
}
@@ -497,49 +508,14 @@ export const taskRouter = router({
// Clear stale heartbeat timeout error if task is no longer running
if (task.status !== 'running' && task.error === 'Heartbeat timeout') {
await model.update(task.id, { error: null });
task = { ...task, error: null };
}
// Parallel fetch all related data
const briefModel = ctx.briefModel;
const [subtasks, dependencies, topics, briefs, comments, documents] = await Promise.all([
model.findSubtasks(task.id),
model.getDependencies(task.id),
ctx.taskTopicModel.findWithDetails(task.id),
briefModel.findByTaskId(task.id),
model.getComments(task.id),
model.getTreePinnedDocuments(task.id),
]);
// Fetch dependencies between subtasks
const subtaskIds = subtasks.map((s) => s.id);
const subtaskDeps = await model.getDependenciesByTaskIds(subtaskIds);
// Resolve parent info
let parent: { identifier: string; name: string | null } | null = null;
if (task.parentTaskId) {
const parentTask = await model.findById(task.parentTaskId);
if (parentTask) {
parent = { identifier: parentTask.identifier, name: parentTask.name };
}
const detail = await ctx.taskService.getTaskDetail(task.identifier);
if (!detail) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Task not found' });
}
return {
data: {
...task,
parent,
briefs,
checkpoint: model.getCheckpointConfig(task),
comments,
dependencies,
documents,
review: model.getReviewConfig(task),
subtaskDeps,
subtasks,
topics,
},
success: true,
};
return { data: detail, success: true };
} catch (error) {
if (error instanceof TRPCError) throw error;
console.error('[task:detail]', error);

View File

@@ -0,0 +1,162 @@
import type { TaskDetailData, TaskDetailWorkspaceNode, WorkspaceData } from '@lobechat/types';
import { BriefModel } from '@/database/models/brief';
import { TaskModel } from '@/database/models/task';
import { TaskTopicModel } from '@/database/models/taskTopic';
import type { LobeChatDatabase } from '@/database/type';
const emptyWorkspace: WorkspaceData = { nodeMap: {}, tree: [] };
export class TaskService {
private briefModel: BriefModel;
private taskModel: TaskModel;
private taskTopicModel: TaskTopicModel;
constructor(db: LobeChatDatabase, userId: string) {
this.taskModel = new TaskModel(db, userId);
this.taskTopicModel = new TaskTopicModel(db, userId);
this.briefModel = new BriefModel(db, userId);
}
async getTaskDetail(taskIdOrIdentifier: string): Promise<TaskDetailData | null> {
const task = await this.taskModel.resolve(taskIdOrIdentifier);
if (!task) return null;
const [subtasks, dependencies, topics, briefs, comments, workspace] = await Promise.all([
this.taskModel.findSubtasks(task.id),
this.taskModel.getDependencies(task.id),
this.taskTopicModel.findWithHandoff(task.id).catch(() => []),
this.briefModel.findByTaskId(task.id).catch(() => []),
this.taskModel.getComments(task.id).catch(() => []),
this.taskModel.getTreePinnedDocuments(task.id).catch(() => emptyWorkspace),
]);
// Build subtask dependency map
const subtaskIds = subtasks.map((s) => s.id);
const subtaskDeps =
subtaskIds.length > 0
? await this.taskModel.getDependenciesByTaskIds(subtaskIds).catch(() => [])
: [];
const idToIdentifier = new Map(subtasks.map((s) => [s.id, s.identifier]));
const depMap = new Map<string, string>();
for (const dep of subtaskDeps) {
const depId = idToIdentifier.get(dep.dependsOnId);
if (depId) depMap.set(dep.taskId, depId);
}
// Resolve dependency task identifiers
const depTaskIds = [...new Set(dependencies.map((d) => d.dependsOnId))];
const depTasks = await this.taskModel.findByIds(depTaskIds);
const depIdToInfo = new Map(
depTasks.map((t) => [t.id, { identifier: t.identifier, name: t.name }]),
);
// Resolve parent
let parent: { identifier: string; name: string | null } | null = null;
if (task.parentTaskId) {
const parentTask = await this.taskModel.findById(task.parentTaskId);
if (parentTask) {
parent = { identifier: parentTask.identifier, name: parentTask.name };
}
}
// Build workspace tree (recursive)
const buildWorkspaceNodes = (treeNodes: typeof workspace.tree): TaskDetailWorkspaceNode[] =>
treeNodes.map((node) => {
const doc = workspace.nodeMap[node.id];
return {
children: node.children.length > 0 ? buildWorkspaceNodes(node.children) : undefined,
documentId: node.id,
fileType: doc?.fileType,
size: doc?.charCount,
sourceTaskIdentifier: doc?.sourceTaskIdentifier,
title: doc?.title,
};
});
const workspaceFolders = buildWorkspaceNodes(workspace.tree);
// Build timeline
const toISO = (d: Date | string | null | undefined) =>
d ? new Date(d).toISOString() : undefined;
const timelineTopics = topics.map((t) => ({
id: t.topicId,
seq: t.seq,
status: t.status,
time: toISO(t.createdAt),
title: t.handoffTitle || 'Untitled',
}));
const timelineBriefs = briefs.map((b) => ({
id: b.id,
priority: b.priority,
resolvedAction: b.resolvedAction
? b.resolvedComment
? `${b.resolvedAction}: ${b.resolvedComment}`
: b.resolvedAction
: undefined,
summary: b.summary,
time: toISO(b.createdAt),
title: b.title,
type: b.type,
}));
const timelineComments = comments.map((c) => ({
agentId: c.agentId,
content: c.content,
time: toISO(c.createdAt),
}));
const hasTimeline =
timelineTopics.length > 0 || timelineBriefs.length > 0 || timelineComments.length > 0;
return {
agentId: task.assigneeAgentId,
checkpoint: this.taskModel.getCheckpointConfig(task),
createdAt: task.createdAt ? new Date(task.createdAt).toISOString() : undefined,
dependencies: dependencies.map((d) => {
const info = depIdToInfo.get(d.dependsOnId);
return {
dependsOn: info?.identifier ?? d.dependsOnId,
name: info?.name,
type: d.type,
};
}),
description: task.description,
error: task.error,
heartbeat:
task.heartbeatTimeout || task.lastHeartbeatAt
? {
interval: task.heartbeatInterval,
lastAt: task.lastHeartbeatAt ? new Date(task.lastHeartbeatAt).toISOString() : null,
timeout: task.heartbeatTimeout,
}
: undefined,
identifier: task.identifier,
instruction: task.instruction,
name: task.name,
parent,
priority: task.priority,
review: this.taskModel.getReviewConfig(task),
status: task.status,
userId: task.assigneeUserId,
subtasks: subtasks.map((s) => ({
blockedBy: depMap.get(s.id),
identifier: s.identifier,
name: s.name,
priority: s.priority,
status: s.status,
})),
timeline: hasTimeline
? {
briefs: timelineBriefs.length > 0 ? timelineBriefs : undefined,
comments: timelineComments.length > 0 ? timelineComments : undefined,
topics: timelineTopics.length > 0 ? timelineTopics : undefined,
}
: undefined,
topicCount: timelineTopics.length > 0 ? timelineTopics.length : undefined,
workspace: workspaceFolders.length > 0 ? workspaceFolders : undefined,
};
}
}

View File

@@ -87,7 +87,19 @@ export class TaskLifecycleService {
if (shouldSkipPause) return; // auto-retry in progress, don't pause
}
// 4. Checkpoint — pause for user review
// 4. Check if agent delivered a result brief → auto-complete
// If the latest brief is type 'result' and no review is configured, complete the task
const reviewConfig = currentTask ? this.taskModel.getReviewConfig(currentTask) : null;
if (!reviewConfig?.enabled) {
const briefs = await this.briefModel.findByTaskId(taskId);
const latestBrief = briefs[0]; // sorted by createdAt desc
if (latestBrief?.type === 'result') {
await this.taskModel.updateStatus(taskId, 'completed', { error: null });
return;
}
}
// 5. Checkpoint — pause for user review
if (currentTask && this.taskModel.shouldPauseOnTopicComplete(currentTask)) {
await this.taskModel.updateStatus(taskId, 'paused', { error: null });
}

View File

@@ -7,7 +7,15 @@ import { TaskModel } from '@/database/models/task';
import { type ServerRuntimeRegistration } from './types';
const createBriefRuntime = (briefModel: BriefModel, taskModel: TaskModel, taskId?: string) => ({
const createBriefRuntime = ({
briefModel,
taskId,
taskModel,
}: {
briefModel: BriefModel;
taskId?: string;
taskModel: TaskModel;
}) => ({
createBrief: async (args: {
actions?: Array<{ key: string; label: string; type: string }>;
priority?: string;
@@ -64,7 +72,7 @@ export const briefRuntime: ServerRuntimeRegistration = {
const briefModel = new BriefModel(context.serverDB, context.userId);
const taskModel = new TaskModel(context.serverDB, context.userId);
return createBriefRuntime(briefModel, taskModel, context.taskId);
return createBriefRuntime({ briefModel, taskId: context.taskId, taskModel });
},
identifier: BriefIdentifier,
};

View File

@@ -10,10 +10,19 @@ import {
} from '@lobechat/prompts';
import { TaskModel } from '@/database/models/task';
import { TaskService } from '@/server/services/task';
import { type ServerRuntimeRegistration } from './types';
const createTaskRuntime = (taskModel: TaskModel, taskId?: string) => ({
const createTaskRuntime = ({
taskId,
taskModel,
taskService,
}: {
taskId?: string;
taskModel: TaskModel;
taskService: TaskService;
}) => ({
createTask: async (args: {
instruction: string;
name: string;
@@ -177,36 +186,41 @@ const createTaskRuntime = (taskModel: TaskModel, taskId?: string) => ({
};
},
viewTask: async (args: { identifier?: string }) => {
let task;
if (args.identifier) {
task = await taskModel.resolve(args.identifier);
} else if (taskId) {
task = await taskModel.findById(taskId);
} else {
updateTaskStatus: async (args: { identifier?: string; status: string }) => {
const id = args.identifier || taskId;
if (!id) {
return {
content: 'No task identifier provided and no current task context.',
success: false,
};
}
if (!task) return { content: `Task not found: ${args.identifier || taskId}`, success: false };
const task = await taskModel.resolve(id);
if (!task) return { content: `Task not found: ${id}`, success: false };
const subtasks = await taskModel.findSubtasks(task.id);
const deps = await taskModel.getDependencies(task.id);
const updated = await taskModel.updateStatus(task.id, args.status);
if (!updated) return { content: `Failed to update task ${task.identifier}`, success: false };
return {
content: formatTaskDetail({
dependencies: deps.map((d) => ({ dependsOn: d.dependsOnId, type: d.type })),
identifier: task.identifier,
instruction: task.instruction,
name: task.name,
parentTaskId: task.parentTaskId,
priority: task.priority,
status: task.status,
subtasks,
}),
content: `Task ${task.identifier} status updated to ${args.status}.`,
success: true,
};
},
viewTask: async (args: { identifier?: string }) => {
const id = args.identifier || taskId;
if (!id) {
return {
content: 'No task identifier provided and no current task context.',
success: false,
};
}
const detail = await taskService.getTaskDetail(id);
if (!detail) return { content: `Task not found: ${id}`, success: false };
return {
content: formatTaskDetail(detail),
success: true,
};
},
@@ -219,8 +233,9 @@ export const taskRuntime: ServerRuntimeRegistration = {
}
const taskModel = new TaskModel(context.serverDB, context.userId);
const taskService = new TaskService(context.serverDB, context.userId);
return createTaskRuntime(taskModel, context.taskId);
return createTaskRuntime({ taskId: context.taskId, taskModel, taskService });
},
identifier: TaskIdentifier,
};