@agent-play/cli 3.1.0 → 3.2.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/README.md +13 -0
- package/dist/README.md +91 -0
- package/dist/cli.js +449 -15
- package/package.json +8 -4
- package/templates/agent-starter/langchain/.env.example +6 -0
- package/templates/agent-starter/langchain/README.md +24 -0
- package/templates/agent-starter/langchain/package.json +25 -0
- package/templates/agent-starter/langchain/src/bare-server.ts +12 -0
- package/templates/agent-starter/langchain/src/builtins/definitions.ts +66 -0
- package/templates/agent-starter/langchain/src/builtins/toolkits/starter-tools.ts +84 -0
- package/templates/agent-starter/langchain/src/express-server.ts +27 -0
- package/templates/agent-starter/langchain/src/index.ts +6 -0
- package/templates/agent-starter/langchain/src/load-env.ts +5 -0
- package/templates/agent-starter/langchain/src/register/register-builtins.ts +62 -0
- package/templates/agent-starter/langchain/src/register-agents.ts +1 -0
- package/templates/agent-starter/langchain/src/tool-handlers/assist-brainstorm.ts +8 -0
- package/templates/agent-starter/langchain/src/tool-handlers/assist-calculate-coefficient.ts +35 -0
- package/templates/agent-starter/langchain/src/tool-handlers/assist-collect-scene-details.ts +32 -0
- package/templates/agent-starter/langchain/src/tool-handlers/chat-tool.ts +6 -0
- package/templates/agent-starter/langchain/src/tool-handlers/execute-tool-capability.ts +12 -0
- package/templates/agent-starter/langchain/src/tool-handlers/tool-capability-registry.ts +49 -0
- package/templates/agent-starter/langchain/tsconfig.json +13 -0
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ For Redis-direct checks (ops/CI), use **`node-tools`** script **`scripts/validat
|
|
|
52
52
|
| **`delete-main-node`** | — | **DELETE /api/nodes** — confirm by typing main node id; cascades. |
|
|
53
53
|
| **`validate-main-node`** | — | **POST /api/nodes/validate** for main node id. |
|
|
54
54
|
| **`validate-agent-node`** | — | **`--all`** or **`--agent-node-ids id1,id2,...`** — validate agent node ids. |
|
|
55
|
+
| **`initialize`** | `init` | Interactive scaffold for a starter agent codebase, optional node bootstrap, asks for agent count (1-2), and writes env-variable node ids into `.env` when bootstrapped. |
|
|
55
56
|
| **`clear-node-credentials`** | — | Removes **`~/.agent-play/credentials.json`**. |
|
|
56
57
|
|
|
57
58
|
## Genesis and main node
|
|
@@ -68,6 +69,7 @@ npx agent-play validate-main-node
|
|
|
68
69
|
npx agent-play inspect-node
|
|
69
70
|
npx agent-play create-agent-node
|
|
70
71
|
npx agent-play validate-agent-node --all
|
|
72
|
+
npx agent-play initialize
|
|
71
73
|
npx agent-play list-agent-nodes
|
|
72
74
|
npx agent-play delete-agent-node
|
|
73
75
|
npx agent-play delete-agent-node <agent-uuid>
|
|
@@ -76,3 +78,14 @@ npx agent-play clear-node-credentials
|
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
For SDK usage after bootstrap, use **`RemotePlayWorld`** and register players with **`mainNodeId`** and **`agentId`** from the CLI output.
|
|
81
|
+
|
|
82
|
+
## Initialize quick start
|
|
83
|
+
|
|
84
|
+
- `npx agent-play initialize` (or `npx agent-play init`) scaffolds starter files.
|
|
85
|
+
- The flow asks whether to create nodes now and how many agent nodes to provision (max `2`).
|
|
86
|
+
- If bootstrap is selected, it writes:
|
|
87
|
+
- `AGENT_PLAY_MAIN_NODE_ID`
|
|
88
|
+
- `AGENT_PLAY_AGENT_NODE_ID_1`
|
|
89
|
+
- `AGENT_PLAY_AGENT_NODE_ID_2` (when requested)
|
|
90
|
+
into generated `.env`.
|
|
91
|
+
- Generated code references these env vars directly (no hardcoded ids).
|
package/dist/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @agent-play/cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for **Agent Play**: create a **main developer node** (platform signup), add **agent nodes**, **validate** those identities against the server, and manage agent registrations. The server must use a **Redis**-backed agent repository (`REDIS_URL` on the server).
|
|
4
|
+
|
|
5
|
+
Authentication uses **`x-node-id`** and **`x-node-passw`** on every request **except** main-node creation, which sends hashed passphrase material in the JSON body to **`POST /api/nodes**.
|
|
6
|
+
|
|
7
|
+
## Documentation
|
|
8
|
+
|
|
9
|
+
- **[Repository](https://github.com/wilforlan/agent-play)**
|
|
10
|
+
- **[CLI guide](https://github.com/wilforlan/agent-play/blob/main/docs/cli.md)** — full **Node setup** and **Node validation** sections
|
|
11
|
+
- **[API reference](https://wilforlan.github.io/agent-play/)** — TypeDoc
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @agent-play/cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Binary name: **`agent-play`**.
|
|
20
|
+
|
|
21
|
+
Default server URL: **`http://127.0.0.1:3000`**, or override with **`AGENT_PLAY_SERVER_URL`**.
|
|
22
|
+
|
|
23
|
+
Root key for derivation: **`--root-file`**, **`AGENT_PLAY_ROOT_FILE_PATH`**, or a **`.root`** file under **`~/.agent-play/`** or the current working directory (must match the server’s `.root`).
|
|
24
|
+
|
|
25
|
+
## Node setup
|
|
26
|
+
|
|
27
|
+
1. **Server:** Redis (`REDIS_URL`) and a deployed web UI/API the CLI can reach (`AGENT_PLAY_SERVER_URL`).
|
|
28
|
+
2. **Local `.root`:** Must match the server genesis root key (see resolution order above).
|
|
29
|
+
3. **`create-main-node`** (`bootstrap-node`): prompts for server URL, generates a passphrase, registers **`POST /api/nodes`**, writes **`~/.agent-play/credentials.json`** with **`serverUrl`**, **`nodeId`**, and the human passphrase.
|
|
30
|
+
4. **`create-agent-node`**: derives an agent node under your main node, **`POST /api/nodes/agent-node`**, appends to **`credentials.json` → `agentNodes`**.
|
|
31
|
+
5. **`inspect-node`**, **`list-agent-nodes`**, **`delete-*`**, **`clear-node-credentials`**: inspect or tear down registrations; see **`docs/cli.md`** for the full table.
|
|
32
|
+
|
|
33
|
+
> **@deprecated** **`POST /api/agents`** does not create agent **node** identity. Use **`POST /api/nodes/agent-node`**, then attach runtime data with **`world.addPlayer`**.
|
|
34
|
+
|
|
35
|
+
## Node validation
|
|
36
|
+
|
|
37
|
+
- **`validate-main-node`** — calls **`POST /api/nodes/validate`** for your main node id (uses **`credentials.json`** + **`.root`**).
|
|
38
|
+
- **`validate-agent-node --all`** — validates every id in **`credentials.json` → `agentNodes`** (includes **`mainNodeId`** in the validate body).
|
|
39
|
+
- **`validate-agent-node --agent-node-ids id1,id2`** — same for explicit ids.
|
|
40
|
+
|
|
41
|
+
For Redis-direct checks (ops/CI), use **`node-tools`** script **`scripts/validate-node-derivative.mjs`**; details in **`docs/cli.md`** and **`docs/notes/node-id-v1-migration.md`**.
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
| Command | Aliases | What it does |
|
|
46
|
+
|--------|---------|----------------|
|
|
47
|
+
| **`create-main-node`** | `bootstrap-node` | Sign up a **main** node: **`POST /api/nodes`** (no node headers), save **`~/.agent-play/credentials.json`**. Optional **`--root-file`**. |
|
|
48
|
+
| **`inspect-node`** | — | **GET /api/nodes** — genesis id, main node, **agent node ids** (`create-agent-node`), and **runtime** agent rows (SDK metadata) if present. |
|
|
49
|
+
| **`create-agent-node`** | `create` | **POST /api/nodes/agent-node** — new agent node under your main node. |
|
|
50
|
+
| **`list-agent-nodes`** | `list` | **GET /api/agents** — lists registered agents. |
|
|
51
|
+
| **`delete-agent-node`** | `delete`, `remove` | **DELETE /api/agents** — optional **`[agent-id]`**; if omitted, prompts. |
|
|
52
|
+
| **`delete-main-node`** | — | **DELETE /api/nodes** — confirm by typing main node id; cascades. |
|
|
53
|
+
| **`validate-main-node`** | — | **POST /api/nodes/validate** for main node id. |
|
|
54
|
+
| **`validate-agent-node`** | — | **`--all`** or **`--agent-node-ids id1,id2,...`** — validate agent node ids. |
|
|
55
|
+
| **`initialize`** | `init` | Interactive scaffold for a starter agent codebase, optional node bootstrap, asks for agent count (1-2), and writes env-variable node ids into `.env` when bootstrapped. |
|
|
56
|
+
| **`clear-node-credentials`** | — | Removes **`~/.agent-play/credentials.json`**. |
|
|
57
|
+
|
|
58
|
+
## Genesis and main node
|
|
59
|
+
|
|
60
|
+
Every **main node id** is derived from passphrase material and the platform **root key** (the **genesis** identity). **`inspect-node`** and **`create-main-node`** output should agree with **`.root`** when both sides use the same key.
|
|
61
|
+
|
|
62
|
+
Node kinds: **`root` → `main` → `agent`**. Root has no passphrase; main and agent persist hashed material server-side.
|
|
63
|
+
|
|
64
|
+
## Usage examples
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npx agent-play create-main-node
|
|
68
|
+
npx agent-play validate-main-node
|
|
69
|
+
npx agent-play inspect-node
|
|
70
|
+
npx agent-play create-agent-node
|
|
71
|
+
npx agent-play validate-agent-node --all
|
|
72
|
+
npx agent-play initialize
|
|
73
|
+
npx agent-play list-agent-nodes
|
|
74
|
+
npx agent-play delete-agent-node
|
|
75
|
+
npx agent-play delete-agent-node <agent-uuid>
|
|
76
|
+
npx agent-play delete-main-node
|
|
77
|
+
npx agent-play clear-node-credentials
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For SDK usage after bootstrap, use **`RemotePlayWorld`** and register players with **`mainNodeId`** and **`agentId`** from the CLI output.
|
|
81
|
+
|
|
82
|
+
## Initialize quick start
|
|
83
|
+
|
|
84
|
+
- `npx agent-play initialize` (or `npx agent-play init`) scaffolds starter files.
|
|
85
|
+
- The flow asks whether to create nodes now and how many agent nodes to provision (max `2`).
|
|
86
|
+
- If bootstrap is selected, it writes:
|
|
87
|
+
- `AGENT_PLAY_MAIN_NODE_ID`
|
|
88
|
+
- `AGENT_PLAY_AGENT_NODE_ID_1`
|
|
89
|
+
- `AGENT_PLAY_AGENT_NODE_ID_2` (when requested)
|
|
90
|
+
into generated `.env`.
|
|
91
|
+
- Generated code references these env vars directly (no hardcoded ids).
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { existsSync } from "fs";
|
|
5
|
-
import { mkdir, unlink, writeFile } from "fs/promises";
|
|
4
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5
|
+
import { mkdir as mkdir2, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { join, resolve } from "path";
|
|
7
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
8
8
|
import { createInterface } from "readline/promises";
|
|
9
9
|
import { stdin as input, stdout as output } from "process";
|
|
10
10
|
import {
|
|
@@ -15,6 +15,247 @@ import {
|
|
|
15
15
|
loadAgentPlayCredentialsFileFromPath,
|
|
16
16
|
loadRootKey
|
|
17
17
|
} from "@agent-play/node-tools";
|
|
18
|
+
|
|
19
|
+
// src/initialize.ts
|
|
20
|
+
import { existsSync } from "fs";
|
|
21
|
+
import { mkdir, readdir, readFile, writeFile } from "fs/promises";
|
|
22
|
+
import { dirname, join, resolve } from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
function resolveServerUrlForEnvironment(environment) {
|
|
25
|
+
if (environment === "test") {
|
|
26
|
+
return "https://test-agent-play.com";
|
|
27
|
+
}
|
|
28
|
+
if (environment === "production") {
|
|
29
|
+
return "https://agent-play.com";
|
|
30
|
+
}
|
|
31
|
+
return "http://127.0.0.1:3000";
|
|
32
|
+
}
|
|
33
|
+
var TEMPLATE_ROOT = fileURLToPath(
|
|
34
|
+
new URL("../templates/agent-starter/langchain", import.meta.url)
|
|
35
|
+
);
|
|
36
|
+
function parseInitializeArgs(argv) {
|
|
37
|
+
const out = {
|
|
38
|
+
template: "langchain",
|
|
39
|
+
yes: false,
|
|
40
|
+
force: false
|
|
41
|
+
};
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const token = argv[i];
|
|
44
|
+
if (token === "--dir" && typeof argv[i + 1] === "string") {
|
|
45
|
+
out.dir = argv[++i];
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (token === "--name" && typeof argv[i + 1] === "string") {
|
|
49
|
+
out.name = argv[++i];
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (token === "--template" && typeof argv[i + 1] === "string") {
|
|
53
|
+
const template = argv[++i];
|
|
54
|
+
if (template !== "langchain") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
out.template = template;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (token === "--yes") {
|
|
61
|
+
out.yes = true;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (token === "--force") {
|
|
65
|
+
out.force = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (token === "--bootstrap-nodes") {
|
|
69
|
+
out.bootstrapNodes = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (token === "--agent-count" && typeof argv[i + 1] === "string") {
|
|
73
|
+
const raw = Number(argv[++i]);
|
|
74
|
+
if (raw !== 1 && raw !== 2) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
out.agentCount = raw;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (token === "--environment" && typeof argv[i + 1] === "string") {
|
|
81
|
+
const value = argv[++i].trim().toLowerCase();
|
|
82
|
+
if (value === "development" || value === "test" || value === "production") {
|
|
83
|
+
out.environment = value;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (token === "--server-type" && typeof argv[i + 1] === "string") {
|
|
89
|
+
const value = argv[++i].trim().toLowerCase();
|
|
90
|
+
if (value === "bare" || value === "express") {
|
|
91
|
+
out.serverType = value;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function normalizeProjectName(raw) {
|
|
101
|
+
if (typeof raw !== "string") {
|
|
102
|
+
return "agent-play-agent-starter";
|
|
103
|
+
}
|
|
104
|
+
const normalized = raw.trim().toLowerCase().replace(/[^a-z0-9-_]+/g, "-");
|
|
105
|
+
if (normalized.length === 0) {
|
|
106
|
+
return "agent-play-agent-starter";
|
|
107
|
+
}
|
|
108
|
+
return normalized;
|
|
109
|
+
}
|
|
110
|
+
async function listTemplateFiles(dir) {
|
|
111
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
112
|
+
const files = [];
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const full = join(dir, entry.name);
|
|
115
|
+
if (entry.isDirectory()) {
|
|
116
|
+
const nested = await listTemplateFiles(full);
|
|
117
|
+
files.push(...nested.map((path) => join(entry.name, path)));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
files.push(entry.name);
|
|
121
|
+
}
|
|
122
|
+
return files;
|
|
123
|
+
}
|
|
124
|
+
async function ensureSafeTarget(options) {
|
|
125
|
+
const targetExists = existsSync(options.targetDir);
|
|
126
|
+
if (!targetExists) {
|
|
127
|
+
await mkdir(options.targetDir, { recursive: true });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (options.force) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const existing = await readdir(options.targetDir);
|
|
134
|
+
if (existing.length > 0) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Target directory is not empty: ${options.targetDir}. Re-run with --force to overwrite scaffold-managed files.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function patchEnvContent(options) {
|
|
141
|
+
const lines = options.envContent.split(/\r?\n/);
|
|
142
|
+
const updates = /* @__PURE__ */ new Map([
|
|
143
|
+
["AGENT_PLAY_WEB_UI_URL", options.serverUrl],
|
|
144
|
+
["AGENT_PLAY_MAIN_NODE_ID", options.mainNodeId],
|
|
145
|
+
["AGENT_PLAY_AGENT_NODE_ID_1", options.agentNodeIds[0] ?? ""],
|
|
146
|
+
["AGENT_PLAY_AGENT_NODE_ID_2", options.agentNodeIds[1] ?? ""]
|
|
147
|
+
]);
|
|
148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
149
|
+
const next = lines.map((line) => {
|
|
150
|
+
const eqIndex = line.indexOf("=");
|
|
151
|
+
if (eqIndex <= 0) {
|
|
152
|
+
return line;
|
|
153
|
+
}
|
|
154
|
+
const key = line.slice(0, eqIndex);
|
|
155
|
+
const update = updates.get(key);
|
|
156
|
+
if (update === void 0) {
|
|
157
|
+
return line;
|
|
158
|
+
}
|
|
159
|
+
seen.add(key);
|
|
160
|
+
return `${key}=${update}`;
|
|
161
|
+
});
|
|
162
|
+
for (const [key, value] of updates.entries()) {
|
|
163
|
+
if (!seen.has(key)) {
|
|
164
|
+
next.push(`${key}=${value}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return next.join("\n");
|
|
168
|
+
}
|
|
169
|
+
async function renderTemplate(options) {
|
|
170
|
+
const files = await listTemplateFiles(TEMPLATE_ROOT);
|
|
171
|
+
for (const relativePath of files) {
|
|
172
|
+
const templatePath = join(TEMPLATE_ROOT, relativePath);
|
|
173
|
+
const targetPath = join(options.targetDir, relativePath);
|
|
174
|
+
if (!options.force && existsSync(targetPath)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
178
|
+
const source = await readFile(templatePath, "utf8");
|
|
179
|
+
const serverModule = options.serverType === "express" ? "./express-server.js" : "./bare-server.js";
|
|
180
|
+
const content = source.replaceAll("__PROJECT_NAME__", options.projectName).replaceAll("__AGENT_NAME__", "Starter Agent").replaceAll("__SERVER_MODULE__", serverModule);
|
|
181
|
+
await writeFile(targetPath, content, "utf8");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function cmdInitialize(options) {
|
|
185
|
+
const parsed = parseInitializeArgs(options.argv);
|
|
186
|
+
if (parsed === null) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
"Usage: agent-play initialize [--dir <path>] [--name <project-name>] [--template langchain] [--environment <development|test|production>] [--server-type <bare|express>] [--yes] [--force] [--bootstrap-nodes] [--agent-count <1|2>]"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const targetDir = resolve(parsed.dir ?? process.cwd());
|
|
192
|
+
const projectName = normalizeProjectName(parsed.name ?? basenameFromPath(targetDir));
|
|
193
|
+
const environment = parsed.environment ?? (parsed.yes ? "development" : await options.promptApi.askEnvironment());
|
|
194
|
+
const serverType = parsed.serverType ?? (parsed.yes ? "bare" : await options.promptApi.askServerType());
|
|
195
|
+
const serverUrl = resolveServerUrlForEnvironment(environment);
|
|
196
|
+
const agentCount = parsed.agentCount ?? (parsed.yes ? 1 : await options.promptApi.askAgentCount());
|
|
197
|
+
const bootstrapNodes = parsed.bootstrapNodes ?? (parsed.yes ? false : await options.promptApi.askBootstrapNodes());
|
|
198
|
+
await ensureSafeTarget({ targetDir, force: parsed.force });
|
|
199
|
+
await renderTemplate({
|
|
200
|
+
targetDir,
|
|
201
|
+
projectName,
|
|
202
|
+
serverType,
|
|
203
|
+
force: parsed.force
|
|
204
|
+
});
|
|
205
|
+
const envExamplePath = join(targetDir, ".env.example");
|
|
206
|
+
const envPath = join(targetDir, ".env");
|
|
207
|
+
if (!existsSync(envPath) && existsSync(envExamplePath)) {
|
|
208
|
+
await writeFile(envPath, await readFile(envExamplePath, "utf8"), "utf8");
|
|
209
|
+
}
|
|
210
|
+
if (bootstrapNodes) {
|
|
211
|
+
const bootstrapped = await options.runtimeApi.bootstrapNodeIds({
|
|
212
|
+
agentCount,
|
|
213
|
+
serverUrl
|
|
214
|
+
});
|
|
215
|
+
const envContent = existsSync(envPath) ? await readFile(envPath, "utf8") : "";
|
|
216
|
+
const nextEnv = patchEnvContent({
|
|
217
|
+
envContent,
|
|
218
|
+
serverUrl,
|
|
219
|
+
mainNodeId: bootstrapped.mainNodeId,
|
|
220
|
+
agentNodeIds: bootstrapped.agentNodeIds
|
|
221
|
+
});
|
|
222
|
+
await writeFile(envPath, nextEnv, "utf8");
|
|
223
|
+
console.log(`Bootstrapped main node id: ${bootstrapped.mainNodeId}`);
|
|
224
|
+
for (const [index, nodeId] of bootstrapped.agentNodeIds.entries()) {
|
|
225
|
+
console.log(`Bootstrapped agent node ${String(index + 1)} id: ${nodeId}`);
|
|
226
|
+
}
|
|
227
|
+
} else if (existsSync(envPath)) {
|
|
228
|
+
const envContent = await readFile(envPath, "utf8");
|
|
229
|
+
const nextEnv = patchEnvContent({
|
|
230
|
+
envContent,
|
|
231
|
+
serverUrl,
|
|
232
|
+
mainNodeId: "",
|
|
233
|
+
agentNodeIds: []
|
|
234
|
+
});
|
|
235
|
+
await writeFile(envPath, nextEnv, "utf8");
|
|
236
|
+
}
|
|
237
|
+
console.log("");
|
|
238
|
+
console.log("Agent starter scaffold created.");
|
|
239
|
+
console.log(`Location: ${targetDir}`);
|
|
240
|
+
console.log("Next steps:");
|
|
241
|
+
console.log(` cd "${targetDir}"`);
|
|
242
|
+
console.log(" npm install");
|
|
243
|
+
if (!bootstrapNodes) {
|
|
244
|
+
console.log(" npx agent-play create-main-node");
|
|
245
|
+
console.log(" npx agent-play create-agent-node");
|
|
246
|
+
if (agentCount === 2) {
|
|
247
|
+
console.log(" npx agent-play create-agent-node");
|
|
248
|
+
}
|
|
249
|
+
console.log(" copy node ids into .env");
|
|
250
|
+
}
|
|
251
|
+
console.log(" npm run dev");
|
|
252
|
+
}
|
|
253
|
+
function basenameFromPath(pathValue) {
|
|
254
|
+
const split = pathValue.split(/[\\/]/).filter((part) => part.length > 0);
|
|
255
|
+
return split[split.length - 1] ?? "agent-play-agent-starter";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/cli.ts
|
|
18
259
|
function nodeAuthHeaders(cred) {
|
|
19
260
|
return {
|
|
20
261
|
"x-node-id": cred.nodeId,
|
|
@@ -36,15 +277,15 @@ function parseAgentRows(agentsRaw) {
|
|
|
36
277
|
return agents;
|
|
37
278
|
}
|
|
38
279
|
function credentialsPath() {
|
|
39
|
-
return
|
|
280
|
+
return join2(homedir(), ".agent-play", "credentials.json");
|
|
40
281
|
}
|
|
41
282
|
async function loadCredentials() {
|
|
42
283
|
return loadAgentPlayCredentialsFileFromPath(credentialsPath());
|
|
43
284
|
}
|
|
44
285
|
async function saveCredentials(c) {
|
|
45
|
-
const dir =
|
|
46
|
-
await
|
|
47
|
-
await
|
|
286
|
+
const dir = join2(homedir(), ".agent-play");
|
|
287
|
+
await mkdir2(dir, { recursive: true });
|
|
288
|
+
await writeFile2(
|
|
48
289
|
credentialsPath(),
|
|
49
290
|
JSON.stringify(c, null, 2),
|
|
50
291
|
"utf8"
|
|
@@ -101,6 +342,102 @@ async function promptBootstrapEnvironment(rl) {
|
|
|
101
342
|
);
|
|
102
343
|
}
|
|
103
344
|
}
|
|
345
|
+
function parseInitializeBootstrapAnswer(raw) {
|
|
346
|
+
const trimmed = raw.trim().toLowerCase();
|
|
347
|
+
if (trimmed === "" || trimmed === "y" || trimmed === "yes") {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
if (trimmed === "n" || trimmed === "no") {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
async function promptInitializeBootstrapChoice(rl) {
|
|
356
|
+
for (; ; ) {
|
|
357
|
+
const answer = await rl.question("Create node identities now? [Y/n]: ");
|
|
358
|
+
const parsed = parseInitializeBootstrapAnswer(answer);
|
|
359
|
+
if (parsed !== null) {
|
|
360
|
+
return parsed;
|
|
361
|
+
}
|
|
362
|
+
console.log("Please answer yes or no.");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function parseInitializeAgentCount(raw) {
|
|
366
|
+
const trimmed = raw.trim();
|
|
367
|
+
if (trimmed === "" || trimmed === "1") {
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
if (trimmed === "2") {
|
|
371
|
+
return 2;
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
async function promptInitializeAgentCount(rl) {
|
|
376
|
+
for (; ; ) {
|
|
377
|
+
const answer = await rl.question("How many agents do you want to deploy? (1-2) [1]: ");
|
|
378
|
+
const parsed = parseInitializeAgentCount(answer);
|
|
379
|
+
if (parsed !== null) {
|
|
380
|
+
return parsed;
|
|
381
|
+
}
|
|
382
|
+
console.log("Invalid value. Enter 1 or 2.");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function parseInitializeEnvironmentAnswer(raw) {
|
|
386
|
+
const trimmed = raw.trim().toLowerCase();
|
|
387
|
+
if (trimmed === "" || trimmed === "1" || trimmed === "development" || trimmed === "dev") {
|
|
388
|
+
return "development";
|
|
389
|
+
}
|
|
390
|
+
if (trimmed === "2" || trimmed === "test") {
|
|
391
|
+
return "test";
|
|
392
|
+
}
|
|
393
|
+
if (trimmed === "3" || trimmed === "production" || trimmed === "prod") {
|
|
394
|
+
return "production";
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
async function promptInitializeEnvironment(rl) {
|
|
399
|
+
const lines = [
|
|
400
|
+
"Choose environment for initialization:",
|
|
401
|
+
" 1) development \u2192 http://127.0.0.1:3000",
|
|
402
|
+
" 2) test \u2192 https://test-agent-play.com",
|
|
403
|
+
" 3) production \u2192 https://agent-play.com",
|
|
404
|
+
"Enter 1-3, or development/test/production [1]: "
|
|
405
|
+
].join("\n");
|
|
406
|
+
for (; ; ) {
|
|
407
|
+
const answer = await rl.question(lines);
|
|
408
|
+
const parsed = parseInitializeEnvironmentAnswer(answer);
|
|
409
|
+
if (parsed !== null) {
|
|
410
|
+
return parsed;
|
|
411
|
+
}
|
|
412
|
+
console.log("Invalid choice. Enter 1, 2, 3, development, test, or production.");
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function parseInitializeServerTypeAnswer(raw) {
|
|
416
|
+
const trimmed = raw.trim().toLowerCase();
|
|
417
|
+
if (trimmed === "" || trimmed === "1" || trimmed === "bare") {
|
|
418
|
+
return "bare";
|
|
419
|
+
}
|
|
420
|
+
if (trimmed === "2" || trimmed === "express") {
|
|
421
|
+
return "express";
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
async function promptInitializeServerType(rl) {
|
|
426
|
+
const lines = [
|
|
427
|
+
"Choose server runtime:",
|
|
428
|
+
" 1) bare \u2192 simple process entrypoint (minimal)",
|
|
429
|
+
" 2) express \u2192 deployable HTTP server with /health endpoint",
|
|
430
|
+
"Enter 1-2, or bare/express [1]: "
|
|
431
|
+
].join("\n");
|
|
432
|
+
for (; ; ) {
|
|
433
|
+
const answer = await rl.question(lines);
|
|
434
|
+
const parsed = parseInitializeServerTypeAnswer(answer);
|
|
435
|
+
if (parsed !== null) {
|
|
436
|
+
return parsed;
|
|
437
|
+
}
|
|
438
|
+
console.log("Invalid choice. Enter 1, 2, bare, or express.");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
104
441
|
function parseBootstrapNodeArgs(argv) {
|
|
105
442
|
const out = {};
|
|
106
443
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -137,18 +474,18 @@ function parseValidateAgentNodeArgs(argv) {
|
|
|
137
474
|
}
|
|
138
475
|
function resolveAgentPlayRootPath(options) {
|
|
139
476
|
if (typeof options.rootFilePath === "string" && options.rootFilePath.trim().length > 0) {
|
|
140
|
-
return
|
|
477
|
+
return resolve2(options.rootFilePath.trim());
|
|
141
478
|
}
|
|
142
479
|
const fromEnv = process.env.AGENT_PLAY_ROOT_FILE_PATH;
|
|
143
480
|
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
144
|
-
return
|
|
481
|
+
return resolve2(fromEnv.trim());
|
|
145
482
|
}
|
|
146
|
-
const homeRoot =
|
|
147
|
-
if (
|
|
483
|
+
const homeRoot = join2(homedir(), ".agent-play", ".root");
|
|
484
|
+
if (existsSync2(homeRoot)) {
|
|
148
485
|
return homeRoot;
|
|
149
486
|
}
|
|
150
|
-
const cwdRoot =
|
|
151
|
-
if (
|
|
487
|
+
const cwdRoot = resolve2(process.cwd(), ".root");
|
|
488
|
+
if (existsSync2(cwdRoot)) {
|
|
152
489
|
return cwdRoot;
|
|
153
490
|
}
|
|
154
491
|
throw new Error(
|
|
@@ -198,8 +535,8 @@ async function cmdBootstrapNode(argv) {
|
|
|
198
535
|
console.log(`Using server: ${serverUrl}`);
|
|
199
536
|
const rootPath = resolveAgentPlayRootPath(opts);
|
|
200
537
|
const rootKey = loadRootKey(rootPath);
|
|
201
|
-
const dir =
|
|
202
|
-
await
|
|
538
|
+
const dir = join2(homedir(), ".agent-play");
|
|
539
|
+
await mkdir2(dir, { recursive: true });
|
|
203
540
|
const generatedPassw = generateNodePassw();
|
|
204
541
|
const hashedPassw = hashNodePassword(generatedPassw);
|
|
205
542
|
const credential = createNodeCredentialFromPassw({ passw: hashedPassw, rootKey });
|
|
@@ -320,6 +657,67 @@ async function cmdCreateAgentNode() {
|
|
|
320
657
|
console.log("Keep this material safe. Losing it means losing access.");
|
|
321
658
|
console.log("");
|
|
322
659
|
}
|
|
660
|
+
async function ensureMainCredentialsForInitialize(serverUrl) {
|
|
661
|
+
const existing = await loadCredentials();
|
|
662
|
+
if (existing !== null && existing.serverUrl.replace(/\/$/, "") === serverUrl.replace(/\/$/, "")) {
|
|
663
|
+
return existing;
|
|
664
|
+
}
|
|
665
|
+
const rootKey = loadRootKey(resolveAgentPlayRootPath({}));
|
|
666
|
+
const generatedPassw = generateNodePassw();
|
|
667
|
+
const hashedPassw = hashNodePassword(generatedPassw);
|
|
668
|
+
const credential = createNodeCredentialFromPassw({ passw: hashedPassw, rootKey });
|
|
669
|
+
await registerNodeOnServer(serverUrl, hashedPassw, credential.nodeId);
|
|
670
|
+
const created = {
|
|
671
|
+
serverUrl,
|
|
672
|
+
nodeId: credential.nodeId,
|
|
673
|
+
passw: generatedPassw
|
|
674
|
+
};
|
|
675
|
+
await saveCredentials(created);
|
|
676
|
+
return created;
|
|
677
|
+
}
|
|
678
|
+
async function createAgentNodeForInitialize(cred) {
|
|
679
|
+
const rootKey = loadRootKey(resolveAgentPlayRootPath({}));
|
|
680
|
+
const agentPassw = generateNodePassw();
|
|
681
|
+
const hashedAgentPassw = hashNodePassword(agentPassw);
|
|
682
|
+
const agentNodeId = deriveNodeIdFromPassword({
|
|
683
|
+
password: hashedAgentPassw,
|
|
684
|
+
rootKey
|
|
685
|
+
});
|
|
686
|
+
const res = await fetch(`${cred.serverUrl}/api/nodes/agent-node`, {
|
|
687
|
+
method: "POST",
|
|
688
|
+
headers: {
|
|
689
|
+
"content-type": "application/json",
|
|
690
|
+
...nodeAuthHeaders(cred)
|
|
691
|
+
},
|
|
692
|
+
body: JSON.stringify({
|
|
693
|
+
kind: "agent",
|
|
694
|
+
parentNodeId: cred.nodeId,
|
|
695
|
+
agentNodeId,
|
|
696
|
+
agentNodePassw: hashedAgentPassw
|
|
697
|
+
})
|
|
698
|
+
});
|
|
699
|
+
const text = await res.text();
|
|
700
|
+
if (!res.ok) {
|
|
701
|
+
throw new Error(`Create failed (${String(res.status)}): ${text}`);
|
|
702
|
+
}
|
|
703
|
+
const json = JSON.parse(text);
|
|
704
|
+
if (typeof json.agentId !== "string" || json.agentId !== agentNodeId) {
|
|
705
|
+
throw new Error("Invalid agent creation response.");
|
|
706
|
+
}
|
|
707
|
+
const nextAgentNodes = [
|
|
708
|
+
...(cred.agentNodes ?? []).filter((n) => n.nodeId !== agentNodeId),
|
|
709
|
+
{
|
|
710
|
+
nodeId: agentNodeId,
|
|
711
|
+
passw: agentPassw,
|
|
712
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
713
|
+
}
|
|
714
|
+
];
|
|
715
|
+
await saveCredentials({
|
|
716
|
+
...cred,
|
|
717
|
+
agentNodes: nextAgentNodes
|
|
718
|
+
});
|
|
719
|
+
return agentNodeId;
|
|
720
|
+
}
|
|
323
721
|
async function cmdInspectNode() {
|
|
324
722
|
const cred = await loadCredentials();
|
|
325
723
|
if (cred === null) {
|
|
@@ -642,6 +1040,37 @@ async function cmdValidateAgentNode(argv) {
|
|
|
642
1040
|
}
|
|
643
1041
|
console.log(`Validated ${String(dedupedIds.length)} agent node(s) successfully.`);
|
|
644
1042
|
}
|
|
1043
|
+
async function cmdInitialize2(argv) {
|
|
1044
|
+
const rl = createInterface({ input, output });
|
|
1045
|
+
try {
|
|
1046
|
+
await cmdInitialize({
|
|
1047
|
+
argv,
|
|
1048
|
+
promptApi: {
|
|
1049
|
+
askEnvironment: () => promptInitializeEnvironment(rl),
|
|
1050
|
+
askServerType: () => promptInitializeServerType(rl),
|
|
1051
|
+
askBootstrapNodes: () => promptInitializeBootstrapChoice(rl),
|
|
1052
|
+
askAgentCount: () => promptInitializeAgentCount(rl)
|
|
1053
|
+
},
|
|
1054
|
+
runtimeApi: {
|
|
1055
|
+
bootstrapNodeIds: async (options) => {
|
|
1056
|
+
const mainCred = await ensureMainCredentialsForInitialize(options.serverUrl);
|
|
1057
|
+
const agentNodeIds = [];
|
|
1058
|
+
for (let i = 0; i < options.agentCount; i++) {
|
|
1059
|
+
const refreshedCred = await loadCredentials() ?? mainCred;
|
|
1060
|
+
const nodeId = await createAgentNodeForInitialize(refreshedCred);
|
|
1061
|
+
agentNodeIds.push(nodeId);
|
|
1062
|
+
}
|
|
1063
|
+
return {
|
|
1064
|
+
mainNodeId: mainCred.nodeId,
|
|
1065
|
+
agentNodeIds
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
} finally {
|
|
1071
|
+
rl.close();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
645
1074
|
async function main() {
|
|
646
1075
|
const cmd = process.argv[2];
|
|
647
1076
|
if (cmd === "bootstrap-node" || cmd === "create-main-node") {
|
|
@@ -680,6 +1109,10 @@ async function main() {
|
|
|
680
1109
|
await cmdValidateAgentNode(process.argv.slice(3));
|
|
681
1110
|
return;
|
|
682
1111
|
}
|
|
1112
|
+
if (cmd === "initialize" || cmd === "init") {
|
|
1113
|
+
await cmdInitialize2(process.argv.slice(3));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
683
1116
|
console.error(
|
|
684
1117
|
[
|
|
685
1118
|
"Usage:",
|
|
@@ -692,6 +1125,7 @@ async function main() {
|
|
|
692
1125
|
" agent-play validate-main-node",
|
|
693
1126
|
" agent-play validate-agent-node --all",
|
|
694
1127
|
" agent-play validate-agent-node --agent-node-ids <id1,id2,...>",
|
|
1128
|
+
" agent-play initialize | init [--dir <path>] [--name <project-name>] [--template langchain] [--environment <development|test|production>] [--server-type <bare|express>] [--yes] [--force] [--bootstrap-nodes] [--agent-count <1|2>]",
|
|
695
1129
|
" agent-play clear-node-credentials"
|
|
696
1130
|
].join("\n")
|
|
697
1131
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-play/cli",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Command-line tool for Agent Play:
|
|
3
|
+
"version": "3.2.0",
|
|
4
|
+
"description": "Command-line tool for Agent Play: node configurations, agent starter kit, and agent registration against the web UI.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"templates",
|
|
12
13
|
"README.md"
|
|
13
14
|
],
|
|
14
15
|
"repository": {
|
|
@@ -20,7 +21,9 @@
|
|
|
20
21
|
"access": "public"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
23
|
-
"build": "tsup src/cli.ts --format esm --platform node --target node20 --out-dir dist --clean --external @agent-play/node-tools && node ../../scripts/copy-root-file.mjs cli"
|
|
24
|
+
"build": "tsup src/cli.ts --format esm --platform node --target node20 --out-dir dist --clean --external @agent-play/node-tools && node ../../scripts/copy-root-file.mjs cli && cp README.md dist/README.md",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"smoke:initialize": "node ./scripts/smoke-initialize.mjs"
|
|
24
27
|
},
|
|
25
28
|
"dependencies": {
|
|
26
29
|
"@agent-play/node-tools": "1.0.0"
|
|
@@ -28,6 +31,7 @@
|
|
|
28
31
|
"devDependencies": {
|
|
29
32
|
"@types/node": "^22.10.0",
|
|
30
33
|
"tsup": "^8.5.1",
|
|
31
|
-
"typescript": "~5.9.3"
|
|
34
|
+
"typescript": "~5.9.3",
|
|
35
|
+
"vitest": "^4.1.1"
|
|
32
36
|
}
|
|
33
37
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
Starter scaffold generated by `agent-play initialize`.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
1. Install dependencies:
|
|
8
|
+
- `npm install`
|
|
9
|
+
2. Prepare local env:
|
|
10
|
+
- `cp .env.example .env`
|
|
11
|
+
3. If you skipped bootstrap during initialize:
|
|
12
|
+
- `npx agent-play create-main-node`
|
|
13
|
+
- `npx agent-play create-agent-node` (once or twice)
|
|
14
|
+
- copy values from `~/.agent-play/credentials.json` into `.env`
|
|
15
|
+
4. Run:
|
|
16
|
+
- `npm run dev`
|
|
17
|
+
|
|
18
|
+
## Node identity env contract
|
|
19
|
+
|
|
20
|
+
- `AGENT_PLAY_MAIN_NODE_ID`: main developer node id.
|
|
21
|
+
- `AGENT_PLAY_AGENT_NODE_ID_1`: first agent node id.
|
|
22
|
+
- `AGENT_PLAY_AGENT_NODE_ID_2`: optional second agent node id.
|
|
23
|
+
|
|
24
|
+
The generated runtime uses env variables for node ids; no hardcoded identities are embedded.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -p tsconfig.json",
|
|
8
|
+
"dev": "tsx src/index.ts",
|
|
9
|
+
"start": "node dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@agent-play/sdk": "^3.2.1",
|
|
13
|
+
"@langchain/openai": "^1.3.0",
|
|
14
|
+
"dotenv": "^17.3.1",
|
|
15
|
+
"express": "^4.21.2",
|
|
16
|
+
"langchain": "^1.2.36",
|
|
17
|
+
"zod": "^3.24.2"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/express": "^4.17.21",
|
|
21
|
+
"@types/node": "^22.10.0",
|
|
22
|
+
"tsx": "^4.21.0",
|
|
23
|
+
"typescript": "~5.9.3"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { loadEnv } from "./load-env.js";
|
|
2
|
+
import { registerAgents } from "./register-agents.js";
|
|
3
|
+
|
|
4
|
+
export async function startServer(): Promise<void> {
|
|
5
|
+
loadEnv();
|
|
6
|
+
const { world, registeredAgentIds } = await registerAgents();
|
|
7
|
+
console.log(`Registered ${String(registeredAgentIds.length)} agent(s):`);
|
|
8
|
+
for (const id of registeredAgentIds) {
|
|
9
|
+
console.log(` - ${id}`);
|
|
10
|
+
}
|
|
11
|
+
await world.hold().for(30 * 60);
|
|
12
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ChatOpenAI } from "@langchain/openai";
|
|
2
|
+
import { createAgent } from "langchain";
|
|
3
|
+
import { calculatorTools, policeReportTools } from "./toolkits/starter-tools.js";
|
|
4
|
+
|
|
5
|
+
function randomAgentName(prefix: string): string {
|
|
6
|
+
return `${prefix}-${Math.random().toString(36).slice(2, 8)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function randomSystemPrompt(prefix: string, rolePrompt: string): string {
|
|
10
|
+
const nonce = Math.random().toString(36).slice(2, 10);
|
|
11
|
+
return `${rolePrompt} Session nonce: ${nonce}. Agent label: ${prefix}.`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function requiredEnv(name: string): string {
|
|
15
|
+
const value = process.env[name]?.trim();
|
|
16
|
+
if (value === undefined || value.length === 0) {
|
|
17
|
+
throw new Error(`Missing required env var: ${name}`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type StarterAgentDefinition = {
|
|
23
|
+
nodeId: string;
|
|
24
|
+
name: string;
|
|
25
|
+
type: "langchain";
|
|
26
|
+
agent: ReturnType<typeof createAgent>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function getStarterAgentDefinitions(agentCount: 1 | 2): StarterAgentDefinition[] {
|
|
30
|
+
const model = new ChatOpenAI({
|
|
31
|
+
apiKey: requiredEnv("OPENAI_API_KEY"),
|
|
32
|
+
model: process.env.OPENAI_MODEL?.trim() || "gpt-4.1",
|
|
33
|
+
});
|
|
34
|
+
const first = {
|
|
35
|
+
nodeId: requiredEnv("AGENT_PLAY_AGENT_NODE_ID_1"),
|
|
36
|
+
name: randomAgentName("CalculatorAgent"),
|
|
37
|
+
type: "langchain" as const,
|
|
38
|
+
agent: createAgent({
|
|
39
|
+
name: randomAgentName("lc-calculator-agent"),
|
|
40
|
+
model,
|
|
41
|
+
tools: [...calculatorTools],
|
|
42
|
+
systemPrompt: randomSystemPrompt(
|
|
43
|
+
"Calculator Agent",
|
|
44
|
+
"You are a rigorous calculator agent for production operations. Always validate the equation format, show the extracted coefficient and brief reasoning, and ask clarifying questions when equation syntax is ambiguous. Use chat_tool for conversational explanations and assist_calculate_coefficient for structured extraction."
|
|
45
|
+
),
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
if (agentCount === 1) {
|
|
49
|
+
return [first];
|
|
50
|
+
}
|
|
51
|
+
const second = {
|
|
52
|
+
nodeId: requiredEnv("AGENT_PLAY_AGENT_NODE_ID_2"),
|
|
53
|
+
name: randomAgentName("PoliceReportAgent"),
|
|
54
|
+
type: "langchain" as const,
|
|
55
|
+
agent: createAgent({
|
|
56
|
+
name: randomAgentName("lc-police-report-agent"),
|
|
57
|
+
model,
|
|
58
|
+
tools: [...policeReportTools],
|
|
59
|
+
systemPrompt: randomSystemPrompt(
|
|
60
|
+
"Police Report Agent",
|
|
61
|
+
"You are a police report intake agent. Collect factual, time-stamped incident details, separate witness statements from assumptions, and maintain neutral language suitable for official records. Use chat_tool for guided conversation and assist_collect_scene_details to gather structured scene data."
|
|
62
|
+
),
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
return [first, second];
|
|
66
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { tool } from "langchain";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
const calculatorChatTool = tool(
|
|
5
|
+
({ message }: { message: string }) => `calculator:${message}`,
|
|
6
|
+
{
|
|
7
|
+
name: "chat_tool",
|
|
8
|
+
description: "Production-style calculator assistant chat tool.",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
message: z.string(),
|
|
11
|
+
}),
|
|
12
|
+
}
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const assistCalculateCoefficient = tool(
|
|
16
|
+
({
|
|
17
|
+
equation,
|
|
18
|
+
variable,
|
|
19
|
+
}: {
|
|
20
|
+
equation: string;
|
|
21
|
+
variable: string;
|
|
22
|
+
}) => `coefficient:${equation}:${variable}`,
|
|
23
|
+
{
|
|
24
|
+
name: "assist_calculate_coefficient",
|
|
25
|
+
description:
|
|
26
|
+
"Extract the coefficient of a variable from a linear or polynomial equation string.",
|
|
27
|
+
schema: z.object({
|
|
28
|
+
equation: z.string(),
|
|
29
|
+
variable: z.string(),
|
|
30
|
+
}),
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const policeChatTool = tool(
|
|
35
|
+
({ message }: { message: string }) => `police:${message}`,
|
|
36
|
+
{
|
|
37
|
+
name: "chat_tool",
|
|
38
|
+
description: "Production-style police incident reporting chat tool.",
|
|
39
|
+
schema: z.object({
|
|
40
|
+
message: z.string(),
|
|
41
|
+
}),
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const assistCollectSceneDetails = tool(
|
|
46
|
+
({
|
|
47
|
+
location,
|
|
48
|
+
incidentType,
|
|
49
|
+
witnesses,
|
|
50
|
+
injuriesReported,
|
|
51
|
+
suspectDescription,
|
|
52
|
+
immediateRisk,
|
|
53
|
+
}: {
|
|
54
|
+
location: string;
|
|
55
|
+
incidentType: string;
|
|
56
|
+
witnesses?: string;
|
|
57
|
+
injuriesReported?: boolean;
|
|
58
|
+
suspectDescription?: string;
|
|
59
|
+
immediateRisk?: "low" | "medium" | "high";
|
|
60
|
+
}) =>
|
|
61
|
+
`scene:${location}:${incidentType}:${witnesses ?? ""}:${String(injuriesReported ?? false)}:${
|
|
62
|
+
suspectDescription ?? ""
|
|
63
|
+
}:${immediateRisk ?? "low"}`,
|
|
64
|
+
{
|
|
65
|
+
name: "assist_collect_scene_details",
|
|
66
|
+
description:
|
|
67
|
+
"Collect structured details from an incident scene report for police documentation.",
|
|
68
|
+
schema: z.object({
|
|
69
|
+
location: z.string(),
|
|
70
|
+
incidentType: z.string(),
|
|
71
|
+
witnesses: z.string().optional(),
|
|
72
|
+
injuriesReported: z.boolean().optional(),
|
|
73
|
+
suspectDescription: z.string().optional(),
|
|
74
|
+
immediateRisk: z.enum(["low", "medium", "high"]).optional(),
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export const calculatorTools = [
|
|
80
|
+
calculatorChatTool,
|
|
81
|
+
assistCalculateCoefficient,
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
export const policeReportTools = [policeChatTool, assistCollectSceneDetails] as const;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { loadEnv } from "./load-env.js";
|
|
3
|
+
import { registerAgents } from "./register-agents.js";
|
|
4
|
+
|
|
5
|
+
export async function startServer(): Promise<void> {
|
|
6
|
+
loadEnv();
|
|
7
|
+
const port = Number(process.env.PORT ?? "3100");
|
|
8
|
+
const host = process.env.HOST ?? "0.0.0.0";
|
|
9
|
+
|
|
10
|
+
const app = express();
|
|
11
|
+
app.use(express.json());
|
|
12
|
+
app.get("/health", (_req, res) => {
|
|
13
|
+
res.json({ ok: true });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const { world, registeredAgentIds } = await registerAgents();
|
|
17
|
+
console.log(`Registered ${String(registeredAgentIds.length)} agent(s):`);
|
|
18
|
+
for (const id of registeredAgentIds) {
|
|
19
|
+
console.log(` - ${id}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
app.listen(port, host, () => {
|
|
23
|
+
console.log(`[starter-express] listening on http://${host}:${String(port)}`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await world.hold().for(30 * 60);
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
langchainRegistration,
|
|
3
|
+
RemotePlayWorld,
|
|
4
|
+
type RegisteredPlayer,
|
|
5
|
+
} from "@agent-play/sdk";
|
|
6
|
+
import { getStarterAgentDefinitions } from "../builtins/definitions.js";
|
|
7
|
+
import { executeToolCapability } from "../tool-handlers/execute-tool-capability.js";
|
|
8
|
+
|
|
9
|
+
type RegisterResult = {
|
|
10
|
+
world: RemotePlayWorld;
|
|
11
|
+
registeredAgentIds: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function requiredEnv(name: string): string {
|
|
15
|
+
const value = process.env[name]?.trim();
|
|
16
|
+
if (value === undefined || value.length === 0) {
|
|
17
|
+
throw new Error(`Missing required env var: ${name}`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function registerBuiltinAgents(): Promise<RegisterResult> {
|
|
23
|
+
const mainNodeId = requiredEnv("AGENT_PLAY_MAIN_NODE_ID");
|
|
24
|
+
const world = new RemotePlayWorld({ logging: "on" });
|
|
25
|
+
const openAiApiKey = process.env.OPENAI_API_KEY?.trim();
|
|
26
|
+
if (openAiApiKey !== undefined && openAiApiKey.length > 0) {
|
|
27
|
+
world.initAudio({
|
|
28
|
+
openai: {
|
|
29
|
+
apiKey: openAiApiKey,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
await world.connect({ mainNodeId });
|
|
34
|
+
|
|
35
|
+
const hasSecondAgent =
|
|
36
|
+
(process.env.AGENT_PLAY_AGENT_NODE_ID_2?.trim() ?? "").length > 0;
|
|
37
|
+
const definitions = getStarterAgentDefinitions(hasSecondAgent ? 2 : 1);
|
|
38
|
+
const registeredAgentIds: string[] = [];
|
|
39
|
+
const chatAgentsByPlayerId = new Map<string, unknown>();
|
|
40
|
+
const registeredPlayers: RegisteredPlayer[] = [];
|
|
41
|
+
for (const def of definitions) {
|
|
42
|
+
const registered = await world.addAgent({
|
|
43
|
+
name: def.name,
|
|
44
|
+
type: def.type,
|
|
45
|
+
agent: langchainRegistration(def.agent),
|
|
46
|
+
nodeId: def.nodeId,
|
|
47
|
+
enableP2a: "on",
|
|
48
|
+
});
|
|
49
|
+
registeredAgentIds.push(registered.id);
|
|
50
|
+
registeredPlayers.push(registered);
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < registeredPlayers.length; i++) {
|
|
53
|
+
chatAgentsByPlayerId.set(registeredPlayers[i].id, definitions[i].agent);
|
|
54
|
+
}
|
|
55
|
+
world.subscribeIntercomCommands({
|
|
56
|
+
playerIds: registeredPlayers.map((player) => player.id),
|
|
57
|
+
executeTool: executeToolCapability,
|
|
58
|
+
chatAgentsByPlayerId: chatAgentsByPlayerId as Map<string, never>,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return { world, registeredAgentIds };
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerBuiltinAgents as registerAgents } from "./register/register-builtins.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function normalizeEquation(equation: string): string {
|
|
2
|
+
return equation.replace(/\s+/g, "");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function executeAssistCalculateCoefficient(args: {
|
|
6
|
+
equation?: string;
|
|
7
|
+
variable?: string;
|
|
8
|
+
}): Record<string, unknown> {
|
|
9
|
+
const equation = typeof args.equation === "string" ? args.equation : "";
|
|
10
|
+
const variable = typeof args.variable === "string" ? args.variable : "";
|
|
11
|
+
if (equation.length === 0 || variable.length === 0) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
error: "equation and variable are required.",
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const normalized = normalizeEquation(equation);
|
|
18
|
+
const regex = new RegExp(`([+-]?\\d*\\.?\\d*)${variable}`);
|
|
19
|
+
const match = normalized.match(regex);
|
|
20
|
+
if (match === null) {
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
error: `No coefficient found for variable ${variable}.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const raw = match[1];
|
|
27
|
+
const coefficient =
|
|
28
|
+
raw === "" || raw === "+" ? 1 : raw === "-" ? -1 : Number(raw);
|
|
29
|
+
return {
|
|
30
|
+
ok: Number.isFinite(coefficient),
|
|
31
|
+
variable,
|
|
32
|
+
coefficient,
|
|
33
|
+
equation,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function executeAssistCollectSceneDetails(args: {
|
|
2
|
+
location?: string;
|
|
3
|
+
incidentType?: string;
|
|
4
|
+
witnesses?: string;
|
|
5
|
+
injuriesReported?: boolean;
|
|
6
|
+
suspectDescription?: string;
|
|
7
|
+
immediateRisk?: "low" | "medium" | "high";
|
|
8
|
+
}): Record<string, unknown> {
|
|
9
|
+
const location = typeof args.location === "string" ? args.location : "";
|
|
10
|
+
const incidentType =
|
|
11
|
+
typeof args.incidentType === "string" ? args.incidentType : "";
|
|
12
|
+
if (location.length === 0 || incidentType.length === 0) {
|
|
13
|
+
return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: "location and incidentType are required.",
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
ok: true,
|
|
20
|
+
scene: {
|
|
21
|
+
location,
|
|
22
|
+
incidentType,
|
|
23
|
+
witnesses: typeof args.witnesses === "string" ? args.witnesses : "",
|
|
24
|
+
injuriesReported: args.injuriesReported === true,
|
|
25
|
+
suspectDescription:
|
|
26
|
+
typeof args.suspectDescription === "string"
|
|
27
|
+
? args.suspectDescription
|
|
28
|
+
: "",
|
|
29
|
+
immediateRisk: args.immediateRisk ?? "low",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { resolveToolCapabilityHandler } from "./tool-capability-registry.js";
|
|
2
|
+
|
|
3
|
+
export async function executeToolCapability(options: {
|
|
4
|
+
toolName: string;
|
|
5
|
+
args: Record<string, unknown>;
|
|
6
|
+
}): Promise<Record<string, unknown>> {
|
|
7
|
+
const handler = resolveToolCapabilityHandler(options.toolName);
|
|
8
|
+
if (handler === null) {
|
|
9
|
+
throw new Error(`unknown tool capability: ${options.toolName}`);
|
|
10
|
+
}
|
|
11
|
+
return await Promise.resolve(handler(options.args));
|
|
12
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { executeAssistCalculateCoefficient } from "./assist-calculate-coefficient.js";
|
|
2
|
+
import { executeAssistCollectSceneDetails } from "./assist-collect-scene-details.js";
|
|
3
|
+
import { executeChatTool } from "./chat-tool.js";
|
|
4
|
+
|
|
5
|
+
export type ToolCapabilityHandler = (
|
|
6
|
+
args: Record<string, unknown>
|
|
7
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
8
|
+
|
|
9
|
+
const registry = new Map<string, ToolCapabilityHandler>([
|
|
10
|
+
["chat_tool", (args) => executeChatTool({ message: String(args.message ?? "") })],
|
|
11
|
+
[
|
|
12
|
+
"assist_calculate_coefficient",
|
|
13
|
+
(args) =>
|
|
14
|
+
executeAssistCalculateCoefficient({
|
|
15
|
+
equation: String(args.equation ?? ""),
|
|
16
|
+
variable: String(args.variable ?? ""),
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
"assist_collect_scene_details",
|
|
21
|
+
(args) =>
|
|
22
|
+
executeAssistCollectSceneDetails({
|
|
23
|
+
location: String(args.location ?? ""),
|
|
24
|
+
incidentType: String(args.incidentType ?? ""),
|
|
25
|
+
witnesses:
|
|
26
|
+
typeof args.witnesses === "string" ? args.witnesses : undefined,
|
|
27
|
+
injuriesReported:
|
|
28
|
+
typeof args.injuriesReported === "boolean"
|
|
29
|
+
? args.injuriesReported
|
|
30
|
+
: undefined,
|
|
31
|
+
suspectDescription:
|
|
32
|
+
typeof args.suspectDescription === "string"
|
|
33
|
+
? args.suspectDescription
|
|
34
|
+
: undefined,
|
|
35
|
+
immediateRisk:
|
|
36
|
+
args.immediateRisk === "low" ||
|
|
37
|
+
args.immediateRisk === "medium" ||
|
|
38
|
+
args.immediateRisk === "high"
|
|
39
|
+
? args.immediateRisk
|
|
40
|
+
: undefined,
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export function resolveToolCapabilityHandler(
|
|
46
|
+
toolName: string
|
|
47
|
+
): ToolCapabilityHandler | null {
|
|
48
|
+
return registry.get(toolName) ?? null;
|
|
49
|
+
}
|