@parkgogogo/openclaw-reflection 0.1.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.
@@ -0,0 +1,447 @@
1
+ import type {
2
+ AgentRunResult,
3
+ AgentStep,
4
+ AgentTool,
5
+ GenerateObjectParams,
6
+ JsonSchema,
7
+ LLMService as LLMServiceContract,
8
+ LLMServiceConfig,
9
+ LLMServiceOptions,
10
+ FetchLike,
11
+ RunAgentParams,
12
+ } from "./types.js";
13
+
14
+ type ObjectSchema = Extract<JsonSchema, { type: "object" }>;
15
+
16
+ interface ChatCompletionResponse {
17
+ choices?: Array<{
18
+ message?: {
19
+ content?: unknown;
20
+ tool_calls?: unknown;
21
+ function_call?: unknown;
22
+ };
23
+ }>;
24
+ }
25
+
26
+ interface AgentToolDefinition {
27
+ type: "function";
28
+ function: {
29
+ name: string;
30
+ description: string;
31
+ parameters: ObjectSchema;
32
+ strict: true;
33
+ };
34
+ }
35
+
36
+ interface AgentToolCallResponse {
37
+ id: string;
38
+ name: string;
39
+ input: unknown;
40
+ }
41
+
42
+ interface AgentToolCallRequest {
43
+ id: string;
44
+ type: "function";
45
+ function: {
46
+ name: string;
47
+ arguments: string;
48
+ };
49
+ }
50
+
51
+ type AgentRequestMessage =
52
+ | {
53
+ role: "system" | "user";
54
+ content: string;
55
+ }
56
+ | {
57
+ role: "assistant";
58
+ content: string | null;
59
+ tool_calls: AgentToolCallRequest[];
60
+ }
61
+ | {
62
+ role: "tool";
63
+ tool_call_id: string;
64
+ content: string;
65
+ };
66
+
67
+ function isRecord(value: unknown): value is Record<string, unknown> {
68
+ return typeof value === "object" && value !== null;
69
+ }
70
+
71
+ function validateAgainstSchema(
72
+ value: unknown,
73
+ schema: JsonSchema,
74
+ path = "$"
75
+ ): string[] {
76
+ switch (schema.type) {
77
+ case "string": {
78
+ if (typeof value !== "string") {
79
+ return [`${path} should be a string`];
80
+ }
81
+
82
+ if (schema.enum && !schema.enum.includes(value)) {
83
+ return [`${path} should be one of: ${schema.enum.join(", ")}`];
84
+ }
85
+
86
+ return [];
87
+ }
88
+ case "boolean": {
89
+ return typeof value === "boolean" ? [] : [`${path} should be a boolean`];
90
+ }
91
+ case "array": {
92
+ if (!Array.isArray(value)) {
93
+ return [`${path} should be an array`];
94
+ }
95
+
96
+ return value.flatMap((item, index) =>
97
+ validateAgainstSchema(item, schema.items, `${path}[${index}]`)
98
+ );
99
+ }
100
+ case "object": {
101
+ if (!isRecord(value) || Array.isArray(value)) {
102
+ return [`${path} should be an object`];
103
+ }
104
+
105
+ const errors: string[] = [];
106
+ const required = new Set(schema.required ?? []);
107
+
108
+ for (const key of required) {
109
+ if (!(key in value)) {
110
+ errors.push(`${path}.${key} is required`);
111
+ }
112
+ }
113
+
114
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
115
+ if (key in value) {
116
+ errors.push(
117
+ ...validateAgainstSchema(value[key], propertySchema, `${path}.${key}`)
118
+ );
119
+ }
120
+ }
121
+
122
+ if (schema.additionalProperties === false) {
123
+ for (const key of Object.keys(value)) {
124
+ if (!(key in schema.properties)) {
125
+ errors.push(`${path}.${key} is not allowed`);
126
+ }
127
+ }
128
+ }
129
+
130
+ return errors;
131
+ }
132
+ }
133
+ }
134
+
135
+ function normalizeBaseURL(baseURL: string): string {
136
+ return baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL;
137
+ }
138
+
139
+ function extractAssistantMessage(response: unknown): {
140
+ content?: unknown;
141
+ tool_calls?: unknown;
142
+ } {
143
+ if (!isRecord(response)) {
144
+ throw new Error("Provider returned non-object response");
145
+ }
146
+
147
+ const parsed = response as ChatCompletionResponse;
148
+ const message = parsed.choices?.[0]?.message;
149
+
150
+ if (!isRecord(message)) {
151
+ throw new Error("Provider returned empty assistant message");
152
+ }
153
+
154
+ if (message.function_call !== undefined) {
155
+ throw new Error(
156
+ "Provider returned legacy function_call payload; expected tool_calls"
157
+ );
158
+ }
159
+
160
+ return message;
161
+ }
162
+
163
+ function extractMessageContent(response: unknown): string {
164
+ const content = extractAssistantMessage(response).content;
165
+
166
+ if (typeof content !== "string" || content.trim() === "") {
167
+ throw new Error("Provider returned empty message content");
168
+ }
169
+
170
+ return content;
171
+ }
172
+
173
+ function parseStrictJSONObject(text: string, context = "content"): unknown {
174
+ try {
175
+ return JSON.parse(text);
176
+ } catch (error) {
177
+ throw new Error(
178
+ `Provider returned invalid JSON ${context}: ${
179
+ error instanceof Error ? error.message : String(error)
180
+ }`
181
+ );
182
+ }
183
+ }
184
+
185
+ function getToolInputSchema(tool: AgentTool): ObjectSchema {
186
+ if (tool.inputSchema.type !== "object") {
187
+ throw new Error(`Tool ${tool.name} input schema must be an object schema`);
188
+ }
189
+
190
+ return tool.inputSchema;
191
+ }
192
+
193
+ function parseAssistantToolCalls(rawToolCalls: unknown): AgentToolCallResponse[] {
194
+ if (rawToolCalls === undefined) {
195
+ return [];
196
+ }
197
+
198
+ if (!Array.isArray(rawToolCalls)) {
199
+ throw new Error("Provider returned invalid tool_calls payload");
200
+ }
201
+
202
+ return rawToolCalls.map((rawToolCall, index) => {
203
+ if (!isRecord(rawToolCall)) {
204
+ throw new Error("Provider returned invalid tool call entry");
205
+ }
206
+
207
+ const type = rawToolCall.type;
208
+ if (typeof type === "string" && type !== "function") {
209
+ throw new Error(`Unsupported tool call type: ${type}`);
210
+ }
211
+
212
+ if (!isRecord(rawToolCall.function)) {
213
+ throw new Error("Provider returned tool call without function payload");
214
+ }
215
+
216
+ const name = rawToolCall.function.name;
217
+ if (typeof name !== "string" || name.trim() === "") {
218
+ throw new Error("Provider returned tool call without function name");
219
+ }
220
+
221
+ const argumentsText = rawToolCall.function.arguments;
222
+ if (typeof argumentsText !== "string") {
223
+ throw new Error(`Provider returned non-string arguments for tool ${name}`);
224
+ }
225
+
226
+ const id =
227
+ typeof rawToolCall.id === "string" && rawToolCall.id.trim() !== ""
228
+ ? rawToolCall.id
229
+ : `tool_call_${index + 1}`;
230
+
231
+ return {
232
+ id,
233
+ name,
234
+ input: parseToolInputArguments(argumentsText, name),
235
+ };
236
+ });
237
+ }
238
+
239
+ function normalizeAssistantMessage(content: unknown): string | undefined {
240
+ if (typeof content !== "string") {
241
+ return undefined;
242
+ }
243
+
244
+ const text = content.trim();
245
+ return text === "" ? undefined : text;
246
+ }
247
+
248
+ function parseToolInputArguments(argumentsText: string, toolName: string): unknown {
249
+ if (argumentsText.trim() === "") {
250
+ return {};
251
+ }
252
+
253
+ return parseStrictJSONObject(argumentsText, `arguments for tool ${toolName}`);
254
+ }
255
+
256
+ export class LLMService implements LLMServiceContract {
257
+ private config: LLMServiceConfig;
258
+ private fetchImpl: FetchLike;
259
+
260
+ constructor(config: LLMServiceConfig, options: LLMServiceOptions = {}) {
261
+ this.config = config;
262
+ const globalFetch = (globalThis as { fetch?: FetchLike }).fetch;
263
+ const resolvedFetch = options.fetch ?? globalFetch;
264
+
265
+ if (typeof resolvedFetch !== "function") {
266
+ throw new Error("Global fetch is unavailable");
267
+ }
268
+
269
+ this.fetchImpl = resolvedFetch;
270
+ }
271
+
272
+ async generateObject<T>(params: GenerateObjectParams<T>): Promise<T> {
273
+ const parsed = await this.requestJSON({
274
+ systemPrompt: params.systemPrompt,
275
+ userPrompt: params.userPrompt,
276
+ schema: params.schema,
277
+ schemaName: "structured_output",
278
+ });
279
+ const errors = validateAgainstSchema(parsed, params.schema);
280
+
281
+ if (errors.length > 0) {
282
+ throw new Error(`Schema validation failed: ${errors.join("; ")}`);
283
+ }
284
+
285
+ return parsed as T;
286
+ }
287
+
288
+ async runAgent(params: RunAgentParams): Promise<AgentRunResult> {
289
+ const steps: AgentStep[] = [];
290
+ let didWrite = false;
291
+ const tools = new Map(params.tools.map((tool) => [tool.name, tool]));
292
+ const toolDefinitions: AgentToolDefinition[] = params.tools.map((tool) => ({
293
+ type: "function",
294
+ function: {
295
+ name: tool.name,
296
+ description: tool.description,
297
+ parameters: getToolInputSchema(tool),
298
+ strict: true,
299
+ },
300
+ }));
301
+ const messages: AgentRequestMessage[] = [
302
+ { role: "system", content: params.systemPrompt },
303
+ { role: "user", content: params.userPrompt },
304
+ ];
305
+
306
+ for (let stepIndex = 0; stepIndex < params.maxSteps; stepIndex += 1) {
307
+ const responsePayload = await this.requestChatCompletion({
308
+ messages,
309
+ tools: toolDefinitions,
310
+ tool_choice: "auto",
311
+ });
312
+ const assistantMessage = extractAssistantMessage(responsePayload);
313
+ const toolCalls = parseAssistantToolCalls(assistantMessage.tool_calls);
314
+
315
+ if (toolCalls.length === 0) {
316
+ const finalMessage = normalizeAssistantMessage(assistantMessage.content);
317
+ steps.push({
318
+ type: "assistant",
319
+ response: { action: "finish", message: finalMessage },
320
+ });
321
+ return {
322
+ steps,
323
+ didWrite,
324
+ finalMessage,
325
+ };
326
+ }
327
+
328
+ const assistantToolCalls: AgentToolCallRequest[] = toolCalls.map((toolCall) => ({
329
+ id: toolCall.id,
330
+ type: "function",
331
+ function: {
332
+ name: toolCall.name,
333
+ arguments: JSON.stringify(toolCall.input),
334
+ },
335
+ }));
336
+
337
+ steps.push({
338
+ type: "assistant",
339
+ response: {
340
+ action: "tool",
341
+ tool_calls: toolCalls.map((toolCall) => ({
342
+ tool_name: toolCall.name,
343
+ tool_input: toolCall.input,
344
+ })),
345
+ },
346
+ });
347
+ messages.push({
348
+ role: "assistant",
349
+ content: typeof assistantMessage.content === "string" ? assistantMessage.content : null,
350
+ tool_calls: assistantToolCalls,
351
+ });
352
+
353
+ for (const toolCall of toolCalls) {
354
+ const tool = tools.get(toolCall.name);
355
+ if (!tool) {
356
+ throw new Error(`Agent requested unknown tool: ${toolCall.name}`);
357
+ }
358
+
359
+ const toolErrors = validateAgainstSchema(toolCall.input, tool.inputSchema);
360
+ if (toolErrors.length > 0) {
361
+ throw new Error(
362
+ `Tool input schema validation failed for ${tool.name}: ${toolErrors.join("; ")}`
363
+ );
364
+ }
365
+
366
+ const toolOutput = await tool.execute(toolCall.input);
367
+ steps.push({
368
+ type: "tool",
369
+ toolName: tool.name,
370
+ toolInput: toolCall.input,
371
+ toolOutput,
372
+ });
373
+ messages.push({
374
+ role: "tool",
375
+ tool_call_id: toolCall.id,
376
+ content: toolOutput,
377
+ });
378
+
379
+ if (tool.name === "write") {
380
+ didWrite = true;
381
+ }
382
+ }
383
+ }
384
+
385
+ return {
386
+ steps,
387
+ didWrite,
388
+ finalMessage: "Agent stopped after reaching max steps",
389
+ };
390
+ }
391
+
392
+ private async requestJSON(params: {
393
+ systemPrompt: string;
394
+ userPrompt: string;
395
+ schema: JsonSchema;
396
+ schemaName: string;
397
+ }): Promise<unknown> {
398
+ const payload = await this.requestChatCompletion({
399
+ messages: [
400
+ {
401
+ role: "system",
402
+ content: params.systemPrompt,
403
+ },
404
+ {
405
+ role: "user",
406
+ content: params.userPrompt,
407
+ },
408
+ ],
409
+ response_format: {
410
+ type: "json_schema",
411
+ json_schema: {
412
+ name: params.schemaName,
413
+ strict: true,
414
+ schema: params.schema,
415
+ },
416
+ },
417
+ });
418
+ const content = extractMessageContent(payload);
419
+ return parseStrictJSONObject(content);
420
+ }
421
+
422
+ private async requestChatCompletion(body: Record<string, unknown>): Promise<unknown> {
423
+ const response = await this.fetchImpl(
424
+ `${normalizeBaseURL(this.config.baseURL)}/chat/completions`,
425
+ {
426
+ method: "POST",
427
+ headers: {
428
+ "content-type": "application/json",
429
+ authorization: `Bearer ${this.config.apiKey}`,
430
+ },
431
+ body: JSON.stringify({
432
+ model: this.config.model,
433
+ ...body,
434
+ }),
435
+ }
436
+ );
437
+
438
+ if (!response.ok) {
439
+ const errorText = await response.text();
440
+ throw new Error(
441
+ `Provider request failed with status ${response.status}: ${errorText}`
442
+ );
443
+ }
444
+
445
+ return (await response.json()) as unknown;
446
+ }
447
+ }
@@ -0,0 +1,87 @@
1
+ export type JsonSchema =
2
+ | {
3
+ type: "string";
4
+ description?: string;
5
+ enum?: string[];
6
+ }
7
+ | {
8
+ type: "boolean";
9
+ description?: string;
10
+ }
11
+ | {
12
+ type: "array";
13
+ description?: string;
14
+ items: JsonSchema;
15
+ }
16
+ | {
17
+ type: "object";
18
+ description?: string;
19
+ properties: Record<string, JsonSchema>;
20
+ required?: string[];
21
+ additionalProperties?: boolean;
22
+ };
23
+
24
+ export interface GenerateObjectParams<T> {
25
+ systemPrompt: string;
26
+ userPrompt: string;
27
+ schema: JsonSchema;
28
+ }
29
+
30
+ export interface AgentTool {
31
+ name: string;
32
+ description: string;
33
+ inputSchema: JsonSchema;
34
+ execute(input: unknown): Promise<string>;
35
+ }
36
+
37
+ export interface RunAgentParams {
38
+ systemPrompt: string;
39
+ userPrompt: string;
40
+ tools: AgentTool[];
41
+ maxSteps: number;
42
+ }
43
+
44
+ export interface AgentStep {
45
+ type: "assistant" | "tool";
46
+ response?: unknown;
47
+ toolName?: string;
48
+ toolInput?: unknown;
49
+ toolOutput?: string;
50
+ }
51
+
52
+ export interface AgentRunResult {
53
+ steps: AgentStep[];
54
+ didWrite: boolean;
55
+ finalMessage?: string;
56
+ }
57
+
58
+ export interface LLMServiceConfig {
59
+ baseURL: string;
60
+ apiKey: string;
61
+ model: string;
62
+ }
63
+
64
+ export interface FetchLikeResponse {
65
+ ok: boolean;
66
+ status: number;
67
+ json(): Promise<unknown>;
68
+ text(): Promise<string>;
69
+ }
70
+
71
+ export type FetchLike = (
72
+ input: string,
73
+ init?: {
74
+ method?: string;
75
+ headers?: Record<string, string>;
76
+ body?: string;
77
+ }
78
+ ) => Promise<FetchLikeResponse>;
79
+
80
+ export interface LLMServiceOptions {
81
+ fetch?: FetchLike;
82
+ }
83
+
84
+ export interface LLMService {
85
+ generateObject<T>(params: GenerateObjectParams<T>): Promise<T>;
86
+ runAgent(params: RunAgentParams): Promise<AgentRunResult>;
87
+ }
package/src/logger.ts ADDED
@@ -0,0 +1,125 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import type { LogEntry, LogLevel } from "./types.js";
4
+
5
+ const LOG_LEVELS: Record<LogLevel, number> = {
6
+ debug: 0,
7
+ info: 1,
8
+ warn: 2,
9
+ error: 3,
10
+ };
11
+
12
+ function parseLogLevel(level: string): LogLevel {
13
+ if (
14
+ level === "debug" ||
15
+ level === "info" ||
16
+ level === "warn" ||
17
+ level === "error"
18
+ ) {
19
+ return level;
20
+ }
21
+
22
+ return "info";
23
+ }
24
+
25
+ export class FileLogger {
26
+ private pluginRootDir: string;
27
+ private level: LogLevel;
28
+ private logsDir: string;
29
+
30
+ constructor(pluginRootDir: string, level: string) {
31
+ this.pluginRootDir = pluginRootDir;
32
+ this.level = parseLogLevel(level);
33
+ this.logsDir = path.join(pluginRootDir, "logs");
34
+ this.ensureLogsDir();
35
+ }
36
+
37
+ private ensureLogsDir(): void {
38
+ if (!fs.existsSync(this.logsDir)) {
39
+ fs.mkdirSync(this.logsDir, { recursive: true });
40
+ }
41
+ }
42
+
43
+ private getLogFilePath(): string {
44
+ const date = new Date().toISOString().split("T")[0];
45
+ return path.join(this.logsDir, `reflection-${date}.log`);
46
+ }
47
+
48
+ private shouldLog(level: LogLevel): boolean {
49
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.level];
50
+ }
51
+
52
+ private formatTimestamp(): string {
53
+ return new Date().toISOString();
54
+ }
55
+
56
+ private writeLog(entry: LogEntry): void {
57
+ const logLine = `${JSON.stringify(entry)}\n`;
58
+ const logFile = this.getLogFilePath();
59
+
60
+ try {
61
+ fs.appendFileSync(logFile, logLine, "utf-8");
62
+ } catch (error) {
63
+ console.error("[ReflectionPlugin] Failed to write log:", error);
64
+ }
65
+ }
66
+
67
+ private log(
68
+ level: LogLevel,
69
+ component: string,
70
+ event: string,
71
+ details?: Record<string, unknown>,
72
+ sessionKey?: string
73
+ ): void {
74
+ if (!this.shouldLog(level)) {
75
+ return;
76
+ }
77
+
78
+ const entry: LogEntry = {
79
+ timestamp: this.formatTimestamp(),
80
+ level,
81
+ component,
82
+ event,
83
+ details,
84
+ sessionKey,
85
+ };
86
+
87
+ this.writeLog(entry);
88
+ }
89
+
90
+ debug(
91
+ component: string,
92
+ event: string,
93
+ details?: Record<string, unknown>,
94
+ sessionKey?: string
95
+ ): void {
96
+ this.log("debug", component, event, details, sessionKey);
97
+ }
98
+
99
+ info(
100
+ component: string,
101
+ event: string,
102
+ details?: Record<string, unknown>,
103
+ sessionKey?: string
104
+ ): void {
105
+ this.log("info", component, event, details, sessionKey);
106
+ }
107
+
108
+ warn(
109
+ component: string,
110
+ event: string,
111
+ details?: Record<string, unknown>,
112
+ sessionKey?: string
113
+ ): void {
114
+ this.log("warn", component, event, details, sessionKey);
115
+ }
116
+
117
+ error(
118
+ component: string,
119
+ event: string,
120
+ details?: Record<string, unknown>,
121
+ sessionKey?: string
122
+ ): void {
123
+ this.log("error", component, event, details, sessionKey);
124
+ }
125
+ }