@apa-network/agent-sdk 0.2.0-beta.12 → 0.2.0-beta.14
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 +1 -1
- package/dist/cli.js +31 -21
- package/dist/cli.next_decision.e2e.test.js +57 -2
- package/dist/commands/register.js +7 -3
- package/dist/commands/register.test.js +12 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ apa-bot submit-decision --decision-id <decision_id> --action raise --amount 300
|
|
|
88
88
|
- rejects out-of-range amounts (`amount_out_of_range`)
|
|
89
89
|
|
|
90
90
|
Runtime disconnect handling:
|
|
91
|
-
- If `next-decision` receives `reconnect_grace_started`, it emits `{"type":"
|
|
91
|
+
- If `next-decision` receives `reconnect_grace_started`, it emits `{"type":"table_closing","reason":"table_closing",...}`.
|
|
92
92
|
- If `next-decision` receives `table_closed`/`session_closed`, it emits `{"type":"table_closed",...}` and clears local session state.
|
|
93
93
|
- If `submit-decision` returns `table_closing` or `opponent_disconnected`, CLI emits `{"type":"table_closing",...}` and clears pending decision.
|
|
94
94
|
- If `submit-decision` returns `table_closed`, CLI emits `{"type":"table_closed",...}` and clears pending decision.
|
package/dist/cli.js
CHANGED
|
@@ -246,16 +246,22 @@ async function ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId)
|
|
|
246
246
|
}
|
|
247
247
|
const balance = Number(me?.balance_cc ?? 0);
|
|
248
248
|
const rooms = await client.listPublicRooms();
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
249
|
+
if (joinMode === "select") {
|
|
250
|
+
const pickedRoom = pickRoom(rooms.items || [], joinMode, roomId);
|
|
251
|
+
if (balance < pickedRoom.min_buyin_cc) {
|
|
252
|
+
throw new Error(`insufficient_balance (balance=${balance}, min=${pickedRoom.min_buyin_cc})`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
const lowestRoom = pickRoom(rooms.items || [], "random");
|
|
257
|
+
if (balance < lowestRoom.min_buyin_cc) {
|
|
258
|
+
throw new Error(`insufficient_balance (balance=${balance}, min=${lowestRoom.min_buyin_cc})`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const sessionInput = joinMode === "select"
|
|
262
|
+
? { agentID: agentId, apiKey, joinMode: "select", roomID: roomId }
|
|
263
|
+
: { agentID: agentId, apiKey, joinMode: "random" };
|
|
264
|
+
const session = await client.createSession(sessionInput).catch(async (err) => {
|
|
259
265
|
const recovered = recoverSessionFromConflict(err, apiBase);
|
|
260
266
|
if (!recovered) {
|
|
261
267
|
throw err;
|
|
@@ -302,6 +308,7 @@ async function runNextDecision(args) {
|
|
|
302
308
|
let decided = false;
|
|
303
309
|
let newLastEventId = lastEventId;
|
|
304
310
|
let pendingDecision;
|
|
311
|
+
let stateCleared = false;
|
|
305
312
|
try {
|
|
306
313
|
newLastEventId = await parseSSEOnce(streamURL, lastEventId, timeoutMs, async (evt) => {
|
|
307
314
|
let envelope;
|
|
@@ -321,13 +328,14 @@ async function runNextDecision(args) {
|
|
|
321
328
|
last_turn_id: "",
|
|
322
329
|
pending_decision: undefined
|
|
323
330
|
});
|
|
331
|
+
stateCleared = true;
|
|
324
332
|
emit({ type: "table_closed", session_id: sessionId, reason: data?.reason || "table_closed" });
|
|
325
333
|
decided = true;
|
|
326
334
|
return true;
|
|
327
335
|
}
|
|
328
336
|
if (evType === "reconnect_grace_started") {
|
|
329
337
|
emit({
|
|
330
|
-
type: "
|
|
338
|
+
type: "table_closing",
|
|
331
339
|
reason: "table_closing",
|
|
332
340
|
event: evType,
|
|
333
341
|
session_id: sessionId,
|
|
@@ -339,7 +347,7 @@ async function runNextDecision(args) {
|
|
|
339
347
|
}
|
|
340
348
|
if (evType === "opponent_forfeited") {
|
|
341
349
|
emit({
|
|
342
|
-
type: "
|
|
350
|
+
type: "table_closing",
|
|
343
351
|
reason: "table_closing",
|
|
344
352
|
event: evType,
|
|
345
353
|
session_id: sessionId
|
|
@@ -353,7 +361,7 @@ async function runNextDecision(args) {
|
|
|
353
361
|
const tableStatus = String(data?.table_status || "active");
|
|
354
362
|
if (tableStatus === "closing") {
|
|
355
363
|
emit({
|
|
356
|
-
type: "
|
|
364
|
+
type: "table_closing",
|
|
357
365
|
reason: "table_closing",
|
|
358
366
|
event: evType,
|
|
359
367
|
session_id: sessionId,
|
|
@@ -371,6 +379,7 @@ async function runNextDecision(args) {
|
|
|
371
379
|
last_turn_id: "",
|
|
372
380
|
pending_decision: undefined
|
|
373
381
|
});
|
|
382
|
+
stateCleared = true;
|
|
374
383
|
emit({
|
|
375
384
|
type: "table_closed",
|
|
376
385
|
session_id: sessionId,
|
|
@@ -415,17 +424,18 @@ async function runNextDecision(args) {
|
|
|
415
424
|
});
|
|
416
425
|
}
|
|
417
426
|
catch (err) {
|
|
418
|
-
emit({ type: "error", error: err instanceof Error ? err.message : String(err) });
|
|
419
427
|
throw err;
|
|
420
428
|
}
|
|
421
429
|
finally {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
430
|
+
if (!stateCleared) {
|
|
431
|
+
await saveDecisionState({
|
|
432
|
+
session_id: sessionId,
|
|
433
|
+
stream_url: streamURL,
|
|
434
|
+
last_event_id: newLastEventId,
|
|
435
|
+
last_turn_id: "",
|
|
436
|
+
pending_decision: pendingDecision
|
|
437
|
+
});
|
|
438
|
+
}
|
|
429
439
|
}
|
|
430
440
|
if (!decided) {
|
|
431
441
|
emit({ type: "noop" });
|
|
@@ -127,6 +127,9 @@ test("next-decision creates session, emits decision_request (without protocol fi
|
|
|
127
127
|
assert.deepEqual(state.pending_decision?.action_constraints, { bet: { min: 100, max: 1200 } });
|
|
128
128
|
const created = calls.find((c) => c.url === `${apiBase}/agent/sessions` && c.method === "POST");
|
|
129
129
|
assert.ok(created);
|
|
130
|
+
const createdPayload = JSON.parse(String(created?.body || "{}"));
|
|
131
|
+
assert.equal(createdPayload.join_mode, "random");
|
|
132
|
+
assert.equal(createdPayload.room_id, undefined);
|
|
130
133
|
});
|
|
131
134
|
});
|
|
132
135
|
test("next-decision recovers from 409 and reuses existing session from response body", async () => {
|
|
@@ -362,7 +365,7 @@ test("submit-decision blocks out-of-range amount locally", async () => {
|
|
|
362
365
|
assert.equal(actionCall, undefined);
|
|
363
366
|
});
|
|
364
367
|
});
|
|
365
|
-
test("next-decision emits
|
|
368
|
+
test("next-decision emits table_closing when table is closing", async () => {
|
|
366
369
|
await withTempCwd(async () => {
|
|
367
370
|
const apiBase = "http://mock.local/api";
|
|
368
371
|
await saveCredential({
|
|
@@ -406,10 +409,62 @@ test("next-decision emits noop when table is closing", async () => {
|
|
|
406
409
|
.filter(Boolean)
|
|
407
410
|
.map((line) => JSON.parse(line));
|
|
408
411
|
assert.equal(messages.length, 1);
|
|
409
|
-
assert.equal(messages[0]?.type, "
|
|
412
|
+
assert.equal(messages[0]?.type, "table_closing");
|
|
410
413
|
assert.equal(messages[0]?.reason, "table_closing");
|
|
411
414
|
});
|
|
412
415
|
});
|
|
416
|
+
test("next-decision keeps decision state cleared after table_closed", async () => {
|
|
417
|
+
await withTempCwd(async () => {
|
|
418
|
+
const apiBase = "http://mock.local/api";
|
|
419
|
+
await saveCredential({
|
|
420
|
+
api_base: apiBase,
|
|
421
|
+
agent_name: "BotA",
|
|
422
|
+
agent_id: "agent_1",
|
|
423
|
+
api_key: "apa_1"
|
|
424
|
+
});
|
|
425
|
+
const originalFetch = globalThis.fetch;
|
|
426
|
+
const stdout = captureStdout();
|
|
427
|
+
globalThis.fetch = (async (input, init) => {
|
|
428
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
429
|
+
const method = init?.method || "GET";
|
|
430
|
+
if (url === `${apiBase}/agents/me` && method === "GET") {
|
|
431
|
+
return jsonResponse({ status: "claimed", balance_cc: 10000 });
|
|
432
|
+
}
|
|
433
|
+
if (url === `${apiBase}/public/rooms` && method === "GET") {
|
|
434
|
+
return jsonResponse({ items: [{ id: "room_low", min_buyin_cc: 1000, name: "Low" }] });
|
|
435
|
+
}
|
|
436
|
+
if (url === `${apiBase}/agent/sessions` && method === "POST") {
|
|
437
|
+
return jsonResponse({ session_id: "sess_closed", stream_url: "/api/agent/sessions/sess_closed/events" });
|
|
438
|
+
}
|
|
439
|
+
if (url === `${apiBase}/agent/sessions/sess_closed/events` && method === "GET") {
|
|
440
|
+
return sseResponse([
|
|
441
|
+
"id: 650\nevent: message\ndata: {\"event\":\"table_closed\",\"data\":{\"reason\":\"opponent_reconnect_timeout\"}}\n\n"
|
|
442
|
+
]);
|
|
443
|
+
}
|
|
444
|
+
throw new Error(`unexpected fetch: ${method} ${url}`);
|
|
445
|
+
});
|
|
446
|
+
try {
|
|
447
|
+
await runCLI(["next-decision", "--api-base", apiBase, "--join", "random", "--timeout-ms", "2000"]);
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
globalThis.fetch = originalFetch;
|
|
451
|
+
stdout.restore();
|
|
452
|
+
}
|
|
453
|
+
const messages = stdout.writes
|
|
454
|
+
.join("")
|
|
455
|
+
.split("\n")
|
|
456
|
+
.map((line) => line.trim())
|
|
457
|
+
.filter(Boolean)
|
|
458
|
+
.map((line) => JSON.parse(line));
|
|
459
|
+
assert.equal(messages.length, 1);
|
|
460
|
+
assert.equal(messages[0]?.type, "table_closed");
|
|
461
|
+
const state = await loadDecisionState();
|
|
462
|
+
assert.equal(state.session_id, "");
|
|
463
|
+
assert.equal(state.stream_url, "");
|
|
464
|
+
assert.equal(state.last_event_id, "");
|
|
465
|
+
assert.equal(state.pending_decision, undefined);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
413
468
|
test("submit-decision emits table_closing and clears pending decision", async () => {
|
|
414
469
|
await withTempCwd(async () => {
|
|
415
470
|
const apiBase = "http://mock.local/api";
|
|
@@ -3,9 +3,13 @@ export function buildCredentialFromRegisterResult(result, apiBase, fallbackName)
|
|
|
3
3
|
if (!obj) {
|
|
4
4
|
return null;
|
|
5
5
|
}
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const agent = (obj.agent && typeof obj.agent === "object" ? obj.agent : null);
|
|
7
|
+
if (!agent) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const agentID = typeof agent.agent_id === "string" ? agent.agent_id : "";
|
|
11
|
+
const apiKey = typeof agent.api_key === "string" ? agent.api_key : "";
|
|
12
|
+
const agentName = typeof agent.name === "string" && agent.name.trim() ? agent.name : fallbackName;
|
|
9
13
|
if (!agentID || !apiKey || !agentName) {
|
|
10
14
|
return null;
|
|
11
15
|
}
|
|
@@ -3,9 +3,11 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
import { buildCredentialFromRegisterResult } from "./register.js";
|
|
4
4
|
test("buildCredentialFromRegisterResult returns credential when response has required fields", () => {
|
|
5
5
|
const out = buildCredentialFromRegisterResult({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
agent: {
|
|
7
|
+
agent_id: "agent_123",
|
|
8
|
+
api_key: "apa_123",
|
|
9
|
+
name: "BotX"
|
|
10
|
+
}
|
|
9
11
|
}, "http://localhost:8080/api", "FallbackBot");
|
|
10
12
|
assert.ok(out);
|
|
11
13
|
assert.equal(out?.agent_id, "agent_123");
|
|
@@ -14,13 +16,17 @@ test("buildCredentialFromRegisterResult returns credential when response has req
|
|
|
14
16
|
});
|
|
15
17
|
test("buildCredentialFromRegisterResult falls back to CLI name and rejects incomplete payload", () => {
|
|
16
18
|
const fallback = buildCredentialFromRegisterResult({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
agent: {
|
|
20
|
+
agent_id: "agent_abc",
|
|
21
|
+
api_key: "apa_abc"
|
|
22
|
+
}
|
|
19
23
|
}, "http://localhost:8080/api", "FallbackBot");
|
|
20
24
|
assert.ok(fallback);
|
|
21
25
|
assert.equal(fallback?.agent_name, "FallbackBot");
|
|
22
26
|
const bad = buildCredentialFromRegisterResult({
|
|
23
|
-
|
|
27
|
+
agent: {
|
|
28
|
+
api_key: "apa_missing_agent"
|
|
29
|
+
}
|
|
24
30
|
}, "http://localhost:8080/api", "FallbackBot");
|
|
25
31
|
assert.equal(bad, null);
|
|
26
32
|
});
|