@whitesands/agent-player 0.0.0-stage → 1.0.0
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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +313 -2
- package/dist/index.cjs +1072 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +1030 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.cjs +51 -0
- package/dist/protocol.cjs.map +1 -0
- package/dist/protocol.d.cts +113 -0
- package/dist/protocol.d.ts +113 -0
- package/dist/protocol.js +22 -0
- package/dist/protocol.js.map +1 -0
- package/dist/testing.cjs +319 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +92 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.js +289 -0
- package/dist/testing.js.map +1 -0
- package/dist/types-B4iGtwwk.d.cts +124 -0
- package/dist/types-BgUxWDKm.d.ts +124 -0
- package/package.json +78 -4
package/dist/testing.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// src/loop/model.ts
|
|
2
|
+
function createScriptedModel(script) {
|
|
3
|
+
let index = 0;
|
|
4
|
+
return {
|
|
5
|
+
async decide() {
|
|
6
|
+
const call = script[index] ?? { name: "wait", arguments: {} };
|
|
7
|
+
index += 1;
|
|
8
|
+
return {
|
|
9
|
+
toolCall: call,
|
|
10
|
+
usage: { promptTokens: 1, completionTokens: 1 }
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/loop/session-loop.ts
|
|
17
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
18
|
+
|
|
19
|
+
// src/logger.ts
|
|
20
|
+
function log(logger, level, message, extra) {
|
|
21
|
+
logger?.[level]?.(message, extra);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/protocol.ts
|
|
25
|
+
var SDK_VERSION = "1.0.0";
|
|
26
|
+
var DEFAULT_AGENT_INSTRUCTIONS = [
|
|
27
|
+
"You are a remote game agent. Tools are provided by the game.",
|
|
28
|
+
"On every turn: call wait first. It blocks until the game grants your turn and then returns the current view from see.",
|
|
29
|
+
"After wait returns, call exactly one action tool (not see, not wait).",
|
|
30
|
+
"The view's done list is what this character has already called in this session, oldest first. Use it when choosing the next action.",
|
|
31
|
+
"If an action tool returns an error, retry once with a corrected action. A second failure ends the turn as failed."
|
|
32
|
+
].join("\n");
|
|
33
|
+
|
|
34
|
+
// src/loop/session-loop.ts
|
|
35
|
+
function loopEventToAgentEvent(sessionId, event) {
|
|
36
|
+
if (event.event === "action") {
|
|
37
|
+
return {
|
|
38
|
+
type: "action",
|
|
39
|
+
sessionId,
|
|
40
|
+
action: event.action ?? "unknown",
|
|
41
|
+
args: event.args ?? {}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (event.event === "retry") {
|
|
45
|
+
return {
|
|
46
|
+
type: "retry",
|
|
47
|
+
sessionId,
|
|
48
|
+
action: event.action ?? "unknown",
|
|
49
|
+
args: event.args ?? {},
|
|
50
|
+
error: event.error ?? "action failed"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
type: "turnFailed",
|
|
55
|
+
sessionId,
|
|
56
|
+
error: event.error ?? "turn failed",
|
|
57
|
+
...event.action ? { action: event.action } : {},
|
|
58
|
+
...event.args ? { args: event.args } : {}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function parseToolPayload(result) {
|
|
62
|
+
const text = result.content.filter((entry) => entry.type === "text" && typeof entry.text === "string").map((entry) => entry.text).join("\n");
|
|
63
|
+
let json = text;
|
|
64
|
+
try {
|
|
65
|
+
json = JSON.parse(text);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
return { isError: Boolean(result.isError), text, json };
|
|
69
|
+
}
|
|
70
|
+
function toolErrorMessage(result) {
|
|
71
|
+
if (typeof result.json === "object" && result.json && "error" in result.json) {
|
|
72
|
+
return String(result.json.error);
|
|
73
|
+
}
|
|
74
|
+
return result.text;
|
|
75
|
+
}
|
|
76
|
+
function composeInstructions(instructions, goal) {
|
|
77
|
+
if (!goal) return instructions;
|
|
78
|
+
return `${instructions}
|
|
79
|
+
|
|
80
|
+
Your goal: ${goal}`;
|
|
81
|
+
}
|
|
82
|
+
var SessionLoop = class {
|
|
83
|
+
options;
|
|
84
|
+
client = null;
|
|
85
|
+
stopped = false;
|
|
86
|
+
runPromise = null;
|
|
87
|
+
constructor(options) {
|
|
88
|
+
this.options = options;
|
|
89
|
+
}
|
|
90
|
+
async start() {
|
|
91
|
+
if (this.runPromise) return;
|
|
92
|
+
const client = new Client({ name: "agent-player", version: SDK_VERSION });
|
|
93
|
+
await client.connect(this.options.transport);
|
|
94
|
+
this.client = client;
|
|
95
|
+
this.runPromise = this.run().catch((error) => {
|
|
96
|
+
if (this.stopped) return;
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
98
|
+
log(this.options.logger, "error", `session ${this.options.sessionId} crashed: ${message}`);
|
|
99
|
+
this.options.onTurnResult({ ok: false, error: message, attempts: 0 });
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async stop() {
|
|
103
|
+
this.stopped = true;
|
|
104
|
+
try {
|
|
105
|
+
await this.client?.close();
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
this.client = null;
|
|
109
|
+
await this.runPromise?.catch(() => void 0);
|
|
110
|
+
this.runPromise = null;
|
|
111
|
+
}
|
|
112
|
+
async callTool(name, args) {
|
|
113
|
+
if (!this.client) throw new Error("Client not connected");
|
|
114
|
+
const result = await this.client.callTool({ name, arguments: args });
|
|
115
|
+
return parseToolPayload(result);
|
|
116
|
+
}
|
|
117
|
+
async decideAction(tools, messages, usage) {
|
|
118
|
+
const decision = await this.options.model.decide({
|
|
119
|
+
instructions: composeInstructions(this.options.instructions, this.options.goal),
|
|
120
|
+
tools,
|
|
121
|
+
messages
|
|
122
|
+
});
|
|
123
|
+
usage.promptTokens += decision.usage.promptTokens;
|
|
124
|
+
usage.completionTokens += decision.usage.completionTokens;
|
|
125
|
+
const total = decision.usage.promptTokens + decision.usage.completionTokens;
|
|
126
|
+
await this.options.onUsage(total);
|
|
127
|
+
return decision.toolCall;
|
|
128
|
+
}
|
|
129
|
+
emit(event) {
|
|
130
|
+
this.options.onEvent?.(event);
|
|
131
|
+
}
|
|
132
|
+
async run() {
|
|
133
|
+
if (!this.client) return;
|
|
134
|
+
const listed = await this.client.listTools();
|
|
135
|
+
const tools = listed.tools.map((tool) => ({
|
|
136
|
+
name: tool.name,
|
|
137
|
+
description: tool.description ?? tool.name,
|
|
138
|
+
inputSchema: tool.inputSchema
|
|
139
|
+
}));
|
|
140
|
+
const actionTools = tools.filter((tool) => tool.name !== "wait" && tool.name !== "see");
|
|
141
|
+
while (!this.stopped) {
|
|
142
|
+
const waitResult = await this.callTool("wait", {});
|
|
143
|
+
if (this.stopped) break;
|
|
144
|
+
if (waitResult.isError) {
|
|
145
|
+
this.options.onTurnResult({ ok: false, error: waitResult.text, attempts: 0 });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const usage = { promptTokens: 0, completionTokens: 0 };
|
|
149
|
+
const attemptDetails = [];
|
|
150
|
+
const messages = [
|
|
151
|
+
{
|
|
152
|
+
role: "user",
|
|
153
|
+
content: `It is your turn. Current view:
|
|
154
|
+
${waitResult.text}
|
|
155
|
+
Call exactly one action tool.`
|
|
156
|
+
}
|
|
157
|
+
];
|
|
158
|
+
let action = await this.decideAction(actionTools, messages, usage);
|
|
159
|
+
let result;
|
|
160
|
+
let firstError;
|
|
161
|
+
if (action.name === "wait" || action.name === "see") {
|
|
162
|
+
firstError = `${action.name} is not an action`;
|
|
163
|
+
} else {
|
|
164
|
+
this.emit({ event: "action", action: action.name, args: action.arguments });
|
|
165
|
+
result = await this.callTool(action.name, action.arguments);
|
|
166
|
+
if (!result.isError) {
|
|
167
|
+
this.options.onTurnResult({
|
|
168
|
+
ok: true,
|
|
169
|
+
action: action.name,
|
|
170
|
+
args: action.arguments,
|
|
171
|
+
result: result.json,
|
|
172
|
+
attempts: 1,
|
|
173
|
+
usage
|
|
174
|
+
});
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
firstError = toolErrorMessage(result);
|
|
178
|
+
}
|
|
179
|
+
attemptDetails.push({ action: action.name, args: action.arguments, error: firstError });
|
|
180
|
+
log(this.options.logger, "warn", `session ${this.options.sessionId} action ${action.name} failed: ${firstError}; retrying once`);
|
|
181
|
+
this.emit({ event: "retry", action: action.name, args: action.arguments, error: firstError });
|
|
182
|
+
messages.push({
|
|
183
|
+
role: "assistant",
|
|
184
|
+
content: `Called ${action.name}(${JSON.stringify(action.arguments)})`
|
|
185
|
+
});
|
|
186
|
+
messages.push({
|
|
187
|
+
role: "tool",
|
|
188
|
+
name: action.name,
|
|
189
|
+
content: result?.text ?? firstError,
|
|
190
|
+
isError: true
|
|
191
|
+
});
|
|
192
|
+
messages.push({
|
|
193
|
+
role: "user",
|
|
194
|
+
content: [
|
|
195
|
+
`Action ${action.name} failed: ${firstError}`,
|
|
196
|
+
"Retry once with a different legal action (or different arguments).",
|
|
197
|
+
"Do not repeat the same failing call. Do not call wait or see.",
|
|
198
|
+
`Current view was:
|
|
199
|
+
${waitResult.text}`
|
|
200
|
+
].join("\n")
|
|
201
|
+
});
|
|
202
|
+
const retry = await this.decideAction(actionTools, messages, usage);
|
|
203
|
+
if (retry.name === "wait" || retry.name === "see") {
|
|
204
|
+
const error = `Retry returned ${retry.name} instead of an action (after: ${firstError})`;
|
|
205
|
+
this.emit({ event: "turnFailed", error });
|
|
206
|
+
this.options.onTurnResult({
|
|
207
|
+
ok: false,
|
|
208
|
+
error,
|
|
209
|
+
attempts: 1,
|
|
210
|
+
attemptDetails,
|
|
211
|
+
usage
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
this.emit({ event: "action", action: retry.name, args: retry.arguments });
|
|
216
|
+
result = await this.callTool(retry.name, retry.arguments);
|
|
217
|
+
if (!result.isError) {
|
|
218
|
+
this.options.onTurnResult({
|
|
219
|
+
ok: true,
|
|
220
|
+
action: retry.name,
|
|
221
|
+
args: retry.arguments,
|
|
222
|
+
result: result.json,
|
|
223
|
+
attempts: 2,
|
|
224
|
+
usage
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const secondError = toolErrorMessage(result);
|
|
229
|
+
attemptDetails.push({ action: retry.name, args: retry.arguments, error: secondError });
|
|
230
|
+
log(this.options.logger, "warn", `session ${this.options.sessionId} retry ${retry.name} failed: ${secondError}`);
|
|
231
|
+
this.emit({ event: "turnFailed", action: retry.name, args: retry.arguments, error: secondError });
|
|
232
|
+
this.options.onTurnResult({
|
|
233
|
+
ok: false,
|
|
234
|
+
error: secondError,
|
|
235
|
+
attempts: 2,
|
|
236
|
+
attemptDetails,
|
|
237
|
+
usage
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/loop/test-platform.ts
|
|
244
|
+
function createTestPlatform(script) {
|
|
245
|
+
const model = createScriptedModel(script);
|
|
246
|
+
const loops = /* @__PURE__ */ new Map();
|
|
247
|
+
return {
|
|
248
|
+
async startSession(request) {
|
|
249
|
+
const loop = new SessionLoop({
|
|
250
|
+
sessionId: request.sessionId,
|
|
251
|
+
transport: request.transport,
|
|
252
|
+
instructions: request.instructions,
|
|
253
|
+
...request.goal ? { goal: request.goal } : {},
|
|
254
|
+
model,
|
|
255
|
+
...request.logger ? { logger: request.logger } : {},
|
|
256
|
+
onUsage: async (tokens) => {
|
|
257
|
+
await request.onUsage?.(tokens);
|
|
258
|
+
},
|
|
259
|
+
onTurnResult: request.onTurnResult,
|
|
260
|
+
onEvent: (event) => {
|
|
261
|
+
request.onEvent?.(loopEventToAgentEvent(request.sessionId, event));
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
loops.set(request.sessionId, loop);
|
|
265
|
+
await loop.start();
|
|
266
|
+
},
|
|
267
|
+
async stopSession(sessionId) {
|
|
268
|
+
const loop = loops.get(sessionId);
|
|
269
|
+
if (!loop) return;
|
|
270
|
+
loops.delete(sessionId);
|
|
271
|
+
await loop.stop();
|
|
272
|
+
},
|
|
273
|
+
async close() {
|
|
274
|
+
const ids = [...loops.keys()];
|
|
275
|
+
for (const id of ids) {
|
|
276
|
+
const loop = loops.get(id);
|
|
277
|
+
loops.delete(id);
|
|
278
|
+
await loop?.stop();
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
export {
|
|
284
|
+
SessionLoop,
|
|
285
|
+
createScriptedModel,
|
|
286
|
+
createTestPlatform,
|
|
287
|
+
loopEventToAgentEvent
|
|
288
|
+
};
|
|
289
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/loop/model.ts","../src/loop/session-loop.ts","../src/logger.ts","../src/protocol.ts","../src/loop/test-platform.ts"],"sourcesContent":["export type ToolCallRequest = {\n name: string;\n arguments: Record<string, unknown>;\n};\n\nexport type ModelMessage =\n | { role: \"system\"; content: string }\n | { role: \"user\"; content: string }\n | { role: \"assistant\"; content: string }\n | { role: \"tool\"; name: string; content: string; isError?: boolean };\n\nexport type ModelDecision = {\n toolCall: ToolCallRequest;\n usage: { promptTokens: number; completionTokens: number };\n};\n\nexport type ModelClient = {\n /** Ask the model to choose exactly one tool call. */\n decide(args: {\n instructions: string;\n tools: Array<{ name: string; description: string; inputSchema: unknown }>;\n messages: ModelMessage[];\n }): Promise<ModelDecision>;\n};\n\n/**\n * Scripted model for tests: returns predetermined tool calls in order.\n * When the queue is exhausted, always returns wait.\n */\nexport function createScriptedModel(script: ToolCallRequest[]): ModelClient {\n let index = 0;\n return {\n async decide() {\n const call = script[index] ?? { name: \"wait\", arguments: {} };\n index += 1;\n return {\n toolCall: call,\n usage: { promptTokens: 1, completionTokens: 1 },\n };\n },\n };\n}\n","import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport type { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport type { Logger } from \"../logger.js\";\nimport { log } from \"../logger.js\";\nimport type { AgentEvent } from \"../types.js\";\nimport type { AttemptRecord, SessionEventName, TokenUsage, TurnReport } from \"../protocol.js\";\nimport { SDK_VERSION } from \"../protocol.js\";\nimport type { ModelClient, ModelMessage, ToolCallRequest } from \"./model.js\";\n\nexport type LoopEvent = {\n event: SessionEventName;\n action?: string;\n args?: Record<string, unknown>;\n error?: string;\n};\n\nexport function loopEventToAgentEvent(sessionId: string, event: LoopEvent): AgentEvent {\n if (event.event === \"action\") {\n return {\n type: \"action\",\n sessionId,\n action: event.action ?? \"unknown\",\n args: event.args ?? {},\n };\n }\n if (event.event === \"retry\") {\n return {\n type: \"retry\",\n sessionId,\n action: event.action ?? \"unknown\",\n args: event.args ?? {},\n error: event.error ?? \"action failed\",\n };\n }\n return {\n type: \"turnFailed\",\n sessionId,\n error: event.error ?? \"turn failed\",\n ...(event.action ? { action: event.action } : {}),\n ...(event.args ? { args: event.args } : {}),\n };\n}\n\nexport type SessionLoopOptions = {\n sessionId: string;\n transport: Transport;\n instructions: string;\n goal?: string;\n model: ModelClient;\n logger?: Logger;\n onUsage: (tokens: number) => void | Promise<void>;\n onTurnResult: (result: TurnReport) => void;\n onEvent?: (event: LoopEvent) => void;\n};\n\ntype ToolPayload = { isError: boolean; text: string; json: unknown };\n\nfunction parseToolPayload(result: {\n isError?: boolean;\n content: Array<{ type: string; text?: string }>;\n}): ToolPayload {\n const text = result.content\n .filter((entry) => entry.type === \"text\" && typeof entry.text === \"string\")\n .map((entry) => entry.text!)\n .join(\"\\n\");\n let json: unknown = text;\n try {\n json = JSON.parse(text);\n } catch {\n // keep text\n }\n return { isError: Boolean(result.isError), text, json };\n}\n\nfunction toolErrorMessage(result: ToolPayload): string {\n if (typeof result.json === \"object\" && result.json && \"error\" in result.json) {\n return String((result.json as { error: unknown }).error);\n }\n return result.text;\n}\n\nfunction composeInstructions(instructions: string, goal?: string): string {\n if (!goal) return instructions;\n return `${instructions}\\n\\nYour goal: ${goal}`;\n}\n\n/**\n * Per-session MCP client loop:\n * wait → read view → ask model → one action → retry once on error → report turn.\n */\nexport class SessionLoop {\n private readonly options: SessionLoopOptions;\n private client: Client | null = null;\n private stopped = false;\n private runPromise: Promise<void> | null = null;\n\n constructor(options: SessionLoopOptions) {\n this.options = options;\n }\n\n async start(): Promise<void> {\n if (this.runPromise) return;\n const client = new Client({ name: \"agent-player\", version: SDK_VERSION });\n await client.connect(this.options.transport);\n this.client = client;\n this.runPromise = this.run().catch((error) => {\n if (this.stopped) return;\n const message = error instanceof Error ? error.message : String(error);\n log(this.options.logger, \"error\", `session ${this.options.sessionId} crashed: ${message}`);\n this.options.onTurnResult({ ok: false, error: message, attempts: 0 });\n });\n }\n\n async stop(): Promise<void> {\n this.stopped = true;\n try {\n await this.client?.close();\n } catch {\n // ignore\n }\n this.client = null;\n await this.runPromise?.catch(() => undefined);\n this.runPromise = null;\n }\n\n private async callTool(name: string, args: Record<string, unknown>): Promise<ToolPayload> {\n if (!this.client) throw new Error(\"Client not connected\");\n const result = await this.client.callTool({ name, arguments: args });\n return parseToolPayload(result as {\n isError?: boolean;\n content: Array<{ type: string; text?: string }>;\n });\n }\n\n private async decideAction(\n tools: Array<{ name: string; description: string; inputSchema: unknown }>,\n messages: ModelMessage[],\n usage: TokenUsage,\n ): Promise<ToolCallRequest> {\n const decision = await this.options.model.decide({\n instructions: composeInstructions(this.options.instructions, this.options.goal),\n tools,\n messages,\n });\n usage.promptTokens += decision.usage.promptTokens;\n usage.completionTokens += decision.usage.completionTokens;\n const total = decision.usage.promptTokens + decision.usage.completionTokens;\n await this.options.onUsage(total);\n return decision.toolCall;\n }\n\n private emit(event: LoopEvent): void {\n this.options.onEvent?.(event);\n }\n\n private async run(): Promise<void> {\n if (!this.client) return;\n\n const listed = await this.client.listTools();\n const tools = listed.tools.map((tool) => ({\n name: tool.name,\n description: tool.description ?? tool.name,\n inputSchema: tool.inputSchema,\n }));\n const actionTools = tools.filter((tool) => tool.name !== \"wait\" && tool.name !== \"see\");\n\n while (!this.stopped) {\n const waitResult = await this.callTool(\"wait\", {});\n if (this.stopped) break;\n if (waitResult.isError) {\n this.options.onTurnResult({ ok: false, error: waitResult.text, attempts: 0 });\n continue;\n }\n\n const usage: TokenUsage = { promptTokens: 0, completionTokens: 0 };\n const attemptDetails: AttemptRecord[] = [];\n const messages: ModelMessage[] = [\n {\n role: \"user\",\n content: `It is your turn. Current view:\\n${waitResult.text}\\nCall exactly one action tool.`,\n },\n ];\n\n let action = await this.decideAction(actionTools, messages, usage);\n let result: ToolPayload | undefined;\n let firstError: string;\n if (action.name === \"wait\" || action.name === \"see\") {\n // wait and see are how the loop reads state. They are not a move.\n firstError = `${action.name} is not an action`;\n } else {\n this.emit({ event: \"action\", action: action.name, args: action.arguments });\n result = await this.callTool(action.name, action.arguments);\n if (!result.isError) {\n this.options.onTurnResult({\n ok: true,\n action: action.name,\n args: action.arguments,\n result: result.json,\n attempts: 1,\n usage,\n });\n continue;\n }\n firstError = toolErrorMessage(result);\n }\n attemptDetails.push({ action: action.name, args: action.arguments, error: firstError });\n log(this.options.logger, \"warn\", `session ${this.options.sessionId} action ${action.name} failed: ${firstError}; retrying once`);\n this.emit({ event: \"retry\", action: action.name, args: action.arguments, error: firstError });\n\n messages.push({\n role: \"assistant\",\n content: `Called ${action.name}(${JSON.stringify(action.arguments)})`,\n });\n messages.push({\n role: \"tool\",\n name: action.name,\n content: result?.text ?? firstError,\n isError: true,\n });\n messages.push({\n role: \"user\",\n content: [\n `Action ${action.name} failed: ${firstError}`,\n \"Retry once with a different legal action (or different arguments).\",\n \"Do not repeat the same failing call. Do not call wait or see.\",\n `Current view was:\\n${waitResult.text}`,\n ].join(\"\\n\"),\n });\n\n const retry = await this.decideAction(actionTools, messages, usage);\n if (retry.name === \"wait\" || retry.name === \"see\") {\n const error = `Retry returned ${retry.name} instead of an action (after: ${firstError})`;\n this.emit({ event: \"turnFailed\", error });\n this.options.onTurnResult({\n ok: false,\n error,\n attempts: 1,\n attemptDetails,\n usage,\n });\n continue;\n }\n\n this.emit({ event: \"action\", action: retry.name, args: retry.arguments });\n result = await this.callTool(retry.name, retry.arguments);\n if (!result.isError) {\n this.options.onTurnResult({\n ok: true,\n action: retry.name,\n args: retry.arguments,\n result: result.json,\n attempts: 2,\n usage,\n });\n continue;\n }\n\n const secondError = toolErrorMessage(result);\n attemptDetails.push({ action: retry.name, args: retry.arguments, error: secondError });\n log(this.options.logger, \"warn\", `session ${this.options.sessionId} retry ${retry.name} failed: ${secondError}`);\n this.emit({ event: \"turnFailed\", action: retry.name, args: retry.arguments, error: secondError });\n this.options.onTurnResult({\n ok: false,\n error: secondError,\n attempts: 2,\n attemptDetails,\n usage,\n });\n }\n }\n}\n","export type Logger = {\n debug?: (message: string, extra?: unknown) => void;\n info?: (message: string, extra?: unknown) => void;\n warn?: (message: string, extra?: unknown) => void;\n error?: (message: string, extra?: unknown) => void;\n};\n\nexport function log(logger: Logger | undefined, level: keyof Logger, message: string, extra?: unknown): void {\n logger?.[level]?.(message, extra);\n}\n","/**\n * Wire protocol between AgentServer (SDK) and the platform. Version 1.\n * The platform's Go copy lives in White-Sands-Interactive/Platform, internal/protocol.\n */\n\nexport const PROTOCOL_VERSION = 1;\nexport const SDK_VERSION = \"1.0.0\";\n\n/** How long a disconnected session can be resumed. Shared by both sides. */\nexport const SESSION_RESUME_GRACE_MS = 30_000;\n\nexport type JsonSchema = {\n type?: string | string[];\n properties?: Record<string, JsonSchema>;\n required?: string[];\n description?: string;\n additionalProperties?: boolean;\n [key: string]: unknown;\n};\n\nexport type ToolDescriptor = {\n name: string;\n description: string;\n inputSchema: JsonSchema;\n};\n\nexport type TokenUsage = {\n promptTokens: number;\n completionTokens: number;\n};\n\nexport type AttemptRecord = {\n action: string;\n args: Record<string, unknown>;\n error: string;\n};\n\nexport type TurnReport = {\n ok: boolean;\n error?: string;\n action?: string;\n args?: Record<string, unknown>;\n result?: unknown;\n attempts?: number;\n attemptDetails?: AttemptRecord[];\n usage?: TokenUsage;\n connectionLost?: boolean;\n};\n\nexport type SessionEventName = \"action\" | \"retry\" | \"turnFailed\";\n\nexport type Envelope =\n | {\n type: \"hello\";\n platformKey: string;\n clientId: string;\n protocolVersion: number;\n sdkVersion: string;\n }\n | {\n type: \"hello_ok\";\n accountName: string;\n protocolVersion: number;\n }\n | {\n type: \"hello_err\";\n message: string;\n code?: \"AUTH\" | \"PROTOCOL\";\n }\n | {\n type: \"start_session\";\n sessionId: string;\n sessionToken: string;\n tools: ToolDescriptor[];\n instructions: string;\n goal?: string;\n name?: string;\n }\n | {\n type: \"session_started\";\n sessionId: string;\n }\n | {\n type: \"stop_session\";\n sessionId: string;\n }\n | {\n type: \"session_stopped\";\n sessionId: string;\n }\n | {\n type: \"mcp\";\n sessionId: string;\n message: unknown;\n }\n | ({\n type: \"turn_result\";\n sessionId: string;\n } & TurnReport)\n | {\n type: \"session_event\";\n sessionId: string;\n event: SessionEventName;\n action?: string;\n args?: Record<string, unknown>;\n error?: string;\n }\n | {\n type: \"ping\";\n id: string;\n }\n | {\n type: \"pong\";\n id: string;\n }\n | {\n type: \"resume_session\";\n sessionId: string;\n sessionToken: string;\n }\n | {\n type: \"session_resumed\";\n sessionId: string;\n }\n | {\n type: \"resume_err\";\n sessionId: string;\n message: string;\n }\n | {\n type: \"error\";\n message: string;\n sessionId?: string;\n };\n\nexport const DEFAULT_AGENT_INSTRUCTIONS = [\n \"You are a remote game agent. Tools are provided by the game.\",\n \"On every turn: call wait first. It blocks until the game grants your turn and then returns the current view from see.\",\n \"After wait returns, call exactly one action tool (not see, not wait).\",\n \"The view's done list is what this character has already called in this session, oldest first. Use it when choosing the next action.\",\n \"If an action tool returns an error, retry once with a corrected action. A second failure ends the turn as failed.\",\n].join(\"\\n\");\n\nexport function assertNever(value: never): never {\n throw new Error(`Unhandled protocol message: ${JSON.stringify(value)}`);\n}\n","import type { InlinePlatform, StartSessionRequest } from \"../types.js\";\nimport { createScriptedModel, type ToolCallRequest } from \"./model.js\";\nimport { loopEventToAgentEvent, SessionLoop } from \"./session-loop.js\";\n\n/**\n * In-process platform that replays a script of tool calls.\n * Same session loop as production; no network and no platform key.\n */\nexport function createTestPlatform(script: ToolCallRequest[]): InlinePlatform {\n const model = createScriptedModel(script);\n const loops = new Map<string, SessionLoop>();\n\n return {\n async startSession(request: StartSessionRequest) {\n const loop = new SessionLoop({\n sessionId: request.sessionId,\n transport: request.transport,\n instructions: request.instructions,\n ...(request.goal ? { goal: request.goal } : {}),\n model,\n ...(request.logger ? { logger: request.logger } : {}),\n onUsage: async (tokens) => {\n await request.onUsage?.(tokens);\n },\n onTurnResult: request.onTurnResult,\n onEvent: (event) => {\n request.onEvent?.(loopEventToAgentEvent(request.sessionId, event));\n },\n });\n loops.set(request.sessionId, loop);\n await loop.start();\n },\n\n async stopSession(sessionId: string) {\n const loop = loops.get(sessionId);\n if (!loop) return;\n loops.delete(sessionId);\n await loop.stop();\n },\n\n async close() {\n const ids = [...loops.keys()];\n for (const id of ids) {\n const loop = loops.get(id);\n loops.delete(id);\n await loop?.stop();\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
2
|
+
import { TokenUsage, ToolDescriptor, TurnReport } from './protocol.cjs';
|
|
3
|
+
|
|
4
|
+
type Logger = {
|
|
5
|
+
debug?: (message: string, extra?: unknown) => void;
|
|
6
|
+
info?: (message: string, extra?: unknown) => void;
|
|
7
|
+
warn?: (message: string, extra?: unknown) => void;
|
|
8
|
+
error?: (message: string, extra?: unknown) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type TurnResult = {
|
|
12
|
+
action: string;
|
|
13
|
+
args: Record<string, unknown>;
|
|
14
|
+
result: unknown;
|
|
15
|
+
attempts: number;
|
|
16
|
+
usage: TokenUsage;
|
|
17
|
+
};
|
|
18
|
+
type AgentEvent = {
|
|
19
|
+
type: "action";
|
|
20
|
+
sessionId: string;
|
|
21
|
+
action: string;
|
|
22
|
+
args: Record<string, unknown>;
|
|
23
|
+
} | {
|
|
24
|
+
type: "retry";
|
|
25
|
+
sessionId: string;
|
|
26
|
+
action: string;
|
|
27
|
+
args: Record<string, unknown>;
|
|
28
|
+
error: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: "turnFailed";
|
|
31
|
+
sessionId: string;
|
|
32
|
+
error: string;
|
|
33
|
+
action?: string;
|
|
34
|
+
args?: Record<string, unknown>;
|
|
35
|
+
} | {
|
|
36
|
+
type: "disconnected";
|
|
37
|
+
} | {
|
|
38
|
+
type: "reconnected";
|
|
39
|
+
};
|
|
40
|
+
type EventListener = (event: AgentEvent) => void;
|
|
41
|
+
type TurnOptions = {
|
|
42
|
+
/** Overrides the server default. The turn rejects with TurnTimeoutError when exceeded. */
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
/** Abort the wait for the platform's turn result. */
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
};
|
|
47
|
+
type AgentHandle = {
|
|
48
|
+
/** Session id for this started player. */
|
|
49
|
+
readonly sessionId: string;
|
|
50
|
+
/**
|
|
51
|
+
* Grant this session one turn. Resolves with what the agent did, or rejects
|
|
52
|
+
* after a second failed action, a timeout, or a lost connection.
|
|
53
|
+
*/
|
|
54
|
+
turn(options?: TurnOptions): Promise<TurnResult>;
|
|
55
|
+
/** Stop the remote agent for this session. The AgentServer stays up. */
|
|
56
|
+
stop(): Promise<void>;
|
|
57
|
+
/** Subscribe to this session's action, retry, and failure events. Returns unsubscribe. */
|
|
58
|
+
on(listener: EventListener): () => void;
|
|
59
|
+
};
|
|
60
|
+
type StartOptions = {
|
|
61
|
+
/** Label stored with the session. Defaults to the class name. */
|
|
62
|
+
name?: string;
|
|
63
|
+
/** Added to the model prompt for this player only. */
|
|
64
|
+
goal?: string;
|
|
65
|
+
/** Extra instructions appended to the default agent prompt. */
|
|
66
|
+
instructions?: string;
|
|
67
|
+
};
|
|
68
|
+
type StartSessionRequest = {
|
|
69
|
+
sessionId: string;
|
|
70
|
+
sessionToken: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
tools: ToolDescriptor[];
|
|
73
|
+
instructions: string;
|
|
74
|
+
goal?: string;
|
|
75
|
+
logger?: Logger;
|
|
76
|
+
transport: Transport;
|
|
77
|
+
onTurnResult: (result: TurnReport) => void;
|
|
78
|
+
onEvent?: (event: AgentEvent) => void;
|
|
79
|
+
onUsage?: (tokens: number) => void | Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* In-process platform used by `@whitesands/agent-player/testing` and local runs.
|
|
83
|
+
* The hosted platform speaks the same session contract over WebSocket.
|
|
84
|
+
*/
|
|
85
|
+
type InlinePlatform = {
|
|
86
|
+
startSession(request: StartSessionRequest): Promise<void>;
|
|
87
|
+
stopSession(sessionId: string): Promise<void>;
|
|
88
|
+
close?(): Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
type AgentServerOptions = {
|
|
91
|
+
/**
|
|
92
|
+
* Platform key naming the billing account.
|
|
93
|
+
* Defaults to the `WSI_PLATFORM_KEY` environment variable.
|
|
94
|
+
* Not required when `inline` is set.
|
|
95
|
+
*/
|
|
96
|
+
platformKey?: string;
|
|
97
|
+
/**
|
|
98
|
+
* WebSocket URL of the platform.
|
|
99
|
+
* Defaults to `WSI_PLATFORM_URL`, then `ws://127.0.0.1:8787`.
|
|
100
|
+
*/
|
|
101
|
+
platformUrl?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Skip the outbound WebSocket and run against this in-process platform.
|
|
104
|
+
* `createTestPlatform` from `@whitesands/agent-player/testing` returns one.
|
|
105
|
+
*/
|
|
106
|
+
inline?: InlinePlatform;
|
|
107
|
+
/** Replaces the default agent instructions. */
|
|
108
|
+
instructions?: string;
|
|
109
|
+
/** Default timeout for `turn()`. Default: 60 seconds. */
|
|
110
|
+
turnTimeoutMs?: number;
|
|
111
|
+
/** Receives connection and session events for every player on this server. */
|
|
112
|
+
onEvent?: EventListener;
|
|
113
|
+
/** Where the SDK writes warnings. Nothing is written when omitted. */
|
|
114
|
+
logger?: Logger;
|
|
115
|
+
/**
|
|
116
|
+
* SQLite file of actions each character has already called.
|
|
117
|
+
* Later views include that session's `done` list.
|
|
118
|
+
* Defaults to `.wsi/agent-memory.sqlite` in the current directory.
|
|
119
|
+
* Pass false to keep no action history.
|
|
120
|
+
*/
|
|
121
|
+
memoryPath?: string | false;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type { AgentServerOptions as A, EventListener as E, InlinePlatform as I, Logger as L, StartOptions as S, TurnOptions as T, AgentHandle as a, AgentEvent as b, TurnResult as c, StartSessionRequest as d };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
2
|
+
import { TokenUsage, ToolDescriptor, TurnReport } from './protocol.js';
|
|
3
|
+
|
|
4
|
+
type Logger = {
|
|
5
|
+
debug?: (message: string, extra?: unknown) => void;
|
|
6
|
+
info?: (message: string, extra?: unknown) => void;
|
|
7
|
+
warn?: (message: string, extra?: unknown) => void;
|
|
8
|
+
error?: (message: string, extra?: unknown) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
type TurnResult = {
|
|
12
|
+
action: string;
|
|
13
|
+
args: Record<string, unknown>;
|
|
14
|
+
result: unknown;
|
|
15
|
+
attempts: number;
|
|
16
|
+
usage: TokenUsage;
|
|
17
|
+
};
|
|
18
|
+
type AgentEvent = {
|
|
19
|
+
type: "action";
|
|
20
|
+
sessionId: string;
|
|
21
|
+
action: string;
|
|
22
|
+
args: Record<string, unknown>;
|
|
23
|
+
} | {
|
|
24
|
+
type: "retry";
|
|
25
|
+
sessionId: string;
|
|
26
|
+
action: string;
|
|
27
|
+
args: Record<string, unknown>;
|
|
28
|
+
error: string;
|
|
29
|
+
} | {
|
|
30
|
+
type: "turnFailed";
|
|
31
|
+
sessionId: string;
|
|
32
|
+
error: string;
|
|
33
|
+
action?: string;
|
|
34
|
+
args?: Record<string, unknown>;
|
|
35
|
+
} | {
|
|
36
|
+
type: "disconnected";
|
|
37
|
+
} | {
|
|
38
|
+
type: "reconnected";
|
|
39
|
+
};
|
|
40
|
+
type EventListener = (event: AgentEvent) => void;
|
|
41
|
+
type TurnOptions = {
|
|
42
|
+
/** Overrides the server default. The turn rejects with TurnTimeoutError when exceeded. */
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
/** Abort the wait for the platform's turn result. */
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
};
|
|
47
|
+
type AgentHandle = {
|
|
48
|
+
/** Session id for this started player. */
|
|
49
|
+
readonly sessionId: string;
|
|
50
|
+
/**
|
|
51
|
+
* Grant this session one turn. Resolves with what the agent did, or rejects
|
|
52
|
+
* after a second failed action, a timeout, or a lost connection.
|
|
53
|
+
*/
|
|
54
|
+
turn(options?: TurnOptions): Promise<TurnResult>;
|
|
55
|
+
/** Stop the remote agent for this session. The AgentServer stays up. */
|
|
56
|
+
stop(): Promise<void>;
|
|
57
|
+
/** Subscribe to this session's action, retry, and failure events. Returns unsubscribe. */
|
|
58
|
+
on(listener: EventListener): () => void;
|
|
59
|
+
};
|
|
60
|
+
type StartOptions = {
|
|
61
|
+
/** Label stored with the session. Defaults to the class name. */
|
|
62
|
+
name?: string;
|
|
63
|
+
/** Added to the model prompt for this player only. */
|
|
64
|
+
goal?: string;
|
|
65
|
+
/** Extra instructions appended to the default agent prompt. */
|
|
66
|
+
instructions?: string;
|
|
67
|
+
};
|
|
68
|
+
type StartSessionRequest = {
|
|
69
|
+
sessionId: string;
|
|
70
|
+
sessionToken: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
tools: ToolDescriptor[];
|
|
73
|
+
instructions: string;
|
|
74
|
+
goal?: string;
|
|
75
|
+
logger?: Logger;
|
|
76
|
+
transport: Transport;
|
|
77
|
+
onTurnResult: (result: TurnReport) => void;
|
|
78
|
+
onEvent?: (event: AgentEvent) => void;
|
|
79
|
+
onUsage?: (tokens: number) => void | Promise<void>;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* In-process platform used by `@whitesands/agent-player/testing` and local runs.
|
|
83
|
+
* The hosted platform speaks the same session contract over WebSocket.
|
|
84
|
+
*/
|
|
85
|
+
type InlinePlatform = {
|
|
86
|
+
startSession(request: StartSessionRequest): Promise<void>;
|
|
87
|
+
stopSession(sessionId: string): Promise<void>;
|
|
88
|
+
close?(): Promise<void>;
|
|
89
|
+
};
|
|
90
|
+
type AgentServerOptions = {
|
|
91
|
+
/**
|
|
92
|
+
* Platform key naming the billing account.
|
|
93
|
+
* Defaults to the `WSI_PLATFORM_KEY` environment variable.
|
|
94
|
+
* Not required when `inline` is set.
|
|
95
|
+
*/
|
|
96
|
+
platformKey?: string;
|
|
97
|
+
/**
|
|
98
|
+
* WebSocket URL of the platform.
|
|
99
|
+
* Defaults to `WSI_PLATFORM_URL`, then `ws://127.0.0.1:8787`.
|
|
100
|
+
*/
|
|
101
|
+
platformUrl?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Skip the outbound WebSocket and run against this in-process platform.
|
|
104
|
+
* `createTestPlatform` from `@whitesands/agent-player/testing` returns one.
|
|
105
|
+
*/
|
|
106
|
+
inline?: InlinePlatform;
|
|
107
|
+
/** Replaces the default agent instructions. */
|
|
108
|
+
instructions?: string;
|
|
109
|
+
/** Default timeout for `turn()`. Default: 60 seconds. */
|
|
110
|
+
turnTimeoutMs?: number;
|
|
111
|
+
/** Receives connection and session events for every player on this server. */
|
|
112
|
+
onEvent?: EventListener;
|
|
113
|
+
/** Where the SDK writes warnings. Nothing is written when omitted. */
|
|
114
|
+
logger?: Logger;
|
|
115
|
+
/**
|
|
116
|
+
* SQLite file of actions each character has already called.
|
|
117
|
+
* Later views include that session's `done` list.
|
|
118
|
+
* Defaults to `.wsi/agent-memory.sqlite` in the current directory.
|
|
119
|
+
* Pass false to keep no action history.
|
|
120
|
+
*/
|
|
121
|
+
memoryPath?: string | false;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type { AgentServerOptions as A, EventListener as E, InlinePlatform as I, Logger as L, StartOptions as S, TurnOptions as T, AgentHandle as a, AgentEvent as b, TurnResult as c, StartSessionRequest as d };
|
package/package.json
CHANGED
|
@@ -1,6 +1,80 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whitesands/agent-player",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Start a remote game agent from a player class. The SDK owns the MCP server; the model stays on the platform.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/White-Sands-Interactive/WSI-Node-Framework.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/White-Sands-Interactive/WSI-Node-Framework#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/White-Sands-Interactive/WSI-Node-Framework/issues"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"main": "./dist/index.cjs",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
},
|
|
24
|
+
"./testing": {
|
|
25
|
+
"types": "./dist/testing.d.ts",
|
|
26
|
+
"import": "./dist/testing.js",
|
|
27
|
+
"require": "./dist/testing.cjs"
|
|
28
|
+
},
|
|
29
|
+
"./protocol": {
|
|
30
|
+
"types": "./dist/protocol.d.ts",
|
|
31
|
+
"import": "./dist/protocol.js",
|
|
32
|
+
"require": "./dist/protocol.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"CHANGELOG.md"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": false,
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"workspaces": [
|
|
46
|
+
"examples/*"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup && npm run build -w @wsi/forest-example && npm run build -w @wsi/courier-example && npm run build -w @wsi/ferry-example",
|
|
50
|
+
"docs": "typedoc",
|
|
51
|
+
"clean": "rm -rf dist docs",
|
|
52
|
+
"version-packages": "changeset version",
|
|
53
|
+
"release": "tsup && npm publish --access public",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"forest": "npm run start -w @wsi/forest-example --",
|
|
57
|
+
"courier": "npm run start -w @wsi/courier-example --",
|
|
58
|
+
"ferry": "npm run start -w @wsi/ferry-example --"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=20"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
65
|
+
"better-sqlite3": "^12.11.1",
|
|
66
|
+
"ws": "^8.18.1",
|
|
67
|
+
"zod": "^3.24.2",
|
|
68
|
+
"zod-to-json-schema": "^3.24.5"
|
|
69
|
+
},
|
|
70
|
+
"devDependencies": {
|
|
71
|
+
"@changesets/cli": "^2.29.2",
|
|
72
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
73
|
+
"@types/node": "^22.13.10",
|
|
74
|
+
"@types/ws": "^8.18.0",
|
|
75
|
+
"tsup": "^8.4.0",
|
|
76
|
+
"typedoc": "^0.28.1",
|
|
77
|
+
"typescript": "^5.8.2",
|
|
78
|
+
"vitest": "^3.0.9"
|
|
79
|
+
}
|
|
80
|
+
}
|