@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,110 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ /** Deterministic in-memory adapter for unit tests and adapter contract examples. */
3
+ export class FakeMemoryAdapter {
4
+ info = {
5
+ id: 'fake_memory',
6
+ packageName: '@purista/harness/testing',
7
+ capabilities: [
8
+ 'memory.kv',
9
+ 'memory.list',
10
+ 'memory.delete',
11
+ 'memory.search',
12
+ 'memory.run',
13
+ 'memory.session',
14
+ 'memory.agent',
15
+ 'memory.user',
16
+ 'memory.tenant',
17
+ 'memory.persistent'
18
+ ]
19
+ };
20
+ capabilities = this.info.capabilities;
21
+ openedScopes = [];
22
+ values = new Map();
23
+ configureHarnessContext() {
24
+ // Fake adapter does not need inherited services, but implements the hook so contract users can assert it is called.
25
+ }
26
+ async open(scope, _ctx) {
27
+ this.openedScopes.push(scope);
28
+ const prefix = scopeKey(scope);
29
+ return {
30
+ get: async (key, ctx) => {
31
+ ctx.signal.throwIfAborted();
32
+ return this.values.get(`${prefix}:${key}`);
33
+ },
34
+ set: async (key, value, ctx) => {
35
+ ctx.signal.throwIfAborted();
36
+ this.values.set(`${prefix}:${key}`, value);
37
+ },
38
+ delete: async (key, ctx) => {
39
+ ctx.signal.throwIfAborted();
40
+ this.values.delete(`${prefix}:${key}`);
41
+ },
42
+ list: async (ctx) => {
43
+ ctx.signal.throwIfAborted();
44
+ const keys = [...this.values.keys()]
45
+ .filter((key) => key.startsWith(`${prefix}:`))
46
+ .map((key) => key.slice(prefix.length + 1))
47
+ .filter((key) => !ctx.opts?.prefix || key.startsWith(ctx.opts.prefix))
48
+ .filter((key) => !ctx.opts?.cursor || key > ctx.opts.cursor)
49
+ .sort()
50
+ .slice(0, ctx.opts?.limit);
51
+ return keys.map((key) => ({ key }));
52
+ },
53
+ search: async (query, ctx) => {
54
+ ctx.signal.throwIfAborted();
55
+ return [...this.values.entries()]
56
+ .filter(([key]) => key.startsWith(`${prefix}:`))
57
+ .map(([key, value]) => ({ key: key.slice(prefix.length + 1), value, score: JSON.stringify(value).includes(query.text) ? 1 : 0 }))
58
+ .filter((result) => result.score > 0)
59
+ .sort((a, b) => (b.score ?? 0) - (a.score ?? 0) || a.key.localeCompare(b.key))
60
+ .slice(0, query.limit);
61
+ }
62
+ };
63
+ }
64
+ }
65
+ /** Shared contract for memory adapters. */
66
+ export function memoryAdapterContract(make) {
67
+ describe('memoryAdapterContract', () => {
68
+ it('round-trips scoped JSON values', async () => {
69
+ const adapter = await make();
70
+ const ctx = contractContext();
71
+ const store = await adapter.open({ kind: 'session', sessionId: 's1' }, ctx);
72
+ await store.set('foo', { a: 1 }, { ...ctx, scope: { kind: 'session', sessionId: 's1' }, operation: 'set' });
73
+ await expect(store.get('foo', { ...ctx, scope: { kind: 'session', sessionId: 's1' }, operation: 'get' })).resolves.toEqual({ a: 1 });
74
+ });
75
+ it('isolates scopes', async () => {
76
+ const adapter = await make();
77
+ const ctx = contractContext();
78
+ const one = await adapter.open({ kind: 'session', sessionId: 's1' }, ctx);
79
+ const two = await adapter.open({ kind: 'session', sessionId: 's2' }, ctx);
80
+ await one.set('foo', 'one', { ...ctx, scope: { kind: 'session', sessionId: 's1' }, operation: 'set' });
81
+ await expect(two.get('foo', { ...ctx, scope: { kind: 'session', sessionId: 's2' }, operation: 'get' })).resolves.toBeUndefined();
82
+ });
83
+ });
84
+ }
85
+ function scopeKey(scope) {
86
+ return [
87
+ scope.kind,
88
+ scope.tenantId,
89
+ scope.userId,
90
+ scope.sessionId,
91
+ scope.runId,
92
+ scope.workflowId,
93
+ scope.agentId
94
+ ].filter(Boolean).join(':');
95
+ }
96
+ function contractContext() {
97
+ const signal = new AbortController().signal;
98
+ return {
99
+ logger: { trace() { }, debug() { }, info() { }, warn() { }, error() { }, fatal() { }, child: () => contractContext().logger },
100
+ telemetry: {
101
+ span: async (_name, _attrs, fn) => fn({ setAttribute: () => undefined, setAttributes: () => undefined, addEvent: () => undefined, recordException: () => undefined, setStatus: () => undefined, end: () => undefined, spanContext: () => ({ traceId: '', spanId: '', traceFlags: 0 }), isRecording: () => false, updateName: () => undefined }),
102
+ recordHistogram() { },
103
+ recordCounter() { },
104
+ currentTraceparent: () => undefined
105
+ },
106
+ metrics: { counter() { }, histogram() { }, duration: async (_name, _attrs, fn) => fn() },
107
+ contentCaptureMode: 'NO_CONTENT',
108
+ signal
109
+ };
110
+ }
@@ -1,6 +1,9 @@
1
1
  export { FakeModelProvider } from './fakeModelProvider.js';
2
+ export { FakeMemoryAdapter, memoryAdapterContract } from './fakeMemoryAdapter.js';
2
3
  export { adapterCapabilitiesContract, fakeCapabilityAdapter, type FakeCapabilityAdapter } from './capabilities.js';
3
4
  export { createInMemoryFeedbackRecorder } from './feedback.js';
5
+ export { evaluateDeterministicScorer } from '../eval/index.js';
6
+ export type { DeterministicScorerDefinition, ScorerResult, ScorerTarget } from '../eval/index.js';
4
7
  export { sandboxContract } from './sandboxContract.js';
5
8
  export { fakeSnapshotSandbox, sandboxSnapshotContract } from './sandboxSnapshot.js';
6
9
  export { stateStoreContract } from './stateStoreContract.js';
@@ -1,7 +1,9 @@
1
1
  import { defineHarness } from '../harness/defineHarness.js';
2
2
  export { FakeModelProvider } from './fakeModelProvider.js';
3
+ export { FakeMemoryAdapter, memoryAdapterContract } from './fakeMemoryAdapter.js';
3
4
  export { adapterCapabilitiesContract, fakeCapabilityAdapter } from './capabilities.js';
4
5
  export { createInMemoryFeedbackRecorder } from './feedback.js';
6
+ export { evaluateDeterministicScorer } from '../eval/index.js';
5
7
  export { sandboxContract } from './sandboxContract.js';
6
8
  export { fakeSnapshotSandbox, sandboxSnapshotContract } from './sandboxSnapshot.js';
7
9
  export { stateStoreContract } from './stateStoreContract.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purista/harness",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Self-hosted enterprise agent harness for typed tools, agents, workflows, state, sandboxing, and telemetry.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,10 +39,12 @@
39
39
  "scripts": {
40
40
  "clean": "rm -rf dist",
41
41
  "build": "npm run clean && tsc -p tsconfig.json",
42
+ "lint": "npm run typecheck",
42
43
  "typecheck": "tsc -p tsconfig.json --noEmit",
43
44
  "test:types": "tsc -p tsconfig.type-tests.json",
44
45
  "test": "vitest run",
45
46
  "test:unit": "vitest run src",
47
+ "test:coverage": "vitest run --coverage",
46
48
  "test:contracts": "vitest run src/ports test/sandbox.test.ts",
47
49
  "test:integration": "vitest run test/harness.test.ts test/tools.test.ts test/skills.test.ts test/telemetry-flow.test.ts",
48
50
  "test:failure": "vitest run test/failure"
@@ -54,7 +56,7 @@
54
56
  "peerDependencies": {
55
57
  "@modelcontextprotocol/sdk": "^1.29.0",
56
58
  "@opentelemetry/api": "^1.9.1",
57
- "just-bash": "^2.14.4"
59
+ "just-bash": "^3.0.1"
58
60
  },
59
61
  "peerDependenciesMeta": {
60
62
  "@modelcontextprotocol/sdk": {
@@ -68,8 +70,11 @@
68
70
  "@modelcontextprotocol/sdk": "^1.29.0",
69
71
  "@opentelemetry/context-async-hooks": "^2.7.1",
70
72
  "@types/node": "^25.6.0",
71
- "just-bash": "^2.14.4",
73
+ "just-bash": "^3.0.1",
72
74
  "typescript": "^6.0.3",
73
75
  "vitest": "^4.1.5"
76
+ },
77
+ "engines": {
78
+ "node": ">=24.15.0"
74
79
  }
75
80
  }