diff --git a/README.md b/README.md index 648cdd3..d140509 100644 --- a/README.md +++ b/README.md @@ -113,10 +113,13 @@ const tf = new ThinkFleetMemory({ | `listPendingReview(params?)` | `GET /projects/:id/admin/memory/review` | | `stats()` | `GET /projects/:id/admin/memory/stats` | | `create(body)` | `POST /projects/:id/admin/memory` | +| `createProcedure(body)` | `POST /projects/:id/admin/memory` (type=procedure)| | `update(memId, body)` | `PATCH /projects/:id/admin/memory/:memId` | | `confirm(memId, body)` | `POST /projects/:id/admin/memory/:memId/confirm`| | `promote(memId, body)` | `POST /projects/:id/admin/memory/:memId/promote`| | `search(body)` | `POST /projects/:id/admin/memory/search` | +| `getPrecedence()` | `GET /projects/:id/admin/memory/precedence` | +| `setPrecedence(policy)` | `PUT /projects/:id/admin/memory/precedence` | | `delete(memId)` | `DELETE /projects/:id/admin/memory/:memId` | | `listFeedback(memId)` | `GET /projects/:id/admin/memory/:memId/feedback`| diff --git a/package-lock.json b/package-lock.json index fb13c23..fde0fcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@thinkfleet/memory-sdk", - "version": "0.3.0", + "version": "0.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@thinkfleet/memory-sdk", - "version": "0.3.0", + "version": "0.7.1", "license": "MIT", "devDependencies": { "@anthropic-ai/sdk": "^0.109.1", diff --git a/package.json b/package.json index 9700913..c4c07e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@thinkfleet/memory-sdk", - "version": "0.6.0", + "version": "0.8.0", "description": "TypeScript SDK for app.memmesh.ai — admin + project memory CRUD, semantic search, feedback, and Lattice behavioral patterns", "type": "module", "main": "./dist/index.cjs", diff --git a/src/core/http-client.ts b/src/core/http-client.ts index ea69500..5a88c1f 100644 --- a/src/core/http-client.ts +++ b/src/core/http-client.ts @@ -52,6 +52,19 @@ export class HttpClient { ) } + async put(path: string, body?: unknown, requestOptions?: RequestOptions): Promise { + const url = this.buildUrl(path, undefined, requestOptions) + return this.request( + url, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: body != null ? JSON.stringify(body) : undefined, + }, + requestOptions, + ) + } + async delete(path: string, requestOptions?: RequestOptions): Promise { const url = this.buildUrl(path, undefined, requestOptions) return this.request(url, { method: 'DELETE' }, requestOptions) diff --git a/src/core/procedural.ts b/src/core/procedural.ts new file mode 100644 index 0000000..7a4b4cf --- /dev/null +++ b/src/core/procedural.ts @@ -0,0 +1,43 @@ +import type { ProceduralMemoryMetadata } from '../types/memory.js' + +/** + * Render a procedure into the injectable `content` string. Kept identical to + * the engine-side renderer so author-time content matches what the server + * would produce. Pure and deterministic. + */ +export function renderProcedureContent(meta: ProceduralMemoryMetadata): string { + const lines: string[] = [`Goal: ${meta.goal.trim()}`] + + if (meta.whenToUse && meta.whenToUse.trim()) { + lines.push(`When: ${meta.whenToUse.trim()}`) + } + + lines.push('Steps:') + meta.steps.forEach((step, i) => { + const pitfall = + step.pitfall && step.pitfall.trim() + ? ` (watch out: ${step.pitfall.trim()})` + : '' + lines.push(`${i + 1}. ${step.text.trim()}${pitfall}`) + }) + + const failures = (meta.failureModes ?? []).filter((f) => f.trim()) + if (failures.length > 0) { + lines.push('Avoid:') + failures.forEach((f) => lines.push(`- ${f.trim()}`)) + } + + return lines.join('\n') +} + +/** The out-of-the-box precedence ladder: human-verified > local > licensed-brain > base. */ +export const DEFAULT_PRECEDENCE_POLICY = { + defaultOrder: [ + 'human_verified', + 'local', + 'licensed_brain', + 'base', + ] as const, + scopeNearestWins: true, + overrides: [] as const, +} diff --git a/src/index.ts b/src/index.ts index d22d1f2..8ed28c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -191,6 +191,12 @@ export type { ReflectResult, Insight, PrefetchRelatedRequest, + ReviewQueueItem, + ProcedureStep, + ProceduralMemoryMetadata, + CreateProcedureRequest, + MemoryPrecedenceOverride, + MemoryPrecedencePolicy, } from './types/memory.js' export { @@ -199,8 +205,16 @@ export { MemoryStatus, MemoryImpact, MemoryFeedbackRating, + MemoryReviewReason, + MemoryProvenanceTier, } from './types/memory.js' +// Procedural memory helpers +export { + renderProcedureContent, + DEFAULT_PRECEDENCE_POLICY, +} from './core/procedural.js' + // Types — lattice export type { BehaviorPatternKind, diff --git a/src/resources/memory.ts b/src/resources/memory.ts index cc42853..7ff8f6b 100644 --- a/src/resources/memory.ts +++ b/src/resources/memory.ts @@ -1,10 +1,14 @@ import { ConsentResource } from './consent.js' import type { HttpClient } from '../core/http-client.js' +import { renderProcedureContent } from '../core/procedural.js' import type { RequestOptions } from '../core/types.js' import { MemoryItemType, MemoryScope, type ConfirmMemoryRequest, + type CreateProcedureRequest, + type MemoryPrecedencePolicy, + type ReviewQueueItem, type CreateMemoryRequest, type ListMemoryParams, type MemoryFeedback, @@ -456,14 +460,16 @@ export class AdminMemoryResource { } /** - * List memories pending review — either freshly extracted (status=pending) - * or auto-flagged by negative feedback (negativeRatingCount >= 3). + * List the adjudication queue — everything the system is unsure about. Each + * row carries a `reviewReason`: `pending` (awaiting confirmation), `flagged` + * (>= 3 negative), `low_confidence` (weak auto-extraction), or `stale` + * (old and long-unused). */ async listPendingReview( params?: { limit?: number; offset?: number }, options?: RequestOptions, - ): Promise { - return this.http.get( + ): Promise { + return this.http.get( '/admin/memory/review', params as Record, options, @@ -486,6 +492,59 @@ export class AdminMemoryResource { return this.http.post('/admin/memory', body, options) } + /** + * Author a procedure — "how this job is done here" (goal + steps + failure + * modes). Stored as a PROCEDURE memory: the structured shape goes on + * `metadata` and the rendered how-to text on `content`, so retrieval injects + * it as an explicit exemplar. A cheaper model reasons better on-domain when + * handed the procedure instead of a flattened fact. + */ + async createProcedure( + body: CreateProcedureRequest, + options?: RequestOptions, + ): Promise { + const { category, scope, importance, ...metadata } = body + return this.create( + { + type: MemoryItemType.PROCEDURE, + content: renderProcedureContent(metadata), + category, + scope: scope ?? MemoryScope.PROJECT, + importance: importance ?? 7, + metadata: metadata as unknown as Record, + }, + options, + ) + } + + /** + * Get the project's memory precedence policy — which memory wins when two + * disagree. Falls back to the default ladder (human-verified > local > + * licensed-brain > base) when unset. + */ + async getPrecedence(options?: RequestOptions): Promise { + return this.http.get( + '/admin/memory/precedence', + undefined, + options, + ) + } + + /** + * Save the project's memory precedence policy. Requires the Memory Steward + * role (MANAGE_MEMORY_POLICY). + */ + async setPrecedence( + policy: MemoryPrecedencePolicy, + options?: RequestOptions, + ): Promise { + return this.http.put( + '/admin/memory/precedence', + policy, + options, + ) + } + /** * Update any memory item. */ diff --git a/src/types/memory.ts b/src/types/memory.ts index 71e327b..5ea6e17 100644 --- a/src/types/memory.ts +++ b/src/types/memory.ts @@ -9,6 +9,14 @@ export enum MemoryItemType { RULE = 'rule', CORRECTION = 'correction', SUMMARY = 'summary', + /** + * A reusable procedure — how a job/task is done here (goal + steps + + * failure modes). Injected as a how-to exemplar, not a fact, so a cheaper + * model reasons better on-domain. Structured shape on `metadata` + * (ProceduralMemoryMetadata); `content` holds the rendered, injectable text + * (see renderProcedureContent). Author with `admin.createProcedure()`. + */ + PROCEDURE = 'procedure', /** Behavioral pattern emitted by Lattice mining. */ BEHAVIOR_PATTERN = 'behavior_pattern', /** Subject-level consent / opt-out record. See ConsentResource. */ @@ -337,3 +345,82 @@ export interface PrefetchRelatedRequest { /** Max related memories to return. Default 10, clamped [1, 100]. */ limit?: number } + +// ── Adjudication queue ─────────────────────────────────────────────── + +/** Why a memory is in the review queue. The queue reports the highest-priority reason. */ +export enum MemoryReviewReason { + /** Awaiting first confirmation (status = pending). */ + PENDING = 'pending', + /** Flagged by repeated negative feedback (>= 3). */ + FLAGGED = 'flagged', + /** Auto-extracted, never human-confirmed, with low confidence. */ + LOW_CONFIDENCE = 'low_confidence', + /** Old and long-unrecalled — a decay/forget candidate. */ + STALE = 'stale', +} + +/** A review-queue row: a memory plus why it needs a steward's attention. */ +export interface ReviewQueueItem extends MemoryItem { + reviewReason: MemoryReviewReason +} + +// ── Procedural memory ──────────────────────────────────────────────── + +/** One step in a procedure. `pitfall` is an optional inline warning. */ +export interface ProcedureStep { + text: string + pitfall?: string +} + +/** Structured shape stored on a PROCEDURE memory's `metadata`. */ +export interface ProceduralMemoryMetadata { + /** What the procedure accomplishes. */ + goal: string + /** When to reach for it — the trigger condition, in words. */ + whenToUse?: string + /** Ordered steps. */ + steps: ProcedureStep[] + /** Known failure modes / anti-patterns — what NOT to do and why. */ + failureModes?: string[] +} + +/** Input for `admin.createProcedure()`. */ +export interface CreateProcedureRequest extends ProceduralMemoryMetadata { + /** Optional category tag — used as the heading when injected. */ + category?: string + /** Override the default PROJECT scope. */ + scope?: MemoryScope + /** 0-10 importance, defaults to 7. */ + importance?: number +} + +// ── Memory precedence policy ───────────────────────────────────────── + +/** Origin tier of a memory, used to resolve conflicts. Default order strongest→weakest. */ +export enum MemoryProvenanceTier { + /** Confirmed by a human steward — most trusted. */ + HUMAN_VERIFIED = 'human_verified', + /** Learned/extracted inside this organization. */ + LOCAL = 'local', + /** Ingested from a purchased/licensed brain. */ + LICENSED_BRAIN = 'licensed_brain', + /** Seed / base-model brain baseline. */ + BASE = 'base', +} + +/** Category-level override: "for this category, this tier wins." */ +export interface MemoryPrecedenceOverride { + category: string + winningTier: MemoryProvenanceTier +} + +/** Which memory wins when two disagree. See `admin.getPrecedence()` / `setPrecedence()`. */ +export interface MemoryPrecedencePolicy { + /** Tier ranking, strongest first. Tiers omitted rank last. */ + defaultOrder: MemoryProvenanceTier[] + /** When true, a nearer (more specific) scope breaks ties within a tier. */ + scopeNearestWins: boolean + /** Category-level exceptions applied before the default order. */ + overrides: MemoryPrecedenceOverride[] +}