@oh-my-pi/pi-coding-agent 14.6.2 → 14.6.4
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/CHANGELOG.md +95 -2
- package/README.md +21 -0
- package/package.json +23 -7
- package/src/cli/grievances-cli.ts +89 -4
- package/src/commands/grievances.ts +33 -7
- package/src/config/prompt-templates.ts +14 -7
- package/src/config/settings-schema.ts +610 -100
- package/src/config/settings.ts +42 -0
- package/src/discovery/helpers.ts +13 -6
- package/src/edit/index.ts +3 -3
- package/src/edit/line-hash.ts +73 -25
- package/src/edit/modes/hashline.lark +10 -3
- package/src/edit/modes/hashline.ts +295 -40
- package/src/edit/renderer.ts +3 -3
- package/src/hindsight/backend.ts +205 -0
- package/src/hindsight/bank.ts +131 -0
- package/src/hindsight/client.ts +598 -0
- package/src/hindsight/config.ts +175 -0
- package/src/hindsight/content.ts +210 -0
- package/src/hindsight/index.ts +8 -0
- package/src/hindsight/mental-models.ts +382 -0
- package/src/hindsight/seeds.json +32 -0
- package/src/hindsight/state.ts +469 -0
- package/src/hindsight/transcript.ts +71 -0
- package/src/main.ts +7 -10
- package/src/memories/index.ts +1 -1
- package/src/memory-backend/index.ts +4 -0
- package/src/memory-backend/local-backend.ts +30 -0
- package/src/memory-backend/off-backend.ts +16 -0
- package/src/memory-backend/resolve.ts +24 -0
- package/src/memory-backend/types.ts +79 -0
- package/src/modes/components/settings-defs.ts +50 -451
- package/src/modes/components/settings-selector.ts +2 -2
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/controllers/command-controller.ts +266 -6
- package/src/modes/controllers/event-controller.ts +12 -0
- package/src/modes/controllers/selector-controller.ts +3 -12
- package/src/modes/theme/theme.ts +4 -0
- package/src/prompts/tools/github.md +3 -0
- package/src/prompts/tools/hashline.md +21 -16
- package/src/prompts/tools/read.md +10 -6
- package/src/prompts/tools/recall.md +5 -0
- package/src/prompts/tools/reflect.md +5 -0
- package/src/prompts/tools/retain.md +5 -0
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +21 -9
- package/src/session/agent-session.ts +118 -3
- package/src/slash-commands/builtin-registry.ts +12 -12
- package/src/task/executor.ts +3 -0
- package/src/task/index.ts +2 -0
- package/src/tools/ast-edit.ts +14 -5
- package/src/tools/ast-grep.ts +12 -3
- package/src/tools/find.ts +47 -7
- package/src/tools/gh-renderer.ts +10 -1
- package/src/tools/gh.ts +233 -5
- package/src/tools/hindsight-recall.ts +68 -0
- package/src/tools/hindsight-reflect.ts +55 -0
- package/src/tools/hindsight-retain.ts +60 -0
- package/src/tools/index.ts +20 -0
- package/src/tools/path-utils.ts +55 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/search.ts +45 -8
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal fetch-based client for the Hindsight HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the `@vectorize-io/hindsight-client` SDK with hand-rolled fetch
|
|
5
|
+
* calls so we depend on nothing more than the API endpoints we actually use:
|
|
6
|
+
* `retain`, `retainBatch`, `recall`, `reflect`, bank + document management,
|
|
7
|
+
* and bulk listing. Centralising construction here keeps a single seam for
|
|
8
|
+
* tests to spy on.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { HindsightConfig } from "./config";
|
|
12
|
+
|
|
13
|
+
const USER_AGENT = "oh-my-pi-coding-agent";
|
|
14
|
+
const DEFAULT_USER_AGENT = USER_AGENT;
|
|
15
|
+
|
|
16
|
+
export type Budget = "low" | "mid" | "high" | string;
|
|
17
|
+
export type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
|
|
18
|
+
export type UpdateMode = "replace" | "append";
|
|
19
|
+
export type ConsolidationState = "failed" | "pending" | "done";
|
|
20
|
+
|
|
21
|
+
export interface HindsightApiOptions {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
userAgent?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RecallResult {
|
|
28
|
+
id?: string;
|
|
29
|
+
text: string;
|
|
30
|
+
type?: string | null;
|
|
31
|
+
mentioned_at?: string | null;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RecallResponse {
|
|
36
|
+
results: RecallResult[];
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ReflectResponse {
|
|
41
|
+
text?: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RetainResponse {
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BankProfileResponse {
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ListMemoriesResponse {
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DocumentResponse {
|
|
58
|
+
[key: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ListDocumentsResponse {
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Mirrors the shape accepted by `POST /v1/default/banks/{bank_id}/memories`. */
|
|
66
|
+
export interface MemoryItemInput {
|
|
67
|
+
content: string;
|
|
68
|
+
timestamp?: Date | string;
|
|
69
|
+
context?: string;
|
|
70
|
+
metadata?: Record<string, string>;
|
|
71
|
+
documentId?: string;
|
|
72
|
+
tags?: string[];
|
|
73
|
+
/** Scoping policy for observations derived from this item. */
|
|
74
|
+
observationScopes?: "per_tag" | "combined" | "all_combinations" | string[][];
|
|
75
|
+
/** Per-item extraction strategy override. */
|
|
76
|
+
strategy?: string;
|
|
77
|
+
updateMode?: UpdateMode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RetainOptions {
|
|
81
|
+
timestamp?: Date | string;
|
|
82
|
+
context?: string;
|
|
83
|
+
metadata?: Record<string, string>;
|
|
84
|
+
documentId?: string;
|
|
85
|
+
async?: boolean;
|
|
86
|
+
tags?: string[];
|
|
87
|
+
updateMode?: UpdateMode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RetainBatchOptions {
|
|
91
|
+
/** Document id applied to every item that doesn't carry its own. */
|
|
92
|
+
documentId?: string;
|
|
93
|
+
/** Tags attached to the resulting document(s), not individual items. */
|
|
94
|
+
documentTags?: string[];
|
|
95
|
+
async?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface RecallOptions {
|
|
99
|
+
types?: string[];
|
|
100
|
+
maxTokens?: number;
|
|
101
|
+
budget?: Budget;
|
|
102
|
+
tags?: string[];
|
|
103
|
+
tagsMatch?: TagsMatch;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ReflectOptions {
|
|
107
|
+
context?: string;
|
|
108
|
+
budget?: Budget;
|
|
109
|
+
tags?: string[];
|
|
110
|
+
tagsMatch?: TagsMatch;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CreateBankOptions {
|
|
114
|
+
reflectMission?: string;
|
|
115
|
+
retainMission?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ListMemoriesOptions {
|
|
119
|
+
limit?: number;
|
|
120
|
+
offset?: number;
|
|
121
|
+
type?: string;
|
|
122
|
+
q?: string;
|
|
123
|
+
consolidationState?: ConsolidationState;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ListDocumentsOptions {
|
|
127
|
+
limit?: number;
|
|
128
|
+
offset?: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface UpdateDocumentOptions {
|
|
132
|
+
tags?: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export type MentalModelDetail = "metadata" | "content" | "full";
|
|
136
|
+
export type MentalModelMode = "full" | "delta";
|
|
137
|
+
|
|
138
|
+
export interface MentalModelTrigger {
|
|
139
|
+
mode?: MentalModelMode;
|
|
140
|
+
refresh_after_consolidation?: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Shape returned by list/get on the mental-models endpoint. Fields are populated by `detail`. */
|
|
144
|
+
export interface MentalModelSummary {
|
|
145
|
+
id: string;
|
|
146
|
+
bank_id: string;
|
|
147
|
+
name: string;
|
|
148
|
+
tags?: string[];
|
|
149
|
+
last_refreshed_at?: string | null;
|
|
150
|
+
created_at?: string | null;
|
|
151
|
+
source_query?: string;
|
|
152
|
+
content?: string;
|
|
153
|
+
max_tokens?: number;
|
|
154
|
+
trigger?: MentalModelTrigger;
|
|
155
|
+
[key: string]: unknown;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface MentalModelListResponse {
|
|
159
|
+
items: MentalModelSummary[];
|
|
160
|
+
[key: string]: unknown;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface MentalModelHistoryEntry {
|
|
164
|
+
previous_content: string | null;
|
|
165
|
+
changed_at: string;
|
|
166
|
+
[key: string]: unknown;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface CreateMentalModelOptions {
|
|
170
|
+
id?: string;
|
|
171
|
+
tags?: string[];
|
|
172
|
+
maxTokens?: number;
|
|
173
|
+
trigger?: MentalModelTrigger;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface CreateMentalModelResponse {
|
|
177
|
+
operation_id?: string;
|
|
178
|
+
[key: string]: unknown;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface RefreshMentalModelResponse {
|
|
182
|
+
operation_id?: string;
|
|
183
|
+
[key: string]: unknown;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface ListMentalModelsOptions {
|
|
187
|
+
detail?: MentalModelDetail;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface GetMentalModelOptions {
|
|
191
|
+
detail?: MentalModelDetail;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export class HindsightError extends Error {
|
|
195
|
+
statusCode?: number;
|
|
196
|
+
details?: unknown;
|
|
197
|
+
|
|
198
|
+
constructor(message: string, statusCode?: number, details?: unknown) {
|
|
199
|
+
super(message);
|
|
200
|
+
this.name = "HindsightError";
|
|
201
|
+
this.statusCode = statusCode;
|
|
202
|
+
this.details = details;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface RequestOptions {
|
|
207
|
+
body?: Record<string, unknown>;
|
|
208
|
+
query?: Record<string, unknown>;
|
|
209
|
+
/** Return null instead of throwing on a 404 response. */
|
|
210
|
+
allow404?: boolean;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export class HindsightApi {
|
|
214
|
+
#baseUrl: string;
|
|
215
|
+
#headers: Record<string, string>;
|
|
216
|
+
|
|
217
|
+
constructor(options: HindsightApiOptions) {
|
|
218
|
+
this.#baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
219
|
+
this.#headers = {
|
|
220
|
+
"User-Agent": options.userAgent ?? DEFAULT_USER_AGENT,
|
|
221
|
+
"Content-Type": "application/json",
|
|
222
|
+
};
|
|
223
|
+
if (options.apiKey) {
|
|
224
|
+
this.#headers.Authorization = `Bearer ${options.apiKey}`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async retain(bankId: string, content: string, options?: RetainOptions): Promise<RetainResponse> {
|
|
229
|
+
const item = buildMemoryItem({
|
|
230
|
+
content,
|
|
231
|
+
timestamp: options?.timestamp,
|
|
232
|
+
context: options?.context,
|
|
233
|
+
metadata: options?.metadata,
|
|
234
|
+
documentId: options?.documentId,
|
|
235
|
+
tags: options?.tags,
|
|
236
|
+
updateMode: options?.updateMode,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
return this.#request<RetainResponse>(
|
|
240
|
+
"POST",
|
|
241
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/memories`,
|
|
242
|
+
"retain",
|
|
243
|
+
{ body: { items: [item], async: options?.async } },
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Retain multiple memories in a single request. Mirrors the official
|
|
249
|
+
* client's `retainBatch` — items hit `POST /memories` together so the
|
|
250
|
+
* server can dedupe and consolidate as a batch instead of N round-trips.
|
|
251
|
+
*
|
|
252
|
+
* Per-item `documentId` wins; `options.documentId` only fills the gaps.
|
|
253
|
+
*/
|
|
254
|
+
async retainBatch(bankId: string, items: MemoryItemInput[], options?: RetainBatchOptions): Promise<RetainResponse> {
|
|
255
|
+
const processed = items.map(item => {
|
|
256
|
+
const built = buildMemoryItem(item);
|
|
257
|
+
if (built.document_id === undefined && options?.documentId !== undefined) {
|
|
258
|
+
built.document_id = options.documentId;
|
|
259
|
+
}
|
|
260
|
+
return built;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return this.#request<RetainResponse>(
|
|
264
|
+
"POST",
|
|
265
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/memories`,
|
|
266
|
+
"retainBatch",
|
|
267
|
+
{
|
|
268
|
+
body: {
|
|
269
|
+
items: processed,
|
|
270
|
+
document_tags: options?.documentTags,
|
|
271
|
+
async: options?.async,
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async recall(bankId: string, query: string, options?: RecallOptions): Promise<RecallResponse> {
|
|
278
|
+
return this.#request<RecallResponse>(
|
|
279
|
+
"POST",
|
|
280
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/memories/recall`,
|
|
281
|
+
"recall",
|
|
282
|
+
{
|
|
283
|
+
body: {
|
|
284
|
+
query,
|
|
285
|
+
types: options?.types,
|
|
286
|
+
max_tokens: options?.maxTokens,
|
|
287
|
+
budget: options?.budget ?? "mid",
|
|
288
|
+
tags: options?.tags,
|
|
289
|
+
tags_match: options?.tagsMatch,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async reflect(bankId: string, query: string, options?: ReflectOptions): Promise<ReflectResponse> {
|
|
296
|
+
return this.#request<ReflectResponse>(
|
|
297
|
+
"POST",
|
|
298
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/reflect`,
|
|
299
|
+
"reflect",
|
|
300
|
+
{
|
|
301
|
+
body: {
|
|
302
|
+
query,
|
|
303
|
+
context: options?.context,
|
|
304
|
+
budget: options?.budget ?? "low",
|
|
305
|
+
tags: options?.tags,
|
|
306
|
+
tags_match: options?.tagsMatch,
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async createBank(bankId: string, options: CreateBankOptions = {}): Promise<BankProfileResponse> {
|
|
313
|
+
return this.#request<BankProfileResponse>(
|
|
314
|
+
"PUT",
|
|
315
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}`,
|
|
316
|
+
"createBank",
|
|
317
|
+
{
|
|
318
|
+
body: {
|
|
319
|
+
reflect_mission: options.reflectMission,
|
|
320
|
+
retain_mission: options.retainMission,
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Bulk-list memory units in a bank with optional filters and pagination.
|
|
328
|
+
* Endpoint: `GET /v1/default/banks/{bank_id}/memories/list`.
|
|
329
|
+
*/
|
|
330
|
+
async listMemories(bankId: string, options?: ListMemoriesOptions): Promise<ListMemoriesResponse> {
|
|
331
|
+
return this.#request<ListMemoriesResponse>(
|
|
332
|
+
"GET",
|
|
333
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/memories/list`,
|
|
334
|
+
"listMemories",
|
|
335
|
+
{
|
|
336
|
+
query: {
|
|
337
|
+
type: options?.type,
|
|
338
|
+
q: options?.q,
|
|
339
|
+
consolidation_state: options?.consolidationState,
|
|
340
|
+
limit: options?.limit,
|
|
341
|
+
offset: options?.offset,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Bulk-list documents in a bank. */
|
|
348
|
+
async listDocuments(bankId: string, options?: ListDocumentsOptions): Promise<ListDocumentsResponse> {
|
|
349
|
+
return this.#request<ListDocumentsResponse>(
|
|
350
|
+
"GET",
|
|
351
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/documents`,
|
|
352
|
+
"listDocuments",
|
|
353
|
+
{ query: { limit: options?.limit, offset: options?.offset } },
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Fetch a document. Returns `null` on 404 instead of throwing. */
|
|
358
|
+
async getDocument(bankId: string, documentId: string): Promise<DocumentResponse | null> {
|
|
359
|
+
return this.#request<DocumentResponse | null>(
|
|
360
|
+
"GET",
|
|
361
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/documents/${encodeURIComponent(documentId)}`,
|
|
362
|
+
"getDocument",
|
|
363
|
+
{ allow404: true },
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Update a document's mutable fields (currently just tags). */
|
|
368
|
+
async updateDocument(bankId: string, documentId: string, options: UpdateDocumentOptions): Promise<DocumentResponse> {
|
|
369
|
+
return this.#request<DocumentResponse>(
|
|
370
|
+
"PATCH",
|
|
371
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/documents/${encodeURIComponent(documentId)}`,
|
|
372
|
+
"updateDocument",
|
|
373
|
+
{ body: { tags: options.tags } },
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Delete a document and every memory derived from it. Returns `true` on
|
|
379
|
+
* success, `false` if the document was already gone (404).
|
|
380
|
+
*/
|
|
381
|
+
async deleteDocument(bankId: string, documentId: string): Promise<boolean> {
|
|
382
|
+
const result = await this.#request<{ __deleted: boolean } | null>(
|
|
383
|
+
"DELETE",
|
|
384
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/documents/${encodeURIComponent(documentId)}`,
|
|
385
|
+
"deleteDocument",
|
|
386
|
+
{ allow404: true },
|
|
387
|
+
);
|
|
388
|
+
return result !== null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* List mental models in a bank. Default `detail=content` includes the
|
|
393
|
+
* generated `content` text but excludes the heavyweight `reflect_response`
|
|
394
|
+
* provenance chain (which can exceed 200KB). Use `detail=metadata` for
|
|
395
|
+
* inventory and `detail=full` only for debug surfaces.
|
|
396
|
+
*/
|
|
397
|
+
async listMentalModels(bankId: string, options?: ListMentalModelsOptions): Promise<MentalModelListResponse> {
|
|
398
|
+
return this.#request<MentalModelListResponse>(
|
|
399
|
+
"GET",
|
|
400
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/mental-models`,
|
|
401
|
+
"listMentalModels",
|
|
402
|
+
{ query: { detail: options?.detail ?? "content" } },
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Fetch a single mental model. Returns `null` on 404. */
|
|
407
|
+
async getMentalModel(
|
|
408
|
+
bankId: string,
|
|
409
|
+
mentalModelId: string,
|
|
410
|
+
options?: GetMentalModelOptions,
|
|
411
|
+
): Promise<MentalModelSummary | null> {
|
|
412
|
+
return this.#request<MentalModelSummary | null>(
|
|
413
|
+
"GET",
|
|
414
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/mental-models/${encodeURIComponent(mentalModelId)}`,
|
|
415
|
+
"getMentalModel",
|
|
416
|
+
{ query: { detail: options?.detail ?? "content" }, allow404: true },
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Create a mental model. Asynchronous on the server: returns an
|
|
422
|
+
* `operation_id`; the model's `content` populates after the background
|
|
423
|
+
* reflect completes.
|
|
424
|
+
*/
|
|
425
|
+
async createMentalModel(
|
|
426
|
+
bankId: string,
|
|
427
|
+
name: string,
|
|
428
|
+
sourceQuery: string,
|
|
429
|
+
options?: CreateMentalModelOptions,
|
|
430
|
+
): Promise<CreateMentalModelResponse> {
|
|
431
|
+
return this.#request<CreateMentalModelResponse>(
|
|
432
|
+
"POST",
|
|
433
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/mental-models`,
|
|
434
|
+
"createMentalModel",
|
|
435
|
+
{
|
|
436
|
+
body: {
|
|
437
|
+
id: options?.id,
|
|
438
|
+
name,
|
|
439
|
+
source_query: sourceQuery,
|
|
440
|
+
tags: options?.tags,
|
|
441
|
+
max_tokens: options?.maxTokens,
|
|
442
|
+
trigger: options?.trigger,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Trigger an out-of-band refresh of a mental model. Returns the operation handle. */
|
|
449
|
+
async refreshMentalModel(bankId: string, mentalModelId: string): Promise<RefreshMentalModelResponse> {
|
|
450
|
+
return this.#request<RefreshMentalModelResponse>(
|
|
451
|
+
"POST",
|
|
452
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/mental-models/${encodeURIComponent(mentalModelId)}/refresh`,
|
|
453
|
+
"refreshMentalModel",
|
|
454
|
+
{},
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Delete a mental model. Returns `true` on success, `false` if it was already gone (404). */
|
|
459
|
+
async deleteMentalModel(bankId: string, mentalModelId: string): Promise<boolean> {
|
|
460
|
+
const result = await this.#request<{ __deleted: boolean } | null>(
|
|
461
|
+
"DELETE",
|
|
462
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/mental-models/${encodeURIComponent(mentalModelId)}`,
|
|
463
|
+
"deleteMentalModel",
|
|
464
|
+
{ allow404: true },
|
|
465
|
+
);
|
|
466
|
+
return result !== null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Fetch the change history of a mental model. Each entry captures the
|
|
471
|
+
* content snapshot BEFORE that change; the current content is read via
|
|
472
|
+
* `getMentalModel`. Most-recent first.
|
|
473
|
+
*/
|
|
474
|
+
async getMentalModelHistory(bankId: string, mentalModelId: string): Promise<MentalModelHistoryEntry[]> {
|
|
475
|
+
const response = await this.#request<MentalModelHistoryEntry[] | { items?: MentalModelHistoryEntry[] }>(
|
|
476
|
+
"GET",
|
|
477
|
+
`/v1/default/banks/${encodeURIComponent(bankId)}/mental-models/${encodeURIComponent(mentalModelId)}/history`,
|
|
478
|
+
"getMentalModelHistory",
|
|
479
|
+
{},
|
|
480
|
+
);
|
|
481
|
+
if (Array.isArray(response)) return response;
|
|
482
|
+
return response.items ?? [];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async #request<T>(method: string, path: string, operation: string, opts?: RequestOptions): Promise<T> {
|
|
486
|
+
let url = `${this.#baseUrl}${path}`;
|
|
487
|
+
if (opts?.query) {
|
|
488
|
+
const qs = buildQueryString(opts.query);
|
|
489
|
+
if (qs) url += `?${qs}`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const init: RequestInit = { method, headers: this.#headers };
|
|
493
|
+
if (opts?.body !== undefined) {
|
|
494
|
+
init.body = JSON.stringify(pruneUndefined(opts.body));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let response: Response;
|
|
498
|
+
try {
|
|
499
|
+
response = await fetch(url, init);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
throw new HindsightError(
|
|
502
|
+
`${operation} request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
503
|
+
undefined,
|
|
504
|
+
err,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (opts?.allow404 && response.status === 404) {
|
|
509
|
+
return null as T;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const text = await response.text();
|
|
513
|
+
const parsed = text ? safeJsonParse(text) : null;
|
|
514
|
+
|
|
515
|
+
if (!response.ok) {
|
|
516
|
+
const details =
|
|
517
|
+
(parsed && typeof parsed === "object"
|
|
518
|
+
? ((parsed as { detail?: unknown; message?: unknown }).detail ??
|
|
519
|
+
(parsed as { message?: unknown }).message)
|
|
520
|
+
: undefined) ??
|
|
521
|
+
parsed ??
|
|
522
|
+
text;
|
|
523
|
+
throw new HindsightError(
|
|
524
|
+
`${operation} failed: ${typeof details === "string" ? details : JSON.stringify(details)}`,
|
|
525
|
+
response.status,
|
|
526
|
+
details,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return (parsed ?? {}) as T;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
interface BuiltMemoryItem {
|
|
535
|
+
content: string;
|
|
536
|
+
timestamp?: string;
|
|
537
|
+
context?: string;
|
|
538
|
+
metadata?: Record<string, string>;
|
|
539
|
+
document_id?: string;
|
|
540
|
+
tags?: string[];
|
|
541
|
+
observation_scopes?: "per_tag" | "combined" | "all_combinations" | string[][];
|
|
542
|
+
strategy?: string;
|
|
543
|
+
update_mode?: UpdateMode;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function buildMemoryItem(item: MemoryItemInput): BuiltMemoryItem {
|
|
547
|
+
const out: BuiltMemoryItem = { content: item.content };
|
|
548
|
+
if (item.timestamp !== undefined) {
|
|
549
|
+
out.timestamp = item.timestamp instanceof Date ? item.timestamp.toISOString() : item.timestamp;
|
|
550
|
+
}
|
|
551
|
+
if (item.context !== undefined) out.context = item.context;
|
|
552
|
+
if (item.metadata !== undefined) out.metadata = item.metadata;
|
|
553
|
+
if (item.documentId !== undefined) out.document_id = item.documentId;
|
|
554
|
+
if (item.tags !== undefined) out.tags = item.tags;
|
|
555
|
+
if (item.observationScopes !== undefined) out.observation_scopes = item.observationScopes;
|
|
556
|
+
if (item.strategy !== undefined) out.strategy = item.strategy;
|
|
557
|
+
if (item.updateMode !== undefined) out.update_mode = item.updateMode;
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function buildQueryString(query: Record<string, unknown>): string {
|
|
562
|
+
const params = new URLSearchParams();
|
|
563
|
+
for (const [key, value] of Object.entries(query)) {
|
|
564
|
+
if (value === undefined || value === null) continue;
|
|
565
|
+
if (Array.isArray(value)) {
|
|
566
|
+
for (const item of value) {
|
|
567
|
+
if (item === undefined || item === null) continue;
|
|
568
|
+
params.append(key, String(item));
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
params.set(key, String(value));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return params.toString();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function pruneUndefined(obj: Record<string, unknown>): Record<string, unknown> {
|
|
578
|
+
const out: Record<string, unknown> = {};
|
|
579
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
580
|
+
if (v !== undefined) out[k] = v;
|
|
581
|
+
}
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function safeJsonParse(text: string): unknown {
|
|
586
|
+
try {
|
|
587
|
+
return JSON.parse(text);
|
|
588
|
+
} catch {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
export function createHindsightClient(config: HindsightConfig & { hindsightApiUrl: string }): HindsightApi {
|
|
593
|
+
return new HindsightApi({
|
|
594
|
+
baseUrl: config.hindsightApiUrl,
|
|
595
|
+
apiKey: config.hindsightApiToken ?? undefined,
|
|
596
|
+
userAgent: USER_AGENT,
|
|
597
|
+
});
|
|
598
|
+
}
|