mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
feat: add background agents with scheduling support
- Add background task scheduling system with cron-based triggers - Add background-task-detail component for viewing agent status - Add agent schedule repo and state management - Update sidebar to show background agents section - Remove old workflow-authoring and workflow-run-ops skills - Add IPC handlers for agent schedule operations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
82db06d724
commit
c447a42d07
20 changed files with 1544 additions and 500 deletions
|
|
@ -24,6 +24,9 @@ import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
|
||||||
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
|
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
|
||||||
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
|
import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js';
|
||||||
import * as composioHandler from './composio-handler.js';
|
import * as composioHandler from './composio-handler.js';
|
||||||
|
import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js';
|
||||||
|
import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js';
|
||||||
|
import { triggerRun as triggerAgentScheduleRun } from '@x/core/dist/agent-schedule/runner.js';
|
||||||
|
|
||||||
type InvokeChannels = ipc.InvokeChannels;
|
type InvokeChannels = ipc.InvokeChannels;
|
||||||
type IPCChannels = ipc.IPCChannels;
|
type IPCChannels = ipc.IPCChannels;
|
||||||
|
|
@ -384,5 +387,38 @@ export function setupIpcHandlers() {
|
||||||
'composio:execute-action': async (_event, args) => {
|
'composio:execute-action': async (_event, args) => {
|
||||||
return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input);
|
return composioHandler.executeAction(args.actionSlug, args.toolkitSlug, args.input);
|
||||||
},
|
},
|
||||||
|
// Agent schedule handlers
|
||||||
|
'agent-schedule:getConfig': async () => {
|
||||||
|
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');
|
||||||
|
try {
|
||||||
|
return await repo.getConfig();
|
||||||
|
} catch {
|
||||||
|
// Return empty config if file doesn't exist
|
||||||
|
return { agents: {} };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'agent-schedule:getState': async () => {
|
||||||
|
const repo = container.resolve<IAgentScheduleStateRepo>('agentScheduleStateRepo');
|
||||||
|
try {
|
||||||
|
return await repo.getState();
|
||||||
|
} catch {
|
||||||
|
// Return empty state if file doesn't exist
|
||||||
|
return { agents: {} };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'agent-schedule:updateAgent': async (_event, args) => {
|
||||||
|
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');
|
||||||
|
await repo.upsert(args.agentName, args.entry);
|
||||||
|
// Trigger the runner to pick up the change immediately
|
||||||
|
triggerAgentScheduleRun();
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
'agent-schedule:deleteAgent': async (_event, args) => {
|
||||||
|
const repo = container.resolve<IAgentScheduleRepo>('agentScheduleRepo');
|
||||||
|
const stateRepo = container.resolve<IAgentScheduleStateRepo>('agentScheduleStateRepo');
|
||||||
|
await repo.delete(args.agentName);
|
||||||
|
await stateRepo.deleteAgentState(args.agentName);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { init as initFirefliesSync } from "@x/core/dist/knowledge/sync_fireflies
|
||||||
import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js";
|
import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js";
|
||||||
import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js";
|
import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js";
|
||||||
import { init as initPreBuiltRunner } from "@x/core/dist/pre_built/runner.js";
|
import { init as initPreBuiltRunner } from "@x/core/dist/pre_built/runner.js";
|
||||||
|
import { init as initAgentRunner } from "@x/core/dist/agent-schedule/runner.js";
|
||||||
import { initConfigs } from "@x/core/dist/config/initConfigs.js";
|
import { initConfigs } from "@x/core/dist/config/initConfigs.js";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
|
@ -156,6 +157,9 @@ app.whenReady().then(async () => {
|
||||||
// start pre-built agent runner
|
// start pre-built agent runner
|
||||||
initPreBuiltRunner();
|
initPreBuiltRunner();
|
||||||
|
|
||||||
|
// start background agent runner (scheduled agents)
|
||||||
|
initAgentRunner();
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,9 @@ import { Separator } from "@/components/ui/separator"
|
||||||
import { Toaster } from "@/components/ui/sonner"
|
import { Toaster } from "@/components/ui/sonner"
|
||||||
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
|
import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links'
|
||||||
import { OnboardingModal } from '@/components/onboarding-modal'
|
import { OnboardingModal } from '@/components/onboarding-modal'
|
||||||
|
import { BackgroundTaskDetail } from '@/components/background-task-detail'
|
||||||
|
import { AgentScheduleConfig } from '@x/shared/dist/agent-schedule.js'
|
||||||
|
import { AgentScheduleState } from '@x/shared/dist/agent-schedule-state.js'
|
||||||
|
|
||||||
type DirEntry = z.infer<typeof workspace.DirEntry>
|
type DirEntry = z.infer<typeof workspace.DirEntry>
|
||||||
type RunEventType = z.infer<typeof RunEvent>
|
type RunEventType = z.infer<typeof RunEvent>
|
||||||
|
|
@ -499,6 +502,22 @@ function App() {
|
||||||
// Onboarding state
|
// Onboarding state
|
||||||
const [showOnboarding, setShowOnboarding] = useState(false)
|
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||||
|
|
||||||
|
// Background tasks state
|
||||||
|
type BackgroundTaskItem = {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
schedule: z.infer<typeof AgentScheduleConfig>["agents"][string]["schedule"]
|
||||||
|
enabled: boolean
|
||||||
|
startingMessage?: string
|
||||||
|
status?: z.infer<typeof AgentScheduleState>["agents"][string]["status"]
|
||||||
|
nextRunAt?: string | null
|
||||||
|
lastRunAt?: string | null
|
||||||
|
lastError?: string | null
|
||||||
|
runCount?: number
|
||||||
|
}
|
||||||
|
const [backgroundTasks, setBackgroundTasks] = useState<BackgroundTaskItem[]>([])
|
||||||
|
const [selectedBackgroundTask, setSelectedBackgroundTask] = useState<string | null>(null)
|
||||||
|
|
||||||
// Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures)
|
// Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runIdRef.current = runId
|
runIdRef.current = runId
|
||||||
|
|
@ -528,12 +547,17 @@ function App() {
|
||||||
const cleanup = window.ipc.on('workspace:didChange', async (event) => {
|
const cleanup = window.ipc.on('workspace:didChange', async (event) => {
|
||||||
loadDirectory().then(setTree)
|
loadDirectory().then(setTree)
|
||||||
|
|
||||||
// Reload current file if it was changed externally
|
|
||||||
if (!selectedPath) return
|
|
||||||
|
|
||||||
const changedPath = event.type === 'changed' ? event.path : null
|
const changedPath = event.type === 'changed' ? event.path : null
|
||||||
const changedPaths = (event.type === 'bulkChanged' ? event.paths : []) ?? []
|
const changedPaths = (event.type === 'bulkChanged' ? event.paths : []) ?? []
|
||||||
|
|
||||||
|
// Reload background tasks if agent-schedule.json changed
|
||||||
|
if (changedPath === 'config/agent-schedule.json' || changedPaths.includes('config/agent-schedule.json')) {
|
||||||
|
loadBackgroundTasks()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload current file if it was changed externally
|
||||||
|
if (!selectedPath) return
|
||||||
|
|
||||||
const isCurrentFileChanged =
|
const isCurrentFileChanged =
|
||||||
changedPath === selectedPath || changedPaths.includes(selectedPath)
|
changedPath === selectedPath || changedPaths.includes(selectedPath)
|
||||||
|
|
||||||
|
|
@ -548,6 +572,7 @@ function App() {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return cleanup
|
return cleanup
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [loadDirectory, selectedPath, editorContent])
|
}, [loadDirectory, selectedPath, editorContent])
|
||||||
|
|
||||||
// Load file content when selected
|
// Load file content when selected
|
||||||
|
|
@ -663,6 +688,63 @@ function App() {
|
||||||
loadRuns()
|
loadRuns()
|
||||||
}, [loadRuns])
|
}, [loadRuns])
|
||||||
|
|
||||||
|
// Load background tasks
|
||||||
|
const loadBackgroundTasks = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const [configResult, stateResult] = await Promise.all([
|
||||||
|
window.ipc.invoke('agent-schedule:getConfig', null),
|
||||||
|
window.ipc.invoke('agent-schedule:getState', null),
|
||||||
|
])
|
||||||
|
|
||||||
|
const tasks: BackgroundTaskItem[] = Object.entries(configResult.agents).map(([name, entry]) => {
|
||||||
|
const state = stateResult.agents[name]
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
description: entry.description,
|
||||||
|
schedule: entry.schedule,
|
||||||
|
enabled: entry.enabled ?? true,
|
||||||
|
startingMessage: entry.startingMessage,
|
||||||
|
status: state?.status,
|
||||||
|
nextRunAt: state?.nextRunAt,
|
||||||
|
lastRunAt: state?.lastRunAt,
|
||||||
|
lastError: state?.lastError,
|
||||||
|
runCount: state?.runCount ?? 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setBackgroundTasks(tasks)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load background tasks:', err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Load background tasks on mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadBackgroundTasks()
|
||||||
|
}, [loadBackgroundTasks])
|
||||||
|
|
||||||
|
// Handle toggling background task enabled state
|
||||||
|
const handleToggleBackgroundTask = useCallback(async (taskName: string, enabled: boolean) => {
|
||||||
|
const task = backgroundTasks.find(t => t.name === taskName)
|
||||||
|
if (!task) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.ipc.invoke('agent-schedule:updateAgent', {
|
||||||
|
agentName: taskName,
|
||||||
|
entry: {
|
||||||
|
schedule: task.schedule,
|
||||||
|
enabled,
|
||||||
|
startingMessage: task.startingMessage,
|
||||||
|
description: task.description,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
// Reload to get updated state
|
||||||
|
await loadBackgroundTasks()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to update background task:', err)
|
||||||
|
}
|
||||||
|
}, [backgroundTasks, loadBackgroundTasks])
|
||||||
|
|
||||||
// Load a specific run and populate conversation
|
// Load a specific run and populate conversation
|
||||||
const loadRun = useCallback(async (id: string) => {
|
const loadRun = useCallback(async (id: string) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1169,6 +1251,7 @@ function App() {
|
||||||
setPendingAskHumanRequests(new Map())
|
setPendingAskHumanRequests(new Map())
|
||||||
setAllPermissionRequests(new Map())
|
setAllPermissionRequests(new Map())
|
||||||
setPermissionResponses(new Map())
|
setPermissionResponses(new Map())
|
||||||
|
setSelectedBackgroundTask(null)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleChatInputSubmit = (text: string) => {
|
const handleChatInputSubmit = (text: string) => {
|
||||||
|
|
@ -1193,6 +1276,8 @@ function App() {
|
||||||
// Clear forward history when navigating to a new file
|
// Clear forward history when navigating to a new file
|
||||||
setFileHistoryForward([])
|
setFileHistoryForward([])
|
||||||
setSelectedPath(path)
|
setSelectedPath(path)
|
||||||
|
// Clear background task selection when navigating to a file
|
||||||
|
setSelectedBackgroundTask(null)
|
||||||
}, [selectedPath])
|
}, [selectedPath])
|
||||||
|
|
||||||
const navigateBack = useCallback(() => {
|
const navigateBack = useCallback(() => {
|
||||||
|
|
@ -1686,7 +1771,16 @@ function App() {
|
||||||
const conversationContentClassName = hasConversation
|
const conversationContentClassName = hasConversation
|
||||||
? "mx-auto w-full max-w-4xl pb-28"
|
? "mx-auto w-full max-w-4xl pb-28"
|
||||||
: "mx-auto w-full max-w-4xl min-h-full items-center justify-center pb-0"
|
: "mx-auto w-full max-w-4xl min-h-full items-center justify-center pb-0"
|
||||||
const headerTitle = selectedPath ? selectedPath : (isGraphOpen ? 'Graph View' : 'Chat')
|
const headerTitle = selectedPath
|
||||||
|
? selectedPath
|
||||||
|
: isGraphOpen
|
||||||
|
? 'Graph View'
|
||||||
|
: selectedBackgroundTask
|
||||||
|
? `Background Task: ${selectedBackgroundTask}`
|
||||||
|
: 'Chat'
|
||||||
|
const selectedTask = selectedBackgroundTask
|
||||||
|
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
|
|
@ -1716,8 +1810,18 @@ function App() {
|
||||||
currentRunId={runId}
|
currentRunId={runId}
|
||||||
tasksActions={{
|
tasksActions={{
|
||||||
onNewChat: handleNewChat,
|
onNewChat: handleNewChat,
|
||||||
onSelectRun: loadRun,
|
onSelectRun: (runIdToLoad) => {
|
||||||
|
setSelectedBackgroundTask(null)
|
||||||
|
loadRun(runIdToLoad)
|
||||||
|
},
|
||||||
|
onSelectBackgroundTask: (taskName) => {
|
||||||
|
setSelectedBackgroundTask(taskName)
|
||||||
|
setSelectedPath(null)
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
backgroundTasks={backgroundTasks}
|
||||||
|
selectedBackgroundTask={selectedBackgroundTask}
|
||||||
/>
|
/>
|
||||||
<SidebarInset className="overflow-hidden! min-h-0">
|
<SidebarInset className="overflow-hidden! min-h-0">
|
||||||
{/* Header with sidebar triggers */}
|
{/* Header with sidebar triggers */}
|
||||||
|
|
@ -1819,6 +1923,21 @@ function App() {
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
) : selectedTask ? (
|
||||||
|
<div className="flex-1 min-h-0 overflow-hidden">
|
||||||
|
<BackgroundTaskDetail
|
||||||
|
name={selectedTask.name}
|
||||||
|
description={selectedTask.description}
|
||||||
|
schedule={selectedTask.schedule}
|
||||||
|
enabled={selectedTask.enabled}
|
||||||
|
status={selectedTask.status}
|
||||||
|
nextRunAt={selectedTask.nextRunAt}
|
||||||
|
lastRunAt={selectedTask.lastRunAt}
|
||||||
|
lastError={selectedTask.lastError}
|
||||||
|
runCount={selectedTask.runCount}
|
||||||
|
onToggleEnabled={(enabled) => handleToggleBackgroundTask(selectedTask.name, enabled)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col">
|
||||||
<Conversation className="relative flex-1 overflow-y-auto [scrollbar-gutter:stable]">
|
<Conversation className="relative flex-1 overflow-y-auto [scrollbar-gutter:stable]">
|
||||||
|
|
|
||||||
175
apps/x/apps/renderer/src/components/background-task-detail.tsx
Normal file
175
apps/x/apps/renderer/src/components/background-task-detail.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
import { Bot, Calendar, Clock, AlertCircle, CheckCircle } from "lucide-react"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
|
||||||
|
interface BackgroundTaskSchedule {
|
||||||
|
type: "cron" | "window" | "once"
|
||||||
|
expression?: string
|
||||||
|
cron?: string
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
runAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackgroundTaskDetailProps {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
schedule: BackgroundTaskSchedule
|
||||||
|
enabled: boolean
|
||||||
|
status?: "scheduled" | "running" | "finished" | "failed" | "triggered"
|
||||||
|
nextRunAt?: string | null
|
||||||
|
lastRunAt?: string | null
|
||||||
|
lastError?: string | null
|
||||||
|
runCount?: number
|
||||||
|
onToggleEnabled: (enabled: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatScheduleDescription(schedule: BackgroundTaskSchedule): string {
|
||||||
|
switch (schedule.type) {
|
||||||
|
case "cron":
|
||||||
|
return `Runs on cron schedule: ${schedule.expression}`
|
||||||
|
case "window":
|
||||||
|
return `Runs once between ${schedule.startTime} and ${schedule.endTime} based on: ${schedule.cron}`
|
||||||
|
case "once":
|
||||||
|
return `Runs once at ${schedule.runAt}`
|
||||||
|
default:
|
||||||
|
return "Unknown schedule type"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(isoString: string | null | undefined): string {
|
||||||
|
if (!isoString) return "Never"
|
||||||
|
try {
|
||||||
|
const date = new Date(isoString)
|
||||||
|
return date.toLocaleString()
|
||||||
|
} catch {
|
||||||
|
return isoString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BackgroundTaskDetail({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
schedule,
|
||||||
|
enabled,
|
||||||
|
status,
|
||||||
|
nextRunAt,
|
||||||
|
lastRunAt,
|
||||||
|
lastError,
|
||||||
|
runCount = 0,
|
||||||
|
onToggleEnabled,
|
||||||
|
}: BackgroundTaskDetailProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="border-b border-border px-6 py-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center justify-center size-10 rounded-lg bg-primary/10">
|
||||||
|
<Bot className="size-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h1 className="text-xl font-semibold truncate">{name}</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">Background Agent</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||||
|
{/* Description */}
|
||||||
|
{description && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-sm font-medium text-muted-foreground mb-2">Description</h2>
|
||||||
|
<p className="text-sm">{description}</p>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Schedule */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-sm font-medium text-muted-foreground mb-2">Schedule</h2>
|
||||||
|
<div className="bg-muted/50 rounded-lg p-4 space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Calendar className="size-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm font-medium capitalize">{schedule.type} Schedule</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{formatScheduleDescription(schedule)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Enabled Toggle - hide for completed one-time schedules */}
|
||||||
|
{status === "triggered" ? (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-sm font-medium text-muted-foreground mb-2">Status</h2>
|
||||||
|
<div className="bg-muted/50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="size-4 text-green-500" />
|
||||||
|
<p className="text-sm font-medium">Completed</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
This one-time agent has finished running and will not run again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-sm font-medium text-muted-foreground mb-2">Status</h2>
|
||||||
|
<div className="flex items-center justify-between bg-muted/50 rounded-lg p-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">{enabled ? "Enabled" : "Disabled"}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{enabled ? "This agent will run according to its schedule" : "This agent is paused and will not run"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onCheckedChange={onToggleEnabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Run Statistics */}
|
||||||
|
<section>
|
||||||
|
<h2 className="text-sm font-medium text-muted-foreground mb-2">Run History</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-muted/50 rounded-lg p-4">
|
||||||
|
<p className="text-2xl font-semibold">{runCount}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Total Runs</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted/50 rounded-lg p-4">
|
||||||
|
<p className="text-sm font-medium">{formatDateTime(lastRunAt)}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Last Run</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Next Run */}
|
||||||
|
{nextRunAt && schedule.type !== "once" && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-sm font-medium text-muted-foreground mb-2">Next Scheduled Run</h2>
|
||||||
|
<div className="bg-muted/50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="size-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm">{formatDateTime(nextRunAt)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Last Error */}
|
||||||
|
{lastError && (
|
||||||
|
<section>
|
||||||
|
<h2 className="text-sm font-medium text-red-500 mb-2">Last Error</h2>
|
||||||
|
<div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertCircle className="size-4 text-red-500 mt-0.5 shrink-0" />
|
||||||
|
<p className="text-sm text-red-700 dark:text-red-400">{lastError}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import {
|
import {
|
||||||
|
Bot,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsDownUp,
|
ChevronsDownUp,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
|
|
@ -78,9 +79,27 @@ type RunListItem = {
|
||||||
agentId: string
|
agentId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BackgroundTaskItem = {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
schedule: {
|
||||||
|
type: "cron" | "window" | "once"
|
||||||
|
expression?: string
|
||||||
|
cron?: string
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
runAt?: string
|
||||||
|
}
|
||||||
|
enabled: boolean
|
||||||
|
status?: "scheduled" | "running" | "finished" | "failed" | "triggered"
|
||||||
|
nextRunAt?: string | null
|
||||||
|
lastRunAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
type TasksActions = {
|
type TasksActions = {
|
||||||
onNewChat: () => void
|
onNewChat: () => void
|
||||||
onSelectRun: (runId: string) => void
|
onSelectRun: (runId: string) => void
|
||||||
|
onSelectBackgroundTask?: (taskName: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type SidebarContentPanelProps = {
|
type SidebarContentPanelProps = {
|
||||||
|
|
@ -93,6 +112,8 @@ type SidebarContentPanelProps = {
|
||||||
runs?: RunListItem[]
|
runs?: RunListItem[]
|
||||||
currentRunId?: string | null
|
currentRunId?: string | null
|
||||||
tasksActions?: TasksActions
|
tasksActions?: TasksActions
|
||||||
|
backgroundTasks?: BackgroundTaskItem[]
|
||||||
|
selectedBackgroundTask?: string | null
|
||||||
} & React.ComponentProps<typeof Sidebar>
|
} & React.ComponentProps<typeof Sidebar>
|
||||||
|
|
||||||
const sectionTitles = {
|
const sectionTitles = {
|
||||||
|
|
@ -110,6 +131,8 @@ export function SidebarContentPanel({
|
||||||
runs = [],
|
runs = [],
|
||||||
currentRunId,
|
currentRunId,
|
||||||
tasksActions,
|
tasksActions,
|
||||||
|
backgroundTasks = [],
|
||||||
|
selectedBackgroundTask,
|
||||||
...props
|
...props
|
||||||
}: SidebarContentPanelProps) {
|
}: SidebarContentPanelProps) {
|
||||||
const { activeSection } = useSidebarSection()
|
const { activeSection } = useSidebarSection()
|
||||||
|
|
@ -137,6 +160,8 @@ export function SidebarContentPanel({
|
||||||
runs={runs}
|
runs={runs}
|
||||||
currentRunId={currentRunId}
|
currentRunId={currentRunId}
|
||||||
actions={tasksActions}
|
actions={tasksActions}
|
||||||
|
backgroundTasks={backgroundTasks}
|
||||||
|
selectedBackgroundTask={selectedBackgroundTask}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
|
|
@ -653,15 +678,40 @@ function Tree({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get status indicator color
|
||||||
|
function getStatusColor(status?: string, enabled?: boolean): string {
|
||||||
|
// Disabled agents always show gray
|
||||||
|
if (enabled === false) {
|
||||||
|
return "bg-gray-400"
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case "running":
|
||||||
|
return "bg-blue-500"
|
||||||
|
case "finished":
|
||||||
|
return "bg-green-500"
|
||||||
|
case "failed":
|
||||||
|
return "bg-red-500"
|
||||||
|
case "triggered":
|
||||||
|
return "bg-gray-400"
|
||||||
|
case "scheduled":
|
||||||
|
default:
|
||||||
|
return "bg-yellow-500"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Tasks Section
|
// Tasks Section
|
||||||
function TasksSection({
|
function TasksSection({
|
||||||
runs,
|
runs,
|
||||||
currentRunId,
|
currentRunId,
|
||||||
actions,
|
actions,
|
||||||
|
backgroundTasks = [],
|
||||||
|
selectedBackgroundTask,
|
||||||
}: {
|
}: {
|
||||||
runs: RunListItem[]
|
runs: RunListItem[]
|
||||||
currentRunId?: string | null
|
currentRunId?: string | null
|
||||||
actions?: TasksActions
|
actions?: TasksActions
|
||||||
|
backgroundTasks?: BackgroundTaskItem[]
|
||||||
|
selectedBackgroundTask?: string | null
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SidebarGroup className="flex-1 flex flex-col overflow-hidden">
|
<SidebarGroup className="flex-1 flex flex-col overflow-hidden">
|
||||||
|
|
@ -677,9 +727,38 @@ function TasksSection({
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</div>
|
</div>
|
||||||
<SidebarGroupContent className="flex-1 overflow-y-auto">
|
<SidebarGroupContent className="flex-1 overflow-y-auto">
|
||||||
{runs.length > 0 && (
|
{/* Background Tasks Section */}
|
||||||
|
{backgroundTasks.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
|
Background Tasks
|
||||||
|
</div>
|
||||||
|
<SidebarMenu>
|
||||||
|
{backgroundTasks.map((task) => (
|
||||||
|
<SidebarMenuItem key={task.name}>
|
||||||
|
<SidebarMenuButton
|
||||||
|
isActive={selectedBackgroundTask === task.name}
|
||||||
|
onClick={() => actions?.onSelectBackgroundTask?.(task.name)}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Bot className="size-4 shrink-0" />
|
||||||
|
<span
|
||||||
|
className={`absolute -bottom-0.5 -right-0.5 size-2 rounded-full ${getStatusColor(task.status, task.enabled)} ${task.status === "running" && task.enabled ? "animate-pulse" : ""}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className={`truncate text-sm ${!task.enabled ? "text-muted-foreground" : ""}`}>
|
||||||
|
{task.name}
|
||||||
|
</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
</SidebarMenu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{runs.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="px-3 py-1.5 mt-4 text-xs font-medium text-muted-foreground">
|
||||||
Chat history
|
Chat history
|
||||||
</div>
|
</div>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@
|
||||||
"@ai-sdk/anthropic": "^2.0.44",
|
"@ai-sdk/anthropic": "^2.0.44",
|
||||||
"@ai-sdk/google": "^2.0.25",
|
"@ai-sdk/google": "^2.0.25",
|
||||||
"@ai-sdk/openai": "^2.0.53",
|
"@ai-sdk/openai": "^2.0.53",
|
||||||
"@composio/core": "^0.6.0",
|
|
||||||
"@ai-sdk/openai-compatible": "^1.0.27",
|
"@ai-sdk/openai-compatible": "^1.0.27",
|
||||||
"@ai-sdk/provider": "^2.0.0",
|
"@ai-sdk/provider": "^2.0.0",
|
||||||
|
"@composio/core": "^0.6.0",
|
||||||
"@google-cloud/local-auth": "^3.0.1",
|
"@google-cloud/local-auth": "^3.0.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.6",
|
"@openrouter/ai-sdk-provider": "^1.2.6",
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
"ai": "^5.0.102",
|
"ai": "^5.0.102",
|
||||||
"awilix": "^12.0.5",
|
"awilix": "^12.0.5",
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
|
"cron-parser": "^5.5.0",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
"google-auth-library": "^10.5.0",
|
"google-auth-library": "^10.5.0",
|
||||||
"googleapis": "^169.0.0",
|
"googleapis": "^169.0.0",
|
||||||
|
|
|
||||||
43
apps/x/packages/core/src/agent-schedule/repo.ts
Normal file
43
apps/x/packages/core/src/agent-schedule/repo.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { WorkDir } from "../config/config.js";
|
||||||
|
import { AgentScheduleConfig, AgentScheduleEntry } from "@x/shared/dist/agent-schedule.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const DEFAULT_AGENT_SCHEDULES: z.infer<typeof AgentScheduleConfig>["agents"] = {};
|
||||||
|
|
||||||
|
export interface IAgentScheduleRepo {
|
||||||
|
ensureConfig(): Promise<void>;
|
||||||
|
getConfig(): Promise<z.infer<typeof AgentScheduleConfig>>;
|
||||||
|
upsert(agentName: string, entry: z.infer<typeof AgentScheduleEntry>): Promise<void>;
|
||||||
|
delete(agentName: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FSAgentScheduleRepo implements IAgentScheduleRepo {
|
||||||
|
private readonly configPath = path.join(WorkDir, "config", "agent-schedule.json");
|
||||||
|
|
||||||
|
async ensureConfig(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.access(this.configPath);
|
||||||
|
} catch {
|
||||||
|
await fs.writeFile(this.configPath, JSON.stringify({ agents: DEFAULT_AGENT_SCHEDULES }, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfig(): Promise<z.infer<typeof AgentScheduleConfig>> {
|
||||||
|
const config = await fs.readFile(this.configPath, "utf8");
|
||||||
|
return AgentScheduleConfig.parse(JSON.parse(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsert(agentName: string, entry: z.infer<typeof AgentScheduleEntry>): Promise<void> {
|
||||||
|
const conf = await this.getConfig();
|
||||||
|
conf.agents[agentName] = entry;
|
||||||
|
await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(agentName: string): Promise<void> {
|
||||||
|
const conf = await this.getConfig();
|
||||||
|
delete conf.agents[agentName];
|
||||||
|
await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
335
apps/x/packages/core/src/agent-schedule/runner.ts
Normal file
335
apps/x/packages/core/src/agent-schedule/runner.ts
Normal file
|
|
@ -0,0 +1,335 @@
|
||||||
|
import { CronExpressionParser } from "cron-parser";
|
||||||
|
import container from "../di/container.js";
|
||||||
|
import { IAgentScheduleRepo } from "./repo.js";
|
||||||
|
import { IAgentScheduleStateRepo } from "./state-repo.js";
|
||||||
|
import { IRunsRepo } from "../runs/repo.js";
|
||||||
|
import { IAgentRuntime } from "../agents/runtime.js";
|
||||||
|
import { IMonotonicallyIncreasingIdGenerator } from "../application/lib/id-gen.js";
|
||||||
|
import { AgentScheduleConfig, AgentScheduleEntry } from "@x/shared/dist/agent-schedule.js";
|
||||||
|
import { AgentScheduleState, AgentScheduleStateEntry } from "@x/shared/dist/agent-schedule-state.js";
|
||||||
|
import { MessageEvent } from "@x/shared/dist/runs.js";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const DEFAULT_STARTING_MESSAGE = "go";
|
||||||
|
|
||||||
|
const POLL_INTERVAL_MS = 60 * 1000; // 1 minute
|
||||||
|
const TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Date to local ISO 8601 string (without Z suffix).
|
||||||
|
* Example: "2024-02-05T08:30:00"
|
||||||
|
*/
|
||||||
|
function toLocalISOString(date: Date): string {
|
||||||
|
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Wake Signal for Immediate Run Trigger ---
|
||||||
|
let wakeResolve: (() => void) | null = null;
|
||||||
|
|
||||||
|
export function triggerRun(): void {
|
||||||
|
if (wakeResolve) {
|
||||||
|
console.log("[AgentRunner] Triggered - waking up immediately");
|
||||||
|
wakeResolve();
|
||||||
|
wakeResolve = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function interruptibleSleep(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
wakeResolve = null;
|
||||||
|
resolve();
|
||||||
|
}, ms);
|
||||||
|
wakeResolve = () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the next run time for a schedule.
|
||||||
|
* Returns ISO datetime string or null if schedule shouldn't run again.
|
||||||
|
*/
|
||||||
|
function calculateNextRunAt(
|
||||||
|
schedule: z.infer<typeof AgentScheduleEntry>["schedule"]
|
||||||
|
): string | null {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
switch (schedule.type) {
|
||||||
|
case "cron": {
|
||||||
|
try {
|
||||||
|
const interval = CronExpressionParser.parse(schedule.expression, {
|
||||||
|
currentDate: now,
|
||||||
|
});
|
||||||
|
return toLocalISOString(interval.next().toDate());
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentRunner] Invalid cron expression:", schedule.expression, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "window": {
|
||||||
|
try {
|
||||||
|
// Parse base cron to get the next occurrence date
|
||||||
|
const interval = CronExpressionParser.parse(schedule.cron, {
|
||||||
|
currentDate: now,
|
||||||
|
});
|
||||||
|
const nextDate = interval.next().toDate();
|
||||||
|
|
||||||
|
// Parse start and end times
|
||||||
|
const [startHour, startMin] = schedule.startTime.split(":").map(Number);
|
||||||
|
const [endHour, endMin] = schedule.endTime.split(":").map(Number);
|
||||||
|
|
||||||
|
// Pick a random time within the window
|
||||||
|
const startMinutes = startHour * 60 + startMin;
|
||||||
|
const endMinutes = endHour * 60 + endMin;
|
||||||
|
const randomMinutes = startMinutes + Math.floor(Math.random() * (endMinutes - startMinutes));
|
||||||
|
|
||||||
|
nextDate.setHours(Math.floor(randomMinutes / 60), randomMinutes % 60, 0, 0);
|
||||||
|
return toLocalISOString(nextDate);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentRunner] Invalid window schedule:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "once": {
|
||||||
|
// Once schedules don't have a "next" run - they're done after first run
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an agent should run now based on its schedule and state.
|
||||||
|
*/
|
||||||
|
function shouldRunNow(
|
||||||
|
entry: z.infer<typeof AgentScheduleEntry>,
|
||||||
|
state: z.infer<typeof AgentScheduleStateEntry> | null
|
||||||
|
): boolean {
|
||||||
|
// Don't run if disabled
|
||||||
|
if (entry.enabled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't run if already running
|
||||||
|
if (state?.status === "running") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't run once-schedules that are already triggered
|
||||||
|
if (entry.schedule.type === "once" && state?.status === "triggered") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// For once-schedules without state, check if runAt time has passed
|
||||||
|
if (entry.schedule.type === "once") {
|
||||||
|
const runAt = new Date(entry.schedule.runAt);
|
||||||
|
return now >= runAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For cron and window schedules, check nextRunAt
|
||||||
|
if (!state?.nextRunAt) {
|
||||||
|
// No nextRunAt set - needs to be initialized, so run now
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextRunAt = new Date(state.nextRunAt);
|
||||||
|
return now >= nextRunAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a single agent.
|
||||||
|
*/
|
||||||
|
async function runAgent(
|
||||||
|
agentName: string,
|
||||||
|
entry: z.infer<typeof AgentScheduleEntry>,
|
||||||
|
stateRepo: IAgentScheduleStateRepo,
|
||||||
|
runsRepo: IRunsRepo,
|
||||||
|
agentRuntime: IAgentRuntime,
|
||||||
|
idGenerator: IMonotonicallyIncreasingIdGenerator
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`[AgentRunner] Starting agent: ${agentName}`);
|
||||||
|
|
||||||
|
const startedAt = toLocalISOString(new Date());
|
||||||
|
|
||||||
|
// Update state to running with startedAt timestamp
|
||||||
|
await stateRepo.updateAgentState(agentName, {
|
||||||
|
status: "running",
|
||||||
|
startedAt: startedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a new run
|
||||||
|
const run = await runsRepo.create({ agentId: agentName });
|
||||||
|
console.log(`[AgentRunner] Created run ${run.id} for agent ${agentName}`);
|
||||||
|
|
||||||
|
// Add the starting message as a user message
|
||||||
|
const startingMessage = entry.startingMessage ?? DEFAULT_STARTING_MESSAGE;
|
||||||
|
const messageEvent: z.infer<typeof MessageEvent> = {
|
||||||
|
runId: run.id,
|
||||||
|
type: "message",
|
||||||
|
messageId: await idGenerator.next(),
|
||||||
|
message: {
|
||||||
|
role: "user",
|
||||||
|
content: startingMessage,
|
||||||
|
},
|
||||||
|
subflow: [],
|
||||||
|
};
|
||||||
|
await runsRepo.appendEvents(run.id, [messageEvent]);
|
||||||
|
console.log(`[AgentRunner] Sent starting message to agent ${agentName}: "${startingMessage}"`);
|
||||||
|
|
||||||
|
// Trigger the run
|
||||||
|
await agentRuntime.trigger(run.id);
|
||||||
|
|
||||||
|
// Calculate next run time
|
||||||
|
const nextRunAt = calculateNextRunAt(entry.schedule);
|
||||||
|
|
||||||
|
// Update state to finished (clear startedAt)
|
||||||
|
const currentState = await stateRepo.getAgentState(agentName);
|
||||||
|
await stateRepo.updateAgentState(agentName, {
|
||||||
|
status: entry.schedule.type === "once" ? "triggered" : "finished",
|
||||||
|
startedAt: null,
|
||||||
|
lastRunAt: toLocalISOString(new Date()),
|
||||||
|
nextRunAt: nextRunAt,
|
||||||
|
lastError: null,
|
||||||
|
runCount: (currentState?.runCount ?? 0) + 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`[AgentRunner] Finished agent: ${agentName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[AgentRunner] Error running agent ${agentName}:`, error);
|
||||||
|
|
||||||
|
// Calculate next run time even on failure (for retry)
|
||||||
|
const nextRunAt = calculateNextRunAt(entry.schedule);
|
||||||
|
|
||||||
|
// Update state to failed (clear startedAt)
|
||||||
|
const currentState = await stateRepo.getAgentState(agentName);
|
||||||
|
await stateRepo.updateAgentState(agentName, {
|
||||||
|
status: "failed",
|
||||||
|
startedAt: null,
|
||||||
|
lastRunAt: toLocalISOString(new Date()),
|
||||||
|
nextRunAt: nextRunAt,
|
||||||
|
lastError: error instanceof Error ? error.message : String(error),
|
||||||
|
runCount: (currentState?.runCount ?? 0) + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for timed-out agents and mark them as failed.
|
||||||
|
*/
|
||||||
|
async function checkForTimeouts(
|
||||||
|
state: z.infer<typeof AgentScheduleState>,
|
||||||
|
config: z.infer<typeof AgentScheduleConfig>,
|
||||||
|
stateRepo: IAgentScheduleStateRepo
|
||||||
|
): Promise<void> {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
for (const [agentName, agentState] of Object.entries(state.agents)) {
|
||||||
|
if (agentState.status === "running" && agentState.startedAt) {
|
||||||
|
const startedAt = new Date(agentState.startedAt);
|
||||||
|
const elapsed = now.getTime() - startedAt.getTime();
|
||||||
|
|
||||||
|
if (elapsed > TIMEOUT_MS) {
|
||||||
|
console.log(`[AgentRunner] Agent ${agentName} timed out after ${Math.round(elapsed / 1000 / 60)} minutes`);
|
||||||
|
|
||||||
|
// Get schedule entry for calculating next run
|
||||||
|
const entry = config.agents[agentName];
|
||||||
|
const nextRunAt = entry ? calculateNextRunAt(entry.schedule) : null;
|
||||||
|
|
||||||
|
await stateRepo.updateAgentState(agentName, {
|
||||||
|
status: "failed",
|
||||||
|
startedAt: null,
|
||||||
|
lastRunAt: toLocalISOString(now),
|
||||||
|
nextRunAt: nextRunAt,
|
||||||
|
lastError: `Timed out after ${Math.round(elapsed / 1000 / 60)} minutes`,
|
||||||
|
runCount: (agentState.runCount ?? 0) + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main polling loop.
|
||||||
|
*/
|
||||||
|
async function pollAndRun(): Promise<void> {
|
||||||
|
const scheduleRepo = container.resolve<IAgentScheduleRepo>("agentScheduleRepo");
|
||||||
|
const stateRepo = container.resolve<IAgentScheduleStateRepo>("agentScheduleStateRepo");
|
||||||
|
const runsRepo = container.resolve<IRunsRepo>("runsRepo");
|
||||||
|
const agentRuntime = container.resolve<IAgentRuntime>("agentRuntime");
|
||||||
|
const idGenerator = container.resolve<IMonotonicallyIncreasingIdGenerator>("idGenerator");
|
||||||
|
|
||||||
|
// Load config and state
|
||||||
|
let config: z.infer<typeof AgentScheduleConfig>;
|
||||||
|
let state: z.infer<typeof AgentScheduleState>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
config = await scheduleRepo.getConfig();
|
||||||
|
state = await stateRepo.getState();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentRunner] Error loading config/state:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for timed-out agents first
|
||||||
|
await checkForTimeouts(state, config, stateRepo);
|
||||||
|
|
||||||
|
// Reload state after timeout checks (state may have changed)
|
||||||
|
try {
|
||||||
|
state = await stateRepo.getState();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentRunner] Error reloading state:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each agent
|
||||||
|
for (const [agentName, entry] of Object.entries(config.agents)) {
|
||||||
|
const agentState = state.agents[agentName] ?? null;
|
||||||
|
|
||||||
|
// Initialize state if needed (set nextRunAt for new agents)
|
||||||
|
if (!agentState && entry.schedule.type !== "once") {
|
||||||
|
const nextRunAt = calculateNextRunAt(entry.schedule);
|
||||||
|
if (nextRunAt) {
|
||||||
|
await stateRepo.updateAgentState(agentName, {
|
||||||
|
status: "scheduled",
|
||||||
|
startedAt: null,
|
||||||
|
lastRunAt: null,
|
||||||
|
nextRunAt: nextRunAt,
|
||||||
|
lastError: null,
|
||||||
|
runCount: 0,
|
||||||
|
});
|
||||||
|
console.log(`[AgentRunner] Initialized state for ${agentName}, next run at ${nextRunAt}`);
|
||||||
|
}
|
||||||
|
continue; // Don't run immediately on first initialization
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRunNow(entry, agentState)) {
|
||||||
|
// Run agent (don't await - let it run in background)
|
||||||
|
runAgent(agentName, entry, stateRepo, runsRepo, agentRuntime, idGenerator).catch((error) => {
|
||||||
|
console.error(`[AgentRunner] Unhandled error in runAgent for ${agentName}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the background agent runner service.
|
||||||
|
* Polls every minute to check for agents that need to run.
|
||||||
|
*/
|
||||||
|
export async function init(): Promise<void> {
|
||||||
|
console.log("[AgentRunner] Starting background agent runner service");
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
await pollAndRun();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentRunner] Error in main loop:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await interruptibleSleep(POLL_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
apps/x/packages/core/src/agent-schedule/state-repo.ts
Normal file
64
apps/x/packages/core/src/agent-schedule/state-repo.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { WorkDir } from "../config/config.js";
|
||||||
|
import { AgentScheduleState, AgentScheduleStateEntry } from "@x/shared/dist/agent-schedule-state.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
const DEFAULT_AGENT_SCHEDULE_STATE: z.infer<typeof AgentScheduleState>["agents"] = {};
|
||||||
|
|
||||||
|
export interface IAgentScheduleStateRepo {
|
||||||
|
ensureState(): Promise<void>;
|
||||||
|
getState(): Promise<z.infer<typeof AgentScheduleState>>;
|
||||||
|
getAgentState(agentName: string): Promise<z.infer<typeof AgentScheduleStateEntry> | null>;
|
||||||
|
updateAgentState(agentName: string, entry: Partial<z.infer<typeof AgentScheduleStateEntry>>): Promise<void>;
|
||||||
|
setAgentState(agentName: string, entry: z.infer<typeof AgentScheduleStateEntry>): Promise<void>;
|
||||||
|
deleteAgentState(agentName: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FSAgentScheduleStateRepo implements IAgentScheduleStateRepo {
|
||||||
|
private readonly statePath = path.join(WorkDir, "config", "agent-schedule-state.json");
|
||||||
|
|
||||||
|
async ensureState(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.access(this.statePath);
|
||||||
|
} catch {
|
||||||
|
await fs.writeFile(this.statePath, JSON.stringify({ agents: DEFAULT_AGENT_SCHEDULE_STATE }, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getState(): Promise<z.infer<typeof AgentScheduleState>> {
|
||||||
|
const state = await fs.readFile(this.statePath, "utf8");
|
||||||
|
return AgentScheduleState.parse(JSON.parse(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAgentState(agentName: string): Promise<z.infer<typeof AgentScheduleStateEntry> | null> {
|
||||||
|
const state = await this.getState();
|
||||||
|
return state.agents[agentName] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAgentState(agentName: string, entry: Partial<z.infer<typeof AgentScheduleStateEntry>>): Promise<void> {
|
||||||
|
const state = await this.getState();
|
||||||
|
const existing = state.agents[agentName] ?? {
|
||||||
|
status: "scheduled" as const,
|
||||||
|
startedAt: null,
|
||||||
|
lastRunAt: null,
|
||||||
|
nextRunAt: null,
|
||||||
|
lastError: null,
|
||||||
|
runCount: 0,
|
||||||
|
};
|
||||||
|
state.agents[agentName] = { ...existing, ...entry };
|
||||||
|
await fs.writeFile(this.statePath, JSON.stringify(state, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAgentState(agentName: string, entry: z.infer<typeof AgentScheduleStateEntry>): Promise<void> {
|
||||||
|
const state = await this.getState();
|
||||||
|
state.agents[agentName] = entry;
|
||||||
|
await fs.writeFile(this.statePath, JSON.stringify(state, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAgentState(agentName: string): Promise<void> {
|
||||||
|
const state = await this.getState();
|
||||||
|
delete state.agents[agentName];
|
||||||
|
await fs.writeFile(this.statePath, JSON.stringify(state, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,555 @@
|
||||||
|
export const skill = String.raw`
|
||||||
|
# Background Agents
|
||||||
|
|
||||||
|
Load this skill whenever a user wants to inspect, create, edit, or schedule background agents inside the Rowboat workspace.
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
**IMPORTANT**: In the CLI, there are NO separate "workflow" files. Everything is an agent.
|
||||||
|
|
||||||
|
- **All definitions live in ` + "`agents/*.md`" + `** - Markdown files with YAML frontmatter
|
||||||
|
- Agents configure a model, tools (in frontmatter), and instructions (in the body)
|
||||||
|
- Tools can be: builtin (like ` + "`executeCommand`" + `), MCP integrations, or **other agents**
|
||||||
|
- **"Workflows" are just agents that orchestrate other agents** by having them as tools
|
||||||
|
- **Background agents run on schedules** defined in ` + "`~/.rowboat/config/agent-schedule.json`" + `
|
||||||
|
|
||||||
|
## How multi-agent workflows work
|
||||||
|
|
||||||
|
1. **Create an orchestrator agent** that has other agents in its ` + "`tools`" + `
|
||||||
|
2. **Schedule the orchestrator** in agent-schedule.json (see Scheduling section below)
|
||||||
|
3. The orchestrator calls other agents as tools when needed
|
||||||
|
4. Data flows through tool call parameters and responses
|
||||||
|
|
||||||
|
## Scheduling Background Agents
|
||||||
|
|
||||||
|
Background agents run automatically based on schedules defined in ` + "`~/.rowboat/config/agent-schedule.json`" + `.
|
||||||
|
|
||||||
|
### Schedule Configuration File
|
||||||
|
|
||||||
|
` + "```json" + `
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"agent_name": {
|
||||||
|
"schedule": { ... },
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
### Schedule Types
|
||||||
|
|
||||||
|
**IMPORTANT: All times are in local time** (the timezone of the machine running Rowboat).
|
||||||
|
|
||||||
|
**1. Cron Schedule** - Runs at exact times defined by cron expression
|
||||||
|
` + "```json" + `
|
||||||
|
{
|
||||||
|
"schedule": {
|
||||||
|
"type": "cron",
|
||||||
|
"expression": "0 8 * * *"
|
||||||
|
},
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
Common cron expressions:
|
||||||
|
- ` + "`*/5 * * * *`" + ` - Every 5 minutes
|
||||||
|
- ` + "`0 8 * * *`" + ` - Every day at 8am
|
||||||
|
- ` + "`0 9 * * 1`" + ` - Every Monday at 9am
|
||||||
|
- ` + "`0 0 1 * *`" + ` - First day of every month at midnight
|
||||||
|
|
||||||
|
**2. Window Schedule** - Runs once during a time window
|
||||||
|
` + "```json" + `
|
||||||
|
{
|
||||||
|
"schedule": {
|
||||||
|
"type": "window",
|
||||||
|
"cron": "0 0 * * *",
|
||||||
|
"startTime": "08:00",
|
||||||
|
"endTime": "10:00"
|
||||||
|
},
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
The agent will run once at a random time within the window. Use this when you want flexibility (e.g., "sometime in the morning" rather than "exactly at 8am").
|
||||||
|
|
||||||
|
**3. Once Schedule** - Runs exactly once at a specific time
|
||||||
|
` + "```json" + `
|
||||||
|
{
|
||||||
|
"schedule": {
|
||||||
|
"type": "once",
|
||||||
|
"runAt": "2024-02-05T10:30:00"
|
||||||
|
},
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
Use this for one-time tasks like migrations or setup scripts. The ` + "`runAt`" + ` is in local time (no Z suffix).
|
||||||
|
|
||||||
|
### Starting Message
|
||||||
|
|
||||||
|
You can specify a ` + "`startingMessage`" + ` that gets sent to the agent when it starts. If not provided, defaults to ` + "`\"go\"`" + `.
|
||||||
|
|
||||||
|
` + "```json" + `
|
||||||
|
{
|
||||||
|
"schedule": { "type": "cron", "expression": "0 8 * * *" },
|
||||||
|
"enabled": true,
|
||||||
|
"startingMessage": "Please summarize my emails from the last 24 hours"
|
||||||
|
}
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
You can add a ` + "`description`" + ` field to describe what the agent does. This is displayed in the UI.
|
||||||
|
|
||||||
|
` + "```json" + `
|
||||||
|
{
|
||||||
|
"schedule": { "type": "cron", "expression": "0 8 * * *" },
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Summarizes emails and calendar events every morning"
|
||||||
|
}
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
### Complete Schedule Example
|
||||||
|
|
||||||
|
` + "```json" + `
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"daily_digest": {
|
||||||
|
"schedule": {
|
||||||
|
"type": "cron",
|
||||||
|
"expression": "0 8 * * *"
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Daily email and calendar summary",
|
||||||
|
"startingMessage": "Summarize my emails and calendar for today"
|
||||||
|
},
|
||||||
|
"morning_briefing": {
|
||||||
|
"schedule": {
|
||||||
|
"type": "window",
|
||||||
|
"cron": "0 0 * * *",
|
||||||
|
"startTime": "07:00",
|
||||||
|
"endTime": "09:00"
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"description": "Morning news and updates briefing"
|
||||||
|
},
|
||||||
|
"one_time_setup": {
|
||||||
|
"schedule": {
|
||||||
|
"type": "once",
|
||||||
|
"runAt": "2024-12-01T12:00:00"
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"description": "One-time data migration task"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
### Schedule State (Read-Only)
|
||||||
|
|
||||||
|
**IMPORTANT: Do NOT modify ` + "`agent-schedule-state.json`" + `** - it is managed automatically by the background runner.
|
||||||
|
|
||||||
|
The runner automatically tracks execution state in ` + "`~/.rowboat/config/agent-schedule-state.json`" + `:
|
||||||
|
- ` + "`status`" + `: scheduled, running, finished, failed, triggered (for once-schedules)
|
||||||
|
- ` + "`lastRunAt`" + `: When the agent last ran
|
||||||
|
- ` + "`nextRunAt`" + `: When the agent will run next
|
||||||
|
- ` + "`lastError`" + `: Error message if the last run failed
|
||||||
|
- ` + "`runCount`" + `: Total number of runs
|
||||||
|
|
||||||
|
When you add an agent to ` + "`agent-schedule.json`" + `, the runner will automatically create and manage its state entry. You only need to edit ` + "`agent-schedule.json`" + `.
|
||||||
|
|
||||||
|
## Agent File Format
|
||||||
|
|
||||||
|
Agent files are **Markdown files with YAML frontmatter**. The frontmatter contains configuration (model, tools), and the body contains the instructions.
|
||||||
|
|
||||||
|
### Basic Structure
|
||||||
|
` + "```markdown" + `
|
||||||
|
---
|
||||||
|
model: gpt-5.1
|
||||||
|
tools:
|
||||||
|
tool_key:
|
||||||
|
type: builtin
|
||||||
|
name: tool_name
|
||||||
|
---
|
||||||
|
# Instructions
|
||||||
|
|
||||||
|
Your detailed instructions go here in Markdown format.
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
### Frontmatter Fields
|
||||||
|
- ` + "`model`" + `: (OPTIONAL) Model to use (e.g., 'gpt-5.1', 'claude-sonnet-4-5')
|
||||||
|
- ` + "`provider`" + `: (OPTIONAL) Provider alias from models.json
|
||||||
|
- ` + "`tools`" + `: (OPTIONAL) Object containing tool definitions
|
||||||
|
|
||||||
|
### Instructions (Body)
|
||||||
|
The Markdown body after the frontmatter contains the agent's instructions. Use standard Markdown formatting.
|
||||||
|
|
||||||
|
### Naming Rules
|
||||||
|
- Agent filename determines the agent name (without .md extension)
|
||||||
|
- Example: ` + "`summariser_agent.md`" + ` creates an agent named "summariser_agent"
|
||||||
|
- Use lowercase with underscores for multi-word names
|
||||||
|
- No spaces or special characters in names
|
||||||
|
- **The agent name in agent-schedule.json must match the filename** (without .md)
|
||||||
|
|
||||||
|
### Agent Format Example
|
||||||
|
` + "```markdown" + `
|
||||||
|
---
|
||||||
|
model: gpt-5.1
|
||||||
|
tools:
|
||||||
|
search:
|
||||||
|
type: mcp
|
||||||
|
name: firecrawl_search
|
||||||
|
description: Search the web
|
||||||
|
mcpServerName: firecrawl
|
||||||
|
inputSchema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
query:
|
||||||
|
type: string
|
||||||
|
description: Search query
|
||||||
|
required:
|
||||||
|
- query
|
||||||
|
---
|
||||||
|
# Web Search Agent
|
||||||
|
|
||||||
|
You are a web search agent. When asked a question:
|
||||||
|
|
||||||
|
1. Use the search tool to find relevant information
|
||||||
|
2. Summarize the results clearly
|
||||||
|
3. Cite your sources
|
||||||
|
|
||||||
|
Be concise and accurate.
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
## Tool Types & Schemas
|
||||||
|
|
||||||
|
Tools in agents must follow one of three types. Each has specific required fields.
|
||||||
|
|
||||||
|
### 1. Builtin Tools
|
||||||
|
Internal Rowboat tools (executeCommand, file operations, MCP queries, etc.)
|
||||||
|
|
||||||
|
**YAML Schema:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
tool_key:
|
||||||
|
type: builtin
|
||||||
|
name: tool_name
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
**Required fields:**
|
||||||
|
- ` + "`type`" + `: Must be "builtin"
|
||||||
|
- ` + "`name`" + `: Builtin tool name (e.g., "executeCommand", "workspace-readFile")
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
bash:
|
||||||
|
type: builtin
|
||||||
|
name: executeCommand
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
**Available builtin tools:**
|
||||||
|
- ` + "`executeCommand`" + ` - Execute shell commands
|
||||||
|
- ` + "`workspace-readFile`" + `, ` + "`workspace-writeFile`" + `, ` + "`workspace-remove`" + ` - File operations
|
||||||
|
- ` + "`workspace-readdir`" + `, ` + "`workspace-exists`" + `, ` + "`workspace-stat`" + ` - Directory operations
|
||||||
|
- ` + "`workspace-mkdir`" + `, ` + "`workspace-rename`" + `, ` + "`workspace-copy`" + ` - File/directory management
|
||||||
|
- ` + "`analyzeAgent`" + ` - Analyze agent structure
|
||||||
|
- ` + "`addMcpServer`" + `, ` + "`listMcpServers`" + `, ` + "`listMcpTools`" + ` - MCP management
|
||||||
|
- ` + "`loadSkill`" + ` - Load skill guidance
|
||||||
|
|
||||||
|
### 2. MCP Tools
|
||||||
|
Tools from external MCP servers (APIs, databases, web scraping, etc.)
|
||||||
|
|
||||||
|
**YAML Schema:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
tool_key:
|
||||||
|
type: mcp
|
||||||
|
name: tool_name_from_server
|
||||||
|
description: What the tool does
|
||||||
|
mcpServerName: server_name_from_config
|
||||||
|
inputSchema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
param:
|
||||||
|
type: string
|
||||||
|
description: Parameter description
|
||||||
|
required:
|
||||||
|
- param
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
**Required fields:**
|
||||||
|
- ` + "`type`" + `: Must be "mcp"
|
||||||
|
- ` + "`name`" + `: Exact tool name from MCP server
|
||||||
|
- ` + "`description`" + `: What the tool does (helps agent understand when to use it)
|
||||||
|
- ` + "`mcpServerName`" + `: Server name from config/mcp.json
|
||||||
|
- ` + "`inputSchema`" + `: Full JSON Schema object for tool parameters
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
search:
|
||||||
|
type: mcp
|
||||||
|
name: firecrawl_search
|
||||||
|
description: Search the web
|
||||||
|
mcpServerName: firecrawl
|
||||||
|
inputSchema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
query:
|
||||||
|
type: string
|
||||||
|
description: Search query
|
||||||
|
required:
|
||||||
|
- query
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
**Important:**
|
||||||
|
- Use ` + "`listMcpTools`" + ` to get the exact inputSchema from the server
|
||||||
|
- Copy the schema exactly—don't modify property types or structure
|
||||||
|
- Only include ` + "`required`" + ` array if parameters are mandatory
|
||||||
|
|
||||||
|
### 3. Agent Tools (for chaining agents)
|
||||||
|
Reference other agents as tools to build multi-agent workflows
|
||||||
|
|
||||||
|
**YAML Schema:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
tool_key:
|
||||||
|
type: agent
|
||||||
|
name: target_agent_name
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
**Required fields:**
|
||||||
|
- ` + "`type`" + `: Must be "agent"
|
||||||
|
- ` + "`name`" + `: Name of the target agent (must exist in agents/ directory)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
summariser:
|
||||||
|
type: agent
|
||||||
|
name: summariser_agent
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
- Use ` + "`type: agent`" + ` to call other agents as tools
|
||||||
|
- The target agent will be invoked with the parameters you pass
|
||||||
|
- Results are returned as tool output
|
||||||
|
- This is how you build multi-agent workflows
|
||||||
|
- The referenced agent file must exist (e.g., ` + "`agents/summariser_agent.md`" + `)
|
||||||
|
|
||||||
|
## Complete Multi-Agent Workflow Example
|
||||||
|
|
||||||
|
**Email digest workflow** - This is all done through agents calling other agents:
|
||||||
|
|
||||||
|
**1. Task-specific agent** (` + "`agents/email_reader.md`" + `):
|
||||||
|
` + "```markdown" + `
|
||||||
|
---
|
||||||
|
model: gpt-5.1
|
||||||
|
tools:
|
||||||
|
read_file:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-readFile
|
||||||
|
list_dir:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-readdir
|
||||||
|
---
|
||||||
|
# Email Reader Agent
|
||||||
|
|
||||||
|
Read emails from the gmail_sync folder and extract key information.
|
||||||
|
Look for unread or recent emails and summarize the sender, subject, and key points.
|
||||||
|
Don't ask for human input.
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
**2. Agent that delegates to other agents** (` + "`agents/daily_summary.md`" + `):
|
||||||
|
` + "```markdown" + `
|
||||||
|
---
|
||||||
|
model: gpt-5.1
|
||||||
|
tools:
|
||||||
|
email_reader:
|
||||||
|
type: agent
|
||||||
|
name: email_reader
|
||||||
|
write_file:
|
||||||
|
type: builtin
|
||||||
|
name: workspace-writeFile
|
||||||
|
---
|
||||||
|
# Daily Summary Agent
|
||||||
|
|
||||||
|
1. Use the email_reader tool to get email summaries
|
||||||
|
2. Create a consolidated daily digest
|
||||||
|
3. Save the digest to ~/Desktop/daily_digest.md
|
||||||
|
|
||||||
|
Don't ask for human input.
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
Note: The output path (` + "`~/Desktop/daily_digest.md`" + `) is hardcoded in the instructions. When creating agents that output files, always ask the user where they want files saved and include the full path in the agent instructions.
|
||||||
|
|
||||||
|
**3. Orchestrator agent** (` + "`agents/morning_briefing.md`" + `):
|
||||||
|
` + "```markdown" + `
|
||||||
|
---
|
||||||
|
model: gpt-5.1
|
||||||
|
tools:
|
||||||
|
daily_summary:
|
||||||
|
type: agent
|
||||||
|
name: daily_summary
|
||||||
|
search:
|
||||||
|
type: mcp
|
||||||
|
name: search
|
||||||
|
mcpServerName: exa
|
||||||
|
description: Search the web for news
|
||||||
|
inputSchema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
query:
|
||||||
|
type: string
|
||||||
|
description: Search query
|
||||||
|
---
|
||||||
|
# Morning Briefing Workflow
|
||||||
|
|
||||||
|
Create a morning briefing:
|
||||||
|
|
||||||
|
1. Get email digest using daily_summary
|
||||||
|
2. Search for relevant news using the search tool
|
||||||
|
3. Compile a comprehensive morning briefing
|
||||||
|
|
||||||
|
Execute these steps in sequence. Don't ask for human input.
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
**4. Schedule the workflow** in ` + "`~/.rowboat/config/agent-schedule.json`" + `:
|
||||||
|
` + "```json" + `
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"morning_briefing": {
|
||||||
|
"schedule": {
|
||||||
|
"type": "cron",
|
||||||
|
"expression": "0 7 * * *"
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
"startingMessage": "Create my morning briefing for today"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
This schedules the morning briefing workflow to run every day at 7am local time.
|
||||||
|
|
||||||
|
## Naming and organization rules
|
||||||
|
- **All agents live in ` + "`agents/*.md`" + `** - Markdown files with YAML frontmatter
|
||||||
|
- Agent filename (without .md) becomes the agent name
|
||||||
|
- When referencing an agent as a tool, use its filename without extension
|
||||||
|
- When scheduling an agent, use its filename without extension in agent-schedule.json
|
||||||
|
- Use relative paths (no \${BASE_DIR} prefixes) when giving examples to users
|
||||||
|
|
||||||
|
## Best practices for background agents
|
||||||
|
1. **Single responsibility**: Each agent should do one specific thing well
|
||||||
|
2. **Clear delegation**: Agent instructions should explicitly say when to call other agents
|
||||||
|
3. **Autonomous operation**: Add "Don't ask for human input" for background agents
|
||||||
|
4. **Data passing**: Make it clear what data to extract and pass between agents
|
||||||
|
5. **Tool naming**: Use descriptive tool keys (e.g., "summariser", "fetch_data", "analyze")
|
||||||
|
6. **Orchestration**: Create a top-level agent that coordinates the workflow
|
||||||
|
7. **Scheduling**: Use appropriate schedule types - cron for recurring, window for flexible timing, once for one-time tasks
|
||||||
|
8. **Error handling**: Background agents should handle errors gracefully since there's no human to intervene
|
||||||
|
9. **Avoid executeCommand**: Do NOT attach ` + "`executeCommand`" + ` to background agents as it poses security risks when running unattended. Instead, use the specific builtin tools needed (` + "`workspace-readFile`" + `, ` + "`workspace-writeFile`" + `, etc.) or MCP tools for external integrations
|
||||||
|
10. **File output paths**: When creating an agent that outputs files, ASK the user where the file should be stored (default to Desktop: ` + "`~/Desktop`" + `). Then hardcode the full output path in the agent's instructions so it knows exactly where to write files. Example instruction: "Save the output to /Users/username/Desktop/daily_report.md"
|
||||||
|
|
||||||
|
## Validation & Best Practices
|
||||||
|
|
||||||
|
### CRITICAL: Schema Compliance
|
||||||
|
- Agent files MUST be valid Markdown with YAML frontmatter
|
||||||
|
- Agent filename (without .md) becomes the agent name
|
||||||
|
- Tools in frontmatter MUST have valid ` + "`type`" + ` ("builtin", "mcp", or "agent")
|
||||||
|
- MCP tools MUST have all required fields: name, description, mcpServerName, inputSchema
|
||||||
|
- Agent tools MUST reference existing agent files
|
||||||
|
- Invalid agents will fail to load and prevent workflow execution
|
||||||
|
|
||||||
|
### File Creation/Update Process
|
||||||
|
1. When creating an agent, use ` + "`workspace-writeFile`" + ` with valid Markdown + YAML frontmatter
|
||||||
|
2. When updating an agent, read it first with ` + "`workspace-readFile`" + `, modify, then use ` + "`workspace-writeFile`" + `
|
||||||
|
3. Validate YAML syntax in frontmatter before writing—malformed YAML breaks the agent
|
||||||
|
4. **Quote strings containing colons** (e.g., ` + "`description: \"Default: 8\"`" + ` not ` + "`description: Default: 8`" + `)
|
||||||
|
5. Test agent loading after creation/update by using ` + "`analyzeAgent`" + `
|
||||||
|
|
||||||
|
### Common Validation Errors to Avoid
|
||||||
|
|
||||||
|
❌ **WRONG - Missing frontmatter delimiters:**
|
||||||
|
` + "```markdown" + `
|
||||||
|
model: gpt-5.1
|
||||||
|
# My Agent
|
||||||
|
Instructions here
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
❌ **WRONG - Invalid YAML indentation:**
|
||||||
|
` + "```markdown" + `
|
||||||
|
---
|
||||||
|
tools:
|
||||||
|
bash:
|
||||||
|
type: builtin
|
||||||
|
---
|
||||||
|
` + "```" + `
|
||||||
|
(bash should be indented under tools)
|
||||||
|
|
||||||
|
❌ **WRONG - Invalid tool type:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
tools:
|
||||||
|
tool1:
|
||||||
|
type: custom
|
||||||
|
name: something
|
||||||
|
` + "```" + `
|
||||||
|
(type must be builtin, mcp, or agent)
|
||||||
|
|
||||||
|
❌ **WRONG - Unquoted strings containing colons:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
tools:
|
||||||
|
search:
|
||||||
|
description: Number of results (default: 8)
|
||||||
|
` + "```" + `
|
||||||
|
(Strings with colons must be quoted: ` + "`description: \"Number of results (default: 8)\"`" + `)
|
||||||
|
|
||||||
|
❌ **WRONG - MCP tool missing required fields:**
|
||||||
|
` + "```yaml" + `
|
||||||
|
tools:
|
||||||
|
search:
|
||||||
|
type: mcp
|
||||||
|
name: firecrawl_search
|
||||||
|
` + "```" + `
|
||||||
|
(Missing: description, mcpServerName, inputSchema)
|
||||||
|
|
||||||
|
✅ **CORRECT - Minimal valid agent** (` + "`agents/simple_agent.md`" + `):
|
||||||
|
` + "```markdown" + `
|
||||||
|
---
|
||||||
|
model: gpt-5.1
|
||||||
|
---
|
||||||
|
# Simple Agent
|
||||||
|
|
||||||
|
Do simple tasks as instructed.
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
✅ **CORRECT - Agent with MCP tool** (` + "`agents/search_agent.md`" + `):
|
||||||
|
` + "```markdown" + `
|
||||||
|
---
|
||||||
|
model: gpt-5.1
|
||||||
|
tools:
|
||||||
|
search:
|
||||||
|
type: mcp
|
||||||
|
name: firecrawl_search
|
||||||
|
description: Search the web
|
||||||
|
mcpServerName: firecrawl
|
||||||
|
inputSchema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
query:
|
||||||
|
type: string
|
||||||
|
---
|
||||||
|
# Search Agent
|
||||||
|
|
||||||
|
Use the search tool to find information on the web.
|
||||||
|
` + "```" + `
|
||||||
|
|
||||||
|
## Capabilities checklist
|
||||||
|
1. Explore ` + "`agents/`" + ` directory to understand existing agents before editing
|
||||||
|
2. Read existing agents with ` + "`workspace-readFile`" + ` before making changes
|
||||||
|
3. Validate YAML frontmatter syntax before creating/updating agents
|
||||||
|
4. Use ` + "`analyzeAgent`" + ` to verify agent structure after creation/update
|
||||||
|
5. When creating multi-agent workflows, create an orchestrator agent
|
||||||
|
6. Add other agents as tools with ` + "`type: agent`" + ` for chaining
|
||||||
|
7. Use ` + "`listMcpServers`" + ` and ` + "`listMcpTools`" + ` when adding MCP integrations
|
||||||
|
8. Configure schedules in ` + "`~/.rowboat/config/agent-schedule.json`" + ` (ONLY edit this file, NOT the state file)
|
||||||
|
9. Confirm work done and outline next steps once changes are complete
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default skill;
|
||||||
|
|
@ -8,9 +8,8 @@ import mcpIntegrationSkill from "./mcp-integration/skill.js";
|
||||||
import meetingPrepSkill from "./meeting-prep/skill.js";
|
import meetingPrepSkill from "./meeting-prep/skill.js";
|
||||||
import organizeFilesSkill from "./organize-files/skill.js";
|
import organizeFilesSkill from "./organize-files/skill.js";
|
||||||
import slackSkill from "./slack/skill.js";
|
import slackSkill from "./slack/skill.js";
|
||||||
import workflowAuthoringSkill from "./workflow-authoring/skill.js";
|
import backgroundAgentsSkill from "./background-agents/skill.js";
|
||||||
import createPresentationsSkill from "./create-presentations/skill.js";
|
import createPresentationsSkill from "./create-presentations/skill.js";
|
||||||
import workflowRunOpsSkill from "./workflow-run-ops/skill.js";
|
|
||||||
|
|
||||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||||
const CATALOG_PREFIX = "src/application/assistant/skills";
|
const CATALOG_PREFIX = "src/application/assistant/skills";
|
||||||
|
|
@ -66,10 +65,10 @@ const definitions: SkillDefinition[] = [
|
||||||
content: slackSkill,
|
content: slackSkill,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "workflow-authoring",
|
id: "background-agents",
|
||||||
title: "Workflow Authoring",
|
title: "Background Agents",
|
||||||
summary: "Creating or editing workflows/agents, validating schema rules, and keeping filenames aligned with JSON ids.",
|
summary: "Creating, editing, and scheduling background agents. Configure schedules in agent-schedule.json and build multi-agent workflows.",
|
||||||
content: workflowAuthoringSkill,
|
content: backgroundAgentsSkill,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "builtin-tools",
|
id: "builtin-tools",
|
||||||
|
|
@ -89,12 +88,6 @@ const definitions: SkillDefinition[] = [
|
||||||
summary: "Following the confirmation process before removing workflows or agents and their dependencies.",
|
summary: "Following the confirmation process before removing workflows or agents and their dependencies.",
|
||||||
content: deletionGuardrailsSkill,
|
content: deletionGuardrailsSkill,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "workflow-run-ops",
|
|
||||||
title: "Workflow Run Operations",
|
|
||||||
summary: "Commands that list workflow runs, inspect paused executions, or manage cron schedules for workflows.",
|
|
||||||
content: workflowRunOpsSkill,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const skillEntries = definitions.map((definition) => ({
|
const skillEntries = definitions.map((definition) => ({
|
||||||
|
|
|
||||||
|
|
@ -1,384 +0,0 @@
|
||||||
export const skill = String.raw`
|
|
||||||
# Agent and Workflow Authoring
|
|
||||||
|
|
||||||
Load this skill whenever a user wants to inspect, create, or update agents inside the Rowboat workspace.
|
|
||||||
|
|
||||||
## Core Concepts
|
|
||||||
|
|
||||||
**IMPORTANT**: In the CLI, there are NO separate "workflow" files. Everything is an agent.
|
|
||||||
|
|
||||||
- **All definitions live in \`agents/*.md\`** - Markdown files with YAML frontmatter
|
|
||||||
- Agents configure a model, tools (in frontmatter), and instructions (in the body)
|
|
||||||
- Tools can be: builtin (like \`executeCommand\`), MCP integrations, or **other agents**
|
|
||||||
- **"Workflows" are just agents that orchestrate other agents** by having them as tools
|
|
||||||
|
|
||||||
## How multi-agent workflows work
|
|
||||||
|
|
||||||
1. **Create an orchestrator agent** that has other agents in its \`tools\`
|
|
||||||
2. **Run the orchestrator**: \`rowboatx --agent orchestrator_name\`
|
|
||||||
3. The orchestrator calls other agents as tools when needed
|
|
||||||
4. Data flows through tool call parameters and responses
|
|
||||||
|
|
||||||
## Agent File Format
|
|
||||||
|
|
||||||
Agent files are **Markdown files with YAML frontmatter**. The frontmatter contains configuration (model, tools), and the body contains the instructions.
|
|
||||||
|
|
||||||
### Basic Structure
|
|
||||||
\`\`\`markdown
|
|
||||||
---
|
|
||||||
model: gpt-5.1
|
|
||||||
tools:
|
|
||||||
tool_key:
|
|
||||||
type: builtin
|
|
||||||
name: tool_name
|
|
||||||
---
|
|
||||||
# Instructions
|
|
||||||
|
|
||||||
Your detailed instructions go here in Markdown format.
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
### Frontmatter Fields
|
|
||||||
- \`model\`: (OPTIONAL) Model to use (e.g., 'gpt-5.1', 'claude-sonnet-4-5')
|
|
||||||
- \`provider\`: (OPTIONAL) Provider alias from models.json
|
|
||||||
- \`tools\`: (OPTIONAL) Object containing tool definitions
|
|
||||||
|
|
||||||
### Instructions (Body)
|
|
||||||
The Markdown body after the frontmatter contains the agent's instructions. Use standard Markdown formatting.
|
|
||||||
|
|
||||||
### Naming Rules
|
|
||||||
- Agent filename determines the agent name (without .md extension)
|
|
||||||
- Example: \`summariser_agent.md\` creates an agent named "summariser_agent"
|
|
||||||
- Use lowercase with underscores for multi-word names
|
|
||||||
- No spaces or special characters in names
|
|
||||||
|
|
||||||
### Agent Format Example
|
|
||||||
\`\`\`markdown
|
|
||||||
---
|
|
||||||
model: gpt-5.1
|
|
||||||
tools:
|
|
||||||
search:
|
|
||||||
type: mcp
|
|
||||||
name: firecrawl_search
|
|
||||||
description: Search the web
|
|
||||||
mcpServerName: firecrawl
|
|
||||||
inputSchema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
query:
|
|
||||||
type: string
|
|
||||||
description: Search query
|
|
||||||
required:
|
|
||||||
- query
|
|
||||||
---
|
|
||||||
# Web Search Agent
|
|
||||||
|
|
||||||
You are a web search agent. When asked a question:
|
|
||||||
|
|
||||||
1. Use the search tool to find relevant information
|
|
||||||
2. Summarize the results clearly
|
|
||||||
3. Cite your sources
|
|
||||||
|
|
||||||
Be concise and accurate.
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Tool Types & Schemas
|
|
||||||
|
|
||||||
Tools in agents must follow one of three types. Each has specific required fields.
|
|
||||||
|
|
||||||
### 1. Builtin Tools
|
|
||||||
Internal Rowboat tools (executeCommand, file operations, MCP queries, etc.)
|
|
||||||
|
|
||||||
**YAML Schema:**
|
|
||||||
\`\`\`yaml
|
|
||||||
tool_key:
|
|
||||||
type: builtin
|
|
||||||
name: tool_name
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Required fields:**
|
|
||||||
- \`type\`: Must be "builtin"
|
|
||||||
- \`name\`: Builtin tool name (e.g., "executeCommand", "workspace-readFile")
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
\`\`\`yaml
|
|
||||||
bash:
|
|
||||||
type: builtin
|
|
||||||
name: executeCommand
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Available builtin tools:**
|
|
||||||
- \`executeCommand\` - Execute shell commands
|
|
||||||
- \`workspace-readFile\`, \`workspace-writeFile\`, \`workspace-remove\` - File operations
|
|
||||||
- \`workspace-readdir\`, \`workspace-exists\`, \`workspace-stat\` - Directory operations
|
|
||||||
- \`workspace-mkdir\`, \`workspace-rename\`, \`workspace-copy\` - File/directory management
|
|
||||||
- \`analyzeAgent\` - Analyze agent structure
|
|
||||||
- \`addMcpServer\`, \`listMcpServers\`, \`listMcpTools\` - MCP management
|
|
||||||
- \`loadSkill\` - Load skill guidance
|
|
||||||
|
|
||||||
### 2. MCP Tools
|
|
||||||
Tools from external MCP servers (APIs, databases, web scraping, etc.)
|
|
||||||
|
|
||||||
**YAML Schema:**
|
|
||||||
\`\`\`yaml
|
|
||||||
tool_key:
|
|
||||||
type: mcp
|
|
||||||
name: tool_name_from_server
|
|
||||||
description: What the tool does
|
|
||||||
mcpServerName: server_name_from_config
|
|
||||||
inputSchema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
param:
|
|
||||||
type: string
|
|
||||||
description: Parameter description
|
|
||||||
required:
|
|
||||||
- param
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Required fields:**
|
|
||||||
- \`type\`: Must be "mcp"
|
|
||||||
- \`name\`: Exact tool name from MCP server
|
|
||||||
- \`description\`: What the tool does (helps agent understand when to use it)
|
|
||||||
- \`mcpServerName\`: Server name from config/mcp.json
|
|
||||||
- \`inputSchema\`: Full JSON Schema object for tool parameters
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
\`\`\`yaml
|
|
||||||
search:
|
|
||||||
type: mcp
|
|
||||||
name: firecrawl_search
|
|
||||||
description: Search the web
|
|
||||||
mcpServerName: firecrawl
|
|
||||||
inputSchema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
query:
|
|
||||||
type: string
|
|
||||||
description: Search query
|
|
||||||
required:
|
|
||||||
- query
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Important:**
|
|
||||||
- Use \`listMcpTools\` to get the exact inputSchema from the server
|
|
||||||
- Copy the schema exactly—don't modify property types or structure
|
|
||||||
- Only include \`required\` array if parameters are mandatory
|
|
||||||
|
|
||||||
### 3. Agent Tools (for chaining agents)
|
|
||||||
Reference other agents as tools to build multi-agent workflows
|
|
||||||
|
|
||||||
**YAML Schema:**
|
|
||||||
\`\`\`yaml
|
|
||||||
tool_key:
|
|
||||||
type: agent
|
|
||||||
name: target_agent_name
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Required fields:**
|
|
||||||
- \`type\`: Must be "agent"
|
|
||||||
- \`name\`: Name of the target agent (must exist in agents/ directory)
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
\`\`\`yaml
|
|
||||||
summariser:
|
|
||||||
type: agent
|
|
||||||
name: summariser_agent
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**How it works:**
|
|
||||||
- Use \`type: agent\` to call other agents as tools
|
|
||||||
- The target agent will be invoked with the parameters you pass
|
|
||||||
- Results are returned as tool output
|
|
||||||
- This is how you build multi-agent workflows
|
|
||||||
- The referenced agent file must exist (e.g., \`agents/summariser_agent.md\`)
|
|
||||||
|
|
||||||
## Complete Multi-Agent Workflow Example
|
|
||||||
|
|
||||||
**Podcast creation workflow** - This is all done through agents calling other agents:
|
|
||||||
|
|
||||||
**1. Task-specific agent** (\`agents/summariser_agent.md\`):
|
|
||||||
\`\`\`markdown
|
|
||||||
---
|
|
||||||
model: gpt-5.1
|
|
||||||
tools:
|
|
||||||
bash:
|
|
||||||
type: builtin
|
|
||||||
name: executeCommand
|
|
||||||
---
|
|
||||||
# Summariser Agent
|
|
||||||
|
|
||||||
Download and summarise an arxiv paper. Use curl to fetch the PDF.
|
|
||||||
Output just the GIST in two lines. Don't ask for human input.
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**2. Agent that delegates to other agents** (\`agents/summarise-a-few.md\`):
|
|
||||||
\`\`\`markdown
|
|
||||||
---
|
|
||||||
model: gpt-5.1
|
|
||||||
tools:
|
|
||||||
summariser:
|
|
||||||
type: agent
|
|
||||||
name: summariser_agent
|
|
||||||
---
|
|
||||||
# Summarise Multiple Papers
|
|
||||||
|
|
||||||
Pick 2 interesting papers and summarise each using the summariser tool.
|
|
||||||
Pass the paper URL to the tool. Don't ask for human input.
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**3. Orchestrator agent** (\`agents/podcast_workflow.md\`):
|
|
||||||
\`\`\`markdown
|
|
||||||
---
|
|
||||||
model: gpt-5.1
|
|
||||||
tools:
|
|
||||||
bash:
|
|
||||||
type: builtin
|
|
||||||
name: executeCommand
|
|
||||||
summarise_papers:
|
|
||||||
type: agent
|
|
||||||
name: summarise-a-few
|
|
||||||
text_to_speech:
|
|
||||||
type: mcp
|
|
||||||
name: text_to_speech
|
|
||||||
mcpServerName: elevenLabs
|
|
||||||
description: Generate audio from text
|
|
||||||
inputSchema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
text:
|
|
||||||
type: string
|
|
||||||
description: Text to convert to speech
|
|
||||||
---
|
|
||||||
# Podcast Workflow
|
|
||||||
|
|
||||||
Create a podcast from arXiv papers:
|
|
||||||
|
|
||||||
1. Fetch arXiv papers about agents using bash
|
|
||||||
2. Pick papers and summarise them using summarise_papers
|
|
||||||
3. Create a podcast transcript
|
|
||||||
4. Generate audio using text_to_speech
|
|
||||||
|
|
||||||
Execute these steps in sequence.
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**To run this workflow**: \`rowboatx --agent podcast_workflow\`
|
|
||||||
|
|
||||||
## Naming and organization rules
|
|
||||||
- **All agents live in \`agents/*.md\`** - Markdown files with YAML frontmatter
|
|
||||||
- Agent filename (without .md) becomes the agent name
|
|
||||||
- When referencing an agent as a tool, use its filename without extension
|
|
||||||
- Use relative paths (no \${BASE_DIR} prefixes) when giving examples to users
|
|
||||||
|
|
||||||
## Best practices for multi-agent design
|
|
||||||
1. **Single responsibility**: Each agent should do one specific thing well
|
|
||||||
2. **Clear delegation**: Agent instructions should explicitly say when to call other agents
|
|
||||||
3. **Autonomous operation**: Add "Don't ask for human input" for autonomous workflows
|
|
||||||
4. **Data passing**: Make it clear what data to extract and pass between agents
|
|
||||||
5. **Tool naming**: Use descriptive tool keys (e.g., "summariser", "fetch_data", "analyze")
|
|
||||||
6. **Orchestration**: Create a top-level agent that coordinates the workflow
|
|
||||||
|
|
||||||
## Validation & Best Practices
|
|
||||||
|
|
||||||
### CRITICAL: Schema Compliance
|
|
||||||
- Agent files MUST be valid Markdown with YAML frontmatter
|
|
||||||
- Agent filename (without .md) becomes the agent name
|
|
||||||
- Tools in frontmatter MUST have valid \`type\` ("builtin", "mcp", or "agent")
|
|
||||||
- MCP tools MUST have all required fields: name, description, mcpServerName, inputSchema
|
|
||||||
- Agent tools MUST reference existing agent files
|
|
||||||
- Invalid agents will fail to load and prevent workflow execution
|
|
||||||
|
|
||||||
### File Creation/Update Process
|
|
||||||
1. When creating an agent, use \`workspace-writeFile\` with valid Markdown + YAML frontmatter
|
|
||||||
2. When updating an agent, read it first with \`workspace-readFile\`, modify, then use \`workspace-writeFile\`
|
|
||||||
3. Validate YAML syntax in frontmatter before writing—malformed YAML breaks the agent
|
|
||||||
4. **Quote strings containing colons** (e.g., \`description: "Default: 8"\` not \`description: Default: 8\`)
|
|
||||||
5. Test agent loading after creation/update by using \`analyzeAgent\`
|
|
||||||
|
|
||||||
### Common Validation Errors to Avoid
|
|
||||||
|
|
||||||
❌ **WRONG - Missing frontmatter delimiters:**
|
|
||||||
\`\`\`markdown
|
|
||||||
model: gpt-5.1
|
|
||||||
# My Agent
|
|
||||||
Instructions here
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
❌ **WRONG - Invalid YAML indentation:**
|
|
||||||
\`\`\`markdown
|
|
||||||
---
|
|
||||||
tools:
|
|
||||||
bash:
|
|
||||||
type: builtin
|
|
||||||
---
|
|
||||||
\`\`\`
|
|
||||||
(bash should be indented under tools)
|
|
||||||
|
|
||||||
❌ **WRONG - Invalid tool type:**
|
|
||||||
\`\`\`yaml
|
|
||||||
tools:
|
|
||||||
tool1:
|
|
||||||
type: custom
|
|
||||||
name: something
|
|
||||||
\`\`\`
|
|
||||||
(type must be builtin, mcp, or agent)
|
|
||||||
|
|
||||||
❌ **WRONG - Unquoted strings containing colons:**
|
|
||||||
\`\`\`yaml
|
|
||||||
tools:
|
|
||||||
search:
|
|
||||||
description: Number of results (default: 8)
|
|
||||||
\`\`\`
|
|
||||||
(Strings with colons must be quoted: \`description: "Number of results (default: 8)"\`)
|
|
||||||
|
|
||||||
❌ **WRONG - MCP tool missing required fields:**
|
|
||||||
\`\`\`yaml
|
|
||||||
tools:
|
|
||||||
search:
|
|
||||||
type: mcp
|
|
||||||
name: firecrawl_search
|
|
||||||
\`\`\`
|
|
||||||
(Missing: description, mcpServerName, inputSchema)
|
|
||||||
|
|
||||||
✅ **CORRECT - Minimal valid agent** (\`agents/simple_agent.md\`):
|
|
||||||
\`\`\`markdown
|
|
||||||
---
|
|
||||||
model: gpt-5.1
|
|
||||||
---
|
|
||||||
# Simple Agent
|
|
||||||
|
|
||||||
Do simple tasks as instructed.
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
✅ **CORRECT - Agent with MCP tool** (\`agents/search_agent.md\`):
|
|
||||||
\`\`\`markdown
|
|
||||||
---
|
|
||||||
model: gpt-5.1
|
|
||||||
tools:
|
|
||||||
search:
|
|
||||||
type: mcp
|
|
||||||
name: firecrawl_search
|
|
||||||
description: Search the web
|
|
||||||
mcpServerName: firecrawl
|
|
||||||
inputSchema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
query:
|
|
||||||
type: string
|
|
||||||
---
|
|
||||||
# Search Agent
|
|
||||||
|
|
||||||
Use the search tool to find information on the web.
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Capabilities checklist
|
|
||||||
1. Explore \`agents/\` directory to understand existing agents before editing
|
|
||||||
2. Read existing agents with \`workspace-readFile\` before making changes
|
|
||||||
3. Validate YAML frontmatter syntax before creating/updating agents
|
|
||||||
4. Use \`analyzeAgent\` to verify agent structure after creation/update
|
|
||||||
5. When creating multi-agent workflows, create an orchestrator agent
|
|
||||||
6. Add other agents as tools with \`type: agent\` for chaining
|
|
||||||
7. Use \`listMcpServers\` and \`listMcpTools\` when adding MCP integrations
|
|
||||||
8. Confirm work done and outline next steps once changes are complete
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default skill;
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
export const skill = String.raw`
|
|
||||||
# Agent Run Operations
|
|
||||||
|
|
||||||
Package of repeatable commands for running agents, inspecting agent run history under ~/.rowboat/runs, and managing cron schedules. Load this skill whenever a user asks about running agents, execution history, paused runs, or scheduling.
|
|
||||||
|
|
||||||
## When to use
|
|
||||||
- User wants to run an agent (including multi-agent workflows)
|
|
||||||
- User wants to list or filter agent runs (all runs, by agent, time range, or paused for input)
|
|
||||||
- User wants to inspect cron jobs or change agent schedules
|
|
||||||
- User asks how to set up monitoring for waiting runs
|
|
||||||
|
|
||||||
## Running Agents
|
|
||||||
|
|
||||||
**To run any agent**:
|
|
||||||
\`\`\`bash
|
|
||||||
rowboatx --agent <agent-name>
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**With input**:
|
|
||||||
\`\`\`bash
|
|
||||||
rowboatx --agent <agent-name> --input "your input here"
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Non-interactive** (for automation/cron):
|
|
||||||
\`\`\`bash
|
|
||||||
rowboatx --agent <agent-name> --input "input" --no-interactive
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Note**: Multi-agent workflows are just agents that have other agents in their tools. Run the orchestrator agent to trigger the whole workflow.
|
|
||||||
|
|
||||||
## Run monitoring examples
|
|
||||||
Operate from ~/.rowboat (Rowboat tools already set this as the working directory). Use executeCommand with the sample Bash snippets below, modifying placeholders as needed.
|
|
||||||
|
|
||||||
Each run file name starts with a timestamp like '2025-11-12T08-02-41Z'. You can use this to filter for date/time ranges.
|
|
||||||
|
|
||||||
Each line of the run file contains a running log with the first line containing information about the agent run. E.g. '{"type":"start","runId":"2025-11-12T08-02-41Z-0014322-000","agent":"agent_name","interactive":true,"ts":"2025-11-12T08:02:41.168Z"}'
|
|
||||||
|
|
||||||
If a run is waiting for human input the last line will contain 'paused_for_human_input'. See examples below.
|
|
||||||
|
|
||||||
1. **List all runs**
|
|
||||||
|
|
||||||
ls ~/.rowboat/runs
|
|
||||||
|
|
||||||
|
|
||||||
2. **Filter by agent**
|
|
||||||
|
|
||||||
grep -rl '"agent":"<agent-name>"' ~/.rowboat/runs | xargs -n1 basename | sed 's/\.jsonl$//' | sort -r
|
|
||||||
|
|
||||||
Replace <agent-name> with the desired agent name.
|
|
||||||
|
|
||||||
3. **Filter by time window**
|
|
||||||
To the previous commands add the below through unix pipe
|
|
||||||
|
|
||||||
awk -F'/' '$NF >= "2025-11-12T08-03" && $NF <= "2025-11-12T08-10"'
|
|
||||||
|
|
||||||
Use the correct timestamps.
|
|
||||||
|
|
||||||
4. **Show runs waiting for human input**
|
|
||||||
|
|
||||||
awk 'FNR==1{if (NR>1) print fn, last; fn=FILENAME} {last=$0} END{print fn, last}' ~/.rowboat/runs/*.jsonl | grep 'pause-for-human-input' | awk '{print $1}'
|
|
||||||
|
|
||||||
Prints the files whose last line equals 'pause-for-human-input'.
|
|
||||||
|
|
||||||
## Cron management examples
|
|
||||||
|
|
||||||
For scheduling agents to run automatically at specific times.
|
|
||||||
|
|
||||||
1. **View current cron schedule**
|
|
||||||
\`\`\`bash
|
|
||||||
crontab -l 2>/dev/null || echo 'No crontab entries configured.'
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
2. **Schedule an agent to run periodically**
|
|
||||||
\`\`\`bash
|
|
||||||
(crontab -l 2>/dev/null; echo '0 10 * * * cd /path/to/cli && rowboatx --agent <agent-name> --input "input" --no-interactive >> ~/.rowboat/logs/<agent-name>.log 2>&1') | crontab -
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Example (runs daily at 10 AM):
|
|
||||||
\`\`\`bash
|
|
||||||
(crontab -l 2>/dev/null; echo '0 10 * * * cd ~/rowboat-V2/apps/cli && rowboatx --agent podcast_workflow --no-interactive >> ~/.rowboat/logs/podcast.log 2>&1') | crontab -
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
3. **Unschedule/remove an agent**
|
|
||||||
\`\`\`bash
|
|
||||||
crontab -l | grep -v '<agent-name>' | crontab -
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## Common cron schedule patterns
|
|
||||||
- \`0 10 * * *\` - Daily at 10 AM
|
|
||||||
- \`0 */6 * * *\` - Every 6 hours
|
|
||||||
- \`0 9 * * 1\` - Every Monday at 9 AM
|
|
||||||
- \`*/30 * * * *\` - Every 30 minutes
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default skill;
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import container from "../di/container.js";
|
import container from "../di/container.js";
|
||||||
import type { IModelConfigRepo } from "../models/repo.js";
|
import type { IModelConfigRepo } from "../models/repo.js";
|
||||||
import type { IMcpConfigRepo } from "../mcp/repo.js";
|
import type { IMcpConfigRepo } from "../mcp/repo.js";
|
||||||
|
import type { IAgentScheduleRepo } from "../agent-schedule/repo.js";
|
||||||
|
import type { IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
|
||||||
import { ensureSecurityConfig } from "./security.js";
|
import { ensureSecurityConfig } from "./security.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11,10 +13,14 @@ export async function initConfigs(): Promise<void> {
|
||||||
// Resolve repos and explicitly call their ensureConfig methods
|
// Resolve repos and explicitly call their ensureConfig methods
|
||||||
const modelConfigRepo = container.resolve<IModelConfigRepo>("modelConfigRepo");
|
const modelConfigRepo = container.resolve<IModelConfigRepo>("modelConfigRepo");
|
||||||
const mcpConfigRepo = container.resolve<IMcpConfigRepo>("mcpConfigRepo");
|
const mcpConfigRepo = container.resolve<IMcpConfigRepo>("mcpConfigRepo");
|
||||||
|
const agentScheduleRepo = container.resolve<IAgentScheduleRepo>("agentScheduleRepo");
|
||||||
|
const agentScheduleStateRepo = container.resolve<IAgentScheduleStateRepo>("agentScheduleStateRepo");
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
modelConfigRepo.ensureConfig(),
|
modelConfigRepo.ensureConfig(),
|
||||||
mcpConfigRepo.ensureConfig(),
|
mcpConfigRepo.ensureConfig(),
|
||||||
|
agentScheduleRepo.ensureConfig(),
|
||||||
|
agentScheduleStateRepo.ensureState(),
|
||||||
ensureSecurityConfig(),
|
ensureSecurityConfig(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import { FSOAuthRepo, IOAuthRepo } from "../auth/repo.js";
|
||||||
import { FSClientRegistrationRepo, IClientRegistrationRepo } from "../auth/client-repo.js";
|
import { FSClientRegistrationRepo, IClientRegistrationRepo } from "../auth/client-repo.js";
|
||||||
import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/repo.js";
|
import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/repo.js";
|
||||||
import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js";
|
import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js";
|
||||||
|
import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js";
|
||||||
|
import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js";
|
||||||
|
|
||||||
const container = createContainer({
|
const container = createContainer({
|
||||||
injectionMode: InjectionMode.PROXY,
|
injectionMode: InjectionMode.PROXY,
|
||||||
|
|
@ -33,6 +35,8 @@ container.register({
|
||||||
oauthRepo: asClass<IOAuthRepo>(FSOAuthRepo).singleton(),
|
oauthRepo: asClass<IOAuthRepo>(FSOAuthRepo).singleton(),
|
||||||
clientRegistrationRepo: asClass<IClientRegistrationRepo>(FSClientRegistrationRepo).singleton(),
|
clientRegistrationRepo: asClass<IClientRegistrationRepo>(FSClientRegistrationRepo).singleton(),
|
||||||
granolaConfigRepo: asClass<IGranolaConfigRepo>(FSGranolaConfigRepo).singleton(),
|
granolaConfigRepo: asClass<IGranolaConfigRepo>(FSGranolaConfigRepo).singleton(),
|
||||||
|
agentScheduleRepo: asClass<IAgentScheduleRepo>(FSAgentScheduleRepo).singleton(),
|
||||||
|
agentScheduleStateRepo: asClass<IAgentScheduleStateRepo>(FSAgentScheduleStateRepo).singleton(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default container;
|
export default container;
|
||||||
17
apps/x/packages/shared/src/agent-schedule-state.ts
Normal file
17
apps/x/packages/shared/src/agent-schedule-state.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
// "triggered" is terminal state for once-schedules (will not run again)
|
||||||
|
export const AgentScheduleStatus = z.enum(["scheduled", "running", "finished", "failed", "triggered"]);
|
||||||
|
|
||||||
|
export const AgentScheduleStateEntry = z.object({
|
||||||
|
status: AgentScheduleStatus,
|
||||||
|
startedAt: z.string().nullable(), // When current run started (for timeout detection)
|
||||||
|
lastRunAt: z.string().nullable(), // ISO 8601 local datetime
|
||||||
|
nextRunAt: z.string().nullable(), // ISO 8601 local datetime
|
||||||
|
lastError: z.string().nullable(),
|
||||||
|
runCount: z.number().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AgentScheduleState = z.object({
|
||||||
|
agents: z.record(z.string(), AgentScheduleStateEntry),
|
||||||
|
});
|
||||||
44
apps/x/packages/shared/src/agent-schedule.ts
Normal file
44
apps/x/packages/shared/src/agent-schedule.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import z from "zod";
|
||||||
|
|
||||||
|
// Cron schedule - runs at exact times defined by cron expression.
|
||||||
|
// Examples:
|
||||||
|
// - Every 5 minutes: "*/5 * * * *"
|
||||||
|
// - Everyday at 8am: "0 8 * * *"
|
||||||
|
// - Every Monday at 9am: "0 9 * * 1"
|
||||||
|
export const CronSchedule = z.object({
|
||||||
|
type: z.literal("cron"),
|
||||||
|
expression: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window schedule - runs once during a time window.
|
||||||
|
// The agent will run once at a random time within the specified window.
|
||||||
|
// Examples:
|
||||||
|
// - Daily between 8am and 10am: cron="0 0 * * *", startTime="08:00", endTime="10:00"
|
||||||
|
// - Weekly on Monday between 9am-12pm: cron="0 0 * * 1", startTime="09:00", endTime="12:00"
|
||||||
|
export const WindowSchedule = z.object({
|
||||||
|
type: z.literal("window"),
|
||||||
|
cron: z.string(), // Base frequency cron expression
|
||||||
|
startTime: z.string(), // "HH:MM" format
|
||||||
|
endTime: z.string(), // "HH:MM" format
|
||||||
|
});
|
||||||
|
|
||||||
|
// Once schedule - runs exactly once at a specific time, then never again.
|
||||||
|
// Examples:
|
||||||
|
// - Run once at specific datetime: runAt="2024-02-05T10:30:00"
|
||||||
|
export const OnceSchedule = z.object({
|
||||||
|
type: z.literal("once"),
|
||||||
|
runAt: z.string(), // ISO 8601 datetime (local time, e.g., "2024-02-05T10:30:00")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ScheduleDefinition = z.union([CronSchedule, WindowSchedule, OnceSchedule]);
|
||||||
|
|
||||||
|
export const AgentScheduleEntry = z.object({
|
||||||
|
schedule: ScheduleDefinition,
|
||||||
|
enabled: z.boolean().optional().default(true),
|
||||||
|
startingMessage: z.string().optional(), // Message sent to agent when run starts (defaults to "go")
|
||||||
|
description: z.string().optional(), // Brief description of what the agent does (for UI display)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AgentScheduleConfig = z.object({
|
||||||
|
agents: z.record(z.string(), AgentScheduleEntry),
|
||||||
|
});
|
||||||
|
|
@ -4,4 +4,6 @@ export * as ipc from './ipc.js';
|
||||||
export * as models from './models.js';
|
export * as models from './models.js';
|
||||||
export * as workspace from './workspace.js';
|
export * as workspace from './workspace.js';
|
||||||
export * as mcp from './mcp.js';
|
export * as mcp from './mcp.js';
|
||||||
|
export * as agentSchedule from './agent-schedule.js';
|
||||||
|
export * as agentScheduleState from './agent-schedule-state.js';
|
||||||
export { PrefixLogger };
|
export { PrefixLogger };
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { RelPath, Encoding, Stat, DirEntry, ReaddirOptions, ReadFileResult, Work
|
||||||
import { ListToolsResponse } from './mcp.js';
|
import { ListToolsResponse } from './mcp.js';
|
||||||
import { AskHumanResponsePayload, CreateRunOptions, Run, ListRunsResponse, ToolPermissionAuthorizePayload } from './runs.js';
|
import { AskHumanResponsePayload, CreateRunOptions, Run, ListRunsResponse, ToolPermissionAuthorizePayload } from './runs.js';
|
||||||
import { LlmModelConfig } from './models.js';
|
import { LlmModelConfig } from './models.js';
|
||||||
|
import { AgentScheduleConfig, AgentScheduleEntry } from './agent-schedule.js';
|
||||||
|
import { AgentScheduleState } from './agent-schedule-state.js';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Runtime Validation Schemas (Single Source of Truth)
|
// Runtime Validation Schemas (Single Source of Truth)
|
||||||
|
|
@ -353,6 +355,32 @@ const ipcSchemas = {
|
||||||
}),
|
}),
|
||||||
res: z.null(),
|
res: z.null(),
|
||||||
},
|
},
|
||||||
|
// Agent schedule channels
|
||||||
|
'agent-schedule:getConfig': {
|
||||||
|
req: z.null(),
|
||||||
|
res: AgentScheduleConfig,
|
||||||
|
},
|
||||||
|
'agent-schedule:getState': {
|
||||||
|
req: z.null(),
|
||||||
|
res: AgentScheduleState,
|
||||||
|
},
|
||||||
|
'agent-schedule:updateAgent': {
|
||||||
|
req: z.object({
|
||||||
|
agentName: z.string(),
|
||||||
|
entry: AgentScheduleEntry,
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
success: z.literal(true),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'agent-schedule:deleteAgent': {
|
||||||
|
req: z.object({
|
||||||
|
agentName: z.string(),
|
||||||
|
}),
|
||||||
|
res: z.object({
|
||||||
|
success: z.literal(true),
|
||||||
|
}),
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
22
apps/x/pnpm-lock.yaml
generated
22
apps/x/pnpm-lock.yaml
generated
|
|
@ -320,6 +320,9 @@ importers:
|
||||||
chokidar:
|
chokidar:
|
||||||
specifier: ^4.0.3
|
specifier: ^4.0.3
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
|
cron-parser:
|
||||||
|
specifier: ^5.5.0
|
||||||
|
version: 5.5.0
|
||||||
glob:
|
glob:
|
||||||
specifier: ^13.0.0
|
specifier: ^13.0.0
|
||||||
version: 13.0.0
|
version: 13.0.0
|
||||||
|
|
@ -3452,6 +3455,10 @@ packages:
|
||||||
crelt@1.0.6:
|
crelt@1.0.6:
|
||||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
|
cron-parser@5.5.0:
|
||||||
|
resolution: {integrity: sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
cross-dirname@0.1.0:
|
cross-dirname@0.1.0:
|
||||||
resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==}
|
resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==}
|
||||||
|
|
||||||
|
|
@ -4248,6 +4255,7 @@ packages:
|
||||||
|
|
||||||
glob@10.5.0:
|
glob@10.5.0:
|
||||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||||
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@13.0.0:
|
glob@13.0.0:
|
||||||
|
|
@ -4256,12 +4264,12 @@ packages:
|
||||||
|
|
||||||
glob@7.2.3:
|
glob@7.2.3:
|
||||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
glob@8.1.0:
|
glob@8.1.0:
|
||||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
deprecated: Glob versions prior to v9 are no longer supported
|
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||||
|
|
||||||
global-agent@3.0.0:
|
global-agent@3.0.0:
|
||||||
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
|
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
|
||||||
|
|
@ -4898,6 +4906,10 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
luxon@3.7.2:
|
||||||
|
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
macos-alias@0.2.12:
|
macos-alias@0.2.12:
|
||||||
resolution: {integrity: sha512-yiLHa7cfJcGRFq4FrR4tMlpNHb4Vy4mWnpajlSSIFM5k4Lv8/7BbbDLzCAVogWNl0LlLhizRp1drXv0hK9h0Yw==}
|
resolution: {integrity: sha512-yiLHa7cfJcGRFq4FrR4tMlpNHb4Vy4mWnpajlSSIFM5k4Lv8/7BbbDLzCAVogWNl0LlLhizRp1drXv0hK9h0Yw==}
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
@ -10620,6 +10632,10 @@ snapshots:
|
||||||
|
|
||||||
crelt@1.0.6: {}
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
|
cron-parser@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
luxon: 3.7.2
|
||||||
|
|
||||||
cross-dirname@0.1.0: {}
|
cross-dirname@0.1.0: {}
|
||||||
|
|
||||||
cross-spawn@6.0.6:
|
cross-spawn@6.0.6:
|
||||||
|
|
@ -12345,6 +12361,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
|
|
||||||
|
luxon@3.7.2: {}
|
||||||
|
|
||||||
macos-alias@0.2.12:
|
macos-alias@0.2.12:
|
||||||
dependencies:
|
dependencies:
|
||||||
nan: 2.24.0
|
nan: 2.24.0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue