@agentmessier/openclaw-agent-messier 0.3.0 → 0.3.1

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/src/watcher.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  /**
2
- * Soccer team-observation watcher — background SSE loop that feeds the agent.
2
+ * Autoplay observation watcher — a background SSE loop that feeds the agent.
3
3
  *
4
- * Subscribes to GET /matches/:id/agents/:agentId/observe, which streams a
5
- * TeamView (the agent's whole side) every tick (10 Hz). We never block the
6
- * match: the reader keeps only the LATEST view; a single in-flight delivery
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 to your play tool with a move for every player you control. ` +
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): the legacy
102
- // plugin-side rendering.
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
- `${describeTeam(v, mode)}\n\n` +
106
- `Decide and act now: make ONE soccer_play call with a move for every player you control. ` +
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 url = `${base}/matches/${encodeURIComponent(cfg.matchId)}/agents/${encodeURIComponent(cfg.agentId)}/observe`;
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
- // The match's spec snapshot (instructions frozen per game). Normally arrives
126
- // as the SSE `event: spec` handshake; the guard below lazily fetches it when
127
- // a frame shows up first (handshake lost / pre-envelope server) at most one
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(`[agentnet-soccer] spec recovered via API (rulesVersion ${s.rulesVersion})`); } })
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(`[agentnet-soccer] deliver failed: ${String(e)}`))
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(`[agentnet-soccer] re-claim failed: ${String(e)}`); }
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
  }