@pantheon.ai/agents 0.2.2 → 0.3.1
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/README.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
Create/update `.env`:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
+
# Use mysql://... for TiDB, or postgresql://... for db9.
|
|
16
17
|
DATABASE_URL=mysql://root@127.0.0.1:4000/pantheon_agents
|
|
17
18
|
PANTHEON_API_KEY=<your_pantheon_api_key>
|
|
18
19
|
DEFAULT_PANTHEON_PROJECT_ID=<optional_default_project_id>
|
|
@@ -48,6 +49,28 @@ To avoid running it twice, apply the rest of the file to `pantheon_agents`:
|
|
|
48
49
|
sed '1d' src/db/schema/tidb.sql | mysql --host 127.0.0.1 --port 4000 -u root pantheon_agents
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
## Versioned Migrations
|
|
53
|
+
|
|
54
|
+
When schema changes across versions, add versioned migration SQL files under:
|
|
55
|
+
|
|
56
|
+
- `src/db/migrations/tidb`
|
|
57
|
+
- `src/db/migrations/db9`
|
|
58
|
+
|
|
59
|
+
Example migration added in this version:
|
|
60
|
+
|
|
61
|
+
- `src/db/migrations/tidb/20260304_0001_add_task_retry_columns.sql`
|
|
62
|
+
- `src/db/migrations/db9/20260304_0001_add_task_retry_columns.sql`
|
|
63
|
+
|
|
64
|
+
Apply manually:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# TiDB/MySQL
|
|
68
|
+
mysql --host 127.0.0.1 --port 4000 -u root pantheon_agents < src/db/migrations/tidb/20260304_0001_add_task_retry_columns.sql
|
|
69
|
+
|
|
70
|
+
# db9/PostgreSQL
|
|
71
|
+
psql "$DATABASE_URL" -f src/db/migrations/db9/20260304_0001_add_task_retry_columns.sql
|
|
72
|
+
```
|
|
73
|
+
|
|
51
74
|
## Developer Local Setup
|
|
52
75
|
|
|
53
76
|
This section is only for developing this package locally.
|
|
@@ -135,6 +158,9 @@ pantheon-agents show-config <agent-name>
|
|
|
135
158
|
pantheon-agents show-tasks --all
|
|
136
159
|
pantheon-agents get-task <agent-name> <task-id>
|
|
137
160
|
pantheon-agents cancel-task <agent-name> <task-id> [reason] --yes
|
|
161
|
+
pantheon-agents retry-task <agent-name> <task-id> [reason] --yes
|
|
162
|
+
pantheon-agents skill.sh
|
|
163
|
+
pantheon-agents gen-migration-sql --provider tidb --from 0.3.0
|
|
138
164
|
pantheon-agents delete-task <agent-name> <task-id>
|
|
139
165
|
```
|
|
140
166
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- introduced_in: 0.3.1
|
|
2
|
+
-- Add retry metadata and policy columns for db9/PostgreSQL.
|
|
3
|
+
|
|
4
|
+
ALTER TABLE task
|
|
5
|
+
ADD COLUMN IF NOT EXISTS attempt_count INT NOT NULL DEFAULT 1;
|
|
6
|
+
|
|
7
|
+
ALTER TABLE agent_project_config
|
|
8
|
+
ADD COLUMN IF NOT EXISTS max_retry_attempts INT NOT NULL DEFAULT 3;
|
|
9
|
+
|
|
10
|
+
ALTER TABLE agent_project_config
|
|
11
|
+
ADD COLUMN IF NOT EXISTS retry_backoff_seconds INT NOT NULL DEFAULT 30;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- introduced_in: 0.3.1
|
|
2
|
+
-- Add retry metadata and policy columns for TiDB/MySQL.
|
|
3
|
+
|
|
4
|
+
ALTER TABLE task
|
|
5
|
+
ADD COLUMN IF NOT EXISTS attempt_count INT NOT NULL DEFAULT 1;
|
|
6
|
+
|
|
7
|
+
ALTER TABLE agent_project_config
|
|
8
|
+
ADD COLUMN IF NOT EXISTS max_retry_attempts INT NOT NULL DEFAULT 3,
|
|
9
|
+
ADD COLUMN IF NOT EXISTS retry_backoff_seconds INT NOT NULL DEFAULT 30;
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Command, InvalidArgumentError, createCommand } from "commander";
|
|
4
4
|
import process$1 from "node:process";
|
|
5
|
-
import * as fs from "node:fs";
|
|
5
|
+
import * as fs$1 from "node:fs";
|
|
6
|
+
import fs from "node:fs";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import { multistream, pino, transport } from "pino";
|
|
8
|
-
import { Kysely, MysqlDialect } from "kysely";
|
|
9
|
-
import {
|
|
9
|
+
import { Kysely, MysqlDialect, PostgresDialect } from "kysely";
|
|
10
|
+
import { Pool } from "pg";
|
|
10
11
|
import z$1, { z } from "zod";
|
|
12
|
+
import { createPool } from "mysql2";
|
|
11
13
|
import readline from "node:readline/promises";
|
|
12
14
|
import expandTilde from "expand-tilde";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
13
16
|
import blessed from "reblessed";
|
|
14
17
|
import { inspect } from "node:util";
|
|
15
18
|
import { parse } from "shell-quote";
|
|
@@ -84,7 +87,7 @@ var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
84
87
|
//#endregion
|
|
85
88
|
//#region ../../node_modules/dotenv/lib/main.js
|
|
86
89
|
var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
87
|
-
const fs$
|
|
90
|
+
const fs$2 = __require("fs");
|
|
88
91
|
const path$1 = __require("path");
|
|
89
92
|
const os = __require("os");
|
|
90
93
|
const crypto = __require("crypto");
|
|
@@ -221,10 +224,10 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
221
224
|
function _vaultPath(options) {
|
|
222
225
|
let possibleVaultPath = null;
|
|
223
226
|
if (options && options.path && options.path.length > 0) if (Array.isArray(options.path)) {
|
|
224
|
-
for (const filepath of options.path) if (fs$
|
|
227
|
+
for (const filepath of options.path) if (fs$2.existsSync(filepath)) possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
|
|
225
228
|
} else possibleVaultPath = options.path.endsWith(".vault") ? options.path : `${options.path}.vault`;
|
|
226
229
|
else possibleVaultPath = path$1.resolve(process.cwd(), ".env.vault");
|
|
227
|
-
if (fs$
|
|
230
|
+
if (fs$2.existsSync(possibleVaultPath)) return possibleVaultPath;
|
|
228
231
|
return null;
|
|
229
232
|
}
|
|
230
233
|
function _resolveHome(envPath) {
|
|
@@ -258,7 +261,7 @@ var require_main = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
258
261
|
let lastError;
|
|
259
262
|
const parsedAll = {};
|
|
260
263
|
for (const path of optionPaths) try {
|
|
261
|
-
const parsed = DotenvModule.parse(fs$
|
|
264
|
+
const parsed = DotenvModule.parse(fs$2.readFileSync(path, { encoding }));
|
|
262
265
|
DotenvModule.populate(parsedAll, parsed, options);
|
|
263
266
|
} catch (e) {
|
|
264
267
|
if (debug) _debug(`Failed to load ${path} ${e.message}`);
|
|
@@ -396,21 +399,19 @@ var require_cli_options = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
396
399
|
|
|
397
400
|
//#endregion
|
|
398
401
|
//#region package.json
|
|
399
|
-
var version = "0.
|
|
402
|
+
var version = "0.3.0";
|
|
400
403
|
|
|
401
404
|
//#endregion
|
|
402
|
-
//#region src/db/
|
|
403
|
-
function
|
|
405
|
+
//#region src/db/db9.ts
|
|
406
|
+
function createDb9PoolOptions(databaseUrl) {
|
|
404
407
|
return {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
bigNumberStrings: true,
|
|
408
|
-
timezone: "Z"
|
|
408
|
+
connectionString: databaseUrl,
|
|
409
|
+
ssl: process.env.DB9_INSECURE === "true" ? { rejectUnauthorized: false } : true
|
|
409
410
|
};
|
|
410
411
|
}
|
|
411
|
-
function
|
|
412
|
+
function createDb9Db(databaseUrl = process.env.DATABASE_URL) {
|
|
412
413
|
if (!databaseUrl) throw new Error("DATABASE_URL environment variable is not set.");
|
|
413
|
-
return new Kysely({ dialect: new
|
|
414
|
+
return new Kysely({ dialect: new PostgresDialect({ pool: new Pool(createDb9PoolOptions(databaseUrl)) }) });
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
//#endregion
|
|
@@ -428,8 +429,10 @@ const taskCommonSchema = z.object({
|
|
|
428
429
|
base_branch_id: z.uuid(),
|
|
429
430
|
parent_task_id: z.string().nullable().optional(),
|
|
430
431
|
config_version: z.number().int().positive().nullable().default(null),
|
|
432
|
+
attempt_count: z.coerce.number().int().positive().optional(),
|
|
431
433
|
cancel_reason: z.string().nullable().default(null),
|
|
432
|
-
queued_at: z.coerce.date()
|
|
434
|
+
queued_at: z.coerce.date(),
|
|
435
|
+
error: z.string().nullable().optional()
|
|
433
436
|
});
|
|
434
437
|
const taskItemSchema = z.discriminatedUnion("status", [
|
|
435
438
|
taskCommonSchema.extend({ status: z.literal("pending") }),
|
|
@@ -511,6 +514,7 @@ var TaskListProvider = class {
|
|
|
511
514
|
base_branch_id: params.base_branch_id,
|
|
512
515
|
parent_task_id: parentTaskId,
|
|
513
516
|
config_version: params.type === "reconfig" || params.type === "bootstrap" ? params.configVersion : null,
|
|
517
|
+
attempt_count: 1,
|
|
514
518
|
cancel_reason: null,
|
|
515
519
|
queued_at: /* @__PURE__ */ new Date()
|
|
516
520
|
});
|
|
@@ -602,6 +606,154 @@ var TaskListProvider = class {
|
|
|
602
606
|
}
|
|
603
607
|
};
|
|
604
608
|
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/providers/task-list-db9-provider.ts
|
|
611
|
+
var TaskListDb9Provider = class extends TaskListProvider {
|
|
612
|
+
db;
|
|
613
|
+
ownsDb;
|
|
614
|
+
constructor(agentName, logger, options = {}) {
|
|
615
|
+
super(agentName, logger);
|
|
616
|
+
if (options.db) {
|
|
617
|
+
this.db = options.db;
|
|
618
|
+
this.ownsDb = false;
|
|
619
|
+
} else {
|
|
620
|
+
this.db = createDb9Db();
|
|
621
|
+
this.ownsDb = true;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async close() {
|
|
625
|
+
if (this.ownsDb) await this.db.destroy();
|
|
626
|
+
}
|
|
627
|
+
selectTask() {
|
|
628
|
+
return this.db.selectFrom("task").selectAll().where("agent", "=", this.agentName);
|
|
629
|
+
}
|
|
630
|
+
async getAgentConfig(projectId) {
|
|
631
|
+
const config = await this.db.selectFrom("agent_project_config").innerJoin("task", "task.id", "agent_project_config.config_task_id").select([
|
|
632
|
+
"agent_project_config.agent",
|
|
633
|
+
"agent_project_config.project_id",
|
|
634
|
+
"agent_project_config.base_branch_id",
|
|
635
|
+
"agent_project_config.config_version",
|
|
636
|
+
"agent_project_config.config_task_id",
|
|
637
|
+
"agent_project_config.concurrency",
|
|
638
|
+
"agent_project_config.max_retry_attempts",
|
|
639
|
+
"agent_project_config.retry_backoff_seconds",
|
|
640
|
+
"agent_project_config.role",
|
|
641
|
+
"agent_project_config.skills",
|
|
642
|
+
"agent_project_config.prototype_url",
|
|
643
|
+
"agent_project_config.execute_agent"
|
|
644
|
+
]).select((eb) => eb.ref("task.status").$castTo().as("config_task_status")).where("agent_project_config.project_id", "=", projectId).where("agent_project_config.agent", "=", this.agentName).executeTakeFirst();
|
|
645
|
+
if (config == null) return null;
|
|
646
|
+
return config;
|
|
647
|
+
}
|
|
648
|
+
async getAgentConfigs() {
|
|
649
|
+
return await this.db.selectFrom("agent_project_config").innerJoin("task", "task.id", "agent_project_config.config_task_id").select([
|
|
650
|
+
"agent_project_config.agent",
|
|
651
|
+
"agent_project_config.project_id",
|
|
652
|
+
"agent_project_config.base_branch_id",
|
|
653
|
+
"agent_project_config.config_version",
|
|
654
|
+
"agent_project_config.config_task_id",
|
|
655
|
+
"agent_project_config.concurrency",
|
|
656
|
+
"agent_project_config.max_retry_attempts",
|
|
657
|
+
"agent_project_config.retry_backoff_seconds",
|
|
658
|
+
"agent_project_config.role",
|
|
659
|
+
"agent_project_config.skills",
|
|
660
|
+
"agent_project_config.prototype_url",
|
|
661
|
+
"agent_project_config.execute_agent"
|
|
662
|
+
]).select((eb) => eb.ref("task.status").$castTo().as("config_task_status")).where("agent_project_config.agent", "=", this.agentName).execute();
|
|
663
|
+
}
|
|
664
|
+
async setAgentConfig({ skills, ...config }) {
|
|
665
|
+
const maxRetryAttempts = config.max_retry_attempts ?? 3;
|
|
666
|
+
const retryBackoffSeconds = config.retry_backoff_seconds ?? 30;
|
|
667
|
+
await this.db.insertInto("agent_project_config").values({
|
|
668
|
+
agent: this.agentName,
|
|
669
|
+
skills: JSON.stringify(skills),
|
|
670
|
+
...config,
|
|
671
|
+
max_retry_attempts: maxRetryAttempts,
|
|
672
|
+
retry_backoff_seconds: retryBackoffSeconds
|
|
673
|
+
}).execute();
|
|
674
|
+
}
|
|
675
|
+
async updateAgentConfig({ skills, ...config }) {
|
|
676
|
+
const maxRetryAttempts = config.max_retry_attempts;
|
|
677
|
+
const retryBackoffSeconds = config.retry_backoff_seconds;
|
|
678
|
+
const result = await this.db.updateTable("agent_project_config").set({
|
|
679
|
+
skills: JSON.stringify(skills),
|
|
680
|
+
...config,
|
|
681
|
+
...maxRetryAttempts == null ? {} : { max_retry_attempts: maxRetryAttempts },
|
|
682
|
+
...retryBackoffSeconds == null ? {} : { retry_backoff_seconds: retryBackoffSeconds }
|
|
683
|
+
}).where("agent", "=", this.agentName).where("project_id", "=", config.project_id).executeTakeFirst();
|
|
684
|
+
if (Number(result.numUpdatedRows ?? 0) === 0) throw new Error(`No config found to update for agent ${this.agentName} and project ${config.project_id}.`);
|
|
685
|
+
}
|
|
686
|
+
async getTask(taskId) {
|
|
687
|
+
const taskItem = await this.selectTask().where("id", "=", taskId).executeTakeFirst();
|
|
688
|
+
if (taskItem == null) return null;
|
|
689
|
+
return taskItemSchema.parse(taskItem);
|
|
690
|
+
}
|
|
691
|
+
async getTasks({ status, order_by, order_direction = "asc", from, limit, type, min_config_version, max_config_version, parent_task_id, project_id } = {}) {
|
|
692
|
+
let builder = this.selectTask();
|
|
693
|
+
if (status && status.length > 0) builder = builder.where("status", "in", status);
|
|
694
|
+
if (from) builder = builder.where("queued_at", ">=", from);
|
|
695
|
+
if (type) builder = builder.where("type", "=", type);
|
|
696
|
+
if (min_config_version != null) builder = builder.where("config_version", ">=", min_config_version);
|
|
697
|
+
if (max_config_version != null) builder = builder.where("config_version", "<=", max_config_version);
|
|
698
|
+
if (parent_task_id === null) builder = builder.where("parent_task_id", "is", null);
|
|
699
|
+
else if (parent_task_id) builder = builder.where("parent_task_id", "=", parent_task_id);
|
|
700
|
+
if (project_id != null) builder = builder.where("project_id", "=", project_id);
|
|
701
|
+
if (order_by) builder = builder.orderBy(order_by, order_direction);
|
|
702
|
+
if (limit != null) builder = builder.limit(limit);
|
|
703
|
+
return (await builder.execute()).map((item) => taskItemSchema.parse(item));
|
|
704
|
+
}
|
|
705
|
+
async listAgentNames() {
|
|
706
|
+
const [taskAgents, configAgents] = await Promise.all([this.db.selectFrom("task").select("agent").distinct().execute(), this.db.selectFrom("agent_project_config").select("agent").distinct().execute()]);
|
|
707
|
+
const agents = /* @__PURE__ */ new Set();
|
|
708
|
+
for (const item of taskAgents) agents.add(item.agent);
|
|
709
|
+
for (const item of configAgents) agents.add(item.agent);
|
|
710
|
+
return Array.from(agents).sort();
|
|
711
|
+
}
|
|
712
|
+
async updateTask(taskItem) {
|
|
713
|
+
const { id, started_at, ended_at, queued_at, cancelled_at, ...rest } = taskItem;
|
|
714
|
+
await this.db.updateTable("task").set({
|
|
715
|
+
started_at,
|
|
716
|
+
ended_at,
|
|
717
|
+
queued_at,
|
|
718
|
+
cancelled_at,
|
|
719
|
+
...rest
|
|
720
|
+
}).where("id", "=", id).where("agent", "=", this.agentName).execute();
|
|
721
|
+
}
|
|
722
|
+
async insertTask(taskItem) {
|
|
723
|
+
const { started_at, ended_at, queued_at, cancelled_at, ...rest } = taskItem;
|
|
724
|
+
const attemptCount = taskItem.attempt_count ?? 1;
|
|
725
|
+
const inserted = await this.db.insertInto("task").values({
|
|
726
|
+
agent: this.agentName,
|
|
727
|
+
started_at,
|
|
728
|
+
ended_at,
|
|
729
|
+
queued_at,
|
|
730
|
+
cancelled_at,
|
|
731
|
+
...rest,
|
|
732
|
+
attempt_count: attemptCount
|
|
733
|
+
}).returningAll().executeTakeFirstOrThrow();
|
|
734
|
+
return taskItemSchema.parse(inserted);
|
|
735
|
+
}
|
|
736
|
+
async deleteTask(taskId) {
|
|
737
|
+
const result = await this.db.deleteFrom("task").where("id", "=", taskId).where("agent", "=", this.agentName).executeTakeFirst();
|
|
738
|
+
return Number(result.numDeletedRows ?? 0) > 0;
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
//#endregion
|
|
743
|
+
//#region src/db/tidb.ts
|
|
744
|
+
function createTidbPoolOptions(databaseUrl) {
|
|
745
|
+
return {
|
|
746
|
+
uri: databaseUrl,
|
|
747
|
+
supportBigNumbers: true,
|
|
748
|
+
bigNumberStrings: true,
|
|
749
|
+
timezone: "Z"
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function createTidbDb(databaseUrl = process.env.DATABASE_URL) {
|
|
753
|
+
if (!databaseUrl) throw new Error("DATABASE_URL environment variable is not set.");
|
|
754
|
+
return new Kysely({ dialect: new MysqlDialect({ pool: createPool(createTidbPoolOptions(databaseUrl)) }) });
|
|
755
|
+
}
|
|
756
|
+
|
|
605
757
|
//#endregion
|
|
606
758
|
//#region src/providers/task-list-tidb-provider.ts
|
|
607
759
|
var TaskListTidbProvider = class extends TaskListProvider {
|
|
@@ -632,16 +784,24 @@ var TaskListTidbProvider = class extends TaskListProvider {
|
|
|
632
784
|
return await this.db.selectFrom("agent_project_config").innerJoin("task", "task.id", "agent_project_config.config_task_id").selectAll("agent_project_config").select((eb) => eb.ref("task.status").$castTo().as("config_task_status")).where("agent_project_config.agent", "=", this.agentName).execute();
|
|
633
785
|
}
|
|
634
786
|
async setAgentConfig({ skills, ...config }) {
|
|
787
|
+
const maxRetryAttempts = config.max_retry_attempts ?? 3;
|
|
788
|
+
const retryBackoffSeconds = config.retry_backoff_seconds ?? 30;
|
|
635
789
|
await this.db.insertInto("agent_project_config").values({
|
|
636
790
|
agent: this.agentName,
|
|
637
791
|
skills: JSON.stringify(skills),
|
|
638
|
-
...config
|
|
792
|
+
...config,
|
|
793
|
+
max_retry_attempts: maxRetryAttempts,
|
|
794
|
+
retry_backoff_seconds: retryBackoffSeconds
|
|
639
795
|
}).execute();
|
|
640
796
|
}
|
|
641
797
|
async updateAgentConfig({ skills, ...config }) {
|
|
798
|
+
const maxRetryAttempts = config.max_retry_attempts;
|
|
799
|
+
const retryBackoffSeconds = config.retry_backoff_seconds;
|
|
642
800
|
const result = await this.db.updateTable("agent_project_config").set({
|
|
643
801
|
skills: JSON.stringify(skills),
|
|
644
|
-
...config
|
|
802
|
+
...config,
|
|
803
|
+
...maxRetryAttempts == null ? {} : { max_retry_attempts: maxRetryAttempts },
|
|
804
|
+
...retryBackoffSeconds == null ? {} : { retry_backoff_seconds: retryBackoffSeconds }
|
|
645
805
|
}).where("agent", "=", this.agentName).where("project_id", "=", config.project_id).executeTakeFirst();
|
|
646
806
|
if (Number(result.numUpdatedRows ?? 0) === 0) throw new Error(`No config found to update for agent ${this.agentName} and project ${config.project_id}.`);
|
|
647
807
|
}
|
|
@@ -683,13 +843,15 @@ var TaskListTidbProvider = class extends TaskListProvider {
|
|
|
683
843
|
}
|
|
684
844
|
async insertTask(taskItem) {
|
|
685
845
|
const { started_at, ended_at, queued_at, cancelled_at, ...rest } = taskItem;
|
|
846
|
+
const attemptCount = taskItem.attempt_count ?? 1;
|
|
686
847
|
const { insertId } = await this.db.insertInto("task").values({
|
|
687
848
|
agent: this.agentName,
|
|
688
849
|
started_at,
|
|
689
850
|
ended_at,
|
|
690
851
|
queued_at,
|
|
691
852
|
cancelled_at,
|
|
692
|
-
...rest
|
|
853
|
+
...rest,
|
|
854
|
+
attempt_count: attemptCount
|
|
693
855
|
}).executeTakeFirstOrThrow();
|
|
694
856
|
return {
|
|
695
857
|
...taskItem,
|
|
@@ -702,6 +864,28 @@ var TaskListTidbProvider = class extends TaskListProvider {
|
|
|
702
864
|
}
|
|
703
865
|
};
|
|
704
866
|
|
|
867
|
+
//#endregion
|
|
868
|
+
//#region src/providers/task-list-provider-factory.ts
|
|
869
|
+
function getDatabaseProtocol(databaseUrl) {
|
|
870
|
+
try {
|
|
871
|
+
return new URL(databaseUrl).protocol.toLowerCase();
|
|
872
|
+
} catch {
|
|
873
|
+
throw new Error("Invalid DATABASE_URL. Expected a URL starting with mysql:// or postgresql://.");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function resolveTaskListProviderKind(databaseUrl) {
|
|
877
|
+
const protocol = getDatabaseProtocol(databaseUrl);
|
|
878
|
+
if (protocol === "mysql:") return "tidb";
|
|
879
|
+
if (protocol === "postgresql:") return "db9";
|
|
880
|
+
throw new Error(`Unsupported DATABASE_URL protocol '${protocol}'. Use mysql:// for TiDB or postgresql:// for db9.`);
|
|
881
|
+
}
|
|
882
|
+
function createTaskListProvider(agentName, logger) {
|
|
883
|
+
const databaseUrl = process.env.DATABASE_URL?.trim();
|
|
884
|
+
if (!databaseUrl) throw new Error("DATABASE_URL environment variable is not set.");
|
|
885
|
+
if (resolveTaskListProviderKind(databaseUrl) === "db9") return new TaskListDb9Provider(agentName, logger);
|
|
886
|
+
return new TaskListTidbProvider(agentName, logger);
|
|
887
|
+
}
|
|
888
|
+
|
|
705
889
|
//#endregion
|
|
706
890
|
//#region src/core/agent-config-utils.ts
|
|
707
891
|
function normalizeRepoUrl(value) {
|
|
@@ -1016,7 +1200,7 @@ const branchExecutionResultSchema = z.object({
|
|
|
1016
1200
|
status: z.string(),
|
|
1017
1201
|
status_text: z.string(),
|
|
1018
1202
|
branch_id: z.string(),
|
|
1019
|
-
snap_id: z.string(),
|
|
1203
|
+
snap_id: z.string().nullable(),
|
|
1020
1204
|
background_task_id: z.string().nullable(),
|
|
1021
1205
|
started_at: zodJsonDate,
|
|
1022
1206
|
last_polled_at: zodJsonDate,
|
|
@@ -1093,7 +1277,7 @@ const explorationResultSchema = z.object({
|
|
|
1093
1277
|
status: z.string(),
|
|
1094
1278
|
status_text: z.string().nullable(),
|
|
1095
1279
|
branch_id: z.string(),
|
|
1096
|
-
snap_id: z.string(),
|
|
1280
|
+
snap_id: z.string().nullable(),
|
|
1097
1281
|
background_task_id: z.string().nullable(),
|
|
1098
1282
|
started_at: zodJsonDate,
|
|
1099
1283
|
last_polled_at: zodJsonDate,
|
|
@@ -1902,7 +2086,65 @@ async function executeOnPantheon({ projectId, branchId, prompt, agent }) {
|
|
|
1902
2086
|
//#endregion
|
|
1903
2087
|
//#region src/core/task-list.ts
|
|
1904
2088
|
const DEFAULT_CONCURRENCY = 1;
|
|
2089
|
+
const DEFAULT_MAX_RETRY_ATTEMPTS$1 = 3;
|
|
2090
|
+
const DEFAULT_RETRY_BACKOFF_SECONDS$1 = 30;
|
|
1905
2091
|
const AUTO_RECONFIG_PROMPT = "Config outdated. Reconfigure this branch against the latest agent config.";
|
|
2092
|
+
function getTaskAttemptCount(task) {
|
|
2093
|
+
if (Number.isInteger(task.attempt_count) && task.attempt_count > 0) return task.attempt_count;
|
|
2094
|
+
return 1;
|
|
2095
|
+
}
|
|
2096
|
+
function getMaxRetryAttempts(config, logger) {
|
|
2097
|
+
const configured = config.max_retry_attempts;
|
|
2098
|
+
if (configured == null) return DEFAULT_MAX_RETRY_ATTEMPTS$1;
|
|
2099
|
+
if (Number.isInteger(configured) && configured >= 0) return configured;
|
|
2100
|
+
logger.warn("Invalid max_retry_attempts=%o for project %s; fallback to %d.", configured, config.project_id, DEFAULT_MAX_RETRY_ATTEMPTS$1);
|
|
2101
|
+
return DEFAULT_MAX_RETRY_ATTEMPTS$1;
|
|
2102
|
+
}
|
|
2103
|
+
function getRetryBackoffSeconds(config, logger) {
|
|
2104
|
+
const configured = config.retry_backoff_seconds;
|
|
2105
|
+
if (configured == null) return DEFAULT_RETRY_BACKOFF_SECONDS$1;
|
|
2106
|
+
if (Number.isInteger(configured) && configured > 0) return configured;
|
|
2107
|
+
logger.warn("Invalid retry_backoff_seconds=%o for project %s; fallback to %d.", configured, config.project_id, DEFAULT_RETRY_BACKOFF_SECONDS$1);
|
|
2108
|
+
return DEFAULT_RETRY_BACKOFF_SECONDS$1;
|
|
2109
|
+
}
|
|
2110
|
+
function getBackoffDelaySeconds(backoffBaseSeconds, nextAttemptCount) {
|
|
2111
|
+
return backoffBaseSeconds * 2 ** (Math.max(1, nextAttemptCount - 1) - 1);
|
|
2112
|
+
}
|
|
2113
|
+
async function retryFailedTaskInPlace(provider, task, logger, options = {}) {
|
|
2114
|
+
const config = await provider.getAgentConfig(task.project_id);
|
|
2115
|
+
if (!config) {
|
|
2116
|
+
logger.warn("Skip retry for task %s because project config %s is missing.", task.id, task.project_id);
|
|
2117
|
+
return null;
|
|
2118
|
+
}
|
|
2119
|
+
const currentAttemptCount = getTaskAttemptCount(task);
|
|
2120
|
+
const retriesUsed = Math.max(0, currentAttemptCount - 1);
|
|
2121
|
+
const maxRetryAttempts = getMaxRetryAttempts(config, logger);
|
|
2122
|
+
if (!options.ignoreRetryLimit && retriesUsed >= maxRetryAttempts) {
|
|
2123
|
+
logger.info("Task %s reached max retries (%d). Keep failed status.", task.id, maxRetryAttempts);
|
|
2124
|
+
return null;
|
|
2125
|
+
}
|
|
2126
|
+
const nextAttemptCount = currentAttemptCount + 1;
|
|
2127
|
+
const now = Date.now();
|
|
2128
|
+
const delaySeconds = options.immediate ? 0 : getBackoffDelaySeconds(getRetryBackoffSeconds(config, logger), nextAttemptCount);
|
|
2129
|
+
const queuedAt = new Date(now + delaySeconds * 1e3);
|
|
2130
|
+
const retriedTask = {
|
|
2131
|
+
status: "pending",
|
|
2132
|
+
id: task.id,
|
|
2133
|
+
task: task.task,
|
|
2134
|
+
type: task.type,
|
|
2135
|
+
project_id: task.project_id,
|
|
2136
|
+
base_branch_id: task.base_branch_id,
|
|
2137
|
+
parent_task_id: task.parent_task_id ?? null,
|
|
2138
|
+
config_version: task.config_version ?? null,
|
|
2139
|
+
attempt_count: nextAttemptCount,
|
|
2140
|
+
cancel_reason: task.cancel_reason ?? null,
|
|
2141
|
+
queued_at: queuedAt,
|
|
2142
|
+
error: task.error
|
|
2143
|
+
};
|
|
2144
|
+
await provider.updateTask(retriedTask);
|
|
2145
|
+
logger.info("Requeued failed task %s for attempt %d at %s%s.", task.id, nextAttemptCount, queuedAt.toISOString(), options.reason ? ` (reason: ${options.reason})` : "");
|
|
2146
|
+
return retriedTask;
|
|
2147
|
+
}
|
|
1906
2148
|
function buildTaskPromptSequence(config, taskPrompt, taskType = "default") {
|
|
1907
2149
|
if (taskType === "bootstrap" && taskPrompt === "$setup") return [buildConfigSetupStep({
|
|
1908
2150
|
prototypeRepoUrl: config.prototype_url,
|
|
@@ -1929,7 +2171,7 @@ async function startTaskListLoop(agentName, { loopInterval = 5 }, logger) {
|
|
|
1929
2171
|
abortController.abort(signal);
|
|
1930
2172
|
process.exit(1);
|
|
1931
2173
|
});
|
|
1932
|
-
const provider =
|
|
2174
|
+
const provider = createTaskListProvider(agentName, logger);
|
|
1933
2175
|
while (!abortController.signal.aborted) {
|
|
1934
2176
|
const loopIndex = i++;
|
|
1935
2177
|
await loopOnce(provider, logger.child({ name: `loop:${loopIndex}` }));
|
|
@@ -1989,6 +2231,8 @@ async function pollRunningTaskState(provider, state, logger) {
|
|
|
1989
2231
|
} else if (newStatus.state === "failed") {
|
|
1990
2232
|
logger.info(`Task failed on Branch[id = ${newStatus.branch.id},snap_id=${newStatus.branch.latest_snap_id}] with error: %s`, newStatus.error);
|
|
1991
2233
|
await provider.failTask(state, newStatus.error);
|
|
2234
|
+
const failedTask = await provider.getTask(state.id);
|
|
2235
|
+
if (failedTask?.status === "failed") await retryFailedTaskInPlace(provider, failedTask, logger.child({ name: `task:${state.id}:auto-retry` }));
|
|
1992
2236
|
}
|
|
1993
2237
|
}
|
|
1994
2238
|
function isTaskOutdated(task, config) {
|
|
@@ -2113,6 +2357,7 @@ async function startPendingTasksUpToConcurrency(provider, pendingTasks, runningT
|
|
|
2113
2357
|
return config;
|
|
2114
2358
|
};
|
|
2115
2359
|
for (const task of pendingTasks) {
|
|
2360
|
+
if (task.queued_at.getTime() > Date.now()) continue;
|
|
2116
2361
|
const config = await getProjectConfig(task.project_id);
|
|
2117
2362
|
if (!config) {
|
|
2118
2363
|
if (!missingConfigProjects.has(task.project_id)) {
|
|
@@ -2129,6 +2374,8 @@ async function startPendingTasksUpToConcurrency(provider, pendingTasks, runningT
|
|
|
2129
2374
|
runningCountByProject.set(task.project_id, runningCount + 1);
|
|
2130
2375
|
} catch (e) {
|
|
2131
2376
|
logger.error(`Failed to start task ${task.id}: ${getErrorMessage(e)}`);
|
|
2377
|
+
const failedTask = await provider.getTask(task.id);
|
|
2378
|
+
if (failedTask?.status === "failed") await retryFailedTaskInPlace(provider, failedTask, logger.child({ name: `task:${task.id}:auto-retry` }));
|
|
2132
2379
|
}
|
|
2133
2380
|
}
|
|
2134
2381
|
}
|
|
@@ -2558,10 +2805,12 @@ var WatchStepAggregator = class {
|
|
|
2558
2805
|
|
|
2559
2806
|
//#endregion
|
|
2560
2807
|
//#region src/core/index.ts
|
|
2808
|
+
const DEFAULT_MAX_RETRY_ATTEMPTS = 3;
|
|
2809
|
+
const DEFAULT_RETRY_BACKOFF_SECONDS = 30;
|
|
2561
2810
|
async function runAgent(name, options, logger) {
|
|
2562
2811
|
const agentDir = path.join(options.dataDir, "agents", name);
|
|
2563
2812
|
const pidFile = path.join(agentDir, "pid");
|
|
2564
|
-
await fs.promises.mkdir(agentDir, { recursive: true });
|
|
2813
|
+
await fs$1.promises.mkdir(agentDir, { recursive: true });
|
|
2565
2814
|
await assertsSingleton(logger, pidFile);
|
|
2566
2815
|
await startTaskListLoop(name, { loopInterval: options.loopInterval }, logger);
|
|
2567
2816
|
}
|
|
@@ -2575,7 +2824,7 @@ function resolveConfigRole(options) {
|
|
|
2575
2824
|
return { error: "--role is required for first-time config in a project." };
|
|
2576
2825
|
}
|
|
2577
2826
|
async function configAgent(name, options) {
|
|
2578
|
-
const provider =
|
|
2827
|
+
const provider = createTaskListProvider(name, pino());
|
|
2579
2828
|
try {
|
|
2580
2829
|
const previousConfig = await provider.getAgentConfig(options.projectId);
|
|
2581
2830
|
const resolvedRoleResult = resolveConfigRole({
|
|
@@ -2594,6 +2843,18 @@ async function configAgent(name, options) {
|
|
|
2594
2843
|
process.exitCode = 1;
|
|
2595
2844
|
return;
|
|
2596
2845
|
}
|
|
2846
|
+
const maxRetryAttempts = options.maxRetryAttempts ?? previousConfig?.max_retry_attempts ?? DEFAULT_MAX_RETRY_ATTEMPTS;
|
|
2847
|
+
if (!Number.isInteger(maxRetryAttempts) || maxRetryAttempts < 0) {
|
|
2848
|
+
console.error("--max-retry-attempts must be a non-negative integer.");
|
|
2849
|
+
process.exitCode = 1;
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const retryBackoffSeconds = options.retryBackoffSeconds ?? previousConfig?.retry_backoff_seconds ?? DEFAULT_RETRY_BACKOFF_SECONDS;
|
|
2853
|
+
if (!Number.isInteger(retryBackoffSeconds) || retryBackoffSeconds <= 0) {
|
|
2854
|
+
console.error("--retry-backoff-seconds must be a positive integer.");
|
|
2855
|
+
process.exitCode = 1;
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2597
2858
|
const resolvedSkills = options.skills ?? normalizeSkills(previousConfig?.skills);
|
|
2598
2859
|
const resolvedExecuteAgent = options.executeAgent.trim() || previousConfig?.execute_agent || "codex";
|
|
2599
2860
|
const resolvedPrototypeUrl = options.prototypeUrl.trim() || previousConfig?.prototype_url || "https://github.com/pingcap-inc/pantheon-agents";
|
|
@@ -2624,6 +2885,8 @@ async function configAgent(name, options) {
|
|
|
2624
2885
|
config_task_id: configTaskId,
|
|
2625
2886
|
config_version: previousConfig.config_version + (options.prompt ? 2 : 1),
|
|
2626
2887
|
concurrency,
|
|
2888
|
+
max_retry_attempts: maxRetryAttempts,
|
|
2889
|
+
retry_backoff_seconds: retryBackoffSeconds,
|
|
2627
2890
|
execute_agent: resolvedExecuteAgent,
|
|
2628
2891
|
role: resolvedRole ?? previousConfig.role,
|
|
2629
2892
|
skills: resolvedSkills,
|
|
@@ -2691,6 +2954,8 @@ async function configAgent(name, options) {
|
|
|
2691
2954
|
config_task_id: configTaskId,
|
|
2692
2955
|
config_version: configVersion,
|
|
2693
2956
|
concurrency,
|
|
2957
|
+
max_retry_attempts: maxRetryAttempts,
|
|
2958
|
+
retry_backoff_seconds: retryBackoffSeconds,
|
|
2694
2959
|
execute_agent: resolvedExecuteAgent,
|
|
2695
2960
|
role: resolvedRole,
|
|
2696
2961
|
skills: resolvedSkills,
|
|
@@ -2702,6 +2967,8 @@ async function configAgent(name, options) {
|
|
|
2702
2967
|
config_task_id: configTaskId,
|
|
2703
2968
|
config_version: configVersion,
|
|
2704
2969
|
concurrency,
|
|
2970
|
+
max_retry_attempts: maxRetryAttempts,
|
|
2971
|
+
retry_backoff_seconds: retryBackoffSeconds,
|
|
2705
2972
|
execute_agent: resolvedExecuteAgent,
|
|
2706
2973
|
role: resolvedRole ?? previousConfig.role,
|
|
2707
2974
|
skills: resolvedSkills,
|
|
@@ -2735,7 +3002,7 @@ async function resolveInitialBaseBranchId(projectId, rootBranchId) {
|
|
|
2735
3002
|
return project.root_branch_id;
|
|
2736
3003
|
}
|
|
2737
3004
|
async function addTask(name, options) {
|
|
2738
|
-
const provider =
|
|
3005
|
+
const provider = createTaskListProvider(name, pino());
|
|
2739
3006
|
try {
|
|
2740
3007
|
const config = await provider.getAgentConfig(options.projectId);
|
|
2741
3008
|
if (!config) throw new Error(`Agent ${name} not configured for project ${options.projectId}`);
|
|
@@ -2751,7 +3018,7 @@ async function addTask(name, options) {
|
|
|
2751
3018
|
}
|
|
2752
3019
|
}
|
|
2753
3020
|
async function deleteTask(agentName, taskId) {
|
|
2754
|
-
const provider =
|
|
3021
|
+
const provider = createTaskListProvider(agentName, pino());
|
|
2755
3022
|
try {
|
|
2756
3023
|
const task = await provider.getTask(taskId);
|
|
2757
3024
|
if (!task) return null;
|
|
@@ -2762,7 +3029,7 @@ async function deleteTask(agentName, taskId) {
|
|
|
2762
3029
|
}
|
|
2763
3030
|
}
|
|
2764
3031
|
async function getTask(agentName, taskId) {
|
|
2765
|
-
const provider =
|
|
3032
|
+
const provider = createTaskListProvider(agentName, pino());
|
|
2766
3033
|
try {
|
|
2767
3034
|
return await provider.getTask(taskId);
|
|
2768
3035
|
} finally {
|
|
@@ -2770,7 +3037,7 @@ async function getTask(agentName, taskId) {
|
|
|
2770
3037
|
}
|
|
2771
3038
|
}
|
|
2772
3039
|
async function cancelTask(agentName, taskId, reason) {
|
|
2773
|
-
const provider =
|
|
3040
|
+
const provider = createTaskListProvider(agentName, pino());
|
|
2774
3041
|
try {
|
|
2775
3042
|
const task = await provider.getTask(taskId);
|
|
2776
3043
|
if (!task) return null;
|
|
@@ -2781,8 +3048,24 @@ async function cancelTask(agentName, taskId, reason) {
|
|
|
2781
3048
|
await provider.close();
|
|
2782
3049
|
}
|
|
2783
3050
|
}
|
|
3051
|
+
async function retryTask(agentName, taskId, reason) {
|
|
3052
|
+
const provider = createTaskListProvider(agentName, pino());
|
|
3053
|
+
try {
|
|
3054
|
+
const task = await provider.getTask(taskId);
|
|
3055
|
+
if (!task) return null;
|
|
3056
|
+
if (task.status !== "failed") throw new Error(`Task ${taskId} is ${task.status}; only failed tasks can be retried.`);
|
|
3057
|
+
if (!await retryFailedTaskInPlace(provider, task, pino(), {
|
|
3058
|
+
immediate: true,
|
|
3059
|
+
reason,
|
|
3060
|
+
ignoreRetryLimit: true
|
|
3061
|
+
})) throw new Error(`Task ${taskId} retry was not scheduled.`);
|
|
3062
|
+
return await provider.getTask(taskId);
|
|
3063
|
+
} finally {
|
|
3064
|
+
await provider.close();
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
2784
3067
|
async function showAgentConfig(agentName, projectId) {
|
|
2785
|
-
const provider =
|
|
3068
|
+
const provider = createTaskListProvider(agentName, pino());
|
|
2786
3069
|
try {
|
|
2787
3070
|
return await provider.getAgentConfig(projectId);
|
|
2788
3071
|
} finally {
|
|
@@ -2790,7 +3073,7 @@ async function showAgentConfig(agentName, projectId) {
|
|
|
2790
3073
|
}
|
|
2791
3074
|
}
|
|
2792
3075
|
async function showAgentConfigs(agentName) {
|
|
2793
|
-
const provider =
|
|
3076
|
+
const provider = createTaskListProvider(agentName, pino());
|
|
2794
3077
|
try {
|
|
2795
3078
|
return await provider.getAgentConfigs();
|
|
2796
3079
|
} finally {
|
|
@@ -2798,7 +3081,7 @@ async function showAgentConfigs(agentName) {
|
|
|
2798
3081
|
}
|
|
2799
3082
|
}
|
|
2800
3083
|
async function showTasks(name, options) {
|
|
2801
|
-
const provider =
|
|
3084
|
+
const provider = createTaskListProvider(name, pino());
|
|
2802
3085
|
try {
|
|
2803
3086
|
return await provider.getTasks({
|
|
2804
3087
|
status: options.status,
|
|
@@ -2811,7 +3094,7 @@ async function showTasks(name, options) {
|
|
|
2811
3094
|
}
|
|
2812
3095
|
}
|
|
2813
3096
|
async function listAgentNames() {
|
|
2814
|
-
const provider =
|
|
3097
|
+
const provider = createTaskListProvider("list-agents", pino());
|
|
2815
3098
|
try {
|
|
2816
3099
|
return await provider.listAgentNames();
|
|
2817
3100
|
} finally {
|
|
@@ -2883,24 +3166,26 @@ function formatConciseTaskLine(options) {
|
|
|
2883
3166
|
const statusText = formatStatus(task.status, useColor);
|
|
2884
3167
|
const timeText = formatRelativeTime(timestamp);
|
|
2885
3168
|
const taskText = truncateText(task.task, maxTaskLength);
|
|
3169
|
+
const attempt = Number.isInteger(task.attempt_count) && task.attempt_count > 0 ? task.attempt_count : 1;
|
|
2886
3170
|
return [
|
|
2887
3171
|
agent,
|
|
2888
3172
|
statusText,
|
|
2889
3173
|
task.id,
|
|
3174
|
+
`attempt:${attempt}`,
|
|
2890
3175
|
timeText,
|
|
2891
3176
|
taskText
|
|
2892
3177
|
].filter((part) => part !== "").join(" ");
|
|
2893
3178
|
}
|
|
2894
3179
|
async function assertsSingleton(logger, pidFile) {
|
|
2895
3180
|
try {
|
|
2896
|
-
const pid = await fs.promises.readFile(pidFile, "utf-8");
|
|
3181
|
+
const pid = await fs$1.promises.readFile(pidFile, "utf-8");
|
|
2897
3182
|
process.kill(parseInt(pid), 0);
|
|
2898
3183
|
console.error("Failed to assert singleton agent process:");
|
|
2899
3184
|
process.exit(1);
|
|
2900
3185
|
} catch (e) {
|
|
2901
|
-
await fs.promises.writeFile(pidFile, process.pid.toString());
|
|
3186
|
+
await fs$1.promises.writeFile(pidFile, process.pid.toString());
|
|
2902
3187
|
process.on("exit", () => {
|
|
2903
|
-
fs.promises.rm(pidFile);
|
|
3188
|
+
fs$1.promises.rm(pidFile);
|
|
2904
3189
|
});
|
|
2905
3190
|
}
|
|
2906
3191
|
}
|
|
@@ -3069,8 +3354,18 @@ function parseConcurrency(value) {
|
|
|
3069
3354
|
if (!Number.isInteger(parsed) || parsed < 1) throw new InvalidArgumentError("concurrency must be a positive integer.");
|
|
3070
3355
|
return parsed;
|
|
3071
3356
|
}
|
|
3357
|
+
function parseNonNegativeInteger(value, fieldName) {
|
|
3358
|
+
const parsed = Number(value);
|
|
3359
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new InvalidArgumentError(`${fieldName} must be a non-negative integer.`);
|
|
3360
|
+
return parsed;
|
|
3361
|
+
}
|
|
3362
|
+
function parsePositiveInteger(value, fieldName) {
|
|
3363
|
+
const parsed = Number(value);
|
|
3364
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new InvalidArgumentError(`${fieldName} must be a positive integer.`);
|
|
3365
|
+
return parsed;
|
|
3366
|
+
}
|
|
3072
3367
|
function createConfigAgentCommand(version, deps = {}) {
|
|
3073
|
-
return createCommand("config").version(version).description("Queue a configuration task for an agent/project").argument("<name>", "The name of the agent.").argument("[prompt]", "The configuration task prompt.").option("--project-id <project-id>", "The project id of the agent. Defaults to DEFAULT_PANTHEON_PROJECT_ID.").option("--task-id <task-id>", "Optional parent task id.").option("--role <role>", "Role metadata. Required for first-time project config; optional override later.").option("--skills <skills>", "The skills of the agent. Multiple values are separated by comma.", parseUniqueCommaList).option("--execute-agent <agent>", "The execute agent of the agent.", "codex").option("--concurrency <number>", "Max number of parallel running tasks for this project.", parseConcurrency).option("--root-branch-id <branchId>", "The root branch id of the agent. Defaults to DEFAULT_PANTHEON_ROOT_BRANCH_ID, then project root branch id.").option("--prototype-url <url>", "Role and skill definitions repo.", "https://github.com/pingcap-inc/pantheon-agents").action(async function() {
|
|
3368
|
+
return createCommand("config").version(version).description("Queue a configuration task for an agent/project").argument("<name>", "The name of the agent.").argument("[prompt]", "The configuration task prompt.").option("--project-id <project-id>", "The project id of the agent. Defaults to DEFAULT_PANTHEON_PROJECT_ID.").option("--task-id <task-id>", "Optional parent task id.").option("--role <role>", "Role metadata. Required for first-time project config; optional override later.").option("--skills <skills>", "The skills of the agent. Multiple values are separated by comma.", parseUniqueCommaList).option("--execute-agent <agent>", "The execute agent of the agent.", "codex").option("--concurrency <number>", "Max number of parallel running tasks for this project.", parseConcurrency).option("--max-retry-attempts <number>", "Max automatic retry attempts per task after failures.", (value) => parseNonNegativeInteger(value, "max-retry-attempts")).option("--retry-backoff-seconds <seconds>", "Base delay (seconds) for exponential retry backoff.", (value) => parsePositiveInteger(value, "retry-backoff-seconds")).option("--root-branch-id <branchId>", "The root branch id of the agent. Defaults to DEFAULT_PANTHEON_ROOT_BRANCH_ID, then project root branch id.").option("--prototype-url <url>", "Role and skill definitions repo.", "https://github.com/pingcap-inc/pantheon-agents").action(async function() {
|
|
3074
3369
|
const [name, prompt] = this.args;
|
|
3075
3370
|
const options = this.opts();
|
|
3076
3371
|
const resolvedProjectId = resolvePantheonProjectId(options.projectId);
|
|
@@ -3088,6 +3383,8 @@ function createConfigAgentCommand(version, deps = {}) {
|
|
|
3088
3383
|
role: options.role,
|
|
3089
3384
|
executeAgent: options.executeAgent,
|
|
3090
3385
|
concurrency: options.concurrency,
|
|
3386
|
+
maxRetryAttempts: options.maxRetryAttempts,
|
|
3387
|
+
retryBackoffSeconds: options.retryBackoffSeconds,
|
|
3091
3388
|
skills: options.skills,
|
|
3092
3389
|
prototypeUrl: options.prototypeUrl,
|
|
3093
3390
|
rootBranchId: resolvedRootBranchId
|
|
@@ -3142,9 +3439,11 @@ function formatTaskAsSkillTask(agent, task) {
|
|
|
3142
3439
|
const endedAt = "ended_at" in task ? task.ended_at ?? null : null;
|
|
3143
3440
|
const canceledAt = task.status === "cancelled" ? task.cancelled_at : null;
|
|
3144
3441
|
const finishedAt = endedAt ?? canceledAt ?? null;
|
|
3442
|
+
const attempt = Number.isInteger(task.attempt_count) && task.attempt_count > 0 ? task.attempt_count : 1;
|
|
3145
3443
|
return {
|
|
3146
3444
|
agent,
|
|
3147
3445
|
task: task.task,
|
|
3446
|
+
attempt,
|
|
3148
3447
|
parent_task_id: task.parent_task_id ?? null,
|
|
3149
3448
|
status: toSkillTaskStatus(task.status),
|
|
3150
3449
|
queued_at: task.queued_at.toISOString(),
|
|
@@ -3154,7 +3453,7 @@ function formatTaskAsSkillTask(agent, task) {
|
|
|
3154
3453
|
canceled_at: toIsoOrNull(canceledAt),
|
|
3155
3454
|
cancel_reason: task.cancel_reason ?? null,
|
|
3156
3455
|
output: task.status === "completed" ? task.output : null,
|
|
3157
|
-
error: task.
|
|
3456
|
+
error: task.error ?? null
|
|
3158
3457
|
};
|
|
3159
3458
|
}
|
|
3160
3459
|
function createGetTaskCommand(version, deps = {}) {
|
|
@@ -3235,6 +3534,8 @@ function createShowConfigCommand(version) {
|
|
|
3235
3534
|
config_version: config.config_version,
|
|
3236
3535
|
config_task_id: config.config_task_id,
|
|
3237
3536
|
concurrency: config.concurrency,
|
|
3537
|
+
max_retry_attempts: config.max_retry_attempts ?? 3,
|
|
3538
|
+
retry_backoff_seconds: config.retry_backoff_seconds ?? 30,
|
|
3238
3539
|
role: config.role,
|
|
3239
3540
|
execute_agent: config.execute_agent,
|
|
3240
3541
|
prototype_url: config.prototype_url,
|
|
@@ -3259,6 +3560,8 @@ function createShowConfigCommand(version) {
|
|
|
3259
3560
|
config_version: config.config_version,
|
|
3260
3561
|
config_task_id: config.config_task_id,
|
|
3261
3562
|
concurrency: config.concurrency,
|
|
3563
|
+
max_retry_attempts: config.max_retry_attempts ?? 3,
|
|
3564
|
+
retry_backoff_seconds: config.retry_backoff_seconds ?? 30,
|
|
3262
3565
|
role: config.role,
|
|
3263
3566
|
execute_agent: config.execute_agent,
|
|
3264
3567
|
prototype_url: config.prototype_url,
|
|
@@ -3382,6 +3685,135 @@ function createShowTasksCommand(version, deps = {}) {
|
|
|
3382
3685
|
});
|
|
3383
3686
|
}
|
|
3384
3687
|
|
|
3688
|
+
//#endregion
|
|
3689
|
+
//#region src/cli/commands/retry-task.ts
|
|
3690
|
+
function resolveTaskAttempt(value) {
|
|
3691
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) return value;
|
|
3692
|
+
return 1;
|
|
3693
|
+
}
|
|
3694
|
+
function createRetryTaskCommand(version, deps = {}) {
|
|
3695
|
+
return createCommand("retry-task").version(version).description("Retry a failed task for an agent").argument("<name>", "The name of the agent.").argument("<task-id>", "The id of the failed task.").argument("[reason]", "Optional retry reason.").option("-y, --yes", "Skip confirmation prompt.").option("-f, --force", "Alias for --yes.").action(async function() {
|
|
3696
|
+
const [name, taskId, reason] = this.args;
|
|
3697
|
+
const options = this.opts();
|
|
3698
|
+
if (!ensureEnv(["DATABASE_URL"])) return;
|
|
3699
|
+
const rl = options.yes || options.force ? null : readline.createInterface({
|
|
3700
|
+
input: process$1.stdin,
|
|
3701
|
+
output: process$1.stdout
|
|
3702
|
+
});
|
|
3703
|
+
try {
|
|
3704
|
+
if (rl) {
|
|
3705
|
+
if ((await rl.question(`Type the task id (${taskId}) to confirm retry: `)).trim() !== taskId) {
|
|
3706
|
+
console.error("Confirmation failed. Task id did not match.");
|
|
3707
|
+
process$1.exitCode = 1;
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
if ((await rl.question("Type RETRY to requeue this failed task: ")).trim() !== "RETRY") {
|
|
3711
|
+
console.error("Confirmation failed. Aborting retry.");
|
|
3712
|
+
process$1.exitCode = 1;
|
|
3713
|
+
return;
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
const updatedTask = await (deps.retryTask ?? retryTask)(name, taskId, reason);
|
|
3717
|
+
if (!updatedTask) {
|
|
3718
|
+
console.error(`Task ${taskId} not found for agent ${name}.`);
|
|
3719
|
+
process$1.exitCode = 1;
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
if (updatedTask.status !== "pending") {
|
|
3723
|
+
console.error(`Unexpected result: task ${taskId} status is ${updatedTask.status} after retry.`);
|
|
3724
|
+
process$1.exitCode = 1;
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const attempt = resolveTaskAttempt(updatedTask.attempt_count);
|
|
3728
|
+
console.log(`Requeued task ${taskId} for agent ${name} as attempt ${attempt}, next run at ${updatedTask.queued_at.toISOString()}.${reason ? ` Reason: ${reason}` : ""}`);
|
|
3729
|
+
} catch (error) {
|
|
3730
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3731
|
+
process$1.exitCode = 1;
|
|
3732
|
+
} finally {
|
|
3733
|
+
rl?.close();
|
|
3734
|
+
}
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
//#endregion
|
|
3739
|
+
//#region src/cli/commands/skill-sh.ts
|
|
3740
|
+
const EMBEDDED_SKILL_MARKDOWN = "---\nname: pantheon-agents\ndescription: \"Pantheon Agents CLI Usage\"\n---\n\n> Pantheon is the project name of an internal VM environment management project. Do not guess any usage from the name.\n\n\n## Pantheon Agents CLI Usage\n\n### Concepts\n\n- agent: Each agent has a role (like `developer`) and several skills to use.\n- environment: The context of tools, codes or anything else for agents to run tasks. You don't need to know the details.\n- tasks: tasks for each agent to run, each task will create a new environment.\n\n#### Task structure\n\n- agent: The agent to executing the task\n- task (text): The task content\n- attempt: Attempt number (starts from 1 and increases on retries)\n- parent_task_id: The parent task id\n- status: `pending`, `running`, `completed`, `failed`, `canceled`\n- queued_at\n- started_at\n- ended_at\n- finished_at\n- canceled_at\n- cancel_reason\n- output: The output of the completed task.\n- error: The last known error message (failed task error is kept across retries).\n\n#### Other internal implementation details you don't need to know or use\n\n- project\n- branches\n\n### CLI Commands\n\n**You can only use the CLI commands listed below.**\n\n#### Setup a base environment for an agent:\n\n```shell\n# Use default setup script at first time configuration\npantheon-agents config 'Kris' \\\n --role 'developer' \\\n --skills pantheon-issue-resolve,pantheon-solution-design \\\n --max-retry-attempts 3 \\\n --retry-backoff-seconds 30\n\n# Add some custom prompts\npantheon-agents config 'Kris' \\\n --role 'developer' \\\n --skills pantheon-issue-resolve,pantheon-solution-design \\\n 'Update tools to the latest version.'\n```\n\n#### Update agent's environment\n\n##### Configure agent environment by prompt\n\n```shell\npantheon-agents config 'Kris' 'Install some tools'\n```\n\n##### Configure agent to a task result environment\n\npantheon-agents config 'Kris' --task-id 42\n\n#### Enqueue tasks\n\n```shell\npantheon-agents add-task 'Kris' 'Some awesome task'\n ```\n\nWhen adding tasks, cli will output the task id. You can add tasks with `--parent-task-id` to create a task hierarchy.\n\n```shell\npantheon-agents add-task 'Kris' 'Some awesome task' --parent-task-id 42\n```\n\n\n#### List tasks\n\n```shell\npantheon-agents show-tasks --json 'Kris'\n```\n\n\n#### Show single task info\n\n```shell\npantheon-agents get-task 'Kris' 42\n```\n\nReturns task info using this task structure (without internal project/branch fields):\n\n```json\n{\n \"agent\": \"Kris\",\n \"task\": \"Some awesome task\",\n \"attempt\": 1,\n \"parent_task_id\": \"41\",\n \"status\": \"completed\",\n \"queued_at\": \"2026-02-12T00:00:00.000Z\",\n \"started_at\": \"2026-02-12T00:00:05.000Z\",\n \"ended_at\": \"2026-02-12T00:05:00.000Z\",\n \"finished_at\": \"2026-02-12T00:05:00.000Z\",\n \"canceled_at\": null,\n \"cancel_reason\": null,\n \"output\": \"Task output\",\n \"error\": null\n}\n```\n\n#### Cancel task\n\n```shell\npantheon-agents cancel-task 'Kris' 42 'Reason'\n```\n\n#### Retry failed task\n\n```shell\n# Manual retry (immediate requeue and increases attempt)\npantheon-agents retry-task 'Kris' 42 'Reason'\n```\n\n#### Print embedded skill markdown\n\n```shell\npantheon-agents skill.sh\n```\n";
|
|
3741
|
+
function createSkillShCommand(version) {
|
|
3742
|
+
return createCommand("skill.sh").version(version).description("Print embedded pantheon-agents skill markdown").action(() => {
|
|
3743
|
+
process$1.stdout.write(EMBEDDED_SKILL_MARKDOWN);
|
|
3744
|
+
});
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
//#endregion
|
|
3748
|
+
//#region src/cli/commands/gen-migration-sql.ts
|
|
3749
|
+
const introducedInPattern = /^--\s*introduced_in:\s*([0-9]+\.[0-9]+\.[0-9]+)\s*$/m;
|
|
3750
|
+
function parseProvider(value) {
|
|
3751
|
+
if (value === "tidb" || value === "db9") return value;
|
|
3752
|
+
throw new InvalidArgumentError("provider must be one of: tidb, db9");
|
|
3753
|
+
}
|
|
3754
|
+
function parseSemver(value) {
|
|
3755
|
+
const matched = value.trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
3756
|
+
if (!matched) return null;
|
|
3757
|
+
return [
|
|
3758
|
+
Number(matched[1]),
|
|
3759
|
+
Number(matched[2]),
|
|
3760
|
+
Number(matched[3])
|
|
3761
|
+
];
|
|
3762
|
+
}
|
|
3763
|
+
function parseSemverOption(value) {
|
|
3764
|
+
if (parseSemver(value) == null) throw new InvalidArgumentError("from must be a semver like 0.3.0");
|
|
3765
|
+
return value;
|
|
3766
|
+
}
|
|
3767
|
+
function compareSemver(a, b) {
|
|
3768
|
+
const pa = parseSemver(a);
|
|
3769
|
+
const pb = parseSemver(b);
|
|
3770
|
+
if (!pa || !pb) throw new Error(`Invalid semver comparison: '${a}' vs '${b}'`);
|
|
3771
|
+
for (let i = 0; i < 3; i++) {
|
|
3772
|
+
if (pa[i] > pb[i]) return 1;
|
|
3773
|
+
if (pa[i] < pb[i]) return -1;
|
|
3774
|
+
}
|
|
3775
|
+
return 0;
|
|
3776
|
+
}
|
|
3777
|
+
function resolveMigrationDir(provider, cwd) {
|
|
3778
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
3779
|
+
const candidates = [
|
|
3780
|
+
path.resolve(cwd, "src/db/migrations", provider),
|
|
3781
|
+
path.resolve(cwd, "packages/agents/src/db/migrations", provider),
|
|
3782
|
+
path.resolve(moduleDir, "db/migrations", provider),
|
|
3783
|
+
path.resolve(moduleDir, "../db/migrations", provider),
|
|
3784
|
+
path.resolve(moduleDir, "../../db/migrations", provider)
|
|
3785
|
+
];
|
|
3786
|
+
for (const candidate of candidates) if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
|
|
3787
|
+
throw new Error(`Migration directory not found for provider '${provider}'. Checked: ${candidates.join(", ")}`);
|
|
3788
|
+
}
|
|
3789
|
+
function loadProviderMigrations(provider, options = {}) {
|
|
3790
|
+
const migrationDir = resolveMigrationDir(provider, options.cwd ?? process$1.cwd());
|
|
3791
|
+
return fs.readdirSync(migrationDir).filter((name) => name.endsWith(".sql")).sort((a, b) => a.localeCompare(b)).map((fileName) => {
|
|
3792
|
+
const filePath = path.join(migrationDir, fileName);
|
|
3793
|
+
const sql = fs.readFileSync(filePath, "utf8");
|
|
3794
|
+
const introduced = sql.match(introducedInPattern)?.[1];
|
|
3795
|
+
if (!introduced) throw new Error(`Migration file '${fileName}' missing required header '-- introduced_in: x.y.z'.`);
|
|
3796
|
+
return {
|
|
3797
|
+
fileName,
|
|
3798
|
+
introducedIn: introduced,
|
|
3799
|
+
sql
|
|
3800
|
+
};
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
function getMigrationSql(provider, fromVersion, options = {}) {
|
|
3804
|
+
if (!parseSemver(fromVersion)) throw new Error(`Invalid from version '${fromVersion}'. Expected x.y.z`);
|
|
3805
|
+
const migrations = loadProviderMigrations(provider, options).filter((migration) => compareSemver(migration.introducedIn, fromVersion) > 0);
|
|
3806
|
+
if (migrations.length === 0) return `-- No migrations for provider '${provider}' since ${fromVersion}.\n`;
|
|
3807
|
+
return migrations.map((migration) => `-- migration: ${migration.fileName}\n${migration.sql.trimEnd()}\n`).join("\n");
|
|
3808
|
+
}
|
|
3809
|
+
function createGenMigrationSqlCommand(version, deps = {}) {
|
|
3810
|
+
return createCommand("gen-migration-sql").version(version).description("Print SQL migrations since a given package version").requiredOption("--provider <provider>", "Database provider: tidb or db9.", parseProvider).requiredOption("--from <version>", "Print migrations introduced after this version.", parseSemverOption).action(async function() {
|
|
3811
|
+
const options = this.opts();
|
|
3812
|
+
const output = (deps.getMigrationSql ?? getMigrationSql)(options.provider, options.from);
|
|
3813
|
+
process$1.stdout.write(output);
|
|
3814
|
+
});
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3385
3817
|
//#endregion
|
|
3386
3818
|
//#region ../agent-stream-parser/src/utils.ts
|
|
3387
3819
|
function isRecord(value) {
|
|
@@ -5068,6 +5500,11 @@ async function resolveWatchTargets(options) {
|
|
|
5068
5500
|
warnings
|
|
5069
5501
|
};
|
|
5070
5502
|
}
|
|
5503
|
+
function createWatchDb() {
|
|
5504
|
+
const databaseUrl = process.env.DATABASE_URL?.trim();
|
|
5505
|
+
if (!databaseUrl) throw new Error("DATABASE_URL environment variable is not set.");
|
|
5506
|
+
return resolveTaskListProviderKind(databaseUrl) === "db9" ? createDb9Db() : createTidbDb();
|
|
5507
|
+
}
|
|
5071
5508
|
const ANSI = {
|
|
5072
5509
|
reset: "\x1B[0m",
|
|
5073
5510
|
bold: "\x1B[1m",
|
|
@@ -5153,7 +5590,7 @@ function createWatchCommand(version) {
|
|
|
5153
5590
|
console.error("Use either --tasks or --agents, not both.");
|
|
5154
5591
|
process.exit(1);
|
|
5155
5592
|
}
|
|
5156
|
-
const db =
|
|
5593
|
+
const db = createWatchDb();
|
|
5157
5594
|
try {
|
|
5158
5595
|
const { targets, warnings } = await resolveWatchTargets({
|
|
5159
5596
|
db,
|
|
@@ -5802,7 +6239,7 @@ function createWatchStreamCommand(version) {
|
|
|
5802
6239
|
|
|
5803
6240
|
//#endregion
|
|
5804
6241
|
//#region src/cli/index.ts
|
|
5805
|
-
const program = new Command().name("pantheon-agents").description("Pantheon agents CLI").version(version).showHelpAfterError().showSuggestionAfterError().addHelpCommand().addCommand(createAddTaskCommand(version)).addCommand(createConfigAgentCommand(version)).addCommand(createDeleteTaskCommand(version)).addCommand(createCancelTaskCommand(version)).addCommand(createGetTaskCommand(version)).addCommand(createRunAgentCommand(version)).addCommand(createShowConfigCommand(version)).addCommand(createShowTasksCommand(version)).addCommand(createWatchCommand(version)).addCommand(createWatchStreamCommand(version));
|
|
6242
|
+
const program = new Command().name("pantheon-agents").description("Pantheon agents CLI").version(version).showHelpAfterError().showSuggestionAfterError().addHelpCommand().addCommand(createAddTaskCommand(version)).addCommand(createConfigAgentCommand(version)).addCommand(createDeleteTaskCommand(version)).addCommand(createCancelTaskCommand(version)).addCommand(createGetTaskCommand(version)).addCommand(createRunAgentCommand(version)).addCommand(createRetryTaskCommand(version)).addCommand(createSkillShCommand(version)).addCommand(createGenMigrationSqlCommand(version)).addCommand(createShowConfigCommand(version)).addCommand(createShowTasksCommand(version)).addCommand(createWatchCommand(version)).addCommand(createWatchStreamCommand(version));
|
|
5806
6243
|
async function main() {
|
|
5807
6244
|
if (process$1.argv.length <= 2) {
|
|
5808
6245
|
program.outputHelp();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pantheon.ai/agents",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pantheon-agents": "dist/index.js"
|
|
7
7
|
},
|
|
@@ -12,20 +12,22 @@
|
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "rolldown -c rolldown.config.ts && chmod +x dist/index.js",
|
|
15
|
+
"build": "rolldown -c rolldown.config.ts && mkdir -p dist/db && cp -R src/db/migrations dist/db/ && chmod +x dist/index.js",
|
|
16
16
|
"typecheck": "tsc --noEmit --skipLibCheck --allowImportingTsExtensions --lib ESNext,DOM,DOM.Iterable",
|
|
17
17
|
"test": "bun test",
|
|
18
18
|
"test:coverage": "bun test --coverage",
|
|
19
19
|
"dev:db:start": "tiup playground --without-monitor --tag pantheon-agents",
|
|
20
|
-
"dev:db:gen": "kysely-codegen "
|
|
20
|
+
"dev:db:gen": "kysely-codegen --no-domains"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@types/pg": "^8.18.0",
|
|
23
24
|
"@types/shell-quote": "^1.7.5",
|
|
24
25
|
"commander": "^14.0.3",
|
|
25
26
|
"dotenv": "^17.2.4",
|
|
26
27
|
"expand-tilde": "^2.0.2",
|
|
27
28
|
"kysely": "^0.28.11",
|
|
28
29
|
"mysql2": "^3.16.3",
|
|
30
|
+
"pg": "^8.19.0",
|
|
29
31
|
"pino": "^10.3.0",
|
|
30
32
|
"pino-pretty": "^13.1.3",
|
|
31
33
|
"pino-roll": "^4.0.0",
|