mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 00:46:23 +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
|
|
@ -12,9 +12,9 @@
|
|||
"@ai-sdk/anthropic": "^2.0.44",
|
||||
"@ai-sdk/google": "^2.0.25",
|
||||
"@ai-sdk/openai": "^2.0.53",
|
||||
"@composio/core": "^0.6.0",
|
||||
"@ai-sdk/openai-compatible": "^1.0.27",
|
||||
"@ai-sdk/provider": "^2.0.0",
|
||||
"@composio/core": "^0.6.0",
|
||||
"@google-cloud/local-auth": "^3.0.1",
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
"@openrouter/ai-sdk-provider": "^1.2.6",
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
"ai": "^5.0.102",
|
||||
"awilix": "^12.0.5",
|
||||
"chokidar": "^4.0.3",
|
||||
"cron-parser": "^5.5.0",
|
||||
"glob": "^13.0.0",
|
||||
"google-auth-library": "^10.5.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 organizeFilesSkill from "./organize-files/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 workflowRunOpsSkill from "./workflow-run-ops/skill.js";
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CATALOG_PREFIX = "src/application/assistant/skills";
|
||||
|
|
@ -66,10 +65,10 @@ const definitions: SkillDefinition[] = [
|
|||
content: slackSkill,
|
||||
},
|
||||
{
|
||||
id: "workflow-authoring",
|
||||
title: "Workflow Authoring",
|
||||
summary: "Creating or editing workflows/agents, validating schema rules, and keeping filenames aligned with JSON ids.",
|
||||
content: workflowAuthoringSkill,
|
||||
id: "background-agents",
|
||||
title: "Background Agents",
|
||||
summary: "Creating, editing, and scheduling background agents. Configure schedules in agent-schedule.json and build multi-agent workflows.",
|
||||
content: backgroundAgentsSkill,
|
||||
},
|
||||
{
|
||||
id: "builtin-tools",
|
||||
|
|
@ -89,12 +88,6 @@ const definitions: SkillDefinition[] = [
|
|||
summary: "Following the confirmation process before removing workflows or agents and their dependencies.",
|
||||
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) => ({
|
||||
|
|
|
|||
|
|
@ -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 type { IModelConfigRepo } from "../models/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";
|
||||
|
||||
/**
|
||||
|
|
@ -11,10 +13,14 @@ export async function initConfigs(): Promise<void> {
|
|||
// Resolve repos and explicitly call their ensureConfig methods
|
||||
const modelConfigRepo = container.resolve<IModelConfigRepo>("modelConfigRepo");
|
||||
const mcpConfigRepo = container.resolve<IMcpConfigRepo>("mcpConfigRepo");
|
||||
const agentScheduleRepo = container.resolve<IAgentScheduleRepo>("agentScheduleRepo");
|
||||
const agentScheduleStateRepo = container.resolve<IAgentScheduleStateRepo>("agentScheduleStateRepo");
|
||||
|
||||
await Promise.all([
|
||||
modelConfigRepo.ensureConfig(),
|
||||
mcpConfigRepo.ensureConfig(),
|
||||
agentScheduleRepo.ensureConfig(),
|
||||
agentScheduleStateRepo.ensureState(),
|
||||
ensureSecurityConfig(),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { FSOAuthRepo, IOAuthRepo } from "../auth/repo.js";
|
|||
import { FSClientRegistrationRepo, IClientRegistrationRepo } from "../auth/client-repo.js";
|
||||
import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/repo.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({
|
||||
injectionMode: InjectionMode.PROXY,
|
||||
|
|
@ -33,6 +35,8 @@ container.register({
|
|||
oauthRepo: asClass<IOAuthRepo>(FSOAuthRepo).singleton(),
|
||||
clientRegistrationRepo: asClass<IClientRegistrationRepo>(FSClientRegistrationRepo).singleton(),
|
||||
granolaConfigRepo: asClass<IGranolaConfigRepo>(FSGranolaConfigRepo).singleton(),
|
||||
agentScheduleRepo: asClass<IAgentScheduleRepo>(FSAgentScheduleRepo).singleton(),
|
||||
agentScheduleStateRepo: asClass<IAgentScheduleStateRepo>(FSAgentScheduleStateRepo).singleton(),
|
||||
});
|
||||
|
||||
export default container;
|
||||
Loading…
Add table
Add a link
Reference in a new issue