@persistio/openclaw-plugin 0.1.8 → 0.2.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/src/client.ts CHANGED
@@ -1,61 +1,43 @@
1
- import type { PersistioIngestPolicy } from './ingest-policy.js';
2
-
3
- export interface PersistioConfig {
4
- baseURL: string;
5
- apiKey: string;
6
- tokenBudget: number;
7
- recallTopK: number;
8
- recallMinSimilarity?: number;
9
- recallTimeout: number;
10
- recallIncludePending: boolean;
11
- includeRelatedMemories: boolean;
12
- ingest: PersistioIngestPolicy;
13
- send: PersistioSendConfig;
14
- }
15
-
16
- export type PersistioSendRoleStatus = 'enabled' | 'disabled';
17
-
18
- export interface PersistioSendConfig {
19
- roles: {
20
- user: PersistioSendRoleStatus;
21
- agent: PersistioSendRoleStatus;
22
- tool: PersistioSendRoleStatus;
23
- };
24
- }
1
+ import type { PersistioV2Config } from './config.js';
25
2
 
26
3
  export interface PersistioMemory {
27
4
  id: string;
28
5
  data: string;
29
6
  subject: string;
30
7
  similarity?: number;
31
- categories: string[];
32
- confidence: number;
33
- }
34
-
35
- export interface GetMemoryOptions {
36
- includePending?: boolean;
8
+ confidence?: number;
9
+ categories?: string[];
10
+ source?: string;
11
+ edge_type?: string | null;
37
12
  }
38
13
 
39
14
  export interface RecallBundle {
40
15
  global_user_rules?: string[];
41
- user_rules: string[];
42
- user_preferences: string[];
43
- task_patterns: string[];
44
- workflows: string[];
45
- project: string[];
46
- constraints: string[];
47
- decisions: string[];
48
- system_facts: string[];
49
- domain_knowledge: string[];
16
+ user_rules?: string[];
17
+ user_preferences?: string[];
18
+ task_patterns?: string[];
19
+ workflows?: string[];
20
+ project?: string[];
21
+ constraints?: string[];
22
+ decisions?: string[];
23
+ system_facts?: string[];
24
+ domain_knowledge?: string[];
50
25
  }
51
26
 
52
27
  export interface RecallBundleResponse {
53
- bundle: RecallBundle;
28
+ bundle?: RecallBundle;
54
29
  related_bundle?: RecallBundle;
55
30
  }
56
31
 
57
- export interface RecallBundleOptions {
58
- includeRelated?: boolean;
32
+ export interface RecallResult {
33
+ memories: PersistioMemory[];
34
+ relatedMemories: PersistioMemory[];
35
+ }
36
+
37
+ export interface IngestChunk {
38
+ role: string;
39
+ content: string;
40
+ timestamp: string;
59
41
  }
60
42
 
61
43
  export class PersistioTimeoutError extends Error {
@@ -68,83 +50,49 @@ export class PersistioTimeoutError extends Error {
68
50
  export class PersistioClient {
69
51
  private readonly baseURL: string;
70
52
  private readonly apiKey: string;
71
- private readonly recallTopK: number;
72
- private readonly recallMinSimilarity?: number;
73
- private readonly recallTimeout: number;
74
- private readonly recallIncludePending: boolean;
75
- private readonly includeRelatedMemories: boolean;
76
- private readonly ingestTimeout: number;
77
- private readonly writeTimeout: number;
78
-
79
- constructor(config: PersistioConfig) {
53
+
54
+ constructor(private readonly config: PersistioV2Config) {
80
55
  this.baseURL = config.baseURL.replace(/\/$/, '');
81
56
  this.apiKey = config.apiKey;
82
- this.recallTopK = config.recallTopK;
83
- this.recallMinSimilarity = config.recallMinSimilarity;
84
- this.recallTimeout = config.recallTimeout;
85
- this.recallIncludePending = config.recallIncludePending;
86
- this.includeRelatedMemories = config.includeRelatedMemories;
87
- this.ingestTimeout = config.ingest.timeoutMs;
88
- this.writeTimeout = config.ingest.timeoutMs;
89
- }
90
-
91
- private headers(): Record<string, string> {
92
- return {
93
- 'Content-Type': 'application/json',
94
- 'Authorization': `Bearer ${this.apiKey}`,
95
- };
96
57
  }
97
58
 
98
- async recall(query: string): Promise<PersistioMemory[]> {
99
- return withRequestDeadline('recall', this.recallTimeout, async (signal) => {
100
- const body: Record<string, unknown> = {
101
- query,
102
- top_k: this.recallTopK,
103
- include_pending: this.recallIncludePending
104
- };
105
- if (typeof this.recallMinSimilarity === 'number') {
106
- body.min_similarity = this.recallMinSimilarity;
107
- }
108
-
59
+ async recall(query: string, options: { maxResults?: number } = {}): Promise<RecallResult> {
60
+ return withRequestDeadline('recall', this.config.recall.timeoutMs, async (signal) => {
61
+ const body = this.buildRecallBody(query, options.maxResults);
109
62
  const res = await fetch(`${this.baseURL}/v1/recall`, {
110
63
  method: 'POST',
111
64
  headers: this.headers(),
112
65
  body: JSON.stringify(body),
113
66
  signal,
114
67
  });
115
- if (!res.ok) throw new Error(`Persistio recall failed: ${res.status}`);
116
- const data = await res.json() as { memories: PersistioMemory[] };
117
- return data.memories ?? [];
68
+ if (!res.ok) throw new Error(await formatHttpError('recall', res));
69
+ const data = await res.json() as { memories?: PersistioMemory[]; related_memories?: PersistioMemory[] };
70
+ return {
71
+ memories: Array.isArray(data.memories) ? data.memories : [],
72
+ relatedMemories: Array.isArray(data.related_memories) ? data.related_memories : [],
73
+ };
118
74
  });
119
75
  }
120
76
 
121
- async recallBundle(query: string, topK?: number, options: RecallBundleOptions = {}): Promise<RecallBundleResponse> {
122
- return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
123
- const body: Record<string, unknown> = {
124
- query,
125
- top_k: topK ?? this.recallTopK,
126
- include_pending: this.recallIncludePending,
127
- include_related: options.includeRelated ?? this.includeRelatedMemories
77
+ async recallBundle(query: string): Promise<RecallBundleResponse> {
78
+ return withRequestDeadline('recallBundle', this.config.recall.timeoutMs, async (signal) => {
79
+ const body = {
80
+ ...this.buildRecallBody(query, this.config.recall.maxResults),
128
81
  };
129
- if (typeof this.recallMinSimilarity === 'number') {
130
- body.min_similarity = this.recallMinSimilarity;
131
- }
132
-
133
82
  const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
134
83
  method: 'POST',
135
84
  headers: this.headers(),
136
85
  body: JSON.stringify(body),
137
86
  signal,
138
87
  });
139
- if (!res.ok) throw new Error(`Persistio recallBundle failed: ${res.status}`);
140
- const data = await res.json() as RecallBundleResponse;
141
- return data;
88
+ if (!res.ok) throw new Error(await formatHttpError('recallBundle', res));
89
+ return await res.json() as RecallBundleResponse;
142
90
  });
143
91
  }
144
92
 
145
- async ingest(sessionId: string, chunks: Array<{ role: string; content: string; timestamp: string }>): Promise<void> {
93
+ async ingest(sessionId: string, chunks: IngestChunk[]): Promise<void> {
146
94
  if (chunks.length === 0) return;
147
- await withRequestDeadline('ingest', this.ingestTimeout, async (signal) => {
95
+ await withRequestDeadline('ingest', this.config.capture.timeoutMs, async (signal) => {
148
96
  const res = await fetch(`${this.baseURL}/v1/ingest`, {
149
97
  method: 'POST',
150
98
  headers: this.headers(),
@@ -155,56 +103,52 @@ export class PersistioClient {
155
103
  });
156
104
  }
157
105
 
158
- async addMemory(data: string, subject: string): Promise<void> {
159
- await withRequestDeadline('addMemory', this.writeTimeout, async (signal) => {
106
+ async storeMemory(data: string, subject: string): Promise<PersistioMemory> {
107
+ return withRequestDeadline('memory_store', this.config.capture.timeoutMs, async (signal) => {
160
108
  const res = await fetch(`${this.baseURL}/v1/memories`, {
161
109
  method: 'POST',
162
110
  headers: this.headers(),
163
111
  body: JSON.stringify({ data, subject }),
164
112
  signal,
165
113
  });
166
- if (!res.ok) throw new Error(`Persistio addMemory failed: ${res.status}`);
114
+ if (!res.ok) throw new Error(await formatHttpError('memory_store', res));
115
+ return await res.json() as PersistioMemory;
167
116
  });
168
117
  }
169
118
 
170
- async deleteMemory(id: string): Promise<void> {
171
- await withRequestDeadline('deleteMemory', this.writeTimeout, async (signal) => {
172
- const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
119
+ async forgetMemory(id: string): Promise<void> {
120
+ await withRequestDeadline('memory_forget', this.config.capture.timeoutMs, async (signal) => {
121
+ const res = await fetch(`${this.baseURL}/v1/memories/${encodeURIComponent(id)}`, {
173
122
  method: 'DELETE',
174
123
  headers: this.headers(),
175
124
  signal,
176
125
  });
177
- if (!res.ok) throw new Error(`Persistio deleteMemory failed: ${res.status}`);
126
+ if (!res.ok) throw new Error(await formatHttpError('memory_forget', res));
178
127
  });
179
128
  }
180
129
 
181
- async getMemory(id: string, options: GetMemoryOptions = {}): Promise<PersistioMemory | null> {
182
- return withRequestDeadline('getMemory', this.recallTimeout, async (signal) => {
183
- const query = options.includePending ? '?include_pending=true' : '';
184
- const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
185
- headers: this.headers(),
186
- signal,
187
- });
188
- if (res.status === 404) return null;
189
- if (!res.ok) throw new Error(`Persistio getMemory failed: ${res.status}`);
190
- return await res.json() as PersistioMemory;
191
- });
130
+ private buildRecallBody(query: string, maxResults = this.config.recall.maxResults): Record<string, unknown> {
131
+ const body: Record<string, unknown> = {
132
+ query,
133
+ top_k: maxResults,
134
+ include_pending: this.config.recall.includePending,
135
+ include_related: this.config.recall.includeRelated,
136
+ };
137
+ if (typeof this.config.recall.minSimilarity === 'number') {
138
+ body.min_similarity = this.config.recall.minSimilarity;
139
+ }
140
+ return body;
192
141
  }
193
142
 
194
- async listMemories(): Promise<PersistioMemory[]> {
195
- return withRequestDeadline('listMemories', this.recallTimeout, async (signal) => {
196
- const res = await fetch(`${this.baseURL}/v1/memories`, {
197
- headers: this.headers(),
198
- signal,
199
- });
200
- if (!res.ok) throw new Error(`Persistio listMemories failed: ${res.status}`);
201
- const data = await res.json() as { items: PersistioMemory[] };
202
- return data.items ?? [];
203
- });
143
+ private headers(): Record<string, string> {
144
+ return {
145
+ 'Content-Type': 'application/json',
146
+ 'Authorization': `Bearer ${this.apiKey}`,
147
+ };
204
148
  }
205
149
  }
206
150
 
207
- async function withRequestDeadline<T>(
151
+ export async function withRequestDeadline<T>(
208
152
  operation: string,
209
153
  timeoutMs: number,
210
154
  run: (signal: AbortSignal) => Promise<T>,
@@ -215,7 +159,6 @@ async function withRequestDeadline<T>(
215
159
 
216
160
  const controller = new AbortController();
217
161
  let timeout: ReturnType<typeof setTimeout> | undefined;
218
-
219
162
  const deadline = new Promise<never>((_resolve, reject) => {
220
163
  timeout = setTimeout(() => {
221
164
  controller.abort();
@@ -236,8 +179,7 @@ async function withRequestDeadline<T>(
236
179
  }
237
180
 
238
181
  function isAbortLikeError(err: unknown): boolean {
239
- if (!(err instanceof Error)) return false;
240
- return err.name === 'AbortError' || err.name === 'TimeoutError';
182
+ return err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError');
241
183
  }
242
184
 
243
185
  async function formatHttpError(operation: string, res: Response): Promise<string> {
@@ -245,7 +187,7 @@ async function formatHttpError(operation: string, res: Response): Promise<string
245
187
  try {
246
188
  detail = (await res.text()).trim().slice(0, 500);
247
189
  } catch {
248
- // Ignore response body read failures; the status is still actionable.
190
+ // Status code is still useful if the body cannot be read.
249
191
  }
250
192
 
251
193
  return detail
package/src/config.ts ADDED
@@ -0,0 +1,125 @@
1
+ export type PersistioCaptureRoleStatus = 'enabled' | 'bounded' | 'disabled';
2
+
3
+ export interface PersistioV2Config {
4
+ baseURL: string;
5
+ apiKey: string;
6
+ autoRecall: boolean;
7
+ autoCapture: boolean;
8
+ recall: {
9
+ timeoutMs: number;
10
+ maxResults: number;
11
+ tokenBudget: number;
12
+ minSimilarity?: number;
13
+ includePending: boolean;
14
+ includeRelated: boolean;
15
+ queryMaxChars: number;
16
+ };
17
+ capture: {
18
+ timeoutMs: number;
19
+ maxCharsPerTurn: number;
20
+ maxCharsPerMessage: number;
21
+ maxChunksPerTurn: number;
22
+ maxChunkChars: number;
23
+ roles: {
24
+ user: 'enabled' | 'disabled';
25
+ assistant: PersistioCaptureRoleStatus;
26
+ tool: 'enabled' | 'disabled';
27
+ };
28
+ };
29
+ }
30
+
31
+ const DEFAULT_CONFIG: PersistioV2Config = {
32
+ baseURL: '',
33
+ apiKey: '',
34
+ autoRecall: true,
35
+ autoCapture: true,
36
+ recall: {
37
+ timeoutMs: 1200,
38
+ maxResults: 4,
39
+ tokenBudget: 400,
40
+ includePending: false,
41
+ includeRelated: false,
42
+ queryMaxChars: 1200,
43
+ },
44
+ capture: {
45
+ timeoutMs: 10000,
46
+ maxCharsPerTurn: 6000,
47
+ maxCharsPerMessage: 3000,
48
+ maxChunksPerTurn: 4,
49
+ maxChunkChars: 2000,
50
+ roles: {
51
+ user: 'enabled',
52
+ assistant: 'bounded',
53
+ tool: 'disabled',
54
+ },
55
+ },
56
+ };
57
+
58
+ function readObject(value: unknown): Record<string, unknown> {
59
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
60
+ ? value as Record<string, unknown>
61
+ : {};
62
+ }
63
+
64
+ function readString(value: unknown, fallback = ''): string {
65
+ return typeof value === 'string' ? value : fallback;
66
+ }
67
+
68
+ function readBoolean(value: unknown, fallback: boolean): boolean {
69
+ return typeof value === 'boolean' ? value : fallback;
70
+ }
71
+
72
+ function readPositiveInteger(value: unknown, fallback: number, min = 1): number {
73
+ return typeof value === 'number' && Number.isFinite(value) && value >= min
74
+ ? Math.floor(value)
75
+ : fallback;
76
+ }
77
+
78
+ function readSimilarity(value: unknown): number | undefined {
79
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 1
80
+ ? value
81
+ : undefined;
82
+ }
83
+
84
+ function readEnabledDisabled(value: unknown, fallback: 'enabled' | 'disabled'): 'enabled' | 'disabled' {
85
+ return value === 'enabled' || value === 'disabled' ? value : fallback;
86
+ }
87
+
88
+ function readAssistantRole(value: unknown, fallback: PersistioCaptureRoleStatus): PersistioCaptureRoleStatus {
89
+ return value === 'enabled' || value === 'bounded' || value === 'disabled' ? value : fallback;
90
+ }
91
+
92
+ export function resolveConfig(raw: unknown): PersistioV2Config {
93
+ const input = readObject(raw);
94
+ const recall = readObject(input['recall']);
95
+ const capture = readObject(input['capture']);
96
+ const roles = readObject(capture['roles']);
97
+
98
+ return {
99
+ baseURL: readString(input['baseURL'], DEFAULT_CONFIG.baseURL),
100
+ apiKey: readString(input['apiKey'], DEFAULT_CONFIG.apiKey),
101
+ autoRecall: readBoolean(input['autoRecall'], DEFAULT_CONFIG.autoRecall),
102
+ autoCapture: readBoolean(input['autoCapture'], DEFAULT_CONFIG.autoCapture),
103
+ recall: {
104
+ timeoutMs: readPositiveInteger(recall['timeoutMs'], DEFAULT_CONFIG.recall.timeoutMs),
105
+ maxResults: readPositiveInteger(recall['maxResults'], DEFAULT_CONFIG.recall.maxResults),
106
+ tokenBudget: readPositiveInteger(recall['tokenBudget'], DEFAULT_CONFIG.recall.tokenBudget),
107
+ minSimilarity: readSimilarity(recall['minSimilarity']),
108
+ includePending: readBoolean(recall['includePending'], DEFAULT_CONFIG.recall.includePending),
109
+ includeRelated: readBoolean(recall['includeRelated'], DEFAULT_CONFIG.recall.includeRelated),
110
+ queryMaxChars: readPositiveInteger(recall['queryMaxChars'], DEFAULT_CONFIG.recall.queryMaxChars, 100),
111
+ },
112
+ capture: {
113
+ timeoutMs: readPositiveInteger(capture['timeoutMs'], DEFAULT_CONFIG.capture.timeoutMs),
114
+ maxCharsPerTurn: readPositiveInteger(capture['maxCharsPerTurn'], DEFAULT_CONFIG.capture.maxCharsPerTurn),
115
+ maxCharsPerMessage: readPositiveInteger(capture['maxCharsPerMessage'], DEFAULT_CONFIG.capture.maxCharsPerMessage),
116
+ maxChunksPerTurn: readPositiveInteger(capture['maxChunksPerTurn'], DEFAULT_CONFIG.capture.maxChunksPerTurn),
117
+ maxChunkChars: readPositiveInteger(capture['maxChunkChars'], DEFAULT_CONFIG.capture.maxChunkChars, 256),
118
+ roles: {
119
+ user: readEnabledDisabled(roles['user'], DEFAULT_CONFIG.capture.roles.user),
120
+ assistant: readAssistantRole(roles['assistant'], DEFAULT_CONFIG.capture.roles.assistant),
121
+ tool: readEnabledDisabled(roles['tool'], DEFAULT_CONFIG.capture.roles.tool),
122
+ },
123
+ },
124
+ };
125
+ }