@purista/harness 1.2.1 → 1.2.2

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 (45) hide show
  1. package/dist/agents/index.d.ts +1 -0
  2. package/dist/agents/index.js +276 -141
  3. package/dist/errors/catalog.d.ts +4 -3
  4. package/dist/harness/defineHarness.d.ts +26 -2
  5. package/dist/harness/defineHarness.js +51 -2
  6. package/dist/index.d.ts +1 -1
  7. package/dist/memory/sandbox/index.js +7 -1
  8. package/dist/models/registry.js +45 -3
  9. package/dist/ports/base-model-provider.js +2 -0
  10. package/dist/ports/capabilities.d.ts +2 -0
  11. package/dist/ports/harness-context.d.ts +1 -0
  12. package/dist/ports/model-provider.d.ts +4 -0
  13. package/dist/ports/state.d.ts +6 -0
  14. package/dist/runtime/abort.d.ts +5 -0
  15. package/dist/runtime/abort.js +33 -0
  16. package/dist/runtime/durable.d.ts +2 -0
  17. package/dist/runtime/durable.js +6 -2
  18. package/dist/runtime/sessionDurable.d.ts +49 -0
  19. package/dist/runtime/sessionDurable.js +135 -0
  20. package/dist/runtime/steps.d.ts +19 -1
  21. package/dist/runtime/steps.js +21 -3
  22. package/dist/sandbox/index.d.ts +34 -0
  23. package/dist/sandbox/index.js +40 -3
  24. package/dist/sessions/index.d.ts +15 -2
  25. package/dist/sessions/index.js +212 -99
  26. package/dist/skills/index.js +19 -6
  27. package/dist/state/in-memory.d.ts +1 -0
  28. package/dist/state/in-memory.js +15 -0
  29. package/dist/telemetry/shim.js +9 -4
  30. package/dist/testing/durableWorkspaceStoreContract.d.ts +1 -1
  31. package/dist/testing/durableWorkspaceStoreContract.js +64 -28
  32. package/dist/tools/index.d.ts +2 -0
  33. package/dist/tools/index.js +15 -1
  34. package/dist/tools/mcp/runner.js +11 -6
  35. package/dist/tools/mcp/stdio.js +170 -1
  36. package/dist/ulid/index.d.ts +6 -1
  37. package/dist/ulid/index.js +31 -13
  38. package/dist/version.d.ts +2 -0
  39. package/dist/version.js +2 -0
  40. package/dist/workflows/index.js +7 -1
  41. package/dist/workspace/in-memory.d.ts +9 -10
  42. package/dist/workspace/in-memory.js +191 -48
  43. package/package.json +1 -1
  44. package/dist/harness/errors.d.ts +0 -62
  45. package/dist/harness/errors.js +0 -67
@@ -1,3 +1,12 @@
1
+ import { OperationCancelledError, WorkspaceError, WorkspaceQuotaExceededError } from '../errors/index.js';
2
+ const RETENTION = {
3
+ pausedTtlMs: 86_400_000,
4
+ terminalFailureTtlMs: 86_400_000,
5
+ terminalSuccessTtlMs: 0,
6
+ abortedTtlMs: 86_400_000,
7
+ cleanupMode: 'manual_only'
8
+ };
9
+ const QUOTA = { maxActiveWorkspaces: 100, maxWorkspaceBytes: 10_000_000, maxCheckpointPayloadBytes: 1_000_000 };
1
10
  /** In-process durable workspace store for local development, examples, and tests. */
2
11
  export class InMemoryDurableWorkspaceStore {
3
12
  info = {
@@ -13,25 +22,44 @@ export class InMemoryDurableWorkspaceStore {
13
22
  'workspace_store.retention',
14
23
  'workspace_store.quota'
15
24
  ],
16
- policy: {
17
- retention: {
18
- pausedTtlMs: 86_400_000,
19
- terminalFailureTtlMs: 86_400_000,
20
- terminalSuccessTtlMs: 0,
21
- cleanupMode: 'manual_only'
22
- },
23
- quota: { maxActiveWorkspaces: 100, maxWorkspaceBytes: 10_000_000 }
24
- }
25
+ policy: { retention: RETENTION, quota: { maxActiveWorkspaces: QUOTA.maxActiveWorkspaces, maxWorkspaceBytes: QUOTA.maxWorkspaceBytes } }
25
26
  };
26
27
  capabilities = this.info.capabilities;
27
28
  workspaces = new Map();
29
+ startKeys = new Map();
30
+ opResults = new Map();
28
31
  nextId = 1;
29
32
  configureHarnessContext() { }
30
33
  async startWorkspace(opts) {
31
- opts.signal?.throwIfAborted();
34
+ throwIfAborted(opts.signal);
35
+ // Idempotent start: a repeated key replays the same workspace; a key reused
36
+ // with a different identity is a conflict.
37
+ const prior = this.startKeys.get(opts.idempotencyKey);
38
+ if (prior) {
39
+ if (prior.runId !== opts.runId || prior.sessionId !== opts.sessionId) {
40
+ throw new WorkspaceError('Workspace start idempotency key reused with a different run/session.', {
41
+ reason: 'idempotency_conflict',
42
+ workspace_ref: prior.workspaceRef,
43
+ run_id: opts.runId,
44
+ session_id: opts.sessionId
45
+ });
46
+ }
47
+ const existing = this.workspaces.get(prior.workspaceRef);
48
+ if (existing)
49
+ return this.toHandle(existing);
50
+ }
51
+ const activeCount = [...this.workspaces.values()].filter((w) => w.state === 'active' || w.state === 'paused').length;
52
+ if (activeCount >= QUOTA.maxActiveWorkspaces) {
53
+ throw new WorkspaceQuotaExceededError('Active workspace quota exceeded.', {
54
+ quota: 'maxActiveWorkspaces',
55
+ limit: QUOTA.maxActiveWorkspaces,
56
+ actual: activeCount,
57
+ run_id: opts.runId,
58
+ session_id: opts.sessionId
59
+ });
60
+ }
32
61
  const workspaceRef = `workspace_${this.nextId++}`;
33
62
  const now = new Date().toISOString();
34
- const metadata = { ...(opts.metadata ?? {}) };
35
63
  const workspace = {
36
64
  workspaceRef,
37
65
  state: 'active',
@@ -40,17 +68,42 @@ export class InMemoryDurableWorkspaceStore {
40
68
  attempt: opts.attempt,
41
69
  createdAt: now,
42
70
  updatedAt: now,
43
- metadata,
44
- checkpoints: []
71
+ metadata: { ...(opts.metadata ?? {}) },
72
+ checkpoints: [],
73
+ bytes: 0
45
74
  };
46
75
  this.workspaces.set(workspaceRef, workspace);
47
- return { workspaceRef, runId: opts.runId, sessionId: opts.sessionId, state: 'active', startedAt: now, attempt: opts.attempt, metadata };
76
+ this.startKeys.set(opts.idempotencyKey, { workspaceRef, runId: opts.runId, sessionId: opts.sessionId });
77
+ return this.toHandle(workspace);
48
78
  }
49
79
  async pauseWorkspace(opts) {
50
- opts.signal?.throwIfAborted();
51
- const workspace = this.requireWorkspace(opts.handle.workspaceRef);
80
+ throwIfAborted(opts.signal);
81
+ const replay = this.opResults.get(opts.idempotencyKey);
82
+ if (replay)
83
+ return replay;
84
+ const workspace = this.requireLiveWorkspace(opts.handle.workspaceRef);
85
+ const payloadBytes = opts.checkpointPayload === undefined ? 0 : byteLength(opts.checkpointPayload);
86
+ if (payloadBytes > QUOTA.maxCheckpointPayloadBytes) {
87
+ throw new WorkspaceQuotaExceededError('Checkpoint payload exceeds the size quota.', {
88
+ quota: 'maxCheckpointPayloadBytes',
89
+ limit: QUOTA.maxCheckpointPayloadBytes,
90
+ actual: payloadBytes,
91
+ workspace_ref: workspace.workspaceRef
92
+ });
93
+ }
94
+ if (workspace.bytes + payloadBytes > QUOTA.maxWorkspaceBytes) {
95
+ throw new WorkspaceQuotaExceededError('Workspace byte quota exceeded.', {
96
+ quota: 'maxWorkspaceBytes',
97
+ limit: QUOTA.maxWorkspaceBytes,
98
+ actual: workspace.bytes + payloadBytes,
99
+ workspace_ref: workspace.workspaceRef
100
+ });
101
+ }
102
+ const committedAt = new Date().toISOString();
52
103
  workspace.state = 'paused';
53
- workspace.updatedAt = new Date().toISOString();
104
+ workspace.updatedAt = committedAt;
105
+ workspace.bytes += payloadBytes;
106
+ const expiresAt = expiryFor('paused', committedAt);
54
107
  const checkpoint = {
55
108
  workspaceRef: workspace.workspaceRef,
56
109
  checkpointRef: `${workspace.workspaceRef}:checkpoint:${opts.sequence}`,
@@ -59,61 +112,101 @@ export class InMemoryDurableWorkspaceStore {
59
112
  stepId: opts.stepId,
60
113
  sequence: opts.sequence,
61
114
  attempt: opts.attempt,
62
- committedAt: workspace.updatedAt,
115
+ committedAt,
116
+ ...(expiresAt ? { expiresAt } : {}),
117
+ sizeBytes: payloadBytes,
63
118
  metadata: {
64
119
  reason: opts.reason,
65
120
  ...(opts.checkpointPayload !== undefined ? { checkpointPayload: opts.checkpointPayload } : {})
66
121
  }
67
122
  };
68
123
  workspace.checkpoints.push(checkpoint);
124
+ this.opResults.set(opts.idempotencyKey, checkpoint);
69
125
  return checkpoint;
70
126
  }
71
127
  async resumeWorkspace(opts) {
72
- opts.signal?.throwIfAborted();
73
- const workspace = this.requireWorkspace(opts.workspaceRef);
128
+ throwIfAborted(opts.signal);
129
+ const replay = this.opResults.get(opts.idempotencyKey);
130
+ if (replay)
131
+ return replay;
132
+ const workspace = this.workspaces.get(opts.workspaceRef);
133
+ if (!workspace || workspace.state === 'cleaned') {
134
+ throw new WorkspaceError('Workspace not found.', { reason: 'not_found', workspace_ref: opts.workspaceRef });
135
+ }
136
+ if (workspace.state === 'aborted') {
137
+ throw new WorkspaceError('Workspace was aborted and cannot resume.', { reason: 'aborted', workspace_ref: opts.workspaceRef });
138
+ }
139
+ if (this.isExpired(workspace)) {
140
+ throw new WorkspaceError('Workspace has expired.', { reason: 'expired', workspace_ref: opts.workspaceRef });
141
+ }
74
142
  if (opts.checkpointRef && !workspace.checkpoints.some((checkpoint) => checkpoint.checkpointRef === opts.checkpointRef)) {
75
- throw new Error(`Unknown workspace checkpoint: ${opts.checkpointRef}`);
143
+ throw new WorkspaceError('Workspace checkpoint not found.', { reason: 'missing_checkpoint', workspace_ref: opts.workspaceRef, checkpoint_ref: opts.checkpointRef });
76
144
  }
77
145
  workspace.state = 'active';
78
146
  workspace.runId = opts.runId;
79
147
  workspace.sessionId = opts.sessionId;
80
148
  workspace.attempt = opts.attempt;
81
149
  workspace.updatedAt = new Date().toISOString();
82
- return {
83
- workspaceRef: workspace.workspaceRef,
84
- runId: workspace.runId,
85
- sessionId: workspace.sessionId,
86
- state: 'active',
87
- startedAt: workspace.updatedAt,
88
- attempt: workspace.attempt,
89
- metadata: workspace.metadata
90
- };
150
+ const handle = this.toHandle(workspace);
151
+ this.opResults.set(opts.idempotencyKey, handle);
152
+ return handle;
91
153
  }
92
154
  async abortWorkspace(opts) {
93
- opts.signal?.throwIfAborted();
94
- const workspace = this.requireWorkspace(opts.workspaceRef);
155
+ throwIfAborted(opts.signal);
156
+ const replay = this.opResults.get(opts.idempotencyKey);
157
+ if (replay)
158
+ return replay;
159
+ const workspace = this.workspaces.get(opts.workspaceRef);
160
+ if (!workspace || workspace.state === 'cleaned') {
161
+ throw new WorkspaceError('Workspace not found.', { reason: 'not_found', workspace_ref: opts.workspaceRef });
162
+ }
163
+ const abortedAt = new Date().toISOString();
95
164
  workspace.state = 'aborted';
96
- workspace.updatedAt = new Date().toISOString();
97
- return { workspaceRef: opts.workspaceRef, state: 'aborted', abortedAt: workspace.updatedAt };
165
+ workspace.updatedAt = abortedAt;
166
+ const cleanupEligibleAt = expiryFor('aborted', abortedAt);
167
+ const result = { workspaceRef: opts.workspaceRef, state: 'aborted', abortedAt, ...(cleanupEligibleAt ? { cleanupEligibleAt } : {}) };
168
+ this.opResults.set(opts.idempotencyKey, result);
169
+ return result;
98
170
  }
99
171
  async cleanupWorkspace(opts) {
100
- opts.signal?.throwIfAborted();
101
- const workspace = this.requireWorkspace(opts.workspaceRef);
172
+ throwIfAborted(opts.signal);
173
+ const replay = this.opResults.get(opts.idempotencyKey);
174
+ if (replay)
175
+ return replay;
176
+ const workspace = this.workspaces.get(opts.workspaceRef);
177
+ const completedAt = new Date().toISOString();
178
+ // Cleanup is idempotent: an already-cleaned (or unknown) workspace returns a
179
+ // terminal cleaned result rather than throwing.
180
+ if (!workspace || workspace.state === 'cleaned') {
181
+ const result = { workspaceRef: opts.workspaceRef, state: 'cleaned', completedAt, deletedBytes: 0, deletedFiles: 0 };
182
+ this.opResults.set(opts.idempotencyKey, result);
183
+ return result;
184
+ }
185
+ const deletedBytes = workspace.bytes;
186
+ const deletedFiles = workspace.checkpoints.length;
102
187
  workspace.state = 'cleaned';
103
- workspace.updatedAt = new Date().toISOString();
104
- this.workspaces.delete(opts.workspaceRef);
105
- return { workspaceRef: opts.workspaceRef, state: 'cleaned', completedAt: workspace.updatedAt };
188
+ workspace.updatedAt = completedAt;
189
+ workspace.checkpoints = [];
190
+ workspace.bytes = 0;
191
+ const result = { workspaceRef: opts.workspaceRef, state: 'cleaned', completedAt, deletedBytes, deletedFiles };
192
+ this.opResults.set(opts.idempotencyKey, result);
193
+ return result;
106
194
  }
107
195
  async inspectWorkspace(opts) {
108
- opts.signal?.throwIfAborted();
196
+ throwIfAborted(opts.signal);
109
197
  const workspaceRef = opts.workspaceRef ?? this.findWorkspaceByCheckpoint(opts.checkpointRef);
110
- const workspace = this.requireWorkspace(workspaceRef);
198
+ const workspace = this.workspaces.get(workspaceRef);
199
+ if (!workspace) {
200
+ throw new WorkspaceError('Workspace not found.', { reason: 'not_found', workspace_ref: workspaceRef });
201
+ }
111
202
  const latest = workspace.checkpoints.at(-1);
203
+ const expiresAt = expiryFor(workspace.state, workspace.updatedAt);
112
204
  return {
113
205
  workspaceRef: workspace.workspaceRef,
114
206
  state: workspace.state,
115
207
  checkpoints: workspace.checkpoints,
116
208
  ...(latest ? { currentCheckpointRef: latest.checkpointRef } : {}),
209
+ ...(expiresAt ? { expiresAt, cleanupEligibleAt: expiresAt } : {}),
117
210
  retention: this.info.policy.retention,
118
211
  quota: this.info.policy.quota,
119
212
  createdAt: workspace.createdAt,
@@ -121,21 +214,71 @@ export class InMemoryDurableWorkspaceStore {
121
214
  metadata: workspace.metadata
122
215
  };
123
216
  }
217
+ toHandle(workspace) {
218
+ return {
219
+ workspaceRef: workspace.workspaceRef,
220
+ runId: workspace.runId,
221
+ sessionId: workspace.sessionId,
222
+ state: 'active',
223
+ startedAt: workspace.updatedAt,
224
+ attempt: workspace.attempt,
225
+ metadata: workspace.metadata
226
+ };
227
+ }
228
+ isExpired(workspace) {
229
+ const expiresAt = expiryFor(workspace.state, workspace.updatedAt);
230
+ return expiresAt !== undefined && Date.parse(expiresAt) <= Date.now();
231
+ }
124
232
  findWorkspaceByCheckpoint(checkpointRef) {
125
- if (!checkpointRef)
126
- throw new Error('workspaceRef or checkpointRef is required.');
233
+ if (!checkpointRef) {
234
+ throw new WorkspaceError('workspaceRef or checkpointRef is required.', { reason: 'invalid_reference' });
235
+ }
127
236
  const found = [...this.workspaces.values()].find((workspace) => workspace.checkpoints.some((checkpoint) => checkpoint.checkpointRef === checkpointRef));
128
- if (!found)
129
- throw new Error(`Unknown workspace checkpoint: ${checkpointRef}`);
237
+ if (!found) {
238
+ throw new WorkspaceError('Workspace checkpoint not found.', { reason: 'invalid_reference', checkpoint_ref: checkpointRef });
239
+ }
130
240
  return found.workspaceRef;
131
241
  }
132
- requireWorkspace(workspaceRef) {
242
+ requireLiveWorkspace(workspaceRef) {
133
243
  const workspace = this.workspaces.get(workspaceRef);
134
- if (!workspace)
135
- throw new Error(`Unknown workspace: ${workspaceRef}`);
244
+ if (!workspace || workspace.state === 'cleaned') {
245
+ throw new WorkspaceError('Workspace not found.', { reason: 'not_found', workspace_ref: workspaceRef });
246
+ }
247
+ if (workspace.state === 'aborted') {
248
+ throw new WorkspaceError('Workspace was aborted.', { reason: 'aborted', workspace_ref: workspaceRef });
249
+ }
136
250
  return workspace;
137
251
  }
138
252
  }
253
+ function throwIfAborted(signal) {
254
+ if (signal?.aborted) {
255
+ throw new OperationCancelledError('Workspace operation was cancelled.', { scope: 'workspace' });
256
+ }
257
+ }
258
+ function ttlForState(state) {
259
+ switch (state) {
260
+ case 'paused':
261
+ return RETENTION.pausedTtlMs;
262
+ case 'aborted':
263
+ return RETENTION.abortedTtlMs ?? RETENTION.terminalFailureTtlMs;
264
+ default:
265
+ return undefined;
266
+ }
267
+ }
268
+ function expiryFor(state, fromIso) {
269
+ const ttl = ttlForState(state);
270
+ if (ttl === undefined || ttl <= 0)
271
+ return undefined;
272
+ return new Date(Date.parse(fromIso) + ttl).toISOString();
273
+ }
274
+ function byteLength(value) {
275
+ try {
276
+ return Buffer.byteLength(JSON.stringify(value) ?? '', 'utf8');
277
+ }
278
+ catch {
279
+ return 0;
280
+ }
281
+ }
139
282
  /** Creates a fresh in-process durable workspace store. */
140
283
  export function inMemoryDurableWorkspaceStore() {
141
284
  return new InMemoryDurableWorkspaceStore();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@purista/harness",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Self-hosted enterprise agent harness for typed tools, agents, workflows, state, sandboxing, and telemetry.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,62 +0,0 @@
1
- import type { JsonValue } from './types.js';
2
- export declare class HarnessError extends Error {
3
- code: string;
4
- category: string;
5
- retriable: boolean;
6
- meta?: Record<string, JsonValue>;
7
- constructor(message: string, opts: {
8
- code: string;
9
- category: string;
10
- retriable: boolean;
11
- meta?: Record<string, JsonValue>;
12
- });
13
- }
14
- export declare class HarnessConfigError extends HarnessError {
15
- constructor(message: string, meta?: Record<string, JsonValue>);
16
- }
17
- export declare class ValidationError extends HarnessError {
18
- constructor(message: string, meta?: Record<string, JsonValue>);
19
- }
20
- export declare class PermissionDeniedError extends HarnessError {
21
- constructor(message: string, meta?: Record<string, JsonValue>);
22
- }
23
- export declare class SandboxError extends HarnessError {
24
- constructor(message: string, meta?: Record<string, JsonValue>);
25
- }
26
- export declare class SandboxNoExecutorError extends HarnessError {
27
- constructor();
28
- }
29
- export declare class ModelError extends HarnessError {
30
- constructor(message: string, meta?: Record<string, JsonValue>);
31
- }
32
- export declare class ToolError extends HarnessError {
33
- constructor(message: string, meta?: Record<string, JsonValue>);
34
- }
35
- export declare class ToolNotFoundError extends HarnessError {
36
- constructor(tool: string);
37
- }
38
- export declare class SkillManifestError extends HarnessError {
39
- constructor(message: string, meta?: Record<string, JsonValue>);
40
- }
41
- export declare class AgentLoopBudgetError extends HarnessError {
42
- constructor();
43
- }
44
- export declare class SessionBusyError extends HarnessError {
45
- constructor(reason?: string);
46
- }
47
- export declare class OperationTimeoutError extends HarnessError {
48
- constructor(scope: string);
49
- }
50
- export declare class OperationCancelledError extends HarnessError {
51
- constructor(scope: string);
52
- }
53
- export declare class InternalError extends HarnessError {
54
- constructor(message: string, meta?: Record<string, JsonValue>);
55
- }
56
- export declare function serializeError(error: unknown): {
57
- code: string;
58
- category: string;
59
- retriable: boolean;
60
- message: string;
61
- meta?: Record<string, JsonValue>;
62
- };
@@ -1,67 +0,0 @@
1
- export class HarnessError extends Error {
2
- code;
3
- category;
4
- retriable;
5
- meta;
6
- constructor(message, opts) {
7
- super(message);
8
- this.name = new.target.name;
9
- this.code = opts.code;
10
- this.category = opts.category;
11
- this.retriable = opts.retriable;
12
- if (opts.meta !== undefined)
13
- this.meta = opts.meta;
14
- }
15
- }
16
- function errorOptions(opts, meta) {
17
- return meta === undefined ? opts : { ...opts, meta };
18
- }
19
- export class HarnessConfigError extends HarnessError {
20
- constructor(message, meta) { super(message, errorOptions({ code: 'HARNESS_CONFIG_ERROR', category: 'config', retriable: false }, meta)); }
21
- }
22
- export class ValidationError extends HarnessError {
23
- constructor(message, meta) { super(message, errorOptions({ code: 'VALIDATION_ERROR', category: 'validation', retriable: false }, meta)); }
24
- }
25
- export class PermissionDeniedError extends HarnessError {
26
- constructor(message, meta) { super(message, errorOptions({ code: 'PERMISSION_DENIED', category: 'permission', retriable: false }, meta)); }
27
- }
28
- export class SandboxError extends HarnessError {
29
- constructor(message, meta) { super(message, errorOptions({ code: 'SANDBOX_ERROR', category: 'sandbox', retriable: false }, meta)); }
30
- }
31
- export class SandboxNoExecutorError extends HarnessError {
32
- constructor() { super('Sandbox executor unavailable', { code: 'SANDBOX_NO_EXECUTOR', category: 'sandbox', retriable: false }); }
33
- }
34
- export class ModelError extends HarnessError {
35
- constructor(message, meta) { super(message, errorOptions({ code: 'MODEL_ERROR', category: 'model', retriable: true }, meta)); }
36
- }
37
- export class ToolError extends HarnessError {
38
- constructor(message, meta) { super(message, errorOptions({ code: 'TOOL_ERROR', category: 'tool', retriable: true }, meta)); }
39
- }
40
- export class ToolNotFoundError extends HarnessError {
41
- constructor(tool) { super(`Tool not found: ${tool}`, { code: 'TOOL_NOT_FOUND', category: 'tool', retriable: false, meta: { tool } }); }
42
- }
43
- export class SkillManifestError extends HarnessError {
44
- constructor(message, meta) { super(message, errorOptions({ code: 'SKILL_MANIFEST_ERROR', category: 'config', retriable: false }, meta)); }
45
- }
46
- export class AgentLoopBudgetError extends HarnessError {
47
- constructor() { super('Agent maxSteps exceeded', { code: 'AGENT_LOOP_BUDGET', category: 'agent', retriable: false, meta: { reason: 'iterations_exceeded' } }); }
48
- }
49
- export class SessionBusyError extends HarnessError {
50
- constructor(reason = 'run_in_flight') { super('Session is busy', { code: 'SESSION_BUSY', category: 'session', retriable: true, meta: { reason } }); }
51
- }
52
- export class OperationTimeoutError extends HarnessError {
53
- constructor(scope) { super('Operation timed out', { code: 'OPERATION_TIMEOUT', category: 'harness', retriable: true, meta: { scope } }); }
54
- }
55
- export class OperationCancelledError extends HarnessError {
56
- constructor(scope) { super('Operation cancelled', { code: 'OPERATION_CANCELLED', category: 'harness', retriable: false, meta: { scope } }); }
57
- }
58
- export class InternalError extends HarnessError {
59
- constructor(message, meta) { super(message, errorOptions({ code: 'INTERNAL_ERROR', category: 'internal', retriable: false }, meta)); }
60
- }
61
- export function serializeError(error) {
62
- if (error instanceof HarnessError) {
63
- const serialized = { code: error.code, category: error.category, retriable: error.retriable, message: error.message };
64
- return error.meta === undefined ? serialized : { ...serialized, meta: error.meta };
65
- }
66
- return { code: 'INTERNAL_ERROR', category: 'internal', retriable: false, message: error instanceof Error ? error.message : String(error) };
67
- }