@purista/harness 1.0.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/LICENSE +201 -0
- package/README.md +23 -0
- package/dist/agents/index.d.ts +34 -0
- package/dist/agents/index.js +301 -0
- package/dist/errors/catalog.d.ts +185 -0
- package/dist/errors/catalog.js +144 -0
- package/dist/errors/harness-error.d.ts +64 -0
- package/dist/errors/harness-error.js +58 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/redaction.d.ts +5 -0
- package/dist/errors/redaction.js +64 -0
- package/dist/harness/defineHarness.d.ts +640 -0
- package/dist/harness/defineHarness.js +176 -0
- package/dist/harness/errors.d.ts +62 -0
- package/dist/harness/errors.js +67 -0
- package/dist/harness/types.d.ts +27 -0
- package/dist/harness/types.js +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/logger/index.d.ts +2 -0
- package/dist/logger/index.js +2 -0
- package/dist/logger/json-logger.d.ts +31 -0
- package/dist/logger/json-logger.js +65 -0
- package/dist/logger/logger.d.ts +31 -0
- package/dist/logger/logger.js +1 -0
- package/dist/models/json.d.ts +6 -0
- package/dist/models/json.js +1 -0
- package/dist/models/registry.d.ts +112 -0
- package/dist/models/registry.js +286 -0
- package/dist/models/state.d.ts +64 -0
- package/dist/models/state.js +1 -0
- package/dist/ports/base-model-provider.d.ts +56 -0
- package/dist/ports/base-model-provider.js +343 -0
- package/dist/ports/capabilities.d.ts +70 -0
- package/dist/ports/capabilities.js +38 -0
- package/dist/ports/feedback.d.ts +29 -0
- package/dist/ports/feedback.js +1 -0
- package/dist/ports/harness-context.d.ts +20 -0
- package/dist/ports/harness-context.js +1 -0
- package/dist/ports/index.d.ts +6 -0
- package/dist/ports/index.js +6 -0
- package/dist/ports/model-provider.d.ts +280 -0
- package/dist/ports/model-provider.js +1 -0
- package/dist/ports/state.d.ts +72 -0
- package/dist/ports/state.js +24 -0
- package/dist/runtime/durable.d.ts +134 -0
- package/dist/runtime/durable.js +185 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/steps.d.ts +22 -0
- package/dist/runtime/steps.js +51 -0
- package/dist/sandbox/index.d.ts +111 -0
- package/dist/sandbox/index.js +165 -0
- package/dist/sessions/index.d.ts +23 -0
- package/dist/sessions/index.js +718 -0
- package/dist/skills/index.d.ts +8 -0
- package/dist/skills/index.js +88 -0
- package/dist/state/in-memory.d.ts +35 -0
- package/dist/state/in-memory.js +140 -0
- package/dist/telemetry/index.d.ts +1 -0
- package/dist/telemetry/index.js +1 -0
- package/dist/telemetry/shim.d.ts +26 -0
- package/dist/telemetry/shim.js +120 -0
- package/dist/testing/capabilities.d.ts +11 -0
- package/dist/testing/capabilities.js +20 -0
- package/dist/testing/fakeModelProvider.d.ts +25 -0
- package/dist/testing/fakeModelProvider.js +79 -0
- package/dist/testing/feedback.d.ts +10 -0
- package/dist/testing/feedback.js +24 -0
- package/dist/testing/fixtures/mcp/fake-http-server.d.ts +8 -0
- package/dist/testing/fixtures/mcp/fake-http-server.js +95 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/sandboxContract.d.ts +4 -0
- package/dist/testing/sandboxContract.js +74 -0
- package/dist/testing/sandboxSnapshot.d.ts +7 -0
- package/dist/testing/sandboxSnapshot.js +201 -0
- package/dist/testing/stateStoreContract.d.ts +2 -0
- package/dist/testing/stateStoreContract.js +109 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +123 -0
- package/dist/tools/mcp/http.d.ts +2 -0
- package/dist/tools/mcp/http.js +109 -0
- package/dist/tools/mcp/index.d.ts +2 -0
- package/dist/tools/mcp/index.js +2 -0
- package/dist/tools/mcp/runner.d.ts +74 -0
- package/dist/tools/mcp/runner.js +238 -0
- package/dist/tools/mcp/schema.d.ts +41 -0
- package/dist/tools/mcp/schema.js +251 -0
- package/dist/tools/mcp/stdio.d.ts +2 -0
- package/dist/tools/mcp/stdio.js +122 -0
- package/dist/ulid/index.d.ts +6 -0
- package/dist/ulid/index.js +35 -0
- package/dist/workflows/index.d.ts +8 -0
- package/dist/workflows/index.js +26 -0
- package/package.json +75 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { ModelCapabilityError, ModelError, OperationCancelledError, OperationTimeoutError, HarnessError, sanitizeForLog, sanitizeProviderBody, sanitizeProviderMessage } from '../errors/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Base class for model adapters.
|
|
4
|
+
*
|
|
5
|
+
* Adapter packages should map provider-specific requests/responses in protected
|
|
6
|
+
* `do*` methods. The base class owns cross-cutting harness behavior:
|
|
7
|
+
* cancellation, timeout, safe logs, metrics/spans, and error normalization.
|
|
8
|
+
*/
|
|
9
|
+
export class BaseModelProvider {
|
|
10
|
+
id;
|
|
11
|
+
genAiSystem;
|
|
12
|
+
logger;
|
|
13
|
+
telemetry;
|
|
14
|
+
timeoutMs;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.id = options.id;
|
|
17
|
+
this.genAiSystem = options.genAiSystem;
|
|
18
|
+
this.logger = options.logger;
|
|
19
|
+
this.telemetry = options.telemetry;
|
|
20
|
+
this.timeoutMs = options.timeoutMs;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Called by the harness during composition so adapters automatically inherit
|
|
24
|
+
* harness-level logging, telemetry, and timeout defaults. Explicit adapter
|
|
25
|
+
* constructor options win over inherited values.
|
|
26
|
+
*/
|
|
27
|
+
configureHarnessContext(context) {
|
|
28
|
+
this.logger ??= context.logger;
|
|
29
|
+
this.telemetry ??= context.telemetry;
|
|
30
|
+
if (this.timeoutMs === undefined) {
|
|
31
|
+
this.timeoutMs = context.defaults.modelTimeoutMs;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
text(req) {
|
|
35
|
+
return this.call('text', req, (next) => this.doText(next));
|
|
36
|
+
}
|
|
37
|
+
textStream(req) {
|
|
38
|
+
return this.stream('textStream', req, (next) => this.doTextStream(next));
|
|
39
|
+
}
|
|
40
|
+
object(req) {
|
|
41
|
+
return this.call('object', req, (next) => this.doObject(next));
|
|
42
|
+
}
|
|
43
|
+
objectStream(req) {
|
|
44
|
+
return this.stream('objectStream', req, (next) => this.doObjectStream(next));
|
|
45
|
+
}
|
|
46
|
+
embed(req) {
|
|
47
|
+
return this.call('embed', req, (next) => this.doEmbed(next));
|
|
48
|
+
}
|
|
49
|
+
rerank(req) {
|
|
50
|
+
return this.call('rerank', req, (next) => this.doRerank(next));
|
|
51
|
+
}
|
|
52
|
+
doText(_req) {
|
|
53
|
+
throw this.methodMissing('text');
|
|
54
|
+
}
|
|
55
|
+
doTextStream(_req) {
|
|
56
|
+
throw this.methodMissing('textStream');
|
|
57
|
+
}
|
|
58
|
+
doObject(_req) {
|
|
59
|
+
throw this.methodMissing('object');
|
|
60
|
+
}
|
|
61
|
+
doObjectStream(_req) {
|
|
62
|
+
throw this.methodMissing('objectStream');
|
|
63
|
+
}
|
|
64
|
+
doEmbed(_req) {
|
|
65
|
+
throw this.methodMissing('embed');
|
|
66
|
+
}
|
|
67
|
+
doRerank(_req) {
|
|
68
|
+
throw this.methodMissing('rerank');
|
|
69
|
+
}
|
|
70
|
+
normalizeError(error, method, req) {
|
|
71
|
+
if (error instanceof HarnessError)
|
|
72
|
+
return error;
|
|
73
|
+
if (req.signal.aborted || isAbortError(error)) {
|
|
74
|
+
return new OperationCancelledError('Model call was cancelled.', { scope: 'model' }, error);
|
|
75
|
+
}
|
|
76
|
+
const details = extractProviderErrorDetails(error);
|
|
77
|
+
const status = details.status;
|
|
78
|
+
const code = details.providerCode;
|
|
79
|
+
const reason = code === 'context_length_exceeded'
|
|
80
|
+
? 'context_length_exceeded'
|
|
81
|
+
: status !== undefined
|
|
82
|
+
? 'http_error'
|
|
83
|
+
: 'network';
|
|
84
|
+
return new ModelError(modelErrorMessage(details), {
|
|
85
|
+
provider: this.id,
|
|
86
|
+
model: req.model,
|
|
87
|
+
method,
|
|
88
|
+
...(status !== undefined ? { status } : {}),
|
|
89
|
+
...details,
|
|
90
|
+
reason
|
|
91
|
+
}, error);
|
|
92
|
+
}
|
|
93
|
+
async call(method, req, fn) {
|
|
94
|
+
req.signal.throwIfAborted();
|
|
95
|
+
const attrs = this.attrs(method, req);
|
|
96
|
+
const started = Date.now();
|
|
97
|
+
const execute = async (span) => {
|
|
98
|
+
const next = this.withTimeout(req, method);
|
|
99
|
+
try {
|
|
100
|
+
const operation = fn(next.req);
|
|
101
|
+
const result = await (next.timeoutPromise ? Promise.race([operation, next.timeoutPromise]) : operation);
|
|
102
|
+
this.telemetry?.recordHistogram('harness.model.duration', (Date.now() - started) / 1000, attrs);
|
|
103
|
+
this.recordUsage(method, req.model, result);
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const normalized = this.normalizeError(error, method, next.req);
|
|
108
|
+
span?.setAttributes?.(modelErrorTelemetryAttrs(normalized));
|
|
109
|
+
this.telemetry?.recordCounter('harness.model.errors', 1, { ...attrs, 'error.code': normalized.code });
|
|
110
|
+
this.logger?.error('Model provider call failed.', {
|
|
111
|
+
provider: this.id,
|
|
112
|
+
model: req.model,
|
|
113
|
+
method,
|
|
114
|
+
error: sanitizeForLog({ code: normalized.code, category: normalized.category, retriable: normalized.retriable, meta: normalized.meta })
|
|
115
|
+
});
|
|
116
|
+
throw normalized;
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
next.cleanup();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return this.telemetry ? this.telemetry.span(`harness.model.${method}`, attrs, execute) : execute();
|
|
123
|
+
}
|
|
124
|
+
stream(method, req, fn) {
|
|
125
|
+
req.signal.throwIfAborted();
|
|
126
|
+
const attrs = this.attrs(method, req);
|
|
127
|
+
const started = Date.now();
|
|
128
|
+
const iterate = async function* (span) {
|
|
129
|
+
const next = this.withTimeout(req, method);
|
|
130
|
+
try {
|
|
131
|
+
for await (const chunk of fn(next.req)) {
|
|
132
|
+
next.req.signal.throwIfAborted();
|
|
133
|
+
yield chunk;
|
|
134
|
+
}
|
|
135
|
+
this.telemetry?.recordHistogram('harness.model.duration', (Date.now() - started) / 1000, attrs);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const normalized = this.normalizeError(error, method, next.req);
|
|
139
|
+
span?.setAttributes?.(modelErrorTelemetryAttrs(normalized));
|
|
140
|
+
this.telemetry?.recordCounter('harness.model.errors', 1, { ...attrs, 'error.code': normalized.code });
|
|
141
|
+
this.logger?.error('Model provider stream failed.', {
|
|
142
|
+
provider: this.id,
|
|
143
|
+
model: req.model,
|
|
144
|
+
method,
|
|
145
|
+
error: sanitizeForLog({ code: normalized.code, category: normalized.category, retriable: normalized.retriable, meta: normalized.meta })
|
|
146
|
+
});
|
|
147
|
+
throw normalized;
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
next.cleanup();
|
|
151
|
+
}
|
|
152
|
+
}.bind(this);
|
|
153
|
+
if (!this.telemetry)
|
|
154
|
+
return iterate();
|
|
155
|
+
return streamWithSpan(this.telemetry, `harness.model.${method}`, attrs, iterate);
|
|
156
|
+
}
|
|
157
|
+
withTimeout(req, method) {
|
|
158
|
+
if (!this.timeoutMs || this.timeoutMs <= 0) {
|
|
159
|
+
return { req, cleanup: () => undefined };
|
|
160
|
+
}
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
const relay = () => controller.abort(req.signal.reason);
|
|
163
|
+
req.signal.addEventListener('abort', relay, { once: true });
|
|
164
|
+
let rejectTimeout;
|
|
165
|
+
const timeoutPromise = new Promise((_, reject) => { rejectTimeout = reject; });
|
|
166
|
+
const timeout = setTimeout(() => {
|
|
167
|
+
const error = new OperationTimeoutError('Model call timed out.', { scope: 'model', timeout_ms: this.timeoutMs });
|
|
168
|
+
controller.abort(error);
|
|
169
|
+
rejectTimeout?.(error);
|
|
170
|
+
}, this.timeoutMs);
|
|
171
|
+
return {
|
|
172
|
+
req: { ...req, signal: controller.signal },
|
|
173
|
+
timeoutPromise,
|
|
174
|
+
cleanup: () => {
|
|
175
|
+
clearTimeout(timeout);
|
|
176
|
+
req.signal.removeEventListener('abort', relay);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
methodMissing(method) {
|
|
181
|
+
return new ModelCapabilityError('Model provider method is not implemented.', {
|
|
182
|
+
alias: this.id,
|
|
183
|
+
method,
|
|
184
|
+
reason: 'method_missing'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
attrs(method, req) {
|
|
188
|
+
return {
|
|
189
|
+
'gen_ai.system': this.genAiSystem,
|
|
190
|
+
'gen_ai.request.model': req.model,
|
|
191
|
+
'model.provider': this.id,
|
|
192
|
+
'model.method': method
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
recordUsage(method, model, result) {
|
|
196
|
+
const usage = result.usage;
|
|
197
|
+
if (!usage)
|
|
198
|
+
return;
|
|
199
|
+
const attrs = { 'gen_ai.system': this.genAiSystem, 'gen_ai.request.model': model, 'model.provider': this.id, 'model.method': method };
|
|
200
|
+
this.telemetry?.recordCounter('harness.model.tokens.input', usage.inputTokens, attrs);
|
|
201
|
+
this.telemetry?.recordCounter('harness.model.tokens.output', usage.outputTokens, attrs);
|
|
202
|
+
this.telemetry?.recordCounter('harness.model.tokens.total', usage.totalTokens, attrs);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function* streamWithSpan(telemetry, name, attrs, iterate) {
|
|
206
|
+
const queue = [];
|
|
207
|
+
let done = false;
|
|
208
|
+
let failure;
|
|
209
|
+
let notify;
|
|
210
|
+
const wake = () => {
|
|
211
|
+
notify?.();
|
|
212
|
+
notify = undefined;
|
|
213
|
+
};
|
|
214
|
+
const producer = telemetry.span(name, attrs, async (span) => {
|
|
215
|
+
for await (const chunk of iterate(span)) {
|
|
216
|
+
queue.push(chunk);
|
|
217
|
+
wake();
|
|
218
|
+
}
|
|
219
|
+
}).catch((error) => {
|
|
220
|
+
failure = error;
|
|
221
|
+
}).finally(() => {
|
|
222
|
+
done = true;
|
|
223
|
+
wake();
|
|
224
|
+
});
|
|
225
|
+
while (!done || queue.length > 0) {
|
|
226
|
+
const next = queue.shift();
|
|
227
|
+
if (next !== undefined) {
|
|
228
|
+
yield next;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (failure)
|
|
232
|
+
throw failure;
|
|
233
|
+
await new Promise((resolve) => { notify = resolve; });
|
|
234
|
+
}
|
|
235
|
+
await producer;
|
|
236
|
+
if (failure)
|
|
237
|
+
throw failure;
|
|
238
|
+
}
|
|
239
|
+
function isAbortError(error) {
|
|
240
|
+
const value = error;
|
|
241
|
+
return value?.name === 'AbortError' || value?.code === 'ABORT_ERR';
|
|
242
|
+
}
|
|
243
|
+
function modelErrorMessage(details) {
|
|
244
|
+
const parts = ['Model provider call failed'];
|
|
245
|
+
const qualifiers = [
|
|
246
|
+
details.status !== undefined ? `HTTP ${details.status}` : undefined,
|
|
247
|
+
details.providerCode,
|
|
248
|
+
details.providerType,
|
|
249
|
+
details.providerParam
|
|
250
|
+
].filter((part) => Boolean(part));
|
|
251
|
+
if (qualifiers.length > 0)
|
|
252
|
+
parts.push(`(${qualifiers.join(', ')})`);
|
|
253
|
+
if (details.providerMessage)
|
|
254
|
+
parts.push(`: ${details.providerMessage.slice(0, 500)}`);
|
|
255
|
+
return `${parts.join('')}.`;
|
|
256
|
+
}
|
|
257
|
+
function modelErrorTelemetryAttrs(error) {
|
|
258
|
+
const meta = asRecord(error.meta);
|
|
259
|
+
return {
|
|
260
|
+
'harness.error.provider': stringTelemetryAttr(meta?.['provider']),
|
|
261
|
+
'harness.error.model': stringTelemetryAttr(meta?.['model']),
|
|
262
|
+
'harness.error.model_provider_status': numberTelemetryAttr(meta?.['status']),
|
|
263
|
+
'harness.error.model_provider_code': stringTelemetryAttr(meta?.['providerCode']),
|
|
264
|
+
'harness.error.model_provider_type': stringTelemetryAttr(meta?.['providerType']),
|
|
265
|
+
'harness.error.model_provider_param': stringTelemetryAttr(meta?.['providerParam']),
|
|
266
|
+
'harness.error.model_provider_request_id': stringTelemetryAttr(meta?.['providerRequestId']),
|
|
267
|
+
'harness.error.model_provider_message': stringTelemetryAttr(meta?.['providerMessage']),
|
|
268
|
+
'harness.error.model_provider_body': jsonTelemetryAttr(meta?.['providerBody'])
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function stringTelemetryAttr(value) {
|
|
272
|
+
return typeof value === 'string' && value.length > 0 ? value.slice(0, 4000) : undefined;
|
|
273
|
+
}
|
|
274
|
+
function numberTelemetryAttr(value) {
|
|
275
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
276
|
+
}
|
|
277
|
+
function jsonTelemetryAttr(value) {
|
|
278
|
+
if (value === undefined)
|
|
279
|
+
return undefined;
|
|
280
|
+
try {
|
|
281
|
+
return JSON.stringify(value).slice(0, 8000);
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function extractProviderErrorDetails(error) {
|
|
288
|
+
const record = asRecord(error);
|
|
289
|
+
if (!record)
|
|
290
|
+
return {};
|
|
291
|
+
const response = asRecord(record['response']);
|
|
292
|
+
const errorBody = asRecord(record['error']);
|
|
293
|
+
const headers = normalizeHeaders(record['headers'] ?? response?.['headers']);
|
|
294
|
+
const providerBody = sanitizeJsonLike(record['body'] ?? response?.['body'] ?? response?.['data'] ?? record['error']);
|
|
295
|
+
const status = numberField(record, 'status')
|
|
296
|
+
?? numberField(record, 'statusCode')
|
|
297
|
+
?? numberField(response, 'status')
|
|
298
|
+
?? numberField(response, 'statusCode');
|
|
299
|
+
const providerCode = stringField(record, 'code') ?? stringField(errorBody, 'code');
|
|
300
|
+
const providerType = stringField(record, 'type') ?? stringField(errorBody, 'type');
|
|
301
|
+
const providerParam = stringField(record, 'param') ?? stringField(errorBody, 'param');
|
|
302
|
+
const providerRequestId = stringField(record, 'request_id')
|
|
303
|
+
?? stringField(record, 'requestID')
|
|
304
|
+
?? headers?.['x-request-id']
|
|
305
|
+
?? headers?.['request-id'];
|
|
306
|
+
const providerMessageRaw = stringField(errorBody, 'message') ?? stringField(record, 'message');
|
|
307
|
+
const providerMessage = providerMessageRaw ? sanitizeProviderMessage(providerMessageRaw) : undefined;
|
|
308
|
+
return {
|
|
309
|
+
...(status !== undefined ? { status } : {}),
|
|
310
|
+
...(providerCode ? { providerCode } : {}),
|
|
311
|
+
...(providerType ? { providerType } : {}),
|
|
312
|
+
...(providerParam ? { providerParam } : {}),
|
|
313
|
+
...(providerRequestId ? { providerRequestId } : {}),
|
|
314
|
+
...(providerMessage ? { providerMessage } : {}),
|
|
315
|
+
...(providerBody !== undefined ? { providerBody } : {})
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function asRecord(value) {
|
|
319
|
+
return value !== null && typeof value === 'object' ? value : undefined;
|
|
320
|
+
}
|
|
321
|
+
function stringField(record, key) {
|
|
322
|
+
const value = record?.[key];
|
|
323
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
324
|
+
}
|
|
325
|
+
function numberField(record, key) {
|
|
326
|
+
const value = record?.[key];
|
|
327
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
328
|
+
}
|
|
329
|
+
function normalizeHeaders(value) {
|
|
330
|
+
const record = asRecord(value);
|
|
331
|
+
if (!record)
|
|
332
|
+
return undefined;
|
|
333
|
+
const headers = {};
|
|
334
|
+
for (const [key, headerValue] of Object.entries(record)) {
|
|
335
|
+
if (typeof headerValue === 'string' || typeof headerValue === 'number' || typeof headerValue === 'boolean') {
|
|
336
|
+
headers[key.toLowerCase()] = String(headerValue).slice(0, 2000);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return Object.keys(headers).length > 0 ? headers : undefined;
|
|
340
|
+
}
|
|
341
|
+
function sanitizeJsonLike(value) {
|
|
342
|
+
return sanitizeProviderBody(value);
|
|
343
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable capability ids declared by non-model harness adapters.
|
|
3
|
+
*
|
|
4
|
+
* Adapter capabilities describe setup-level behavior the harness may rely on.
|
|
5
|
+
* They are separate from `ModelCapability`, which describes model operations.
|
|
6
|
+
*/
|
|
7
|
+
export type AdapterCapability =
|
|
8
|
+
/** Sandbox supports filesystem access. */
|
|
9
|
+
'sandbox.fs'
|
|
10
|
+
/** Sandbox supports command execution. */
|
|
11
|
+
| 'sandbox.exec'
|
|
12
|
+
/** Sandbox filesystem survives adapter-level close/reopen for the same session. */
|
|
13
|
+
| 'sandbox.persistent_fs'
|
|
14
|
+
/** Sandbox can create durable snapshots. */
|
|
15
|
+
| 'sandbox.snapshot'
|
|
16
|
+
/** Sandbox can resume a session from a durable snapshot. */
|
|
17
|
+
| 'sandbox.resume'
|
|
18
|
+
/** Sandbox can snapshot and release active compute. */
|
|
19
|
+
| 'sandbox.hibernate'
|
|
20
|
+
/** Runtime can commit stable checkpoints. */
|
|
21
|
+
| 'runtime.checkpoint'
|
|
22
|
+
/** Runtime can retry durable boundaries. */
|
|
23
|
+
| 'runtime.retry'
|
|
24
|
+
/** Runtime can coordinate distributed session/run ownership. */
|
|
25
|
+
| 'runtime.distributed_lock'
|
|
26
|
+
/** Runtime can resume from committed checkpoints. */
|
|
27
|
+
| 'runtime.resume_from_checkpoint'
|
|
28
|
+
/** Adapter can record feedback. */
|
|
29
|
+
| 'feedback.record';
|
|
30
|
+
/** Data-only descriptor implemented by adapters that expose capability metadata. */
|
|
31
|
+
export interface AdapterCapabilities {
|
|
32
|
+
readonly capabilities: readonly AdapterCapability[];
|
|
33
|
+
}
|
|
34
|
+
/** Adapter descriptor surfaced through `harness.inspect()`. */
|
|
35
|
+
export interface AdapterInspection {
|
|
36
|
+
readonly kind: 'state' | 'sandbox' | 'runtime' | 'feedback' | 'model';
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly capabilities: readonly AdapterCapability[];
|
|
39
|
+
readonly metadata?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
/** Data-only snapshot of resolved harness setup. */
|
|
42
|
+
export interface HarnessInspection {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly capabilities: readonly AdapterCapability[];
|
|
45
|
+
readonly requiredCapabilities: readonly AdapterCapability[];
|
|
46
|
+
readonly adapters: readonly AdapterInspection[];
|
|
47
|
+
}
|
|
48
|
+
/** Optional durable runtime adapter surface for capability-gated setup. */
|
|
49
|
+
export interface DurableRuntimeAdapter extends AdapterCapabilities {
|
|
50
|
+
readonly id?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Result returned when comparing required and available adapter capabilities. */
|
|
53
|
+
export interface AdapterCapabilityValidation {
|
|
54
|
+
readonly required: readonly AdapterCapability[];
|
|
55
|
+
readonly available: readonly AdapterCapability[];
|
|
56
|
+
readonly missing: readonly AdapterCapability[];
|
|
57
|
+
readonly ok: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Returns `true` when a value exposes an adapter capability descriptor. */
|
|
60
|
+
export declare function hasAdapterCapabilities(value: unknown): value is AdapterCapabilities;
|
|
61
|
+
/** Deduplicates capabilities while preserving first-seen order. */
|
|
62
|
+
export declare function uniqueCapabilities(capabilities: readonly AdapterCapability[]): readonly AdapterCapability[];
|
|
63
|
+
/** Returns the required capabilities that are not present in `available`. */
|
|
64
|
+
export declare function missingCapabilities(required: readonly AdapterCapability[], available: readonly AdapterCapability[]): readonly AdapterCapability[];
|
|
65
|
+
/** Aggregates effective capabilities from adapter descriptors. */
|
|
66
|
+
export declare function collectAdapterCapabilities(adapters: readonly (AdapterCapabilities | undefined | null)[]): readonly AdapterCapability[];
|
|
67
|
+
/** Compares required capabilities with the currently available capability set. */
|
|
68
|
+
export declare function validateAdapterCapabilities(required: readonly AdapterCapability[], available: readonly AdapterCapability[]): AdapterCapabilityValidation;
|
|
69
|
+
/** Throws when required adapter capabilities are missing. */
|
|
70
|
+
export declare function assertAdapterCapabilities(required: readonly AdapterCapability[], available: readonly AdapterCapability[], message?: string): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Returns `true` when a value exposes an adapter capability descriptor. */
|
|
2
|
+
export function hasAdapterCapabilities(value) {
|
|
3
|
+
return Boolean(value
|
|
4
|
+
&& typeof value === 'object'
|
|
5
|
+
&& Array.isArray(value.capabilities));
|
|
6
|
+
}
|
|
7
|
+
/** Deduplicates capabilities while preserving first-seen order. */
|
|
8
|
+
export function uniqueCapabilities(capabilities) {
|
|
9
|
+
return [...new Set(capabilities)];
|
|
10
|
+
}
|
|
11
|
+
/** Returns the required capabilities that are not present in `available`. */
|
|
12
|
+
export function missingCapabilities(required, available) {
|
|
13
|
+
const availableSet = new Set(available);
|
|
14
|
+
return uniqueCapabilities(required).filter((capability) => !availableSet.has(capability));
|
|
15
|
+
}
|
|
16
|
+
/** Aggregates effective capabilities from adapter descriptors. */
|
|
17
|
+
export function collectAdapterCapabilities(adapters) {
|
|
18
|
+
return uniqueCapabilities(adapters.flatMap((adapter) => adapter?.capabilities ?? []));
|
|
19
|
+
}
|
|
20
|
+
/** Compares required capabilities with the currently available capability set. */
|
|
21
|
+
export function validateAdapterCapabilities(required, available) {
|
|
22
|
+
const requiredCapabilities = uniqueCapabilities(required);
|
|
23
|
+
const availableCapabilities = uniqueCapabilities(available);
|
|
24
|
+
const missing = missingCapabilities(requiredCapabilities, availableCapabilities);
|
|
25
|
+
return {
|
|
26
|
+
required: requiredCapabilities,
|
|
27
|
+
available: availableCapabilities,
|
|
28
|
+
missing,
|
|
29
|
+
ok: missing.length === 0
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Throws when required adapter capabilities are missing. */
|
|
33
|
+
export function assertAdapterCapabilities(required, available, message = 'Required adapter capabilities are not available.') {
|
|
34
|
+
const result = validateAdapterCapabilities(required, available);
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
throw new Error(`${message} Missing: ${result.missing.join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { JsonValue } from '../models/json.js';
|
|
2
|
+
/** Harness-native target that optional feedback can attach to. */
|
|
3
|
+
export type FeedbackTarget = {
|
|
4
|
+
kind: 'run';
|
|
5
|
+
runId: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'message';
|
|
8
|
+
sessionId: string;
|
|
9
|
+
messageId: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'tool_call';
|
|
12
|
+
runId: string;
|
|
13
|
+
callId: string;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'agent_invocation';
|
|
16
|
+
runId: string;
|
|
17
|
+
agentId: string;
|
|
18
|
+
};
|
|
19
|
+
/** Optional feedback signal associated with a harness-native target. */
|
|
20
|
+
export interface FeedbackRecord {
|
|
21
|
+
id: string;
|
|
22
|
+
target: FeedbackTarget;
|
|
23
|
+
source: 'user' | 'application' | 'deterministic_rule' | 'evaluator' | 'human_review';
|
|
24
|
+
label: string;
|
|
25
|
+
score?: number;
|
|
26
|
+
comment?: string;
|
|
27
|
+
metadata?: Record<string, JsonValue>;
|
|
28
|
+
createdAt: string;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Logger } from '../logger/index.js';
|
|
2
|
+
import type { TelemetryShim } from '../telemetry/index.js';
|
|
3
|
+
/** Harness-level context inherited by adapters registered with the harness. */
|
|
4
|
+
export interface HarnessAdapterContext {
|
|
5
|
+
harnessName: string;
|
|
6
|
+
logger: Logger;
|
|
7
|
+
telemetry: TelemetryShim;
|
|
8
|
+
defaults: {
|
|
9
|
+
agentMaxIterations: number;
|
|
10
|
+
runTimeoutMs: number;
|
|
11
|
+
toolTimeoutMs: number;
|
|
12
|
+
skillTimeoutMs: number;
|
|
13
|
+
modelTimeoutMs: number;
|
|
14
|
+
historyWindow?: number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Optional structural hook implemented by adapter base classes. */
|
|
18
|
+
export interface HarnessContextConfigurable {
|
|
19
|
+
configureHarnessContext(context: HarnessAdapterContext): void;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|