@openpond/harness 0.1.0 → 0.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.
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  export * from "./common.js";
2
+ export * from "./evaluation-review.js";
2
3
  export * from "./harness.js";
3
4
  export * from "./harness-improvements.js";
4
5
  export * from "./harness-workspaces.js";
5
6
  export * from "./models.js";
7
+ export * from "./refiner.js";
8
+ export * from "./refiner-detection.js";
9
+ export * from "./refiner-support.js";
6
10
  export * from "./tools.js";
@@ -0,0 +1,482 @@
1
+ import { createImprovementObservation, createRefinementTriggerDecision, } from "./harness-improvements.js";
2
+ import { contentHash } from "./common.js";
3
+ export const DEFAULT_REFINEMENT_TRIGGER_POLICY = {
4
+ schemaVersion: "openpond.refinementTriggerPolicy.v1",
5
+ maxEstimatedCostUsd: 0.05,
6
+ cooldownMs: 0,
7
+ maxPendingPlans: 2,
8
+ maxEvidenceEvents: 20,
9
+ maxProposalEdits: 4,
10
+ maxProposalBytes: 20_000,
11
+ };
12
+ export function detectHarnessImprovementAtBoundary(input) {
13
+ const policy = input.policy ?? DEFAULT_REFINEMENT_TRIGGER_POLICY;
14
+ const outcomes = normalizeToolOutcomes(input.events);
15
+ const observations = collectObservations({ ...input, outcomes });
16
+ const actionable = observations.filter(isActionableObservation);
17
+ const deduplicationKey = contentHash({
18
+ runRef: input.runRef,
19
+ boundary: input.boundary.kind,
20
+ observations: actionable.map((observation) => ({
21
+ kind: observation.kind,
22
+ deterministicClass: observation.deterministicClass,
23
+ tool: observation.tool,
24
+ state: observation.state,
25
+ eventRefs: observation.eventRefs,
26
+ })),
27
+ turnId: input.turnId,
28
+ });
29
+ const base = {
30
+ schemaVersion: "openpond.refinementTriggerDecision.v1",
31
+ id: stableId("refinement-trigger", {
32
+ turnId: input.turnId,
33
+ boundary: input.boundary,
34
+ deduplicationKey,
35
+ priorDeduplicationKeys: [...(input.priorDeduplicationKeys ?? [])].sort(),
36
+ pendingPlanCount: input.pendingPlanCount ?? 0,
37
+ cooldownUntil: input.cooldownUntil ?? null,
38
+ }),
39
+ runRef: input.runRef,
40
+ turnId: input.turnId,
41
+ harnessRelease: input.harnessRelease,
42
+ overlay: input.overlay,
43
+ observations: actionable
44
+ .slice(0, policy.maxEvidenceEvents)
45
+ .map((observation) => ({
46
+ id: observation.id,
47
+ contentHash: observation.contentHash,
48
+ })),
49
+ deduplicationKey,
50
+ policy,
51
+ pendingPlanCount: input.pendingPlanCount ?? 0,
52
+ boundary: input.boundary,
53
+ cooldownUntil: input.cooldownUntil ?? null,
54
+ createdAt: input.boundary.occurredAt,
55
+ metadata: {
56
+ loadedSkillNames: [...new Set(input.loadedSkillNames ?? [])].sort(),
57
+ },
58
+ };
59
+ if (actionable.length === 0) {
60
+ return {
61
+ observations,
62
+ trigger: createRefinementTriggerDecision({
63
+ ...base,
64
+ observations: [],
65
+ decision: "no_action",
66
+ deterministicRoute: null,
67
+ suggestedRoutes: [],
68
+ reason: observations.some((observation) => observation.state === "open")
69
+ ? "A tool failure is still open; wait for recovery or a terminal turn boundary."
70
+ : "The completed boundary contains no new reviewable turn or reusable detour.",
71
+ estimatedMaxCostUsd: 0,
72
+ }),
73
+ };
74
+ }
75
+ if (input.turnReviewAlreadyQueued) {
76
+ return {
77
+ observations,
78
+ trigger: createRefinementTriggerDecision({
79
+ ...base,
80
+ decision: "no_action",
81
+ deterministicRoute: null,
82
+ suggestedRoutes: [],
83
+ reason: "This turn already has a queued background Harness review.",
84
+ estimatedMaxCostUsd: 0,
85
+ }),
86
+ };
87
+ }
88
+ if (input.priorDeduplicationKeys?.has(deduplicationKey)) {
89
+ return {
90
+ observations,
91
+ trigger: createRefinementTriggerDecision({
92
+ ...base,
93
+ decision: "no_action",
94
+ deterministicRoute: null,
95
+ suggestedRoutes: [],
96
+ reason: "Equivalent improvement evidence was already routed for this run.",
97
+ estimatedMaxCostUsd: 0,
98
+ }),
99
+ };
100
+ }
101
+ if (isCoolingDown(input.boundary.occurredAt, input.cooldownUntil)) {
102
+ return {
103
+ observations,
104
+ trigger: createRefinementTriggerDecision({
105
+ ...base,
106
+ decision: "no_action",
107
+ deterministicRoute: null,
108
+ suggestedRoutes: [],
109
+ reason: "The Refiner is in its configured cooldown window.",
110
+ estimatedMaxCostUsd: 0,
111
+ }),
112
+ };
113
+ }
114
+ if ((input.pendingPlanCount ?? 0) >= policy.maxPendingPlans) {
115
+ return {
116
+ observations,
117
+ trigger: createRefinementTriggerDecision({
118
+ ...base,
119
+ decision: "no_action",
120
+ deterministicRoute: null,
121
+ suggestedRoutes: [],
122
+ reason: "The run already has the maximum number of pending improvement plans.",
123
+ estimatedMaxCostUsd: 0,
124
+ }),
125
+ };
126
+ }
127
+ const estimatedMaxCostUsd = input.estimatedRefinerCostUsd ?? 0.01;
128
+ if (estimatedMaxCostUsd > policy.maxEstimatedCostUsd) {
129
+ return {
130
+ observations,
131
+ trigger: createRefinementTriggerDecision({
132
+ ...base,
133
+ decision: "no_action",
134
+ deterministicRoute: null,
135
+ suggestedRoutes: [],
136
+ reason: "The bounded Refiner estimate exceeds the configured run budget.",
137
+ estimatedMaxCostUsd: 0,
138
+ }),
139
+ };
140
+ }
141
+ return {
142
+ observations,
143
+ trigger: createRefinementTriggerDecision({
144
+ ...base,
145
+ decision: "queue_refiner",
146
+ deterministicRoute: null,
147
+ suggestedRoutes: [],
148
+ reason: actionable.some((observation) => observation.kind === "user_turn")
149
+ ? "A completed user turn is ready for bounded background Harness review."
150
+ : "A recovered detour may contain a reusable Harness improvement.",
151
+ estimatedMaxCostUsd,
152
+ }),
153
+ };
154
+ }
155
+ function collectObservations(input) {
156
+ const observations = [];
157
+ const openFailures = [];
158
+ const seenFailureKeys = new Set();
159
+ const recoveredFailureKeys = new Set();
160
+ const userTurnEvent = input.boundary.kind === "turn_completed"
161
+ ? latestUserTurnEvent(input.events)
162
+ : null;
163
+ if (userTurnEvent) {
164
+ observations.push(observationForEvents({
165
+ input,
166
+ kind: "user_turn",
167
+ state: "terminal",
168
+ events: [userTurnEvent],
169
+ deterministicClass: null,
170
+ summary: "The completed user turn is available for bounded background review.",
171
+ }));
172
+ }
173
+ for (const outcome of input.outcomes) {
174
+ if (outcome.action === "refine_request" && !outcome.failed) {
175
+ const requestedSummary = typeof outcome.args.summary === "string"
176
+ ? outcome.args.summary.trim()
177
+ : "The agent explicitly requested bounded refinement.";
178
+ observations.push(observationFor({
179
+ input,
180
+ kind: "reusable_success",
181
+ state: "terminal",
182
+ outcomes: [outcome],
183
+ deterministicClass: "refine_requested",
184
+ summary: requestedSummary.slice(0, 100_000),
185
+ }));
186
+ continue;
187
+ }
188
+ if (outcome.failed) {
189
+ const failureKey = contentHash({
190
+ action: outcome.action,
191
+ deterministicClass: outcome.deterministicClass,
192
+ invocationKey: outcome.invocationKey,
193
+ });
194
+ if (!seenFailureKeys.has(failureKey)) {
195
+ observations.push(observationFor({
196
+ input,
197
+ kind: "tool_failure",
198
+ state: input.boundary.kind === "turn_completed" ? "terminal" : "open",
199
+ outcomes: [outcome],
200
+ deterministicClass: outcome.deterministicClass,
201
+ summary: `Tool ${outcome.action} failed${outcome.deterministicClass ? ` (${outcome.deterministicClass})` : ""}.`,
202
+ }));
203
+ seenFailureKeys.add(failureKey);
204
+ }
205
+ openFailures.push(outcome);
206
+ continue;
207
+ }
208
+ const sameActionIndex = findLastIndex(openFailures, (candidate) => candidate.action === outcome.action);
209
+ const priorFailureIndex = sameActionIndex >= 0
210
+ ? sameActionIndex
211
+ : openFailures.length - 1;
212
+ const priorFailure = openFailures[priorFailureIndex];
213
+ if (!priorFailure)
214
+ continue;
215
+ const failureKey = contentHash({
216
+ action: priorFailure.action,
217
+ deterministicClass: priorFailure.deterministicClass,
218
+ invocationKey: priorFailure.invocationKey,
219
+ });
220
+ if (recoveredFailureKeys.has(failureKey))
221
+ continue;
222
+ const failureObservationIndex = observations.findIndex((observation) => observation.kind === "tool_failure" &&
223
+ observation.eventRefs.some((reference) => reference.id === priorFailure.event.id));
224
+ if (failureObservationIndex >= 0) {
225
+ observations[failureObservationIndex] = observationFor({
226
+ input,
227
+ kind: "tool_failure",
228
+ state: "recovered",
229
+ outcomes: [priorFailure],
230
+ deterministicClass: priorFailure.deterministicClass,
231
+ summary: `Tool ${priorFailure.action} failed and recovered within the same run${priorFailure.deterministicClass
232
+ ? ` (${priorFailure.deterministicClass})`
233
+ : ""}.`,
234
+ });
235
+ }
236
+ if (priorFailure.action === outcome.action) {
237
+ observations.push(observationFor({
238
+ input,
239
+ kind: "retry",
240
+ state: "recovered",
241
+ outcomes: [priorFailure, outcome],
242
+ deterministicClass: priorFailure.deterministicClass,
243
+ summary: `Tool ${outcome.action} was retried after a failure.`,
244
+ }));
245
+ }
246
+ observations.push(observationFor({
247
+ input,
248
+ kind: "recovery",
249
+ state: "recovered",
250
+ outcomes: [priorFailure, outcome],
251
+ deterministicClass: recoveredClass(priorFailure.deterministicClass),
252
+ summary: priorFailure.action === outcome.action
253
+ ? `Tool ${outcome.action} recovered within the same run.`
254
+ : `Tool ${outcome.action} recovered after ${priorFailure.action} failed.`,
255
+ }));
256
+ recoveredFailureKeys.add(failureKey);
257
+ openFailures.splice(priorFailureIndex, 1);
258
+ }
259
+ const recovered = observations.filter((observation) => observation.kind === "recovery");
260
+ if (input.boundary.kind === "turn_completed" && recovered.length > 0) {
261
+ const relevantOutcomes = input.outcomes.filter((outcome) => recovered.some((observation) => observation.eventRefs.some((reference) => reference.id === outcome.event.id)));
262
+ observations.push(observationFor({
263
+ input,
264
+ kind: "completion_detour",
265
+ state: "recovered",
266
+ outcomes: relevantOutcomes,
267
+ deterministicClass: "completed_after_recovery",
268
+ summary: "The turn completed after one or more recoverable tool detours.",
269
+ }));
270
+ }
271
+ return observations;
272
+ }
273
+ function observationForEvents(input) {
274
+ const eventRefs = input.events.map((event) => ({
275
+ id: event.id,
276
+ sequence: event.sequence ?? null,
277
+ contentHash: contentHash(event),
278
+ }));
279
+ return createImprovementObservation({
280
+ schemaVersion: "openpond.improvementObservation.v1",
281
+ id: stableId("improvement-observation", {
282
+ runRef: input.input.runRef,
283
+ kind: input.kind,
284
+ state: input.state,
285
+ deterministicClass: input.deterministicClass,
286
+ summary: input.summary,
287
+ eventRefs,
288
+ boundary: input.input.boundary,
289
+ }),
290
+ runRef: input.input.runRef,
291
+ turnId: input.input.turnId,
292
+ harnessRelease: input.input.harnessRelease,
293
+ overlay: input.input.overlay,
294
+ eventRefs,
295
+ kind: input.kind,
296
+ state: input.state,
297
+ tool: null,
298
+ deterministicClass: input.deterministicClass,
299
+ summary: input.summary,
300
+ createdAt: input.input.boundary.occurredAt,
301
+ metadata: {},
302
+ });
303
+ }
304
+ function latestUserTurnEvent(events) {
305
+ return [...events]
306
+ .sort(compareEvents)
307
+ .reverse()
308
+ .find((event) => event.name === "turn.started") ?? null;
309
+ }
310
+ function findLastIndex(values, predicate) {
311
+ for (let index = values.length - 1; index >= 0; index -= 1) {
312
+ if (predicate(values[index]))
313
+ return index;
314
+ }
315
+ return -1;
316
+ }
317
+ function observationFor(input) {
318
+ const eventRefs = input.outcomes.map(({ event }) => ({
319
+ id: event.id,
320
+ sequence: event.sequence ?? null,
321
+ contentHash: contentHash(event),
322
+ }));
323
+ const first = input.outcomes[0] ?? null;
324
+ return createImprovementObservation({
325
+ schemaVersion: "openpond.improvementObservation.v1",
326
+ id: stableId("improvement-observation", {
327
+ runRef: input.input.runRef,
328
+ kind: input.kind,
329
+ state: input.state,
330
+ deterministicClass: input.deterministicClass,
331
+ summary: input.summary,
332
+ eventRefs,
333
+ boundary: input.input.boundary,
334
+ }),
335
+ runRef: input.input.runRef,
336
+ turnId: input.input.turnId,
337
+ harnessRelease: input.input.harnessRelease,
338
+ overlay: input.input.overlay,
339
+ eventRefs,
340
+ kind: input.kind,
341
+ state: input.state,
342
+ tool: first
343
+ ? { name: first.action, invocationKey: first.invocationKey }
344
+ : null,
345
+ deterministicClass: input.deterministicClass,
346
+ summary: input.summary,
347
+ createdAt: input.input.boundary.occurredAt,
348
+ metadata: {},
349
+ });
350
+ }
351
+ function normalizeToolOutcomes(events) {
352
+ const startedByCallId = new Map();
353
+ const outcomes = [];
354
+ for (const event of [...events].sort(compareEvents)) {
355
+ const callId = toolCallId(event);
356
+ if (event.name === "tool.started") {
357
+ if (callId)
358
+ startedByCallId.set(callId, event);
359
+ continue;
360
+ }
361
+ // Workspace action results are implementation details nested beneath a
362
+ // provider tool call. Treating cleanup/status actions as independent tool
363
+ // successes can falsely mark the parent tool failure as recovered.
364
+ if (event.name !== "tool.completed")
365
+ continue;
366
+ const action = toolAction(event);
367
+ const started = callId ? startedByCallId.get(callId) : undefined;
368
+ const failed = toolEventFailed(event);
369
+ outcomes.push({
370
+ event,
371
+ action,
372
+ args: asRecord(started?.args ?? event.args),
373
+ invocationKey: contentHash({
374
+ action,
375
+ args: started?.args ?? event.args ?? {},
376
+ }),
377
+ failed,
378
+ deterministicClass: failed ? classifyToolFailure(event) : null,
379
+ });
380
+ }
381
+ return outcomes;
382
+ }
383
+ function toolEventFailed(event) {
384
+ if (event.status === "failed")
385
+ return true;
386
+ const data = asRecord(event.data);
387
+ const result = asRecord(data.result);
388
+ if (result.ok === false)
389
+ return true;
390
+ const status = String(data.status ?? result.status ?? "").toLowerCase();
391
+ return ["failed", "error", "errored", "blocked", "timed_out"].includes(status);
392
+ }
393
+ function classifyToolFailure(event) {
394
+ const data = asRecord(event.data);
395
+ const result = asRecord(data.result);
396
+ if (result.timedOut === true)
397
+ return "timeout";
398
+ if (typeof result.exitCode === "number" && result.exitCode !== 0) {
399
+ return "command_exit_nonzero";
400
+ }
401
+ const structuredStatus = String(data.status ?? result.status ?? "").toLowerCase();
402
+ if (["timed_out", "timeout"].includes(structuredStatus))
403
+ return "timeout";
404
+ const text = [
405
+ event.error,
406
+ event.output,
407
+ result.error,
408
+ result.stderr,
409
+ result.stdout,
410
+ ].filter((value) => typeof value === "string").join("\n").toLowerCase();
411
+ if (text.includes("modulenotfounderror") ||
412
+ text.includes("module_not_found") ||
413
+ text.includes("cannot find module") ||
414
+ text.includes("no module named")) {
415
+ return "dependency_missing";
416
+ }
417
+ if (text.includes("research limit") || text.includes("rate limit") || text.includes("quota")) {
418
+ return "tool_budget_exhausted";
419
+ }
420
+ if (text.includes("permission denied") || text.includes("forbidden") || text.includes("unauthorized")) {
421
+ return "permission_denied";
422
+ }
423
+ if (/\btimed out\b|\btimeout\b/.test(text))
424
+ return "timeout";
425
+ if (text.includes("invalid argument") ||
426
+ text.includes("invalid_request") ||
427
+ text.includes("validation")) {
428
+ return "invalid_tool_arguments";
429
+ }
430
+ if (text.includes("enoent") || text.includes("no such file") || text.includes("not found")) {
431
+ return "missing_file_or_resource";
432
+ }
433
+ if (text.includes("exit code") || text.includes("non-zero") || text.includes("nonzero")) {
434
+ return "command_exit_nonzero";
435
+ }
436
+ return "unclassified_tool_failure";
437
+ }
438
+ function recoveredClass(deterministicClass) {
439
+ return deterministicClass ? `recovered_${deterministicClass}` : "recovered_tool_failure";
440
+ }
441
+ function isActionableObservation(observation) {
442
+ return (observation.kind === "recovery" ||
443
+ observation.kind === "completion_detour" ||
444
+ observation.kind === "user_turn" ||
445
+ observation.kind === "reusable_success" ||
446
+ (observation.kind === "validation" && observation.state === "terminal") ||
447
+ (observation.kind === "tool_failure" && observation.state === "terminal"));
448
+ }
449
+ function toolAction(event) {
450
+ if (event.action?.trim())
451
+ return event.action.trim();
452
+ const data = asRecord(event.data);
453
+ return typeof data.tool === "string" && data.tool.trim() ? data.tool.trim() : "unknown_tool";
454
+ }
455
+ function toolCallId(event) {
456
+ const data = asRecord(event.data);
457
+ for (const value of [data.toolCallId, data.id, data.callId]) {
458
+ if (typeof value === "string" && value.trim())
459
+ return value.trim();
460
+ }
461
+ return null;
462
+ }
463
+ function asRecord(value) {
464
+ return value && typeof value === "object" && !Array.isArray(value)
465
+ ? value
466
+ : {};
467
+ }
468
+ function compareEvents(left, right) {
469
+ if (left.sequence !== undefined || right.sequence !== undefined) {
470
+ return (left.sequence ?? Number.MAX_SAFE_INTEGER) -
471
+ (right.sequence ?? Number.MAX_SAFE_INTEGER);
472
+ }
473
+ return left.timestamp.localeCompare(right.timestamp);
474
+ }
475
+ function isCoolingDown(now, cooldownUntil) {
476
+ if (!cooldownUntil)
477
+ return false;
478
+ return Date.parse(now) < Date.parse(cooldownUntil);
479
+ }
480
+ function stableId(prefix, value) {
481
+ return `${prefix}-${contentHash(value).slice(0, 24)}`;
482
+ }
@@ -0,0 +1,105 @@
1
+ import { contentHash } from "./common.js";
2
+ export function memoryKeyFromTarget(target) {
3
+ const match = /^memory\/([a-z0-9][a-z0-9-]{0,119})$/.exec(target.replaceAll("\\", "/"));
4
+ if (!match)
5
+ throw new Error(`Invalid Harness memory target: ${target}.`);
6
+ return match[1];
7
+ }
8
+ export function expectedMemoryRevision(proposal, target) {
9
+ const expected = proposal.metadata.expectedMemory;
10
+ if (!expected || typeof expected !== "object" || Array.isArray(expected)) {
11
+ throw new Error("Memory proposal is missing its expected revision snapshot.");
12
+ }
13
+ const record = expected;
14
+ if (record.key !== memoryKeyFromTarget(target)) {
15
+ throw new Error("Memory proposal expected revision targets a different key.");
16
+ }
17
+ if (record.revision !== null && (!Number.isInteger(record.revision) || Number(record.revision) < 1)) {
18
+ throw new Error("Memory proposal expected revision is invalid.");
19
+ }
20
+ if (record.contentHash !== null && typeof record.contentHash !== "string") {
21
+ throw new Error("Memory proposal expected content hash is invalid.");
22
+ }
23
+ if (record.status !== null && record.status !== "active" && record.status !== "deleted") {
24
+ throw new Error("Memory proposal expected status is invalid.");
25
+ }
26
+ return {
27
+ revision: record.revision,
28
+ contentHash: record.contentHash,
29
+ status: record.status,
30
+ };
31
+ }
32
+ export function boundedTriggerEvidence(trigger) {
33
+ return {
34
+ id: trigger.id,
35
+ runRef: trigger.runRef,
36
+ turnId: trigger.turnId,
37
+ reason: trigger.reason,
38
+ suggestedRoutes: trigger.suggestedRoutes,
39
+ boundary: trigger.boundary,
40
+ };
41
+ }
42
+ export function boundedObservationEvidence(observation) {
43
+ return {
44
+ id: observation.id,
45
+ kind: observation.kind,
46
+ state: observation.state,
47
+ tool: observation.tool?.name ?? null,
48
+ deterministicClass: observation.deterministicClass,
49
+ summary: observation.summary,
50
+ };
51
+ }
52
+ export function proposalEvidence(observations) {
53
+ const byId = new Map();
54
+ for (const observation of observations) {
55
+ for (const event of observation.eventRefs) {
56
+ const evidence = observationEvidence(observation, event);
57
+ byId.set(`${evidence.kind}:${evidence.id}`, evidence);
58
+ }
59
+ }
60
+ return [...byId.values()];
61
+ }
62
+ function observationEvidence(observation, event) {
63
+ const kind = observation.kind === "recovery"
64
+ ? "recovery"
65
+ : observation.kind === "validation"
66
+ ? "validation"
67
+ : observation.kind === "user_turn"
68
+ ? "user_turn"
69
+ : "tool_event";
70
+ return {
71
+ kind: kind,
72
+ id: event.id,
73
+ contentHash: event.contentHash,
74
+ };
75
+ }
76
+ export function uniqueEventRefs(observations) {
77
+ const byId = new Map();
78
+ for (const observation of observations) {
79
+ for (const event of observation.eventRefs)
80
+ byId.set(event.id, event);
81
+ }
82
+ return [...byId.values()];
83
+ }
84
+ export function sameOverlayRef(overlay, reference) {
85
+ return overlay.id === reference.id &&
86
+ overlay.revision === reference.revision &&
87
+ overlay.contentHash === reference.contentHash;
88
+ }
89
+ export function overlayRef(overlay) {
90
+ return {
91
+ id: overlay.id,
92
+ revision: overlay.revision,
93
+ contentHash: overlay.contentHash,
94
+ };
95
+ }
96
+ export function sameWorkspaceRevision(workspace, overlay) {
97
+ return workspace.revision === overlay.workspace.revision &&
98
+ workspace.sourceRevision === overlay.workspace.sourceRevision &&
99
+ workspace.currentChannel.revision === overlay.workspace.channelRevision &&
100
+ workspace.currentChannel.release?.id === overlay.baseHarnessRelease.id &&
101
+ workspace.currentChannel.release.contentHash === overlay.baseHarnessRelease.contentHash;
102
+ }
103
+ export function stableId(prefix, value) {
104
+ return `${prefix}-${contentHash(value).slice(0, 24)}`;
105
+ }