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:
Arjun 2026-02-04 23:21:13 +05:30
parent 82db06d724
commit c447a42d07
20 changed files with 1544 additions and 500 deletions

View file

@ -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",

View 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));
}
}

View 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);
}
}

View 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));
}
}

View file

@ -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 exactlydon'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 writingmalformed 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;

View file

@ -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) => ({

View file

@ -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 exactlydon'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 writingmalformed 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;

View file

@ -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;

View file

@ -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(),
]);
}

View file

@ -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;