@fickydev/pigent 0.1.16 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -1
- package/TODO.md +7 -5
- package/agents/assistant/agent.yaml +2 -1
- package/drizzle/migrations/0006_flippant_bruce_banner.sql +1 -0
- package/drizzle/migrations/meta/0006_snapshot.json +711 -0
- package/drizzle/migrations/meta/_journal.json +7 -0
- package/package.json +2 -1
- package/skills/task-management/SKILL.md +59 -0
- package/src/agents/AgentRegistry.ts +4 -4
- package/src/agents/AgentRunner.ts +49 -0
- package/src/agents/BotCommandHandler.ts +2 -2
- package/src/config/loadConfig.ts +5 -3
- package/src/config/schemas.ts +6 -1
- package/src/daemon/AgentDaemon.ts +1 -0
- package/src/daemon/Scheduler.ts +49 -0
- package/src/db/repositories/TaskConfigRepository.ts +17 -1
- package/src/db/schema.ts +1 -0
- package/src/pi/PiAgentRunner.ts +35 -3
- package/src/pi/tools/taskTools.ts +188 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fickydev/pigent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Autonomous multi-agent daemon using Pi as core execution engine.",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"files": [
|
|
24
24
|
"src",
|
|
25
|
+
"skills",
|
|
25
26
|
"agents",
|
|
26
27
|
"profiles",
|
|
27
28
|
"drizzle",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: task-management
|
|
3
|
+
description: Manage scheduled tasks — list, create, update, and remove recurring prompts for this agent and chat.
|
|
4
|
+
disable-model-invocation: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Task Management
|
|
8
|
+
|
|
9
|
+
You can manage scheduled recurring tasks for this agent and chat.
|
|
10
|
+
|
|
11
|
+
## Available Tools
|
|
12
|
+
|
|
13
|
+
### `task_list`
|
|
14
|
+
List all scheduled tasks for this agent and chat.
|
|
15
|
+
|
|
16
|
+
### `task_create`
|
|
17
|
+
Create a new scheduled task. Provide:
|
|
18
|
+
- `prompt` (required): what to do at each interval. Be specific.
|
|
19
|
+
- `intervalMs` (optional): how often to run. Default 600000 (10 minutes). Minimum 60000 (1 minute).
|
|
20
|
+
|
|
21
|
+
When the task runs, if the result is "NOOP", nothing is sent to the user.
|
|
22
|
+
|
|
23
|
+
### `task_update`
|
|
24
|
+
Update an existing task's prompt or interval. Provide:
|
|
25
|
+
- `id` (required): task ID
|
|
26
|
+
- `prompt` (optional): new prompt text
|
|
27
|
+
- `intervalMs` (optional): new interval in milliseconds
|
|
28
|
+
|
|
29
|
+
### `task_remove`
|
|
30
|
+
Remove a scheduled task by ID.
|
|
31
|
+
|
|
32
|
+
## Limits
|
|
33
|
+
|
|
34
|
+
- Maximum 5 tasks per agent per chat.
|
|
35
|
+
- Minimum interval: 1 minute.
|
|
36
|
+
|
|
37
|
+
## When to Use
|
|
38
|
+
|
|
39
|
+
- User asks to "check something periodically" or "monitor something"
|
|
40
|
+
- User asks to "run something every X minutes/hours"
|
|
41
|
+
- User wants to change or stop a recurring task
|
|
42
|
+
- User asks "what tasks do I have?" or "what's running?"
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
User: "check my build every 5 minutes"
|
|
47
|
+
→ task_create({ prompt: "Check the build pipeline status. Report failures. NOOP if everything is green.", intervalMs: 300000 })
|
|
48
|
+
|
|
49
|
+
User: "remind me to review PRs every hour"
|
|
50
|
+
→ task_create({ prompt: "Check for open pull requests that need review. List them. NOOP if none.", intervalMs: 3600000 })
|
|
51
|
+
|
|
52
|
+
User: "change the build checker to run every 2 minutes"
|
|
53
|
+
→ task_list() to find the task, then task_update({ id: "abc123", intervalMs: 120000 })
|
|
54
|
+
|
|
55
|
+
User: "stop the build checker"
|
|
56
|
+
→ task_list() to find the task, then task_remove({ id: "abc123" })
|
|
57
|
+
|
|
58
|
+
User: "what recurring tasks do I have?"
|
|
59
|
+
→ task_list()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { LoadedAgentConfig, LoadedConfig,
|
|
1
|
+
import type { LoadedAgentConfig, LoadedConfig, LoadedProfileConfig } from "../config/schemas";
|
|
2
2
|
import type { Repositories } from "../db/repositories";
|
|
3
3
|
|
|
4
4
|
export class AgentRegistry {
|
|
5
5
|
private readonly agentsById: Map<string, LoadedAgentConfig>;
|
|
6
|
-
private readonly profilesById: Map<string,
|
|
6
|
+
private readonly profilesById: Map<string, LoadedProfileConfig>;
|
|
7
7
|
|
|
8
8
|
constructor(
|
|
9
9
|
private readonly config: LoadedConfig,
|
|
@@ -23,7 +23,7 @@ export class AgentRegistry {
|
|
|
23
23
|
return [...this.agentsById.values()];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
listProfiles():
|
|
26
|
+
listProfiles(): LoadedProfileConfig[] {
|
|
27
27
|
return [...this.profilesById.values()];
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ export class AgentRegistry {
|
|
|
31
31
|
return this.agentsById.get(id) ?? null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
getProfile(id: string):
|
|
34
|
+
getProfile(id: string): LoadedProfileConfig | null {
|
|
35
35
|
return this.profilesById.get(id) ?? null;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { InboundMessage } from "../channels/types";
|
|
3
|
+
import type { LoadedAgentConfig } from "../config/schemas";
|
|
2
4
|
import type { Repositories } from "../db/repositories";
|
|
3
5
|
import { logger } from "../logging/logger";
|
|
4
6
|
import { PiAgentRunner } from "../pi/PiAgentRunner";
|
|
7
|
+
import { createTaskTools, type TaskToolContext } from "../pi/tools/taskTools";
|
|
5
8
|
import type { AgentRegistry } from "./AgentRegistry";
|
|
9
|
+
import type { Scheduler } from "../daemon/Scheduler";
|
|
6
10
|
|
|
7
11
|
export type AgentRunInput = {
|
|
8
12
|
agentId: string;
|
|
@@ -21,8 +25,13 @@ export class AgentRunner {
|
|
|
21
25
|
constructor(
|
|
22
26
|
private readonly registry: AgentRegistry,
|
|
23
27
|
private readonly repositories: Repositories,
|
|
28
|
+
private scheduler: Scheduler | null = null,
|
|
24
29
|
) {}
|
|
25
30
|
|
|
31
|
+
setScheduler(scheduler: Scheduler): void {
|
|
32
|
+
this.scheduler = scheduler;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
async run(input: AgentRunInput): Promise<string> {
|
|
27
36
|
return this.withSessionLock(input, () => this.runUnlocked(input));
|
|
28
37
|
}
|
|
@@ -109,6 +118,7 @@ export class AgentRunner {
|
|
|
109
118
|
profile: this.registry.getProfile(agent.profile),
|
|
110
119
|
session,
|
|
111
120
|
prompt: this.composePrompt(input, chatInstructions),
|
|
121
|
+
customTools: this.buildCustomTools(agent, input.message),
|
|
112
122
|
});
|
|
113
123
|
|
|
114
124
|
if (session.piSessionId !== result.piSessionId || session.piSessionPath !== result.piSessionPath) {
|
|
@@ -135,6 +145,45 @@ export class AgentRunner {
|
|
|
135
145
|
}
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
private buildCustomTools(agent: LoadedAgentConfig, message: InboundMessage): ToolDefinition[] | undefined {
|
|
149
|
+
const tools: ToolDefinition[] = [];
|
|
150
|
+
|
|
151
|
+
if (this.hasSkill(agent, "task-management") && this.scheduler) {
|
|
152
|
+
const ctx: TaskToolContext = {
|
|
153
|
+
agentId: agent.id,
|
|
154
|
+
channel: message.channel,
|
|
155
|
+
chatId: message.chatId,
|
|
156
|
+
taskConfigRepo: this.repositories.taskConfigs,
|
|
157
|
+
schedulerHotReload: async (action, task) => {
|
|
158
|
+
if (action === "add") {
|
|
159
|
+
const row = await this.repositories.taskConfigs.findById(task.id);
|
|
160
|
+
if (row) {
|
|
161
|
+
this.scheduler!.addDynamicTaskFromRow(row);
|
|
162
|
+
}
|
|
163
|
+
} else if (action === "remove") {
|
|
164
|
+
await this.scheduler!.removeDynamicTask(task.id);
|
|
165
|
+
} else if (action === "update") {
|
|
166
|
+
await this.scheduler!.reloadTasks();
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
tools.push(...createTaskTools(ctx));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return tools.length > 0 ? tools : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private hasSkill(agent: LoadedAgentConfig, skillName: string): boolean {
|
|
177
|
+
const allSkills = [
|
|
178
|
+
...(agent.profile ? (this.registry.getProfile(agent.profile)?.defaultSkills ?? []) : []),
|
|
179
|
+
...agent.skills,
|
|
180
|
+
];
|
|
181
|
+
return allSkills.some((s) => {
|
|
182
|
+
const base = s.split("/").pop() ?? s;
|
|
183
|
+
return base === skillName || s === skillName;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
138
187
|
private composePrompt(input: AgentRunInput, chatInstructions: string): string {
|
|
139
188
|
const parts: string[] = [
|
|
140
189
|
`[Channel]\n${input.message.channel}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { InboundMessage, InlineKeyboardButton } from "../channels/types";
|
|
3
|
-
import type { LoadedAgentConfig,
|
|
3
|
+
import type { LoadedAgentConfig, LoadedProfileConfig, ModelChoiceConfig } from "../config/schemas";
|
|
4
4
|
import type { AgentSessionRow } from "../db/schema";
|
|
5
5
|
import type { Repositories } from "../db/repositories";
|
|
6
6
|
import { PiAgentRunner, type PiContextUsage } from "../pi/PiAgentRunner";
|
|
@@ -169,7 +169,7 @@ export class BotCommandHandler {
|
|
|
169
169
|
].join("\n");
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
private async loadPiStatus(session: AgentSessionRow, agent: LoadedAgentConfig, profile:
|
|
172
|
+
private async loadPiStatus(session: AgentSessionRow, agent: LoadedAgentConfig, profile: LoadedProfileConfig | null) {
|
|
173
173
|
try {
|
|
174
174
|
return await this.piRunner.status({ agent, profile, session });
|
|
175
175
|
} catch {
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { readdir, readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import YAML from "yaml";
|
|
5
5
|
import {
|
|
6
6
|
AgentConfigSchema,
|
|
7
7
|
type LoadedAgentConfig,
|
|
8
8
|
type LoadedConfig,
|
|
9
|
+
type LoadedProfileConfig,
|
|
9
10
|
ProfileConfigSchema,
|
|
10
11
|
RootConfigSchema,
|
|
11
12
|
} from "./schemas";
|
|
@@ -60,7 +61,7 @@ async function loadAgents(rootDir: string): Promise<LoadedAgentConfig[]> {
|
|
|
60
61
|
async function loadProfiles(rootDir: string) {
|
|
61
62
|
const profilesDir = join(rootDir, "profiles");
|
|
62
63
|
const entries = await readdir(profilesDir, { withFileTypes: true }).catch(() => []);
|
|
63
|
-
const profiles = [];
|
|
64
|
+
const profiles: LoadedProfileConfig[] = [];
|
|
64
65
|
|
|
65
66
|
for (const entry of entries) {
|
|
66
67
|
if (!entry.isFile()) continue;
|
|
@@ -68,7 +69,8 @@ async function loadProfiles(rootDir: string) {
|
|
|
68
69
|
|
|
69
70
|
const configPath = join(profilesDir, entry.name);
|
|
70
71
|
const parsed = await readYamlFile(configPath);
|
|
71
|
-
|
|
72
|
+
const profile = ProfileConfigSchema.parse(parsed);
|
|
73
|
+
profiles.push({ ...profile, baseDir: dirname(configPath) });
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
return profiles;
|
package/src/config/schemas.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const TaskConfigSchema = z.object({
|
|
|
18
18
|
prompt: z.string().min(1).default("If no useful action is needed, reply exactly: NOOP."),
|
|
19
19
|
channel: z.string().default("telegram"),
|
|
20
20
|
chatId: z.string().optional(),
|
|
21
|
+
maxRunsPerHour: z.number().int().nonnegative().default(12),
|
|
21
22
|
});
|
|
22
23
|
|
|
23
24
|
export const SchedulerConfigSchema = z.object({
|
|
@@ -102,9 +103,13 @@ export type LoadedAgentConfig = AgentConfig & {
|
|
|
102
103
|
systemPrompt: string;
|
|
103
104
|
};
|
|
104
105
|
|
|
106
|
+
export type LoadedProfileConfig = ProfileConfig & {
|
|
107
|
+
baseDir: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
105
110
|
export type LoadedConfig = {
|
|
106
111
|
agents: LoadedAgentConfig[];
|
|
107
|
-
profiles:
|
|
112
|
+
profiles: LoadedProfileConfig[];
|
|
108
113
|
telegramChats: TelegramChatConfig[];
|
|
109
114
|
modelChoices: ModelChoiceConfig[];
|
|
110
115
|
scheduler: SchedulerConfig;
|
|
@@ -28,6 +28,7 @@ export class AgentDaemon {
|
|
|
28
28
|
this.registry = new AgentRegistry(config, repositories);
|
|
29
29
|
this.runner = new AgentRunner(this.registry, repositories);
|
|
30
30
|
this.scheduler = new Scheduler(config.scheduler, this.registry, repositories, this.adapters, this.runner);
|
|
31
|
+
this.runner.setScheduler(this.scheduler);
|
|
31
32
|
this.commands = new BotCommandHandler(this.registry, repositories, config.modelChoices, this.scheduler);
|
|
32
33
|
this.router = new MessageRouter(repositories, this.registry);
|
|
33
34
|
}
|
package/src/daemon/Scheduler.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { AgentRunner } from "../agents/AgentRunner";
|
|
|
7
7
|
import type { ChannelAdapter } from "../channels/types";
|
|
8
8
|
import { isTaskDue } from "./taskDue";
|
|
9
9
|
|
|
10
|
+
const ONE_HOUR_MS = 3_600_000;
|
|
11
|
+
|
|
10
12
|
export class Scheduler {
|
|
11
13
|
private running = false;
|
|
12
14
|
private tickTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -70,6 +72,7 @@ export class Scheduler {
|
|
|
70
72
|
prompt: saved.prompt,
|
|
71
73
|
channel: saved.channel,
|
|
72
74
|
chatId: saved.chatId,
|
|
75
|
+
maxRunsPerHour: saved.maxRunsPerHour ?? 12,
|
|
73
76
|
});
|
|
74
77
|
|
|
75
78
|
if (!this.running && this.tasks.length === 1) {
|
|
@@ -97,6 +100,34 @@ export class Scheduler {
|
|
|
97
100
|
return removed;
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
/** Add a task from an already-saved DB row (e.g., created by a tool). Hot-adds to scheduler. */
|
|
104
|
+
addDynamicTaskFromRow(row: TaskConfigRow): void {
|
|
105
|
+
// Avoid duplicates
|
|
106
|
+
if (this.tasks.some((t) => t.id === row.id)) return;
|
|
107
|
+
|
|
108
|
+
this.tasks.push({
|
|
109
|
+
id: row.id,
|
|
110
|
+
agent: row.agent,
|
|
111
|
+
intervalMs: row.intervalMs,
|
|
112
|
+
prompt: row.prompt,
|
|
113
|
+
channel: row.channel,
|
|
114
|
+
chatId: row.chatId,
|
|
115
|
+
maxRunsPerHour: row.maxRunsPerHour ?? 12,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!this.running && this.tasks.length === 1) {
|
|
119
|
+
this.running = true;
|
|
120
|
+
this.tick();
|
|
121
|
+
logger.info("scheduler started on first dynamic task");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Reload all tasks from config + DB. Used after task updates. */
|
|
126
|
+
async reloadTasks(): Promise<void> {
|
|
127
|
+
this.tasks = await this.loadAllTasks();
|
|
128
|
+
logger.info("scheduler tasks reloaded", { taskCount: this.tasks.length });
|
|
129
|
+
}
|
|
130
|
+
|
|
100
131
|
private async loadAllTasks(): Promise<TaskConfig[]> {
|
|
101
132
|
const configTasks: TaskConfig[] = this.schedulerConfig.tasks;
|
|
102
133
|
const dbTasks = await this.repositories.taskConfigs.findAllEnabled();
|
|
@@ -108,6 +139,7 @@ export class Scheduler {
|
|
|
108
139
|
prompt: row.prompt,
|
|
109
140
|
channel: row.channel,
|
|
110
141
|
chatId: row.chatId,
|
|
142
|
+
maxRunsPerHour: row.maxRunsPerHour ?? 12,
|
|
111
143
|
}));
|
|
112
144
|
|
|
113
145
|
return [...configTasks, ...dbTaskConfigs];
|
|
@@ -129,6 +161,23 @@ export class Scheduler {
|
|
|
129
161
|
const lastRun = await this.repositories.taskRuns.findLatest(task.agent, task.id);
|
|
130
162
|
if (!isTaskDue(lastRun?.createdAt ?? null, task.intervalMs)) return;
|
|
131
163
|
|
|
164
|
+
// Enforce hourly rate limit
|
|
165
|
+
const maxPerHour = task.maxRunsPerHour ?? 12;
|
|
166
|
+
const recentCount = await this.repositories.taskRuns.countRecentNotified(
|
|
167
|
+
task.agent,
|
|
168
|
+
task.id,
|
|
169
|
+
Date.now() - ONE_HOUR_MS,
|
|
170
|
+
);
|
|
171
|
+
if (recentCount >= maxPerHour) {
|
|
172
|
+
logger.warn("task rate limited — max runs per hour reached", {
|
|
173
|
+
taskId: task.id,
|
|
174
|
+
agent: task.agent,
|
|
175
|
+
recentCount,
|
|
176
|
+
maxPerHour,
|
|
177
|
+
});
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
132
181
|
const agent = this.registry.getAgent(task.agent);
|
|
133
182
|
if (!agent) {
|
|
134
183
|
logger.warn("task references unknown agent, skipping", { taskId: task.id, agent: task.agent });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { eq } from "drizzle-orm";
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
2
|
import { nanoid } from "nanoid";
|
|
3
3
|
import type { DbClient } from "../client";
|
|
4
4
|
import { taskConfigs, type TaskConfigRow } from "../schema";
|
|
@@ -60,4 +60,20 @@ export class TaskConfigRepository {
|
|
|
60
60
|
where: eq(taskConfigs.enabled, true),
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
async update(id: string, patch: { prompt?: string; intervalMs?: number }): Promise<TaskConfigRow | null> {
|
|
65
|
+
await this.db
|
|
66
|
+
.update(taskConfigs)
|
|
67
|
+
.set({ ...patch, updatedAt: Date.now() })
|
|
68
|
+
.where(eq(taskConfigs.id, id));
|
|
69
|
+
|
|
70
|
+
return this.findById(id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async countByAgentAndChatId(agent: string, chatId: string): Promise<number> {
|
|
74
|
+
const rows = await this.db.query.taskConfigs.findMany({
|
|
75
|
+
where: and(eq(taskConfigs.agent, agent), eq(taskConfigs.chatId, chatId), eq(taskConfigs.enabled, true)),
|
|
76
|
+
});
|
|
77
|
+
return rows.length;
|
|
78
|
+
}
|
|
63
79
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -116,6 +116,7 @@ export const taskConfigs = sqliteTable("task_configs", {
|
|
|
116
116
|
channel: text("channel").notNull().default("telegram"),
|
|
117
117
|
chatId: text("chat_id").notNull(),
|
|
118
118
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
119
|
+
maxRunsPerHour: integer("max_runs_per_hour"),
|
|
119
120
|
createdAt: integer("created_at").notNull(),
|
|
120
121
|
updatedAt: integer("updated_at").notNull(),
|
|
121
122
|
});
|
package/src/pi/PiAgentRunner.ts
CHANGED
|
@@ -5,19 +5,21 @@ import {
|
|
|
5
5
|
ModelRegistry,
|
|
6
6
|
getAgentDir,
|
|
7
7
|
SettingsManager,
|
|
8
|
+
type ToolDefinition,
|
|
8
9
|
} from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { mkdir } from "node:fs/promises";
|
|
10
11
|
import { resolve } from "node:path";
|
|
11
|
-
import type { LoadedAgentConfig,
|
|
12
|
+
import type { LoadedAgentConfig, LoadedProfileConfig } from "../config/schemas";
|
|
12
13
|
import type { AgentSessionRow } from "../db/schema";
|
|
13
14
|
import { resolveModelSelection } from "./PiModelResolver";
|
|
14
15
|
import { loadOrCreatePiSession } from "./PiSessionFactory";
|
|
15
16
|
|
|
16
17
|
export type PiAgentRunInput = {
|
|
17
18
|
agent: LoadedAgentConfig;
|
|
18
|
-
profile:
|
|
19
|
+
profile: LoadedProfileConfig | null;
|
|
19
20
|
session: AgentSessionRow;
|
|
20
21
|
prompt: string;
|
|
22
|
+
customTools?: ToolDefinition[];
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
export type PiContextUsage = {
|
|
@@ -80,11 +82,13 @@ export class PiAgentRunner {
|
|
|
80
82
|
compaction: { enabled: false },
|
|
81
83
|
});
|
|
82
84
|
const agentDir = getAgentDir();
|
|
85
|
+
const skillPaths = resolveSkillPaths(input.agent, input.profile);
|
|
83
86
|
const resourceLoader = new DefaultResourceLoader({
|
|
84
87
|
cwd: workspace,
|
|
85
88
|
agentDir,
|
|
86
89
|
settingsManager,
|
|
87
90
|
systemPromptOverride: () => systemPrompt,
|
|
91
|
+
additionalSkillPaths: skillPaths,
|
|
88
92
|
});
|
|
89
93
|
await resourceLoader.reload();
|
|
90
94
|
|
|
@@ -103,13 +107,41 @@ export class PiAgentRunner {
|
|
|
103
107
|
settingsManager,
|
|
104
108
|
agentDir,
|
|
105
109
|
tools: [],
|
|
110
|
+
customTools: input.customTools,
|
|
106
111
|
});
|
|
107
112
|
|
|
108
113
|
return { session, piSession };
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|
|
112
|
-
function
|
|
117
|
+
function resolveSkillPaths(agent: LoadedAgentConfig, profile: LoadedProfileConfig | null): string[] {
|
|
118
|
+
const paths: string[] = [];
|
|
119
|
+
|
|
120
|
+
if (profile) {
|
|
121
|
+
for (const s of profile.defaultSkills) {
|
|
122
|
+
paths.push(resolveSkillPath(s, profile.baseDir));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const s of agent.skills) {
|
|
127
|
+
paths.push(resolveSkillPath(s, agent.baseDir));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return paths;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveSkillPath(skillRef: string, baseDir: string): string {
|
|
134
|
+
// Absolute path
|
|
135
|
+
if (skillRef.startsWith("/")) return skillRef;
|
|
136
|
+
// Home-relative path
|
|
137
|
+
if (skillRef.startsWith("~/")) return resolve(skillRef);
|
|
138
|
+
// Explicit relative path
|
|
139
|
+
if (skillRef.startsWith("./") || skillRef.startsWith("../")) return resolve(baseDir, skillRef);
|
|
140
|
+
// Built-in pigent skill (bare name like "task-management")
|
|
141
|
+
return resolve(import.meta.dir, "../../skills", skillRef);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function composeSystemPrompt(agent: LoadedAgentConfig, profile: LoadedProfileConfig | null): string {
|
|
113
145
|
return [
|
|
114
146
|
profile?.instructions,
|
|
115
147
|
agent.systemPrompt,
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { TaskConfigRepository } from "../../db/repositories/TaskConfigRepository";
|
|
4
|
+
|
|
5
|
+
export type TaskToolContext = {
|
|
6
|
+
agentId: string;
|
|
7
|
+
channel: string;
|
|
8
|
+
chatId: string;
|
|
9
|
+
taskConfigRepo: TaskConfigRepository;
|
|
10
|
+
schedulerHotReload: (action: "add" | "remove" | "update", task: { id: string }) => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const MAX_TASKS_PER_CHAT = 5;
|
|
14
|
+
|
|
15
|
+
export function createTaskTools(ctx: TaskToolContext): ToolDefinition[] {
|
|
16
|
+
return [
|
|
17
|
+
createTaskListTool(ctx),
|
|
18
|
+
createTaskCreateTool(ctx),
|
|
19
|
+
createTaskUpdateTool(ctx),
|
|
20
|
+
createTaskRemoveTool(ctx),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createTaskListTool(ctx: TaskToolContext): ToolDefinition {
|
|
25
|
+
return defineTool({
|
|
26
|
+
name: "task_list",
|
|
27
|
+
label: "List Scheduled Tasks",
|
|
28
|
+
description: "List all scheduled tasks for the current agent and chat.",
|
|
29
|
+
promptSnippet: "List scheduled tasks",
|
|
30
|
+
parameters: Type.Object({}),
|
|
31
|
+
async execute(_callId, _params, _signal, _onUpdate, _extCtx) {
|
|
32
|
+
const tasks = await ctx.taskConfigRepo.findAllByChatId(ctx.chatId);
|
|
33
|
+
const mine = tasks.filter((t) => t.agent === ctx.agentId && t.enabled);
|
|
34
|
+
|
|
35
|
+
if (mine.length === 0) {
|
|
36
|
+
return { content: [{ type: "text", text: "No scheduled tasks." }], details: { count: 0 } };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lines = mine.map((t) => {
|
|
40
|
+
const mins = Math.round(t.intervalMs / 60_000);
|
|
41
|
+
return `- ${t.id}: "${t.prompt}" (every ${mins}m)`;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text", text: `Scheduled tasks (${mine.length}):\n${lines.join("\n")}` }],
|
|
46
|
+
details: { count: mine.length, tasks: mine },
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createTaskCreateTool(ctx: TaskToolContext): ToolDefinition {
|
|
53
|
+
return defineTool({
|
|
54
|
+
name: "task_create",
|
|
55
|
+
label: "Create Scheduled Task",
|
|
56
|
+
description:
|
|
57
|
+
"Create a new scheduled task for the current agent and chat. " +
|
|
58
|
+
"The task will run the given prompt at the specified interval. " +
|
|
59
|
+
"If the prompt result is NOOP, nothing is sent to the chat. " +
|
|
60
|
+
`Maximum ${MAX_TASKS_PER_CHAT} tasks per agent per chat.`,
|
|
61
|
+
promptSnippet: "Create a scheduled task with a prompt and interval",
|
|
62
|
+
parameters: Type.Object({
|
|
63
|
+
prompt: Type.String({ description: "The prompt to run at each interval. Be specific about what to check or do." }),
|
|
64
|
+
intervalMs: Type.Optional(
|
|
65
|
+
Type.Number({ description: "Interval in milliseconds. Default: 600000 (10 minutes). Minimum: 60000 (1 minute)." }),
|
|
66
|
+
),
|
|
67
|
+
}),
|
|
68
|
+
async execute(_callId, params, _signal, _onUpdate, _extCtx) {
|
|
69
|
+
const intervalMs = Math.max(60_000, params.intervalMs ?? 600_000);
|
|
70
|
+
|
|
71
|
+
const count = await ctx.taskConfigRepo.countByAgentAndChatId(ctx.agentId, ctx.chatId);
|
|
72
|
+
if (count >= MAX_TASKS_PER_CHAT) {
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: "text" as const,
|
|
77
|
+
text: `Cannot create task: maximum ${MAX_TASKS_PER_CHAT} tasks per agent per chat reached. Remove an existing task first.`,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
details: { error: "max_tasks_reached", count },
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const saved = await ctx.taskConfigRepo.create({
|
|
85
|
+
agent: ctx.agentId,
|
|
86
|
+
intervalMs,
|
|
87
|
+
prompt: params.prompt,
|
|
88
|
+
channel: ctx.channel,
|
|
89
|
+
chatId: ctx.chatId,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await ctx.schedulerHotReload("add", { id: saved.id });
|
|
93
|
+
|
|
94
|
+
const mins = Math.round(intervalMs / 60_000);
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text" as const,
|
|
99
|
+
text: `Task created: ${saved.id}\nPrompt: "${params.prompt}"\nInterval: every ${mins}m`,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
details: { id: saved.id, intervalMs },
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function createTaskUpdateTool(ctx: TaskToolContext): ToolDefinition {
|
|
109
|
+
return defineTool({
|
|
110
|
+
name: "task_update",
|
|
111
|
+
label: "Update Scheduled Task",
|
|
112
|
+
description: "Update the prompt or interval of an existing scheduled task.",
|
|
113
|
+
parameters: Type.Object({
|
|
114
|
+
id: Type.String({ description: "Task ID to update." }),
|
|
115
|
+
prompt: Type.Optional(Type.String({ description: "New prompt text." })),
|
|
116
|
+
intervalMs: Type.Optional(Type.Number({ description: "New interval in milliseconds. Minimum: 60000 (1 minute)." })),
|
|
117
|
+
}),
|
|
118
|
+
async execute(_callId, params, _signal, _onUpdate, _extCtx) {
|
|
119
|
+
if (!params.prompt && !params.intervalMs) {
|
|
120
|
+
return {
|
|
121
|
+
content: [{ type: "text", text: "Nothing to update. Provide at least a prompt or intervalMs." }],
|
|
122
|
+
details: { error: "no_changes" },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const existing = await ctx.taskConfigRepo.findById(params.id);
|
|
127
|
+
if (!existing || existing.agent !== ctx.agentId || existing.chatId !== ctx.chatId) {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: `Task not found: ${params.id}` }],
|
|
130
|
+
details: { error: "not_found" },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const patch: { prompt?: string; intervalMs?: number } = {};
|
|
135
|
+
if (params.prompt) patch.prompt = params.prompt;
|
|
136
|
+
if (params.intervalMs) patch.intervalMs = Math.max(60_000, params.intervalMs);
|
|
137
|
+
|
|
138
|
+
const updated = await ctx.taskConfigRepo.update(params.id, patch);
|
|
139
|
+
if (!updated) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text", text: `Failed to update task: ${params.id}` }],
|
|
142
|
+
details: { error: "update_failed" },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
await ctx.schedulerHotReload("update", { id: params.id });
|
|
147
|
+
|
|
148
|
+
const mins = Math.round(updated.intervalMs / 60_000);
|
|
149
|
+
return {
|
|
150
|
+
content: [
|
|
151
|
+
{
|
|
152
|
+
type: "text",
|
|
153
|
+
text: `Task updated: ${updated.id}\nPrompt: "${updated.prompt}"\nInterval: every ${mins}m`,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
details: { id: updated.id },
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function createTaskRemoveTool(ctx: TaskToolContext): ToolDefinition {
|
|
163
|
+
return defineTool({
|
|
164
|
+
name: "task_remove",
|
|
165
|
+
label: "Remove Scheduled Task",
|
|
166
|
+
description: "Remove a scheduled task by ID.",
|
|
167
|
+
parameters: Type.Object({
|
|
168
|
+
id: Type.String({ description: "Task ID to remove." }),
|
|
169
|
+
}),
|
|
170
|
+
async execute(_callId, params, _signal, _onUpdate, _extCtx) {
|
|
171
|
+
const existing = await ctx.taskConfigRepo.findById(params.id);
|
|
172
|
+
if (!existing || existing.agent !== ctx.agentId || existing.chatId !== ctx.chatId) {
|
|
173
|
+
return {
|
|
174
|
+
content: [{ type: "text", text: `Task not found: ${params.id}` }],
|
|
175
|
+
details: { error: "not_found" },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await ctx.taskConfigRepo.remove(params.id);
|
|
180
|
+
await ctx.schedulerHotReload("remove", { id: params.id });
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
content: [{ type: "text", text: `Task removed: ${params.id}` }],
|
|
184
|
+
details: { id: params.id },
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|