@agentmessier/openclaw-agent-messier 0.3.11 → 0.3.12
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 +47 -8
- package/package.json +1 -1
- package/src/decide.ts +111 -14
- package/src/watcher.ts +8 -2
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { memberTools, venuesTool, agentIdOf, identityOf, venueUrl, type PluginCf
|
|
|
3
3
|
import { defaultVenueTools, defaultRealtimeVenue, joinVenue } from "./src/generate.js";
|
|
4
4
|
import { startObserveWatcher } from "./src/watcher.js";
|
|
5
5
|
import { session } from "./src/state.js";
|
|
6
|
-
import { runAutoplayTurn, STRICT_JSON_DIRECTIVE } from "./src/decide.js";
|
|
6
|
+
import { runAutoplayTurn, postDecisionReport, STRICT_JSON_DIRECTIVE } from "./src/decide.js";
|
|
7
7
|
|
|
8
8
|
/** A lobby row is "ours" if it references our agentId anywhere (soccer puts it in
|
|
9
9
|
* sides.home/away; a generic venue may shape it differently). Deep, shape-blind. */
|
|
@@ -55,6 +55,10 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
55
55
|
|
|
56
56
|
let controller: AbortController | null = null;
|
|
57
57
|
let poller: ReturnType<typeof setInterval> | null = null;
|
|
58
|
+
// The per-match session key currently in play. We persist ONE session per match
|
|
59
|
+
// (so the agent accumulates its own decision history); on LEAVING a match we
|
|
60
|
+
// delete it so a new match starts fresh and stale transcripts don't pile up.
|
|
61
|
+
let activeSessionKey: string | null = null;
|
|
58
62
|
|
|
59
63
|
api.registerService({
|
|
60
64
|
id: `agentnet-${venue.id}-watcher`,
|
|
@@ -75,11 +79,18 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
75
79
|
|
|
76
80
|
controller?.abort(); // leaving a previous room
|
|
77
81
|
controller = new AbortController();
|
|
82
|
+
// Leaving the previous match → drop its persistent session so the new
|
|
83
|
+
// match starts with fresh context (and stale transcripts don't pile up).
|
|
84
|
+
if (sessionKey && activeSessionKey && activeSessionKey !== `${sessionKey}:${seat.id}`) {
|
|
85
|
+
api.runtime.subagent.deleteSession({ sessionKey: activeSessionKey }).catch(() => {});
|
|
86
|
+
}
|
|
87
|
+
// ONE persistent session for THIS match, stable across all its turns.
|
|
88
|
+
activeSessionKey = sessionKey ? `${sessionKey}:${seat.id}` : null;
|
|
78
89
|
let move = 0;
|
|
79
90
|
// Fire-and-forget: the watcher runs until aborted or the gateway stops.
|
|
80
91
|
void startObserveWatcher(
|
|
81
92
|
{ serverUrl: cfg.serverUrl, matchId: seat.id!, agentId, mode: cfg.mode, strategyFile: cfg.strategyFile, actTool, label },
|
|
82
|
-
async ({ system, user }) => {
|
|
93
|
+
async ({ system, user, tick, clock }) => {
|
|
83
94
|
if (!sessionKey) {
|
|
84
95
|
ctx.logger.warn(`[${label}] no sessionKey configured; cannot deliver move prompts.`);
|
|
85
96
|
return;
|
|
@@ -90,34 +101,58 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
90
101
|
// 2-decisions-in-157s). soccer_play stays registered for interactive
|
|
91
102
|
// chat play; only AUTOPLAY changes to this server-driven loop.
|
|
92
103
|
//
|
|
93
|
-
//
|
|
94
|
-
// agent
|
|
104
|
+
// ONE persistent session per MATCH (sessionKey:matchId, stable across
|
|
105
|
+
// turns) so the agent accumulates context — its own prior decisions —
|
|
106
|
+
// and the session transcript is the full per-match request log. `turn`
|
|
107
|
+
// is kept only for the idempotency key and the context-growth cap.
|
|
95
108
|
const turn = move++;
|
|
109
|
+
const matchSessionKey = `${sessionKey}:${seat.id}`;
|
|
96
110
|
const idempotencyKey = `agentnet:${venue.id}:${seat.id}:${agentId}:${turn}`;
|
|
111
|
+
const did = session.did ?? agentId;
|
|
112
|
+
const sys = system || undefined;
|
|
113
|
+
const msg = `${user}${STRICT_JSON_DIRECTIVE}`;
|
|
97
114
|
// Mark when this prompt was handed to the agent: x-agent-decision-ms is
|
|
98
115
|
// the prompt→reply latency measured inside runAutoplayTurn.
|
|
99
116
|
session.promptDeliveredAt = Date.now();
|
|
100
117
|
const result = await runAutoplayTurn({
|
|
101
118
|
runtime: api.runtime,
|
|
102
|
-
sessionKey:
|
|
119
|
+
sessionKey: matchSessionKey,
|
|
120
|
+
turn,
|
|
103
121
|
idempotencyKey,
|
|
104
122
|
// The static rulebook (spec.instructions.system) rides the SYSTEM
|
|
105
123
|
// channel; the per-tick board is the user message. '' on the
|
|
106
124
|
// fallback path → no extra system prompt.
|
|
107
|
-
extraSystemPrompt:
|
|
125
|
+
extraSystemPrompt: sys,
|
|
108
126
|
// Steer the agent to reply with ONLY the moves JSON (no tool call).
|
|
109
|
-
message:
|
|
127
|
+
message: msg,
|
|
110
128
|
// 45s ceiling, matching the watcher's per-delivery watchdog backstop:
|
|
111
129
|
// a run that hasn't produced a decision by then is treated as stalled
|
|
112
130
|
// (was 300s, which let one hung run silence the team for 5 min — m171).
|
|
113
131
|
timeoutMs: 45_000,
|
|
114
132
|
matchId: seat.id!,
|
|
115
133
|
cfg,
|
|
116
|
-
did
|
|
134
|
+
did,
|
|
117
135
|
token: session.token,
|
|
118
136
|
base,
|
|
119
137
|
logger: ctx.logger,
|
|
120
138
|
});
|
|
139
|
+
// Report EVERY decision to the pitch — acted AND no-response — so the
|
|
140
|
+
// decision inspector sees no-response turns too. Best-effort, off the
|
|
141
|
+
// hot path, never throws.
|
|
142
|
+
void postDecisionReport(
|
|
143
|
+
{
|
|
144
|
+
tick,
|
|
145
|
+
clock,
|
|
146
|
+
prompt: { ...(sys ? { system: sys } : {}), user: msg },
|
|
147
|
+
outcome: result.outcome,
|
|
148
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
149
|
+
...(result.moves ? { moves: result.moves } : {}),
|
|
150
|
+
rawText: result.rawText,
|
|
151
|
+
latencyMs: result.latencyMs,
|
|
152
|
+
...(session.lastModel ? { model: session.lastModel } : {}),
|
|
153
|
+
},
|
|
154
|
+
{ base, matchId: seat.id!, agentId: did, token: session.token, logger: ctx.logger },
|
|
155
|
+
);
|
|
121
156
|
// act-verification by the natural signal: did we parse+post (or did the
|
|
122
157
|
// model act via the tool)? A parse-miss = "responded without acting" —
|
|
123
158
|
// log, keep standing orders, continue (never freeze). The watcher's own
|
|
@@ -186,6 +221,10 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
186
221
|
if (poller) { clearInterval(poller); poller = null; }
|
|
187
222
|
controller?.abort();
|
|
188
223
|
controller = null;
|
|
224
|
+
if (activeSessionKey) {
|
|
225
|
+
api.runtime.subagent.deleteSession({ sessionKey: activeSessionKey }).catch(() => {});
|
|
226
|
+
activeSessionKey = null;
|
|
227
|
+
}
|
|
189
228
|
session.joinAndWatch = null;
|
|
190
229
|
session.matchId = null;
|
|
191
230
|
},
|
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.12",
|
|
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/decide.ts
CHANGED
|
@@ -203,8 +203,10 @@ function blockText(content: unknown): string {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/** The text of the LAST assistant message in a getSessionMessages transcript.
|
|
206
|
-
*
|
|
207
|
-
*
|
|
206
|
+
* With a PERSISTENT per-match session the transcript GROWS across turns
|
|
207
|
+
* (user/assistant pairs accumulate), so we scan from the end and return the
|
|
208
|
+
* most recent assistant reply — never an earlier turn's. Returns "" when there
|
|
209
|
+
* is no assistant text. */
|
|
208
210
|
export function lastAssistantText(messages: unknown[]): string {
|
|
209
211
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
210
212
|
const m = messages[i];
|
|
@@ -270,11 +272,70 @@ export async function executeMoves(
|
|
|
270
272
|
return { posted: results.length, results };
|
|
271
273
|
}
|
|
272
274
|
|
|
275
|
+
// ── decision reporting: POST every turn's decision to the pitch ──────────────
|
|
276
|
+
|
|
277
|
+
export type DecisionReportDeps = {
|
|
278
|
+
base: string;
|
|
279
|
+
matchId: string;
|
|
280
|
+
/** Reporting agentId (session.did ?? agentId). */
|
|
281
|
+
agentId: string;
|
|
282
|
+
/** Seat token — sent as x-agent-token (same auth as executeMoves). */
|
|
283
|
+
token?: string | null | undefined;
|
|
284
|
+
fetch?: typeof fetch;
|
|
285
|
+
logger?: { warn: (m: string) => void };
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export type DecisionReport = {
|
|
289
|
+
tick: number;
|
|
290
|
+
clock: number;
|
|
291
|
+
prompt: { system?: string; user: string };
|
|
292
|
+
outcome: "acted" | "no_response";
|
|
293
|
+
reason?: string;
|
|
294
|
+
moves?: Move[];
|
|
295
|
+
rawText?: string;
|
|
296
|
+
latencyMs?: number;
|
|
297
|
+
model?: string;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* POST a decision report to `/matches/:id/agents/:agentId/decision` so EVERY
|
|
302
|
+
* turn — acted AND no-response — reaches the pitch's decision inspector. Auth is
|
|
303
|
+
* the seat token (x-agent-token), same as executeMoves. Best-effort and off the
|
|
304
|
+
* hot path: it NEVER throws (caller fires it `.catch`-free via this swallow) so a
|
|
305
|
+
* reporting failure can't disrupt play.
|
|
306
|
+
*/
|
|
307
|
+
export async function postDecisionReport(report: DecisionReport, deps: DecisionReportDeps): Promise<void> {
|
|
308
|
+
const f = deps.fetch ?? fetch;
|
|
309
|
+
const base = deps.base.replace(/\/$/, "");
|
|
310
|
+
const url = `${base}/matches/${encodeURIComponent(deps.matchId)}/agents/${encodeURIComponent(deps.agentId)}/decision`;
|
|
311
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
312
|
+
if (deps.token) headers["x-agent-token"] = deps.token;
|
|
313
|
+
try {
|
|
314
|
+
await f(url, { method: "POST", headers, body: JSON.stringify(report) });
|
|
315
|
+
} catch (e) {
|
|
316
|
+
deps.logger?.warn(`decision report POST failed: ${String(e)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
273
320
|
// ── the full autoplay turn: run → wait → read → parse → post ─────────────────
|
|
274
321
|
|
|
322
|
+
/** Context-growth safety cap. A persistent per-match session accumulates the
|
|
323
|
+
* agent's whole decision history (good: it remembers its own prior orders), but
|
|
324
|
+
* a long match would otherwise grow the transcript without bound. OpenClaw
|
|
325
|
+
* auto-compacts, but as a hard backstop we RESET the session (deleteSession +
|
|
326
|
+
* start fresh) once a single match exceeds this many turns. Tradeoff: the agent
|
|
327
|
+
* loses its accumulated in-match memory at the reset boundary, but context can
|
|
328
|
+
* never grow unbounded. Keep it generous enough that most matches never hit it. */
|
|
329
|
+
export const SESSION_RESET_TURN_CAP = 60;
|
|
330
|
+
|
|
275
331
|
export type AutoplayTurnDeps = {
|
|
276
332
|
runtime: PluginRuntime;
|
|
333
|
+
/** Stable PER-MATCH session key — persists across turns so the agent
|
|
334
|
+
* accumulates context (its own prior decisions) and the transcript is the
|
|
335
|
+
* full per-match request log. Reset only on leave/new-match/cap. */
|
|
277
336
|
sessionKey: string;
|
|
337
|
+
/** This match's turn ordinal (0-based). Used for the reset cap only. */
|
|
338
|
+
turn: number;
|
|
278
339
|
idempotencyKey: string;
|
|
279
340
|
/** The strict-JSON move prompt (the per-tick board) to deliver to the agent. */
|
|
280
341
|
message: string;
|
|
@@ -291,10 +352,29 @@ export type AutoplayTurnDeps = {
|
|
|
291
352
|
logger?: { info: (m: string) => void; warn: (m: string) => void; error: (m: string) => void };
|
|
292
353
|
};
|
|
293
354
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
355
|
+
/** Everything a decision REPORT needs, returned alongside the act outcome so the
|
|
356
|
+
* caller (index.ts) can POST one /decision report per turn — for acted AND
|
|
357
|
+
* no-response turns — using the tick/clock it has from the delivered frame. */
|
|
358
|
+
export type AutoplayTurnObservability = {
|
|
359
|
+
/** "acted" when the team was moved this cycle (direct post OR the model's own
|
|
360
|
+
* tool call); "no_response" when the run finished without a usable decision. */
|
|
361
|
+
outcome: "acted" | "no_response";
|
|
362
|
+
/** No-act reason (parse miss / empty reply); undefined when acted. */
|
|
363
|
+
reason?: string;
|
|
364
|
+
/** The agent's raw reply text (last assistant message), "" if unreadable. */
|
|
365
|
+
rawText: string;
|
|
366
|
+
/** Parsed moves (present only when we parsed + posted; undefined on tool/miss). */
|
|
367
|
+
moves?: Move[];
|
|
368
|
+
/** Measured prompt→reply latency (ms). */
|
|
369
|
+
latencyMs: number;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export type AutoplayTurnResult = AutoplayTurnObservability &
|
|
373
|
+
(
|
|
374
|
+
| { acted: true; via: "post"; posted: number }
|
|
375
|
+
| { acted: true; via: "tool" } // the model called soccer_play itself
|
|
376
|
+
| { acted: false } // responded without acting (parse miss / empty reply)
|
|
377
|
+
);
|
|
298
378
|
|
|
299
379
|
/**
|
|
300
380
|
* Force exactly one agent turn for a delivered situation and act on its reply.
|
|
@@ -306,6 +386,15 @@ export async function runAutoplayTurn(deps: AutoplayTurnDeps): Promise<AutoplayT
|
|
|
306
386
|
// act-verification baseline: if soccer_play runs during this turn it advances
|
|
307
387
|
// session.lastActAt past this — our cue to NOT double-post.
|
|
308
388
|
const actAtBefore = session.lastActAt;
|
|
389
|
+
|
|
390
|
+
// Context-growth backstop: once a single match's persistent session exceeds the
|
|
391
|
+
// cap, drop it so the next run starts fresh — bounds transcript size at the
|
|
392
|
+
// cost of the agent's accumulated in-match memory (see SESSION_RESET_TURN_CAP).
|
|
393
|
+
if (deps.turn > 0 && deps.turn % SESSION_RESET_TURN_CAP === 0) {
|
|
394
|
+
await runtime.subagent.deleteSession({ sessionKey: deps.sessionKey }).catch(() => {});
|
|
395
|
+
deps.logger?.info(`session reset at turn ${deps.turn} (cap ${SESSION_RESET_TURN_CAP}) — context bounded`);
|
|
396
|
+
}
|
|
397
|
+
|
|
309
398
|
const startedAt = Date.now();
|
|
310
399
|
|
|
311
400
|
const { runId } = await runtime.subagent.run({
|
|
@@ -319,28 +408,36 @@ export async function runAutoplayTurn(deps: AutoplayTurnDeps): Promise<AutoplayT
|
|
|
319
408
|
const decisionMs = Math.max(0, Date.now() - startedAt);
|
|
320
409
|
|
|
321
410
|
// The model called the act tool itself (it POSTed + stamped lastActAt). Treat
|
|
322
|
-
// the cycle as acted; do NOT parse+post again (would double-apply).
|
|
411
|
+
// the cycle as acted; do NOT parse+post again (would double-apply). The
|
|
412
|
+
// session PERSISTS — no deleteSession here; it's the per-match request log.
|
|
323
413
|
if (session.lastActAt !== actAtBefore) {
|
|
324
|
-
return { acted: true, via: "tool" };
|
|
414
|
+
return { acted: true, via: "tool", outcome: "acted", rawText: "", latencyMs: decisionMs };
|
|
325
415
|
}
|
|
326
416
|
|
|
327
417
|
let text = "";
|
|
328
418
|
try {
|
|
419
|
+
// The persistent transcript GROWS each turn; we read the most recent slice
|
|
420
|
+
// and lastAssistantText returns the LATEST assistant reply (not an old one).
|
|
329
421
|
const { messages } = await runtime.subagent.getSessionMessages({ sessionKey: deps.sessionKey, limit: 10 });
|
|
330
422
|
text = lastAssistantText(messages);
|
|
331
423
|
} catch (e) {
|
|
332
424
|
deps.logger?.warn(`getSessionMessages failed: ${String(e)}`);
|
|
333
|
-
} finally {
|
|
334
|
-
// Fresh session per turn → drop it so transcripts don't accumulate. Cleanup
|
|
335
|
-
// failure is non-fatal (the run already happened).
|
|
336
|
-
runtime.subagent.deleteSession({ sessionKey: deps.sessionKey }).catch(() => {});
|
|
337
425
|
}
|
|
426
|
+
// NOTE: no per-turn deleteSession — the session persists for the whole match so
|
|
427
|
+
// the agent accumulates its own decision history. Reset happens only on
|
|
428
|
+
// leave/new-match (index.ts) or the turn cap above.
|
|
338
429
|
|
|
339
430
|
let moves: Move[];
|
|
340
431
|
try {
|
|
341
432
|
moves = parseMoves(text);
|
|
342
433
|
} catch (e) {
|
|
343
|
-
return {
|
|
434
|
+
return {
|
|
435
|
+
acted: false,
|
|
436
|
+
outcome: "no_response",
|
|
437
|
+
reason: e instanceof DecideError ? e.message : String(e),
|
|
438
|
+
rawText: text,
|
|
439
|
+
latencyMs: decisionMs,
|
|
440
|
+
};
|
|
344
441
|
}
|
|
345
442
|
|
|
346
443
|
const { posted } = await executeMoves(deps.matchId, moves, {
|
|
@@ -350,5 +447,5 @@ export async function runAutoplayTurn(deps: AutoplayTurnDeps): Promise<AutoplayT
|
|
|
350
447
|
token: deps.token,
|
|
351
448
|
decisionMs,
|
|
352
449
|
});
|
|
353
|
-
return { acted: true, via: "post", posted };
|
|
450
|
+
return { acted: true, via: "post", posted, outcome: "acted", rawText: text, moves, latencyMs: decisionMs };
|
|
354
451
|
}
|
package/src/watcher.ts
CHANGED
|
@@ -104,8 +104,10 @@ export function parseSseBlock(block: string): { event?: string; data?: string }
|
|
|
104
104
|
* - `system`: the STATIC game rules (spec.instructions.system) — passed as
|
|
105
105
|
* subagent.run's extraSystemPrompt. Empty on the fallback path (no spec).
|
|
106
106
|
* - `user`: the PER-TICK board — strategy + rendered situation + play guidance
|
|
107
|
-
* + the act/JSON directive. This is what changes every tick.
|
|
108
|
-
|
|
107
|
+
* + the act/JSON directive. This is what changes every tick.
|
|
108
|
+
* - `tick`/`clock`: the delivered frame's game time, threaded through so the
|
|
109
|
+
* deliver callback can stamp the per-turn decision report. */
|
|
110
|
+
export type DeliverPrompt = { system: string; user: string; tick: number; clock: number };
|
|
109
111
|
|
|
110
112
|
const actDirective = (actTool: string) =>
|
|
111
113
|
`Decide and act NOW: make ONE ${actTool} call with a move for every player you control — ` +
|
|
@@ -129,6 +131,8 @@ export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advan
|
|
|
129
131
|
`${v.summary}\n\n` +
|
|
130
132
|
`${ins.play}\n\n` +
|
|
131
133
|
actDirective(actTool),
|
|
134
|
+
tick: v.tick,
|
|
135
|
+
clock: v.clock,
|
|
132
136
|
};
|
|
133
137
|
}
|
|
134
138
|
// Fallback (pre-envelope server or handshake not yet arrived). describeTeam is
|
|
@@ -139,6 +143,8 @@ export function prompt(v: TeamView & { summary?: string }, mode: "easy" | "advan
|
|
|
139
143
|
return {
|
|
140
144
|
system: "",
|
|
141
145
|
user: stratBlock + `${rendered}\n\n` + actDirective(actTool),
|
|
146
|
+
tick: v.tick,
|
|
147
|
+
clock: v.clock,
|
|
142
148
|
};
|
|
143
149
|
}
|
|
144
150
|
|