@stagewhisper/stagewhisper 0.45.0 → 0.47.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 +123 -38
- package/src/openresponses.ts +16 -0
- package/src/reasoning.ts +24 -12
- package/src/service.ts +8 -0
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/plugin-main.ts
CHANGED
|
@@ -3,6 +3,34 @@ import { stagewhisperPlugin } from "./src/channel.js";
|
|
|
3
3
|
import { setRuntime } from "./src/runtime.js";
|
|
4
4
|
import { createRelayService } from "./src/service.js";
|
|
5
5
|
|
|
6
|
+
async function ensureResponsesEndpoint(api: Parameters<Parameters<typeof definePluginEntry>[0]["register"]>[0]): Promise<void> {
|
|
7
|
+
try {
|
|
8
|
+
const cfg = await api.runtime.config.loadConfig();
|
|
9
|
+
const gw = ((cfg as Record<string, unknown>)["gateway"] ?? {}) as Record<string, unknown>;
|
|
10
|
+
const http = (gw["http"] ?? {}) as Record<string, unknown>;
|
|
11
|
+
const endpoints = (http["endpoints"] ?? {}) as Record<string, unknown>;
|
|
12
|
+
const responses = (endpoints["responses"] ?? {}) as Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
if (responses["enabled"] === true) return;
|
|
15
|
+
|
|
16
|
+
const auth = (gw["auth"] ?? {}) as Record<string, unknown>;
|
|
17
|
+
if (auth["mode"] === "none" && !auth["token"] && !auth["password"]) return;
|
|
18
|
+
|
|
19
|
+
responses["enabled"] = true;
|
|
20
|
+
endpoints["responses"] = responses;
|
|
21
|
+
http["endpoints"] = endpoints;
|
|
22
|
+
gw["http"] = http;
|
|
23
|
+
(cfg as Record<string, unknown>)["gateway"] = gw;
|
|
24
|
+
|
|
25
|
+
await api.runtime.config.writeConfigFile(cfg);
|
|
26
|
+
api.logger.info(
|
|
27
|
+
"Enabled gateway.http.endpoints.responses for StageWhisper reasoning. Restart the gateway for it to take effect.",
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
// best-effort — reasoning-check will surface the real error
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
6
34
|
export default definePluginEntry({
|
|
7
35
|
id: "stagewhisper",
|
|
8
36
|
name: "StageWhisper",
|
|
@@ -10,6 +38,8 @@ export default definePluginEntry({
|
|
|
10
38
|
register(api) {
|
|
11
39
|
api.registerChannel({ plugin: stagewhisperPlugin });
|
|
12
40
|
|
|
41
|
+
ensureResponsesEndpoint(api);
|
|
42
|
+
|
|
13
43
|
api.registerCli(
|
|
14
44
|
({ program }) => {
|
|
15
45
|
const sw = program
|
|
@@ -27,11 +57,13 @@ export default definePluginEntry({
|
|
|
27
57
|
"https://api.stagewhisper.io",
|
|
28
58
|
)
|
|
29
59
|
.option("--label <label>", "Label for this OpenClaw host", "OpenClaw")
|
|
60
|
+
.option("--no-enable-responses", "Skip enabling the gateway OpenResponses HTTP API")
|
|
30
61
|
.action(
|
|
31
62
|
async (opts: {
|
|
32
63
|
code: string;
|
|
33
64
|
apiUrl: string;
|
|
34
65
|
label?: string;
|
|
66
|
+
enableResponses: boolean;
|
|
35
67
|
}) => {
|
|
36
68
|
const { StageWhisperClient } = await import("./src/client.js");
|
|
37
69
|
const client = new StageWhisperClient(opts.apiUrl, "", "");
|
|
@@ -65,13 +97,33 @@ export default definePluginEntry({
|
|
|
65
97
|
};
|
|
66
98
|
(cfg as Record<string, unknown>)["channels"] = channels;
|
|
67
99
|
|
|
100
|
+
if (opts.enableResponses) {
|
|
101
|
+
const gw = ((cfg as Record<string, unknown>)["gateway"] ?? {}) as Record<string, unknown>;
|
|
102
|
+
const gwAuth = (gw["auth"] as Record<string, unknown>) ?? {};
|
|
103
|
+
const authMode = gwAuth["mode"] as string | undefined;
|
|
104
|
+
const hasToken = typeof gwAuth["token"] === "string" && (gwAuth["token"] as string).length > 0;
|
|
105
|
+
|
|
106
|
+
if (authMode === "none" && !hasToken) {
|
|
107
|
+
console.warn(" ⚠ gateway.auth.mode is 'none' with no token — skipping HTTP API enablement.");
|
|
108
|
+
console.warn(" Set a gateway auth token first for reasoning to work.\n");
|
|
109
|
+
} else {
|
|
110
|
+
const http = (gw["http"] ?? {}) as Record<string, unknown>;
|
|
111
|
+
const endpoints = (http["endpoints"] ?? {}) as Record<string, unknown>;
|
|
112
|
+
const responses = (endpoints["responses"] ?? {}) as Record<string, unknown>;
|
|
113
|
+
responses["enabled"] = true;
|
|
114
|
+
endpoints["responses"] = responses;
|
|
115
|
+
http["endpoints"] = endpoints;
|
|
116
|
+
gw["http"] = http;
|
|
117
|
+
(cfg as Record<string, unknown>)["gateway"] = gw;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
68
121
|
await api.runtime.config.writeConfigFile(cfg);
|
|
69
122
|
|
|
70
123
|
console.log(
|
|
71
124
|
`\n✓ Paired with StageWhisper (${result.label})`,
|
|
72
125
|
);
|
|
73
|
-
console.log(" Config saved
|
|
74
|
-
console.log(" Restart the gateway to activate the relay:\n");
|
|
126
|
+
console.log(" Config saved. Restart the gateway to activate:\n");
|
|
75
127
|
console.log(" openclaw gateway restart\n");
|
|
76
128
|
} catch (err) {
|
|
77
129
|
console.error(`\n✗ Pairing failed: ${err}\n`);
|
|
@@ -84,7 +136,8 @@ export default definePluginEntry({
|
|
|
84
136
|
.description(
|
|
85
137
|
"Remove StageWhisper pairing (run before `openclaw plugins uninstall`)",
|
|
86
138
|
)
|
|
87
|
-
.
|
|
139
|
+
.option("--keep-responses", "Keep the OpenResponses HTTP API enabled after unpair")
|
|
140
|
+
.action(async (opts: { keepResponses?: boolean }) => {
|
|
88
141
|
try {
|
|
89
142
|
const cfg = await api.runtime.config.loadConfig();
|
|
90
143
|
const plugins = (cfg as Record<string, unknown>)["plugins"] as Record<string, unknown> ?? {};
|
|
@@ -102,6 +155,20 @@ export default definePluginEntry({
|
|
|
102
155
|
}
|
|
103
156
|
}
|
|
104
157
|
|
|
158
|
+
if (!opts.keepResponses) {
|
|
159
|
+
const gw = (cfg as Record<string, unknown>)["gateway"] as Record<string, unknown> | undefined;
|
|
160
|
+
const http = gw?.["http"] as Record<string, unknown> | undefined;
|
|
161
|
+
const endpoints = http?.["endpoints"] as Record<string, unknown> | undefined;
|
|
162
|
+
const responses = endpoints?.["responses"] as Record<string, unknown> | undefined;
|
|
163
|
+
if (responses?.["enabled"] === true) {
|
|
164
|
+
delete responses["enabled"];
|
|
165
|
+
if (Object.keys(responses).length === 0 && endpoints) delete endpoints["responses"];
|
|
166
|
+
if (endpoints && Object.keys(endpoints).length === 0 && http) delete http["endpoints"];
|
|
167
|
+
if (http && Object.keys(http).length === 0 && gw) delete gw["http"];
|
|
168
|
+
console.log(" ℹ Disabled gateway.http.endpoints.responses. Use --keep-responses to preserve it.");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
105
172
|
await api.runtime.config.writeConfigFile(cfg);
|
|
106
173
|
console.log("\n✓ StageWhisper unpaired.");
|
|
107
174
|
console.log(" Config cleaned. You can now safely uninstall:\n");
|
|
@@ -116,7 +183,7 @@ export default definePluginEntry({
|
|
|
116
183
|
.description("Test reasoning capability against the local OpenResponses endpoint")
|
|
117
184
|
.option("--model <model>", "Model to use (omit to use your configured default)", "openclaw/default")
|
|
118
185
|
.action(async (opts: { model: string }) => {
|
|
119
|
-
const { callOpenResponses } = await import("./src/openresponses.js");
|
|
186
|
+
const { callOpenResponses, isResponsesEndpointEnabled } = await import("./src/openresponses.js");
|
|
120
187
|
const modelLabel = opts.model === "openclaw/default" ? "default (configured)" : opts.model;
|
|
121
188
|
|
|
122
189
|
const cfg = api.config as Record<string, unknown>;
|
|
@@ -124,10 +191,47 @@ export default definePluginEntry({
|
|
|
124
191
|
const auth = (gw?.auth as Record<string, unknown>) ?? {};
|
|
125
192
|
const port = Number(gw?.port) || 18789;
|
|
126
193
|
const hasToken = typeof auth?.token === "string" && auth.token.length > 0;
|
|
127
|
-
|
|
128
|
-
|
|
194
|
+
const responsesEnabled = isResponsesEndpointEnabled(api);
|
|
195
|
+
|
|
196
|
+
console.log("Preflight checks:");
|
|
197
|
+
console.log(` Gateway port: ${port}`);
|
|
198
|
+
console.log(` Auth token: ${hasToken ? "✓ present" : "✗ MISSING"}`);
|
|
199
|
+
console.log(` responses.enabled: ${responsesEnabled ? "✓ true" : "✗ false"}`);
|
|
200
|
+
|
|
201
|
+
if (!responsesEnabled) {
|
|
202
|
+
console.warn("\n⚠ responses.enabled is false in the running config.");
|
|
203
|
+
console.warn(" The plugin auto-enables it on startup — restart the gateway if you haven't:");
|
|
204
|
+
console.warn(" openclaw gateway restart\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!hasToken) {
|
|
208
|
+
console.warn("\n⚠ No gateway auth token found — request may be rejected.\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log(`\nTesting reasoning with model: ${modelLabel}`);
|
|
129
212
|
console.log("Sending test request to local /v1/responses ...");
|
|
130
213
|
|
|
214
|
+
const testSchema = {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
signals: {
|
|
218
|
+
type: "array",
|
|
219
|
+
items: {
|
|
220
|
+
type: "object",
|
|
221
|
+
properties: {
|
|
222
|
+
severity: { type: "string", enum: ["green", "orange", "red"] },
|
|
223
|
+
message: { type: "string" },
|
|
224
|
+
},
|
|
225
|
+
required: ["severity", "message"],
|
|
226
|
+
additionalProperties: false,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
no_signal_reason: { type: "string" },
|
|
230
|
+
},
|
|
231
|
+
required: ["signals", "no_signal_reason"],
|
|
232
|
+
additionalProperties: false,
|
|
233
|
+
};
|
|
234
|
+
|
|
131
235
|
const start = Date.now();
|
|
132
236
|
try {
|
|
133
237
|
const result = await callOpenResponses(api, {
|
|
@@ -136,34 +240,14 @@ export default definePluginEntry({
|
|
|
136
240
|
transcript: "Candidate: I think we should use Redis for caching.",
|
|
137
241
|
playbook_guidance: "Evaluate technical decisions",
|
|
138
242
|
}),
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
type: "array",
|
|
148
|
-
items: {
|
|
149
|
-
type: "object",
|
|
150
|
-
properties: {
|
|
151
|
-
severity: { type: "string", enum: ["green", "orange", "red"] },
|
|
152
|
-
message: { type: "string" },
|
|
153
|
-
},
|
|
154
|
-
required: ["severity", "message"],
|
|
155
|
-
additionalProperties: false,
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
no_signal_reason: { type: "string" },
|
|
159
|
-
},
|
|
160
|
-
required: ["signals", "no_signal_reason"],
|
|
161
|
-
additionalProperties: false,
|
|
162
|
-
},
|
|
163
|
-
strict: true,
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
temperature: 0.2,
|
|
243
|
+
instructions: [
|
|
244
|
+
'You are a structured reasoning engine for the "reasoning_test" task.',
|
|
245
|
+
"You MUST respond with a JSON object conforming to this schema.",
|
|
246
|
+
"Output ONLY valid JSON. No markdown fences, no explanation, no extra text.",
|
|
247
|
+
"",
|
|
248
|
+
"JSON Schema:",
|
|
249
|
+
JSON.stringify(testSchema, null, 2),
|
|
250
|
+
].join("\n"),
|
|
167
251
|
max_output_tokens: 1024,
|
|
168
252
|
});
|
|
169
253
|
|
|
@@ -177,17 +261,18 @@ export default definePluginEntry({
|
|
|
177
261
|
}
|
|
178
262
|
|
|
179
263
|
const output = result.output;
|
|
180
|
-
const
|
|
264
|
+
const msgItem = Array.isArray(output)
|
|
181
265
|
? (output.find((o) => o.type === "message") as Record<string, unknown> | undefined)
|
|
182
266
|
: null;
|
|
183
|
-
const textContent =
|
|
184
|
-
? ((
|
|
267
|
+
const textContent = msgItem
|
|
268
|
+
? ((msgItem.content as Array<Record<string, unknown>>)?.find(
|
|
185
269
|
(c) => c.type === "output_text",
|
|
186
270
|
)?.text as string | undefined)
|
|
187
271
|
: null;
|
|
188
272
|
if (textContent) {
|
|
273
|
+
const cleaned = textContent.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
|
|
189
274
|
try {
|
|
190
|
-
const parsed = JSON.parse(
|
|
275
|
+
const parsed = JSON.parse(cleaned);
|
|
191
276
|
console.log(" Schema-valid JSON: ✓");
|
|
192
277
|
console.log(` Output: ${JSON.stringify(parsed, null, 2)}`);
|
|
193
278
|
} catch {
|
package/src/openresponses.ts
CHANGED
|
@@ -16,6 +16,15 @@ function resolveGatewayConfig(api: OpenClawPluginApi): GatewayConfig {
|
|
|
16
16
|
return { url, apiKey: token };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export function isResponsesEndpointEnabled(api: OpenClawPluginApi): boolean {
|
|
20
|
+
const cfg = api.config as Record<string, unknown>;
|
|
21
|
+
const gw = (cfg?.gateway as Record<string, unknown>) ?? {};
|
|
22
|
+
const http = (gw?.http as Record<string, unknown>) ?? {};
|
|
23
|
+
const endpoints = (http?.endpoints as Record<string, unknown>) ?? {};
|
|
24
|
+
const responses = (endpoints?.responses as Record<string, unknown>) ?? {};
|
|
25
|
+
return responses?.enabled === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
export async function callOpenResponses(
|
|
20
29
|
api: OpenClawPluginApi,
|
|
21
30
|
requestBody: OpenResponsesCreateResponseRequestBody,
|
|
@@ -44,6 +53,13 @@ export async function callOpenResponses(
|
|
|
44
53
|
|
|
45
54
|
if (!response.ok) {
|
|
46
55
|
const body = await response.text().catch(() => "");
|
|
56
|
+
if (response.status === 404) {
|
|
57
|
+
throw new OpenResponsesError(
|
|
58
|
+
"POST /v1/responses returned 404 — the OpenResponses HTTP API is most likely disabled. " +
|
|
59
|
+
'Enable it in OpenClaw config: gateway.http.endpoints.responses.enabled = true, then restart the gateway.',
|
|
60
|
+
response.status,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
47
63
|
throw new OpenResponsesError(`POST /v1/responses returned ${response.status}: ${body}`, response.status);
|
|
48
64
|
}
|
|
49
65
|
return (await response.json()) as OpenResponsesResponseResource;
|
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
|
@@ -7,6 +7,7 @@ import type { StageWhisperAccount } from "./channel.js";
|
|
|
7
7
|
import { resolveAccount } from "./channel.js";
|
|
8
8
|
import { createHealthTracker } from "./health.js";
|
|
9
9
|
import { executeReasoningJob, probeOpenResponses, type ReasoningJobEnvelope } from "./reasoning.js";
|
|
10
|
+
import { isResponsesEndpointEnabled } from "./openresponses.js";
|
|
10
11
|
|
|
11
12
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
12
13
|
const RECONNECT_BASE_MS = 1_000;
|
|
@@ -497,6 +498,13 @@ export function createRelayService(api: OpenClawPluginApi) {
|
|
|
497
498
|
|
|
498
499
|
state.running = true;
|
|
499
500
|
|
|
501
|
+
if (!isResponsesEndpointEnabled(api)) {
|
|
502
|
+
api.logger.warn(
|
|
503
|
+
"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",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
500
508
|
api.logger.info("Probing /v1/responses to verify local AI connectivity...");
|
|
501
509
|
const probe = await probeOpenResponses(api);
|
|
502
510
|
if (probe.ok) {
|