@persistio/openclaw-plugin 0.1.7 → 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,57 +1,45 @@
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
- ingest: PersistioIngestPolicy;
11
- send: PersistioSendConfig;
12
- }
13
-
14
- export type PersistioSendRoleStatus = 'enabled' | 'disabled';
15
-
16
- export interface PersistioSendConfig {
17
- roles: {
18
- user: PersistioSendRoleStatus;
19
- agent: PersistioSendRoleStatus;
20
- tool: PersistioSendRoleStatus;
21
- };
22
- }
1
+ import type { PersistioV2Config } from './config.js';
23
2
 
24
3
  export interface PersistioMemory {
25
4
  id: string;
26
5
  data: string;
27
6
  subject: string;
28
7
  similarity?: number;
29
- categories: string[];
30
- confidence: number;
31
- }
32
-
33
- export interface GetMemoryOptions {
34
- includePending?: boolean;
8
+ confidence?: number;
9
+ categories?: string[];
10
+ source?: string;
11
+ edge_type?: string | null;
35
12
  }
36
13
 
37
14
  export interface RecallBundle {
38
15
  global_user_rules?: string[];
39
- user_rules: string[];
40
- user_preferences: string[];
41
- task_patterns: string[];
42
- workflows: string[];
43
- project: string[];
44
- constraints: string[];
45
- decisions: string[];
46
- system_facts: string[];
47
- 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[];
48
25
  }
49
26
 
50
27
  export interface RecallBundleResponse {
51
- bundle: RecallBundle;
28
+ bundle?: RecallBundle;
52
29
  related_bundle?: RecallBundle;
53
30
  }
54
31
 
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;
41
+ }
42
+
55
43
  export class PersistioTimeoutError extends Error {
56
44
  constructor(operation: string, timeoutMs: number) {
57
45
  super(`Persistio ${operation} timed out after ${timeoutMs}ms`);
@@ -62,70 +50,49 @@ export class PersistioTimeoutError extends Error {
62
50
  export class PersistioClient {
63
51
  private readonly baseURL: string;
64
52
  private readonly apiKey: string;
65
- private readonly recallTopK: number;
66
- private readonly recallMinSimilarity?: number;
67
- private readonly recallTimeout: number;
68
- private readonly ingestTimeout: number;
69
- private readonly writeTimeout: number;
70
53
 
71
- constructor(config: PersistioConfig) {
54
+ constructor(private readonly config: PersistioV2Config) {
72
55
  this.baseURL = config.baseURL.replace(/\/$/, '');
73
56
  this.apiKey = config.apiKey;
74
- this.recallTopK = config.recallTopK;
75
- this.recallMinSimilarity = config.recallMinSimilarity;
76
- this.recallTimeout = config.recallTimeout;
77
- this.ingestTimeout = config.ingest.timeoutMs;
78
- this.writeTimeout = config.ingest.timeoutMs;
79
57
  }
80
58
 
81
- private headers(): Record<string, string> {
82
- return {
83
- 'Content-Type': 'application/json',
84
- 'Authorization': `Bearer ${this.apiKey}`,
85
- };
86
- }
87
-
88
- async recall(query: string): Promise<PersistioMemory[]> {
89
- return withRequestDeadline('recall', this.recallTimeout, async (signal) => {
90
- const body: Record<string, unknown> = { query, top_k: this.recallTopK, include_pending: true };
91
- if (typeof this.recallMinSimilarity === 'number') {
92
- body.min_similarity = this.recallMinSimilarity;
93
- }
94
-
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);
95
62
  const res = await fetch(`${this.baseURL}/v1/recall`, {
96
63
  method: 'POST',
97
64
  headers: this.headers(),
98
65
  body: JSON.stringify(body),
99
66
  signal,
100
67
  });
101
- if (!res.ok) throw new Error(`Persistio recall failed: ${res.status}`);
102
- const data = await res.json() as { memories: PersistioMemory[] };
103
- 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
+ };
104
74
  });
105
75
  }
106
76
 
107
- async recallBundle(query: string, topK?: number): Promise<RecallBundleResponse> {
108
- return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
109
- const body: Record<string, unknown> = { query, top_k: topK ?? this.recallTopK, include_pending: true };
110
- if (typeof this.recallMinSimilarity === 'number') {
111
- body.min_similarity = this.recallMinSimilarity;
112
- }
113
-
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),
81
+ };
114
82
  const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
115
83
  method: 'POST',
116
84
  headers: this.headers(),
117
85
  body: JSON.stringify(body),
118
86
  signal,
119
87
  });
120
- if (!res.ok) throw new Error(`Persistio recallBundle failed: ${res.status}`);
121
- const data = await res.json() as RecallBundleResponse;
122
- return data;
88
+ if (!res.ok) throw new Error(await formatHttpError('recallBundle', res));
89
+ return await res.json() as RecallBundleResponse;
123
90
  });
124
91
  }
125
92
 
126
- async ingest(sessionId: string, chunks: Array<{ role: string; content: string; timestamp: string }>): Promise<void> {
93
+ async ingest(sessionId: string, chunks: IngestChunk[]): Promise<void> {
127
94
  if (chunks.length === 0) return;
128
- await withRequestDeadline('ingest', this.ingestTimeout, async (signal) => {
95
+ await withRequestDeadline('ingest', this.config.capture.timeoutMs, async (signal) => {
129
96
  const res = await fetch(`${this.baseURL}/v1/ingest`, {
130
97
  method: 'POST',
131
98
  headers: this.headers(),
@@ -136,56 +103,52 @@ export class PersistioClient {
136
103
  });
137
104
  }
138
105
 
139
- async addMemory(data: string, subject: string): Promise<void> {
140
- 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) => {
141
108
  const res = await fetch(`${this.baseURL}/v1/memories`, {
142
109
  method: 'POST',
143
110
  headers: this.headers(),
144
111
  body: JSON.stringify({ data, subject }),
145
112
  signal,
146
113
  });
147
- 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;
148
116
  });
149
117
  }
150
118
 
151
- async deleteMemory(id: string): Promise<void> {
152
- await withRequestDeadline('deleteMemory', this.writeTimeout, async (signal) => {
153
- 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)}`, {
154
122
  method: 'DELETE',
155
123
  headers: this.headers(),
156
124
  signal,
157
125
  });
158
- if (!res.ok) throw new Error(`Persistio deleteMemory failed: ${res.status}`);
126
+ if (!res.ok) throw new Error(await formatHttpError('memory_forget', res));
159
127
  });
160
128
  }
161
129
 
162
- async getMemory(id: string, options: GetMemoryOptions = {}): Promise<PersistioMemory | null> {
163
- return withRequestDeadline('getMemory', this.recallTimeout, async (signal) => {
164
- const query = options.includePending ? '?include_pending=true' : '';
165
- const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
166
- headers: this.headers(),
167
- signal,
168
- });
169
- if (res.status === 404) return null;
170
- if (!res.ok) throw new Error(`Persistio getMemory failed: ${res.status}`);
171
- return await res.json() as PersistioMemory;
172
- });
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;
173
141
  }
174
142
 
175
- async listMemories(): Promise<PersistioMemory[]> {
176
- return withRequestDeadline('listMemories', this.recallTimeout, async (signal) => {
177
- const res = await fetch(`${this.baseURL}/v1/memories`, {
178
- headers: this.headers(),
179
- signal,
180
- });
181
- if (!res.ok) throw new Error(`Persistio listMemories failed: ${res.status}`);
182
- const data = await res.json() as { items: PersistioMemory[] };
183
- return data.items ?? [];
184
- });
143
+ private headers(): Record<string, string> {
144
+ return {
145
+ 'Content-Type': 'application/json',
146
+ 'Authorization': `Bearer ${this.apiKey}`,
147
+ };
185
148
  }
186
149
  }
187
150
 
188
- async function withRequestDeadline<T>(
151
+ export async function withRequestDeadline<T>(
189
152
  operation: string,
190
153
  timeoutMs: number,
191
154
  run: (signal: AbortSignal) => Promise<T>,
@@ -196,7 +159,6 @@ async function withRequestDeadline<T>(
196
159
 
197
160
  const controller = new AbortController();
198
161
  let timeout: ReturnType<typeof setTimeout> | undefined;
199
-
200
162
  const deadline = new Promise<never>((_resolve, reject) => {
201
163
  timeout = setTimeout(() => {
202
164
  controller.abort();
@@ -217,8 +179,7 @@ async function withRequestDeadline<T>(
217
179
  }
218
180
 
219
181
  function isAbortLikeError(err: unknown): boolean {
220
- if (!(err instanceof Error)) return false;
221
- return err.name === 'AbortError' || err.name === 'TimeoutError';
182
+ return err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError');
222
183
  }
223
184
 
224
185
  async function formatHttpError(operation: string, res: Response): Promise<string> {
@@ -226,7 +187,7 @@ async function formatHttpError(operation: string, res: Response): Promise<string
226
187
  try {
227
188
  detail = (await res.text()).trim().slice(0, 500);
228
189
  } catch {
229
- // Ignore response body read failures; the status is still actionable.
190
+ // Status code is still useful if the body cannot be read.
230
191
  }
231
192
 
232
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
+ }