@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.
Files changed (50) hide show
  1. package/README.md +15 -0
  2. package/dist/agents/index.d.ts +5 -3
  3. package/dist/agents/index.js +84 -8
  4. package/dist/errors/catalog.d.ts +45 -5
  5. package/dist/errors/catalog.js +19 -0
  6. package/dist/errors/harness-error.d.ts +2 -0
  7. package/dist/eval/index.d.ts +57 -0
  8. package/dist/eval/index.js +181 -0
  9. package/dist/harness/defineHarness.d.ts +96 -20
  10. package/dist/harness/defineHarness.js +59 -2
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +4 -0
  13. package/dist/memory/sandbox/index.d.ts +17 -0
  14. package/dist/memory/sandbox/index.js +122 -0
  15. package/dist/models/registry.js +32 -7
  16. package/dist/ports/capabilities.d.ts +46 -2
  17. package/dist/ports/harness-context.d.ts +4 -1
  18. package/dist/ports/index.d.ts +2 -0
  19. package/dist/ports/index.js +2 -0
  20. package/dist/ports/memory/facade.d.ts +5 -0
  21. package/dist/ports/memory/facade.js +123 -0
  22. package/dist/ports/memory/telemetry.d.ts +16 -0
  23. package/dist/ports/memory/telemetry.js +77 -0
  24. package/dist/ports/memory/types.d.ts +204 -0
  25. package/dist/ports/memory/types.js +1 -0
  26. package/dist/ports/memory/validation.d.ts +19 -0
  27. package/dist/ports/memory/validation.js +160 -0
  28. package/dist/ports/memory.d.ts +3 -0
  29. package/dist/ports/memory.js +3 -0
  30. package/dist/ports/workspace.d.ts +177 -0
  31. package/dist/ports/workspace.js +32 -0
  32. package/dist/runtime/durable.d.ts +3 -0
  33. package/dist/runtime/durable.js +2 -1
  34. package/dist/sessions/index.d.ts +2 -0
  35. package/dist/sessions/index.js +275 -68
  36. package/dist/skills/index.d.ts +2 -1
  37. package/dist/skills/index.js +263 -35
  38. package/dist/telemetry/shim.d.ts +20 -0
  39. package/dist/telemetry/shim.js +28 -0
  40. package/dist/testing/durableWorkspaceStoreContract.d.ts +3 -0
  41. package/dist/testing/durableWorkspaceStoreContract.js +41 -0
  42. package/dist/testing/fakeMemoryAdapter.d.ts +16 -0
  43. package/dist/testing/fakeMemoryAdapter.js +110 -0
  44. package/dist/testing/index.d.ts +5 -0
  45. package/dist/testing/index.js +4 -0
  46. package/dist/workspace/in-memory.d.ts +35 -0
  47. package/dist/workspace/in-memory.js +142 -0
  48. package/dist/workspace/index.d.ts +1 -0
  49. package/dist/workspace/index.js +1 -0
  50. package/package.json +12 -6
@@ -2,10 +2,13 @@ import { z } from 'zod';
2
2
  import { type Logger } from '../logger/index.js';
3
3
  import type { ModelAlias, ModelCapability, TokenUsage } from '../ports/model-provider.js';
4
4
  import type { StateStore } from '../ports/state.js';
5
- import type { TelemetryShim } from '../telemetry/index.js';
5
+ import type { Metrics, TelemetryShim } from '../telemetry/index.js';
6
6
  import type { HarnessAdapterContext } from '../ports/harness-context.js';
7
+ import type { MemoryAdapter, MemoryFacade, SessionMemory } from '../ports/memory.js';
8
+ import type { DurableWorkspaceStore } from '../ports/workspace.js';
7
9
  import type { JsonValue } from '../models/json.js';
8
10
  import type { Message } from '../models/state.js';
11
+ import type { RunStatus } from '../models/state.js';
9
12
  import type { HarnessError } from '../errors/harness-error.js';
10
13
  import { type Sandbox } from '../sandbox/index.js';
11
14
  import type { ModelHandle } from '../models/registry.js';
@@ -13,12 +16,13 @@ import { type AdapterCapability, type DurableRuntimeAdapter, type HarnessInspect
13
16
  /** Stable harness version string for diagnostics and generated documentation. */
14
17
  export declare const HARNESS_VERSION = "0.0.0";
15
18
  /** OpenTelemetry capture controls used by the harness. */
19
+ export type TelemetryFlavor = 'dual' | 'gen_ai_only' | 'openinference_only';
20
+ export type ContentCaptureMode = 'NO_CONTENT' | 'SPAN_ONLY' | 'EVENT_ONLY' | 'SPAN_AND_EVENT';
16
21
  export interface TelemetryOptions {
17
- /**
18
- * When `true`, emitted telemetry may include full prompt/message content.
19
- * The default is `false` to avoid accidental sensitive-content capture.
20
- */
21
- captureContent?: boolean;
22
+ /** Backend emission shape. */
23
+ flavor?: TelemetryFlavor;
24
+ /** Span/event content capture mode. */
25
+ contentCaptureMode?: ContentCaptureMode;
22
26
  }
23
27
  /** Default harness budgets and execution behavior. */
24
28
  export interface HarnessDefaults {
@@ -51,6 +55,12 @@ export interface InvokeOptions {
51
55
  timeoutMs?: number;
52
56
  /** Optional history-window override for this call only. */
53
57
  historyWindow?: number;
58
+ /** Optional W3C Trace Context parent. */
59
+ traceparent?: string;
60
+ /** Optional W3C Trace Context state. */
61
+ tracestate?: string;
62
+ /** Scalar metadata exposed to handlers and telemetry sanitizers. */
63
+ metadata?: Record<string, JsonValue>;
54
64
  }
55
65
  /** Canonical built-in tool names provided by the harness. */
56
66
  export type BuiltinToolName = 'bash' | 'read' | 'write' | 'edit' | 'glob' | 'grep' | 'list';
@@ -91,27 +101,47 @@ export interface PermissionContext {
91
101
  export type PermissionDecision = 'allow' | 'deny';
92
102
  /** Async permission hook used for interactive approvals or custom policy engines. */
93
103
  export type OnPermission = (ctx: PermissionContext) => Promise<PermissionDecision>;
104
+ /** Skill frontmatter parsed from `SKILL.md`. */
105
+ export interface SkillFrontmatter {
106
+ name: string;
107
+ description: string;
108
+ license?: string;
109
+ compatibility?: string;
110
+ metadata?: Record<string, string>;
111
+ 'allowed-tools'?: string;
112
+ }
113
+ /** Validation mode for `SKILL.md` frontmatter. */
114
+ export type SkillValidationMode = 'strict' | 'lenient';
115
+ /** Diagnostic produced while parsing or discovering skills. */
116
+ export interface SkillDiagnostic {
117
+ level: 'warn' | 'error';
118
+ code: 'missing_skill_md' | 'invalid_frontmatter' | 'missing_description' | 'invalid_name' | 'name_mismatch' | 'directory_missing' | 'collision_shadowed' | 'untrusted_project_skill' | 'scan_limit_reached';
119
+ message: string;
120
+ skillName?: string;
121
+ directory?: string;
122
+ source?: string;
123
+ }
94
124
  /** Mounted skill metadata after frontmatter parsing. */
95
125
  export interface ResolvedSkill {
96
126
  /** Public skill id. */
97
127
  name: string;
98
128
  /** Short user-facing description from frontmatter. */
99
129
  description: string;
100
- /** Optional skill version. */
101
- version?: string;
102
130
  /** Absolute directory mounted into `/skills/<name>`. */
103
131
  directory: string;
104
- }
105
- /** Sandbox-backed per-session memory facade. */
106
- export interface SessionMemory {
107
- /** Reads `/memory/<key>.json` and returns the parsed JSON value if present. */
108
- read<T = JsonValue>(key: string): Promise<T | undefined>;
109
- /** Writes JSON-serializable data to `/memory/<key>.json`. */
110
- write(key: string, value: JsonValue): Promise<void>;
111
- /** Deletes `/memory/<key>.json` if it exists. */
112
- delete(key: string): Promise<void>;
113
- /** Lists known memory keys without the `.json` suffix. */
114
- list(): Promise<string[]>;
132
+ /** Absolute path to the parsed `SKILL.md`. */
133
+ skillPath: string;
134
+ /** Absolute path exposed as the skill instruction file location. */
135
+ location: string;
136
+ /** Sandbox mount path for this skill. */
137
+ mountPath: `/skills/${string}`;
138
+ license?: string;
139
+ compatibility?: string;
140
+ metadata?: Record<string, string>;
141
+ allowedTools?: string;
142
+ trust: 'trusted' | 'project' | 'user';
143
+ source?: string;
144
+ diagnostics: readonly SkillDiagnostic[];
115
145
  }
116
146
  /** Conversation history accessor for a single session thread. */
117
147
  export interface ConversationHistory {
@@ -127,6 +157,8 @@ export interface ToolHandlerContext {
127
157
  sandbox: import('../sandbox/index.js').SandboxSession;
128
158
  logger: Logger;
129
159
  telemetry: TelemetryShim;
160
+ metrics: Metrics;
161
+ memory: MemoryFacade;
130
162
  runId: string;
131
163
  sessionId: string;
132
164
  agentId: string;
@@ -214,9 +246,32 @@ export type ToolsConfig = Record<string, ToolDefinition>;
214
246
  export interface SkillDefinition {
215
247
  /** Absolute path to the directory containing `SKILL.md`. */
216
248
  directory: string;
249
+ validationMode?: SkillValidationMode;
250
+ trust?: 'trusted' | 'project' | 'user';
251
+ source?: string;
217
252
  }
218
253
  /** Full skill registry shape. */
219
254
  export type SkillsConfig = Record<string, SkillDefinition>;
255
+ /** Options for local Agent Skills discovery. */
256
+ export interface DiscoverSkillsOptions {
257
+ projectRoot?: string;
258
+ clientName?: string;
259
+ includeProjectAgentsDir?: boolean;
260
+ includeProjectClientDir?: boolean;
261
+ includeUserAgentsDir?: boolean;
262
+ includeUserClientDir?: boolean;
263
+ includeClaudeCompatDir?: boolean;
264
+ includeAncestorProjectDirs?: boolean;
265
+ trustedProjectRoots?: readonly string[];
266
+ validationMode?: SkillValidationMode;
267
+ maxDepth?: number;
268
+ maxDirectories?: number;
269
+ }
270
+ /** Result of local Agent Skills discovery. */
271
+ export interface DiscoveredSkills {
272
+ skills: SkillsConfig;
273
+ diagnostics: readonly SkillDiagnostic[];
274
+ }
220
275
  /** Alias map passed to `.models(...)`. */
221
276
  export type ModelsConfig = Record<string, ModelAlias>;
222
277
  /** Builder-state accumulator used for type propagation across the fluent harness builder. */
@@ -258,7 +313,9 @@ export interface AgentContextMinimal<S extends BuilderState, I> {
258
313
  sessionId: string;
259
314
  runId: string;
260
315
  history: ConversationHistory;
261
- memory: SessionMemory;
316
+ memory: MemoryFacade;
317
+ metadata: Readonly<Record<string, JsonValue>>;
318
+ metrics: Metrics;
262
319
  }
263
320
  /** Full context passed to workflow handlers. */
264
321
  export interface WorkflowContext<S extends BuilderState, I, O> {
@@ -270,6 +327,9 @@ export interface WorkflowContext<S extends BuilderState, I, O> {
270
327
  signal: AbortSignal;
271
328
  runId: string;
272
329
  sessionId: string;
330
+ metadata: Readonly<Record<string, JsonValue>>;
331
+ memory: MemoryFacade;
332
+ metrics: Metrics;
273
333
  output?: O;
274
334
  }
275
335
  /** Full context passed to custom agent handlers. */
@@ -442,6 +502,7 @@ export interface Session<S extends BuilderState> {
442
502
  };
443
503
  memory: SessionMemory;
444
504
  history: ConversationHistory;
505
+ getRunSummary(runId: string): Promise<RunSummary | undefined>;
445
506
  clearHistory(): Promise<void>;
446
507
  replaceHistory(messages: ReadonlyArray<Omit<Message, 'id' | 'timestamp'>>): Promise<void>;
447
508
  close(): Promise<void>;
@@ -454,6 +515,18 @@ export interface SerializedError {
454
515
  message: string;
455
516
  meta?: Record<string, unknown>;
456
517
  }
518
+ export interface RunSummary {
519
+ runId: string;
520
+ sessionId: string;
521
+ status: RunStatus;
522
+ startedAt: string;
523
+ finishedAt?: string;
524
+ tokenTotals: TokenUsage;
525
+ modelCalls: number;
526
+ toolCalls: number;
527
+ agentCalls: number;
528
+ error?: SerializedError;
529
+ }
457
530
  /** Harness streaming events emitted from `session.workflows.<id>.stream(...)`. */
458
531
  export type RunEvent = {
459
532
  type: 'run.started';
@@ -512,6 +585,7 @@ export type RunEvent = {
512
585
  runId: string;
513
586
  agentId?: string;
514
587
  object: JsonValue;
588
+ usage?: TokenUsage;
515
589
  } | {
516
590
  type: 'model.embedding.completed';
517
591
  runId: string;
@@ -538,7 +612,9 @@ export interface HarnessBuilder<S extends BuilderState = {}> {
538
612
  logger(logger: Logger): HarnessBuilder<S>;
539
613
  state(store: StateStore): HarnessBuilder<S>;
540
614
  sandbox(sandbox?: Sandbox<any>): HarnessBuilder<S>;
615
+ memory(adapter: MemoryAdapter): HarnessBuilder<S>;
541
616
  runtime(runtime: DurableRuntimeAdapter): HarnessBuilder<S>;
617
+ workspaceStore(store: DurableWorkspaceStore): HarnessBuilder<S>;
542
618
  requires(capabilities: readonly AdapterCapability[]): HarnessBuilder<S>;
543
619
  defaults(defaults: HarnessDefaults): HarnessBuilder<S>;
544
620
  models<const M extends ModelsConfig>(models: M): HarnessBuilder<S & {
@@ -1,5 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import { JsonLogger } from '../logger/index.js';
3
+ import { sandboxMemory } from '../memory/sandbox/index.js';
4
+ import { validateMemoryAdapter } from '../ports/memory.js';
5
+ import { validateDurableWorkspaceStore } from '../ports/workspace.js';
3
6
  import { InMemoryStateStore } from '../state/in-memory.js';
4
7
  import { HarnessConfigError } from '../errors/catalog.js';
5
8
  import { autoDetectSandbox } from '../sandbox/index.js';
@@ -26,9 +29,23 @@ class Builder {
26
29
  sandbox(sandbox = autoDetectSandbox()) {
27
30
  return this.clone({ sandbox });
28
31
  }
32
+ memory(memory) {
33
+ if (this.configured.memory) {
34
+ throw new HarnessConfigError('Memory adapter is already configured.', { reason: 'duplicate_adapter', path: 'memory' });
35
+ }
36
+ validateMemoryAdapter(memory);
37
+ return this.clone({ memory });
38
+ }
29
39
  runtime(runtime) {
30
40
  return this.clone({ runtime });
31
41
  }
42
+ workspaceStore(workspaceStore) {
43
+ if (this.configured.workspaceStore) {
44
+ throw new HarnessConfigError('Workspace store is already configured.', { reason: 'duplicate_adapter', path: 'workspaceStore' });
45
+ }
46
+ validateDurableWorkspaceStore(workspaceStore);
47
+ return this.clone({ workspaceStore });
48
+ }
32
49
  requires(capabilities) {
33
50
  return this.clone({ requiredCapabilities: uniqueCapabilities(capabilities) });
34
51
  }
@@ -54,6 +71,7 @@ class Builder {
54
71
  const resolved = typeof agents === 'function'
55
72
  ? agents({ agent: (definition) => definition })
56
73
  : agents;
74
+ this.validateAgentSkillReferences(resolved);
57
75
  return this.clone({ agents: resolved });
58
76
  }
59
77
  workflows(workflows) {
@@ -68,7 +86,11 @@ class Builder {
68
86
  throw new HarnessConfigError('At least one model alias is required.', { reason: 'missing_models', path: 'models' });
69
87
  }
70
88
  const sandbox = this.configured.sandbox ?? autoDetectSandbox();
71
- const inspection = this.resolveInspection(this.options.name ?? 'agent-harness', sandbox, models);
89
+ const memory = this.configured.memory ?? sandboxMemory();
90
+ validateMemoryAdapter(memory);
91
+ if (this.configured.workspaceStore)
92
+ validateDurableWorkspaceStore(this.configured.workspaceStore);
93
+ const inspection = this.resolveInspection(this.options.name ?? 'agent-harness', sandbox, memory, models);
72
94
  const missing = missingCapabilities(inspection.requiredCapabilities, inspection.capabilities);
73
95
  if (missing.length > 0) {
74
96
  throw new HarnessConfigError('Required adapter capabilities are not available.', {
@@ -83,6 +105,7 @@ class Builder {
83
105
  ...(this.configured.telemetry ? { telemetry: this.configured.telemetry } : {}),
84
106
  state: this.configured.state ?? new InMemoryStateStore(),
85
107
  sandbox,
108
+ memory,
86
109
  defaults: {
87
110
  agentMaxIterations: this.configured.defaults?.agentMaxIterations ?? 16,
88
111
  runTimeoutMs: this.configured.defaults?.runTimeoutMs ?? 600_000,
@@ -103,7 +126,21 @@ class Builder {
103
126
  clone(patch) {
104
127
  return new Builder(this.options, { ...this.configured, ...patch });
105
128
  }
106
- resolveInspection(name, sandbox, models) {
129
+ validateAgentSkillReferences(agents) {
130
+ const configuredSkills = new Set(Object.keys(this.configured.skills ?? {}));
131
+ for (const [agentId, agent] of Object.entries(agents)) {
132
+ for (const skillId of agent.skills ?? []) {
133
+ if (!configuredSkills.has(skillId)) {
134
+ throw new HarnessConfigError('Agent references an unknown skill.', {
135
+ reason: 'invalid_agent',
136
+ path: `agents.${agentId}.skills`,
137
+ id: skillId
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ resolveInspection(name, sandbox, memory, models) {
107
144
  const adapters = [];
108
145
  const sandboxCapabilities = hasAdapterCapabilities(sandbox) ? uniqueCapabilities(sandbox.capabilities) : [];
109
146
  adapters.push({
@@ -111,6 +148,15 @@ class Builder {
111
148
  id: getAdapterId(sandbox, 'sandbox'),
112
149
  capabilities: sandboxCapabilities
113
150
  });
151
+ adapters.push({
152
+ kind: 'memory',
153
+ id: memory.info.id,
154
+ capabilities: uniqueCapabilities(memory.info.capabilities),
155
+ metadata: {
156
+ packageName: memory.info.packageName,
157
+ ...(memory.info.version ? { version: memory.info.version } : {})
158
+ }
159
+ });
114
160
  if (this.configured.runtime) {
115
161
  adapters.push({
116
162
  kind: 'runtime',
@@ -118,6 +164,17 @@ class Builder {
118
164
  capabilities: uniqueCapabilities(this.configured.runtime.capabilities)
119
165
  });
120
166
  }
167
+ if (this.configured.workspaceStore) {
168
+ adapters.push({
169
+ kind: 'workspace_store',
170
+ id: this.configured.workspaceStore.info.id,
171
+ capabilities: uniqueCapabilities(this.configured.workspaceStore.info.capabilities),
172
+ metadata: {
173
+ packageName: this.configured.workspaceStore.info.packageName,
174
+ policy: this.configured.workspaceStore.info.policy
175
+ }
176
+ });
177
+ }
121
178
  for (const [alias, model] of Object.entries(models)) {
122
179
  adapters.push({
123
180
  kind: 'model',
package/dist/index.d.ts CHANGED
@@ -9,6 +9,10 @@ export * from './state/in-memory.js';
9
9
  export * from './models/json.js';
10
10
  export type { SessionRecord, Message, RunRecord, PersistedRunEvent, RunStatus } from './models/state.js';
11
11
  export * from './models/registry.js';
12
+ export * from './eval/index.js';
13
+ export * from './memory/sandbox/index.js';
14
+ export * from './skills/index.js';
12
15
  export * from './sandbox/index.js';
16
+ export * from './workspace/index.js';
13
17
  export * from './tools/mcp/index.js';
14
18
  export * from './harness/defineHarness.js';
package/dist/index.js CHANGED
@@ -7,6 +7,10 @@ export { createDurableWorkflowContext, DurableStepError, DurableRunLeaseError, D
7
7
  export * from './state/in-memory.js';
8
8
  export * from './models/json.js';
9
9
  export * from './models/registry.js';
10
+ export * from './eval/index.js';
11
+ export * from './memory/sandbox/index.js';
12
+ export * from './skills/index.js';
10
13
  export * from './sandbox/index.js';
14
+ export * from './workspace/index.js';
11
15
  export * from './tools/mcp/index.js';
12
16
  export * from './harness/defineHarness.js';
@@ -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
  *
@@ -25,15 +25,59 @@ export type AdapterCapability =
25
25
  | 'runtime.distributed_lock'
26
26
  /** Runtime can resume from committed checkpoints. */
27
27
  | 'runtime.resume_from_checkpoint'
28
+ /** Runtime checkpoint records can carry durable workspace references. */
29
+ | 'runtime.workspace_checkpoint'
30
+ /** Runtime exposes checkpoint retention and expiry metadata. */
31
+ | 'runtime.checkpoint_retention'
32
+ /** Workspace store persists state beyond process exit. */
33
+ | 'workspace_store.durable'
34
+ /** Workspace store can produce stable checkpoints. */
35
+ | 'workspace_store.checkpoint'
36
+ /** Workspace store can resume committed checkpoints. */
37
+ | 'workspace_store.resume'
38
+ /** Workspace store can abort active or paused workspaces. */
39
+ | 'workspace_store.abort'
40
+ /** Workspace store supports idempotent cleanup. */
41
+ | 'workspace_store.cleanup'
42
+ /** Workspace store supports read-only inspection. */
43
+ | 'workspace_store.inspect'
44
+ /** Workspace store exposes retention policy and expiry metadata. */
45
+ | 'workspace_store.retention'
46
+ /** Workspace store enforces and reports quota policy. */
47
+ | 'workspace_store.quota'
48
+ /** Workspace store encrypts checkpoint, snapshot, file, and metadata storage. */
49
+ | 'workspace_store.encrypted_storage'
28
50
  /** Adapter can record feedback. */
29
- | 'feedback.record';
51
+ | 'feedback.record'
52
+ /** Memory adapter supports key/value reads and writes. */
53
+ | 'memory.kv'
54
+ /** Memory adapter supports key listing. */
55
+ | 'memory.list'
56
+ /** Memory adapter supports key deletion. */
57
+ | 'memory.delete'
58
+ /** Memory adapter supports text search over stored memory. */
59
+ | 'memory.search'
60
+ /** Memory adapter supports entry expiration. */
61
+ | 'memory.ttl'
62
+ /** Memory adapter supports run-scoped memory. */
63
+ | 'memory.run'
64
+ /** Memory adapter supports session-scoped memory. */
65
+ | 'memory.session'
66
+ /** Memory adapter supports agent-scoped memory. */
67
+ | 'memory.agent'
68
+ /** Memory adapter supports user-scoped memory. */
69
+ | 'memory.user'
70
+ /** Memory adapter supports tenant-scoped memory. */
71
+ | 'memory.tenant'
72
+ /** Memory survives adapter close/reopen for the same logical scope. */
73
+ | 'memory.persistent';
30
74
  /** Data-only descriptor implemented by adapters that expose capability metadata. */
31
75
  export interface AdapterCapabilities {
32
76
  readonly capabilities: readonly AdapterCapability[];
33
77
  }
34
78
  /** Adapter descriptor surfaced through `harness.inspect()`. */
35
79
  export interface AdapterInspection {
36
- readonly kind: 'state' | 'sandbox' | 'runtime' | 'feedback' | 'model';
80
+ readonly kind: 'state' | 'sandbox' | 'runtime' | 'workspace_store' | 'feedback' | 'model' | 'memory';
37
81
  readonly id: string;
38
82
  readonly capabilities: readonly AdapterCapability[];
39
83
  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,5 @@ 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';
8
+ export * from './workspace.js';
@@ -4,3 +4,5 @@ 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';
8
+ export * from './workspace.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;