@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.
@@ -2,7 +2,7 @@
2
2
  "id": "stagewhisper",
3
3
  "name": "StageWhisper",
4
4
  "description": "Turn live call moments into assistant tasks via StageWhisper",
5
- "version": "0.46.0",
5
+ "version": "0.49.0",
6
6
  "channels": [
7
7
  "stagewhisper"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stagewhisper/stagewhisper",
3
- "version": "0.46.0",
3
+ "version": "0.49.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin that connects StageWhisper live calls to your AI assistant",
6
6
  "license": "MIT",
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
- text: {
223
- format: {
224
- type: "json_schema" as const,
225
- name: "reasoning_test",
226
- schema: {
227
- type: "object",
228
- properties: {
229
- signals: {
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 text = Array.isArray(output)
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 = text
267
- ? ((text.content as Array<Record<string, unknown>>)?.find(
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(textContent);
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: (job.payload.system_instruction as string) ?? undefined,
113
- text: {
114
- format: {
115
- type: "json_schema",
116
- name: `reasoning_${job.purpose}`,
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(textOutput) as Record<string, unknown>;
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(`Received reasoning job: ${job.job_id} (purpose: ${job.purpose}, correlation: ${correlationId ?? "none"})`);
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(job.job_id, {
335
- job_id: job.job_id,
336
- status: "failed",
337
- provider_run_id: null,
338
- model_ref: displayModel,
339
- usage: null,
340
- output: null,
341
- error_code: "execution_error",
342
- error_message: errMsg,
343
- }, correlationId);
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(job.job_id, result as unknown as Record<string, unknown>, correlationId);
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
- "Enable it in config and restart the gateway, or re-pair with: openclaw stagewhisper pair --code <CODE> --enable-responses",
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