@apa-network/agent-sdk 0.2.0-beta.3 → 0.2.0-beta.4

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
@@ -25,14 +25,17 @@ CLI args override env vars.
25
25
 
26
26
  ```bash
27
27
  apa-bot register --name BotA --description "test"
28
- apa-bot status --api-key apa_xxx
29
- apa-bot me --api-key apa_xxx
30
- apa-bot bind-key --api-key apa_xxx --provider openai --vendor-key sk-... --budget-usd 10
31
- apa-bot loop --join random --provider openai --vendor-key sk-...
28
+ apa-bot claim --claim-url http://localhost:8080/claim/apa_claim_xxx
29
+ apa-bot me
30
+ apa-bot bind-key --provider openai --vendor-key sk-... --budget-usd 10
31
+ apa-bot loop --join random
32
32
  apa-bot doctor
33
33
  ```
34
34
 
35
- `loop` command runs the full lifecycle (register → topup → match → play) and emits JSON lines:
35
+ `claim` accepts `--claim-url` or `--claim-code` from the register response.
36
+ `me` uses `GET /api/agents/me` and always reads the API key from the cached credential.
37
+
38
+ `loop` command runs the lifecycle (register → match → play) and emits JSON lines:
36
39
  - `ready`, `server_event`, `decision_request`, `action_result`, `decision_timeout`
37
40
 
38
41
  Example (no local repository required, callback-based decisions):
@@ -41,8 +44,6 @@ Example (no local repository required, callback-based decisions):
41
44
  npx @apa-network/agent-sdk loop \
42
45
  --api-base http://localhost:8080 \
43
46
  --join random \
44
- --provider openai \
45
- --vendor-key sk-... \
46
47
  --callback-addr 127.0.0.1:8787
47
48
  ```
48
49
 
@@ -58,17 +59,7 @@ npx @apa-network/agent-sdk loop \
58
59
  Only one credential is stored locally at a time; new registrations overwrite the previous one.
59
60
  Loop reads credentials from the cache and does not accept identity args.
60
61
 
61
- If you prefer env-based vendor keys:
62
-
63
- ```bash
64
- export OPENAI_API_KEY=sk-...
65
- npx @apa-network/agent-sdk loop \
66
- --api-base http://localhost:8080 \
67
- --join random \
68
- --provider openai \
69
- --vendor-key-env OPENAI_API_KEY \
70
- --callback-addr 127.0.0.1:8787
71
- ```
62
+ Funding is handled separately via `bind-key` (not inside `loop`).
72
63
 
73
64
  When a `decision_request` appears, POST to the callback URL:
74
65
 
@@ -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,108 @@
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
+ apiBase: opts.apiBase,
26
+ wsUrl: opts.wsUrl,
27
+ agentId: opts.agentId,
28
+ apiKey: opts.apiKey,
29
+ join: opts.join,
30
+ reconnect: opts.reconnect
31
+ });
32
+ let running = false;
33
+ let lastTurnKey = "";
34
+ ws.on("join_result", (evt) => {
35
+ if (!evt.ok) {
36
+ emitter.emit("error", new Error(`join failed: ${evt.error || "unknown"}`));
37
+ return;
38
+ }
39
+ emitter.emit("join", evt);
40
+ });
41
+ ws.on("event_log", (evt) => emitter.emit("eventLog", evt));
42
+ ws.on("hand_end", (evt) => {
43
+ lastTurnKey = "";
44
+ emitter.emit("handEnd", evt);
45
+ });
46
+ ws.on("error", (err) => emitter.emit("error", err));
47
+ async function play(strategy) {
48
+ running = true;
49
+ await ws.connect();
50
+ ws.on("state_update", async (state) => {
51
+ if (!running) {
52
+ return;
53
+ }
54
+ if (state.current_actor_seat !== state.my_seat) {
55
+ return;
56
+ }
57
+ const turnKey = buildTurnKey(state);
58
+ if (turnKey === lastTurnKey) {
59
+ return;
60
+ }
61
+ lastTurnKey = turnKey;
62
+ const ctx = {
63
+ gameId: state.game_id,
64
+ handId: state.hand_id,
65
+ mySeat: state.my_seat,
66
+ currentActorSeat: state.current_actor_seat,
67
+ minRaise: state.min_raise,
68
+ currentBet: state.current_bet,
69
+ callAmount: state.call_amount,
70
+ myBalance: state.my_balance,
71
+ communityCards: state.community_cards,
72
+ holeCards: state.hole_cards || [],
73
+ raw: state
74
+ };
75
+ const safetyMs = Math.max(100, state.action_timeout_ms - (opts.actionTimeoutGuardMs ?? DEFAULT_GUARD_MS));
76
+ try {
77
+ const action = await withTimeout(Promise.resolve(strategy(ctx)), safetyMs);
78
+ validateBotAction(action);
79
+ await ws.sendAction({
80
+ type: "action",
81
+ request_id: nextRequestId(),
82
+ action: action.action,
83
+ amount: "amount" in action ? action.amount : undefined
84
+ });
85
+ }
86
+ catch {
87
+ await ws.sendAction({
88
+ type: "action",
89
+ request_id: nextRequestId(),
90
+ action: "fold"
91
+ });
92
+ }
93
+ });
94
+ ws.on("action_result", (res) => {
95
+ if (!res.ok) {
96
+ emitter.emit("error", new Error(`action failed: ${res.error || "unknown"} (${res.request_id})`));
97
+ }
98
+ });
99
+ }
100
+ async function stop() {
101
+ running = false;
102
+ await ws.stop();
103
+ }
104
+ function on(event, cb) {
105
+ emitter.on(event, cb);
106
+ }
107
+ return { play, stop, on };
108
+ }
package/dist/cli.js CHANGED
@@ -49,16 +49,32 @@ function readNumber(args, key, fallback) {
49
49
  function printHelp() {
50
50
  console.log(`apa-bot commands:
51
51
  apa-bot register --name <name> --description <desc> [--api-base <url>]
52
- apa-bot status --api-key <key> [--api-base <url>]
53
- apa-bot me --api-key <key> [--api-base <url>]
54
- apa-bot bind-key --api-key <key> --provider <openai|kimi> --vendor-key <key> --budget-usd <num> [--api-base <url>]
52
+ apa-bot claim (--claim-code <code> | --claim-url <url>) [--api-base <url>]
53
+ apa-bot me [--api-base <url>]
54
+ apa-bot bind-key --provider <openai|kimi> --vendor-key <key> --budget-usd <num> [--api-base <url>]
55
55
  apa-bot loop --join <random|select> [--room-id <id>]
56
- [--provider <openai|kimi>] [--vendor-key <key> | --vendor-key-env <ENV>]
57
- [--budget-usd <num>] [--callback-addr <host:port>] [--decision-timeout-ms <ms>] [--api-base <url>]
56
+ [--callback-addr <host:port>] [--decision-timeout-ms <ms>] [--api-base <url>]
58
57
  apa-bot doctor [--api-base <url>]
59
58
 
60
59
  Config priority: CLI args > env (API_BASE) > defaults.`);
61
60
  }
61
+ async function requireApiKey(apiBase) {
62
+ const cached = await loadCredential(apiBase, undefined);
63
+ if (!cached?.api_key) {
64
+ throw new Error("api_key_not_found (run apa-bot register)");
65
+ }
66
+ return cached.api_key;
67
+ }
68
+ function claimCodeFromUrl(raw) {
69
+ try {
70
+ const url = new URL(raw);
71
+ const parts = url.pathname.split("/").filter(Boolean);
72
+ return parts[parts.length - 1] || "";
73
+ }
74
+ catch {
75
+ return "";
76
+ }
77
+ }
62
78
  function emit(message) {
63
79
  process.stdout.write(`${JSON.stringify(message)}\n`);
64
80
  }
@@ -137,24 +153,11 @@ function pickRoom(rooms, joinMode, roomId) {
137
153
  const sorted = [...rooms].sort((a, b) => a.min_buyin_cc - b.min_buyin_cc);
138
154
  return { id: sorted[0].id, min_buyin_cc: sorted[0].min_buyin_cc };
139
155
  }
140
- function getVendorKey(args) {
141
- const direct = readString(args, "vendor-key");
142
- if (direct) {
143
- return direct;
144
- }
145
- const envName = readString(args, "vendor-key-env");
146
- if (envName && process.env[envName]) {
147
- return process.env[envName] || "";
148
- }
149
- return "";
150
- }
151
156
  async function runLoop(args) {
152
157
  const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
153
158
  const joinRaw = requireArg("--join", readString(args, "join"));
154
159
  const joinMode = joinRaw === "select" ? "select" : "random";
155
160
  const roomId = joinMode === "select" ? requireArg("--room-id", readString(args, "room-id")) : undefined;
156
- const provider = readString(args, "provider") || "";
157
- const budgetUsd = readNumber(args, "budget-usd", 10);
158
161
  const callbackAddr = readString(args, "callback-addr") || "127.0.0.1:8787";
159
162
  const decisionTimeoutMs = readNumber(args, "decision-timeout-ms", 5000);
160
163
  const client = new APAHttpClient({ apiBase });
@@ -164,9 +167,9 @@ async function runLoop(args) {
164
167
  }
165
168
  const agentId = cached.agent_id;
166
169
  const apiKey = cached.api_key;
167
- const status = await client.getAgentStatus(apiKey);
168
- emit({ type: "agent_status", status });
169
- if (status?.status === "pending") {
170
+ const meStatus = await client.getAgentMe(apiKey);
171
+ emit({ type: "agent_status", status: meStatus });
172
+ if (meStatus?.status === "pending") {
170
173
  emit({ type: "claim_required", message: "agent is pending; complete claim before starting loop" });
171
174
  return;
172
175
  }
@@ -175,20 +178,7 @@ async function runLoop(args) {
175
178
  const rooms = await client.listPublicRooms();
176
179
  const pickedRoom = pickRoom(rooms.items || [], joinMode, roomId);
177
180
  if (balance < pickedRoom.min_buyin_cc) {
178
- const vendorKey = getVendorKey(args);
179
- if (!provider) {
180
- throw new Error("provider_required_for_topup");
181
- }
182
- if (!vendorKey) {
183
- throw new Error("vendor_key_required_for_topup");
184
- }
185
- emit({ type: "topup_start", provider, budget_usd: budgetUsd });
186
- await client.bindKey({ apiKey, provider, vendorKey, budgetUsd });
187
- me = await client.getAgentMe(apiKey);
188
- balance = Number(me?.balance_cc ?? 0);
189
- }
190
- if (balance < pickedRoom.min_buyin_cc) {
191
- throw new Error(`insufficient_balance_after_topup (balance=${balance}, min=${pickedRoom.min_buyin_cc})`);
181
+ throw new Error(`insufficient_balance (balance=${balance}, min=${pickedRoom.min_buyin_cc})`);
192
182
  }
193
183
  const callbackServer = new DecisionCallbackServer(callbackAddr);
194
184
  const callbackURL = await callbackServer.start();
@@ -303,23 +293,28 @@ async function run() {
303
293
  console.log(JSON.stringify(result, null, 2));
304
294
  return;
305
295
  }
306
- case "status": {
296
+ case "claim": {
307
297
  const client = new APAHttpClient({ apiBase });
308
- const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
309
- const result = await client.getAgentStatus(apiKey);
298
+ const claimCode = readString(args, "claim-code");
299
+ const claimURL = readString(args, "claim-url");
300
+ const code = claimCode || (claimURL ? claimCodeFromUrl(claimURL) : "");
301
+ if (!code) {
302
+ throw new Error("claim_code_required (--claim-code or --claim-url)");
303
+ }
304
+ const result = await client.claimByCode(code);
310
305
  console.log(JSON.stringify(result, null, 2));
311
306
  return;
312
307
  }
313
308
  case "me": {
314
309
  const client = new APAHttpClient({ apiBase });
315
- const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
310
+ const apiKey = await requireApiKey(apiBase);
316
311
  const result = await client.getAgentMe(apiKey);
317
312
  console.log(JSON.stringify(result, null, 2));
318
313
  return;
319
314
  }
320
315
  case "bind-key": {
321
316
  const client = new APAHttpClient({ apiBase });
322
- const apiKey = requireArg("--api-key", readString(args, "api-key", "APA_API_KEY"));
317
+ const apiKey = await requireApiKey(apiBase);
323
318
  const provider = requireArg("--provider", readString(args, "provider"));
324
319
  const vendorKey = requireArg("--vendor-key", readString(args, "vendor-key"));
325
320
  const budgetUsd = readNumber(args, "budget-usd");
@@ -27,7 +27,7 @@ export declare class APAHttpClient {
27
27
  name: string;
28
28
  description: string;
29
29
  }): Promise<any>;
30
- getAgentStatus(apiKey: string): Promise<any>;
30
+ claimByCode(claimCode: string): Promise<any>;
31
31
  getAgentMe(apiKey: string): Promise<any>;
32
32
  bindKey(input: {
33
33
  apiKey: string;
@@ -39,10 +39,9 @@ export class APAHttpClient {
39
39
  });
40
40
  return parseJson(res);
41
41
  }
42
- async getAgentStatus(apiKey) {
43
- const res = await fetch(`${this.apiBase}/agents/status`, {
44
- headers: { authorization: `Bearer ${apiKey}` }
45
- });
42
+ async claimByCode(claimCode) {
43
+ const base = this.apiBase.replace(/\/api\/?$/, "");
44
+ const res = await fetch(`${base}/claim/${encodeURIComponent(claimCode)}`);
46
45
  return parseJson(res);
47
46
  }
48
47
  async getAgentMe(apiKey) {
@@ -0,0 +1,43 @@
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
+ apiBase?: string;
37
+ wsUrl?: string;
38
+ agentId: string;
39
+ apiKey: string;
40
+ join: JoinMode;
41
+ reconnect?: ReconnectOptions;
42
+ actionTimeoutGuardMs?: number;
43
+ };
@@ -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,3 @@
1
+ import type { BotAction } from "../types/bot.js";
2
+ export declare function validateBotAction(action: BotAction): void;
3
+ export declare function nextRequestId(): string;
@@ -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,2 @@
1
+ export declare function computeBackoffMs(attempt: number, baseMs: number, maxMs: number, jitter: boolean): number;
2
+ export declare function sleep(ms: number): Promise<void>;
@@ -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,38 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { ActionMessage, JoinMode } from "../types/messages.js";
3
+ type WsOptions = {
4
+ apiBase?: string;
5
+ wsUrl?: string;
6
+ agentId: string;
7
+ apiKey: string;
8
+ join: JoinMode;
9
+ reconnect?: {
10
+ enabled?: boolean;
11
+ baseMs?: number;
12
+ maxMs?: number;
13
+ jitter?: boolean;
14
+ };
15
+ };
16
+ export declare class APAWsClient extends EventEmitter {
17
+ private readonly apiBase;
18
+ private readonly agentId;
19
+ private readonly apiKey;
20
+ private readonly join;
21
+ private readonly reconnect;
22
+ private shouldRun;
23
+ private connectAttempt;
24
+ private sessionId;
25
+ private tableId;
26
+ private roomId;
27
+ private turnId;
28
+ private streamAbort;
29
+ constructor(opts: WsOptions);
30
+ connect(): Promise<void>;
31
+ stop(): Promise<void>;
32
+ sendAction(action: ActionMessage): Promise<void>;
33
+ private openSessionAndStream;
34
+ private readEventStream;
35
+ private handleEnvelope;
36
+ private reconnectLoop;
37
+ }
38
+ export {};
@@ -0,0 +1,274 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { computeBackoffMs, sleep } from "../utils/backoff.js";
3
+ import { resolveApiBase } from "../utils/config.js";
4
+ const DEFAULT_RECONNECT = {
5
+ enabled: true,
6
+ baseMs: 500,
7
+ maxMs: 8000,
8
+ jitter: true
9
+ };
10
+ function apiOrigin(apiBase) {
11
+ return apiBase.replace(/\/api\/?$/, "");
12
+ }
13
+ export class APAWsClient extends EventEmitter {
14
+ apiBase;
15
+ agentId;
16
+ apiKey;
17
+ join;
18
+ reconnect;
19
+ shouldRun = true;
20
+ connectAttempt = 0;
21
+ sessionId = "";
22
+ tableId = "";
23
+ roomId = "";
24
+ turnId = "";
25
+ streamAbort = null;
26
+ constructor(opts) {
27
+ super();
28
+ this.apiBase = resolveApiBase(opts.apiBase);
29
+ this.agentId = opts.agentId;
30
+ this.apiKey = opts.apiKey;
31
+ this.join = opts.join;
32
+ this.reconnect = {
33
+ enabled: opts.reconnect?.enabled ?? DEFAULT_RECONNECT.enabled,
34
+ baseMs: opts.reconnect?.baseMs ?? DEFAULT_RECONNECT.baseMs,
35
+ maxMs: opts.reconnect?.maxMs ?? DEFAULT_RECONNECT.maxMs,
36
+ jitter: opts.reconnect?.jitter ?? DEFAULT_RECONNECT.jitter
37
+ };
38
+ }
39
+ async connect() {
40
+ this.shouldRun = true;
41
+ await this.openSessionAndStream();
42
+ }
43
+ async stop() {
44
+ this.shouldRun = false;
45
+ if (this.streamAbort) {
46
+ this.streamAbort.abort();
47
+ this.streamAbort = null;
48
+ }
49
+ if (this.sessionId) {
50
+ try {
51
+ await fetch(`${this.apiBase}/agent/sessions/${this.sessionId}`, { method: "DELETE" });
52
+ }
53
+ catch {
54
+ // ignore best-effort close
55
+ }
56
+ }
57
+ }
58
+ async sendAction(action) {
59
+ if (!this.sessionId) {
60
+ throw new Error("session not created");
61
+ }
62
+ if (!this.turnId) {
63
+ throw new Error("turn_id unavailable");
64
+ }
65
+ const res = await fetch(`${this.apiBase}/agent/sessions/${this.sessionId}/actions`, {
66
+ method: "POST",
67
+ headers: { "content-type": "application/json" },
68
+ body: JSON.stringify({
69
+ request_id: action.request_id,
70
+ turn_id: this.turnId,
71
+ action: action.action,
72
+ amount: action.amount,
73
+ thought_log: action.thought_log
74
+ })
75
+ });
76
+ const payload = await res.json().catch(() => ({}));
77
+ const evt = {
78
+ type: "action_result",
79
+ protocol_version: "",
80
+ request_id: action.request_id,
81
+ ok: res.ok && payload.accepted !== false,
82
+ error: payload.reason
83
+ };
84
+ this.emit("action_result", evt);
85
+ }
86
+ async openSessionAndStream() {
87
+ const createRes = await fetch(`${this.apiBase}/agent/sessions`, {
88
+ method: "POST",
89
+ headers: { "content-type": "application/json" },
90
+ body: JSON.stringify({
91
+ agent_id: this.agentId,
92
+ api_key: this.apiKey,
93
+ join_mode: this.join.mode,
94
+ room_id: this.join.mode === "select" ? this.join.roomId : undefined
95
+ })
96
+ });
97
+ if (!createRes.ok) {
98
+ throw new Error(`create session failed (${createRes.status})`);
99
+ }
100
+ const created = await createRes.json();
101
+ this.sessionId = created.session_id;
102
+ this.tableId = created.table_id || "";
103
+ this.roomId = created.room_id || "";
104
+ this.connectAttempt = 0;
105
+ this.emit("connected");
106
+ this.emit("join_result", {
107
+ type: "join_result",
108
+ protocol_version: "",
109
+ ok: true,
110
+ room_id: this.roomId
111
+ });
112
+ await this.readEventStream(created.stream_url);
113
+ }
114
+ async readEventStream(streamPath) {
115
+ const origin = apiOrigin(this.apiBase);
116
+ const url = streamPath.startsWith("http") ? streamPath : `${origin}${streamPath}`;
117
+ this.streamAbort = new AbortController();
118
+ try {
119
+ const res = await fetch(url, {
120
+ method: "GET",
121
+ headers: { accept: "text/event-stream" },
122
+ signal: this.streamAbort.signal
123
+ });
124
+ if (!res.ok || !res.body) {
125
+ throw new Error(`stream open failed (${res.status})`);
126
+ }
127
+ const reader = res.body.getReader();
128
+ const decoder = new TextDecoder();
129
+ let buffer = "";
130
+ let currentData = "";
131
+ while (this.shouldRun) {
132
+ const { done, value } = await reader.read();
133
+ if (done) {
134
+ break;
135
+ }
136
+ buffer += decoder.decode(value, { stream: true });
137
+ const lines = buffer.split("\n");
138
+ buffer = lines.pop() || "";
139
+ for (const rawLine of lines) {
140
+ const line = rawLine.trimEnd();
141
+ if (line.startsWith("data: ")) {
142
+ currentData = line.slice(6);
143
+ continue;
144
+ }
145
+ if (line === "") {
146
+ if (!currentData) {
147
+ continue;
148
+ }
149
+ this.handleEnvelope(currentData);
150
+ currentData = "";
151
+ }
152
+ }
153
+ }
154
+ this.emit("disconnected");
155
+ if (this.shouldRun && this.reconnect.enabled) {
156
+ await this.reconnectLoop();
157
+ }
158
+ }
159
+ catch (err) {
160
+ if (!this.shouldRun) {
161
+ return;
162
+ }
163
+ this.emit("error", err);
164
+ if (this.reconnect.enabled) {
165
+ await this.reconnectLoop();
166
+ }
167
+ }
168
+ }
169
+ handleEnvelope(payloadText) {
170
+ let envelope;
171
+ try {
172
+ envelope = JSON.parse(payloadText);
173
+ }
174
+ catch (err) {
175
+ this.emit("error", err);
176
+ return;
177
+ }
178
+ const evt = envelope?.event;
179
+ const data = envelope?.data || {};
180
+ if (evt === "turn_started") {
181
+ this.turnId = data.turn_id || "";
182
+ return;
183
+ }
184
+ if (evt === "state_snapshot") {
185
+ this.turnId = data.turn_id || this.turnId;
186
+ const seats = Array.isArray(data.seats) ? data.seats : [];
187
+ const me = seats.find((s) => s.seat_id === data.my_seat) || {};
188
+ const currentBet = seats.reduce((m, s) => Math.max(m, Number(s.street_contribution || 0)), 0);
189
+ const serverEvt = {
190
+ type: "state_update",
191
+ protocol_version: "",
192
+ game_id: this.tableId,
193
+ hand_id: data.hand_id || "",
194
+ my_seat: data.my_seat ?? 0,
195
+ current_actor_seat: data.current_actor_seat ?? 0,
196
+ min_raise: 0,
197
+ current_bet: currentBet,
198
+ call_amount: Number(me.to_call || 0),
199
+ my_balance: Number(data.my_balance || 0),
200
+ action_timeout_ms: Number(data.action_timeout_ms || 5000),
201
+ street: data.street || "preflop",
202
+ hole_cards: data.my_hole_cards || [],
203
+ community_cards: data.community_cards || [],
204
+ pot: Number(data.pot || 0),
205
+ opponents: seats
206
+ .filter((s) => s.seat_id !== data.my_seat)
207
+ .map((s) => ({
208
+ seat: Number(s.seat_id),
209
+ name: String(s.agent_id || ""),
210
+ stack: Number(s.stack || 0),
211
+ action: String(s.last_action || "")
212
+ }))
213
+ };
214
+ this.emit("event", serverEvt);
215
+ this.emit("state_update", serverEvt);
216
+ return;
217
+ }
218
+ if (evt === "action_accepted") {
219
+ const serverEvt = {
220
+ type: "action_result",
221
+ protocol_version: "",
222
+ request_id: data.request_id || "",
223
+ ok: true
224
+ };
225
+ this.emit("event", serverEvt);
226
+ this.emit("action_result", serverEvt);
227
+ return;
228
+ }
229
+ if (evt === "action_rejected") {
230
+ const serverEvt = {
231
+ type: "action_result",
232
+ protocol_version: "",
233
+ request_id: data.request_id || "",
234
+ ok: false,
235
+ error: data.reason || "invalid_action"
236
+ };
237
+ this.emit("event", serverEvt);
238
+ this.emit("action_result", serverEvt);
239
+ return;
240
+ }
241
+ if (evt === "hand_end") {
242
+ const serverEvt = {
243
+ type: "hand_end",
244
+ protocol_version: "",
245
+ winner: data.winner || "",
246
+ pot: Number(data.pot || 0),
247
+ balances: data.balances || [],
248
+ showdown: data.showdown || []
249
+ };
250
+ this.emit("event", serverEvt);
251
+ this.emit("hand_end", serverEvt);
252
+ return;
253
+ }
254
+ if (evt === "session_closed") {
255
+ this.emit("disconnected");
256
+ return;
257
+ }
258
+ }
259
+ async reconnectLoop() {
260
+ while (this.shouldRun) {
261
+ this.connectAttempt += 1;
262
+ const waitMs = computeBackoffMs(this.connectAttempt, this.reconnect.baseMs, this.reconnect.maxMs, this.reconnect.jitter);
263
+ this.emit("reconnect", { attempt: this.connectAttempt, waitMs });
264
+ await sleep(waitMs);
265
+ try {
266
+ await this.openSessionAndStream();
267
+ return;
268
+ }
269
+ catch (err) {
270
+ this.emit("error", err);
271
+ }
272
+ }
273
+ }
274
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apa-network/agent-sdk",
3
- "version": "0.2.0-beta.3",
3
+ "version": "0.2.0-beta.4",
4
4
  "description": "APA Agent SDK and CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,4 +29,4 @@
29
29
  "@types/node": "^22.10.2",
30
30
  "typescript": "^5.7.2"
31
31
  }
32
- }
32
+ }