@stagewhisper/stagewhisper 0.49.0 → 0.52.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.
@@ -1,116 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { executeReasoningJob, type ReasoningJobEnvelope } from "./reasoning.js";
3
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
4
-
5
- vi.mock("./openresponses.js", () => ({
6
- callOpenResponses: vi.fn(),
7
- OpenResponsesError: class extends Error {
8
- status: number;
9
- retryable: boolean;
10
- constructor(message: string, status: number) {
11
- super(message);
12
- this.status = status;
13
- this.retryable = status >= 500;
14
- }
15
- },
16
- }));
17
-
18
- const { callOpenResponses, OpenResponsesError } = await import("./openresponses.js");
19
- const mockCall = vi.mocked(callOpenResponses);
20
-
21
- function makeApi(): OpenClawPluginApi {
22
- return {
23
- logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
24
- config: { get: vi.fn().mockReturnValue({}) },
25
- } as unknown as OpenClawPluginApi;
26
- }
27
-
28
- function makeJob(overrides: Partial<ReasoningJobEnvelope> = {}): ReasoningJobEnvelope {
29
- return {
30
- event_type: "reasoning_job",
31
- job_id: "job-123",
32
- purpose: "live_analysis",
33
- deadline_at: new Date(Date.now() + 30_000).toISOString(),
34
- idempotency_key: "idem-1",
35
- schema_version: 1,
36
- response_schema: {
37
- type: "object",
38
- properties: { signals: { type: "array" } },
39
- required: ["signals"],
40
- },
41
- payload: { transcript: "Hello world" },
42
- ...overrides,
43
- };
44
- }
45
-
46
- describe("executeReasoningJob", () => {
47
- beforeEach(() => {
48
- vi.clearAllMocks();
49
- });
50
-
51
- it("returns completed with parsed JSON output", async () => {
52
- mockCall.mockResolvedValue({
53
- id: "resp-abc",
54
- model: "gpt-4o",
55
- output: [
56
- {
57
- type: "message",
58
- content: [
59
- { type: "output_text", text: '{"signals": [{"severity": "green", "message": "ok"}]}' },
60
- ],
61
- },
62
- ],
63
- usage: { input_tokens: 100, output_tokens: 50 },
64
- } as never);
65
-
66
- const result = await executeReasoningJob(makeApi(), makeJob(), "gpt-4o");
67
- expect(result.status).toBe("completed");
68
- expect(result.job_id).toBe("job-123");
69
- expect(result.output).toEqual({ signals: [{ severity: "green", message: "ok" }] });
70
- expect(result.usage).toEqual({ input_tokens: 100, output_tokens: 50 });
71
- expect(result.provider_run_id).toBe("resp-abc");
72
- });
73
-
74
- it("returns timed_out when deadline already passed", async () => {
75
- const job = makeJob({ deadline_at: new Date(Date.now() - 1000).toISOString() });
76
- const result = await executeReasoningJob(makeApi(), job, "gpt-4o");
77
- expect(result.status).toBe("timed_out");
78
- expect(result.error_code).toBe("deadline_expired_before_start");
79
- expect(mockCall).not.toHaveBeenCalled();
80
- });
81
-
82
- it("returns failed when output is not valid JSON", async () => {
83
- mockCall.mockResolvedValue({
84
- id: "resp-bad",
85
- output: [
86
- { type: "message", content: [{ type: "output_text", text: "not json" }] },
87
- ],
88
- usage: { input_tokens: 10, output_tokens: 5 },
89
- } as never);
90
-
91
- const result = await executeReasoningJob(makeApi(), makeJob(), "gpt-4o");
92
- expect(result.status).toBe("failed");
93
- expect(result.error_code).toBe("response_parse_error");
94
- });
95
-
96
- it("returns failed with retryable flag on 5xx errors", async () => {
97
- const err = new (OpenResponsesError as unknown as new (msg: string, status: number) => Error & { retryable: boolean })(
98
- "Internal Server Error",
99
- 500,
100
- );
101
- mockCall.mockRejectedValue(err);
102
-
103
- const result = await executeReasoningJob(makeApi(), makeJob(), "gpt-4o");
104
- expect(result.status).toBe("failed");
105
- expect(result.error_code).toBe("retryable_error");
106
- });
107
-
108
- it("returns failed with execution_error on non-retryable errors", async () => {
109
- mockCall.mockRejectedValue(new Error("Network failure"));
110
-
111
- const result = await executeReasoningJob(makeApi(), makeJob(), "gpt-4o");
112
- expect(result.status).toBe("failed");
113
- expect(result.error_code).toBe("execution_error");
114
- expect(result.error_message).toBe("Network failure");
115
- });
116
- });
package/src/reasoning.ts DELETED
@@ -1,210 +0,0 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
- import {
3
- callOpenResponses,
4
- OpenResponsesError,
5
- type OpenResponsesCreateResponseRequestBody,
6
- type OpenResponsesResponseResource,
7
- } from "./openresponses.js";
8
-
9
- export type ProbeResult = {
10
- ok: boolean;
11
- model: string | null;
12
- error: string | null;
13
- };
14
-
15
- export async function probeOpenResponses(
16
- api: OpenClawPluginApi,
17
- ): Promise<ProbeResult> {
18
- const body: OpenResponsesCreateResponseRequestBody = {
19
- model: "openclaw/default",
20
- input: "Reply with exactly: OK",
21
- max_output_tokens: 16,
22
- };
23
-
24
- const controller = new AbortController();
25
- const timer = setTimeout(() => controller.abort(), 15_000);
26
-
27
- try {
28
- const result = await callOpenResponses(api, body, controller.signal, undefined);
29
- const model = (result as Record<string, unknown>).model as string ?? null;
30
- return { ok: true, model, error: null };
31
- } catch (err) {
32
- return {
33
- ok: false,
34
- model: null,
35
- error: err instanceof Error ? err.message : String(err),
36
- };
37
- } finally {
38
- clearTimeout(timer);
39
- }
40
- }
41
-
42
- export type ReasoningJobEnvelope = {
43
- event_type: "reasoning_job";
44
- job_id: string;
45
- purpose: string;
46
- deadline_at: string;
47
- idempotency_key: string;
48
- schema_version: number;
49
- response_schema: Record<string, unknown>;
50
- payload: Record<string, unknown>;
51
- model?: string;
52
- correlation_id?: string;
53
- };
54
-
55
- export type ReasoningJobResult = {
56
- job_id: string;
57
- status: "completed" | "failed" | "timed_out";
58
- provider_run_id: string | null;
59
- model_ref: string | null;
60
- usage: { input_tokens: number; output_tokens: number } | null;
61
- output: Record<string, unknown> | null;
62
- error_code: string | null;
63
- error_message: string | null;
64
- };
65
-
66
- function extractTextOutput(result: OpenResponsesResponseResource): string | null {
67
- const output = result.output;
68
- if (!Array.isArray(output)) return null;
69
- for (const item of output) {
70
- if (item.type !== "message") continue;
71
- const content = (item as Record<string, unknown>).content;
72
- if (!Array.isArray(content)) continue;
73
- for (const part of content) {
74
- const p = part as Record<string, unknown>;
75
- if (p.type === "output_text" && typeof p.text === "string") {
76
- return p.text;
77
- }
78
- }
79
- }
80
- return null;
81
- }
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
-
100
- export async function executeReasoningJob(
101
- api: OpenClawPluginApi,
102
- job: ReasoningJobEnvelope,
103
- displayModel: string | null,
104
- ): Promise<ReasoningJobResult> {
105
- const parsedDeadline = new Date(job.deadline_at).getTime();
106
- const deadlineMs = Number.isFinite(parsedDeadline)
107
- ? parsedDeadline - Date.now()
108
- : -1;
109
- if (deadlineMs <= 0) {
110
- return {
111
- job_id: job.job_id,
112
- status: "timed_out",
113
- provider_run_id: null,
114
- model_ref: displayModel,
115
- usage: null,
116
- output: null,
117
- error_code: "deadline_expired_before_start",
118
- error_message: "Job deadline had already passed when execution began",
119
- };
120
- }
121
-
122
- const model = job.model ?? displayModel ?? "openclaw/default";
123
- const correlationId = job.correlation_id;
124
-
125
- const requestBody: OpenResponsesCreateResponseRequestBody = {
126
- model,
127
- input: JSON.stringify(job.payload),
128
- instructions: buildSchemaInstruction(
129
- job.response_schema,
130
- job.purpose,
131
- (job.payload.system_instruction as string) ?? undefined,
132
- ),
133
- max_output_tokens: 4096,
134
- };
135
-
136
- const controller = new AbortController();
137
- const timer = setTimeout(() => controller.abort(), deadlineMs);
138
-
139
- try {
140
- const result = await callOpenResponses(api, requestBody, controller.signal, correlationId);
141
- const textOutput = extractTextOutput(result);
142
-
143
- let parsed: Record<string, unknown> | null = null;
144
- if (textOutput) {
145
- const cleaned = textOutput.replace(/^```(?:json)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
146
- try {
147
- parsed = JSON.parse(cleaned) as Record<string, unknown>;
148
- } catch {
149
- return {
150
- job_id: job.job_id,
151
- status: "failed",
152
- provider_run_id: result.id,
153
- model_ref: (result as Record<string, unknown>).model as string ?? model,
154
- usage: result.usage
155
- ? {
156
- input_tokens: result.usage.input_tokens ?? 0,
157
- output_tokens: result.usage.output_tokens ?? 0,
158
- }
159
- : null,
160
- output: null,
161
- error_code: "response_parse_error",
162
- error_message: "Response text is not valid JSON",
163
- };
164
- }
165
- }
166
-
167
- return {
168
- job_id: job.job_id,
169
- status: "completed",
170
- provider_run_id: result.id,
171
- model_ref: (result as Record<string, unknown>).model as string ?? model,
172
- usage: result.usage
173
- ? {
174
- input_tokens: result.usage.input_tokens ?? 0,
175
- output_tokens: result.usage.output_tokens ?? 0,
176
- }
177
- : null,
178
- output: parsed,
179
- error_code: null,
180
- error_message: null,
181
- };
182
- } catch (err) {
183
- if (controller.signal.aborted) {
184
- return {
185
- job_id: job.job_id,
186
- status: "timed_out",
187
- provider_run_id: null,
188
- model_ref: model,
189
- usage: null,
190
- output: null,
191
- error_code: "deadline_exceeded",
192
- error_message: `Reasoning execution exceeded deadline of ${deadlineMs}ms`,
193
- };
194
- }
195
-
196
- const isRetryable = err instanceof OpenResponsesError && err.retryable;
197
- return {
198
- job_id: job.job_id,
199
- status: "failed",
200
- provider_run_id: null,
201
- model_ref: model,
202
- usage: null,
203
- output: null,
204
- error_code: isRetryable ? "retryable_error" : "execution_error",
205
- error_message: err instanceof Error ? err.message : String(err),
206
- };
207
- } finally {
208
- clearTimeout(timer);
209
- }
210
- }
package/src/runtime.ts DELETED
@@ -1,6 +0,0 @@
1
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
2
- import type { PluginRuntime } from "openclaw/plugin-sdk/core";
3
-
4
- export const { setRuntime, getRuntime } = createPluginRuntimeStore<PluginRuntime>(
5
- "StageWhisper plugin runtime not initialized",
6
- );