@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.
Files changed (97) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/dist/agents/index.d.ts +34 -0
  4. package/dist/agents/index.js +301 -0
  5. package/dist/errors/catalog.d.ts +185 -0
  6. package/dist/errors/catalog.js +144 -0
  7. package/dist/errors/harness-error.d.ts +64 -0
  8. package/dist/errors/harness-error.js +58 -0
  9. package/dist/errors/index.d.ts +3 -0
  10. package/dist/errors/index.js +3 -0
  11. package/dist/errors/redaction.d.ts +5 -0
  12. package/dist/errors/redaction.js +64 -0
  13. package/dist/harness/defineHarness.d.ts +640 -0
  14. package/dist/harness/defineHarness.js +176 -0
  15. package/dist/harness/errors.d.ts +62 -0
  16. package/dist/harness/errors.js +67 -0
  17. package/dist/harness/types.d.ts +27 -0
  18. package/dist/harness/types.js +1 -0
  19. package/dist/index.d.ts +14 -0
  20. package/dist/index.js +12 -0
  21. package/dist/logger/index.d.ts +2 -0
  22. package/dist/logger/index.js +2 -0
  23. package/dist/logger/json-logger.d.ts +31 -0
  24. package/dist/logger/json-logger.js +65 -0
  25. package/dist/logger/logger.d.ts +31 -0
  26. package/dist/logger/logger.js +1 -0
  27. package/dist/models/json.d.ts +6 -0
  28. package/dist/models/json.js +1 -0
  29. package/dist/models/registry.d.ts +112 -0
  30. package/dist/models/registry.js +286 -0
  31. package/dist/models/state.d.ts +64 -0
  32. package/dist/models/state.js +1 -0
  33. package/dist/ports/base-model-provider.d.ts +56 -0
  34. package/dist/ports/base-model-provider.js +343 -0
  35. package/dist/ports/capabilities.d.ts +70 -0
  36. package/dist/ports/capabilities.js +38 -0
  37. package/dist/ports/feedback.d.ts +29 -0
  38. package/dist/ports/feedback.js +1 -0
  39. package/dist/ports/harness-context.d.ts +20 -0
  40. package/dist/ports/harness-context.js +1 -0
  41. package/dist/ports/index.d.ts +6 -0
  42. package/dist/ports/index.js +6 -0
  43. package/dist/ports/model-provider.d.ts +280 -0
  44. package/dist/ports/model-provider.js +1 -0
  45. package/dist/ports/state.d.ts +72 -0
  46. package/dist/ports/state.js +24 -0
  47. package/dist/runtime/durable.d.ts +134 -0
  48. package/dist/runtime/durable.js +185 -0
  49. package/dist/runtime/index.d.ts +2 -0
  50. package/dist/runtime/index.js +2 -0
  51. package/dist/runtime/steps.d.ts +22 -0
  52. package/dist/runtime/steps.js +51 -0
  53. package/dist/sandbox/index.d.ts +111 -0
  54. package/dist/sandbox/index.js +165 -0
  55. package/dist/sessions/index.d.ts +23 -0
  56. package/dist/sessions/index.js +718 -0
  57. package/dist/skills/index.d.ts +8 -0
  58. package/dist/skills/index.js +88 -0
  59. package/dist/state/in-memory.d.ts +35 -0
  60. package/dist/state/in-memory.js +140 -0
  61. package/dist/telemetry/index.d.ts +1 -0
  62. package/dist/telemetry/index.js +1 -0
  63. package/dist/telemetry/shim.d.ts +26 -0
  64. package/dist/telemetry/shim.js +120 -0
  65. package/dist/testing/capabilities.d.ts +11 -0
  66. package/dist/testing/capabilities.js +20 -0
  67. package/dist/testing/fakeModelProvider.d.ts +25 -0
  68. package/dist/testing/fakeModelProvider.js +79 -0
  69. package/dist/testing/feedback.d.ts +10 -0
  70. package/dist/testing/feedback.js +24 -0
  71. package/dist/testing/fixtures/mcp/fake-http-server.d.ts +8 -0
  72. package/dist/testing/fixtures/mcp/fake-http-server.js +95 -0
  73. package/dist/testing/index.d.ts +8 -0
  74. package/dist/testing/index.js +11 -0
  75. package/dist/testing/sandboxContract.d.ts +4 -0
  76. package/dist/testing/sandboxContract.js +74 -0
  77. package/dist/testing/sandboxSnapshot.d.ts +7 -0
  78. package/dist/testing/sandboxSnapshot.js +201 -0
  79. package/dist/testing/stateStoreContract.d.ts +2 -0
  80. package/dist/testing/stateStoreContract.js +109 -0
  81. package/dist/tools/index.d.ts +9 -0
  82. package/dist/tools/index.js +123 -0
  83. package/dist/tools/mcp/http.d.ts +2 -0
  84. package/dist/tools/mcp/http.js +109 -0
  85. package/dist/tools/mcp/index.d.ts +2 -0
  86. package/dist/tools/mcp/index.js +2 -0
  87. package/dist/tools/mcp/runner.d.ts +74 -0
  88. package/dist/tools/mcp/runner.js +238 -0
  89. package/dist/tools/mcp/schema.d.ts +41 -0
  90. package/dist/tools/mcp/schema.js +251 -0
  91. package/dist/tools/mcp/stdio.d.ts +2 -0
  92. package/dist/tools/mcp/stdio.js +122 -0
  93. package/dist/ulid/index.d.ts +6 -0
  94. package/dist/ulid/index.js +35 -0
  95. package/dist/workflows/index.d.ts +8 -0
  96. package/dist/workflows/index.js +26 -0
  97. package/package.json +75 -0
@@ -0,0 +1,718 @@
1
+ import { z } from 'zod';
2
+ import { InternalError, OperationCancelledError, OperationTimeoutError, HarnessError, SessionBusyError, StateError, ValidationError, WorkflowNotFoundError, serializeError } from '../errors/index.js';
3
+ import { ulid } from '../ulid/index.js';
4
+ import { runDefaultAgent } from '../agents/index.js';
5
+ import { runWorkflow } from '../workflows/index.js';
6
+ import { loadSkillsSync } from '../skills/index.js';
7
+ import { createModelRegistry } from '../models/registry.js';
8
+ import { createTelemetryShim } from '../telemetry/index.js';
9
+ import { createMcpRunnerRegistry } from '../tools/mcp/runner.js';
10
+ const MEMORY_KEY_PATTERN = /^[A-Za-z0-9_.\-:]{1,256}$/;
11
+ function now() {
12
+ return new Date().toISOString();
13
+ }
14
+ function makeMemory(sessionId, sandboxSession) {
15
+ return {
16
+ async read(key) {
17
+ validateMemoryKey(key);
18
+ const path = `/memory/${key}.json`;
19
+ if (!(await sandboxSession.exists(path))) {
20
+ return undefined;
21
+ }
22
+ return JSON.parse(await sandboxSession.readText(path));
23
+ },
24
+ async write(key, value) {
25
+ validateMemoryKey(key);
26
+ let encoded;
27
+ try {
28
+ encoded = JSON.stringify(value);
29
+ }
30
+ catch (error) {
31
+ throw new ValidationError('Memory value must be JSON-serializable.', { where: 'memory_value', issues: { key } }, error);
32
+ }
33
+ await sandboxSession.write(`/memory/${key}.json`, encoded);
34
+ },
35
+ async delete(key) {
36
+ validateMemoryKey(key);
37
+ await sandboxSession.remove(`/memory/${key}.json`).catch(() => undefined);
38
+ },
39
+ async list() {
40
+ const entries = await sandboxSession.list('/memory').catch(() => []);
41
+ return entries
42
+ .filter((entry) => entry.kind === 'file' && entry.name.endsWith('.json'))
43
+ .map((entry) => entry.name.slice(0, -5))
44
+ .sort();
45
+ }
46
+ };
47
+ }
48
+ function validateMemoryKey(key) {
49
+ if (!MEMORY_KEY_PATTERN.test(key)) {
50
+ throw new ValidationError('Invalid session memory key.', { where: 'memory_key', issues: { key } });
51
+ }
52
+ }
53
+ function validateInvokeOptions(opts) {
54
+ if (opts?.historyWindow !== undefined && opts.historyWindow < 0) {
55
+ throw new ValidationError('Invoke options are invalid.', { where: 'invoke_options', issues: { historyWindow: opts.historyWindow } });
56
+ }
57
+ if (opts?.timeoutMs !== undefined && opts.timeoutMs < 0) {
58
+ throw new ValidationError('Invoke options are invalid.', { where: 'invoke_options', issues: { timeoutMs: opts.timeoutMs } });
59
+ }
60
+ }
61
+ function normalizeMessage(message, sessionId) {
62
+ return {
63
+ ...message,
64
+ sessionId,
65
+ id: ulid(),
66
+ timestamp: now()
67
+ };
68
+ }
69
+ export function createSessionHarness(definition) {
70
+ const resolvedSkills = loadSkillsSync(definition.skills);
71
+ const sessionStates = new Map();
72
+ const telemetry = definition.telemetryShim ?? createTelemetryShim();
73
+ const adapterContext = {
74
+ harnessName: definition.name,
75
+ logger: definition.logger,
76
+ telemetry,
77
+ defaults: {
78
+ agentMaxIterations: definition.defaults.agentMaxIterations ?? 16,
79
+ runTimeoutMs: definition.defaults.runTimeoutMs ?? 600_000,
80
+ toolTimeoutMs: definition.defaults.toolTimeoutMs ?? 120_000,
81
+ skillTimeoutMs: definition.defaults.skillTimeoutMs ?? 60_000,
82
+ modelTimeoutMs: definition.defaults.modelTimeoutMs ?? 300_000,
83
+ ...(definition.defaults.historyWindow !== undefined ? { historyWindow: definition.defaults.historyWindow } : {})
84
+ }
85
+ };
86
+ configureHarnessAdapters(adapterContext, definition.models, definition.state, definition.sandbox, definition.tools);
87
+ const modelRegistry = createModelRegistry(definition.models, { telemetry, harnessName: definition.name });
88
+ const mcpRegistry = createMcpRunnerRegistry();
89
+ const captureContent = definition.telemetry?.captureContent === true;
90
+ async function ensureSessionRecord(sessionId) {
91
+ const existing = await definition.state.getSession(sessionId);
92
+ if (existing) {
93
+ return existing;
94
+ }
95
+ const created = {
96
+ id: sessionId,
97
+ createdAt: now(),
98
+ updatedAt: now(),
99
+ runCount: 0
100
+ };
101
+ await definition.state.upsertSession(created);
102
+ return created;
103
+ }
104
+ async function getSessionState(sessionId) {
105
+ const existing = sessionStates.get(sessionId);
106
+ if (existing) {
107
+ return existing;
108
+ }
109
+ const sandboxSession = await definition.sandbox.open({ sessionId, runId: `init_${ulid()}` });
110
+ const created = { busy: false, sandboxSession, mountedSkills: new Set() };
111
+ sessionStates.set(sessionId, created);
112
+ return created;
113
+ }
114
+ async function appendEvents(runId, events) {
115
+ try {
116
+ await definition.state.appendEvents(runId, events);
117
+ }
118
+ catch (error) {
119
+ telemetry.recordCounter('harness.events.persist_errors', 1, { harness: definition.name });
120
+ definition.logger.error('Failed to persist run events.', { harness: definition.name, run_id: runId, error: serializeError(error) });
121
+ }
122
+ }
123
+ return {
124
+ inspect() {
125
+ return definition.inspection;
126
+ },
127
+ async getSession(sessionId) {
128
+ await ensureSessionRecord(sessionId);
129
+ const state = await getSessionState(sessionId);
130
+ const memory = makeMemory(sessionId, state.sandboxSession);
131
+ const workflowEntries = Object.entries(definition.workflows).map(([workflowId, workflow]) => {
132
+ const invoker = {
133
+ prompt: (input, opts) => runWorkflowCall(sessionId, workflowId, workflow, input, opts),
134
+ async *stream(input, opts) {
135
+ for await (const event of streamWorkflowCall(sessionId, workflowId, workflow, input, opts)) {
136
+ yield event;
137
+ }
138
+ }
139
+ };
140
+ return [workflowId, invoker];
141
+ });
142
+ const workflows = Object.fromEntries(workflowEntries);
143
+ const agentEntries = Object.entries(definition.agents).map(([agentId, agent]) => {
144
+ const invoker = {
145
+ prompt: (input, opts) => runAgentCall(sessionId, agentId, agent, input, opts),
146
+ async *stream(input, opts) {
147
+ for await (const event of streamAgentCall(sessionId, agentId, agent, input, opts)) {
148
+ yield event;
149
+ }
150
+ }
151
+ };
152
+ return [agentId, invoker];
153
+ });
154
+ const agents = Object.fromEntries(agentEntries);
155
+ return {
156
+ id: sessionId,
157
+ agents,
158
+ workflows,
159
+ memory,
160
+ history: {
161
+ list: (opts) => definition.state.listMessages(sessionId, opts)
162
+ },
163
+ async clearHistory() {
164
+ if (state.busy) {
165
+ throw new SessionBusyError('Session is busy.', { session_id: sessionId, reason: 'history_clear_during_run' });
166
+ }
167
+ await definition.state.clearMessages(sessionId);
168
+ },
169
+ async replaceHistory(messages) {
170
+ if (state.busy) {
171
+ throw new SessionBusyError('Session is busy.', { session_id: sessionId, reason: 'history_replace_during_run' });
172
+ }
173
+ const parsed = messages.map((message) => {
174
+ try {
175
+ return normalizeMessage(message, sessionId);
176
+ }
177
+ catch (error) {
178
+ throw new ValidationError('Session history replacement failed validation.', { where: 'session_history', issues: { message } }, error);
179
+ }
180
+ });
181
+ await definition.state.clearMessages(sessionId);
182
+ if (parsed.length > 0) {
183
+ await definition.state.appendMessages(sessionId, parsed);
184
+ }
185
+ },
186
+ async close() {
187
+ await definition.state.closeSession(sessionId);
188
+ sessionStates.delete(sessionId);
189
+ await state.sandboxSession.close();
190
+ }
191
+ };
192
+ },
193
+ async shutdown() {
194
+ const errors = [];
195
+ try {
196
+ await mcpRegistry.close();
197
+ }
198
+ catch (error) {
199
+ errors.push(error instanceof HarnessError ? error : new InternalError('Failed to close MCP registry.', undefined, error));
200
+ }
201
+ for (const [sessionId, state] of sessionStates) {
202
+ try {
203
+ await state.sandboxSession.close();
204
+ }
205
+ catch (error) {
206
+ errors.push(error instanceof HarnessError ? error : new InternalError('Failed to close sandbox session.', { session_id: sessionId }, error));
207
+ }
208
+ }
209
+ sessionStates.clear();
210
+ try {
211
+ await definition.state.close?.();
212
+ }
213
+ catch (error) {
214
+ errors.push(error instanceof HarnessError ? error : new InternalError('Failed to close state store.', undefined, error));
215
+ }
216
+ return { errors };
217
+ },
218
+ $infer: {}
219
+ };
220
+ async function* streamAgentCall(sessionId, agentId, agent, input, opts) {
221
+ const buffer = [];
222
+ const maxBufferedEvents = 1024;
223
+ let dropped = 0;
224
+ let done = false;
225
+ let failure;
226
+ let liveRunId = 'unknown';
227
+ const result = runAgentCall(sessionId, agentId, agent, input, opts, (event) => {
228
+ if ('runId' in event)
229
+ liveRunId = event.runId;
230
+ if (buffer.length >= maxBufferedEvents) {
231
+ const dropIndex = buffer.findIndex((candidate) => candidate.type !== 'run.finished');
232
+ if (dropIndex >= 0) {
233
+ buffer.splice(dropIndex, 1);
234
+ dropped += 1;
235
+ }
236
+ }
237
+ buffer.push(event);
238
+ return Promise.resolve();
239
+ }).catch((error) => {
240
+ failure = error;
241
+ return undefined;
242
+ }).finally(() => {
243
+ done = true;
244
+ });
245
+ let cursor = 0;
246
+ while (true) {
247
+ if (dropped > 0) {
248
+ yield { type: 'stream.overflow', runId: liveRunId, at: now(), dropped };
249
+ dropped = 0;
250
+ }
251
+ while (cursor < buffer.length) {
252
+ yield buffer[cursor];
253
+ cursor += 1;
254
+ }
255
+ if (done) {
256
+ await result.catch(() => undefined);
257
+ if (failure)
258
+ throw failure;
259
+ return;
260
+ }
261
+ await new Promise((resolve) => setTimeout(resolve, 5));
262
+ }
263
+ }
264
+ async function runAgentCall(sessionId, agentId, agent, input, opts, onEvent) {
265
+ validateInvokeOptions(opts);
266
+ if (opts?.signal?.aborted) {
267
+ throw new OperationCancelledError('Run was cancelled before start.', { scope: 'run' });
268
+ }
269
+ const runSignal = createRunSignal(opts?.signal, opts?.timeoutMs ?? definition.defaults.runTimeoutMs);
270
+ const state = await getSessionState(sessionId);
271
+ const memory = makeMemory(sessionId, state.sandboxSession);
272
+ if (state.busy) {
273
+ throw new SessionBusyError('Session is busy.', { session_id: sessionId, reason: 'concurrent_run' });
274
+ }
275
+ state.busy = true;
276
+ const startedAt = now();
277
+ const runId = ulid();
278
+ const runRecord = {
279
+ id: runId,
280
+ sessionId,
281
+ kind: 'agent',
282
+ target: agentId,
283
+ startedAt,
284
+ status: 'running',
285
+ input: input
286
+ };
287
+ const emit = async (event) => {
288
+ const eventAt = 'at' in event ? event.at : now();
289
+ await onEvent?.(event);
290
+ await appendEvents(runId, [{ id: ulid(), runId, at: eventAt, type: event.type, payload: sanitizeEventForPersistence(event, captureContent) }]);
291
+ };
292
+ try {
293
+ await definition.state.createRun(runRecord);
294
+ }
295
+ catch (error) {
296
+ state.busy = false;
297
+ throw error;
298
+ }
299
+ try {
300
+ const result = await telemetry.span('harness.session.agent_prompt', {
301
+ 'harness.name': definition.name,
302
+ 'harness.session.id': sessionId,
303
+ 'harness.run.id': runId,
304
+ 'harness.agent.id': agentId
305
+ }, async () => {
306
+ await emit({ type: 'run.started', runId, at: startedAt });
307
+ const resolvedHistoryWindow = opts?.historyWindow ?? definition.defaults.historyWindow;
308
+ const run = await runDefaultAgent({
309
+ harnessName: definition.name,
310
+ agentId,
311
+ runId,
312
+ sessionId,
313
+ input,
314
+ history: await definition.state.listMessages(sessionId),
315
+ agent,
316
+ models: modelRegistry,
317
+ skills: resolvedSkills,
318
+ customTools: definition.tools,
319
+ mcpRegistry,
320
+ session: state.sandboxSession,
321
+ memory,
322
+ mountedSkills: state.mountedSkills,
323
+ ...(resolvedHistoryWindow !== undefined ? { historyWindow: resolvedHistoryWindow } : {}),
324
+ maxSteps: definition.defaults.agentMaxIterations ?? 16,
325
+ signal: runSignal.signal,
326
+ toolTimeoutMs: definition.defaults.toolTimeoutMs ?? 120_000,
327
+ logger: definition.logger,
328
+ telemetry,
329
+ emitEvent: emit
330
+ });
331
+ if (run.emitted.length > 0) {
332
+ await definition.state.appendMessages(sessionId, run.emitted);
333
+ }
334
+ return run.output;
335
+ });
336
+ const finishedAt = now();
337
+ await emit({ type: 'run.finished', runId, at: finishedAt, output: result });
338
+ await definition.state.finishRun(runId, { status: 'succeeded', finishedAt, output: result });
339
+ const sessionRecord = await ensureSessionRecord(sessionId);
340
+ await definition.state.upsertSession({ ...sessionRecord, updatedAt: finishedAt, runCount: sessionRecord.runCount + 1 });
341
+ return result;
342
+ }
343
+ catch (error) {
344
+ const finalError = normalizeRunError(error, runSignal.signal);
345
+ const finishedAt = now();
346
+ const serialized = serializeError(finalError);
347
+ const log = finalError instanceof OperationCancelledError ? definition.logger.warn.bind(definition.logger) : definition.logger.error.bind(definition.logger);
348
+ log('Harness agent run failed.', {
349
+ harness: definition.name,
350
+ session_id: sessionId,
351
+ run_id: runId,
352
+ agent_id: agentId,
353
+ error: serialized
354
+ });
355
+ const runFinished = { type: 'run.finished', runId, at: finishedAt, error: serialized };
356
+ await terminalizeFailedRun({
357
+ kind: 'agent',
358
+ targetId: agentId,
359
+ sessionId,
360
+ runId,
361
+ primaryError: serialized,
362
+ emitRunFinished: () => emit(runFinished),
363
+ finishRun: () => definition.state.finishRun(runId, {
364
+ status: finalError instanceof OperationCancelledError ? 'cancelled' : 'failed',
365
+ finishedAt,
366
+ error: serialized
367
+ }),
368
+ upsertSession: async () => {
369
+ const sessionRecord = await ensureSessionRecord(sessionId);
370
+ await definition.state.upsertSession({ ...sessionRecord, updatedAt: finishedAt, runCount: sessionRecord.runCount + 1 });
371
+ }
372
+ });
373
+ throw finalError;
374
+ }
375
+ finally {
376
+ runSignal.cleanup();
377
+ state.busy = false;
378
+ }
379
+ }
380
+ async function* streamWorkflowCall(sessionId, workflowId, workflow, input, opts) {
381
+ const buffer = [];
382
+ const maxBufferedEvents = 1024;
383
+ let dropped = 0;
384
+ let done = false;
385
+ let failure;
386
+ let liveRunId = 'unknown';
387
+ const result = runWorkflowCall(sessionId, workflowId, workflow, input, opts, (event) => {
388
+ if ('runId' in event)
389
+ liveRunId = event.runId;
390
+ if (buffer.length >= maxBufferedEvents) {
391
+ const dropIndex = buffer.findIndex((candidate) => candidate.type !== 'run.finished');
392
+ if (dropIndex >= 0) {
393
+ buffer.splice(dropIndex, 1);
394
+ dropped += 1;
395
+ }
396
+ }
397
+ buffer.push(event);
398
+ return Promise.resolve();
399
+ }).catch((error) => {
400
+ failure = error;
401
+ return undefined;
402
+ }).finally(() => {
403
+ done = true;
404
+ });
405
+ let cursor = 0;
406
+ while (true) {
407
+ if (dropped > 0) {
408
+ yield { type: 'stream.overflow', runId: liveRunId, at: now(), dropped };
409
+ dropped = 0;
410
+ }
411
+ while (cursor < buffer.length) {
412
+ yield buffer[cursor];
413
+ cursor += 1;
414
+ }
415
+ if (done) {
416
+ await result.catch(() => undefined);
417
+ if (failure)
418
+ throw failure;
419
+ return;
420
+ }
421
+ await new Promise((resolve) => setTimeout(resolve, 5));
422
+ }
423
+ }
424
+ async function runWorkflowCall(sessionId, workflowId, workflow, input, opts, onEvent) {
425
+ validateInvokeOptions(opts);
426
+ if (opts?.signal?.aborted) {
427
+ throw new OperationCancelledError('Run was cancelled before start.', { scope: 'run' });
428
+ }
429
+ const runSignal = createRunSignal(opts?.signal, opts?.timeoutMs ?? definition.defaults.runTimeoutMs);
430
+ const state = await getSessionState(sessionId);
431
+ const memory = makeMemory(sessionId, state.sandboxSession);
432
+ if (state.busy) {
433
+ throw new SessionBusyError('Session is busy.', { session_id: sessionId, reason: 'concurrent_run' });
434
+ }
435
+ state.busy = true;
436
+ const startedAt = now();
437
+ const runId = ulid();
438
+ const runRecord = {
439
+ id: runId,
440
+ sessionId,
441
+ kind: 'workflow',
442
+ target: workflowId,
443
+ startedAt,
444
+ status: 'running',
445
+ input: input
446
+ };
447
+ const emit = async (event) => {
448
+ const eventAt = 'at' in event ? event.at : now();
449
+ await onEvent?.(event);
450
+ await appendEvents(runId, [{ id: ulid(), runId, at: eventAt, type: event.type, payload: sanitizeEventForPersistence(event, captureContent) }]);
451
+ };
452
+ try {
453
+ await definition.state.createRun(runRecord);
454
+ }
455
+ catch (error) {
456
+ state.busy = false;
457
+ throw error;
458
+ }
459
+ try {
460
+ const result = await telemetry.span('harness.session.prompt', {
461
+ 'harness.name': definition.name,
462
+ 'harness.session.id': sessionId,
463
+ 'harness.run.id': runId,
464
+ 'harness.workflow.id': workflowId
465
+ }, async () => {
466
+ const runStarted = { type: 'run.started', runId, at: startedAt };
467
+ await emit(runStarted);
468
+ const workflowArgs = {
469
+ workflowId,
470
+ workflow,
471
+ input,
472
+ ctx: {
473
+ signal: runSignal.signal,
474
+ runId,
475
+ sessionId,
476
+ models: modelRegistry,
477
+ agents: Object.fromEntries(Object.entries(definition.agents).map(([agentId, agent]) => [
478
+ agentId,
479
+ async (agentInput, agentOpts) => {
480
+ const agentSignal = combineSignals(runSignal.signal, agentOpts?.signal);
481
+ try {
482
+ const resolvedHistoryWindow = agentOpts?.historyWindow ?? opts?.historyWindow ?? definition.defaults.historyWindow;
483
+ const run = await runDefaultAgent({
484
+ harnessName: definition.name,
485
+ agentId,
486
+ runId,
487
+ sessionId,
488
+ workflowId,
489
+ input: agentInput,
490
+ history: await definition.state.listMessages(sessionId),
491
+ agent: agent,
492
+ models: modelRegistry,
493
+ skills: resolvedSkills,
494
+ customTools: definition.tools,
495
+ mcpRegistry,
496
+ session: state.sandboxSession,
497
+ memory,
498
+ mountedSkills: state.mountedSkills,
499
+ ...(resolvedHistoryWindow !== undefined ? { historyWindow: resolvedHistoryWindow } : {}),
500
+ maxSteps: definition.defaults.agentMaxIterations ?? 16,
501
+ signal: agentSignal.signal,
502
+ toolTimeoutMs: definition.defaults.toolTimeoutMs ?? 120_000,
503
+ logger: definition.logger,
504
+ telemetry,
505
+ emitEvent: emit
506
+ });
507
+ if (run.emitted.length > 0) {
508
+ await definition.state.appendMessages(sessionId, run.emitted);
509
+ }
510
+ return run.output;
511
+ }
512
+ finally {
513
+ agentSignal.cleanup();
514
+ }
515
+ }
516
+ ]))
517
+ }
518
+ };
519
+ return telemetry.span('harness.workflow.run', {
520
+ 'harness.name': definition.name,
521
+ 'harness.session.id': sessionId,
522
+ 'harness.run.id': runId,
523
+ 'harness.workflow.id': workflowId
524
+ }, async () => runWorkflow({
525
+ ...workflowArgs,
526
+ ...(opts ? { opts: { ...opts, signal: runSignal.signal } } : { opts: { signal: runSignal.signal } })
527
+ }));
528
+ });
529
+ const finishedAt = now();
530
+ const runFinished = { type: 'run.finished', runId, at: finishedAt, output: result };
531
+ await emit(runFinished);
532
+ await definition.state.finishRun(runId, { status: 'succeeded', finishedAt, output: result });
533
+ const sessionRecord = await ensureSessionRecord(sessionId);
534
+ await definition.state.upsertSession({ ...sessionRecord, updatedAt: finishedAt, runCount: sessionRecord.runCount + 1 });
535
+ return result;
536
+ }
537
+ catch (error) {
538
+ const finalError = normalizeRunError(error, runSignal.signal);
539
+ const finishedAt = now();
540
+ const serialized = serializeError(finalError);
541
+ const log = finalError instanceof OperationCancelledError ? definition.logger.warn.bind(definition.logger) : definition.logger.error.bind(definition.logger);
542
+ log('Harness workflow run failed.', {
543
+ harness: definition.name,
544
+ session_id: sessionId,
545
+ run_id: runId,
546
+ workflow_id: workflowId,
547
+ error: serialized
548
+ });
549
+ const runFinished = { type: 'run.finished', runId, at: finishedAt, error: serialized };
550
+ await terminalizeFailedRun({
551
+ kind: 'workflow',
552
+ targetId: workflowId,
553
+ sessionId,
554
+ runId,
555
+ primaryError: serialized,
556
+ emitRunFinished: () => emit(runFinished),
557
+ finishRun: () => definition.state.finishRun(runId, {
558
+ status: finalError instanceof OperationCancelledError ? 'cancelled' : 'failed',
559
+ finishedAt,
560
+ error: serialized
561
+ }),
562
+ upsertSession: async () => {
563
+ const sessionRecord = await ensureSessionRecord(sessionId);
564
+ await definition.state.upsertSession({ ...sessionRecord, updatedAt: finishedAt, runCount: sessionRecord.runCount + 1 });
565
+ }
566
+ });
567
+ throw finalError;
568
+ }
569
+ finally {
570
+ runSignal.cleanup();
571
+ state.busy = false;
572
+ }
573
+ }
574
+ async function terminalizeFailedRun(args) {
575
+ await runFailureTerminalizationStep(args, 'emit_run_finished', args.emitRunFinished);
576
+ await runFailureTerminalizationStep(args, 'finish_run', args.finishRun);
577
+ await runFailureTerminalizationStep(args, 'upsert_session', args.upsertSession);
578
+ }
579
+ async function runFailureTerminalizationStep(args, operation, step) {
580
+ try {
581
+ await step();
582
+ }
583
+ catch (error) {
584
+ telemetry.recordCounter('harness.runs.terminalization_errors', 1, {
585
+ harness: definition.name,
586
+ 'harness.run.kind': args.kind,
587
+ 'harness.run.terminalization.operation': operation
588
+ });
589
+ definition.logger.error('Failed to terminalize failed run; preserving primary run error.', {
590
+ harness: definition.name,
591
+ session_id: args.sessionId,
592
+ run_id: args.runId,
593
+ [`${args.kind}_id`]: args.targetId,
594
+ operation,
595
+ primary_error: args.primaryError,
596
+ error: serializeError(error)
597
+ });
598
+ }
599
+ }
600
+ }
601
+ function configureHarnessAdapters(context, models, state, sandbox, tools) {
602
+ const seen = new Set();
603
+ for (const alias of Object.values(models)) {
604
+ configureOne(alias.provider, context, seen);
605
+ }
606
+ configureOne(state, context, seen);
607
+ configureOne(sandbox, context, seen);
608
+ for (const tool of Object.values(tools)) {
609
+ configureOne(tool, context, seen);
610
+ }
611
+ }
612
+ function configureOne(adapter, context, seen) {
613
+ const configurable = adapter;
614
+ if (!configurable.configureHarnessContext || seen.has(adapter))
615
+ return;
616
+ configurable.configureHarnessContext(context);
617
+ seen.add(adapter);
618
+ }
619
+ function normalizeRunError(error, signal) {
620
+ if (!signal.aborted)
621
+ return error;
622
+ if (signal.reason instanceof OperationTimeoutError)
623
+ return signal.reason;
624
+ if (error instanceof OperationCancelledError || error instanceof OperationTimeoutError)
625
+ return error;
626
+ return new OperationCancelledError('Run was cancelled.', { scope: 'run' }, signal.reason ?? error);
627
+ }
628
+ function sanitizeEventForPersistence(event, captureContent) {
629
+ if (captureContent) {
630
+ const { runId: _runId, at: _at, type: _type, ...payload } = event;
631
+ return payload;
632
+ }
633
+ switch (event.type) {
634
+ case 'run.started':
635
+ return {};
636
+ case 'run.finished':
637
+ return {
638
+ ...(event.output !== undefined ? { output: '[redacted]' } : {}),
639
+ ...(event.error ? { error: event.error } : {})
640
+ };
641
+ case 'agent.started':
642
+ return { agentId: event.agentId };
643
+ case 'agent.finished':
644
+ return {
645
+ agentId: event.agentId,
646
+ ...(event.output !== undefined ? { output: '[redacted]' } : {}),
647
+ ...(event.error ? { error: event.error } : {})
648
+ };
649
+ case 'tool.started':
650
+ return { agentId: event.agentId, toolId: event.toolId, callId: event.callId, input: '[redacted]' };
651
+ case 'tool.finished':
652
+ return {
653
+ agentId: event.agentId,
654
+ toolId: event.toolId,
655
+ callId: event.callId,
656
+ ...(event.output !== undefined ? { output: '[redacted]' } : {}),
657
+ ...(event.error ? { error: event.error } : {})
658
+ };
659
+ case 'model.message':
660
+ return { agentId: event.agentId, message: '[redacted]' };
661
+ case 'model.delta':
662
+ return { agentId: event.agentId, delta: '[redacted]' };
663
+ case 'model.object.partial':
664
+ return { ...(event.agentId ? { agentId: event.agentId } : {}), partial: '[redacted]' };
665
+ case 'model.object':
666
+ return { ...(event.agentId ? { agentId: event.agentId } : {}), object: '[redacted]' };
667
+ case 'model.embedding.completed':
668
+ return {
669
+ ...(event.agentId ? { agentId: event.agentId } : {}),
670
+ count: event.count,
671
+ ...(event.dimensions !== undefined ? { dimensions: event.dimensions } : {}),
672
+ ...(event.usage ? { usage: event.usage } : {})
673
+ };
674
+ case 'model.rerank.completed':
675
+ return {
676
+ ...(event.agentId ? { agentId: event.agentId } : {}),
677
+ count: event.count,
678
+ ...(event.topN !== undefined ? { topN: event.topN } : {}),
679
+ ...(event.usage ? { usage: event.usage } : {})
680
+ };
681
+ case 'stream.overflow':
682
+ return { dropped: event.dropped };
683
+ }
684
+ }
685
+ function createRunSignal(parent, timeoutMs) {
686
+ const controller = new AbortController();
687
+ const relay = () => controller.abort(parent?.reason);
688
+ if (parent)
689
+ parent.addEventListener('abort', relay, { once: true });
690
+ const timeout = timeoutMs && timeoutMs > 0
691
+ ? setTimeout(() => controller.abort(new OperationTimeoutError('Run timed out.', { scope: 'run', timeout_ms: timeoutMs })), timeoutMs)
692
+ : undefined;
693
+ return {
694
+ signal: controller.signal,
695
+ cleanup: () => {
696
+ if (timeout)
697
+ clearTimeout(timeout);
698
+ if (parent)
699
+ parent.removeEventListener('abort', relay);
700
+ }
701
+ };
702
+ }
703
+ function combineSignals(primary, secondary) {
704
+ if (!secondary)
705
+ return { signal: primary, cleanup: () => undefined };
706
+ const controller = new AbortController();
707
+ const relayPrimary = () => controller.abort(primary.reason);
708
+ const relaySecondary = () => controller.abort(secondary.reason);
709
+ primary.addEventListener('abort', relayPrimary, { once: true });
710
+ secondary.addEventListener('abort', relaySecondary, { once: true });
711
+ return {
712
+ signal: controller.signal,
713
+ cleanup: () => {
714
+ primary.removeEventListener('abort', relayPrimary);
715
+ secondary.removeEventListener('abort', relaySecondary);
716
+ }
717
+ };
718
+ }