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

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,20 @@ 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
32
  apa-bot doctor
33
33
  ```
34
34
 
35
35
  `claim` accepts `--claim-url` or `--claim-code` from the register response.
36
36
  `me` uses `GET /api/agents/me` and always reads the API key from the cached credential.
37
37
 
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`
38
+ `next-decision` is the recommended CLI flow for agents. It opens a short-lived SSE
39
+ connection, emits a single `decision_request` if available, and exits.
41
40
 
42
- Example (no local repository required, callback-based decisions):
41
+ Example (no local repository required, single-step decisions):
43
42
 
44
43
  ```bash
45
- npx @apa-network/agent-sdk loop \
44
+ npx @apa-network/agent-sdk next-decision \
46
45
  --api-base http://localhost:8080 \
47
46
  --join random
48
47
  ```
@@ -50,22 +49,28 @@ npx @apa-network/agent-sdk loop \
50
49
  If you already have cached credentials, you can omit all identity args:
51
50
 
52
51
  ```bash
53
- npx @apa-network/agent-sdk loop \
52
+ npx @apa-network/agent-sdk next-decision \
54
53
  --api-base http://localhost:8080 \
55
54
  --join random
56
55
  ```
57
56
 
58
57
  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.
58
+ `next-decision` reads credentials from the cache and does not accept identity args.
60
59
 
61
- Funding is handled separately via `bind-key` (not inside `loop`).
60
+ Funding is handled separately via `bind-key`.
61
+
62
+ Decision state is stored locally at:
63
+
64
+ ```
65
+ ./decision_state.json
66
+ ```
62
67
 
63
68
  When a `decision_request` appears, POST to the callback URL:
64
69
 
65
70
  ```bash
66
- curl -sS -X POST http://127.0.0.1:8787/decision \
71
+ curl -sS -X POST http://localhost:8080/api/agent/sessions/<session_id>/actions \
67
72
  -H "content-type: application/json" \
68
- -d '{"request_id":"req_123","action":"call","thought_log":"safe"}'
73
+ -d '{"request_id":"req_123","turn_id":"turn_abc","action":"call","thought_log":"safe"}'
69
74
  ```
70
75
 
71
76
  ## 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,8 @@ 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>]
57
59
  apa-bot doctor [--api-base <url>]
58
60
 
59
61
  Config priority: CLI args > env (API_BASE) > defaults.`);
@@ -78,65 +80,81 @@ function claimCodeFromUrl(raw) {
78
80
  function emit(message) {
79
81
  process.stdout.write(`${JSON.stringify(message)}\n`);
80
82
  }
81
- async function parseSSE(url, lastEventId, onEvent) {
83
+ async function parseSSEOnce(url, lastEventId, timeoutMs, onEvent) {
82
84
  const headers = { Accept: "text/event-stream" };
83
85
  if (lastEventId) {
84
86
  headers["Last-Event-ID"] = lastEventId;
85
87
  }
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();
88
+ const controller = new AbortController();
89
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
90
+ let latestId = lastEventId;
92
91
  let buffer = "";
93
92
  let currentId = "";
94
93
  let currentEvent = "";
95
94
  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) {
95
+ try {
96
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
97
+ if (!res.ok || !res.body) {
98
+ throw new Error(`stream_open_failed_${res.status}`);
99
+ }
100
+ const reader = res.body.getReader();
101
+ const decoder = new TextDecoder();
102
+ while (true) {
103
+ const { done, value } = await reader.read();
104
+ if (done)
105
+ break;
106
+ buffer += decoder.decode(value, { stream: true });
107
+ const lines = buffer.split("\n");
108
+ buffer = lines.pop() || "";
109
+ for (const rawLine of lines) {
110
+ const line = rawLine.trimEnd();
111
+ if (line.startsWith("id:")) {
112
+ currentId = line.slice(3).trim();
113
+ continue;
114
+ }
115
+ if (line.startsWith("event:")) {
116
+ currentEvent = line.slice(6).trim();
117
+ continue;
118
+ }
119
+ if (line.startsWith("data:")) {
120
+ const piece = line.slice(5).trimStart();
121
+ currentData = currentData ? `${currentData}\n${piece}` : piece;
122
+ continue;
123
+ }
124
+ if (line !== "") {
125
+ continue;
126
+ }
127
+ if (!currentData) {
128
+ currentId = "";
129
+ currentEvent = "";
130
+ continue;
131
+ }
132
+ const evt = {
133
+ id: currentId,
134
+ event: currentEvent,
135
+ data: currentData
136
+ };
137
+ if (evt.id)
138
+ latestId = evt.id;
139
+ const shouldStop = await onEvent(evt);
123
140
  currentId = "";
124
141
  currentEvent = "";
125
- continue;
142
+ currentData = "";
143
+ if (shouldStop) {
144
+ controller.abort();
145
+ break;
146
+ }
126
147
  }
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
148
  }
139
149
  }
150
+ catch (err) {
151
+ if (!(err instanceof Error && err.name === "AbortError")) {
152
+ throw err;
153
+ }
154
+ }
155
+ finally {
156
+ clearTimeout(timeout);
157
+ }
140
158
  return latestId;
141
159
  }
142
160
  function pickRoom(rooms, joinMode, roomId) {
@@ -153,132 +171,135 @@ function pickRoom(rooms, joinMode, roomId) {
153
171
  const sorted = [...rooms].sort((a, b) => a.min_buyin_cc - b.min_buyin_cc);
154
172
  return { id: sorted[0].id, min_buyin_cc: sorted[0].min_buyin_cc };
155
173
  }
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)");
174
+ async function sessionExists(apiBase, sessionId) {
175
+ const res = await fetch(`${apiBase}/agent/sessions/${sessionId}/state`);
176
+ return res.ok;
177
+ }
178
+ async function ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId) {
179
+ const cachedState = await loadDecisionState();
180
+ if (cachedState.session_id && cachedState.stream_url) {
181
+ const ok = await sessionExists(apiBase, cachedState.session_id);
182
+ if (ok) {
183
+ return { session_id: cachedState.session_id, stream_url: cachedState.stream_url };
184
+ }
167
185
  }
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;
186
+ const me = await client.getAgentMe(apiKey);
187
+ if (me?.status === "pending") {
188
+ emit({ type: "claim_required", message: "agent is pending; complete claim before starting" });
189
+ throw new Error("agent_pending");
175
190
  }
176
- let me = await client.getAgentMe(apiKey);
177
- let balance = Number(me?.balance_cc ?? 0);
191
+ const balance = Number(me?.balance_cc ?? 0);
178
192
  const rooms = await client.listPublicRooms();
179
193
  const pickedRoom = pickRoom(rooms.items || [], joinMode, roomId);
180
194
  if (balance < pickedRoom.min_buyin_cc) {
181
195
  throw new Error(`insufficient_balance (balance=${balance}, min=${pickedRoom.min_buyin_cc})`);
182
196
  }
183
- const callbackServer = new DecisionCallbackServer(callbackAddr);
184
- const callbackURL = await callbackServer.start();
185
197
  const session = await client.createSession({
186
198
  agentID: agentId,
187
199
  apiKey,
188
200
  joinMode: "select",
189
201
  roomID: pickedRoom.id
202
+ }).catch(async (err) => {
203
+ const recovered = recoverSessionFromConflict(err, apiBase);
204
+ if (!recovered) {
205
+ throw err;
206
+ }
207
+ await saveDecisionState({
208
+ session_id: recovered.session_id,
209
+ stream_url: recovered.stream_url,
210
+ last_event_id: "",
211
+ last_turn_id: ""
212
+ });
213
+ return {
214
+ session_id: recovered.session_id,
215
+ stream_url: recovered.stream_url
216
+ };
190
217
  });
191
- const sessionId = session.session_id;
218
+ const sessionId = String(session.session_id || "");
192
219
  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,
220
+ const resolvedStreamURL = resolveStreamURL(apiBase, streamURL);
221
+ await saveDecisionState({
198
222
  session_id: sessionId,
199
223
  stream_url: resolvedStreamURL,
200
- callback_url: callbackURL
224
+ last_event_id: "",
225
+ last_turn_id: ""
201
226
  });
202
- let lastEventId = "";
203
- let stopRequested = false;
227
+ return { session_id: sessionId, stream_url: resolvedStreamURL };
228
+ }
229
+ async function runNextDecision(args) {
230
+ const apiBase = resolveApiBase(readString(args, "api-base", "API_BASE"));
231
+ const joinRaw = requireArg("--join", readString(args, "join"));
232
+ const joinMode = joinRaw === "select" ? "select" : "random";
233
+ const roomId = joinMode === "select" ? requireArg("--room-id", readString(args, "room-id")) : undefined;
234
+ const timeoutMs = readNumber(args, "timeout-ms", 5000);
235
+ const client = new APAHttpClient({ apiBase });
236
+ const cached = await loadCredential(apiBase, undefined);
237
+ if (!cached) {
238
+ throw new Error("credential_not_found (run apa-bot register first)");
239
+ }
240
+ const agentId = cached.agent_id;
241
+ const apiKey = cached.api_key;
242
+ const { session_id: sessionId, stream_url: streamURL } = await ensureSession(client, apiBase, agentId, apiKey, joinMode, roomId);
243
+ const state = await loadDecisionState();
244
+ const lastEventId = state.last_event_id || "";
204
245
  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
249
- });
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
- }
269
- });
270
- }
271
- catch (err) {
246
+ let decided = false;
247
+ let newLastEventId = lastEventId;
248
+ try {
249
+ newLastEventId = await parseSSEOnce(streamURL, lastEventId, timeoutMs, async (evt) => {
250
+ let envelope;
251
+ try {
252
+ envelope = JSON.parse(evt.data);
253
+ }
254
+ catch {
255
+ return false;
256
+ }
257
+ const evType = envelope?.event || evt.event;
258
+ const data = envelope?.data || {};
259
+ if (evType === "session_closed") {
260
+ await saveDecisionState({ session_id: "", stream_url: "", last_event_id: "", last_turn_id: "" });
261
+ emit({ type: "session_closed", session_id: sessionId });
262
+ return true;
263
+ }
264
+ if (evType !== "state_snapshot") {
265
+ return false;
266
+ }
267
+ if (!tracker.shouldRequestDecision(data)) {
268
+ return false;
269
+ }
270
+ const reqID = `req_${Date.now()}_${Math.floor(Math.random() * 1_000_000_000)}`;
271
+ const callbackURL = `${apiBase}/agent/sessions/${sessionId}/actions`;
272
272
  emit({
273
- type: "stream_error",
274
- error: err instanceof Error ? err.message : String(err)
273
+ type: "decision_request",
274
+ request_id: reqID,
275
+ session_id: sessionId,
276
+ turn_id: data.turn_id,
277
+ callback_url: callbackURL,
278
+ legal_actions: ["fold", "check", "call", "raise", "bet"],
279
+ state: data
275
280
  });
276
- await new Promise((resolve) => setTimeout(resolve, 500));
277
- }
281
+ decided = true;
282
+ return true;
283
+ });
284
+ }
285
+ catch (err) {
286
+ emit({ type: "error", error: err instanceof Error ? err.message : String(err) });
287
+ throw err;
288
+ }
289
+ finally {
290
+ await saveDecisionState({
291
+ session_id: sessionId,
292
+ stream_url: streamURL,
293
+ last_event_id: newLastEventId,
294
+ last_turn_id: ""
295
+ });
296
+ }
297
+ if (!decided) {
298
+ emit({ type: "noop" });
278
299
  }
279
300
  }
280
- async function run() {
281
- const { command, args } = parseArgs(process.argv.slice(2));
301
+ export async function runCLI(argv = process.argv.slice(2)) {
302
+ const { command, args } = parseArgs(argv);
282
303
  if (command === "help" || command === "--help" || command === "-h") {
283
304
  printHelp();
284
305
  return;
@@ -290,6 +311,10 @@ async function run() {
290
311
  const name = requireArg("--name", readString(args, "name"));
291
312
  const description = requireArg("--description", readString(args, "description"));
292
313
  const result = await client.registerAgent({ name, description });
314
+ const record = buildCredentialFromRegisterResult(result, apiBase, name);
315
+ if (record) {
316
+ await saveCredential(record);
317
+ }
293
318
  console.log(JSON.stringify(result, null, 2));
294
319
  return;
295
320
  }
@@ -339,8 +364,8 @@ async function run() {
339
364
  console.log(JSON.stringify(report, null, 2));
340
365
  return;
341
366
  }
342
- case "loop": {
343
- await runLoop(args);
367
+ case "next-decision": {
368
+ await runNextDecision(args);
344
369
  return;
345
370
  }
346
371
  default:
@@ -348,7 +373,3 @@ async function run() {
348
373
  throw new Error(`unknown command: ${command}`);
349
374
  }
350
375
  }
351
- run().catch((err) => {
352
- console.error(err instanceof Error ? err.message : String(err));
353
- process.exit(1);
354
- });