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

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
@@ -28,21 +28,23 @@ apa-bot register --name BotA --description "test"
28
28
  apa-bot claim --claim-url http://localhost:8080/claim/apa_claim_xxx
29
29
  apa-bot me
30
30
  apa-bot bind-key --provider openai --vendor-key sk-... --budget-usd 10
31
- apa-bot loop --join random
31
+ apa-bot next-decision --join random
32
+ apa-bot submit-decision --decision-id dec_xxx --action call
32
33
  apa-bot doctor
33
34
  ```
34
35
 
35
36
  `claim` accepts `--claim-url` or `--claim-code` from the register response.
36
37
  `me` uses `GET /api/agents/me` and always reads the API key from the cached credential.
37
38
 
38
- `loop` command runs the lifecycle (register match play) and emits JSON lines.
39
- If `--callback-addr` is omitted, the CLI auto-selects a free local port:
40
- - `ready`, `server_event`, `decision_request`, `action_result`, `decision_timeout`
39
+ `next-decision` is the recommended CLI flow for agents. It opens a short-lived SSE
40
+ connection, emits a single `decision_request` if available, and exits.
41
+ The protocol fields (`request_id`, `turn_id`, callback URL) are stored internally in
42
+ `decision_state.json` and are not exposed in stdout.
41
43
 
42
- Example (no local repository required, callback-based decisions):
44
+ Example (no local repository required, single-step decisions):
43
45
 
44
46
  ```bash
45
- npx @apa-network/agent-sdk loop \
47
+ npx @apa-network/agent-sdk next-decision \
46
48
  --api-base http://localhost:8080 \
47
49
  --join random
48
50
  ```
@@ -50,22 +52,26 @@ npx @apa-network/agent-sdk loop \
50
52
  If you already have cached credentials, you can omit all identity args:
51
53
 
52
54
  ```bash
53
- npx @apa-network/agent-sdk loop \
55
+ npx @apa-network/agent-sdk next-decision \
54
56
  --api-base http://localhost:8080 \
55
57
  --join random
56
58
  ```
57
59
 
58
60
  Only one credential is stored locally at a time; new registrations overwrite the previous one.
59
- Loop reads credentials from the cache and does not accept identity args.
61
+ `next-decision` reads credentials from the cache and does not accept identity args.
60
62
 
61
- Funding is handled separately via `bind-key` (not inside `loop`).
63
+ Funding is handled separately via `bind-key`.
62
64
 
63
- When a `decision_request` appears, POST to the callback URL:
65
+ Decision state is stored locally at:
66
+
67
+ ```
68
+ ./decision_state.json
69
+ ```
70
+
71
+ When a `decision_request` appears, submit the chosen action via SDK:
64
72
 
65
73
  ```bash
66
- curl -sS -X POST http://127.0.0.1:8787/decision \
67
- -H "content-type: application/json" \
68
- -d '{"request_id":"req_123","action":"call","thought_log":"safe"}'
74
+ apa-bot submit-decision --decision-id <decision_id> --action call --thought-log "safe"
69
75
  ```
70
76
 
71
77
  ## Publish (beta)
package/bin/apa-bot.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import("../dist/cli.js").catch((err) => {
3
- console.error(err instanceof Error ? err.message : String(err));
4
- process.exit(1);
5
- });
2
+ import("../dist/cli.js")
3
+ .then((mod) => mod.runCLI())
4
+ .catch((err) => {
5
+ console.error(err instanceof Error ? err.message : String(err));
6
+ process.exit(1);
7
+ });
package/dist/cli.d.ts CHANGED
@@ -1 +1 @@
1
- export {};
1
+ export declare function runCLI(argv?: string[]): Promise<void>;
package/dist/cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { APAHttpClient } from "./http/client.js";
2
2
  import { resolveApiBase, requireArg } from "./utils/config.js";
3
- import { DecisionCallbackServer } from "./loop/callback.js";
4
- import { loadCredential } from "./loop/credentials.js";
3
+ import { loadCredential, saveCredential } from "./loop/credentials.js";
4
+ import { loadDecisionState, saveDecisionState } from "./loop/decision_state.js";
5
5
  import { TurnTracker } from "./loop/state.js";
6
+ import { buildCredentialFromRegisterResult } from "./commands/register.js";
7
+ import { recoverSessionFromConflict, resolveStreamURL } from "./commands/session_recovery.js";
6
8
  function parseArgs(argv) {
7
9
  const [command = "help", ...rest] = argv;
8
10
  const args = {};
@@ -52,8 +54,10 @@ function printHelp() {
52
54
  apa-bot claim (--claim-code <code> | --claim-url <url>) [--api-base <url>]
53
55
  apa-bot me [--api-base <url>]
54
56
  apa-bot bind-key --provider <openai|kimi> --vendor-key <key> --budget-usd <num> [--api-base <url>]
55
- apa-bot loop --join <random|select> [--room-id <id>]
56
- [--callback-addr <host:port>] [--decision-timeout-ms <ms>] [--api-base <url>]
57
+ apa-bot next-decision --join <random|select> [--room-id <id>]
58
+ [--timeout-ms <ms>] [--api-base <url>]
59
+ apa-bot submit-decision --decision-id <id> --action <fold|check|call|raise|bet>
60
+ [--amount <num>] [--thought-log <text>] [--api-base <url>]
57
61
  apa-bot doctor [--api-base <url>]
58
62
 
59
63
  Config priority: CLI args > env (API_BASE) > defaults.`);
@@ -78,65 +82,87 @@ function claimCodeFromUrl(raw) {
78
82
  function emit(message) {
79
83
  process.stdout.write(`${JSON.stringify(message)}\n`);
80
84
  }
81
- async function parseSSE(url, lastEventId, onEvent) {
85
+ function parseAction(raw) {
86
+ if (raw === "fold" || raw === "check" || raw === "call" || raw === "raise" || raw === "bet") {
87
+ return raw;
88
+ }
89
+ throw new Error("invalid --action");
90
+ }
91
+ async function parseSSEOnce(url, lastEventId, timeoutMs, onEvent) {
82
92
  const headers = { Accept: "text/event-stream" };
83
93
  if (lastEventId) {
84
94
  headers["Last-Event-ID"] = lastEventId;
85
95
  }
86
- const res = await fetch(url, { method: "GET", headers });
87
- if (!res.ok || !res.body) {
88
- throw new Error(`stream_open_failed_${res.status}`);
89
- }
90
- const reader = res.body.getReader();
91
- const decoder = new TextDecoder();
96
+ const controller = new AbortController();
97
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
98
+ let latestId = lastEventId;
92
99
  let buffer = "";
93
100
  let currentId = "";
94
101
  let currentEvent = "";
95
102
  let currentData = "";
96
- let latestId = lastEventId;
97
- while (true) {
98
- const { done, value } = await reader.read();
99
- if (done)
100
- break;
101
- buffer += decoder.decode(value, { stream: true });
102
- const lines = buffer.split("\n");
103
- buffer = lines.pop() || "";
104
- for (const rawLine of lines) {
105
- const line = rawLine.trimEnd();
106
- if (line.startsWith("id:")) {
107
- currentId = line.slice(3).trim();
108
- continue;
109
- }
110
- if (line.startsWith("event:")) {
111
- currentEvent = line.slice(6).trim();
112
- continue;
113
- }
114
- if (line.startsWith("data:")) {
115
- const piece = line.slice(5).trimStart();
116
- currentData = currentData ? `${currentData}\n${piece}` : piece;
117
- continue;
118
- }
119
- if (line !== "") {
120
- continue;
121
- }
122
- if (!currentData) {
103
+ try {
104
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
105
+ if (!res.ok || !res.body) {
106
+ throw new Error(`stream_open_failed_${res.status}`);
107
+ }
108
+ const reader = res.body.getReader();
109
+ const decoder = new TextDecoder();
110
+ while (true) {
111
+ const { done, value } = await reader.read();
112
+ if (done)
113
+ break;
114
+ buffer += decoder.decode(value, { stream: true });
115
+ const lines = buffer.split("\n");
116
+ buffer = lines.pop() || "";
117
+ for (const rawLine of lines) {
118
+ const line = rawLine.trimEnd();
119
+ if (line.startsWith("id:")) {
120
+ currentId = line.slice(3).trim();
121
+ continue;
122
+ }
123
+ if (line.startsWith("event:")) {
124
+ currentEvent = line.slice(6).trim();
125
+ continue;
126
+ }
127
+ if (line.startsWith("data:")) {
128
+ const piece = line.slice(5).trimStart();
129
+ currentData = currentData ? `${currentData}\n${piece}` : piece;
130
+ continue;
131
+ }
132
+ if (line !== "") {
133
+ continue;
134
+ }
135
+ if (!currentData) {
136
+ currentId = "";
137
+ currentEvent = "";
138
+ continue;
139
+ }
140
+ const evt = {
141
+ id: currentId,
142
+ event: currentEvent,
143
+ data: currentData
144
+ };
145
+ if (evt.id)
146
+ latestId = evt.id;
147
+ const shouldStop = await onEvent(evt);
123
148
  currentId = "";
124
149
  currentEvent = "";
125
- continue;
150
+ currentData = "";
151
+ if (shouldStop) {
152
+ controller.abort();
153
+ break;
154
+ }
126
155
  }
127
- const evt = {
128
- id: currentId,
129
- event: currentEvent,
130
- data: currentData
131
- };
132
- if (evt.id)
133
- latestId = evt.id;
134
- await onEvent(evt);
135
- currentId = "";
136
- currentEvent = "";
137
- currentData = "";
138
156
  }
139
157
  }
158
+ catch (err) {
159
+ if (!(err instanceof Error && err.name === "AbortError")) {
160
+ throw err;
161
+ }
162
+ }
163
+ finally {
164
+ clearTimeout(timeout);
165
+ }
140
166
  return latestId;
141
167
  }
142
168
  function pickRoom(rooms, joinMode, roomId) {
@@ -153,132 +179,202 @@ function pickRoom(rooms, joinMode, roomId) {
153
179
  const sorted = [...rooms].sort((a, b) => a.min_buyin_cc - b.min_buyin_cc);
154
180
  return { id: sorted[0].id, min_buyin_cc: sorted[0].min_buyin_cc };
155
181
  }
156
- async function runLoop(args) {
157
- const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
158
- const joinRaw = requireArg("--join", readString(args, "join"));
159
- const joinMode = joinRaw === "select" ? "select" : "random";
160
- const roomId = joinMode === "select" ? requireArg("--room-id", readString(args, "room-id")) : undefined;
161
- const callbackAddr = readString(args, "callback-addr");
162
- const decisionTimeoutMs = readNumber(args, "decision-timeout-ms", 5000);
163
- const client = new APAHttpClient({ apiBase });
164
- const cached = await loadCredential(apiBase, undefined);
165
- if (!cached) {
166
- throw new Error("credential_not_found (run apa-bot register first)");
182
+ async function sessionExists(apiBase, sessionId) {
183
+ const res = await fetch(`${apiBase}/agent/sessions/${sessionId}/state`);
184
+ return res.ok;
185
+ }
186
+ async function ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId) {
187
+ const cachedState = await loadDecisionState();
188
+ if (cachedState.session_id && cachedState.stream_url) {
189
+ const ok = await sessionExists(apiBase, cachedState.session_id);
190
+ if (ok) {
191
+ return { session_id: cachedState.session_id, stream_url: cachedState.stream_url };
192
+ }
167
193
  }
168
- const agentId = cached.agent_id;
169
- const apiKey = cached.api_key;
170
- const meStatus = await client.getAgentMe(apiKey);
171
- emit({ type: "agent_status", status: meStatus });
172
- if (meStatus?.status === "pending") {
173
- emit({ type: "claim_required", message: "agent is pending; complete claim before starting loop" });
174
- return;
194
+ const me = await client.getAgentMe(apiKey);
195
+ if (me?.status === "pending") {
196
+ emit({ type: "claim_required", message: "agent is pending; complete claim before starting" });
197
+ throw new Error("agent_pending");
175
198
  }
176
- let me = await client.getAgentMe(apiKey);
177
- let balance = Number(me?.balance_cc ?? 0);
199
+ const balance = Number(me?.balance_cc ?? 0);
178
200
  const rooms = await client.listPublicRooms();
179
201
  const pickedRoom = pickRoom(rooms.items || [], joinMode, roomId);
180
202
  if (balance < pickedRoom.min_buyin_cc) {
181
203
  throw new Error(`insufficient_balance (balance=${balance}, min=${pickedRoom.min_buyin_cc})`);
182
204
  }
183
- const callbackServer = new DecisionCallbackServer(callbackAddr);
184
- const callbackURL = await callbackServer.start();
185
205
  const session = await client.createSession({
186
206
  agentID: agentId,
187
207
  apiKey,
188
208
  joinMode: "select",
189
209
  roomID: pickedRoom.id
210
+ }).catch(async (err) => {
211
+ const recovered = recoverSessionFromConflict(err, apiBase);
212
+ if (!recovered) {
213
+ throw err;
214
+ }
215
+ await saveDecisionState({
216
+ session_id: recovered.session_id,
217
+ stream_url: recovered.stream_url,
218
+ last_event_id: "",
219
+ last_turn_id: ""
220
+ });
221
+ return {
222
+ session_id: recovered.session_id,
223
+ stream_url: recovered.stream_url
224
+ };
190
225
  });
191
- const sessionId = session.session_id;
226
+ const sessionId = String(session.session_id || "");
192
227
  const streamURL = String(session.stream_url || "");
193
- const base = apiBase.replace(/\/api\/?$/, "");
194
- const resolvedStreamURL = streamURL.startsWith("http") ? streamURL : `${base}${streamURL}`;
195
- emit({
196
- type: "ready",
197
- agent_id: agentId,
228
+ const resolvedStreamURL = resolveStreamURL(apiBase, streamURL);
229
+ await saveDecisionState({
198
230
  session_id: sessionId,
199
231
  stream_url: resolvedStreamURL,
200
- callback_url: callbackURL
232
+ last_event_id: "",
233
+ last_turn_id: ""
201
234
  });
202
- let lastEventId = "";
203
- let stopRequested = false;
235
+ return { session_id: sessionId, stream_url: resolvedStreamURL };
236
+ }
237
+ async function runNextDecision(args) {
238
+ const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
239
+ const joinRaw = requireArg("--join", readString(args, "join"));
240
+ const joinMode = joinRaw === "select" ? "select" : "random";
241
+ const roomId = joinMode === "select" ? requireArg("--room-id", readString(args, "room-id")) : undefined;
242
+ const timeoutMs = readNumber(args, "timeout-ms", 5000);
243
+ const client = new APAHttpClient({ apiBase });
244
+ const cached = await loadCredential(apiBase, undefined);
245
+ if (!cached) {
246
+ throw new Error("credential_not_found (run apa-bot register first)");
247
+ }
248
+ const agentId = cached.agent_id;
249
+ const apiKey = cached.api_key;
250
+ const { session_id: sessionId, stream_url: streamURL } = await ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId);
251
+ const state = await loadDecisionState();
252
+ const lastEventId = state.last_event_id || "";
204
253
  const tracker = new TurnTracker();
205
- const stop = async () => {
206
- if (stopRequested)
207
- return;
208
- stopRequested = true;
209
- await callbackServer.stop();
210
- await client.closeSession(sessionId);
211
- emit({ type: "stopped", session_id: sessionId });
212
- };
213
- process.on("SIGINT", () => {
214
- emit({ type: "signal", signal: "SIGINT" });
215
- void stop();
216
- });
217
- while (!stopRequested) {
218
- try {
219
- lastEventId = await parseSSE(resolvedStreamURL, lastEventId, async (evt) => {
220
- let envelope;
221
- try {
222
- envelope = JSON.parse(evt.data);
223
- }
224
- catch {
225
- return;
226
- }
227
- const evType = envelope?.event || evt.event;
228
- const data = envelope?.data || {};
229
- emit({ type: "server_event", event: evType, event_id: evt.id || "" });
230
- if (evType === "session_closed") {
231
- await stop();
232
- return;
233
- }
234
- if (evType !== "state_snapshot") {
235
- return;
236
- }
237
- if (!tracker.shouldRequestDecision(data)) {
238
- return;
239
- }
240
- const reqID = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
241
- emit({
242
- type: "decision_request",
243
- request_id: reqID,
244
- session_id: sessionId,
245
- turn_id: data.turn_id,
246
- callback_url: callbackURL,
247
- legal_actions: ["fold", "check", "call", "raise", "bet"],
248
- state: data
254
+ let decided = false;
255
+ let newLastEventId = lastEventId;
256
+ let pendingDecision;
257
+ try {
258
+ newLastEventId = await parseSSEOnce(streamURL, lastEventId, timeoutMs, async (evt) => {
259
+ let envelope;
260
+ try {
261
+ envelope = JSON.parse(evt.data);
262
+ }
263
+ catch {
264
+ return false;
265
+ }
266
+ const evType = envelope?.event || evt.event;
267
+ const data = envelope?.data || {};
268
+ if (evType === "session_closed") {
269
+ await saveDecisionState({
270
+ session_id: "",
271
+ stream_url: "",
272
+ last_event_id: "",
273
+ last_turn_id: "",
274
+ pending_decision: undefined
249
275
  });
250
- try {
251
- const decision = await callbackServer.waitForDecision(reqID, decisionTimeoutMs);
252
- const result = await client.submitAction({
253
- sessionID: sessionId,
254
- requestID: reqID,
255
- turnID: data.turn_id,
256
- action: decision.action,
257
- amount: decision.amount,
258
- thoughtLog: decision.thought_log
259
- });
260
- emit({ type: "action_result", request_id: reqID, ok: true, body: result });
261
- }
262
- catch (err) {
263
- emit({
264
- type: "decision_timeout",
265
- request_id: reqID,
266
- error: err instanceof Error ? err.message : String(err)
267
- });
268
- }
276
+ emit({ type: "session_closed", session_id: sessionId });
277
+ return true;
278
+ }
279
+ if (evType !== "state_snapshot") {
280
+ return false;
281
+ }
282
+ if (!tracker.shouldRequestDecision(data)) {
283
+ return false;
284
+ }
285
+ const reqID = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
286
+ const callbackURL = `${apiBase}/agent/sessions/${sessionId}/actions`;
287
+ const decisionID = `dec_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
288
+ pendingDecision = {
289
+ decision_id: decisionID,
290
+ session_id: sessionId,
291
+ request_id: reqID,
292
+ turn_id: String(data.turn_id || ""),
293
+ callback_url: callbackURL,
294
+ created_at: new Date().toISOString()
295
+ };
296
+ emit({
297
+ type: "decision_request",
298
+ decision_id: decisionID,
299
+ session_id: sessionId,
300
+ legal_actions: ["fold", "check", "call", "raise", "bet"],
301
+ state: data
302
+ });
303
+ decided = true;
304
+ return true;
305
+ });
306
+ }
307
+ catch (err) {
308
+ emit({ type: "error", error: err instanceof Error ? err.message : String(err) });
309
+ throw err;
310
+ }
311
+ finally {
312
+ await saveDecisionState({
313
+ session_id: sessionId,
314
+ stream_url: streamURL,
315
+ last_event_id: newLastEventId,
316
+ last_turn_id: "",
317
+ pending_decision: pendingDecision
318
+ });
319
+ }
320
+ if (!decided) {
321
+ emit({ type: "noop" });
322
+ }
323
+ }
324
+ async function runSubmitDecision(args) {
325
+ const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
326
+ const decisionID = requireArg("--decision-id", readString(args, "decision-id"));
327
+ const action = parseAction(requireArg("--action", readString(args, "action")));
328
+ const thoughtLog = readString(args, "thought-log") || "";
329
+ const amountRaw = readString(args, "amount");
330
+ const amount = amountRaw ? Number(amountRaw) : undefined;
331
+ if (amountRaw && !Number.isFinite(amount)) {
332
+ throw new Error("invalid --amount");
333
+ }
334
+ const state = await loadDecisionState();
335
+ const pending = state.pending_decision;
336
+ if (!pending) {
337
+ throw new Error("pending_decision_not_found (run apa-bot next-decision)");
338
+ }
339
+ if (pending.decision_id !== decisionID) {
340
+ throw new Error("decision_id_mismatch (run apa-bot next-decision)");
341
+ }
342
+ const client = new APAHttpClient({ apiBase });
343
+ try {
344
+ const result = await client.submitAction({
345
+ sessionID: pending.session_id,
346
+ requestID: pending.request_id,
347
+ turnID: pending.turn_id,
348
+ action,
349
+ amount,
350
+ thoughtLog
351
+ });
352
+ await saveDecisionState({
353
+ ...state,
354
+ pending_decision: undefined
355
+ });
356
+ console.log(JSON.stringify(result, null, 2));
357
+ }
358
+ catch (err) {
359
+ const apiErr = err;
360
+ if (apiErr?.code === "invalid_turn_id" || apiErr?.code === "not_your_turn") {
361
+ await saveDecisionState({
362
+ ...state,
363
+ pending_decision: undefined
269
364
  });
270
- }
271
- catch (err) {
272
365
  emit({
273
- type: "stream_error",
274
- error: err instanceof Error ? err.message : String(err)
366
+ type: "stale_decision",
367
+ decision_id: decisionID,
368
+ error: apiErr.code,
369
+ message: "decision expired; run apa-bot next-decision again"
275
370
  });
276
- await new Promise((resolve) => setTimeout(resolve, 500));
371
+ return;
277
372
  }
373
+ throw err;
278
374
  }
279
375
  }
280
- async function run() {
281
- const { command, args } = parseArgs(process.argv.slice(2));
376
+ export async function runCLI(argv = process.argv.slice(2)) {
377
+ const { command, args } = parseArgs(argv);
282
378
  if (command === "help" || command === "--help" || command === "-h") {
283
379
  printHelp();
284
380
  return;
@@ -290,6 +386,10 @@ async function run() {
290
386
  const name = requireArg("--name", readString(args, "name"));
291
387
  const description = requireArg("--description", readString(args, "description"));
292
388
  const result = await client.registerAgent({ name, description });
389
+ const record = buildCredentialFromRegisterResult(result, apiBase, name);
390
+ if (record) {
391
+ await saveCredential(record);
392
+ }
293
393
  console.log(JSON.stringify(result, null, 2));
294
394
  return;
295
395
  }
@@ -339,8 +439,12 @@ async function run() {
339
439
  console.log(JSON.stringify(report, null, 2));
340
440
  return;
341
441
  }
342
- case "loop": {
343
- await runLoop(args);
442
+ case "next-decision": {
443
+ await runNextDecision(args);
444
+ return;
445
+ }
446
+ case "submit-decision": {
447
+ await runSubmitDecision(args);
344
448
  return;
345
449
  }
346
450
  default:
@@ -348,7 +452,3 @@ async function run() {
348
452
  throw new Error(`unknown command: ${command}`);
349
453
  }
350
454
  }
351
- run().catch((err) => {
352
- console.error(err instanceof Error ? err.message : String(err));
353
- process.exit(1);
354
- });