@apa-network/agent-sdk 0.1.0 → 0.2.0-beta.10
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 +109 -14
- package/bin/apa-bot.js +6 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +491 -74
- package/dist/cli.next_decision.e2e.test.js +468 -0
- package/dist/commands/register.d.ts +2 -0
- package/dist/commands/register.js +18 -0
- package/dist/commands/register.test.js +26 -0
- package/dist/commands/session_recovery.d.ts +6 -0
- package/dist/commands/session_recovery.js +25 -0
- package/dist/commands/session_recovery.test.js +27 -0
- package/dist/http/client.d.ts +30 -1
- package/dist/http/client.js +50 -7
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -1
- package/dist/loop/callback.d.ts +20 -0
- package/dist/loop/callback.js +105 -0
- package/dist/loop/callback.test.js +33 -0
- package/dist/loop/credentials.d.ts +10 -0
- package/dist/loop/credentials.js +60 -0
- package/dist/loop/credentials.test.d.ts +1 -0
- package/dist/loop/credentials.test.js +33 -0
- package/dist/loop/decision_state.d.ts +30 -0
- package/dist/loop/decision_state.js +43 -0
- package/dist/loop/state.d.ts +9 -0
- package/dist/loop/state.js +16 -0
- package/dist/loop/state.test.d.ts +1 -0
- package/dist/loop/state.test.js +14 -0
- package/dist/utils/config.d.ts +1 -2
- package/dist/utils/config.js +8 -5
- package/dist/utils/config.test.js +7 -1
- package/package.json +7 -2
- package/dist/bot/createBot.d.ts +0 -14
- package/dist/bot/createBot.js +0 -107
- package/dist/types/bot.d.ts +0 -42
- package/dist/types/messages.d.ts +0 -85
- package/dist/utils/action.d.ts +0 -3
- package/dist/utils/action.js +0 -10
- package/dist/utils/action.test.js +0 -10
- package/dist/utils/backoff.d.ts +0 -2
- package/dist/utils/backoff.js +0 -11
- package/dist/utils/backoff.test.js +0 -8
- package/dist/ws/client.d.ts +0 -32
- package/dist/ws/client.js +0 -116
- /package/dist/{types/bot.js → cli.next_decision.e2e.test.d.ts} +0 -0
- /package/dist/{types/messages.js → commands/register.test.d.ts} +0 -0
- /package/dist/{utils/action.test.d.ts → commands/session_recovery.test.d.ts} +0 -0
- /package/dist/{utils/backoff.test.d.ts → loop/callback.test.d.ts} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { APAHttpClient } from "./http/client.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { resolveApiBase, requireArg } from "./utils/config.js";
|
|
3
|
+
import { loadCredential, saveCredential } from "./loop/credentials.js";
|
|
4
|
+
import { loadDecisionState, saveDecisionState } from "./loop/decision_state.js";
|
|
5
|
+
import { TurnTracker } from "./loop/state.js";
|
|
6
|
+
import { buildCredentialFromRegisterResult } from "./commands/register.js";
|
|
7
|
+
import { recoverSessionFromConflict, resolveStreamURL } from "./commands/session_recovery.js";
|
|
4
8
|
function parseArgs(argv) {
|
|
5
9
|
const [command = "help", ...rest] = argv;
|
|
6
10
|
const args = {};
|
|
7
|
-
for (let i = 0; i < rest.length; i
|
|
11
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
8
12
|
const token = rest[i];
|
|
9
13
|
if (!token.startsWith("--")) {
|
|
10
14
|
continue;
|
|
@@ -30,8 +34,11 @@ function readString(args, key, envKey) {
|
|
|
30
34
|
}
|
|
31
35
|
return undefined;
|
|
32
36
|
}
|
|
33
|
-
function readNumber(args, key) {
|
|
37
|
+
function readNumber(args, key, fallback) {
|
|
34
38
|
const raw = args[key];
|
|
39
|
+
if (raw === undefined && fallback !== undefined) {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
35
42
|
if (typeof raw !== "string") {
|
|
36
43
|
throw new Error(`missing --${key}`);
|
|
37
44
|
}
|
|
@@ -44,60 +51,509 @@ function readNumber(args, key) {
|
|
|
44
51
|
function printHelp() {
|
|
45
52
|
console.log(`apa-bot commands:
|
|
46
53
|
apa-bot register --name <name> --description <desc> [--api-base <url>]
|
|
47
|
-
apa-bot
|
|
48
|
-
apa-bot me
|
|
49
|
-
apa-bot bind-key --
|
|
50
|
-
apa-bot
|
|
51
|
-
|
|
54
|
+
apa-bot claim (--claim-code <code> | --claim-url <url>) [--api-base <url>]
|
|
55
|
+
apa-bot me [--api-base <url>]
|
|
56
|
+
apa-bot bind-key --provider <openai|kimi> --vendor-key <key> --budget-usd <num> [--api-base <url>]
|
|
57
|
+
apa-bot next-decision --join <random|select> [--room-id <id>]
|
|
58
|
+
[--timeout-ms <ms>] [--api-base <url>]
|
|
59
|
+
apa-bot submit-decision --decision-id <id> --action <fold|check|call|raise|bet>
|
|
60
|
+
[--amount <num>] [--thought-log <text>] [--api-base <url>]
|
|
61
|
+
apa-bot doctor [--api-base <url>]
|
|
52
62
|
|
|
53
|
-
Config priority: CLI args > env (API_BASE
|
|
63
|
+
Config priority: CLI args > env (API_BASE) > defaults.`);
|
|
64
|
+
}
|
|
65
|
+
async function requireApiKey(apiBase) {
|
|
66
|
+
const cached = await loadCredential(apiBase, undefined);
|
|
67
|
+
if (!cached?.api_key) {
|
|
68
|
+
throw new Error("api_key_not_found (run apa-bot register)");
|
|
69
|
+
}
|
|
70
|
+
return cached.api_key;
|
|
71
|
+
}
|
|
72
|
+
function claimCodeFromUrl(raw) {
|
|
73
|
+
try {
|
|
74
|
+
const url = new URL(raw);
|
|
75
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
76
|
+
return parts[parts.length - 1] || "";
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function emit(message) {
|
|
83
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
84
|
+
}
|
|
85
|
+
function readLegalActions(state) {
|
|
86
|
+
const raw = state["legal_actions"];
|
|
87
|
+
if (!Array.isArray(raw)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return raw;
|
|
91
|
+
}
|
|
92
|
+
function readActionConstraints(state) {
|
|
93
|
+
const raw = state["action_constraints"];
|
|
94
|
+
if (!raw || typeof raw !== "object") {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const src = raw;
|
|
98
|
+
const out = {};
|
|
99
|
+
const bet = src["bet"];
|
|
100
|
+
if (bet && typeof bet === "object") {
|
|
101
|
+
const b = bet;
|
|
102
|
+
const min = Number(b["min"]);
|
|
103
|
+
const max = Number(b["max"]);
|
|
104
|
+
if (Number.isFinite(min) && Number.isFinite(max)) {
|
|
105
|
+
out.bet = { min, max };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const raise = src["raise"];
|
|
109
|
+
if (raise && typeof raise === "object") {
|
|
110
|
+
const r = raise;
|
|
111
|
+
const minTo = Number(r["min_to"]);
|
|
112
|
+
const maxTo = Number(r["max_to"]);
|
|
113
|
+
if (Number.isFinite(minTo) && Number.isFinite(maxTo)) {
|
|
114
|
+
out.raise = { min_to: minTo, max_to: maxTo };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!out.bet && !out.raise) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
54
121
|
}
|
|
55
|
-
function
|
|
56
|
-
if (
|
|
57
|
-
return
|
|
122
|
+
function parseAction(raw) {
|
|
123
|
+
if (raw === "fold" || raw === "check" || raw === "call" || raw === "raise" || raw === "bet") {
|
|
124
|
+
return raw;
|
|
58
125
|
}
|
|
59
|
-
|
|
60
|
-
|
|
126
|
+
throw new Error("invalid --action");
|
|
127
|
+
}
|
|
128
|
+
async function parseSSEOnce(url, lastEventId, timeoutMs, onEvent) {
|
|
129
|
+
const headers = { Accept: "text/event-stream" };
|
|
130
|
+
if (lastEventId) {
|
|
131
|
+
headers["Last-Event-ID"] = lastEventId;
|
|
61
132
|
}
|
|
62
|
-
|
|
63
|
-
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
135
|
+
let latestId = lastEventId;
|
|
136
|
+
let buffer = "";
|
|
137
|
+
let currentId = "";
|
|
138
|
+
let currentEvent = "";
|
|
139
|
+
let currentData = "";
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
|
|
142
|
+
if (!res.ok || !res.body) {
|
|
143
|
+
throw new Error(`stream_open_failed_${res.status}`);
|
|
144
|
+
}
|
|
145
|
+
const reader = res.body.getReader();
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
while (true) {
|
|
148
|
+
const { done, value } = await reader.read();
|
|
149
|
+
if (done)
|
|
150
|
+
break;
|
|
151
|
+
buffer += decoder.decode(value, { stream: true });
|
|
152
|
+
const lines = buffer.split("\n");
|
|
153
|
+
buffer = lines.pop() || "";
|
|
154
|
+
for (const rawLine of lines) {
|
|
155
|
+
const line = rawLine.trimEnd();
|
|
156
|
+
if (line.startsWith("id:")) {
|
|
157
|
+
currentId = line.slice(3).trim();
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (line.startsWith("event:")) {
|
|
161
|
+
currentEvent = line.slice(6).trim();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (line.startsWith("data:")) {
|
|
165
|
+
const piece = line.slice(5).trimStart();
|
|
166
|
+
currentData = currentData ? `${currentData}\n${piece}` : piece;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (line !== "") {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (!currentData) {
|
|
173
|
+
currentId = "";
|
|
174
|
+
currentEvent = "";
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const evt = {
|
|
178
|
+
id: currentId,
|
|
179
|
+
event: currentEvent,
|
|
180
|
+
data: currentData
|
|
181
|
+
};
|
|
182
|
+
if (evt.id)
|
|
183
|
+
latestId = evt.id;
|
|
184
|
+
const shouldStop = await onEvent(evt);
|
|
185
|
+
currentId = "";
|
|
186
|
+
currentEvent = "";
|
|
187
|
+
currentData = "";
|
|
188
|
+
if (shouldStop) {
|
|
189
|
+
controller.abort();
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
64
194
|
}
|
|
65
|
-
|
|
195
|
+
catch (err) {
|
|
196
|
+
if (!(err instanceof Error && err.name === "AbortError")) {
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
clearTimeout(timeout);
|
|
202
|
+
}
|
|
203
|
+
return latestId;
|
|
66
204
|
}
|
|
67
|
-
|
|
68
|
-
|
|
205
|
+
function pickRoom(rooms, joinMode, roomId) {
|
|
206
|
+
if (rooms.length === 0) {
|
|
207
|
+
throw new Error("no_rooms_available");
|
|
208
|
+
}
|
|
209
|
+
if (joinMode === "select") {
|
|
210
|
+
const match = rooms.find((room) => room.id === roomId);
|
|
211
|
+
if (!match) {
|
|
212
|
+
throw new Error("room_not_found");
|
|
213
|
+
}
|
|
214
|
+
return { id: match.id, min_buyin_cc: match.min_buyin_cc };
|
|
215
|
+
}
|
|
216
|
+
const sorted = [...rooms].sort((a, b) => a.min_buyin_cc - b.min_buyin_cc);
|
|
217
|
+
return { id: sorted[0].id, min_buyin_cc: sorted[0].min_buyin_cc };
|
|
218
|
+
}
|
|
219
|
+
async function sessionExists(apiBase, sessionId) {
|
|
220
|
+
const res = await fetch(`${apiBase}/agent/sessions/${sessionId}/state`);
|
|
221
|
+
return res.ok;
|
|
222
|
+
}
|
|
223
|
+
async function ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId) {
|
|
224
|
+
const cachedState = await loadDecisionState();
|
|
225
|
+
if (cachedState.session_id && cachedState.stream_url) {
|
|
226
|
+
const ok = await sessionExists(apiBase, cachedState.session_id);
|
|
227
|
+
if (ok) {
|
|
228
|
+
return { session_id: cachedState.session_id, stream_url: cachedState.stream_url };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const me = await client.getAgentMe(apiKey);
|
|
232
|
+
if (me?.status === "pending") {
|
|
233
|
+
emit({ type: "claim_required", message: "agent is pending; complete claim before starting" });
|
|
234
|
+
throw new Error("agent_pending");
|
|
235
|
+
}
|
|
236
|
+
const balance = Number(me?.balance_cc ?? 0);
|
|
237
|
+
const rooms = await client.listPublicRooms();
|
|
238
|
+
const pickedRoom = pickRoom(rooms.items || [], joinMode, roomId);
|
|
239
|
+
if (balance < pickedRoom.min_buyin_cc) {
|
|
240
|
+
throw new Error(`insufficient_balance (balance=${balance}, min=${pickedRoom.min_buyin_cc})`);
|
|
241
|
+
}
|
|
242
|
+
const session = await client.createSession({
|
|
243
|
+
agentID: agentId,
|
|
244
|
+
apiKey,
|
|
245
|
+
joinMode: "select",
|
|
246
|
+
roomID: pickedRoom.id
|
|
247
|
+
}).catch(async (err) => {
|
|
248
|
+
const recovered = recoverSessionFromConflict(err, apiBase);
|
|
249
|
+
if (!recovered) {
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
await saveDecisionState({
|
|
253
|
+
session_id: recovered.session_id,
|
|
254
|
+
stream_url: recovered.stream_url,
|
|
255
|
+
last_event_id: "",
|
|
256
|
+
last_turn_id: ""
|
|
257
|
+
});
|
|
258
|
+
return {
|
|
259
|
+
session_id: recovered.session_id,
|
|
260
|
+
stream_url: recovered.stream_url
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
const sessionId = String(session.session_id || "");
|
|
264
|
+
const streamURL = String(session.stream_url || "");
|
|
265
|
+
const resolvedStreamURL = resolveStreamURL(apiBase, streamURL);
|
|
266
|
+
await saveDecisionState({
|
|
267
|
+
session_id: sessionId,
|
|
268
|
+
stream_url: resolvedStreamURL,
|
|
269
|
+
last_event_id: "",
|
|
270
|
+
last_turn_id: ""
|
|
271
|
+
});
|
|
272
|
+
return { session_id: sessionId, stream_url: resolvedStreamURL };
|
|
273
|
+
}
|
|
274
|
+
async function runNextDecision(args) {
|
|
275
|
+
const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
|
|
276
|
+
const joinRaw = requireArg("--join", readString(args, "join"));
|
|
277
|
+
const joinMode = joinRaw === "select" ? "select" : "random";
|
|
278
|
+
const roomId = joinMode === "select" ? requireArg("--room-id", readString(args, "room-id")) : undefined;
|
|
279
|
+
const timeoutMs = readNumber(args, "timeout-ms", 5000);
|
|
280
|
+
const client = new APAHttpClient({ apiBase });
|
|
281
|
+
const cached = await loadCredential(apiBase, undefined);
|
|
282
|
+
if (!cached) {
|
|
283
|
+
throw new Error("credential_not_found (run apa-bot register first)");
|
|
284
|
+
}
|
|
285
|
+
const agentId = cached.agent_id;
|
|
286
|
+
const apiKey = cached.api_key;
|
|
287
|
+
const { session_id: sessionId, stream_url: streamURL } = await ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId);
|
|
288
|
+
const state = await loadDecisionState();
|
|
289
|
+
const lastEventId = state.last_event_id || "";
|
|
290
|
+
const tracker = new TurnTracker();
|
|
291
|
+
let decided = false;
|
|
292
|
+
let newLastEventId = lastEventId;
|
|
293
|
+
let pendingDecision;
|
|
294
|
+
try {
|
|
295
|
+
newLastEventId = await parseSSEOnce(streamURL, lastEventId, timeoutMs, async (evt) => {
|
|
296
|
+
let envelope;
|
|
297
|
+
try {
|
|
298
|
+
envelope = JSON.parse(evt.data);
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const evType = envelope?.event || evt.event;
|
|
304
|
+
const data = envelope?.data || {};
|
|
305
|
+
if (evType === "session_closed" || evType === "table_closed") {
|
|
306
|
+
await saveDecisionState({
|
|
307
|
+
session_id: "",
|
|
308
|
+
stream_url: "",
|
|
309
|
+
last_event_id: "",
|
|
310
|
+
last_turn_id: "",
|
|
311
|
+
pending_decision: undefined
|
|
312
|
+
});
|
|
313
|
+
emit({ type: "table_closed", session_id: sessionId, reason: data?.reason || "table_closed" });
|
|
314
|
+
decided = true;
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
if (evType === "reconnect_grace_started") {
|
|
318
|
+
emit({
|
|
319
|
+
type: "noop",
|
|
320
|
+
reason: "table_closing",
|
|
321
|
+
event: evType,
|
|
322
|
+
session_id: sessionId,
|
|
323
|
+
disconnected_agent_id: data?.disconnected_agent_id,
|
|
324
|
+
deadline_ts: data?.deadline_ts
|
|
325
|
+
});
|
|
326
|
+
decided = true;
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
if (evType === "opponent_forfeited") {
|
|
330
|
+
emit({
|
|
331
|
+
type: "noop",
|
|
332
|
+
reason: "table_closing",
|
|
333
|
+
event: evType,
|
|
334
|
+
session_id: sessionId
|
|
335
|
+
});
|
|
336
|
+
decided = true;
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
if (evType !== "state_snapshot") {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
const tableStatus = String(data?.table_status || "active");
|
|
343
|
+
if (tableStatus === "closing") {
|
|
344
|
+
emit({
|
|
345
|
+
type: "noop",
|
|
346
|
+
reason: "table_closing",
|
|
347
|
+
event: evType,
|
|
348
|
+
session_id: sessionId,
|
|
349
|
+
close_reason: data?.close_reason,
|
|
350
|
+
reconnect_deadline_ts: data?.reconnect_deadline_ts
|
|
351
|
+
});
|
|
352
|
+
decided = true;
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
if (tableStatus === "closed") {
|
|
356
|
+
await saveDecisionState({
|
|
357
|
+
session_id: "",
|
|
358
|
+
stream_url: "",
|
|
359
|
+
last_event_id: "",
|
|
360
|
+
last_turn_id: "",
|
|
361
|
+
pending_decision: undefined
|
|
362
|
+
});
|
|
363
|
+
emit({
|
|
364
|
+
type: "table_closed",
|
|
365
|
+
session_id: sessionId,
|
|
366
|
+
reason: data?.close_reason || "table_closed"
|
|
367
|
+
});
|
|
368
|
+
decided = true;
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
if (!tracker.shouldRequestDecision(data)) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
const reqID = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
|
|
375
|
+
const callbackURL = `${apiBase}/agent/sessions/${sessionId}/actions`;
|
|
376
|
+
const decisionID = `dec_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
|
|
377
|
+
const legalActions = readLegalActions(data);
|
|
378
|
+
const actionConstraints = readActionConstraints(data);
|
|
379
|
+
pendingDecision = {
|
|
380
|
+
decision_id: decisionID,
|
|
381
|
+
session_id: sessionId,
|
|
382
|
+
request_id: reqID,
|
|
383
|
+
turn_id: String(data.turn_id || ""),
|
|
384
|
+
callback_url: callbackURL,
|
|
385
|
+
legal_actions: legalActions,
|
|
386
|
+
action_constraints: actionConstraints,
|
|
387
|
+
created_at: new Date().toISOString()
|
|
388
|
+
};
|
|
389
|
+
const payload = {
|
|
390
|
+
type: "decision_request",
|
|
391
|
+
decision_id: decisionID,
|
|
392
|
+
session_id: sessionId,
|
|
393
|
+
state: data
|
|
394
|
+
};
|
|
395
|
+
if (legalActions.length > 0) {
|
|
396
|
+
payload.legal_actions = legalActions;
|
|
397
|
+
}
|
|
398
|
+
if (actionConstraints) {
|
|
399
|
+
payload.action_constraints = actionConstraints;
|
|
400
|
+
}
|
|
401
|
+
emit(payload);
|
|
402
|
+
decided = true;
|
|
403
|
+
return true;
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
emit({ type: "error", error: err instanceof Error ? err.message : String(err) });
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
await saveDecisionState({
|
|
412
|
+
session_id: sessionId,
|
|
413
|
+
stream_url: streamURL,
|
|
414
|
+
last_event_id: newLastEventId,
|
|
415
|
+
last_turn_id: "",
|
|
416
|
+
pending_decision: pendingDecision
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (!decided) {
|
|
420
|
+
emit({ type: "noop" });
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function runSubmitDecision(args) {
|
|
424
|
+
const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
|
|
425
|
+
const decisionID = requireArg("--decision-id", readString(args, "decision-id"));
|
|
426
|
+
const action = parseAction(requireArg("--action", readString(args, "action")));
|
|
427
|
+
const thoughtLog = readString(args, "thought-log") || "";
|
|
428
|
+
const amountRaw = readString(args, "amount");
|
|
429
|
+
const amount = amountRaw ? Number(amountRaw) : undefined;
|
|
430
|
+
if (amountRaw && !Number.isFinite(amount)) {
|
|
431
|
+
throw new Error("invalid --amount");
|
|
432
|
+
}
|
|
433
|
+
const state = await loadDecisionState();
|
|
434
|
+
const pending = state.pending_decision;
|
|
435
|
+
if (!pending) {
|
|
436
|
+
throw new Error("pending_decision_not_found (run apa-bot next-decision)");
|
|
437
|
+
}
|
|
438
|
+
if (pending.decision_id !== decisionID) {
|
|
439
|
+
throw new Error("decision_id_mismatch (run apa-bot next-decision)");
|
|
440
|
+
}
|
|
441
|
+
const legalActions = pending.legal_actions || [];
|
|
442
|
+
if (legalActions.length > 0 && !legalActions.includes(action)) {
|
|
443
|
+
throw new Error("action_not_legal");
|
|
444
|
+
}
|
|
445
|
+
if ((action === "bet" || action === "raise") && amount === undefined) {
|
|
446
|
+
throw new Error("amount_required_for_bet_or_raise");
|
|
447
|
+
}
|
|
448
|
+
const constraints = pending.action_constraints;
|
|
449
|
+
if (action === "bet" && amount !== undefined && constraints?.bet) {
|
|
450
|
+
if (amount < constraints.bet.min || amount > constraints.bet.max) {
|
|
451
|
+
throw new Error("amount_out_of_range");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (action === "raise" && amount !== undefined && constraints?.raise) {
|
|
455
|
+
if (amount < constraints.raise.min_to || amount > constraints.raise.max_to) {
|
|
456
|
+
throw new Error("amount_out_of_range");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const client = new APAHttpClient({ apiBase });
|
|
460
|
+
try {
|
|
461
|
+
const result = await client.submitAction({
|
|
462
|
+
sessionID: pending.session_id,
|
|
463
|
+
requestID: pending.request_id,
|
|
464
|
+
turnID: pending.turn_id,
|
|
465
|
+
action,
|
|
466
|
+
amount,
|
|
467
|
+
thoughtLog
|
|
468
|
+
});
|
|
469
|
+
await saveDecisionState({
|
|
470
|
+
...state,
|
|
471
|
+
pending_decision: undefined
|
|
472
|
+
});
|
|
473
|
+
console.log(JSON.stringify(result, null, 2));
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
const apiErr = err;
|
|
477
|
+
if (apiErr?.code === "invalid_turn_id" ||
|
|
478
|
+
apiErr?.code === "not_your_turn" ||
|
|
479
|
+
apiErr?.code === "table_closing" ||
|
|
480
|
+
apiErr?.code === "table_closed" ||
|
|
481
|
+
apiErr?.code === "opponent_disconnected") {
|
|
482
|
+
await saveDecisionState({
|
|
483
|
+
...state,
|
|
484
|
+
pending_decision: undefined
|
|
485
|
+
});
|
|
486
|
+
if (apiErr?.code === "table_closed") {
|
|
487
|
+
emit({
|
|
488
|
+
type: "table_closed",
|
|
489
|
+
decision_id: decisionID,
|
|
490
|
+
error: apiErr.code,
|
|
491
|
+
message: "table closed; re-join matchmaking"
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
else if (apiErr?.code === "table_closing" || apiErr?.code === "opponent_disconnected") {
|
|
495
|
+
emit({
|
|
496
|
+
type: "table_closing",
|
|
497
|
+
decision_id: decisionID,
|
|
498
|
+
error: apiErr.code,
|
|
499
|
+
message: "table is closing; pause and fetch new decision later"
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
emit({
|
|
504
|
+
type: "stale_decision",
|
|
505
|
+
decision_id: decisionID,
|
|
506
|
+
error: apiErr.code,
|
|
507
|
+
message: "decision expired; run apa-bot next-decision again"
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
export async function runCLI(argv = process.argv.slice(2)) {
|
|
516
|
+
const { command, args } = parseArgs(argv);
|
|
69
517
|
if (command === "help" || command === "--help" || command === "-h") {
|
|
70
518
|
printHelp();
|
|
71
519
|
return;
|
|
72
520
|
}
|
|
73
521
|
const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
|
|
74
|
-
const wsUrl = resolveWsUrl(readString(args, "ws-url", "WS_URL"));
|
|
75
522
|
switch (command) {
|
|
76
523
|
case "register": {
|
|
77
524
|
const client = new APAHttpClient({ apiBase });
|
|
78
525
|
const name = requireArg("--name", readString(args, "name"));
|
|
79
526
|
const description = requireArg("--description", readString(args, "description"));
|
|
80
527
|
const result = await client.registerAgent({ name, description });
|
|
528
|
+
const record = buildCredentialFromRegisterResult(result, apiBase, name);
|
|
529
|
+
if (record) {
|
|
530
|
+
await saveCredential(record);
|
|
531
|
+
}
|
|
81
532
|
console.log(JSON.stringify(result, null, 2));
|
|
82
533
|
return;
|
|
83
534
|
}
|
|
84
|
-
case "
|
|
535
|
+
case "claim": {
|
|
85
536
|
const client = new APAHttpClient({ apiBase });
|
|
86
|
-
const
|
|
87
|
-
const
|
|
537
|
+
const claimCode = readString(args, "claim-code");
|
|
538
|
+
const claimURL = readString(args, "claim-url");
|
|
539
|
+
const code = claimCode || (claimURL ? claimCodeFromUrl(claimURL) : "");
|
|
540
|
+
if (!code) {
|
|
541
|
+
throw new Error("claim_code_required (--claim-code or --claim-url)");
|
|
542
|
+
}
|
|
543
|
+
const result = await client.claimByCode(code);
|
|
88
544
|
console.log(JSON.stringify(result, null, 2));
|
|
89
545
|
return;
|
|
90
546
|
}
|
|
91
547
|
case "me": {
|
|
92
548
|
const client = new APAHttpClient({ apiBase });
|
|
93
|
-
const apiKey =
|
|
549
|
+
const apiKey = await requireApiKey(apiBase);
|
|
94
550
|
const result = await client.getAgentMe(apiKey);
|
|
95
551
|
console.log(JSON.stringify(result, null, 2));
|
|
96
552
|
return;
|
|
97
553
|
}
|
|
98
554
|
case "bind-key": {
|
|
99
555
|
const client = new APAHttpClient({ apiBase });
|
|
100
|
-
const apiKey =
|
|
556
|
+
const apiKey = await requireApiKey(apiBase);
|
|
101
557
|
const provider = requireArg("--provider", readString(args, "provider"));
|
|
102
558
|
const vendorKey = requireArg("--vendor-key", readString(args, "vendor-key"));
|
|
103
559
|
const budgetUsd = readNumber(args, "budget-usd");
|
|
@@ -105,42 +561,13 @@ async function run() {
|
|
|
105
561
|
console.log(JSON.stringify(result, null, 2));
|
|
106
562
|
return;
|
|
107
563
|
}
|
|
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
564
|
case "doctor": {
|
|
137
565
|
const major = Number(process.versions.node.split(".")[0]);
|
|
138
566
|
const client = new APAHttpClient({ apiBase });
|
|
139
567
|
const report = {
|
|
140
568
|
node: process.versions.node,
|
|
141
569
|
node_ok: major >= 20,
|
|
142
|
-
api_base: apiBase
|
|
143
|
-
ws_url: wsUrl
|
|
570
|
+
api_base: apiBase
|
|
144
571
|
};
|
|
145
572
|
try {
|
|
146
573
|
report.healthz = await client.healthz();
|
|
@@ -148,29 +575,19 @@ async function run() {
|
|
|
148
575
|
catch (err) {
|
|
149
576
|
report.healthz_error = err instanceof Error ? err.message : String(err);
|
|
150
577
|
}
|
|
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
578
|
console.log(JSON.stringify(report, null, 2));
|
|
166
579
|
return;
|
|
167
580
|
}
|
|
581
|
+
case "next-decision": {
|
|
582
|
+
await runNextDecision(args);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
case "submit-decision": {
|
|
586
|
+
await runSubmitDecision(args);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
168
589
|
default:
|
|
169
590
|
printHelp();
|
|
170
591
|
throw new Error(`unknown command: ${command}`);
|
|
171
592
|
}
|
|
172
593
|
}
|
|
173
|
-
run().catch((err) => {
|
|
174
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
175
|
-
process.exit(1);
|
|
176
|
-
});
|