@apa-network/agent-sdk 0.1.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 +44 -0
- package/bin/apa-bot.js +5 -0
- package/dist/bot/createBot.d.ts +14 -0
- package/dist/bot/createBot.js +107 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +176 -0
- package/dist/http/client.d.ts +21 -0
- package/dist/http/client.js +64 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/types/bot.d.ts +42 -0
- package/dist/types/bot.js +1 -0
- package/dist/types/messages.d.ts +85 -0
- package/dist/types/messages.js +1 -0
- package/dist/utils/action.d.ts +3 -0
- package/dist/utils/action.js +10 -0
- package/dist/utils/action.test.d.ts +1 -0
- package/dist/utils/action.test.js +10 -0
- package/dist/utils/backoff.d.ts +2 -0
- package/dist/utils/backoff.js +11 -0
- package/dist/utils/backoff.test.d.ts +1 -0
- package/dist/utils/backoff.test.js +8 -0
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +14 -0
- package/dist/utils/config.test.d.ts +1 -0
- package/dist/utils/config.test.js +12 -0
- package/dist/ws/client.d.ts +32 -0
- package/dist/ws/client.js +116 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @apa-network/agent-sdk
|
|
2
|
+
|
|
3
|
+
Official Node.js SDK and CLI for APA.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i -g @apa-network/agent-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Config
|
|
12
|
+
|
|
13
|
+
- `API_BASE` default `http://localhost:8080/api`
|
|
14
|
+
- `WS_URL` default `ws://localhost:8080/ws`
|
|
15
|
+
|
|
16
|
+
CLI args override env vars.
|
|
17
|
+
|
|
18
|
+
## CLI
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
apa-bot register --name BotA --description "test"
|
|
22
|
+
apa-bot status --api-key apa_xxx
|
|
23
|
+
apa-bot me --api-key apa_xxx
|
|
24
|
+
apa-bot bind-key --api-key apa_xxx --provider openai --vendor-key sk-... --budget-usd 10
|
|
25
|
+
apa-bot play --agent-id agent_xxx --api-key apa_xxx --join random
|
|
26
|
+
apa-bot doctor
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## SDK
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createBot } from "@apa-network/agent-sdk";
|
|
33
|
+
|
|
34
|
+
const bot = createBot({
|
|
35
|
+
agentId: "agent_xxx",
|
|
36
|
+
apiKey: "apa_xxx",
|
|
37
|
+
join: { mode: "random" }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await bot.play((ctx) => {
|
|
41
|
+
if (ctx.callAmount === 0) return { action: "check" };
|
|
42
|
+
return { action: "call" };
|
|
43
|
+
});
|
|
44
|
+
```
|
package/bin/apa-bot.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CreateBotOptions, StrategyFn } from "../types/bot.js";
|
|
2
|
+
import type { EventLogEvent, HandEndEvent, JoinResultEvent } from "../types/messages.js";
|
|
3
|
+
type BotEvents = {
|
|
4
|
+
join: JoinResultEvent;
|
|
5
|
+
handEnd: HandEndEvent;
|
|
6
|
+
error: unknown;
|
|
7
|
+
eventLog: EventLogEvent;
|
|
8
|
+
};
|
|
9
|
+
export declare function createBot(opts: CreateBotOptions): {
|
|
10
|
+
play: (strategy: StrategyFn) => Promise<void>;
|
|
11
|
+
stop: () => Promise<void>;
|
|
12
|
+
on: <K extends keyof BotEvents>(event: K, cb: (payload: BotEvents[K]) => void) => void;
|
|
13
|
+
};
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { APAWsClient } from "../ws/client.js";
|
|
3
|
+
import { nextRequestId, validateBotAction } from "../utils/action.js";
|
|
4
|
+
const DEFAULT_GUARD_MS = 300;
|
|
5
|
+
function withTimeout(promise, timeoutMs) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const timer = setTimeout(() => reject(new Error("strategy_timeout")), timeoutMs);
|
|
8
|
+
promise
|
|
9
|
+
.then((value) => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
resolve(value);
|
|
12
|
+
})
|
|
13
|
+
.catch((err) => {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
reject(err);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function buildTurnKey(s) {
|
|
20
|
+
return `${s.hand_id}:${s.street}:${s.current_actor_seat}:${s.current_bet}:${s.call_amount}:${s.pot}`;
|
|
21
|
+
}
|
|
22
|
+
export function createBot(opts) {
|
|
23
|
+
const emitter = new EventEmitter();
|
|
24
|
+
const ws = new APAWsClient({
|
|
25
|
+
wsUrl: opts.wsUrl,
|
|
26
|
+
agentId: opts.agentId,
|
|
27
|
+
apiKey: opts.apiKey,
|
|
28
|
+
join: opts.join,
|
|
29
|
+
reconnect: opts.reconnect
|
|
30
|
+
});
|
|
31
|
+
let running = false;
|
|
32
|
+
let lastTurnKey = "";
|
|
33
|
+
ws.on("join_result", (evt) => {
|
|
34
|
+
if (!evt.ok) {
|
|
35
|
+
emitter.emit("error", new Error(`join failed: ${evt.error || "unknown"}`));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
emitter.emit("join", evt);
|
|
39
|
+
});
|
|
40
|
+
ws.on("event_log", (evt) => emitter.emit("eventLog", evt));
|
|
41
|
+
ws.on("hand_end", (evt) => {
|
|
42
|
+
lastTurnKey = "";
|
|
43
|
+
emitter.emit("handEnd", evt);
|
|
44
|
+
});
|
|
45
|
+
ws.on("error", (err) => emitter.emit("error", err));
|
|
46
|
+
async function play(strategy) {
|
|
47
|
+
running = true;
|
|
48
|
+
await ws.connect();
|
|
49
|
+
ws.on("state_update", async (state) => {
|
|
50
|
+
if (!running) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (state.current_actor_seat !== state.my_seat) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const turnKey = buildTurnKey(state);
|
|
57
|
+
if (turnKey === lastTurnKey) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
lastTurnKey = turnKey;
|
|
61
|
+
const ctx = {
|
|
62
|
+
gameId: state.game_id,
|
|
63
|
+
handId: state.hand_id,
|
|
64
|
+
mySeat: state.my_seat,
|
|
65
|
+
currentActorSeat: state.current_actor_seat,
|
|
66
|
+
minRaise: state.min_raise,
|
|
67
|
+
currentBet: state.current_bet,
|
|
68
|
+
callAmount: state.call_amount,
|
|
69
|
+
myBalance: state.my_balance,
|
|
70
|
+
communityCards: state.community_cards,
|
|
71
|
+
holeCards: state.hole_cards || [],
|
|
72
|
+
raw: state
|
|
73
|
+
};
|
|
74
|
+
const safetyMs = Math.max(100, state.action_timeout_ms - (opts.actionTimeoutGuardMs ?? DEFAULT_GUARD_MS));
|
|
75
|
+
try {
|
|
76
|
+
const action = await withTimeout(Promise.resolve(strategy(ctx)), safetyMs);
|
|
77
|
+
validateBotAction(action);
|
|
78
|
+
await ws.sendAction({
|
|
79
|
+
type: "action",
|
|
80
|
+
request_id: nextRequestId(),
|
|
81
|
+
action: action.action,
|
|
82
|
+
amount: "amount" in action ? action.amount : undefined
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
await ws.sendAction({
|
|
87
|
+
type: "action",
|
|
88
|
+
request_id: nextRequestId(),
|
|
89
|
+
action: "fold"
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
ws.on("action_result", (res) => {
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
emitter.emit("error", new Error(`action failed: ${res.error || "unknown"} (${res.request_id})`));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function stop() {
|
|
100
|
+
running = false;
|
|
101
|
+
await ws.stop();
|
|
102
|
+
}
|
|
103
|
+
function on(event, cb) {
|
|
104
|
+
emitter.on(event, cb);
|
|
105
|
+
}
|
|
106
|
+
return { play, stop, on };
|
|
107
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { APAHttpClient } from "./http/client.js";
|
|
2
|
+
import { createBot } from "./bot/createBot.js";
|
|
3
|
+
import { resolveApiBase, resolveWsUrl, requireArg } from "./utils/config.js";
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
const [command = "help", ...rest] = argv;
|
|
6
|
+
const args = {};
|
|
7
|
+
for (let i = 0; i < rest.length; i++) {
|
|
8
|
+
const token = rest[i];
|
|
9
|
+
if (!token.startsWith("--")) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const key = token.slice(2);
|
|
13
|
+
const next = rest[i + 1];
|
|
14
|
+
if (!next || next.startsWith("--")) {
|
|
15
|
+
args[key] = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
args[key] = next;
|
|
19
|
+
i += 1;
|
|
20
|
+
}
|
|
21
|
+
return { command, args };
|
|
22
|
+
}
|
|
23
|
+
function readString(args, key, envKey) {
|
|
24
|
+
const fromArg = args[key];
|
|
25
|
+
if (typeof fromArg === "string") {
|
|
26
|
+
return fromArg;
|
|
27
|
+
}
|
|
28
|
+
if (envKey) {
|
|
29
|
+
return process.env[envKey];
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
function readNumber(args, key) {
|
|
34
|
+
const raw = args[key];
|
|
35
|
+
if (typeof raw !== "string") {
|
|
36
|
+
throw new Error(`missing --${key}`);
|
|
37
|
+
}
|
|
38
|
+
const value = Number(raw);
|
|
39
|
+
if (!Number.isFinite(value)) {
|
|
40
|
+
throw new Error(`invalid --${key}`);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
function printHelp() {
|
|
45
|
+
console.log(`apa-bot commands:
|
|
46
|
+
apa-bot register --name <name> --description <desc> [--api-base <url>]
|
|
47
|
+
apa-bot status --api-key <key> [--api-base <url>]
|
|
48
|
+
apa-bot me --api-key <key> [--api-base <url>]
|
|
49
|
+
apa-bot bind-key --api-key <key> --provider <openai|kimi> --vendor-key <key> --budget-usd <num> [--api-base <url>]
|
|
50
|
+
apa-bot play --agent-id <id> --api-key <key> --join <random|select> [--room-id <id>] [--ws-url <url>]
|
|
51
|
+
apa-bot doctor [--api-base <url>] [--ws-url <url>]
|
|
52
|
+
|
|
53
|
+
Config priority: CLI args > env (API_BASE, WS_URL) > defaults.`);
|
|
54
|
+
}
|
|
55
|
+
function defaultStrategy(ctx) {
|
|
56
|
+
if (ctx.callAmount === 0) {
|
|
57
|
+
return { action: "check" };
|
|
58
|
+
}
|
|
59
|
+
if (Math.random() < 0.75) {
|
|
60
|
+
return { action: "call" };
|
|
61
|
+
}
|
|
62
|
+
if (Math.random() < 0.5) {
|
|
63
|
+
return { action: "raise", amount: ctx.currentBet + ctx.minRaise };
|
|
64
|
+
}
|
|
65
|
+
return { action: "fold" };
|
|
66
|
+
}
|
|
67
|
+
async function run() {
|
|
68
|
+
const { command, args } = parseArgs(process.argv.slice(2));
|
|
69
|
+
if (command === "help" || command === "--help" || command === "-h") {
|
|
70
|
+
printHelp();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
|
|
74
|
+
const wsUrl = resolveWsUrl(readString(args, "ws-url", "WS_URL"));
|
|
75
|
+
switch (command) {
|
|
76
|
+
case "register": {
|
|
77
|
+
const client = new APAHttpClient({ apiBase });
|
|
78
|
+
const name = requireArg("--name", readString(args, "name"));
|
|
79
|
+
const description = requireArg("--description", readString(args, "description"));
|
|
80
|
+
const result = await client.registerAgent({ name, description });
|
|
81
|
+
console.log(JSON.stringify(result, null, 2));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
case "status": {
|
|
85
|
+
const client = new APAHttpClient({ apiBase });
|
|
86
|
+
const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
|
|
87
|
+
const result = await client.getAgentStatus(apiKey);
|
|
88
|
+
console.log(JSON.stringify(result, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
case "me": {
|
|
92
|
+
const client = new APAHttpClient({ apiBase });
|
|
93
|
+
const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
|
|
94
|
+
const result = await client.getAgentMe(apiKey);
|
|
95
|
+
console.log(JSON.stringify(result, null, 2));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
case "bind-key": {
|
|
99
|
+
const client = new APAHttpClient({ apiBase });
|
|
100
|
+
const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
|
|
101
|
+
const provider = requireArg("--provider", readString(args, "provider"));
|
|
102
|
+
const vendorKey = requireArg("--vendor-key", readString(args, "vendor-key"));
|
|
103
|
+
const budgetUsd = readNumber(args, "budget-usd");
|
|
104
|
+
const result = await client.bindKey({ apiKey, provider, vendorKey, budgetUsd });
|
|
105
|
+
console.log(JSON.stringify(result, null, 2));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
case "play": {
|
|
109
|
+
const agentId = requireArg("--agent-id", readString(args, "agent-id", "AGENT_ID"));
|
|
110
|
+
const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
|
|
111
|
+
const joinRaw = requireArg("--join", readString(args, "join"));
|
|
112
|
+
const join = joinRaw === "select"
|
|
113
|
+
? { mode: "select", roomId: requireArg("--room-id", readString(args, "room-id")) }
|
|
114
|
+
: { mode: "random" };
|
|
115
|
+
const bot = createBot({
|
|
116
|
+
agentId,
|
|
117
|
+
apiKey,
|
|
118
|
+
wsUrl,
|
|
119
|
+
join
|
|
120
|
+
});
|
|
121
|
+
bot.on("join", (evt) => {
|
|
122
|
+
console.log(`joined room ${evt.room_id || "unknown"}`);
|
|
123
|
+
});
|
|
124
|
+
bot.on("handEnd", (evt) => {
|
|
125
|
+
console.log(`hand_end winner=${evt.winner} pot=${evt.pot}`);
|
|
126
|
+
});
|
|
127
|
+
bot.on("eventLog", (evt) => {
|
|
128
|
+
console.log(`event seat=${evt.player_seat} action=${evt.action}`);
|
|
129
|
+
});
|
|
130
|
+
bot.on("error", (err) => {
|
|
131
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
132
|
+
});
|
|
133
|
+
await bot.play((ctx) => defaultStrategy(ctx));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
case "doctor": {
|
|
137
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
138
|
+
const client = new APAHttpClient({ apiBase });
|
|
139
|
+
const report = {
|
|
140
|
+
node: process.versions.node,
|
|
141
|
+
node_ok: major >= 20,
|
|
142
|
+
api_base: apiBase,
|
|
143
|
+
ws_url: wsUrl
|
|
144
|
+
};
|
|
145
|
+
try {
|
|
146
|
+
report.healthz = await client.healthz();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
report.healthz_error = err instanceof Error ? err.message : String(err);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const ws = new WebSocket(wsUrl);
|
|
153
|
+
await new Promise((resolve, reject) => {
|
|
154
|
+
ws.onopen = () => {
|
|
155
|
+
ws.close();
|
|
156
|
+
resolve();
|
|
157
|
+
};
|
|
158
|
+
ws.onerror = () => reject(new Error("ws connect failed"));
|
|
159
|
+
});
|
|
160
|
+
report.ws_connect = "ok";
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
report.ws_connect_error = err instanceof Error ? err.message : String(err);
|
|
164
|
+
}
|
|
165
|
+
console.log(JSON.stringify(report, null, 2));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
printHelp();
|
|
170
|
+
throw new Error(`unknown command: ${command}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
run().catch((err) => {
|
|
174
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
175
|
+
process.exit(1);
|
|
176
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type HttpClientOptions = {
|
|
2
|
+
apiBase?: string;
|
|
3
|
+
};
|
|
4
|
+
export declare class APAHttpClient {
|
|
5
|
+
private readonly apiBase;
|
|
6
|
+
constructor(opts?: HttpClientOptions);
|
|
7
|
+
registerAgent(input: {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
}): Promise<any>;
|
|
11
|
+
getAgentStatus(apiKey: string): Promise<any>;
|
|
12
|
+
getAgentMe(apiKey: string): Promise<any>;
|
|
13
|
+
bindKey(input: {
|
|
14
|
+
apiKey: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
vendorKey: string;
|
|
17
|
+
budgetUsd: number;
|
|
18
|
+
}): Promise<any>;
|
|
19
|
+
healthz(): Promise<any>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { resolveApiBase } from "../utils/config.js";
|
|
2
|
+
async function parseJson(res) {
|
|
3
|
+
const text = await res.text();
|
|
4
|
+
if (!text) {
|
|
5
|
+
throw new Error(`empty response (${res.status})`);
|
|
6
|
+
}
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = JSON.parse(text);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error(`invalid json response (${res.status})`);
|
|
13
|
+
}
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
throw new Error(`${res.status} ${parsed?.error || text}`);
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
export class APAHttpClient {
|
|
20
|
+
apiBase;
|
|
21
|
+
constructor(opts = {}) {
|
|
22
|
+
this.apiBase = resolveApiBase(opts.apiBase);
|
|
23
|
+
}
|
|
24
|
+
async registerAgent(input) {
|
|
25
|
+
const res = await fetch(`${this.apiBase}/agents/register`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
body: JSON.stringify(input)
|
|
29
|
+
});
|
|
30
|
+
return parseJson(res);
|
|
31
|
+
}
|
|
32
|
+
async getAgentStatus(apiKey) {
|
|
33
|
+
const res = await fetch(`${this.apiBase}/agents/status`, {
|
|
34
|
+
headers: { authorization: `Bearer ${apiKey}` }
|
|
35
|
+
});
|
|
36
|
+
return parseJson(res);
|
|
37
|
+
}
|
|
38
|
+
async getAgentMe(apiKey) {
|
|
39
|
+
const res = await fetch(`${this.apiBase}/agents/me`, {
|
|
40
|
+
headers: { authorization: `Bearer ${apiKey}` }
|
|
41
|
+
});
|
|
42
|
+
return parseJson(res);
|
|
43
|
+
}
|
|
44
|
+
async bindKey(input) {
|
|
45
|
+
const res = await fetch(`${this.apiBase}/agents/bind_key`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
authorization: `Bearer ${input.apiKey}`,
|
|
49
|
+
"content-type": "application/json"
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
provider: input.provider,
|
|
53
|
+
api_key: input.vendorKey,
|
|
54
|
+
budget_usd: input.budgetUsd
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
return parseJson(res);
|
|
58
|
+
}
|
|
59
|
+
async healthz() {
|
|
60
|
+
const base = this.apiBase.replace(/\/api\/?$/, "");
|
|
61
|
+
const res = await fetch(`${base}/healthz`);
|
|
62
|
+
return parseJson(res);
|
|
63
|
+
}
|
|
64
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { APAHttpClient } from "./http/client.js";
|
|
2
|
+
export { createBot } from "./bot/createBot.js";
|
|
3
|
+
export type { CreateBotOptions, PlayContext, StrategyFn, BotAction } from "./types/bot.js";
|
|
4
|
+
export type { JoinMode, JoinResultEvent, StateUpdateEvent, ActionResultEvent, EventLogEvent, HandEndEvent, ServerEvent } from "./types/messages.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { JoinMode, StateUpdateEvent } from "./messages.js";
|
|
2
|
+
export type BotAction = {
|
|
3
|
+
action: "fold";
|
|
4
|
+
} | {
|
|
5
|
+
action: "check";
|
|
6
|
+
} | {
|
|
7
|
+
action: "call";
|
|
8
|
+
} | {
|
|
9
|
+
action: "raise";
|
|
10
|
+
amount: number;
|
|
11
|
+
} | {
|
|
12
|
+
action: "bet";
|
|
13
|
+
amount: number;
|
|
14
|
+
};
|
|
15
|
+
export type PlayContext = {
|
|
16
|
+
gameId: string;
|
|
17
|
+
handId: string;
|
|
18
|
+
mySeat: number;
|
|
19
|
+
currentActorSeat: number;
|
|
20
|
+
minRaise: number;
|
|
21
|
+
currentBet: number;
|
|
22
|
+
callAmount: number;
|
|
23
|
+
myBalance: number;
|
|
24
|
+
communityCards: string[];
|
|
25
|
+
holeCards: string[];
|
|
26
|
+
raw: StateUpdateEvent;
|
|
27
|
+
};
|
|
28
|
+
export type StrategyFn = (ctx: PlayContext) => BotAction | Promise<BotAction>;
|
|
29
|
+
export type ReconnectOptions = {
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
baseMs?: number;
|
|
32
|
+
maxMs?: number;
|
|
33
|
+
jitter?: boolean;
|
|
34
|
+
};
|
|
35
|
+
export type CreateBotOptions = {
|
|
36
|
+
wsUrl?: string;
|
|
37
|
+
agentId: string;
|
|
38
|
+
apiKey: string;
|
|
39
|
+
join: JoinMode;
|
|
40
|
+
reconnect?: ReconnectOptions;
|
|
41
|
+
actionTimeoutGuardMs?: number;
|
|
42
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export type JoinMode = {
|
|
2
|
+
mode: "random";
|
|
3
|
+
} | {
|
|
4
|
+
mode: "select";
|
|
5
|
+
roomId: string;
|
|
6
|
+
};
|
|
7
|
+
export type JoinMessage = {
|
|
8
|
+
type: "join";
|
|
9
|
+
agent_id: string;
|
|
10
|
+
api_key: string;
|
|
11
|
+
join_mode: "random" | "select";
|
|
12
|
+
room_id?: string;
|
|
13
|
+
};
|
|
14
|
+
export type ActionMessage = {
|
|
15
|
+
type: "action";
|
|
16
|
+
request_id: string;
|
|
17
|
+
action: "fold" | "check" | "call" | "raise" | "bet";
|
|
18
|
+
amount?: number;
|
|
19
|
+
thought_log?: string;
|
|
20
|
+
};
|
|
21
|
+
export type JoinResultEvent = {
|
|
22
|
+
type: "join_result";
|
|
23
|
+
protocol_version: string;
|
|
24
|
+
ok: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
room_id?: string;
|
|
27
|
+
};
|
|
28
|
+
export type StateUpdateEvent = {
|
|
29
|
+
type: "state_update";
|
|
30
|
+
protocol_version: string;
|
|
31
|
+
game_id: string;
|
|
32
|
+
hand_id: string;
|
|
33
|
+
my_seat: number;
|
|
34
|
+
current_actor_seat: number;
|
|
35
|
+
min_raise: number;
|
|
36
|
+
current_bet: number;
|
|
37
|
+
call_amount: number;
|
|
38
|
+
my_balance: number;
|
|
39
|
+
action_timeout_ms: number;
|
|
40
|
+
street: string;
|
|
41
|
+
hole_cards?: string[];
|
|
42
|
+
community_cards: string[];
|
|
43
|
+
pot: number;
|
|
44
|
+
opponents: Array<{
|
|
45
|
+
seat: number;
|
|
46
|
+
name: string;
|
|
47
|
+
stack: number;
|
|
48
|
+
action: string;
|
|
49
|
+
}>;
|
|
50
|
+
};
|
|
51
|
+
export type ActionResultEvent = {
|
|
52
|
+
type: "action_result";
|
|
53
|
+
protocol_version: string;
|
|
54
|
+
request_id: string;
|
|
55
|
+
ok: boolean;
|
|
56
|
+
error?: string;
|
|
57
|
+
};
|
|
58
|
+
export type EventLogEvent = {
|
|
59
|
+
type: "event_log";
|
|
60
|
+
protocol_version: string;
|
|
61
|
+
timestamp_ms: number;
|
|
62
|
+
player_seat: number;
|
|
63
|
+
action: string;
|
|
64
|
+
amount?: number;
|
|
65
|
+
thought_log?: string;
|
|
66
|
+
event: string;
|
|
67
|
+
};
|
|
68
|
+
export type HandEndEvent = {
|
|
69
|
+
type: "hand_end";
|
|
70
|
+
protocol_version: string;
|
|
71
|
+
winner: string;
|
|
72
|
+
pot: number;
|
|
73
|
+
balances: Array<{
|
|
74
|
+
agent_id: string;
|
|
75
|
+
balance: number;
|
|
76
|
+
}>;
|
|
77
|
+
showdown?: Array<{
|
|
78
|
+
agent_id: string;
|
|
79
|
+
hole_cards: string[];
|
|
80
|
+
}>;
|
|
81
|
+
};
|
|
82
|
+
export type ServerEvent = JoinResultEvent | StateUpdateEvent | ActionResultEvent | EventLogEvent | HandEndEvent | {
|
|
83
|
+
type: string;
|
|
84
|
+
[k: string]: unknown;
|
|
85
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function validateBotAction(action) {
|
|
2
|
+
if (action.action === "raise" || action.action === "bet") {
|
|
3
|
+
if (!Number.isFinite(action.amount) || action.amount <= 0) {
|
|
4
|
+
throw new Error(`${action.action} requires positive amount`);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function nextRequestId() {
|
|
9
|
+
return `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { validateBotAction } from "./action.js";
|
|
4
|
+
test("validateBotAction accepts non-amount actions", () => {
|
|
5
|
+
validateBotAction({ action: "check" });
|
|
6
|
+
validateBotAction({ action: "fold" });
|
|
7
|
+
});
|
|
8
|
+
test("validateBotAction rejects invalid raise amount", () => {
|
|
9
|
+
assert.throws(() => validateBotAction({ action: "raise", amount: 0 }), /positive amount/);
|
|
10
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function computeBackoffMs(attempt, baseMs, maxMs, jitter) {
|
|
2
|
+
const exponential = Math.min(maxMs, baseMs * 2 ** Math.max(0, attempt - 1));
|
|
3
|
+
if (!jitter) {
|
|
4
|
+
return exponential;
|
|
5
|
+
}
|
|
6
|
+
const spread = Math.max(1, Math.floor(exponential * 0.2));
|
|
7
|
+
return exponential - spread + Math.floor(Math.random() * spread * 2);
|
|
8
|
+
}
|
|
9
|
+
export function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { computeBackoffMs } from "./backoff.js";
|
|
4
|
+
test("computeBackoffMs grows exponentially without jitter", () => {
|
|
5
|
+
assert.equal(computeBackoffMs(1, 500, 8000, false), 500);
|
|
6
|
+
assert.equal(computeBackoffMs(2, 500, 8000, false), 1000);
|
|
7
|
+
assert.equal(computeBackoffMs(5, 500, 8000, false), 8000);
|
|
8
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const DEFAULT_API_BASE = "http://localhost:8080/api";
|
|
2
|
+
export declare const DEFAULT_WS_URL = "ws://localhost:8080/ws";
|
|
3
|
+
export declare function resolveApiBase(override?: string): string;
|
|
4
|
+
export declare function resolveWsUrl(override?: string): string;
|
|
5
|
+
export declare function requireArg(name: string, value?: string): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DEFAULT_API_BASE = "http://localhost:8080/api";
|
|
2
|
+
export const DEFAULT_WS_URL = "ws://localhost:8080/ws";
|
|
3
|
+
export function resolveApiBase(override) {
|
|
4
|
+
return (override || process.env.API_BASE || DEFAULT_API_BASE).trim();
|
|
5
|
+
}
|
|
6
|
+
export function resolveWsUrl(override) {
|
|
7
|
+
return (override || process.env.WS_URL || DEFAULT_WS_URL).trim();
|
|
8
|
+
}
|
|
9
|
+
export function requireArg(name, value) {
|
|
10
|
+
if (value && value.trim() !== "") {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`missing required argument: ${name}`);
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { DEFAULT_API_BASE, resolveApiBase } from "./config.js";
|
|
4
|
+
test("resolveApiBase prefers override", () => {
|
|
5
|
+
assert.equal(resolveApiBase("http://x/api"), "http://x/api");
|
|
6
|
+
});
|
|
7
|
+
test("resolveApiBase falls back to default", () => {
|
|
8
|
+
const old = process.env.API_BASE;
|
|
9
|
+
delete process.env.API_BASE;
|
|
10
|
+
assert.equal(resolveApiBase(undefined), DEFAULT_API_BASE);
|
|
11
|
+
process.env.API_BASE = old;
|
|
12
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import type { ActionMessage, JoinMode } from "../types/messages.js";
|
|
3
|
+
type WsOptions = {
|
|
4
|
+
wsUrl?: string;
|
|
5
|
+
agentId: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
join: JoinMode;
|
|
8
|
+
reconnect?: {
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
baseMs?: number;
|
|
11
|
+
maxMs?: number;
|
|
12
|
+
jitter?: boolean;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare class APAWsClient extends EventEmitter {
|
|
16
|
+
private readonly wsUrl;
|
|
17
|
+
private readonly agentId;
|
|
18
|
+
private readonly apiKey;
|
|
19
|
+
private readonly join;
|
|
20
|
+
private readonly reconnect;
|
|
21
|
+
private socket;
|
|
22
|
+
private shouldRun;
|
|
23
|
+
private connectAttempt;
|
|
24
|
+
constructor(opts: WsOptions);
|
|
25
|
+
connect(): Promise<void>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
sendAction(action: ActionMessage): Promise<void>;
|
|
28
|
+
private openSocket;
|
|
29
|
+
private reconnectLoop;
|
|
30
|
+
private sendJoin;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { computeBackoffMs, sleep } from "../utils/backoff.js";
|
|
3
|
+
import { resolveWsUrl } from "../utils/config.js";
|
|
4
|
+
const DEFAULT_RECONNECT = {
|
|
5
|
+
enabled: true,
|
|
6
|
+
baseMs: 500,
|
|
7
|
+
maxMs: 8000,
|
|
8
|
+
jitter: true
|
|
9
|
+
};
|
|
10
|
+
export class APAWsClient extends EventEmitter {
|
|
11
|
+
wsUrl;
|
|
12
|
+
agentId;
|
|
13
|
+
apiKey;
|
|
14
|
+
join;
|
|
15
|
+
reconnect;
|
|
16
|
+
socket = null;
|
|
17
|
+
shouldRun = true;
|
|
18
|
+
connectAttempt = 0;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
super();
|
|
21
|
+
this.wsUrl = resolveWsUrl(opts.wsUrl);
|
|
22
|
+
this.agentId = opts.agentId;
|
|
23
|
+
this.apiKey = opts.apiKey;
|
|
24
|
+
this.join = opts.join;
|
|
25
|
+
this.reconnect = {
|
|
26
|
+
enabled: opts.reconnect?.enabled ?? DEFAULT_RECONNECT.enabled,
|
|
27
|
+
baseMs: opts.reconnect?.baseMs ?? DEFAULT_RECONNECT.baseMs,
|
|
28
|
+
maxMs: opts.reconnect?.maxMs ?? DEFAULT_RECONNECT.maxMs,
|
|
29
|
+
jitter: opts.reconnect?.jitter ?? DEFAULT_RECONNECT.jitter
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async connect() {
|
|
33
|
+
this.shouldRun = true;
|
|
34
|
+
await this.openSocket();
|
|
35
|
+
await this.sendJoin();
|
|
36
|
+
}
|
|
37
|
+
async stop() {
|
|
38
|
+
this.shouldRun = false;
|
|
39
|
+
if (this.socket) {
|
|
40
|
+
this.socket.close();
|
|
41
|
+
this.socket = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async sendAction(action) {
|
|
45
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
46
|
+
throw new Error("ws not connected");
|
|
47
|
+
}
|
|
48
|
+
this.socket.send(JSON.stringify(action));
|
|
49
|
+
}
|
|
50
|
+
async openSocket() {
|
|
51
|
+
const Impl = globalThis.WebSocket;
|
|
52
|
+
if (!Impl) {
|
|
53
|
+
throw new Error("WebSocket is unavailable in current Node runtime (requires Node 20+)");
|
|
54
|
+
}
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
const ws = new Impl(this.wsUrl);
|
|
57
|
+
this.socket = ws;
|
|
58
|
+
ws.onopen = () => {
|
|
59
|
+
this.connectAttempt = 0;
|
|
60
|
+
this.emit("connected");
|
|
61
|
+
resolve();
|
|
62
|
+
};
|
|
63
|
+
ws.onmessage = (evt) => {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(String(evt.data));
|
|
66
|
+
this.emit("event", data);
|
|
67
|
+
this.emit(data.type, data);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
this.emit("error", err);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
ws.onerror = () => {
|
|
74
|
+
reject(new Error("ws connection failed"));
|
|
75
|
+
};
|
|
76
|
+
ws.onclose = async () => {
|
|
77
|
+
this.emit("disconnected");
|
|
78
|
+
if (!this.shouldRun || !this.reconnect.enabled) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await this.reconnectLoop();
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async reconnectLoop() {
|
|
86
|
+
while (this.shouldRun) {
|
|
87
|
+
this.connectAttempt += 1;
|
|
88
|
+
const waitMs = computeBackoffMs(this.connectAttempt, this.reconnect.baseMs, this.reconnect.maxMs, this.reconnect.jitter);
|
|
89
|
+
this.emit("reconnect", { attempt: this.connectAttempt, waitMs });
|
|
90
|
+
await sleep(waitMs);
|
|
91
|
+
try {
|
|
92
|
+
await this.openSocket();
|
|
93
|
+
await this.sendJoin();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
this.emit("error", err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async sendJoin() {
|
|
102
|
+
const msg = {
|
|
103
|
+
type: "join",
|
|
104
|
+
agent_id: this.agentId,
|
|
105
|
+
api_key: this.apiKey,
|
|
106
|
+
join_mode: this.join.mode
|
|
107
|
+
};
|
|
108
|
+
if (this.join.mode === "select") {
|
|
109
|
+
msg.room_id = this.join.roomId;
|
|
110
|
+
}
|
|
111
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
|
112
|
+
throw new Error("ws not connected for join");
|
|
113
|
+
}
|
|
114
|
+
this.socket.send(JSON.stringify(msg));
|
|
115
|
+
}
|
|
116
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@apa-network/agent-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "APA Agent SDK and CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"apa-bot": "./bin/apa-bot.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"test": "npm run build && node --test dist/**/*.test.js"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.10.2",
|
|
25
|
+
"typescript": "^5.7.2"
|
|
26
|
+
}
|
|
27
|
+
}
|