@purista/harness 1.0.0 → 1.1.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.
@@ -0,0 +1,17 @@
1
+ import type { MemoryAdapter } from '../../ports/memory.js';
2
+ /**
3
+ * Creates the built-in memory adapter backed by the current session sandbox.
4
+ *
5
+ * It is intentionally simple and local: session memory is stored below
6
+ * `/memory/session/`, run memory below `/memory/runs/<runId>/`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const harness = defineHarness()
11
+ * .memory(sandboxMemory())
12
+ * .models({ fast: model })
13
+ * .agents(({ agent }) => ({ assistant: agent({ model: 'fast', instructions: 'Help.' }) }))
14
+ * .build()
15
+ * ```
16
+ */
17
+ export declare function sandboxMemory(): MemoryAdapter;
@@ -0,0 +1,122 @@
1
+ import { StateError } from '../../errors/index.js';
2
+ /**
3
+ * Creates the built-in memory adapter backed by the current session sandbox.
4
+ *
5
+ * It is intentionally simple and local: session memory is stored below
6
+ * `/memory/session/`, run memory below `/memory/runs/<runId>/`.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const harness = defineHarness()
11
+ * .memory(sandboxMemory())
12
+ * .models({ fast: model })
13
+ * .agents(({ agent }) => ({ assistant: agent({ model: 'fast', instructions: 'Help.' }) }))
14
+ * .build()
15
+ * ```
16
+ */
17
+ export function sandboxMemory() {
18
+ return new SandboxMemoryAdapter();
19
+ }
20
+ class SandboxMemoryAdapter {
21
+ info = {
22
+ id: 'sandbox_memory',
23
+ packageName: '@purista/harness',
24
+ capabilities: ['memory.kv', 'memory.list', 'memory.delete', 'memory.run', 'memory.session']
25
+ };
26
+ capabilities = this.info.capabilities;
27
+ configureHarnessContext(_context) {
28
+ // The sandbox adapter receives runtime context through each `open(...)` call.
29
+ }
30
+ async open(scope, ctx) {
31
+ const sandbox = ctx.sandbox;
32
+ if (!sandbox) {
33
+ throw new StateError('sandboxMemory requires an active sandbox session.', {
34
+ op: 'memory.get',
35
+ adapter: 'memory',
36
+ memory_provider: this.info.id,
37
+ reason: 'missing_sandbox'
38
+ });
39
+ }
40
+ const root = scopeRoot(scope);
41
+ const metaRoot = scopeMetaRoot(scope);
42
+ return {
43
+ get: async (key, op) => {
44
+ op.signal.throwIfAborted();
45
+ const path = `${root}/${key}.json`;
46
+ if (!(await sandbox.exists(path)))
47
+ return undefined;
48
+ return JSON.parse(await sandbox.readText(path));
49
+ },
50
+ set: async (key, value, op) => {
51
+ op.signal.throwIfAborted();
52
+ const existing = await readMetadata(sandbox, metaRoot, key);
53
+ const now = new Date().toISOString();
54
+ await sandbox.write(`${root}/${key}.json`, JSON.stringify(value));
55
+ await sandbox.write(`${metaRoot}/${key}.json`, JSON.stringify({
56
+ createdAt: existing?.createdAt ?? now,
57
+ updatedAt: now,
58
+ ...(op.opts?.tags ? { tags: op.opts.tags } : existing?.tags ? { tags: existing.tags } : {}),
59
+ ...(op.opts?.metadata ? { metadata: op.opts.metadata } : existing?.metadata ? { metadata: existing.metadata } : {})
60
+ }));
61
+ },
62
+ delete: async (key, op) => {
63
+ op.signal.throwIfAborted();
64
+ await sandbox.remove(`${root}/${key}.json`).catch(() => undefined);
65
+ await sandbox.remove(`${metaRoot}/${key}.json`).catch(() => undefined);
66
+ },
67
+ list: async (op) => {
68
+ op.signal.throwIfAborted();
69
+ const entries = await sandbox.list(root).catch(() => []);
70
+ const opts = op.opts ?? {};
71
+ const keys = entries
72
+ .filter((entry) => entry.kind === 'file' && entry.name.endsWith('.json'))
73
+ .map((entry) => entry.name.slice(0, -5))
74
+ .filter((key) => !opts.prefix || key.startsWith(opts.prefix))
75
+ .filter((key) => !opts.cursor || key > opts.cursor)
76
+ .sort()
77
+ .slice(0, opts.limit);
78
+ const out = [];
79
+ for (const key of keys) {
80
+ const metadata = await readMetadata(sandbox, metaRoot, key);
81
+ out.push({ key, ...(metadata ?? {}) });
82
+ }
83
+ return out;
84
+ }
85
+ };
86
+ }
87
+ }
88
+ function scopeRoot(scope) {
89
+ if (scope.kind === 'session')
90
+ return '/memory/session';
91
+ if (scope.kind === 'run' && scope.runId)
92
+ return `/memory/runs/${scope.runId}`;
93
+ throw new StateError('Unsupported sandbox memory scope.', {
94
+ op: 'memory.get',
95
+ adapter: 'memory',
96
+ memory_provider: 'sandbox_memory',
97
+ reason: `unsupported_scope:${scope.kind}`
98
+ });
99
+ }
100
+ function scopeMetaRoot(scope) {
101
+ if (scope.kind === 'session')
102
+ return '/memory/.meta/session';
103
+ if (scope.kind === 'run' && scope.runId)
104
+ return `/memory/.meta/runs/${scope.runId}`;
105
+ throw new StateError('Unsupported sandbox memory scope.', {
106
+ op: 'memory.list',
107
+ adapter: 'memory',
108
+ memory_provider: 'sandbox_memory',
109
+ reason: `unsupported_scope:${scope.kind}`
110
+ });
111
+ }
112
+ async function readMetadata(sandbox, metaRoot, key) {
113
+ const path = `${metaRoot}/${key}.json`;
114
+ if (!(await sandbox.exists(path).catch(() => false)))
115
+ return undefined;
116
+ try {
117
+ return JSON.parse(await sandbox.readText(path));
118
+ }
119
+ catch {
120
+ return undefined;
121
+ }
122
+ }
@@ -131,10 +131,13 @@ function withModelStreamSpan(options, aliasKey, alias, method, ctx, fn) {
131
131
  span.setAttributes({
132
132
  [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: lastUsage.inputTokens,
133
133
  [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: lastUsage.outputTokens,
134
- 'gen_ai.usage.total_tokens': lastUsage.totalTokens
134
+ 'gen_ai.usage.total_tokens': lastUsage.totalTokens,
135
+ 'llm.token_count.prompt': lastUsage.inputTokens,
136
+ 'llm.token_count.completion': lastUsage.outputTokens,
137
+ 'llm.token_count.total': lastUsage.totalTokens
135
138
  });
136
- options.telemetry?.recordCounter('gen_ai.client.token.usage', lastUsage.inputTokens, { ...attrs, [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_INPUT });
137
- options.telemetry?.recordCounter('gen_ai.client.token.usage', lastUsage.outputTokens, { ...attrs, [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_OUTPUT });
139
+ options.telemetry?.recordHistogram('gen_ai.client.token.usage', lastUsage.inputTokens, { ...attrs, [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_INPUT });
140
+ options.telemetry?.recordHistogram('gen_ai.client.token.usage', lastUsage.outputTokens, { ...attrs, [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_OUTPUT });
138
141
  }
139
142
  if (lastFinishReason)
140
143
  span.setAttribute(ATTR_GEN_AI_RESPONSE_FINISH_REASONS, [lastFinishReason]);
@@ -188,10 +191,13 @@ async function withModelSpan(options, aliasKey, alias, method, ctx, fn) {
188
191
  span.setAttributes({
189
192
  [ATTR_GEN_AI_USAGE_INPUT_TOKENS]: usage.inputTokens,
190
193
  [ATTR_GEN_AI_USAGE_OUTPUT_TOKENS]: usage.outputTokens,
191
- 'gen_ai.usage.total_tokens': usage.totalTokens
194
+ 'gen_ai.usage.total_tokens': usage.totalTokens,
195
+ 'llm.token_count.prompt': usage.inputTokens,
196
+ 'llm.token_count.completion': usage.outputTokens,
197
+ 'llm.token_count.total': usage.totalTokens
192
198
  });
193
- options.telemetry?.recordCounter('gen_ai.client.token.usage', usage.inputTokens, { ...attrs, [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_INPUT });
194
- options.telemetry?.recordCounter('gen_ai.client.token.usage', usage.outputTokens, { ...attrs, [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_OUTPUT });
199
+ options.telemetry?.recordHistogram('gen_ai.client.token.usage', usage.inputTokens, { ...attrs, [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_INPUT });
200
+ options.telemetry?.recordHistogram('gen_ai.client.token.usage', usage.outputTokens, { ...attrs, [ATTR_GEN_AI_TOKEN_TYPE]: GEN_AI_TOKEN_TYPE_VALUE_OUTPUT });
195
201
  }
196
202
  if (finishReason)
197
203
  span.setAttribute(ATTR_GEN_AI_RESPONSE_FINISH_REASONS, [finishReason]);
@@ -208,11 +214,30 @@ function modelSpanAttrs(options, aliasKey, alias, method, ctx) {
208
214
  'harness.agent.id': ctx?.agentId,
209
215
  'harness.model.alias': aliasKey,
210
216
  'harness.model.method': method,
217
+ 'gen_ai.operation.name': genAiOperationName(method),
218
+ 'openinference.span.kind': openInferenceSpanKind(method),
211
219
  [ATTR_GEN_AI_SYSTEM]: alias.provider.genAiSystem,
220
+ 'gen_ai.provider.name': alias.provider.genAiSystem,
212
221
  [ATTR_GEN_AI_REQUEST_MODEL]: alias.model,
213
- 'model.provider': alias.provider.id
222
+ 'model.provider': alias.provider.id,
223
+ 'llm.provider': alias.provider.genAiSystem,
224
+ 'llm.model_name': alias.model
214
225
  };
215
226
  }
227
+ function genAiOperationName(method) {
228
+ if (method === 'embeddings')
229
+ return 'embeddings';
230
+ if (method === 'rerank')
231
+ return undefined;
232
+ return 'chat';
233
+ }
234
+ function openInferenceSpanKind(method) {
235
+ if (method === 'embeddings')
236
+ return 'EMBEDDING';
237
+ if (method === 'rerank')
238
+ return 'RERANKER';
239
+ return 'LLM';
240
+ }
216
241
  /**
217
242
  * Validates alias capabilities for the requested operation.
218
243
  *
@@ -26,14 +26,36 @@ export type AdapterCapability =
26
26
  /** Runtime can resume from committed checkpoints. */
27
27
  | 'runtime.resume_from_checkpoint'
28
28
  /** Adapter can record feedback. */
29
- | 'feedback.record';
29
+ | 'feedback.record'
30
+ /** Memory adapter supports key/value reads and writes. */
31
+ | 'memory.kv'
32
+ /** Memory adapter supports key listing. */
33
+ | 'memory.list'
34
+ /** Memory adapter supports key deletion. */
35
+ | 'memory.delete'
36
+ /** Memory adapter supports text search over stored memory. */
37
+ | 'memory.search'
38
+ /** Memory adapter supports entry expiration. */
39
+ | 'memory.ttl'
40
+ /** Memory adapter supports run-scoped memory. */
41
+ | 'memory.run'
42
+ /** Memory adapter supports session-scoped memory. */
43
+ | 'memory.session'
44
+ /** Memory adapter supports agent-scoped memory. */
45
+ | 'memory.agent'
46
+ /** Memory adapter supports user-scoped memory. */
47
+ | 'memory.user'
48
+ /** Memory adapter supports tenant-scoped memory. */
49
+ | 'memory.tenant'
50
+ /** Memory survives adapter close/reopen for the same logical scope. */
51
+ | 'memory.persistent';
30
52
  /** Data-only descriptor implemented by adapters that expose capability metadata. */
31
53
  export interface AdapterCapabilities {
32
54
  readonly capabilities: readonly AdapterCapability[];
33
55
  }
34
56
  /** Adapter descriptor surfaced through `harness.inspect()`. */
35
57
  export interface AdapterInspection {
36
- readonly kind: 'state' | 'sandbox' | 'runtime' | 'feedback' | 'model';
58
+ readonly kind: 'state' | 'sandbox' | 'runtime' | 'feedback' | 'model' | 'memory';
37
59
  readonly id: string;
38
60
  readonly capabilities: readonly AdapterCapability[];
39
61
  readonly metadata?: Record<string, unknown>;
@@ -1,10 +1,13 @@
1
1
  import type { Logger } from '../logger/index.js';
2
- import type { TelemetryShim } from '../telemetry/index.js';
2
+ import type { Metrics, TelemetryShim } from '../telemetry/index.js';
3
+ import type { ContentCaptureMode } from '../harness/defineHarness.js';
3
4
  /** Harness-level context inherited by adapters registered with the harness. */
4
5
  export interface HarnessAdapterContext {
5
6
  harnessName: string;
6
7
  logger: Logger;
7
8
  telemetry: TelemetryShim;
9
+ metrics: Metrics;
10
+ contentCaptureMode: ContentCaptureMode;
8
11
  defaults: {
9
12
  agentMaxIterations: number;
10
13
  runTimeoutMs: number;
@@ -4,3 +4,4 @@ export * from './state.js';
4
4
  export * from './harness-context.js';
5
5
  export * from './capabilities.js';
6
6
  export * from './feedback.js';
7
+ export * from './memory.js';
@@ -4,3 +4,4 @@ export * from './state.js';
4
4
  export * from './harness-context.js';
5
5
  export * from './capabilities.js';
6
6
  export * from './feedback.js';
7
+ export * from './memory.js';
@@ -0,0 +1,5 @@
1
+ import type { CreateMemoryFacadeOptions, MemoryFacade, MemoryScope, SessionMemory } from './types.js';
2
+ /** Creates scoped memory helpers for a concrete session/run context. */
3
+ export declare function createMemoryFacade(opts: CreateMemoryFacadeOptions): MemoryFacade;
4
+ /** Creates a key/value memory facade bound to one normalized scope. */
5
+ export declare function createSessionMemory(opts: CreateMemoryFacadeOptions, scope: MemoryScope): SessionMemory;
@@ -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
+ }