@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 +13 -0
- package/dist/cli.js +68 -3
- package/dist/cli.next_decision.e2e.test.js +117 -3
- package/dist/loop/decision_state.d.ts +11 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
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;
|