deadnet-agent 1.0.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.
@@ -0,0 +1,101 @@
1
+ class APIError extends Error {
2
+ status;
3
+ data;
4
+ error;
5
+ constructor(status, data) {
6
+ const error = typeof data === "object" ? data?.error || "unknown" : String(data);
7
+ super(`API ${status}: ${error}`);
8
+ this.status = status;
9
+ this.data = data;
10
+ this.error = error;
11
+ }
12
+ }
13
+ export class DeadNetClient {
14
+ baseUrl;
15
+ token;
16
+ constructor(baseUrl, token) {
17
+ this.baseUrl = baseUrl.replace(/\/$/, "");
18
+ this.token = token;
19
+ }
20
+ get clientHeader() { return "deadnet-agent/1.0"; }
21
+ async call(method, path, body) {
22
+ let networkErrors = 0;
23
+ let rateLimitHits = 0;
24
+ while (true) {
25
+ try {
26
+ const res = await fetch(`${this.baseUrl}${path}`, {
27
+ method,
28
+ headers: {
29
+ Authorization: `Bearer ${this.token}`,
30
+ "Content-Type": "application/json",
31
+ "X-DeadNet-Client": this.clientHeader,
32
+ },
33
+ body: body ? JSON.stringify(body) : undefined,
34
+ signal: AbortSignal.timeout(30000),
35
+ });
36
+ const text = await res.text();
37
+ let data;
38
+ try {
39
+ data = JSON.parse(text);
40
+ }
41
+ catch {
42
+ data = { error: text };
43
+ }
44
+ if (res.status === 429) {
45
+ if (++rateLimitHits > 10)
46
+ throw new APIError(429, data);
47
+ const retryAfter = res.headers.get("Retry-After");
48
+ const parsedSeconds = retryAfter ? parseInt(retryAfter, 10) : NaN;
49
+ const waitMs = Math.min(isNaN(parsedSeconds) ? 5000 : parsedSeconds * 1000, 60_000);
50
+ await new Promise((r) => setTimeout(r, waitMs + Math.floor(Math.random() * 500)));
51
+ continue;
52
+ }
53
+ if (!res.ok)
54
+ throw new APIError(res.status, data);
55
+ return data;
56
+ }
57
+ catch (e) {
58
+ if (e instanceof APIError)
59
+ throw e;
60
+ if (++networkErrors >= 3)
61
+ throw e;
62
+ await new Promise((r) => setTimeout(r, 2 ** networkErrors * 1000));
63
+ }
64
+ }
65
+ }
66
+ async connect() {
67
+ return this.call("POST", "/api/agent/connect");
68
+ }
69
+ async joinQueue(matchType) {
70
+ return this.call("POST", "/api/agent/join-queue", { match_type: matchType });
71
+ }
72
+ async leaveQueue() {
73
+ return this.call("POST", "/api/agent/leave-queue");
74
+ }
75
+ async getMatchState(matchId) {
76
+ return this.call("GET", `/api/agent/matches/${matchId}/state`);
77
+ }
78
+ async submitTurn(matchId, content, requestEnd = false) {
79
+ return this.call("POST", `/api/agent/matches/${matchId}/turn`, {
80
+ content,
81
+ request_end: requestEnd,
82
+ });
83
+ }
84
+ async pollEvents(matchId, since) {
85
+ const q = since ? `?since=${since}` : "";
86
+ return this.call("GET", `/api/agent/matches/${matchId}/events${q}`);
87
+ }
88
+ async forfeit(matchId) {
89
+ return this.call("POST", `/api/agent/matches/${matchId}/forfeit`);
90
+ }
91
+ async getGameState(matchId) {
92
+ return this.call("GET", `/api/agent/matches/${matchId}/game-state`);
93
+ }
94
+ async submitMove(matchId, move, message) {
95
+ return this.call("POST", `/api/agent/matches/${matchId}/move`, { move, message });
96
+ }
97
+ async searchGif(query) {
98
+ return this.call("GET", `/api/agent/search-gif?q=${encodeURIComponent(query)}&type=gifs`);
99
+ }
100
+ }
101
+ export { APIError };
@@ -0,0 +1,3 @@
1
+ import type { AgentConfig } from "./types.js";
2
+ export declare function getConfigDir(): string;
3
+ export declare function loadConfig(agentDir?: string): AgentConfig;
@@ -0,0 +1,212 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { resolve, join } from "path";
3
+ import { homedir } from "os";
4
+ import { config as loadDotenv } from "dotenv";
5
+ export function getConfigDir() {
6
+ if (process.platform === "win32") {
7
+ const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
8
+ return join(appData, "deadnet-agent");
9
+ }
10
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
11
+ return join(xdgConfig, "deadnet-agent");
12
+ }
13
+ const DEFAULT_ENV = `\
14
+ # Your DeadNet agent token — get one at https://deadnet.io/dashboard
15
+ DEADNET_TOKEN=
16
+
17
+ # LLM provider API key — only the one matching "provider" in config.json is needed
18
+ ANTHROPIC_API_KEY=
19
+ # OPENAI_API_KEY=
20
+ `;
21
+ const DEFAULT_CONFIG = `\
22
+ {
23
+ "provider": "anthropic",
24
+ "model": "auto",
25
+ "game_model": "auto",
26
+ "effort": "medium",
27
+ "game_effort": "low",
28
+ "match_type": "debate",
29
+ "auto_requeue": true,
30
+ "gifs": true
31
+ }
32
+ `;
33
+ const DEFAULT_PERSONALITY = `\
34
+ # My DeadNet Agent
35
+
36
+ You are a sharp, articulate competitor. You adapt your tone to the format —
37
+ incisive in debate, inventive in freeform, vivid in story.
38
+
39
+ ## Debate Style
40
+ - Lead with your strongest argument, not a preamble.
41
+ - When countering, name exactly what your opponent got wrong before pivoting.
42
+ - Use concrete examples — real events, real numbers, real consequences.
43
+ - End turns with something memorable: a sharp question, a vivid image, a damning comparison.
44
+ - **Opening:** Plant your flag hard. Make your thesis impossible to ignore.
45
+ - **Rebuttal:** Go surgical. Dismantle their weakest point, then hit them with something new.
46
+ - **Closing:** Hammer home your two strongest points. End with the line the audience remembers.
47
+
48
+ ## Freeform Style
49
+ - Genuinely curious. Ask questions that make the other agent think harder.
50
+ - Make unexpected connections between ideas.
51
+ - Comfortable with disagreement — don't smooth things over.
52
+
53
+ ## Story Style
54
+ - Favor tension and subtext over exposition.
55
+ - Write characters who want something and are willing to act.
56
+ - Descriptions are sensory and specific, never generic.
57
+ `;
58
+ const DEFAULT_STRATEGY = `\
59
+ # Game Strategy
60
+
61
+ ## General Principles
62
+ - Prioritize board control over piece preservation.
63
+ - Look two moves ahead: what does my move enable next turn?
64
+ - If ahead, simplify. If behind, complicate.
65
+ - Never let the opponent settle — keep them reacting to you.
66
+
67
+ ## Drop4
68
+ - Stack the center columns first — they give the most winning lines.
69
+ - Block your opponent's three-in-a-row before extending your own two-in-a-row.
70
+ - When you have a forced win, take it immediately.
71
+
72
+ ## Reversi
73
+ - Control the corners and edges — they can never be flipped.
74
+ - In the early game, fewer pieces is often better (more mobility).
75
+ - Force your opponent into moves that give you corners.
76
+
77
+ ## CTF
78
+ - Rush the opponent's flag while keeping one unit back to defend yours.
79
+ - Snare their fastest unit first to disrupt their attack timing.
80
+
81
+ ## Dots & Boxes
82
+ - Avoid completing the third side of any box early — it hands your opponent a chain.
83
+ - Sacrifice short chains to force your opponent to open the long ones.
84
+ `;
85
+ function setupConfigDir(dir) {
86
+ const isNew = !existsSync(dir);
87
+ if (isNew)
88
+ mkdirSync(dir, { recursive: true });
89
+ const files = [
90
+ { name: ".env", content: DEFAULT_ENV },
91
+ { name: "config.json", content: DEFAULT_CONFIG },
92
+ { name: "PERSONALITY.md", content: DEFAULT_PERSONALITY },
93
+ { name: "STRATEGY.md", content: DEFAULT_STRATEGY },
94
+ ];
95
+ const written = [];
96
+ for (const { name, content } of files) {
97
+ const filePath = join(dir, name);
98
+ if (!existsSync(filePath)) {
99
+ writeFileSync(filePath, content, "utf-8");
100
+ written.push(name);
101
+ }
102
+ }
103
+ if (written.length > 0) {
104
+ console.log(`\nDeadNet agent config ${isNew ? "created" : "updated"} at: ${dir}`);
105
+ console.log(` Created: ${written.join(", ")}`);
106
+ console.log(`\nNext step: add your tokens to ${join(dir, ".env")}`);
107
+ console.log(` DEADNET_TOKEN — get one at https://deadnet.io/dashboard`);
108
+ console.log(` ANTHROPIC_API_KEY — or set OPENAI_API_KEY and change provider in config.json\n`);
109
+ }
110
+ }
111
+ export function loadConfig(agentDir) {
112
+ const dir = resolve(agentDir || getConfigDir());
113
+ // First-run setup: create dir + default files for any that don't exist yet
114
+ setupConfigDir(dir);
115
+ // Load .env
116
+ const envPath = resolve(dir, ".env");
117
+ if (existsSync(envPath)) {
118
+ loadDotenv({ path: envPath });
119
+ }
120
+ // Load config.json (optional)
121
+ let json = {};
122
+ const jsonPath = resolve(dir, "config.json");
123
+ if (existsSync(jsonPath)) {
124
+ json = JSON.parse(readFileSync(jsonPath, "utf-8"));
125
+ }
126
+ // Load PERSONALITY.md
127
+ let personality = "You are a sharp, articulate competitor. You adapt your tone to the " +
128
+ "format — incisive in debate, inventive in freeform, vivid in story. " +
129
+ "You never hedge. You never waffle. Every sentence earns its place.";
130
+ const personalityPath = resolve(dir, "PERSONALITY.md");
131
+ if (existsSync(personalityPath)) {
132
+ const text = readFileSync(personalityPath, "utf-8").trim();
133
+ if (text.length > 2000) {
134
+ console.warn("[config] PERSONALITY.md exceeds 500 tokens — truncating to 2000 chars");
135
+ personality = text.slice(0, 2000);
136
+ }
137
+ else if (text) {
138
+ personality = text;
139
+ }
140
+ }
141
+ // Load STRATEGY.md (game matches only) — cap at 2000 chars (~500 tokens)
142
+ let strategy = "";
143
+ const strategyPath = resolve(dir, "STRATEGY.md");
144
+ if (existsSync(strategyPath)) {
145
+ const text = readFileSync(strategyPath, "utf-8").trim();
146
+ if (text.length > 2000) {
147
+ console.warn("[config] STRATEGY.md exceeds 500 tokens — truncating to 2000 chars");
148
+ strategy = text.slice(0, 2000);
149
+ }
150
+ else {
151
+ strategy = text;
152
+ }
153
+ }
154
+ const provider = (json.provider || process.env.PROVIDER || "anthropic");
155
+ const rawModel = json.model || process.env.MODEL || "auto";
156
+ const model = rawModel === "auto" ? defaultModel(provider) : rawModel;
157
+ // game_model defaults to Haiku for Anthropic (structured task, no quality loss)
158
+ // falls back to the primary model for other providers
159
+ const rawGameModel = json.game_model || process.env.GAME_MODEL || "auto";
160
+ const gameModel = rawGameModel === "auto" ? defaultGameModel(provider, model) : rawGameModel;
161
+ const effort = json.effort || process.env.EFFORT || "medium";
162
+ const gameEffort = json.game_effort || process.env.GAME_EFFORT || "low";
163
+ if (provider === "claude-code" && effort === "max" && !model.includes("opus")) {
164
+ console.warn("[config] effort=max is only supported with Opus models — consider setting model to \"opus\"");
165
+ }
166
+ return {
167
+ deadnetToken: process.env.DEADNET_TOKEN || "",
168
+ deadnetApi: json.deadnet_api || process.env.DEADNET_API || "https://api.deadnet.io",
169
+ matchType: (json.match_type || process.env.MATCH_TYPE || "debate"),
170
+ autoRequeue: json.auto_requeue ?? (process.env.AUTO_REQUEUE !== "false"),
171
+ provider,
172
+ model,
173
+ gameModel,
174
+ effort,
175
+ gameEffort,
176
+ apiKey: apiKeyForProvider(provider),
177
+ ollamaHost: json.ollama_host || process.env.OLLAMA_HOST || "http://localhost:11434",
178
+ personality,
179
+ strategy,
180
+ gifs: json.gifs ?? (process.env.GIFS !== "false"),
181
+ debug: process.env.DEBUG === "1",
182
+ contextWindow: {
183
+ debate: json.context_window?.debate ?? 4,
184
+ freeform: json.context_window?.freeform ?? 6,
185
+ story: json.context_window?.story ?? 12,
186
+ game: json.context_window?.game,
187
+ },
188
+ };
189
+ }
190
+ function defaultModel(provider) {
191
+ switch (provider) {
192
+ case "anthropic": return "claude-sonnet-4-20250514";
193
+ case "openai": return "gpt-4o";
194
+ case "ollama": return "llama3.1";
195
+ default: return "claude-sonnet-4-20250514";
196
+ }
197
+ }
198
+ function defaultGameModel(provider, primaryModel) {
199
+ // For Anthropic, default game moves to Haiku — same strategic quality, ~4x cheaper.
200
+ // For other providers, use the primary model (no known cheaper equivalent).
201
+ if (provider === "anthropic")
202
+ return "claude-haiku-4-5-20251001";
203
+ return primaryModel;
204
+ }
205
+ function apiKeyForProvider(provider) {
206
+ switch (provider) {
207
+ case "anthropic": return process.env.ANTHROPIC_API_KEY || "";
208
+ case "openai": return process.env.OPENAI_API_KEY || "";
209
+ case "ollama": return "";
210
+ default: return process.env.ANTHROPIC_API_KEY || "";
211
+ }
212
+ }
@@ -0,0 +1,51 @@
1
+ import { DeadNetClient } from "./api.js";
2
+ import type { LLMProvider } from "../providers/base.js";
3
+ import type { AgentConfig, AgentPhase, LogEntry, MatchState } from "./types.js";
4
+ type Listener = (phase: AgentPhase, data?: any) => void;
5
+ export declare class AgentEngine {
6
+ config: AgentConfig;
7
+ client: DeadNetClient;
8
+ provider: LLMProvider;
9
+ gameProvider: LLMProvider;
10
+ agentName: string;
11
+ matchId: string | null;
12
+ lastState: MatchState | null;
13
+ lastGameState: any;
14
+ phase: AgentPhase;
15
+ logs: LogEntry[];
16
+ totalInputTokens: number;
17
+ totalOutputTokens: number;
18
+ totalCacheReadTokens: number;
19
+ totalCacheWriteTokens: number;
20
+ apiCalls: number;
21
+ sessionInputTokens: number;
22
+ sessionOutputTokens: number;
23
+ sessionCacheReadTokens: number;
24
+ sessionCacheWriteTokens: number;
25
+ sessionApiCalls: number;
26
+ sessionGameInputTokens: number;
27
+ sessionGameOutputTokens: number;
28
+ sessionGameCacheReadTokens: number;
29
+ sessionGameCacheWriteTokens: number;
30
+ get sessionCost(): number;
31
+ private _modelCost;
32
+ private listeners;
33
+ private running;
34
+ constructor(config: AgentConfig, provider: LLMProvider, gameProvider?: LLMProvider);
35
+ on(listener: Listener): () => void;
36
+ private emit;
37
+ private log;
38
+ private debug;
39
+ run(): Promise<void>;
40
+ stop(): void;
41
+ private connect;
42
+ private pickMatchType;
43
+ private queue;
44
+ private play;
45
+ private takeTurn;
46
+ private takeGameMove;
47
+ private onMatchEnd;
48
+ private resetUsage;
49
+ private sleep;
50
+ }
51
+ export {};