@apa-network/agent-sdk 0.1.0 → 0.2.0-beta.10
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 +109 -14
- package/bin/apa-bot.js +6 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +491 -74
- package/dist/cli.next_decision.e2e.test.js +468 -0
- package/dist/commands/register.d.ts +2 -0
- package/dist/commands/register.js +18 -0
- package/dist/commands/register.test.js +26 -0
- package/dist/commands/session_recovery.d.ts +6 -0
- package/dist/commands/session_recovery.js +25 -0
- package/dist/commands/session_recovery.test.js +27 -0
- package/dist/http/client.d.ts +30 -1
- package/dist/http/client.js +50 -7
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -1
- package/dist/loop/callback.d.ts +20 -0
- package/dist/loop/callback.js +105 -0
- package/dist/loop/callback.test.js +33 -0
- package/dist/loop/credentials.d.ts +10 -0
- package/dist/loop/credentials.js +60 -0
- package/dist/loop/credentials.test.d.ts +1 -0
- package/dist/loop/credentials.test.js +33 -0
- package/dist/loop/decision_state.d.ts +30 -0
- package/dist/loop/decision_state.js +43 -0
- package/dist/loop/state.d.ts +9 -0
- package/dist/loop/state.js +16 -0
- package/dist/loop/state.test.d.ts +1 -0
- package/dist/loop/state.test.js +14 -0
- package/dist/utils/config.d.ts +1 -2
- package/dist/utils/config.js +8 -5
- package/dist/utils/config.test.js +7 -1
- package/package.json +7 -2
- package/dist/bot/createBot.d.ts +0 -14
- package/dist/bot/createBot.js +0 -107
- package/dist/types/bot.d.ts +0 -42
- package/dist/types/messages.d.ts +0 -85
- package/dist/utils/action.d.ts +0 -3
- package/dist/utils/action.js +0 -10
- package/dist/utils/action.test.js +0 -10
- package/dist/utils/backoff.d.ts +0 -2
- package/dist/utils/backoff.js +0 -11
- package/dist/utils/backoff.test.js +0 -8
- package/dist/ws/client.d.ts +0 -32
- package/dist/ws/client.js +0 -116
- /package/dist/{types/bot.js → cli.next_decision.e2e.test.d.ts} +0 -0
- /package/dist/{types/messages.js → commands/register.test.d.ts} +0 -0
- /package/dist/{utils/action.test.d.ts → commands/session_recovery.test.d.ts} +0 -0
- /package/dist/{utils/backoff.test.d.ts → loop/callback.test.d.ts} +0 -0
package/dist/http/client.js
CHANGED
|
@@ -2,17 +2,27 @@ import { resolveApiBase } from "../utils/config.js";
|
|
|
2
2
|
async function parseJson(res) {
|
|
3
3
|
const text = await res.text();
|
|
4
4
|
if (!text) {
|
|
5
|
-
|
|
5
|
+
const err = new Error(`empty response (${res.status})`);
|
|
6
|
+
err.status = res.status;
|
|
7
|
+
throw err;
|
|
6
8
|
}
|
|
7
9
|
let parsed;
|
|
8
10
|
try {
|
|
9
11
|
parsed = JSON.parse(text);
|
|
10
12
|
}
|
|
11
13
|
catch {
|
|
12
|
-
|
|
14
|
+
const err = new Error(`invalid json response (${res.status})`);
|
|
15
|
+
err.status = res.status;
|
|
16
|
+
err.body = text;
|
|
17
|
+
throw err;
|
|
13
18
|
}
|
|
14
19
|
if (!res.ok) {
|
|
15
|
-
|
|
20
|
+
const p = parsed;
|
|
21
|
+
const err = new Error(`${res.status} ${p?.error || text}`);
|
|
22
|
+
err.status = res.status;
|
|
23
|
+
err.code = p?.error || "request_failed";
|
|
24
|
+
err.body = parsed;
|
|
25
|
+
throw err;
|
|
16
26
|
}
|
|
17
27
|
return parsed;
|
|
18
28
|
}
|
|
@@ -29,10 +39,9 @@ export class APAHttpClient {
|
|
|
29
39
|
});
|
|
30
40
|
return parseJson(res);
|
|
31
41
|
}
|
|
32
|
-
async
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
});
|
|
42
|
+
async claimByCode(claimCode) {
|
|
43
|
+
const base = this.apiBase.replace(/\/api\/?$/, "");
|
|
44
|
+
const res = await fetch(`${base}/claim/${encodeURIComponent(claimCode)}`);
|
|
36
45
|
return parseJson(res);
|
|
37
46
|
}
|
|
38
47
|
async getAgentMe(apiKey) {
|
|
@@ -56,6 +65,40 @@ export class APAHttpClient {
|
|
|
56
65
|
});
|
|
57
66
|
return parseJson(res);
|
|
58
67
|
}
|
|
68
|
+
async listPublicRooms() {
|
|
69
|
+
const res = await fetch(`${this.apiBase}/public/rooms`);
|
|
70
|
+
return parseJson(res);
|
|
71
|
+
}
|
|
72
|
+
async createSession(input) {
|
|
73
|
+
const res = await fetch(`${this.apiBase}/agent/sessions`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "content-type": "application/json" },
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
agent_id: input.agentID,
|
|
78
|
+
api_key: input.apiKey,
|
|
79
|
+
join_mode: input.joinMode,
|
|
80
|
+
room_id: input.joinMode === "select" ? input.roomID : undefined
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
return parseJson(res);
|
|
84
|
+
}
|
|
85
|
+
async submitAction(input) {
|
|
86
|
+
const res = await fetch(`${this.apiBase}/agent/sessions/${input.sessionID}/actions`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "content-type": "application/json" },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
request_id: input.requestID,
|
|
91
|
+
turn_id: input.turnID,
|
|
92
|
+
action: input.action,
|
|
93
|
+
amount: input.amount,
|
|
94
|
+
thought_log: input.thoughtLog || ""
|
|
95
|
+
})
|
|
96
|
+
});
|
|
97
|
+
return parseJson(res);
|
|
98
|
+
}
|
|
99
|
+
async closeSession(sessionID) {
|
|
100
|
+
await fetch(`${this.apiBase}/agent/sessions/${sessionID}`, { method: "DELETE" });
|
|
101
|
+
}
|
|
59
102
|
async healthz() {
|
|
60
103
|
const base = this.apiBase.replace(/\/api\/?$/, "");
|
|
61
104
|
const res = await fetch(`${base}/healthz`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
1
|
export { APAHttpClient } from "./http/client.js";
|
|
2
|
-
export { createBot } from "./bot/createBot.js";
|
|
3
|
-
export type { CreateBotOptions, PlayContext, StrategyFn, BotAction } from "./types/bot.js";
|
|
4
|
-
export type { JoinMode, JoinResultEvent, StateUpdateEvent, ActionResultEvent, EventLogEvent, HandEndEvent, ServerEvent } from "./types/messages.js";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type DecisionAction = "fold" | "check" | "call" | "raise" | "bet";
|
|
2
|
+
export type DecisionPayload = {
|
|
3
|
+
request_id: string;
|
|
4
|
+
action: DecisionAction;
|
|
5
|
+
amount?: number;
|
|
6
|
+
thought_log?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class DecisionCallbackServer {
|
|
9
|
+
private readonly addr;
|
|
10
|
+
private readonly decisions;
|
|
11
|
+
private server;
|
|
12
|
+
private callbackURL;
|
|
13
|
+
constructor(addr?: string);
|
|
14
|
+
start(): Promise<string>;
|
|
15
|
+
stop(): Promise<void>;
|
|
16
|
+
waitForDecision(requestID: string, timeoutMs: number): Promise<DecisionPayload>;
|
|
17
|
+
private handleRequest;
|
|
18
|
+
private reply;
|
|
19
|
+
private parseAddr;
|
|
20
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { URL } from "node:url";
|
|
3
|
+
export class DecisionCallbackServer {
|
|
4
|
+
addr;
|
|
5
|
+
decisions = new Map();
|
|
6
|
+
server = null;
|
|
7
|
+
callbackURL = "";
|
|
8
|
+
constructor(addr) {
|
|
9
|
+
this.addr = addr;
|
|
10
|
+
}
|
|
11
|
+
async start() {
|
|
12
|
+
if (this.server) {
|
|
13
|
+
return this.callbackURL;
|
|
14
|
+
}
|
|
15
|
+
const [host, port] = this.parseAddr();
|
|
16
|
+
this.server = http.createServer(this.handleRequest.bind(this));
|
|
17
|
+
await new Promise((resolve, reject) => {
|
|
18
|
+
this.server?.once("error", reject);
|
|
19
|
+
this.server?.listen(port, host, () => resolve());
|
|
20
|
+
});
|
|
21
|
+
const actual = this.server?.address();
|
|
22
|
+
if (!actual || typeof actual === "string") {
|
|
23
|
+
throw new Error("callback_server_address_unavailable");
|
|
24
|
+
}
|
|
25
|
+
this.callbackURL = `http://${actual.address}:${actual.port}/decision`;
|
|
26
|
+
return this.callbackURL;
|
|
27
|
+
}
|
|
28
|
+
async stop() {
|
|
29
|
+
const entries = [...this.decisions.values()];
|
|
30
|
+
this.decisions.clear();
|
|
31
|
+
for (const pending of entries) {
|
|
32
|
+
clearTimeout(pending.timeout);
|
|
33
|
+
pending.reject(new Error("callback_server_stopped"));
|
|
34
|
+
}
|
|
35
|
+
if (!this.server) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const s = this.server;
|
|
39
|
+
this.server = null;
|
|
40
|
+
await new Promise((resolve) => s.close(() => resolve()));
|
|
41
|
+
}
|
|
42
|
+
waitForDecision(requestID, timeoutMs) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const timeout = setTimeout(() => {
|
|
45
|
+
this.decisions.delete(requestID);
|
|
46
|
+
reject(new Error("decision_timeout"));
|
|
47
|
+
}, timeoutMs);
|
|
48
|
+
this.decisions.set(requestID, { resolve, reject, timeout });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async handleRequest(req, res) {
|
|
52
|
+
const method = req.method || "";
|
|
53
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
54
|
+
if (method === "GET" && url.pathname === "/healthz") {
|
|
55
|
+
this.reply(res, 200, { ok: true });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (method !== "POST" || url.pathname !== "/decision") {
|
|
59
|
+
this.reply(res, 404, { error: "not_found" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let body = "";
|
|
63
|
+
req.setEncoding("utf8");
|
|
64
|
+
for await (const chunk of req) {
|
|
65
|
+
body += chunk;
|
|
66
|
+
}
|
|
67
|
+
let payload = null;
|
|
68
|
+
try {
|
|
69
|
+
payload = JSON.parse(body);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
this.reply(res, 400, { error: "invalid_json" });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!payload || typeof payload.request_id !== "string" || typeof payload.action !== "string") {
|
|
76
|
+
this.reply(res, 400, { error: "invalid_payload" });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const pending = this.decisions.get(payload.request_id);
|
|
80
|
+
if (!pending) {
|
|
81
|
+
this.reply(res, 409, { error: "request_not_pending" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.decisions.delete(payload.request_id);
|
|
85
|
+
clearTimeout(pending.timeout);
|
|
86
|
+
pending.resolve(payload);
|
|
87
|
+
this.reply(res, 200, { ok: true });
|
|
88
|
+
}
|
|
89
|
+
reply(res, status, payload) {
|
|
90
|
+
res.statusCode = status;
|
|
91
|
+
res.setHeader("content-type", "application/json");
|
|
92
|
+
res.end(`${JSON.stringify(payload)}\n`);
|
|
93
|
+
}
|
|
94
|
+
parseAddr() {
|
|
95
|
+
if (!this.addr) {
|
|
96
|
+
return ["127.0.0.1", 0];
|
|
97
|
+
}
|
|
98
|
+
const [host, portRaw] = this.addr.split(":");
|
|
99
|
+
const port = Number(portRaw);
|
|
100
|
+
if (!host || !Number.isFinite(port) || port <= 0) {
|
|
101
|
+
throw new Error(`invalid callback addr: ${this.addr}`);
|
|
102
|
+
}
|
|
103
|
+
return [host, port];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { DecisionCallbackServer } from "./callback.js";
|
|
4
|
+
test("callback server receives decision and resolves pending request", async (t) => {
|
|
5
|
+
const server = new DecisionCallbackServer("127.0.0.1:18787");
|
|
6
|
+
let callbackURL = "";
|
|
7
|
+
try {
|
|
8
|
+
callbackURL = await server.start();
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
const code = err && typeof err === "object" ? err.code : "";
|
|
12
|
+
if (code === "EPERM" || code === "EACCES") {
|
|
13
|
+
t.skip("listen not permitted in this environment");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
const decisionPromise = server.waitForDecision("req_1", 2000);
|
|
19
|
+
const res = await fetch(callbackURL, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "content-type": "application/json" },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
request_id: "req_1",
|
|
24
|
+
action: "call",
|
|
25
|
+
thought_log: "ok"
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
assert.equal(res.status, 200);
|
|
29
|
+
const decision = await decisionPromise;
|
|
30
|
+
assert.equal(decision.request_id, "req_1");
|
|
31
|
+
assert.equal(decision.action, "call");
|
|
32
|
+
await server.stop();
|
|
33
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type AgentCredential = {
|
|
2
|
+
api_base: string;
|
|
3
|
+
agent_name: string;
|
|
4
|
+
agent_id: string;
|
|
5
|
+
api_key: string;
|
|
6
|
+
updated_at: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function defaultCredentialPath(): string;
|
|
9
|
+
export declare function loadCredential(apiBase: string, _agentName: string | undefined, filePath?: string): Promise<AgentCredential | null>;
|
|
10
|
+
export declare function saveCredential(record: Omit<AgentCredential, "updated_at">, filePath?: string): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const STORE_VERSION = 2;
|
|
4
|
+
export function defaultCredentialPath() {
|
|
5
|
+
return path.join(process.cwd(), "credentials.json");
|
|
6
|
+
}
|
|
7
|
+
export async function loadCredential(apiBase, _agentName, filePath = defaultCredentialPath()) {
|
|
8
|
+
const store = await readStore(filePath);
|
|
9
|
+
if (!store.credential) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (store.credential.api_base !== apiBase) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return store.credential;
|
|
16
|
+
}
|
|
17
|
+
export async function saveCredential(record, filePath = defaultCredentialPath()) {
|
|
18
|
+
const store = await readStore(filePath);
|
|
19
|
+
store.credential = {
|
|
20
|
+
...record,
|
|
21
|
+
updated_at: new Date().toISOString()
|
|
22
|
+
};
|
|
23
|
+
await writeStore(store, filePath);
|
|
24
|
+
}
|
|
25
|
+
async function readStore(filePath) {
|
|
26
|
+
let raw = "";
|
|
27
|
+
try {
|
|
28
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (isENOENT(err)) {
|
|
32
|
+
return { version: STORE_VERSION };
|
|
33
|
+
}
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
if (!raw.trim()) {
|
|
37
|
+
return { version: STORE_VERSION };
|
|
38
|
+
}
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(raw);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { version: STORE_VERSION };
|
|
45
|
+
}
|
|
46
|
+
if (!parsed || typeof parsed !== "object") {
|
|
47
|
+
return { version: STORE_VERSION };
|
|
48
|
+
}
|
|
49
|
+
if (parsed.credential && typeof parsed.credential === "object") {
|
|
50
|
+
return { version: STORE_VERSION, credential: parsed.credential };
|
|
51
|
+
}
|
|
52
|
+
return { version: STORE_VERSION };
|
|
53
|
+
}
|
|
54
|
+
async function writeStore(store, filePath) {
|
|
55
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
56
|
+
await fs.writeFile(filePath, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
|
|
57
|
+
}
|
|
58
|
+
function isENOENT(err) {
|
|
59
|
+
return Boolean(err && typeof err === "object" && err.code === "ENOENT");
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import { loadCredential, saveCredential } from "./credentials.js";
|
|
7
|
+
test("saveCredential and loadCredential roundtrip", async () => {
|
|
8
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "apa-sdk-creds-"));
|
|
9
|
+
const filePath = path.join(dir, "credentials.json");
|
|
10
|
+
await saveCredential({
|
|
11
|
+
api_base: "http://localhost:8080/api",
|
|
12
|
+
agent_name: "BotA",
|
|
13
|
+
agent_id: "agent_1",
|
|
14
|
+
api_key: "apa_1"
|
|
15
|
+
}, filePath);
|
|
16
|
+
const loaded = await loadCredential("http://localhost:8080/api", "BotA", filePath);
|
|
17
|
+
assert.ok(loaded);
|
|
18
|
+
assert.equal(loaded?.agent_id, "agent_1");
|
|
19
|
+
assert.equal(loaded?.api_key, "apa_1");
|
|
20
|
+
});
|
|
21
|
+
test("loadCredential without agentName returns single match for api base", async () => {
|
|
22
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "apa-sdk-creds-"));
|
|
23
|
+
const filePath = path.join(dir, "credentials.json");
|
|
24
|
+
await saveCredential({
|
|
25
|
+
api_base: "http://localhost:8080/api",
|
|
26
|
+
agent_name: "BotA",
|
|
27
|
+
agent_id: "agent_1",
|
|
28
|
+
api_key: "apa_1"
|
|
29
|
+
}, filePath);
|
|
30
|
+
const loaded = await loadCredential("http://localhost:8080/api", undefined, filePath);
|
|
31
|
+
assert.ok(loaded);
|
|
32
|
+
assert.equal(loaded?.agent_name, "BotA");
|
|
33
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type DecisionState = {
|
|
2
|
+
version?: number;
|
|
3
|
+
session_id?: string;
|
|
4
|
+
stream_url?: string;
|
|
5
|
+
last_event_id?: string;
|
|
6
|
+
last_turn_id?: string;
|
|
7
|
+
pending_decision?: {
|
|
8
|
+
decision_id: string;
|
|
9
|
+
session_id: string;
|
|
10
|
+
request_id: string;
|
|
11
|
+
turn_id: string;
|
|
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
|
+
};
|
|
24
|
+
created_at: string;
|
|
25
|
+
};
|
|
26
|
+
updated_at?: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function defaultDecisionStatePath(): string;
|
|
29
|
+
export declare function loadDecisionState(filePath?: string): Promise<DecisionState>;
|
|
30
|
+
export declare function saveDecisionState(state: DecisionState, filePath?: string): Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const STATE_VERSION = 1;
|
|
4
|
+
export function defaultDecisionStatePath() {
|
|
5
|
+
return path.join(process.cwd(), "decision_state.json");
|
|
6
|
+
}
|
|
7
|
+
export async function loadDecisionState(filePath = defaultDecisionStatePath()) {
|
|
8
|
+
let raw = "";
|
|
9
|
+
try {
|
|
10
|
+
raw = await fs.readFile(filePath, "utf8");
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (isENOENT(err)) {
|
|
14
|
+
return { version: STATE_VERSION };
|
|
15
|
+
}
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
if (!raw.trim()) {
|
|
19
|
+
return { version: STATE_VERSION };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
if (parsed && typeof parsed === "object") {
|
|
24
|
+
return { ...parsed, version: STATE_VERSION };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { version: STATE_VERSION };
|
|
29
|
+
}
|
|
30
|
+
return { version: STATE_VERSION };
|
|
31
|
+
}
|
|
32
|
+
export async function saveDecisionState(state, filePath = defaultDecisionStatePath()) {
|
|
33
|
+
const record = {
|
|
34
|
+
...state,
|
|
35
|
+
version: STATE_VERSION,
|
|
36
|
+
updated_at: new Date().toISOString()
|
|
37
|
+
};
|
|
38
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
39
|
+
await fs.writeFile(filePath, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
function isENOENT(err) {
|
|
42
|
+
return Boolean(err && typeof err === "object" && err.code === "ENOENT");
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class TurnTracker {
|
|
2
|
+
seenTurns = new Set();
|
|
3
|
+
shouldRequestDecision(state) {
|
|
4
|
+
const turnID = typeof state.turn_id === "string" ? state.turn_id : "";
|
|
5
|
+
const mySeat = Number(state.my_seat ?? -1);
|
|
6
|
+
const actorSeat = Number(state.current_actor_seat ?? -2);
|
|
7
|
+
if (!turnID || mySeat !== actorSeat) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
if (this.seenTurns.has(turnID)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
this.seenTurns.add(turnID);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { TurnTracker } from "./state.js";
|
|
4
|
+
test("TurnTracker requests exactly once for my new turn", () => {
|
|
5
|
+
const tracker = new TurnTracker();
|
|
6
|
+
const state = { turn_id: "turn_1", my_seat: 0, current_actor_seat: 0 };
|
|
7
|
+
assert.equal(tracker.shouldRequestDecision(state), true);
|
|
8
|
+
assert.equal(tracker.shouldRequestDecision(state), false);
|
|
9
|
+
});
|
|
10
|
+
test("TurnTracker ignores opponent turn", () => {
|
|
11
|
+
const tracker = new TurnTracker();
|
|
12
|
+
const state = { turn_id: "turn_2", my_seat: 0, current_actor_seat: 1 };
|
|
13
|
+
assert.equal(tracker.shouldRequestDecision(state), false);
|
|
14
|
+
});
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export declare const DEFAULT_API_BASE = "http://localhost:8080/api";
|
|
2
|
-
export declare
|
|
2
|
+
export declare function normalizeApiBase(raw: string): string;
|
|
3
3
|
export declare function resolveApiBase(override?: string): string;
|
|
4
|
-
export declare function resolveWsUrl(override?: string): string;
|
|
5
4
|
export declare function requireArg(name: string, value?: string): string;
|
package/dist/utils/config.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export const DEFAULT_API_BASE = "http://localhost:8080/api";
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
export function normalizeApiBase(raw) {
|
|
3
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
4
|
+
if (trimmed.endsWith("/api")) {
|
|
5
|
+
return trimmed;
|
|
6
|
+
}
|
|
7
|
+
return `${trimmed}/api`;
|
|
5
8
|
}
|
|
6
|
-
export function
|
|
7
|
-
return (override || process.env.
|
|
9
|
+
export function resolveApiBase(override) {
|
|
10
|
+
return normalizeApiBase(override || process.env.API_BASE || DEFAULT_API_BASE);
|
|
8
11
|
}
|
|
9
12
|
export function requireArg(name, value) {
|
|
10
13
|
if (value && value.trim() !== "") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { DEFAULT_API_BASE, resolveApiBase } from "./config.js";
|
|
3
|
+
import { DEFAULT_API_BASE, normalizeApiBase, resolveApiBase } from "./config.js";
|
|
4
4
|
test("resolveApiBase prefers override", () => {
|
|
5
5
|
assert.equal(resolveApiBase("http://x/api"), "http://x/api");
|
|
6
6
|
});
|
|
@@ -10,3 +10,9 @@ test("resolveApiBase falls back to default", () => {
|
|
|
10
10
|
assert.equal(resolveApiBase(undefined), DEFAULT_API_BASE);
|
|
11
11
|
process.env.API_BASE = old;
|
|
12
12
|
});
|
|
13
|
+
test("normalizeApiBase appends /api when missing", () => {
|
|
14
|
+
assert.equal(normalizeApiBase("http://localhost:8080"), "http://localhost:8080/api");
|
|
15
|
+
});
|
|
16
|
+
test("normalizeApiBase keeps existing /api", () => {
|
|
17
|
+
assert.equal(normalizeApiBase("http://localhost:8080/api"), "http://localhost:8080/api");
|
|
18
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apa-network/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-beta.10",
|
|
4
4
|
"description": "APA Agent SDK and CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc -p tsconfig.json",
|
|
17
|
-
"test": "npm run build && node --test dist/**/*.test.js"
|
|
17
|
+
"test": "npm run build && node --test dist/*.test.js dist/**/*.test.js",
|
|
18
|
+
"prepublishOnly": "npm run test",
|
|
19
|
+
"release:beta": "npm publish --tag beta --access public"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
18
23
|
},
|
|
19
24
|
"engines": {
|
|
20
25
|
"node": ">=20"
|
package/dist/bot/createBot.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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 {};
|