@apa-network/agent-sdk 0.2.0-beta.8 → 0.2.0-beta.9

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
@@ -40,6 +40,8 @@ apa-bot doctor
40
40
  connection, emits a single `decision_request` if available, and exits.
41
41
  The protocol fields (`request_id`, `turn_id`, callback URL) are stored internally in
42
42
  `decision_state.json` and are not exposed in stdout.
43
+ When available, the response includes server-authoritative `legal_actions` and
44
+ `action_constraints` (bet/raise amount limits).
43
45
 
44
46
  Example (no local repository required, single-step decisions):
45
47
 
@@ -74,6 +76,17 @@ When a `decision_request` appears, submit the chosen action via SDK:
74
76
  apa-bot submit-decision --decision-id <decision_id> --action call --thought-log "safe"
75
77
  ```
76
78
 
79
+ For `bet`/`raise`, include `--amount` within the provided constraints:
80
+
81
+ ```bash
82
+ apa-bot submit-decision --decision-id <decision_id> --action raise --amount 300 --thought-log "value raise"
83
+ ```
84
+
85
+ `submit-decision` performs local hard validation:
86
+ - rejects illegal actions for the current decision (`action_not_legal`)
87
+ - rejects missing amount for `bet`/`raise` (`amount_required_for_bet_or_raise`)
88
+ - rejects out-of-range amounts (`amount_out_of_range`)
89
+
77
90
  ## Publish (beta)
78
91
 
79
92
  ```bash
package/dist/cli.js CHANGED
@@ -82,6 +82,43 @@ function claimCodeFromUrl(raw) {
82
82
  function emit(message) {
83
83
  process.stdout.write(`${JSON.stringify(message)}\n`);
84
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;
121
+ }
85
122
  function parseAction(raw) {
86
123
  if (raw === "fold" || raw === "check" || raw === "call" || raw === "raise" || raw === "bet") {
87
124
  return raw;
@@ -285,21 +322,31 @@ async function runNextDecision(args) {
285
322
  const reqID = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
286
323
  const callbackURL = `${apiBase}/agent/sessions/${sessionId}/actions`;
287
324
  const decisionID = `dec_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
325
+ const legalActions = readLegalActions(data);
326
+ const actionConstraints = readActionConstraints(data);
288
327
  pendingDecision = {
289
328
  decision_id: decisionID,
290
329
  session_id: sessionId,
291
330
  request_id: reqID,
292
331
  turn_id: String(data.turn_id || ""),
293
332
  callback_url: callbackURL,
333
+ legal_actions: legalActions,
334
+ action_constraints: actionConstraints,
294
335
  created_at: new Date().toISOString()
295
336
  };
296
- emit({
337
+ const payload = {
297
338
  type: "decision_request",
298
339
  decision_id: decisionID,
299
340
  session_id: sessionId,
300
- legal_actions: ["fold", "check", "call", "raise", "bet"],
301
341
  state: data
302
- });
342
+ };
343
+ if (legalActions.length > 0) {
344
+ payload.legal_actions = legalActions;
345
+ }
346
+ if (actionConstraints) {
347
+ payload.action_constraints = actionConstraints;
348
+ }
349
+ emit(payload);
303
350
  decided = true;
304
351
  return true;
305
352
  });
@@ -339,6 +386,24 @@ async function runSubmitDecision(args) {
339
386
  if (pending.decision_id !== decisionID) {
340
387
  throw new Error("decision_id_mismatch (run apa-bot next-decision)");
341
388
  }
389
+ const legalActions = pending.legal_actions || [];
390
+ if (legalActions.length > 0 && !legalActions.includes(action)) {
391
+ throw new Error("action_not_legal");
392
+ }
393
+ if ((action === "bet" || action === "raise") && amount === undefined) {
394
+ throw new Error("amount_required_for_bet_or_raise");
395
+ }
396
+ const constraints = pending.action_constraints;
397
+ if (action === "bet" && amount !== undefined && constraints?.bet) {
398
+ if (amount < constraints.bet.min || amount > constraints.bet.max) {
399
+ throw new Error("amount_out_of_range");
400
+ }
401
+ }
402
+ if (action === "raise" && amount !== undefined && constraints?.raise) {
403
+ if (amount < constraints.raise.min_to || amount > constraints.raise.max_to) {
404
+ throw new Error("amount_out_of_range");
405
+ }
406
+ }
342
407
  const client = new APAHttpClient({ apiBase });
343
408
  try {
344
409
  const result = await client.submitAction({
@@ -88,7 +88,8 @@ test("next-decision creates session, emits decision_request (without protocol fi
88
88
  if (url === `${apiBase}/agent/sessions/sess_1/events` && method === "GET") {
89
89
  return sseResponse([
90
90
  "id: 101\nevent: message\ndata: {\"event\":\"state_snapshot\",\"data\":{\"turn_id\":\"turn_1\",",
91
- "\"my_seat\":0,\"current_actor_seat\":0}}\n\n"
91
+ "\"my_seat\":0,\"current_actor_seat\":0,\"legal_actions\":[\"check\",\"bet\"],",
92
+ "\"action_constraints\":{\"bet\":{\"min\":100,\"max\":1200}}}}\n\n"
92
93
  ]);
93
94
  }
94
95
  throw new Error(`unexpected fetch: ${method} ${url}`);
@@ -112,6 +113,8 @@ test("next-decision creates session, emits decision_request (without protocol fi
112
113
  assert.ok(typeof decision?.decision_id === "string");
113
114
  assert.equal(decision?.turn_id, undefined);
114
115
  assert.equal(decision?.callback_url, undefined);
116
+ assert.deepEqual(decision?.legal_actions, ["check", "bet"]);
117
+ assert.deepEqual(decision?.action_constraints, { bet: { min: 100, max: 1200 } });
115
118
  const state = await loadDecisionState();
116
119
  assert.equal(state.session_id, "sess_1");
117
120
  assert.equal(state.stream_url, `${apiBase}/agent/sessions/sess_1/events`);
@@ -120,6 +123,8 @@ test("next-decision creates session, emits decision_request (without protocol fi
120
123
  assert.equal(state.pending_decision?.session_id, "sess_1");
121
124
  assert.equal(state.pending_decision?.callback_url, `${apiBase}/agent/sessions/sess_1/actions`);
122
125
  assert.equal(state.pending_decision?.turn_id, "turn_1");
126
+ assert.deepEqual(state.pending_decision?.legal_actions, ["check", "bet"]);
127
+ assert.deepEqual(state.pending_decision?.action_constraints, { bet: { min: 100, max: 1200 } });
123
128
  const created = calls.find((c) => c.url === `${apiBase}/agent/sessions` && c.method === "POST");
124
129
  assert.ok(created);
125
130
  });
@@ -153,7 +158,8 @@ test("next-decision recovers from 409 and reuses existing session from response
153
158
  }
154
159
  if (url === `${apiBase}/agent/sessions/sess_conflict/events` && method === "GET") {
155
160
  return sseResponse([
156
- "id: 202\nevent: message\ndata: {\"event\":\"state_snapshot\",\"data\":{\"turn_id\":\"turn_2\",\"my_seat\":1,\"current_actor_seat\":1}}\n\n"
161
+ "id: 202\nevent: message\ndata: {\"event\":\"state_snapshot\",\"data\":{\"turn_id\":\"turn_2\",\"my_seat\":1,\"current_actor_seat\":1,",
162
+ "\"legal_actions\":[\"fold\",\"call\",\"raise\"],\"action_constraints\":{\"raise\":{\"min_to\":300,\"max_to\":1500}}}}\n\n"
157
163
  ]);
158
164
  }
159
165
  throw new Error(`unexpected fetch: ${method} ${url}`);
@@ -182,6 +188,8 @@ test("next-decision recovers from 409 and reuses existing session from response
182
188
  assert.equal(state.last_event_id, "202");
183
189
  assert.equal(state.pending_decision?.session_id, "sess_conflict");
184
190
  assert.equal(state.pending_decision?.turn_id, "turn_2");
191
+ assert.deepEqual(state.pending_decision?.legal_actions, ["fold", "call", "raise"]);
192
+ assert.deepEqual(state.pending_decision?.action_constraints, { raise: { min_to: 300, max_to: 1500 } });
185
193
  });
186
194
  });
187
195
  test("submit-decision uses pending decision metadata and clears pending entry", async () => {
@@ -220,7 +228,7 @@ test("submit-decision uses pending decision metadata and clears pending entry",
220
228
  if (url === `${apiBase}/agent/sessions/sess_1/events` && method === "GET") {
221
229
  return sseResponse([
222
230
  "id: 301\nevent: message\ndata: {\"event\":\"state_snapshot\",\"data\":{\"turn_id\":\"turn_3\",",
223
- "\"my_seat\":0,\"current_actor_seat\":0}}\n\n"
231
+ "\"my_seat\":0,\"current_actor_seat\":0,\"legal_actions\":[\"call\"]}}\n\n"
224
232
  ]);
225
233
  }
226
234
  if (url === `${apiBase}/agent/sessions/sess_1/actions` && method === "POST") {
@@ -248,3 +256,109 @@ test("submit-decision uses pending decision metadata and clears pending entry",
248
256
  assert.equal(finalState.pending_decision, undefined);
249
257
  });
250
258
  });
259
+ test("submit-decision blocks bet/raise without amount locally", async () => {
260
+ await withTempCwd(async () => {
261
+ const apiBase = "http://mock.local/api";
262
+ await saveCredential({
263
+ api_base: apiBase,
264
+ agent_name: "BotA",
265
+ agent_id: "agent_1",
266
+ api_key: "apa_1"
267
+ });
268
+ const calls = [];
269
+ const originalFetch = globalThis.fetch;
270
+ const stdout = captureStdout();
271
+ globalThis.fetch = (async (input, init) => {
272
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
273
+ const method = init?.method || "GET";
274
+ calls.push({ url, method, headers: init?.headers, body: typeof init?.body === "string" ? init.body : undefined });
275
+ if (url === `${apiBase}/agents/me` && method === "GET") {
276
+ return jsonResponse({ status: "claimed", balance_cc: 10000 });
277
+ }
278
+ if (url === `${apiBase}/public/rooms` && method === "GET") {
279
+ return jsonResponse({ items: [{ id: "room_low", min_buyin_cc: 1000, name: "Low" }] });
280
+ }
281
+ if (url === `${apiBase}/agent/sessions` && method === "POST") {
282
+ return jsonResponse({ session_id: "sess_2", stream_url: "/api/agent/sessions/sess_2/events" });
283
+ }
284
+ if (url === `${apiBase}/agent/sessions/sess_2/events` && method === "GET") {
285
+ return sseResponse([
286
+ "id: 401\nevent: message\ndata: {\"event\":\"state_snapshot\",\"data\":{\"turn_id\":\"turn_4\",",
287
+ "\"my_seat\":0,\"current_actor_seat\":0,\"legal_actions\":[\"bet\"],\"action_constraints\":{\"bet\":{\"min\":100,\"max\":500}}}}\n\n"
288
+ ]);
289
+ }
290
+ throw new Error(`unexpected fetch: ${method} ${url}`);
291
+ });
292
+ try {
293
+ await runCLI(["next-decision", "--api-base", apiBase, "--join", "random", "--timeout-ms", "2000"]);
294
+ const state = await loadDecisionState();
295
+ const decisionID = state.pending_decision?.decision_id;
296
+ assert.ok(decisionID);
297
+ await assert.rejects(async () => runCLI(["submit-decision", "--api-base", apiBase, "--decision-id", decisionID, "--action", "bet"]), /amount_required_for_bet_or_raise/);
298
+ }
299
+ finally {
300
+ globalThis.fetch = originalFetch;
301
+ stdout.restore();
302
+ }
303
+ const actionCall = calls.find((c) => c.url.includes("/actions") && c.method === "POST");
304
+ assert.equal(actionCall, undefined);
305
+ });
306
+ });
307
+ test("submit-decision blocks out-of-range amount locally", async () => {
308
+ await withTempCwd(async () => {
309
+ const apiBase = "http://mock.local/api";
310
+ await saveCredential({
311
+ api_base: apiBase,
312
+ agent_name: "BotA",
313
+ agent_id: "agent_1",
314
+ api_key: "apa_1"
315
+ });
316
+ const calls = [];
317
+ const originalFetch = globalThis.fetch;
318
+ const stdout = captureStdout();
319
+ globalThis.fetch = (async (input, init) => {
320
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
321
+ const method = init?.method || "GET";
322
+ calls.push({ url, method, headers: init?.headers, body: typeof init?.body === "string" ? init.body : undefined });
323
+ if (url === `${apiBase}/agents/me` && method === "GET") {
324
+ return jsonResponse({ status: "claimed", balance_cc: 10000 });
325
+ }
326
+ if (url === `${apiBase}/public/rooms` && method === "GET") {
327
+ return jsonResponse({ items: [{ id: "room_low", min_buyin_cc: 1000, name: "Low" }] });
328
+ }
329
+ if (url === `${apiBase}/agent/sessions` && method === "POST") {
330
+ return jsonResponse({ session_id: "sess_3", stream_url: "/api/agent/sessions/sess_3/events" });
331
+ }
332
+ if (url === `${apiBase}/agent/sessions/sess_3/events` && method === "GET") {
333
+ return sseResponse([
334
+ "id: 501\nevent: message\ndata: {\"event\":\"state_snapshot\",\"data\":{\"turn_id\":\"turn_5\",",
335
+ "\"my_seat\":0,\"current_actor_seat\":0,\"legal_actions\":[\"raise\"],\"action_constraints\":{\"raise\":{\"min_to\":300,\"max_to\":600}}}}\n\n"
336
+ ]);
337
+ }
338
+ throw new Error(`unexpected fetch: ${method} ${url}`);
339
+ });
340
+ try {
341
+ await runCLI(["next-decision", "--api-base", apiBase, "--join", "random", "--timeout-ms", "2000"]);
342
+ const state = await loadDecisionState();
343
+ const decisionID = state.pending_decision?.decision_id;
344
+ assert.ok(decisionID);
345
+ await assert.rejects(async () => runCLI([
346
+ "submit-decision",
347
+ "--api-base",
348
+ apiBase,
349
+ "--decision-id",
350
+ decisionID,
351
+ "--action",
352
+ "raise",
353
+ "--amount",
354
+ "700"
355
+ ]), /amount_out_of_range/);
356
+ }
357
+ finally {
358
+ globalThis.fetch = originalFetch;
359
+ stdout.restore();
360
+ }
361
+ const actionCall = calls.find((c) => c.url.includes("/actions") && c.method === "POST");
362
+ assert.equal(actionCall, undefined);
363
+ });
364
+ });
@@ -10,6 +10,17 @@ export type DecisionState = {
10
10
  request_id: string;
11
11
  turn_id: string;
12
12
  callback_url: string;
13
+ legal_actions?: string[];
14
+ action_constraints?: {
15
+ bet?: {
16
+ min: number;
17
+ max: number;
18
+ };
19
+ raise?: {
20
+ min_to: number;
21
+ max_to: number;
22
+ };
23
+ };
13
24
  created_at: string;
14
25
  };
15
26
  updated_at?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apa-network/agent-sdk",
3
- "version": "0.2.0-beta.8",
3
+ "version": "0.2.0-beta.9",
4
4
  "description": "APA Agent SDK and CLI",
5
5
  "type": "module",
6
6
  "bin": {