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