pi-factory-gate 1.0.0
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/AGENTS.md +78 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/package.json +51 -0
- package/src/config.ts +32 -0
- package/src/helpers.ts +114 -0
- package/src/index.ts +45 -0
- package/src/tools/agents.ts +119 -0
- package/src/tools/environments.ts +161 -0
- package/src/tools/events.ts +104 -0
- package/src/tools/jobs.ts +330 -0
- package/src/tools/workers.ts +62 -0
- package/src/tools/workflows.ts +130 -0
- package/src/types.ts +120 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Factory Gate — Agent Usage Guide
|
|
2
|
+
|
|
3
|
+
> You are an AI agent. Use factory-gate tools to interact with the wrok.in AI Factory orchestrator.
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
factory_status() # Check factory health
|
|
9
|
+
factory_list_agents() # See available agents
|
|
10
|
+
factory_create_job(mode="single", agent="code-reviewer", task="Review the diff")
|
|
11
|
+
factory_stream_job(id="<job-id>") # Wait for completion
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Job Modes
|
|
15
|
+
|
|
16
|
+
- **single** — one agent, one task: `factory_create_job(mode="single", agent="analyzer", task="Analyze the codebase")`
|
|
17
|
+
- **chain** — sequential agents: `factory_create_job(mode="chain", steps='[{"agent":"planner","task":"Plan"},{"agent":"builder","task":"Build"}]')`
|
|
18
|
+
- **parallel** — concurrent agents: `factory_create_job(mode="parallel", tasks='[{"agent":"linter","task":"Lint"},{"agent":"tester","task":"Test"}]')`
|
|
19
|
+
|
|
20
|
+
## Environments
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
factory_list_environments() # List registered environments
|
|
24
|
+
factory_create_environment(name="app", repo="https://github.com/user/app.git")
|
|
25
|
+
factory_create_job(mode="single", agent="builder", task="Build", environment="app")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Skip environments with direct repo dispatch:
|
|
29
|
+
```bash
|
|
30
|
+
factory_create_job(mode="single", agent="analyzer", task="Analyze", repo="https://github.com/user/repo.git", branch="main")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Workflows & Monitoring
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
factory_list_workflows() # List workflow templates
|
|
37
|
+
factory_run_workflow(template="ci-review") # Execute a workflow
|
|
38
|
+
factory_list_workers() # Worker health
|
|
39
|
+
factory_get_events() # Recent system events
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Job Lifecycle
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
factory_create_job(...)
|
|
46
|
+
│
|
|
47
|
+
▼
|
|
48
|
+
factory_get_job(id="...") ← check status anytime
|
|
49
|
+
│
|
|
50
|
+
▼
|
|
51
|
+
factory_stream_job(id="...") ← wait for completion
|
|
52
|
+
│
|
|
53
|
+
▼
|
|
54
|
+
[result] ← final state: completed | failed | partial
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Troubleshooting
|
|
58
|
+
|
|
59
|
+
| Problem | Solution |
|
|
60
|
+
|---------|----------|
|
|
61
|
+
| `factory_status()` fails | Check that the orchestrator is running. Verify `.factoryrc.yml` has the correct `orchestratorUrl`. |
|
|
62
|
+
| Job stays "pending" | All workers are busy or offline. Check `factory_list_workers()`. |
|
|
63
|
+
| "Environment not found" | Run `factory_list_environments()` to see what's available, or create one with `factory_create_environment()`. |
|
|
64
|
+
| 409 Conflict on job create | An exclusive agent is already working on the same branch. Wait for it to complete or use `force: true`. |
|
|
65
|
+
| 503 "Job creation disabled" | Jobs are toggled off in the factory dashboard. Toggle "Jobs On" to re-enable. |
|
|
66
|
+
| "Agent not found" | Run `factory_list_agents()` to see available agents. Check the environment context. |
|
|
67
|
+
|
|
68
|
+
## Config Reference
|
|
69
|
+
|
|
70
|
+
`.factoryrc.yml`:
|
|
71
|
+
|
|
72
|
+
| Key | Default | Description |
|
|
73
|
+
|-----|---------|-------------|
|
|
74
|
+
| `orchestratorUrl` | `http://localhost:3001` | Factory orchestrator URL |
|
|
75
|
+
| `defaultEnvironment` | (none) | Default environment for jobs |
|
|
76
|
+
| `defaultLimit` | `20` | Items per list operation |
|
|
77
|
+
| `requestTimeout` | `15000` | API timeout in ms |
|
|
78
|
+
| `maxLogLines` | `500` | Max output lines for streaming |
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nandal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# pi-factory-gate
|
|
2
|
+
|
|
3
|
+
AI Factory orchestration gateway for pi agents — dispatch jobs, manage environments, list agents/workers/workflows, and stream results from a [wrok.in](https://github.com/nandal/wrok.in) orchestrator.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:pi-factory-gate
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
Add a `.factoryrc.yml` to your repo root:
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
orchestratorUrl: http://localhost:3001
|
|
17
|
+
defaultEnvironment: default
|
|
18
|
+
defaultLimit: 20
|
|
19
|
+
requestTimeout: 15000
|
|
20
|
+
maxLogLines: 500
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Key | Default | Description |
|
|
24
|
+
|-----|---------|-------------|
|
|
25
|
+
| `orchestratorUrl` | `http://localhost:3001` | Base URL of the wrok.in orchestrator |
|
|
26
|
+
| `defaultEnvironment` | (none) | Default environment for job dispatch |
|
|
27
|
+
| `defaultLimit` | `20` | Default page size for list operations |
|
|
28
|
+
| `requestTimeout` | `15000` | API call timeout in ms |
|
|
29
|
+
| `maxLogLines` | `500` | Max lines for streamed job output |
|
|
30
|
+
|
|
31
|
+
## Tools
|
|
32
|
+
|
|
33
|
+
### Agents
|
|
34
|
+
|
|
35
|
+
| Tool | Description |
|
|
36
|
+
|------|-------------|
|
|
37
|
+
| `factory_list_agents` | List all agents, optionally filtered by environment |
|
|
38
|
+
| `factory_get_agent` | Get agent detail including inheritance and resolved config |
|
|
39
|
+
|
|
40
|
+
### Jobs
|
|
41
|
+
|
|
42
|
+
| Tool | Description |
|
|
43
|
+
|------|-------------|
|
|
44
|
+
| `factory_create_job` | Dispatch a job (single, chain, or parallel mode) |
|
|
45
|
+
| `factory_get_job` | Get job status and results |
|
|
46
|
+
| `factory_list_jobs` | List all jobs in the factory |
|
|
47
|
+
| `factory_stream_job` | Poll for completion, returns final state |
|
|
48
|
+
|
|
49
|
+
### Environments
|
|
50
|
+
|
|
51
|
+
| Tool | Description |
|
|
52
|
+
|------|-------------|
|
|
53
|
+
| `factory_list_environments` | List all registered environments |
|
|
54
|
+
| `factory_get_environment` | Get environment detail |
|
|
55
|
+
| `factory_create_environment` | Register a new environment (clone a repo) |
|
|
56
|
+
|
|
57
|
+
### Workers
|
|
58
|
+
|
|
59
|
+
| Tool | Description |
|
|
60
|
+
|------|-------------|
|
|
61
|
+
| `factory_list_workers` | List all workers with health/status |
|
|
62
|
+
|
|
63
|
+
### Workflows
|
|
64
|
+
|
|
65
|
+
| Tool | Description |
|
|
66
|
+
|------|-------------|
|
|
67
|
+
| `factory_list_workflows` | List workflow templates (DB + disk discovery) |
|
|
68
|
+
| `factory_run_workflow` | Execute a workflow template |
|
|
69
|
+
|
|
70
|
+
### Events & Status
|
|
71
|
+
|
|
72
|
+
| Tool | Description |
|
|
73
|
+
|------|-------------|
|
|
74
|
+
| `factory_get_events` | Get recent system events |
|
|
75
|
+
| `factory_status` | Health check — is the orchestrator reachable? |
|
|
76
|
+
|
|
77
|
+
## About wrok.in
|
|
78
|
+
|
|
79
|
+
This gate works with the [wrok.in](https://github.com/nandal/wrok.in) AI Factory — a personal AI workflow orchestrator that manages pi workers across Docker containers with multi-repo environments, agent inheritance, DB-backed agents, and workflow scheduling.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-factory-gate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI Factory gateway for pi agents — dispatch jobs, manage environments, list agents/workers/workflows, stream results from a wrok.in orchestrator.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi-extension",
|
|
8
|
+
"ai-factory",
|
|
9
|
+
"orchestrator",
|
|
10
|
+
"agent-management",
|
|
11
|
+
"job-dispatch",
|
|
12
|
+
"workflow"
|
|
13
|
+
],
|
|
14
|
+
"author": "nandal <nandal@users.noreply.github.com>",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/nandal/pi-ext",
|
|
18
|
+
"directory": "factory-gate"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/nandal/pi-ext/tree/main/factory-gate",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/nandal/pi-ext/issues"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"main": "./src/index.ts",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src/",
|
|
31
|
+
"README.md",
|
|
32
|
+
"AGENTS.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
37
|
+
"typebox": "*"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": [
|
|
41
|
+
"./src/index.ts"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"vitest": "^2.1.9"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { FactoryConfig } from "./types";
|
|
4
|
+
import { DEFAULT_CONFIG } from "./types";
|
|
5
|
+
|
|
6
|
+
export function loadConfig(cwd: string): FactoryConfig {
|
|
7
|
+
const configPath = path.join(cwd, ".factoryrc.yml");
|
|
8
|
+
if (!fs.existsSync(configPath)) return { ...DEFAULT_CONFIG };
|
|
9
|
+
try {
|
|
10
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
11
|
+
const result: Record<string, unknown> = {};
|
|
12
|
+
for (const line of content.split("\n")) {
|
|
13
|
+
const m = line.match(/^\s*(\w[\w.]*):\s*(.+)$/);
|
|
14
|
+
if (m) {
|
|
15
|
+
let val = m[2].trim();
|
|
16
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
17
|
+
val = val.slice(1, -1);
|
|
18
|
+
}
|
|
19
|
+
result[m[1]] = val;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
orchestratorUrl: (result["orchestratorUrl"] as string) || DEFAULT_CONFIG.orchestratorUrl,
|
|
24
|
+
defaultEnvironment: (result["defaultEnvironment"] as string) || DEFAULT_CONFIG.defaultEnvironment,
|
|
25
|
+
defaultLimit: parseInt(result["defaultLimit"] as string) || DEFAULT_CONFIG.defaultLimit,
|
|
26
|
+
requestTimeout: parseInt(result["requestTimeout"] as string) || DEFAULT_CONFIG.requestTimeout,
|
|
27
|
+
maxLogLines: parseInt(result["maxLogLines"] as string) || DEFAULT_CONFIG.maxLogLines,
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
return { ...DEFAULT_CONFIG };
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { FactoryConfig } from "./types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Call the wrok.in orchestrator API.
|
|
5
|
+
* Handles JSON parsing, error extraction, and timeouts.
|
|
6
|
+
*/
|
|
7
|
+
export async function factoryApi<T = unknown>(
|
|
8
|
+
config: FactoryConfig,
|
|
9
|
+
endpoint: string,
|
|
10
|
+
method: "GET" | "POST" | "DELETE" = "GET",
|
|
11
|
+
body?: unknown,
|
|
12
|
+
): Promise<{ ok: boolean; data?: T; error?: string; status: number }> {
|
|
13
|
+
const url = `${config.orchestratorUrl.replace(/\/$/, "")}${endpoint}`;
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timeout = setTimeout(() => controller.abort(), config.requestTimeout);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const opts: RequestInit = {
|
|
19
|
+
method,
|
|
20
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
};
|
|
23
|
+
if (body !== undefined && method !== "GET") {
|
|
24
|
+
opts.body = JSON.stringify(body);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const res = await fetch(url, opts);
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
|
|
30
|
+
const contentType = res.headers.get("content-type") || "";
|
|
31
|
+
const isJson = contentType.includes("application/json");
|
|
32
|
+
|
|
33
|
+
if (!res.ok) {
|
|
34
|
+
const errBody = isJson ? await res.json().catch(() => null) : await res.text().catch(() => null);
|
|
35
|
+
const msg = errBody && typeof errBody === "object" && "error" in errBody
|
|
36
|
+
? String((errBody as Record<string, unknown>).error)
|
|
37
|
+
: String(errBody || res.statusText);
|
|
38
|
+
return { ok: false, error: msg, status: res.status };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isJson) {
|
|
42
|
+
const data = await res.json() as T;
|
|
43
|
+
return { ok: true, data, status: res.status };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const text = await res.text();
|
|
47
|
+
return { ok: true, data: text as unknown as T, status: res.status };
|
|
48
|
+
} catch (err: unknown) {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
51
|
+
if (msg.includes("abort") || msg.includes("timeout")) {
|
|
52
|
+
return { ok: false, error: `Request timed out after ${config.requestTimeout}ms`, status: 0 };
|
|
53
|
+
}
|
|
54
|
+
return { ok: false, error: msg, status: 0 };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format a Unix timestamp into a human-readable string.
|
|
60
|
+
*/
|
|
61
|
+
export function formatTimestamp(ts: number): string {
|
|
62
|
+
return new Date(ts).toISOString().slice(0, 19).replace("T", " ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Format duration in ms to a human-readable string.
|
|
67
|
+
*/
|
|
68
|
+
export function formatDuration(ms: number): string {
|
|
69
|
+
if (ms < 1000) return `${ms}ms`;
|
|
70
|
+
const s = Math.floor(ms / 1000);
|
|
71
|
+
if (s < 60) return `${s}s`;
|
|
72
|
+
const m = Math.floor(s / 60);
|
|
73
|
+
if (m < 60) return `${m}m ${s % 60}s`;
|
|
74
|
+
const h = Math.floor(m / 60);
|
|
75
|
+
return `${h}h ${m % 60}m`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Truncate a long string, keeping head and tail for readability.
|
|
80
|
+
*/
|
|
81
|
+
export function truncateOutput(text: string, maxLines: number): { text: string; truncated: boolean; totalLines: number } {
|
|
82
|
+
const lines = text.split("\n");
|
|
83
|
+
const totalLines = lines.length;
|
|
84
|
+
if (totalLines <= maxLines) return { text, truncated: false, totalLines };
|
|
85
|
+
|
|
86
|
+
const head = Math.ceil(maxLines * 0.6);
|
|
87
|
+
const tail = maxLines - head;
|
|
88
|
+
const truncated =
|
|
89
|
+
lines.slice(0, head).join("\n") +
|
|
90
|
+
`\n\n... [${totalLines - maxLines} lines truncated] ...\n\n` +
|
|
91
|
+
lines.slice(-tail).join("\n");
|
|
92
|
+
|
|
93
|
+
return { text: truncated, truncated: true, totalLines };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Status icon helper for job/worker display.
|
|
98
|
+
*/
|
|
99
|
+
export function statusIcon(status: string): string {
|
|
100
|
+
const s = status.toLowerCase();
|
|
101
|
+
if (s === "running" || s === "in_progress") return "🔄";
|
|
102
|
+
if (s === "pending" || s === "queued") return "⏳";
|
|
103
|
+
if (s === "completed" || s === "success") return "✅";
|
|
104
|
+
if (s === "failed" || s === "failure") return "❌";
|
|
105
|
+
if (s === "partial") return "⚠️";
|
|
106
|
+
if (s === "busy") return "🔵";
|
|
107
|
+
if (s === "idle") return "🟢";
|
|
108
|
+
if (s === "offline") return "🔴";
|
|
109
|
+
if (s === "ready") return "✅";
|
|
110
|
+
if (s === "cloning") return "⏳";
|
|
111
|
+
if (s === "empty") return "⚪";
|
|
112
|
+
if (s === "error") return "❌";
|
|
113
|
+
return "⚪";
|
|
114
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-factory-gate — AI Factory Orchestration Gate
|
|
3
|
+
*
|
|
4
|
+
* Tools: factory_list_agents, factory_get_agent, factory_create_job,
|
|
5
|
+
* factory_get_job, factory_list_jobs, factory_stream_job,
|
|
6
|
+
* factory_list_environments, factory_get_environment, factory_create_environment,
|
|
7
|
+
* factory_list_workers, factory_list_workflows, factory_run_workflow,
|
|
8
|
+
* factory_get_events, factory_status
|
|
9
|
+
* Config: .factoryrc.yml
|
|
10
|
+
*/
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { listAgentsTool, getAgentTool } from "./tools/agents";
|
|
13
|
+
import { createJobTool, getJobTool, listJobsTool, streamJobTool } from "./tools/jobs";
|
|
14
|
+
import { listEnvironmentsTool, getEnvironmentTool, createEnvironmentTool } from "./tools/environments";
|
|
15
|
+
import { listWorkersTool } from "./tools/workers";
|
|
16
|
+
import { listWorkflowsTool, runWorkflowTool } from "./tools/workflows";
|
|
17
|
+
import { getEventsTool, statusTool } from "./tools/events";
|
|
18
|
+
|
|
19
|
+
export default function (pi: ExtensionAPI) {
|
|
20
|
+
// Agents
|
|
21
|
+
pi.registerTool(listAgentsTool);
|
|
22
|
+
pi.registerTool(getAgentTool);
|
|
23
|
+
|
|
24
|
+
// Jobs
|
|
25
|
+
pi.registerTool(createJobTool);
|
|
26
|
+
pi.registerTool(getJobTool);
|
|
27
|
+
pi.registerTool(listJobsTool);
|
|
28
|
+
pi.registerTool(streamJobTool);
|
|
29
|
+
|
|
30
|
+
// Environments
|
|
31
|
+
pi.registerTool(listEnvironmentsTool);
|
|
32
|
+
pi.registerTool(getEnvironmentTool);
|
|
33
|
+
pi.registerTool(createEnvironmentTool);
|
|
34
|
+
|
|
35
|
+
// Workers
|
|
36
|
+
pi.registerTool(listWorkersTool);
|
|
37
|
+
|
|
38
|
+
// Workflows
|
|
39
|
+
pi.registerTool(listWorkflowsTool);
|
|
40
|
+
pi.registerTool(runWorkflowTool);
|
|
41
|
+
|
|
42
|
+
// Events & Status
|
|
43
|
+
pi.registerTool(getEventsTool);
|
|
44
|
+
pi.registerTool(statusTool);
|
|
45
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { loadConfig } from "../config";
|
|
4
|
+
import { factoryApi } from "../helpers";
|
|
5
|
+
import type { AgentInfo, AgentDetail } from "../types";
|
|
6
|
+
|
|
7
|
+
// ─── List Agents ───────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export const listAgentsTool = {
|
|
10
|
+
name: "factory_list_agents" as const,
|
|
11
|
+
label: "List Factory Agents",
|
|
12
|
+
description:
|
|
13
|
+
"List all available AI agents in the wrok.in factory. Optionally filter by environment.",
|
|
14
|
+
parameters: Type.Object({
|
|
15
|
+
environment: Type.Optional(
|
|
16
|
+
Type.String({ description: "Filter agents by environment name" }),
|
|
17
|
+
),
|
|
18
|
+
}),
|
|
19
|
+
async execute(_id: string, params: any, _s: any, _u: any, ctx: ExtensionContext) {
|
|
20
|
+
const config = loadConfig(ctx.cwd);
|
|
21
|
+
const qs = params.environment ? `?environment=${encodeURIComponent(params.environment)}` : "";
|
|
22
|
+
const r = await factoryApi<{ agents: AgentInfo[]; environment: string }>(
|
|
23
|
+
config,
|
|
24
|
+
`/agents${qs}`,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!r.ok || !r.data) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: `❌ Failed to list agents: ${r.error || "unknown"}` }],
|
|
30
|
+
isError: true,
|
|
31
|
+
details: {},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { agents } = r.data;
|
|
36
|
+
if (agents.length === 0) {
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text", text: "No agents registered." }],
|
|
39
|
+
details: { count: 0 },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const lines = [`🤖 Factory Agents (${agents.length})` + (params.environment ? ` in "${params.environment}"` : ""), ""];
|
|
44
|
+
for (const a of agents) {
|
|
45
|
+
const badge = a.enabled === false ? "🔴" : "🟢";
|
|
46
|
+
const tools = a.tools?.length ? ` [${a.tools.slice(0, 6).join(", ")}${a.tools.length > 6 ? ", ..." : ""}]` : "";
|
|
47
|
+
lines.push(` ${badge} ${a.name} source=${a.source}`);
|
|
48
|
+
lines.push(` ${a.description || "(no description)"}${tools}`);
|
|
49
|
+
if (a.model && a.model !== "auto") {
|
|
50
|
+
lines.push(` model: ${a.model}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
56
|
+
details: { count: agents.length, environment: params.environment || "default" },
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ─── Get Agent ─────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export const getAgentTool = {
|
|
64
|
+
name: "factory_get_agent" as const,
|
|
65
|
+
label: "Get Agent Detail",
|
|
66
|
+
description:
|
|
67
|
+
"Get detailed information about a specific factory agent, including its resolved configuration (inheritance, overrides).",
|
|
68
|
+
parameters: Type.Object({
|
|
69
|
+
name: Type.String({ description: "Agent name (e.g., 'code-reviewer', 'build-runner')" }),
|
|
70
|
+
environment: Type.Optional(
|
|
71
|
+
Type.String({ description: "Environment context for agent resolution" }),
|
|
72
|
+
),
|
|
73
|
+
}),
|
|
74
|
+
async execute(_id: string, params: any, _s: any, _u: any, ctx: ExtensionContext) {
|
|
75
|
+
const config = loadConfig(ctx.cwd);
|
|
76
|
+
const qs = params.environment ? `?environment=${encodeURIComponent(params.environment)}` : "";
|
|
77
|
+
const r = await factoryApi<AgentDetail>(
|
|
78
|
+
config,
|
|
79
|
+
`/agents/${encodeURIComponent(params.name)}${qs}`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!r.ok) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: `❌ Agent "${params.name}" not found: ${r.error || "unknown"}` }],
|
|
85
|
+
isError: true,
|
|
86
|
+
details: {},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const a = r.data!;
|
|
91
|
+
const lines = [
|
|
92
|
+
`🤖 ${a.name}`,
|
|
93
|
+
` Description: ${a.description || "(none)"}`,
|
|
94
|
+
` Source: ${a.source}`,
|
|
95
|
+
` Resolved: ${a.resolvedFrom || "(direct)"}`,
|
|
96
|
+
` Override: ${a.isOverride ? "yes" : "no"}`,
|
|
97
|
+
` Enabled: ${a.enabled !== false ? "yes" : "no"}`,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
if (a.extends) lines.push(` Extends: ${a.extends}`);
|
|
101
|
+
if (a.model && a.model !== "auto") lines.push(` Model: ${a.model}`);
|
|
102
|
+
if (a.tools?.length) {
|
|
103
|
+
lines.push(` Tools (${a.tools.length}):`);
|
|
104
|
+
for (const t of a.tools) lines.push(` - ${t}`);
|
|
105
|
+
}
|
|
106
|
+
if (a.systemPrompt) {
|
|
107
|
+
const preview = a.systemPrompt.length > 300
|
|
108
|
+
? a.systemPrompt.slice(0, 300) + "..."
|
|
109
|
+
: a.systemPrompt;
|
|
110
|
+
lines.push(` System Prompt:`);
|
|
111
|
+
lines.push(` ${preview}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
116
|
+
details: { name: a.name, source: a.source, resolvedFrom: a.resolvedFrom },
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|