@stagewhisper/stagewhisper 0.46.0 → 0.49.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugin-main.ts +35 -34
- package/src/reasoning.ts +24 -12
- package/src/service.ts +132 -13
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/plugin-main.ts
CHANGED
|
@@ -38,8 +38,6 @@ export default definePluginEntry({
|
|
|
38
38
|
register(api) {
|
|
39
39
|
api.registerChannel({ plugin: stagewhisperPlugin });
|
|
40
40
|
|
|
41
|
-
ensureResponsesEndpoint(api);
|
|
42
|
-
|
|
43
41
|
api.registerCli(
|
|
44
42
|
({ program }) => {
|
|
45
43
|
const sw = program
|
|
@@ -211,6 +209,27 @@ export default definePluginEntry({
|
|
|
211
209
|
console.log(`\nTesting reasoning with model: ${modelLabel}`);
|
|
212
210
|
console.log("Sending test request to local /v1/responses ...");
|
|
213
211
|
|
|
212
|
+
const testSchema = {
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
signals: {
|
|
216
|
+
type: "array",
|
|
217
|
+
items: {
|
|
218
|
+
type: "object",
|
|
219
|
+
properties: {
|
|
220
|
+
severity: { type: "string", enum: ["green", "orange", "red"] },
|
|
221
|
+
message: { type: "string" },
|
|
222
|
+
},
|
|
223
|
+
required: ["severity", "message"],
|
|
224
|
+
additionalProperties: false,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
no_signal_reason: { type: "string" },
|
|
228
|
+
},
|
|
229
|
+
required: ["signals", "no_signal_reason"],
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
};
|
|
232
|
+
|
|
214
233
|
const start = Date.now();
|
|
215
234
|
try {
|
|
216
235
|
const result = await callOpenResponses(api, {
|
|
@@ -219,34 +238,14 @@ export default definePluginEntry({
|
|
|
219
238
|
transcript: "Candidate: I think we should use Redis for caching.",
|
|
220
239
|
playbook_guidance: "Evaluate technical decisions",
|
|
221
240
|
}),
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
type: "array",
|
|
231
|
-
items: {
|
|
232
|
-
type: "object",
|
|
233
|
-
properties: {
|
|
234
|
-
severity: { type: "string", enum: ["green", "orange", "red"] },
|
|
235
|
-
message: { type: "string" },
|
|
236
|
-
},
|
|
237
|
-
required: ["severity", "message"],
|
|
238
|
-
additionalProperties: false,
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
no_signal_reason: { type: "string" },
|
|
242
|
-
},
|
|
243
|
-
required: ["signals", "no_signal_reason"],
|
|
244
|
-
additionalProperties: false,
|
|
245
|
-
},
|
|
246
|
-
strict: true,
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
temperature: 0.2,
|
|
241
|
+
instructions: [
|
|
242
|
+
'You are a structured reasoning engine for the "reasoning_test" task.',
|
|
243
|
+
"You MUST respond with a JSON object conforming to this schema.",
|
|
244
|
+
"Output ONLY valid JSON. No markdown fences, no explanation, no extra text.",
|
|
245
|
+
"",
|
|
246
|
+
"JSON Schema:",
|
|
247
|
+
JSON.stringify(testSchema, null, 2),
|
|
248
|
+
].join("\n"),
|
|
250
249
|
max_output_tokens: 1024,
|
|
251
250
|
});
|
|
252
251
|
|
|
@@ -260,17 +259,18 @@ export default definePluginEntry({
|
|
|
260
259
|
}
|
|
261
260
|
|
|
262
261
|
const output = result.output;
|
|
263
|
-
const
|
|
262
|
+
const msgItem = Array.isArray(output)
|
|
264
263
|
? (output.find((o) => o.type === "message") as Record<string, unknown> | undefined)
|
|
265
264
|
: null;
|
|
266
|
-
const textContent =
|
|
267
|
-
? ((
|
|
265
|
+
const textContent = msgItem
|
|
266
|
+
? ((msgItem.content as Array<Record<string, unknown>>)?.find(
|
|
268
267
|
(c) => c.type === "output_text",
|
|
269
268
|
)?.text as string | undefined)
|
|
270
269
|
: null;
|
|
271
270
|
if (textContent) {
|
|
271
|
+
const cleaned = textContent.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
|
|
272
272
|
try {
|
|
273
|
-
const parsed = JSON.parse(
|
|
273
|
+
const parsed = JSON.parse(cleaned);
|
|
274
274
|
console.log(" Schema-valid JSON: ✓");
|
|
275
275
|
console.log(` Output: ${JSON.stringify(parsed, null, 2)}`);
|
|
276
276
|
} catch {
|
|
@@ -334,6 +334,7 @@ export default definePluginEntry({
|
|
|
334
334
|
|
|
335
335
|
if (api.registrationMode !== "full") return;
|
|
336
336
|
|
|
337
|
+
ensureResponsesEndpoint(api);
|
|
337
338
|
setRuntime(api.runtime);
|
|
338
339
|
const service = createRelayService(api);
|
|
339
340
|
api.registerService(service);
|
package/src/reasoning.ts
CHANGED
|
@@ -19,7 +19,6 @@ export async function probeOpenResponses(
|
|
|
19
19
|
model: "openclaw/default",
|
|
20
20
|
input: "Reply with exactly: OK",
|
|
21
21
|
max_output_tokens: 16,
|
|
22
|
-
temperature: 0,
|
|
23
22
|
};
|
|
24
23
|
|
|
25
24
|
const controller = new AbortController();
|
|
@@ -81,6 +80,23 @@ function extractTextOutput(result: OpenResponsesResponseResource): string | null
|
|
|
81
80
|
return null;
|
|
82
81
|
}
|
|
83
82
|
|
|
83
|
+
function buildSchemaInstruction(schema: Record<string, unknown>, purpose: string, systemInstruction?: string): string {
|
|
84
|
+
const parts: string[] = [];
|
|
85
|
+
if (systemInstruction) {
|
|
86
|
+
parts.push(systemInstruction);
|
|
87
|
+
parts.push("");
|
|
88
|
+
}
|
|
89
|
+
parts.push(
|
|
90
|
+
`You are a structured reasoning engine for the "${purpose}" task.`,
|
|
91
|
+
"You MUST respond with a JSON object conforming to this schema.",
|
|
92
|
+
"Output ONLY valid JSON. No markdown fences, no explanation, no extra text.",
|
|
93
|
+
"",
|
|
94
|
+
"JSON Schema:",
|
|
95
|
+
JSON.stringify(schema, null, 2),
|
|
96
|
+
);
|
|
97
|
+
return parts.join("\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
export async function executeReasoningJob(
|
|
85
101
|
api: OpenClawPluginApi,
|
|
86
102
|
job: ReasoningJobEnvelope,
|
|
@@ -109,17 +125,12 @@ export async function executeReasoningJob(
|
|
|
109
125
|
const requestBody: OpenResponsesCreateResponseRequestBody = {
|
|
110
126
|
model,
|
|
111
127
|
input: JSON.stringify(job.payload),
|
|
112
|
-
instructions: (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
schema: job.response_schema,
|
|
118
|
-
strict: true,
|
|
119
|
-
},
|
|
120
|
-
},
|
|
128
|
+
instructions: buildSchemaInstruction(
|
|
129
|
+
job.response_schema,
|
|
130
|
+
job.purpose,
|
|
131
|
+
(job.payload.system_instruction as string) ?? undefined,
|
|
132
|
+
),
|
|
121
133
|
max_output_tokens: 4096,
|
|
122
|
-
temperature: 0.2,
|
|
123
134
|
};
|
|
124
135
|
|
|
125
136
|
const controller = new AbortController();
|
|
@@ -131,8 +142,9 @@ export async function executeReasoningJob(
|
|
|
131
142
|
|
|
132
143
|
let parsed: Record<string, unknown> | null = null;
|
|
133
144
|
if (textOutput) {
|
|
145
|
+
const cleaned = textOutput.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
|
|
134
146
|
try {
|
|
135
|
-
parsed = JSON.parse(
|
|
147
|
+
parsed = JSON.parse(cleaned) as Record<string, unknown>;
|
|
136
148
|
} catch {
|
|
137
149
|
return {
|
|
138
150
|
job_id: job.job_id,
|
package/src/service.ts
CHANGED
|
@@ -302,12 +302,101 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
async function handleCapabilityProbe(
|
|
306
|
+
job: ReasoningJobEnvelope,
|
|
307
|
+
client: StageWhisperClient,
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
const correlationId = job.correlation_id;
|
|
310
|
+
const displayModel = health.get().displayModel ?? null;
|
|
311
|
+
const prompt =
|
|
312
|
+
((job.payload as Record<string, unknown>)?.prompt as string) ??
|
|
313
|
+
"Briefly describe your capabilities, personality, tools, expertise, goals, and constraints. Plain text, no JSON.";
|
|
314
|
+
|
|
315
|
+
const sessionKey = buildAgentSessionKey({
|
|
316
|
+
agentId: "default",
|
|
317
|
+
channel: "stagewhisper",
|
|
318
|
+
peer: { kind: "direct", id: `sw-probe-${job.job_id}` },
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
const result = await api.runtime.subagent.run({
|
|
323
|
+
sessionKey,
|
|
324
|
+
message: prompt,
|
|
325
|
+
deliver: false,
|
|
326
|
+
idempotencyKey: `sw-probe-${job.job_id}`,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const waitResult = await api.runtime.subagent.waitForRun({
|
|
330
|
+
runId: result.runId,
|
|
331
|
+
timeoutMs: 35_000,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
if (waitResult.status === "ok") {
|
|
335
|
+
const reply = await extractReplyWithRetry(sessionKey);
|
|
336
|
+
|
|
337
|
+
await client.postReasoningResult(
|
|
338
|
+
job.job_id,
|
|
339
|
+
{
|
|
340
|
+
job_id: job.job_id,
|
|
341
|
+
status: "completed",
|
|
342
|
+
provider_run_id: result.runId,
|
|
343
|
+
model_ref: displayModel,
|
|
344
|
+
usage: null,
|
|
345
|
+
output: { raw_description: (reply ?? "").slice(0, 2000) },
|
|
346
|
+
error_code: null,
|
|
347
|
+
error_message: null,
|
|
348
|
+
},
|
|
349
|
+
correlationId,
|
|
350
|
+
);
|
|
351
|
+
api.logger.info(`Capability probe ${job.job_id} completed`);
|
|
352
|
+
} else {
|
|
353
|
+
await client.postReasoningResult(
|
|
354
|
+
job.job_id,
|
|
355
|
+
{
|
|
356
|
+
job_id: job.job_id,
|
|
357
|
+
status: "failed",
|
|
358
|
+
provider_run_id: result.runId,
|
|
359
|
+
model_ref: displayModel,
|
|
360
|
+
usage: null,
|
|
361
|
+
output: null,
|
|
362
|
+
error_code: "agent_error",
|
|
363
|
+
error_message: waitResult.error ?? "Agent run failed",
|
|
364
|
+
},
|
|
365
|
+
correlationId,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
370
|
+
api.logger.error(`Capability probe ${job.job_id} failed: ${errMsg}`);
|
|
371
|
+
try {
|
|
372
|
+
await client.postReasoningResult(
|
|
373
|
+
job.job_id,
|
|
374
|
+
{
|
|
375
|
+
job_id: job.job_id,
|
|
376
|
+
status: "failed",
|
|
377
|
+
provider_run_id: null,
|
|
378
|
+
model_ref: displayModel,
|
|
379
|
+
usage: null,
|
|
380
|
+
output: null,
|
|
381
|
+
error_code: "execution_error",
|
|
382
|
+
error_message: errMsg,
|
|
383
|
+
},
|
|
384
|
+
correlationId,
|
|
385
|
+
);
|
|
386
|
+
} catch (postErr) {
|
|
387
|
+
api.logger.error(`Failed to report probe failure: ${postErr}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
305
392
|
async function handleReasoningJob(
|
|
306
393
|
job: ReasoningJobEnvelope,
|
|
307
394
|
client: StageWhisperClient,
|
|
308
395
|
): Promise<void> {
|
|
309
396
|
const correlationId = job.correlation_id;
|
|
310
|
-
api.logger.info(
|
|
397
|
+
api.logger.info(
|
|
398
|
+
`Received reasoning job: ${job.job_id} (purpose: ${job.purpose}, correlation: ${correlationId ?? "none"})`,
|
|
399
|
+
);
|
|
311
400
|
|
|
312
401
|
if (completedReasoningJobs.has(job.job_id)) {
|
|
313
402
|
api.logger.info(`Skipping completed reasoning job: ${job.job_id}`);
|
|
@@ -319,6 +408,16 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
319
408
|
}
|
|
320
409
|
processingReasoningJobs.add(job.job_id);
|
|
321
410
|
|
|
411
|
+
if (job.purpose === "capability_probe") {
|
|
412
|
+
try {
|
|
413
|
+
await handleCapabilityProbe(job, client);
|
|
414
|
+
completedReasoningJobs.set(job.job_id, Date.now());
|
|
415
|
+
} finally {
|
|
416
|
+
processingReasoningJobs.delete(job.job_id);
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
322
421
|
try {
|
|
323
422
|
const displayModel = health.get().displayModel ?? null;
|
|
324
423
|
|
|
@@ -331,16 +430,20 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
331
430
|
api.logger.error(`Reasoning job ${job.job_id} failed: ${errMsg}`);
|
|
332
431
|
|
|
333
432
|
try {
|
|
334
|
-
await client.postReasoningResult(
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
433
|
+
await client.postReasoningResult(
|
|
434
|
+
job.job_id,
|
|
435
|
+
{
|
|
436
|
+
job_id: job.job_id,
|
|
437
|
+
status: "failed",
|
|
438
|
+
provider_run_id: null,
|
|
439
|
+
model_ref: displayModel,
|
|
440
|
+
usage: null,
|
|
441
|
+
output: null,
|
|
442
|
+
error_code: "execution_error",
|
|
443
|
+
error_message: errMsg,
|
|
444
|
+
},
|
|
445
|
+
correlationId,
|
|
446
|
+
);
|
|
344
447
|
} catch (postErr) {
|
|
345
448
|
api.logger.error(`Failed to report reasoning failure: ${postErr}`);
|
|
346
449
|
}
|
|
@@ -358,7 +461,11 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
358
461
|
}
|
|
359
462
|
|
|
360
463
|
try {
|
|
361
|
-
await client.postReasoningResult(
|
|
464
|
+
await client.postReasoningResult(
|
|
465
|
+
job.job_id,
|
|
466
|
+
result as unknown as Record<string, unknown>,
|
|
467
|
+
correlationId,
|
|
468
|
+
);
|
|
362
469
|
completedReasoningJobs.set(job.job_id, Date.now());
|
|
363
470
|
if (completedReasoningJobs.size > COMPLETED_JOB_MAX_SIZE) {
|
|
364
471
|
evictStaleCompletedJobs();
|
|
@@ -401,6 +508,18 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
401
508
|
health.setConnected();
|
|
402
509
|
api.logger.info("Connected to StageWhisper relay stream");
|
|
403
510
|
|
|
511
|
+
if (health.get().status !== "healthy") {
|
|
512
|
+
api.logger.info("Re-probing /v1/responses after reconnect...");
|
|
513
|
+
const probe = await probeOpenResponses(api);
|
|
514
|
+
if (probe.ok) {
|
|
515
|
+
health.recordSuccess();
|
|
516
|
+
if (probe.model) health.setModel(probe.model);
|
|
517
|
+
api.logger.info(`Local AI verified on reconnect — model: ${probe.model ?? "unknown"}`);
|
|
518
|
+
} else {
|
|
519
|
+
api.logger.warn(`Reconnect probe failed: ${probe.error}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
404
523
|
const reader = res.body.getReader();
|
|
405
524
|
const decoder = new TextDecoder();
|
|
406
525
|
let buffer = "";
|
|
@@ -501,7 +620,7 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
501
620
|
if (!isResponsesEndpointEnabled(api)) {
|
|
502
621
|
api.logger.warn(
|
|
503
622
|
"gateway.http.endpoints.responses.enabled is not true — reasoning jobs will fail with 404. " +
|
|
504
|
-
|
|
623
|
+
"Enable it in config and restart the gateway, or re-pair with: openclaw stagewhisper pair --code <CODE> --enable-responses",
|
|
505
624
|
);
|
|
506
625
|
}
|
|
507
626
|
|