@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.
- package/README.md +15 -0
- package/dist/agents/index.d.ts +5 -3
- package/dist/agents/index.js +58 -6
- package/dist/errors/catalog.d.ts +11 -4
- package/dist/eval/index.d.ts +57 -0
- package/dist/eval/index.js +181 -0
- package/dist/harness/defineHarness.d.ts +38 -18
- package/dist/harness/defineHarness.js +23 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -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 +24 -2
- package/dist/ports/harness-context.d.ts +4 -1
- package/dist/ports/index.d.ts +1 -0
- package/dist/ports/index.js +1 -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/sessions/index.d.ts +2 -0
- package/dist/sessions/index.js +275 -68
- package/dist/telemetry/shim.d.ts +20 -0
- package/dist/telemetry/shim.js +28 -0
- package/dist/testing/fakeMemoryAdapter.d.ts +16 -0
- package/dist/testing/fakeMemoryAdapter.js +110 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +2 -0
- package/package.json +8 -3
|
@@ -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
|
+
}
|
package/dist/models/registry.js
CHANGED
|
@@ -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?.
|
|
137
|
-
options.telemetry?.
|
|
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?.
|
|
194
|
-
options.telemetry?.
|
|
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;
|
package/dist/ports/index.d.ts
CHANGED
package/dist/ports/index.js
CHANGED
|
@@ -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
|
+
}
|