@purista/harness 1.0.0 → 1.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/README.md +15 -0
- package/dist/agents/index.d.ts +5 -3
- package/dist/agents/index.js +84 -8
- package/dist/errors/catalog.d.ts +45 -5
- package/dist/errors/catalog.js +19 -0
- package/dist/errors/harness-error.d.ts +2 -0
- package/dist/eval/index.d.ts +57 -0
- package/dist/eval/index.js +181 -0
- package/dist/harness/defineHarness.d.ts +96 -20
- package/dist/harness/defineHarness.js +59 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/memory/sandbox/index.d.ts +17 -0
- package/dist/memory/sandbox/index.js +122 -0
- package/dist/models/registry.js +32 -7
- package/dist/ports/capabilities.d.ts +46 -2
- package/dist/ports/harness-context.d.ts +4 -1
- package/dist/ports/index.d.ts +2 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/memory/facade.d.ts +5 -0
- package/dist/ports/memory/facade.js +123 -0
- package/dist/ports/memory/telemetry.d.ts +16 -0
- package/dist/ports/memory/telemetry.js +77 -0
- package/dist/ports/memory/types.d.ts +204 -0
- package/dist/ports/memory/types.js +1 -0
- package/dist/ports/memory/validation.d.ts +19 -0
- package/dist/ports/memory/validation.js +160 -0
- package/dist/ports/memory.d.ts +3 -0
- package/dist/ports/memory.js +3 -0
- package/dist/ports/workspace.d.ts +177 -0
- package/dist/ports/workspace.js +32 -0
- package/dist/runtime/durable.d.ts +3 -0
- package/dist/runtime/durable.js +2 -1
- package/dist/sessions/index.d.ts +2 -0
- package/dist/sessions/index.js +275 -68
- package/dist/skills/index.d.ts +2 -1
- package/dist/skills/index.js +263 -35
- package/dist/telemetry/shim.d.ts +20 -0
- package/dist/telemetry/shim.js +28 -0
- package/dist/testing/durableWorkspaceStoreContract.d.ts +3 -0
- package/dist/testing/durableWorkspaceStoreContract.js +41 -0
- package/dist/testing/fakeMemoryAdapter.d.ts +16 -0
- package/dist/testing/fakeMemoryAdapter.js +110 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +4 -0
- package/dist/workspace/in-memory.d.ts +35 -0
- package/dist/workspace/in-memory.js +142 -0
- package/dist/workspace/index.d.ts +1 -0
- package/dist/workspace/index.js +1 -0
- package/package.json +12 -6
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { ModelCapabilityError, ValidationError } from '../../errors/index.js';
|
|
2
|
+
import { createMetrics } from '../../telemetry/index.js';
|
|
3
|
+
import { attachContent, baseAttrs, errorType, hashKey, normalizeMemoryError, resultAttributes, shouldCaptureContent } from './telemetry.js';
|
|
4
|
+
import { assertCapability, normalizeListOptions, normalizeSearchQuery, validateOperationInput } from './validation.js';
|
|
5
|
+
/** Creates scoped memory helpers for a concrete session/run context. */
|
|
6
|
+
export function createMemoryFacade(opts) {
|
|
7
|
+
const base = (scope) => createSessionMemory(opts, normalizeScope(opts, scope));
|
|
8
|
+
return {
|
|
9
|
+
session: base({ kind: 'session', sessionId: opts.sessionId }),
|
|
10
|
+
run: base({ kind: 'run', sessionId: opts.sessionId, runId: requireRunId(opts) }),
|
|
11
|
+
...(opts.agentId ? { agent: base(definedScope({ kind: 'agent', sessionId: opts.sessionId, runId: opts.runId, agentId: opts.agentId, workflowId: opts.workflowId })) } : {}),
|
|
12
|
+
user(userId) {
|
|
13
|
+
return base(definedScope({ kind: 'user', sessionId: opts.sessionId, runId: opts.runId, agentId: opts.agentId, workflowId: opts.workflowId, userId: userId ?? metadataString(opts.metadata, 'userId') }));
|
|
14
|
+
},
|
|
15
|
+
tenant(tenantId) {
|
|
16
|
+
return base(definedScope({ kind: 'tenant', sessionId: opts.sessionId, runId: opts.runId, agentId: opts.agentId, workflowId: opts.workflowId, tenantId: tenantId ?? metadataString(opts.metadata, 'tenantId') }));
|
|
17
|
+
},
|
|
18
|
+
scope(scope) {
|
|
19
|
+
return base(scope);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Creates a key/value memory facade bound to one normalized scope. */
|
|
24
|
+
export function createSessionMemory(opts, scope) {
|
|
25
|
+
const normalized = normalizeScope(opts, scope);
|
|
26
|
+
return {
|
|
27
|
+
read: (key) => runMemoryOperation(opts, normalized, 'get', key, undefined, async (store, ctx) => store.get(key, ctx)),
|
|
28
|
+
write: (key, value, writeOpts) => runMemoryOperation(opts, normalized, 'set', key, { value, ...(writeOpts ? { writeOpts } : {}) }, async (store, ctx) => store.set(key, value, { ...ctx, ...(writeOpts ? { opts: writeOpts } : {}) })),
|
|
29
|
+
delete: (key) => runMemoryOperation(opts, normalized, 'delete', key, undefined, async (store, ctx) => store.delete(key, ctx)),
|
|
30
|
+
list: async (listOpts) => {
|
|
31
|
+
const entries = await runMemoryOperation(opts, normalized, 'list', undefined, listOpts ? { listOpts } : undefined, async (store, ctx) => store.list({ ...ctx, opts: normalizeListOptions(listOpts) }));
|
|
32
|
+
return entries.map((entry) => entry.key).sort();
|
|
33
|
+
},
|
|
34
|
+
search: (query) => runMemoryOperation(opts, normalized, 'search', undefined, { query }, async (store, ctx) => {
|
|
35
|
+
assertCapability(opts.adapter, 'memory.search', 'search');
|
|
36
|
+
if (!store.search) {
|
|
37
|
+
throw new ModelCapabilityError('Memory search is not available for this adapter.', { alias: opts.adapter.info.id, method: 'memory.search', reason: 'method_missing' });
|
|
38
|
+
}
|
|
39
|
+
return store.search(normalizeSearchQuery(query), ctx);
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async function runMemoryOperation(opts, scope, operation, key, content, fn) {
|
|
44
|
+
validateOperationInput(opts.adapter, scope, operation, key, content);
|
|
45
|
+
const metrics = createMetrics(opts.telemetry, baseAttrs(opts, scope, operation));
|
|
46
|
+
const started = Date.now();
|
|
47
|
+
let durationAttrs = {};
|
|
48
|
+
const attrs = {
|
|
49
|
+
...baseAttrs(opts, scope, operation),
|
|
50
|
+
...(key ? { 'harness.memory.key_hash': hashKey(key) } : {}),
|
|
51
|
+
'harness.memory.content_captured': shouldCaptureContent(opts.contentCaptureMode)
|
|
52
|
+
};
|
|
53
|
+
const context = {
|
|
54
|
+
logger: opts.logger,
|
|
55
|
+
telemetry: opts.telemetry,
|
|
56
|
+
metrics,
|
|
57
|
+
contentCaptureMode: opts.contentCaptureMode,
|
|
58
|
+
signal: opts.signal,
|
|
59
|
+
...(opts.sandbox ? { sandbox: opts.sandbox } : {})
|
|
60
|
+
};
|
|
61
|
+
const execute = async (span) => {
|
|
62
|
+
if (shouldCaptureContent(opts.contentCaptureMode))
|
|
63
|
+
attachContent(span, opts.contentCaptureMode, operation, key, content);
|
|
64
|
+
try {
|
|
65
|
+
opts.signal.throwIfAborted();
|
|
66
|
+
const store = await opts.adapter.open(scope, context);
|
|
67
|
+
const result = await fn(store, { ...context, scope, operation });
|
|
68
|
+
const resultAttrs = resultAttributes(operation, result);
|
|
69
|
+
durationAttrs = resultAttrs;
|
|
70
|
+
span.setAttributes(resultAttrs);
|
|
71
|
+
metrics.counter('harness.memory.operations', 1, resultAttrs);
|
|
72
|
+
if (operation === 'search' && Array.isArray(result)) {
|
|
73
|
+
metrics.histogram('harness.memory.search.results', result.length);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const normalized = normalizeMemoryError(opts.adapter, operation, error);
|
|
79
|
+
const errorAttrs = { 'error.type': errorType(normalized) };
|
|
80
|
+
durationAttrs = errorAttrs;
|
|
81
|
+
metrics.counter('harness.memory.operations', 1, errorAttrs);
|
|
82
|
+
throw normalized;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
metrics.histogram('harness.memory.operation.duration', (Date.now() - started) / 1000, durationAttrs);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return opts.telemetry.span(`harness.memory.${operation}`, attrs, execute);
|
|
89
|
+
}
|
|
90
|
+
function normalizeScope(opts, scope) {
|
|
91
|
+
return {
|
|
92
|
+
sessionId: opts.sessionId,
|
|
93
|
+
...(opts.runId ? { runId: opts.runId } : {}),
|
|
94
|
+
...(opts.workflowId ? { workflowId: opts.workflowId } : {}),
|
|
95
|
+
...(opts.agentId ? { agentId: opts.agentId } : {}),
|
|
96
|
+
...scope
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function definedScope(scope) {
|
|
100
|
+
const out = { kind: scope.kind };
|
|
101
|
+
if (scope.sessionId !== undefined)
|
|
102
|
+
out.sessionId = scope.sessionId;
|
|
103
|
+
if (scope.runId !== undefined)
|
|
104
|
+
out.runId = scope.runId;
|
|
105
|
+
if (scope.agentId !== undefined)
|
|
106
|
+
out.agentId = scope.agentId;
|
|
107
|
+
if (scope.workflowId !== undefined)
|
|
108
|
+
out.workflowId = scope.workflowId;
|
|
109
|
+
if (scope.userId !== undefined)
|
|
110
|
+
out.userId = scope.userId;
|
|
111
|
+
if (scope.tenantId !== undefined)
|
|
112
|
+
out.tenantId = scope.tenantId;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function requireRunId(opts) {
|
|
116
|
+
if (opts.runId)
|
|
117
|
+
return opts.runId;
|
|
118
|
+
throw new ValidationError('Run memory is only available inside a run.', { where: 'memory_scope', issues: { reason: 'missing_scope_identifier', field: 'runId', scope: 'run' } });
|
|
119
|
+
}
|
|
120
|
+
function metadataString(metadata, key) {
|
|
121
|
+
const value = metadata?.[key];
|
|
122
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
123
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Span } from '@opentelemetry/api';
|
|
2
|
+
import type { JsonValue } from '../../models/json.js';
|
|
3
|
+
import type { SpanAttrs } from '../../telemetry/index.js';
|
|
4
|
+
import type { CreateMemoryFacadeOptions, MemoryOperation, MemoryScope, MemorySearchQuery, MemoryWriteOptions } from './types.js';
|
|
5
|
+
export declare const CONTENT_ATTR_LIMIT = 8192;
|
|
6
|
+
export declare function baseAttrs(opts: CreateMemoryFacadeOptions, scope: MemoryScope, operation: MemoryOperation): SpanAttrs;
|
|
7
|
+
export declare function resultAttributes(operation: MemoryOperation, result: unknown): SpanAttrs;
|
|
8
|
+
export declare function hashKey(key: string): string;
|
|
9
|
+
export declare function shouldCaptureContent(mode: CreateMemoryFacadeOptions['contentCaptureMode']): boolean;
|
|
10
|
+
export declare function attachContent(span: Span, mode: CreateMemoryFacadeOptions['contentCaptureMode'], operation: MemoryOperation, key: string | undefined, content: {
|
|
11
|
+
value?: JsonValue;
|
|
12
|
+
writeOpts?: MemoryWriteOptions;
|
|
13
|
+
query?: MemorySearchQuery;
|
|
14
|
+
} | undefined): void;
|
|
15
|
+
export declare function normalizeMemoryError(adapter: CreateMemoryFacadeOptions['adapter'], operation: MemoryOperation, error: unknown): unknown;
|
|
16
|
+
export declare function errorType(error: unknown): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { OperationCancelledError, StateError } from '../../errors/index.js';
|
|
3
|
+
import { scopeCapability } from './validation.js';
|
|
4
|
+
export const CONTENT_ATTR_LIMIT = 8192;
|
|
5
|
+
export function baseAttrs(opts, scope, operation) {
|
|
6
|
+
return {
|
|
7
|
+
'harness.name': opts.harnessName,
|
|
8
|
+
'harness.memory.provider': opts.adapter.info.id,
|
|
9
|
+
'harness.memory.operation': operation,
|
|
10
|
+
'harness.memory.scope': scope.kind,
|
|
11
|
+
'harness.memory.capability': operation === 'search' ? 'memory.search' : scopeCapability[scope.kind],
|
|
12
|
+
'harness.session.id': scope.sessionId,
|
|
13
|
+
'harness.run.id': scope.runId,
|
|
14
|
+
'harness.agent.id': scope.agentId,
|
|
15
|
+
'harness.workflow.id': scope.workflowId
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function resultAttributes(operation, result) {
|
|
19
|
+
if (operation === 'get')
|
|
20
|
+
return { 'harness.memory.hit': result !== undefined };
|
|
21
|
+
if (Array.isArray(result) && (operation === 'list' || operation === 'search'))
|
|
22
|
+
return { 'harness.memory.result_count': result.length };
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
export function hashKey(key) {
|
|
26
|
+
return createHash('sha256').update(key).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
export function shouldCaptureContent(mode) {
|
|
29
|
+
return mode !== 'NO_CONTENT';
|
|
30
|
+
}
|
|
31
|
+
export function attachContent(span, mode, operation, key, content) {
|
|
32
|
+
const attrs = {
|
|
33
|
+
...(key ? { 'harness.memory.key': key } : {}),
|
|
34
|
+
...(content?.value !== undefined ? { 'harness.memory.value': limitedJson(content.value) } : {}),
|
|
35
|
+
...(content?.query ? { 'harness.memory.query': content.query.text } : {})
|
|
36
|
+
};
|
|
37
|
+
if (mode === 'SPAN_ONLY' || mode === 'SPAN_AND_EVENT') {
|
|
38
|
+
span.setAttributes(attrs);
|
|
39
|
+
}
|
|
40
|
+
if (mode === 'EVENT_ONLY' || mode === 'SPAN_AND_EVENT') {
|
|
41
|
+
span.addEvent('harness.memory.content', cleanAttrs({ ...attrs, 'harness.memory.operation': operation }));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function normalizeMemoryError(adapter, operation, error) {
|
|
45
|
+
if (error instanceof OperationCancelledError)
|
|
46
|
+
return error;
|
|
47
|
+
if (isAbortError(error))
|
|
48
|
+
return new OperationCancelledError('Memory operation was cancelled.', { scope: 'memory' }, error);
|
|
49
|
+
if (isHarnessError(error))
|
|
50
|
+
return error;
|
|
51
|
+
return new StateError('Memory adapter operation failed.', { op: `memory.${operation}`, adapter: 'memory', memory_provider: adapter.info.id }, error);
|
|
52
|
+
}
|
|
53
|
+
export function errorType(error) {
|
|
54
|
+
return isHarnessError(error) ? error.code : error instanceof Error ? error.name : 'Error';
|
|
55
|
+
}
|
|
56
|
+
function limitedJson(value) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(value).slice(0, CONTENT_ATTR_LIMIT);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function isAbortError(error) {
|
|
65
|
+
return error instanceof Error && error.name === 'AbortError';
|
|
66
|
+
}
|
|
67
|
+
function isHarnessError(error) {
|
|
68
|
+
return Boolean(error && typeof error === 'object' && 'code' in error && 'category' in error);
|
|
69
|
+
}
|
|
70
|
+
function cleanAttrs(attrs) {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
73
|
+
if (value !== undefined)
|
|
74
|
+
out[key] = value;
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { Logger } from '../../logger/index.js';
|
|
2
|
+
import type { ContentCaptureMode } from '../../harness/defineHarness.js';
|
|
3
|
+
import type { JsonValue } from '../../models/json.js';
|
|
4
|
+
import type { SandboxSession } from '../../sandbox/index.js';
|
|
5
|
+
import type { Metrics, TelemetryShim } from '../../telemetry/index.js';
|
|
6
|
+
import type { AdapterCapability, AdapterCapabilities } from '../capabilities.js';
|
|
7
|
+
import type { HarnessContextConfigurable } from '../harness-context.js';
|
|
8
|
+
/** Logical memory scope kinds supported by the harness facade. */
|
|
9
|
+
export type MemoryScopeKind = 'run' | 'session' | 'agent' | 'user' | 'tenant';
|
|
10
|
+
/**
|
|
11
|
+
* Concrete scope used to namespace memory entries.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* await ctx.memory.scope({ kind: 'user', userId: 'u_123' }).write('locale', 'de-DE')
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export interface MemoryScope {
|
|
19
|
+
/** Scope discriminator. */
|
|
20
|
+
kind: MemoryScopeKind;
|
|
21
|
+
/** Harness session id for session-local and run-local memory. */
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
/** Harness run id for run-local memory. */
|
|
24
|
+
runId?: string;
|
|
25
|
+
/** Agent id for agent-scoped memory. */
|
|
26
|
+
agentId?: string;
|
|
27
|
+
/** Workflow id attached for observability when memory is used inside a workflow. */
|
|
28
|
+
workflowId?: string;
|
|
29
|
+
/** Application-owned user id for user-scoped memory. */
|
|
30
|
+
userId?: string;
|
|
31
|
+
/** Application-owned tenant id for tenant-scoped memory. */
|
|
32
|
+
tenantId?: string;
|
|
33
|
+
}
|
|
34
|
+
/** Adapter capabilities are normal harness adapter capabilities with memory-specific names. */
|
|
35
|
+
export type MemoryCapability = Extract<AdapterCapability, `memory.${string}`>;
|
|
36
|
+
/** Data-only adapter descriptor used for validation, inspection, and `.requires(...)`. */
|
|
37
|
+
export interface MemoryAdapterInfo {
|
|
38
|
+
/** Stable adapter id, for example `sandbox_memory` or `redis_memory`. */
|
|
39
|
+
id: string;
|
|
40
|
+
/** Package that provides the adapter factory. */
|
|
41
|
+
packageName: string;
|
|
42
|
+
/** Optional adapter package version for diagnostics. */
|
|
43
|
+
version?: string;
|
|
44
|
+
/** Memory capabilities implemented by this adapter. */
|
|
45
|
+
capabilities: readonly MemoryCapability[];
|
|
46
|
+
}
|
|
47
|
+
/** Per-open context passed to memory adapters. Core owns the standard telemetry wrapper. */
|
|
48
|
+
export interface MemoryOpenContext {
|
|
49
|
+
/** Harness logger inherited by the adapter. */
|
|
50
|
+
readonly logger: Logger;
|
|
51
|
+
/** Harness telemetry shim for adapter-specific nested spans. */
|
|
52
|
+
readonly telemetry: TelemetryShim;
|
|
53
|
+
/** Harness metrics helper for adapter-specific metrics. */
|
|
54
|
+
readonly metrics: Metrics;
|
|
55
|
+
/** Content capture policy; adapters must respect this for custom telemetry/logs. */
|
|
56
|
+
readonly contentCaptureMode: ContentCaptureMode;
|
|
57
|
+
/** Cancellation signal for the memory operation. */
|
|
58
|
+
readonly signal: AbortSignal;
|
|
59
|
+
/** Present for the core `sandboxMemory()` adapter and adapters that intentionally compose with the sandbox. */
|
|
60
|
+
readonly sandbox?: SandboxSession;
|
|
61
|
+
}
|
|
62
|
+
/** Operation names emitted as memory span/metric attributes. */
|
|
63
|
+
export type MemoryOperation = 'get' | 'set' | 'delete' | 'list' | 'search';
|
|
64
|
+
/** Per-operation context passed to memory stores. */
|
|
65
|
+
export interface MemoryOperationContext extends MemoryOpenContext {
|
|
66
|
+
/** Concrete scope for this operation. */
|
|
67
|
+
readonly scope: MemoryScope;
|
|
68
|
+
/** Operation currently being executed. */
|
|
69
|
+
readonly operation: MemoryOperation;
|
|
70
|
+
}
|
|
71
|
+
/** Optional write behavior for adapters that support tags, metadata, or TTL. */
|
|
72
|
+
export interface MemoryWriteOptions {
|
|
73
|
+
/** Positive expiry duration in milliseconds. Requires `memory.ttl`. */
|
|
74
|
+
ttlMs?: number;
|
|
75
|
+
/** Optional index/filter tags. */
|
|
76
|
+
tags?: readonly string[];
|
|
77
|
+
/** Optional JSON metadata stored beside the value. */
|
|
78
|
+
metadata?: Record<string, JsonValue>;
|
|
79
|
+
}
|
|
80
|
+
/** Listing options supported by the facade. */
|
|
81
|
+
export interface MemoryListOptions {
|
|
82
|
+
/** Return keys with this prefix only. */
|
|
83
|
+
prefix?: string;
|
|
84
|
+
/** Maximum entries to return. Defaults to `100`, maximum `1000`. */
|
|
85
|
+
limit?: number;
|
|
86
|
+
/** Adapter-defined cursor; the built-in adapter treats this as the last key. */
|
|
87
|
+
cursor?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Metadata-only memory entry returned from `list`. */
|
|
90
|
+
export interface MemoryEntry {
|
|
91
|
+
/** Memory key without backend-specific path or extension. */
|
|
92
|
+
key: string;
|
|
93
|
+
/** ISO creation timestamp when the adapter tracks it. */
|
|
94
|
+
createdAt?: string;
|
|
95
|
+
/** ISO update timestamp when the adapter tracks it. */
|
|
96
|
+
updatedAt?: string;
|
|
97
|
+
/** ISO expiry timestamp when the adapter tracks TTL. */
|
|
98
|
+
expiresAt?: string;
|
|
99
|
+
/** Optional index/filter tags. */
|
|
100
|
+
tags?: readonly string[];
|
|
101
|
+
/** Optional JSON metadata. */
|
|
102
|
+
metadata?: Record<string, JsonValue>;
|
|
103
|
+
}
|
|
104
|
+
/** Text search query used by adapters with `memory.search`. */
|
|
105
|
+
export interface MemorySearchQuery {
|
|
106
|
+
/** Search text. Empty strings are rejected before adapter I/O. */
|
|
107
|
+
text: string;
|
|
108
|
+
/** Maximum results. Defaults to `100`, maximum `1000`. */
|
|
109
|
+
limit?: number;
|
|
110
|
+
/** Optional tag filter. */
|
|
111
|
+
tags?: readonly string[];
|
|
112
|
+
/** Optional metadata filter. */
|
|
113
|
+
metadata?: Record<string, JsonValue>;
|
|
114
|
+
}
|
|
115
|
+
/** Search result returned by adapters with `memory.search`. */
|
|
116
|
+
export interface MemorySearchResult {
|
|
117
|
+
/** Memory key for the matched entry. */
|
|
118
|
+
key: string;
|
|
119
|
+
/** Adapter-defined relevance score. */
|
|
120
|
+
score?: number;
|
|
121
|
+
/** Optional value payload. Treat as content for telemetry/logging. */
|
|
122
|
+
value?: JsonValue;
|
|
123
|
+
/** Optional JSON metadata. */
|
|
124
|
+
metadata?: Record<string, JsonValue>;
|
|
125
|
+
}
|
|
126
|
+
/** Backend store opened for one concrete memory scope. */
|
|
127
|
+
export interface MemoryStore {
|
|
128
|
+
/** Reads a JSON value by key. */
|
|
129
|
+
get<T = JsonValue>(key: string, ctx: MemoryOperationContext): Promise<T | undefined>;
|
|
130
|
+
/** Writes a JSON value by key. */
|
|
131
|
+
set(key: string, value: JsonValue, ctx: MemoryOperationContext & {
|
|
132
|
+
opts?: MemoryWriteOptions;
|
|
133
|
+
}): Promise<void>;
|
|
134
|
+
/** Deletes a key if it exists. */
|
|
135
|
+
delete(key: string, ctx: MemoryOperationContext): Promise<void>;
|
|
136
|
+
/** Lists memory entries in the opened scope. */
|
|
137
|
+
list(ctx: MemoryOperationContext & {
|
|
138
|
+
opts?: MemoryListOptions;
|
|
139
|
+
}): Promise<MemoryEntry[]>;
|
|
140
|
+
/** Searches memory when the adapter advertises `memory.search`. */
|
|
141
|
+
search?(query: MemorySearchQuery, ctx: MemoryOperationContext): Promise<MemorySearchResult[]>;
|
|
142
|
+
}
|
|
143
|
+
/** Pluggable memory backend. External implementations belong in `@purista/harness-memory-*` packages. */
|
|
144
|
+
export interface MemoryAdapter extends HarnessContextConfigurable, AdapterCapabilities {
|
|
145
|
+
/** Static adapter metadata used for validation and inspection. */
|
|
146
|
+
readonly info: MemoryAdapterInfo;
|
|
147
|
+
/** Adapter capability list mirrored from `info.capabilities`. */
|
|
148
|
+
readonly capabilities: readonly MemoryCapability[];
|
|
149
|
+
/** Opens a backend store for one scope. */
|
|
150
|
+
open(scope: MemoryScope, ctx: MemoryOpenContext): Promise<MemoryStore>;
|
|
151
|
+
/** Releases adapter-owned resources. */
|
|
152
|
+
close?(): Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
/** Session-scoped public key/value facade. */
|
|
155
|
+
export interface SessionMemory {
|
|
156
|
+
/** Reads a JSON value by key. Returns `undefined` when absent. */
|
|
157
|
+
read<T = JsonValue>(key: string): Promise<T | undefined>;
|
|
158
|
+
/** Writes a JSON-serializable value by key. */
|
|
159
|
+
write(key: string, value: JsonValue, opts?: MemoryWriteOptions): Promise<void>;
|
|
160
|
+
/** Deletes a key if it exists. */
|
|
161
|
+
delete(key: string): Promise<void>;
|
|
162
|
+
/** Lists keys in this scope. */
|
|
163
|
+
list(opts?: MemoryListOptions): Promise<string[]>;
|
|
164
|
+
/** Searches memory, or throws `ModelCapabilityError` when the adapter does not support `memory.search`. */
|
|
165
|
+
search(query: MemorySearchQuery): Promise<MemorySearchResult[]>;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Scoped memory helper exposed to workflows, agents, and TypeScript tools.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* await ctx.memory.session.write('last_topic', { value: 'pricing' })
|
|
173
|
+
* const prior = await ctx.memory.user().read<{ value: string }>('preference')
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export interface MemoryFacade {
|
|
177
|
+
/** Session-scoped memory for the current conversation thread. */
|
|
178
|
+
session: SessionMemory;
|
|
179
|
+
/** Run-scoped memory for the current agent or workflow run. */
|
|
180
|
+
run: SessionMemory;
|
|
181
|
+
/** Agent-scoped memory, present in agent and tool contexts. */
|
|
182
|
+
agent?: SessionMemory;
|
|
183
|
+
/** User-scoped memory using the explicit id or `metadata.userId`. */
|
|
184
|
+
user(userId?: string): SessionMemory;
|
|
185
|
+
/** Tenant-scoped memory using the explicit id or `metadata.tenantId`. */
|
|
186
|
+
tenant(tenantId?: string): SessionMemory;
|
|
187
|
+
/** Creates a memory handle for an explicit scope. */
|
|
188
|
+
scope(scope: MemoryScope): SessionMemory;
|
|
189
|
+
}
|
|
190
|
+
/** Internal options for creating a scoped memory facade. */
|
|
191
|
+
export interface CreateMemoryFacadeOptions {
|
|
192
|
+
adapter: MemoryAdapter;
|
|
193
|
+
logger: Logger;
|
|
194
|
+
telemetry: TelemetryShim;
|
|
195
|
+
contentCaptureMode: ContentCaptureMode;
|
|
196
|
+
signal: AbortSignal;
|
|
197
|
+
sandbox?: SandboxSession;
|
|
198
|
+
harnessName: string;
|
|
199
|
+
sessionId: string;
|
|
200
|
+
runId?: string;
|
|
201
|
+
agentId?: string;
|
|
202
|
+
workflowId?: string;
|
|
203
|
+
metadata?: Readonly<Record<string, JsonValue>>;
|
|
204
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JsonValue } from '../../models/json.js';
|
|
2
|
+
import type { MemoryAdapter, MemoryCapability, MemoryListOptions, MemoryOperation, MemoryScope, MemoryScopeKind, MemorySearchQuery, MemoryWriteOptions } from './types.js';
|
|
3
|
+
export declare const MEMORY_KEY_PATTERN: RegExp;
|
|
4
|
+
export declare const scopeCapability: Record<MemoryScopeKind, MemoryCapability>;
|
|
5
|
+
/** Validates static adapter metadata before the adapter is used. */
|
|
6
|
+
export declare function validateMemoryAdapter(adapter: MemoryAdapter): void;
|
|
7
|
+
export declare function validateOperationInput(adapter: MemoryAdapter, scope: MemoryScope, operation: MemoryOperation, key: string | undefined, content: {
|
|
8
|
+
value?: JsonValue;
|
|
9
|
+
writeOpts?: MemoryWriteOptions;
|
|
10
|
+
listOpts?: MemoryListOptions;
|
|
11
|
+
query?: MemorySearchQuery;
|
|
12
|
+
} | undefined): void;
|
|
13
|
+
export declare function validateWriteOptions(adapter: MemoryAdapter, opts: MemoryWriteOptions | undefined): void;
|
|
14
|
+
export declare function normalizeListOptions(opts: MemoryListOptions | undefined): MemoryListOptions;
|
|
15
|
+
export declare function normalizeSearchQuery(query: MemorySearchQuery | undefined): MemorySearchQuery;
|
|
16
|
+
export declare function validateJsonSerializable(value: unknown, where: 'memory_value' | 'memory_write_options' | 'memory_search_query'): void;
|
|
17
|
+
export declare function validateMemoryKey(key: string): void;
|
|
18
|
+
export declare function assertScope(adapter: MemoryAdapter, scope: MemoryScope): void;
|
|
19
|
+
export declare function assertCapability(adapter: MemoryAdapter, capability: MemoryCapability, method: string): void;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { HarnessConfigError, ModelCapabilityError, ValidationError } from '../../errors/index.js';
|
|
2
|
+
export const MEMORY_KEY_PATTERN = /^[A-Za-z0-9_.\-:]{1,256}$/;
|
|
3
|
+
const MEMORY_TAG_PATTERN = /^[A-Za-z0-9_.\-:]{1,64}$/;
|
|
4
|
+
const MEMORY_ADAPTER_ID_PATTERN = /^[a-z][a-z0-9_.-]{1,63}$/;
|
|
5
|
+
const MAX_LIMIT = 1000;
|
|
6
|
+
const DEFAULT_LIMIT = 100;
|
|
7
|
+
export const scopeCapability = {
|
|
8
|
+
run: 'memory.run',
|
|
9
|
+
session: 'memory.session',
|
|
10
|
+
agent: 'memory.agent',
|
|
11
|
+
user: 'memory.user',
|
|
12
|
+
tenant: 'memory.tenant'
|
|
13
|
+
};
|
|
14
|
+
/** Validates static adapter metadata before the adapter is used. */
|
|
15
|
+
export function validateMemoryAdapter(adapter) {
|
|
16
|
+
if (!MEMORY_ADAPTER_ID_PATTERN.test(adapter.info.id)) {
|
|
17
|
+
throw new HarnessConfigError('Invalid memory adapter id.', { reason: 'invalid_memory_adapter', path: 'memory.info.id', id: adapter.info.id });
|
|
18
|
+
}
|
|
19
|
+
if (!adapter.info.packageName) {
|
|
20
|
+
throw new HarnessConfigError('Invalid memory adapter package name.', { reason: 'invalid_memory_adapter', path: 'memory.info.packageName', id: adapter.info.id });
|
|
21
|
+
}
|
|
22
|
+
if (!adapter.info.capabilities.includes('memory.kv')) {
|
|
23
|
+
throw new HarnessConfigError('Memory adapter must support memory.kv.', { reason: 'invalid_memory_adapter', path: 'memory.info.capabilities', id: adapter.info.id });
|
|
24
|
+
}
|
|
25
|
+
if (adapter.capabilities.length !== adapter.info.capabilities.length || adapter.capabilities.some((capability) => !adapter.info.capabilities.includes(capability))) {
|
|
26
|
+
throw new HarnessConfigError('Memory adapter capabilities must match info.capabilities.', { reason: 'invalid_memory_adapter', path: 'memory.capabilities', id: adapter.info.id });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function validateOperationInput(adapter, scope, operation, key, content) {
|
|
30
|
+
assertScope(adapter, scope);
|
|
31
|
+
if (key !== undefined)
|
|
32
|
+
validateMemoryKey(key);
|
|
33
|
+
if (operation === 'set') {
|
|
34
|
+
validateJsonSerializable(content?.value, 'memory_value');
|
|
35
|
+
validateWriteOptions(adapter, content?.writeOpts);
|
|
36
|
+
}
|
|
37
|
+
if (operation === 'list')
|
|
38
|
+
normalizeListOptions(content?.listOpts);
|
|
39
|
+
if (operation === 'search') {
|
|
40
|
+
assertCapability(adapter, 'memory.search', 'search');
|
|
41
|
+
normalizeSearchQuery(content?.query);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function validateWriteOptions(adapter, opts) {
|
|
45
|
+
if (!opts)
|
|
46
|
+
return;
|
|
47
|
+
if (opts.ttlMs !== undefined) {
|
|
48
|
+
if (!Number.isInteger(opts.ttlMs) || opts.ttlMs <= 0) {
|
|
49
|
+
throw new ValidationError('Memory ttlMs must be a positive integer.', { where: 'memory_write_options', issues: { ttlMs: opts.ttlMs } });
|
|
50
|
+
}
|
|
51
|
+
if (!adapter.info.capabilities.includes('memory.ttl')) {
|
|
52
|
+
throw new ValidationError('Memory adapter does not support ttlMs.', { where: 'memory_write_options', issues: { reason: 'ttl_unsupported' } });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
for (const tag of opts.tags ?? []) {
|
|
56
|
+
if (!MEMORY_TAG_PATTERN.test(tag)) {
|
|
57
|
+
throw new ValidationError('Invalid memory tag.', { where: 'memory_write_options', issues: { tag } });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (opts.metadata !== undefined)
|
|
61
|
+
validateJsonSerializable(opts.metadata, 'memory_write_options');
|
|
62
|
+
}
|
|
63
|
+
export function normalizeListOptions(opts) {
|
|
64
|
+
const limit = opts?.limit ?? DEFAULT_LIMIT;
|
|
65
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_LIMIT) {
|
|
66
|
+
throw new ValidationError('Invalid memory list limit.', { where: 'memory_list_options', issues: { limit } });
|
|
67
|
+
}
|
|
68
|
+
return { ...opts, limit };
|
|
69
|
+
}
|
|
70
|
+
export function normalizeSearchQuery(query) {
|
|
71
|
+
const text = query?.text?.trim() ?? '';
|
|
72
|
+
const limit = query?.limit ?? DEFAULT_LIMIT;
|
|
73
|
+
if (text.length === 0 || text.length > 8000) {
|
|
74
|
+
throw new ValidationError('Invalid memory search text.', { where: 'memory_search_query', issues: { textLength: text.length } });
|
|
75
|
+
}
|
|
76
|
+
if (!Number.isInteger(limit) || limit <= 0 || limit > MAX_LIMIT) {
|
|
77
|
+
throw new ValidationError('Invalid memory search limit.', { where: 'memory_search_query', issues: { limit } });
|
|
78
|
+
}
|
|
79
|
+
for (const tag of query?.tags ?? []) {
|
|
80
|
+
if (!MEMORY_TAG_PATTERN.test(tag)) {
|
|
81
|
+
throw new ValidationError('Invalid memory search tag.', { where: 'memory_search_query', issues: { tag } });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (query?.metadata !== undefined)
|
|
85
|
+
validateJsonSerializable(query.metadata, 'memory_search_query');
|
|
86
|
+
return { ...query, text, limit };
|
|
87
|
+
}
|
|
88
|
+
export function validateJsonSerializable(value, where) {
|
|
89
|
+
const seen = new WeakSet();
|
|
90
|
+
const invalid = findInvalidJsonValue(value, seen);
|
|
91
|
+
if (invalid) {
|
|
92
|
+
throw new ValidationError('Memory value must be JSON-serializable.', { where, issues: invalid });
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
throw new ValidationError('Memory value must be JSON-serializable.', { where, issues: {} }, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function findInvalidJsonValue(value, seen, path = '$') {
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
return { path, reason: 'undefined' };
|
|
104
|
+
const valueType = typeof value;
|
|
105
|
+
if (valueType === 'function' || valueType === 'symbol' || valueType === 'bigint') {
|
|
106
|
+
return { path, reason: valueType };
|
|
107
|
+
}
|
|
108
|
+
if (value === null || valueType !== 'object')
|
|
109
|
+
return undefined;
|
|
110
|
+
if (seen.has(value))
|
|
111
|
+
return { path, reason: 'circular' };
|
|
112
|
+
seen.add(value);
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
115
|
+
const invalid = findInvalidJsonValue(value[index], seen, `${path}[${index}]`);
|
|
116
|
+
if (invalid)
|
|
117
|
+
return invalid;
|
|
118
|
+
}
|
|
119
|
+
seen.delete(value);
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
for (const [key, item] of Object.entries(value)) {
|
|
123
|
+
const invalid = findInvalidJsonValue(item, seen, `${path}.${key}`);
|
|
124
|
+
if (invalid)
|
|
125
|
+
return invalid;
|
|
126
|
+
}
|
|
127
|
+
seen.delete(value);
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
export function validateMemoryKey(key) {
|
|
131
|
+
if (!MEMORY_KEY_PATTERN.test(key)) {
|
|
132
|
+
throw new ValidationError('Invalid session memory key.', { where: 'memory_key', issues: { key } });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export function assertScope(adapter, scope) {
|
|
136
|
+
const capability = scopeCapability[scope.kind];
|
|
137
|
+
assertCapability(adapter, capability, scope.kind);
|
|
138
|
+
const missing = scope.kind === 'session' && !scope.sessionId
|
|
139
|
+
? 'sessionId'
|
|
140
|
+
: scope.kind === 'run' && !scope.runId
|
|
141
|
+
? 'runId'
|
|
142
|
+
: scope.kind === 'agent' && !scope.agentId
|
|
143
|
+
? 'agentId'
|
|
144
|
+
: scope.kind === 'user' && !scope.userId
|
|
145
|
+
? 'userId'
|
|
146
|
+
: scope.kind === 'tenant' && !scope.tenantId
|
|
147
|
+
? 'tenantId'
|
|
148
|
+
: undefined;
|
|
149
|
+
if (missing) {
|
|
150
|
+
throw new ValidationError('Missing memory scope identifier.', { where: 'memory_scope', issues: { reason: 'missing_scope_identifier', field: missing, scope: scope.kind } });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function assertCapability(adapter, capability, method) {
|
|
154
|
+
if (adapter.info.capabilities.includes(capability))
|
|
155
|
+
return;
|
|
156
|
+
if (capability === 'memory.search') {
|
|
157
|
+
throw new ModelCapabilityError('Memory search is not available for this adapter.', { alias: adapter.info.id, method: 'memory.search', reason: 'missing_capability' });
|
|
158
|
+
}
|
|
159
|
+
throw new ValidationError('Memory adapter does not support the requested scope or option.', { where: 'memory_scope', issues: { reason: 'scope_unsupported', capability, method } });
|
|
160
|
+
}
|