@agentmessier/openclaw-agent-messier 0.3.2 → 0.3.4
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/package.json +1 -1
- package/src/generate.test.ts +59 -2
- package/src/generate.ts +28 -4
- package/src/tools.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentmessier/openclaw-agent-messier",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Agent Messier multi-venue client for OpenClaw \u2014 play games and work tasks on the AgentNet platform (soccer today; venues discovered from the marketplace registry)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/generate.test.ts
CHANGED
|
@@ -17,8 +17,9 @@ const GOLF_SPEC: GameSpec = {
|
|
|
17
17
|
client: {
|
|
18
18
|
prefix: "golf", noun: "round",
|
|
19
19
|
lobby: { tool: "golf_rounds", route: "/rounds", params: {}, summary: "List rounds." },
|
|
20
|
-
join: { tool: "golf_join", route: "/quickround", seatRoute: "/rounds/{matchId}/join", params: { holes: { type: "integer" } }, seat: { id: "matchId", token: "token", controls: "playerIds" }, summary: "Join a round." },
|
|
20
|
+
join: { tool: "golf_join", route: "/quickround", seatRoute: "/rounds/{matchId}/join", params: { holes: { type: "integer" }, matchId: { type: "string" } }, seat: { id: "matchId", token: "token", controls: "playerIds" }, summary: "Join a round." },
|
|
21
21
|
autoplay: { tool: "golf_autoplay", summary: "Hands-free." },
|
|
22
|
+
leave: { tool: "golf_leave", route: "/rounds/{matchId}/leave", summary: "Leave the round (forfeit if live)." },
|
|
22
23
|
observe: { tool: "golf_observe", params: {}, summary: "See the course." },
|
|
23
24
|
act: { tool: "golf_play", params: { club: { type: "string" } }, summary: "Swing." },
|
|
24
25
|
},
|
|
@@ -38,7 +39,7 @@ describe("generated tool surface comes entirely from the spec", () => {
|
|
|
38
39
|
_resetSeats();
|
|
39
40
|
const tools = generateVenueTools(GOLF_VENUE, GOLF_SPEC, cfg());
|
|
40
41
|
const names = tools.map(t => t.name);
|
|
41
|
-
expect(names).toEqual(["golf_rounds", "golf_join", "golf_observe", "golf_play"]);
|
|
42
|
+
expect(names).toEqual(["golf_rounds", "golf_join", "golf_observe", "golf_play", "golf_leave"]);
|
|
42
43
|
|
|
43
44
|
// The act tool is BATCH (golf seats players) and carries the spec's action enum.
|
|
44
45
|
const play = tools.find(t => t.name === "golf_play")!;
|
|
@@ -122,3 +123,59 @@ describe("realtime-venue detection (which venue the autoplay watcher drives)", (
|
|
|
122
123
|
expect(rt?.spec.client?.act.tool).toBe("soccer_play");
|
|
123
124
|
});
|
|
124
125
|
});
|
|
126
|
+
|
|
127
|
+
describe("join-by-id and leave (the lifecycle gap closed in VA-6)", () => {
|
|
128
|
+
afterEach(() => { vi.unstubAllGlobals(); _resetSeats(); });
|
|
129
|
+
|
|
130
|
+
async function exec(name: string, params: unknown) {
|
|
131
|
+
const t = generateVenueTools(GOLF_VENUE, GOLF_SPEC, cfg()).find(x => x.name === name)!;
|
|
132
|
+
const r = await t.execute("id", params as any) as { content: { text: string }[] };
|
|
133
|
+
return JSON.parse(r.content[0]!.text);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
it("the join tool routes a matchId param through seatRoute (not the body)", async () => {
|
|
137
|
+
let url = "", body: any = null;
|
|
138
|
+
vi.stubGlobal("fetch", vi.fn(async (u: any, init?: any) => {
|
|
139
|
+
url = String(u); body = JSON.parse(init.body);
|
|
140
|
+
return { ok: true, json: async () => ({ token: "t", playerIds: ["p1"] }) } as any;
|
|
141
|
+
}));
|
|
142
|
+
const out = await exec("golf_join", { matchId: "r9", holes: 9 });
|
|
143
|
+
expect(url).toBe("http://golf.test/rounds/r9/join"); // seatRoute, not /quickround
|
|
144
|
+
expect(body.holes).toBe(9);
|
|
145
|
+
expect(body.matchId).toBeUndefined(); // matchId is routing, never a body field
|
|
146
|
+
expect(out.joined).toBe("r9");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("omitting matchId quickmatches (find-or-create) via the join route", async () => {
|
|
150
|
+
let url = "";
|
|
151
|
+
vi.stubGlobal("fetch", vi.fn(async (u: any) => {
|
|
152
|
+
url = String(u);
|
|
153
|
+
return { ok: true, json: async () => ({ matchId: "r1", token: "t", playerIds: ["p1"] }) } as any;
|
|
154
|
+
}));
|
|
155
|
+
await exec("golf_join", { holes: 9 });
|
|
156
|
+
expect(url).toBe("http://golf.test/quickround");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("the leave tool posts to the leave route and frees the seat locally", async () => {
|
|
160
|
+
// seat first so there's a match to leave
|
|
161
|
+
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => ({ matchId: "r5", token: "t", playerIds: ["p1"] }) }) as any));
|
|
162
|
+
await joinVenue(GOLF_VENUE, GOLF_SPEC, cfg(), { matchId: "r5" });
|
|
163
|
+
expect(session.matchId).toBe("r5");
|
|
164
|
+
|
|
165
|
+
let url = "", method = "";
|
|
166
|
+
vi.stubGlobal("fetch", vi.fn(async (u: any, init?: any) => {
|
|
167
|
+
url = String(u); method = init?.method;
|
|
168
|
+
return { ok: true, json: async () => ({ left: "r5", forfeit: true, winner: "away" }) } as any;
|
|
169
|
+
}));
|
|
170
|
+
const out = await exec("golf_leave", {});
|
|
171
|
+
expect(method).toBe("POST");
|
|
172
|
+
expect(url).toBe("http://golf.test/rounds/r5/leave");
|
|
173
|
+
expect(out.left).toBe("r5");
|
|
174
|
+
expect(session.matchId).toBeNull(); // freed → ready to join elsewhere
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("leaving when not in a match is a no-op error, not a crash", async () => {
|
|
178
|
+
const out = await exec("golf_leave", {});
|
|
179
|
+
expect(out.error).toMatch(/not in a match/i);
|
|
180
|
+
});
|
|
181
|
+
});
|
package/src/generate.ts
CHANGED
|
@@ -120,9 +120,15 @@ export function generateVenueTools(venue: Venue, spec: GameSpec, cfg: PluginCfg)
|
|
|
120
120
|
out.push({ name: s.tool, label: s.tool, description: s.summary, parameters: paramsSchema(s.params ?? {}),
|
|
121
121
|
async execute(_id, params) {
|
|
122
122
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
// matchId (if the spec advertises it) targets a SPECIFIC room via
|
|
124
|
+
// seatRoute; it's routing, not a body field, so pull it out of params.
|
|
125
|
+
const { matchId: mid, ...rest } = (params ?? {}) as Record<string, unknown>;
|
|
126
|
+
const seat = await joinVenue(venue, spec, cfg, { matchId: typeof mid === "string" && mid ? mid : undefined, params: rest });
|
|
127
|
+
const watchUrl = `${base}/matches/${seat.id}/view`;
|
|
128
|
+
return ok({ joined: seat.id, yours: seat.controls, watchUrl, managerUrl: seat.managerUrl,
|
|
129
|
+
// Lead with the watch link so the agent SHOWS it to its human — that's
|
|
130
|
+
// how they actually find the match. Then the play loop.
|
|
131
|
+
note: `Seated in ${seat.id}. TELL YOUR HUMAN they can watch live here: ${watchUrl}${seat.managerUrl ? ` (manager console: ${seat.managerUrl})` : ""}. Then observe with ${c.observe.tool} and play with ${c.act.tool}.` });
|
|
126
132
|
} catch (e) { return ok({ error: String(e instanceof Error ? e.message : e) }); }
|
|
127
133
|
} } as AnyAgentTool);
|
|
128
134
|
}
|
|
@@ -180,6 +186,23 @@ export function generateVenueTools(venue: Venue, spec: GameSpec, cfg: PluginCfg)
|
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
// leave — exit the current match (a forfeit if it's live). Frees the seat so
|
|
190
|
+
// the agent can join another room. Server route from spec.client.leave.
|
|
191
|
+
if (c.leave) {
|
|
192
|
+
const s = c.leave;
|
|
193
|
+
out.push({ name: s.tool, label: s.tool, description: s.summary, parameters: paramsSchema(s.params ?? {}),
|
|
194
|
+
async execute() {
|
|
195
|
+
const seat = seats.get(venue.id) ?? {}; const d = did(venue.id, cfg);
|
|
196
|
+
if (!seat.id) return ok({ error: "you are not in a match" });
|
|
197
|
+
const path = sub(s.route, { matchId: seat.id, did: d });
|
|
198
|
+
const r = await vfetch(base, path, { cfg, did: d, method: "POST", body: { agentId: d }, token: seat.token });
|
|
199
|
+
// Clear our seat either way — a failed leave shouldn't leave us wedged.
|
|
200
|
+
seats.delete(venue.id); session.matchId = null; session.players = []; session.token = null;
|
|
201
|
+
if (!r.ok) return ok({ error: r.data?.error ?? `leave ${r.status}`, note: "seat cleared locally; you can try joining again" });
|
|
202
|
+
return ok({ left: r.data?.left ?? seat.id, ...r.data, hint: `you're free — ${c.join?.tool ?? "join"} another room` });
|
|
203
|
+
} } as AnyAgentTool);
|
|
204
|
+
}
|
|
205
|
+
|
|
183
206
|
return out;
|
|
184
207
|
}
|
|
185
208
|
|
|
@@ -226,10 +249,11 @@ const DEFAULT_SPECS: Record<string, GameSpec> = {
|
|
|
226
249
|
client: {
|
|
227
250
|
prefix: "soccer", noun: "match",
|
|
228
251
|
lobby: { tool: "soccer_matches", route: "/matches", params: { status: { type: "string", enum: ["live", "waiting", "ended"] } }, summary: "List soccer matches: live, open seats, scores." },
|
|
229
|
-
join: { tool: "soccer_join", route: "/quickmatch", seatRoute: "/matches/{matchId}/join", params: { teamSize: { type: "integer" }, team: { type: "string" }, name: { type: "string" }, nation: { type: "string" }, clan: { type: "string" }, style: { type: "string" } }, seat: { id: "matchId", token: "token", controls: "playerIds" }, summary: "Join a match and take a whole side
|
|
252
|
+
join: { tool: "soccer_join", route: "/quickmatch", seatRoute: "/matches/{matchId}/join", params: { teamSize: { type: "integer" }, team: { type: "string" }, name: { type: "string" }, nation: { type: "string" }, clan: { type: "string" }, style: { type: "string" }, matchId: { type: "string", description: "join THIS room (e.g. m160) instead of quickmatch" } }, seat: { id: "matchId", token: "token", controls: "playerIds" }, summary: "Join a match and take a whole side. Pass matchId for a specific room, else quickmatch." },
|
|
230
253
|
observe: { tool: "soccer_observe", params: {}, summary: "See the pitch from your side's POV: ball, your players, opponents, score." },
|
|
231
254
|
act: { tool: "soccer_play", params: { dir: { type: "array", items: { type: "number" } }, distance: { type: "number" }, power: { type: "number" }, say: { type: "string" } }, summary: "Order your players — a standing action per player (run/kick need dir)." },
|
|
232
255
|
autoplay: { tool: "soccer_autoplay", summary: "Hands-free play (handled by the watcher service)." },
|
|
256
|
+
leave: { tool: "soccer_leave", route: "/matches/{matchId}/leave", summary: "Leave your match (forfeit if live — opponent wins). Frees you to join another room." },
|
|
233
257
|
},
|
|
234
258
|
},
|
|
235
259
|
"taskmarket": {
|
package/src/tools.ts
CHANGED
|
@@ -63,6 +63,7 @@ export type GameSpec = {
|
|
|
63
63
|
observe: { tool: string; params?: Record<string, unknown>; summary: string };
|
|
64
64
|
act: { tool: string; params?: Record<string, unknown>; summary: string };
|
|
65
65
|
autoplay?: { tool: string; summary: string };
|
|
66
|
+
leave?: { tool: string; route: string; params?: Record<string, unknown>; summary: string } | null;
|
|
66
67
|
};
|
|
67
68
|
};
|
|
68
69
|
|