@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 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":"noop","reason":"table_closing",...}`.
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
- 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) => {
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: "noop",
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: "noop",
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: "noop",
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
- await saveDecisionState({
423
- session_id: sessionId,
424
- stream_url: streamURL,
425
- last_event_id: newLastEventId,
426
- last_turn_id: "",
427
- pending_decision: pendingDecision
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 noop when table is closing", async () => {
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, "noop");
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 agentID = typeof obj.agent_id === "string" ? obj.agent_id : "";
7
- const apiKey = typeof obj.api_key === "string" ? obj.api_key : "";
8
- const agentName = typeof obj.name === "string" && obj.name.trim() ? obj.name : fallbackName;
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
- agent_id: "agent_123",
7
- api_key: "apa_123",
8
- name: "BotX"
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
- agent_id: "agent_abc",
18
- api_key: "apa_abc"
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
- api_key: "apa_missing_agent"
27
+ agent: {
28
+ api_key: "apa_missing_agent"
29
+ }
24
30
  }, "http://localhost:8080/api", "FallbackBot");
25
31
  assert.equal(bad, null);
26
32
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apa-network/agent-sdk",
3
- "version": "0.2.0-beta.12",
3
+ "version": "0.2.0-beta.14",
4
4
  "description": "APA Agent SDK and CLI",
5
5
  "type": "module",
6
6
  "bin": {