@agentmessier/openclaw-agent-messier 0.3.0 → 0.3.2
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/index.ts +86 -53
- package/package.json +1 -1
- package/src/generate.test.ts +124 -0
- package/src/generate.ts +275 -0
- package/src/spec.test.ts +16 -15
- package/src/state.ts +4 -1
- package/src/tools.ts +46 -384
- package/src/venues.test.ts +20 -16
- package/src/watcher.test.ts +19 -3
- package/src/watcher.ts +48 -21
package/src/watcher.test.ts
CHANGED
|
@@ -78,11 +78,11 @@ const VIEW_WITH_SUMMARY = { ...VIEW, summary: "⚽ SERVER-RENDERED SITUATION." }
|
|
|
78
78
|
|
|
79
79
|
describe("generic prompt — instructions + summary come from the server", () => {
|
|
80
80
|
it("uses the server's instructions and summary when a spec is cached", () => {
|
|
81
|
-
const p = prompt(VIEW_WITH_SUMMARY, "easy", undefined, SPEC);
|
|
81
|
+
const p = prompt(VIEW_WITH_SUMMARY, "easy", undefined, SPEC, "golf_play");
|
|
82
82
|
expect(p).toContain("SERVER-RENDERED SITUATION");
|
|
83
83
|
expect(p).toContain("SERVER PLAY GUIDANCE");
|
|
84
|
-
// tool-calling host: act via the
|
|
85
|
-
expect(p
|
|
84
|
+
// tool-calling host: act via the venue's act tool, named from the spec
|
|
85
|
+
expect(p).toContain("golf_play");
|
|
86
86
|
// the plugin's hardcoded soccer prose is gone on this path
|
|
87
87
|
expect(p).not.toContain("YOU ATTACK +x: opponent goal at x=+52.5");
|
|
88
88
|
});
|
|
@@ -119,3 +119,19 @@ describe("parseSseBlock — named events for the handshake", () => {
|
|
|
119
119
|
expect(parseSseBlock("data: ").data).toBeUndefined();
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
|
+
|
|
123
|
+
import { observeUrl } from "./watcher.js";
|
|
124
|
+
|
|
125
|
+
describe("observeUrl — venue-agnostic observe endpoint (VA-4)", () => {
|
|
126
|
+
it("substitutes spec.routes.observe with matchId + did", () => {
|
|
127
|
+
const spec = { routes: { observe: "/matches/{matchId}/agents/{did}/observe" } } as any;
|
|
128
|
+
expect(observeUrl(spec, "m7", "did:wba:me")).toBe("/matches/m7/agents/did%3Awba%3Ame/observe");
|
|
129
|
+
});
|
|
130
|
+
it("uses a non-soccer venue's route shape unchanged", () => {
|
|
131
|
+
const spec = { routes: { observe: "/golf/{matchId}/player/{did}/look" } } as any;
|
|
132
|
+
expect(observeUrl(spec, "g3", "alice")).toBe("/golf/g3/player/alice/look");
|
|
133
|
+
});
|
|
134
|
+
it("falls back to the soccer-literal route when no spec", () => {
|
|
135
|
+
expect(observeUrl(null, "m9", "bob")).toBe("/matches/m9/agents/bob/observe");
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/watcher.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Autoplay observation watcher — a background SSE loop that feeds the agent.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Venue-agnostic at runtime: the observe endpoint comes from spec.routes, the
|
|
5
|
+
* move prompt is the server's spec.instructions + rendered summary, and the act
|
|
6
|
+
* tool it names comes from spec.client.act.tool (passed as cfg.actTool). The
|
|
7
|
+
* soccer specifics that remain are only the frame TYPE (TeamView) and the
|
|
8
|
+
* describeTeam fallback renderer used when a server sends no instructions.
|
|
9
|
+
*
|
|
10
|
+
* Subscribes to the observe route, which streams a view (the agent's whole side
|
|
11
|
+
* in soccer) every tick. We never block the match: the reader keeps only the
|
|
12
|
+
* LATEST view; a single in-flight delivery
|
|
7
13
|
* feeds the agent the freshest state one decision at a time. While the agent is
|
|
8
14
|
* thinking, new frames just update `latest`; when it finishes it gets the
|
|
9
15
|
* newest state. Once the match ends, delivery stops (no prompt = no LLM call).
|
|
@@ -24,6 +30,11 @@ export type WatcherCfg = {
|
|
|
24
30
|
mode?: "easy" | "advanced" | "both";
|
|
25
31
|
/** Human-editable strategy.md injected into the move prompt (Phase 5). */
|
|
26
32
|
strategyFile?: string;
|
|
33
|
+
/** The venue's act tool (spec.client.act.tool) named in the move prompt.
|
|
34
|
+
* Default "soccer_play" so a pre-VA-5 caller is unchanged. */
|
|
35
|
+
actTool?: string;
|
|
36
|
+
/** Log tag for this venue's watcher (e.g. "agent-soccer"). Default "agentnet". */
|
|
37
|
+
label?: string;
|
|
27
38
|
};
|
|
28
39
|
|
|
29
40
|
// ── human-editable strategy (Phase 5) ────────────────────────────────────────
|
|
@@ -80,7 +91,7 @@ export function parseSseBlock(block: string): { event?: string; data?: string }
|
|
|
80
91
|
return event !== undefined ? { event, ...(data !== undefined ? { data } : {}) } : (data !== undefined ? { data } : {});
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advanced" | "both", strategyFile?: string, spec?: GameSpec | null): string {
|
|
94
|
+
export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advanced" | "both", strategyFile?: string, spec?: GameSpec | null, actTool = "soccer_play"): string {
|
|
84
95
|
const standing = strategyText(strategyFile);
|
|
85
96
|
const stratBlock = standing ? `## Your manager's standing instructions\n${standing}\n\n` : "";
|
|
86
97
|
const ins = spec?.instructions;
|
|
@@ -94,20 +105,31 @@ export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advan
|
|
|
94
105
|
stratBlock +
|
|
95
106
|
`${v.summary}\n\n` +
|
|
96
107
|
`${ins.play}\n\n` +
|
|
97
|
-
`Decide and act now: make ONE call
|
|
108
|
+
`Decide and act now: make ONE ${actTool} call with a move for every player you control. ` +
|
|
98
109
|
`Each is a standing order until you change it.`
|
|
99
110
|
);
|
|
100
111
|
}
|
|
101
|
-
// Fallback (pre-envelope server or handshake not yet arrived)
|
|
102
|
-
//
|
|
112
|
+
// Fallback (pre-envelope server or handshake not yet arrived). describeTeam is
|
|
113
|
+
// the soccer-specific renderer — only valid for a TeamView; for any other
|
|
114
|
+
// venue with no server instructions, dump the raw view rather than crash.
|
|
115
|
+
const rendered = Array.isArray(v.mine) ? describeTeam(v, mode) : JSON.stringify(v);
|
|
103
116
|
return (
|
|
104
117
|
stratBlock +
|
|
105
|
-
`${
|
|
106
|
-
`Decide and act now: make ONE
|
|
118
|
+
`${rendered}\n\n` +
|
|
119
|
+
`Decide and act now: make ONE ${actTool} call with a move for every player you control. ` +
|
|
107
120
|
`Each is a standing order until you change it.`
|
|
108
121
|
);
|
|
109
122
|
}
|
|
110
123
|
|
|
124
|
+
/** The observe path for a venue, from spec.routes ({matchId}/{did} substituted),
|
|
125
|
+
* with the soccer-literal fallback when no spec is reachable — so the watcher
|
|
126
|
+
* is endpoint-agnostic (a golf venue would drive the same loop) but never
|
|
127
|
+
* breaks offline. Returns the path (no host); caller prepends the base. */
|
|
128
|
+
export function observeUrl(spec: GameSpec | null, matchId: string, did: string): string {
|
|
129
|
+
const tpl = spec?.routes?.["observe"] ?? "/matches/{matchId}/agents/{did}/observe";
|
|
130
|
+
return tpl.replace("{matchId}", encodeURIComponent(matchId)).replace("{did}", encodeURIComponent(did));
|
|
131
|
+
}
|
|
132
|
+
|
|
111
133
|
export async function startObserveWatcher(
|
|
112
134
|
cfg: WatcherCfg,
|
|
113
135
|
deliver: (msg: string) => void | Promise<void>,
|
|
@@ -115,25 +137,30 @@ export async function startObserveWatcher(
|
|
|
115
137
|
): Promise<void> {
|
|
116
138
|
const { signal, logger } = options;
|
|
117
139
|
const base = (cfg.serverUrl ?? "http://localhost:3010").replace(/\/$/, "");
|
|
118
|
-
const
|
|
140
|
+
const tag = cfg.label ?? "agentnet"; // venue-derived log prefix
|
|
141
|
+
const actTool = cfg.actTool ?? "soccer_play";
|
|
142
|
+
|
|
143
|
+
// Fetch the spec up front so the observe endpoint comes from spec.routes
|
|
144
|
+
// (venue-agnostic) rather than a literal /matches path — falls back to the
|
|
145
|
+
// soccer-literal route when no spec is reachable. Also seeds the prompt's
|
|
146
|
+
// instructions; the SSE `event: spec` handshake still refreshes it.
|
|
147
|
+
let spec: GameSpec | null = await fetchMatchSpec({ serverUrl: cfg.serverUrl } as PluginCfg, cfg.matchId);
|
|
148
|
+
const url = base + observeUrl(spec, cfg.matchId, cfg.agentId);
|
|
119
149
|
|
|
120
150
|
let latest: TeamView | null = null;
|
|
121
151
|
let latestSeq = 0;
|
|
122
152
|
let deliveredSeq = -1;
|
|
123
153
|
let busy = false;
|
|
124
154
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// in-flight attempt, re-tried on later frames until it lands. A null spec
|
|
129
|
-
// only degrades the prompt to the legacy rendering; it never blocks play.
|
|
130
|
-
let spec: GameSpec | null = null;
|
|
155
|
+
// If the up-front fetch failed, the guard below lazily retries when a frame
|
|
156
|
+
// shows up (handshake lost / pre-envelope server) — at most one in-flight
|
|
157
|
+
// attempt. A null spec only degrades the prompt; it never blocks play.
|
|
131
158
|
let specFetching = false;
|
|
132
159
|
function ensureSpec() {
|
|
133
160
|
if (spec !== null || specFetching) return;
|
|
134
161
|
specFetching = true;
|
|
135
162
|
fetchMatchSpec({ serverUrl: cfg.serverUrl } as PluginCfg, cfg.matchId)
|
|
136
|
-
.then((s) => { if (s) { spec = s; logger?.info(`[
|
|
163
|
+
.then((s) => { if (s) { spec = s; logger?.info(`[${tag}] spec recovered via API (rulesVersion ${s.rulesVersion})`); } })
|
|
137
164
|
.finally(() => { specFetching = false; });
|
|
138
165
|
}
|
|
139
166
|
|
|
@@ -144,8 +171,8 @@ export async function startObserveWatcher(
|
|
|
144
171
|
busy = true;
|
|
145
172
|
const seq = latestSeq;
|
|
146
173
|
const obs = latest;
|
|
147
|
-
Promise.resolve(deliver(prompt(obs, cfg.mode ?? "easy", cfg.strategyFile, spec)))
|
|
148
|
-
.catch((e) => logger?.error(`[
|
|
174
|
+
Promise.resolve(deliver(prompt(obs, cfg.mode ?? "easy", cfg.strategyFile, spec, actTool)))
|
|
175
|
+
.catch((e) => logger?.error(`[${tag}] deliver failed: ${String(e)}`))
|
|
149
176
|
.finally(() => { deliveredSeq = seq; busy = false; maybeDeliver(); });
|
|
150
177
|
}
|
|
151
178
|
|
|
@@ -156,7 +183,7 @@ export async function startObserveWatcher(
|
|
|
156
183
|
const res = await fetch(url, { headers: { Accept: "text/event-stream" }, signal });
|
|
157
184
|
if (res.status === 404 && options.onReclaim) {
|
|
158
185
|
// Server forgot us (restart) — re-claim our players, then reconnect.
|
|
159
|
-
try { await options.onReclaim(); } catch (e) { logger?.warn(`[
|
|
186
|
+
try { await options.onReclaim(); } catch (e) { logger?.warn(`[${tag}] re-claim failed: ${String(e)}`); }
|
|
160
187
|
await backoff(attempt++, signal);
|
|
161
188
|
continue;
|
|
162
189
|
}
|