@purista/harness 1.5.0 → 1.5.1
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/agents/index.js +72 -8
- package/dist/harness/defineHarness.d.ts +58 -2
- package/dist/index.d.ts +2 -2
- package/dist/runtime/steps.d.ts +22 -1
- package/dist/runtime/steps.js +53 -2
- package/dist/sessions/index.d.ts +3 -3
- package/dist/sessions/index.js +14 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -5
package/dist/agents/index.js
CHANGED
|
@@ -129,8 +129,7 @@ async function runDefaultAgentInner(args) {
|
|
|
129
129
|
const outputSchema = args.agent.output ?? z.string();
|
|
130
130
|
const parsedInput = parseAgentSchema(inputSchema, args.input, 'agent_input');
|
|
131
131
|
const selectedModelAlias = args.modelAlias ?? args.agent.model;
|
|
132
|
-
|
|
133
|
-
if (!model)
|
|
132
|
+
if (!args.models[selectedModelAlias])
|
|
134
133
|
throw new ValidationError('Unknown model alias', { where: 'agent_input', issues: { model: selectedModelAlias } });
|
|
135
134
|
const skillIds = args.agent.skills ?? [];
|
|
136
135
|
await mountSkillsOnce(args.session, args.mountedSkills, args.skills, skillIds);
|
|
@@ -175,6 +174,7 @@ async function runDefaultAgentInner(args) {
|
|
|
175
174
|
});
|
|
176
175
|
const mcpSpecs = args.mcpRegistry ? await getMcpToolSpecs(args.customTools, enabledCustomTools, { registry: args.mcpRegistry, signal: args.signal, toolTimeoutMs: args.toolTimeoutMs, sandbox: args.session, sandboxKey: args.sessionId }) : [];
|
|
177
176
|
const customSpecs = [...tsCustomSpecs, ...mcpSpecs];
|
|
177
|
+
const allToolSpecs = [...builtinSpecs, ...customSpecs];
|
|
178
178
|
const nonSystem = args.history.filter((m) => m.role !== 'system');
|
|
179
179
|
const system = args.history.filter((m) => m.role === 'system');
|
|
180
180
|
const cappedNonSystem = args.historyWindow === undefined ? nonSystem : args.historyWindow === 0 ? [] : nonSystem.slice(-args.historyWindow);
|
|
@@ -201,19 +201,42 @@ async function runDefaultAgentInner(args) {
|
|
|
201
201
|
throw abortError(args.signal, 'run', 'Run was cancelled.');
|
|
202
202
|
if (steps >= maxSteps)
|
|
203
203
|
throw new AgentLoopBudgetError('Agent loop budget exceeded.', { agent_id: args.agentId, reason: 'iterations_exceeded', limit: maxSteps });
|
|
204
|
+
const prepared = await args.agent.prepareStep?.({
|
|
205
|
+
input: parsedInput,
|
|
206
|
+
runId: args.runId,
|
|
207
|
+
sessionId: args.sessionId,
|
|
208
|
+
history: { list: async () => args.history },
|
|
209
|
+
memory: args.memory,
|
|
210
|
+
checkpoints: args.checkpoints,
|
|
211
|
+
metadata: args.metadata ?? {},
|
|
212
|
+
metrics: args.metrics,
|
|
213
|
+
step: steps,
|
|
214
|
+
model: selectedModelAlias,
|
|
215
|
+
messages: modelMessages,
|
|
216
|
+
tools: allToolSpecs
|
|
217
|
+
});
|
|
218
|
+
const stepModelAlias = prepared?.model ?? selectedModelAlias;
|
|
219
|
+
const model = args.models[stepModelAlias];
|
|
220
|
+
if (!model)
|
|
221
|
+
throw new ValidationError('Unknown model alias', { where: 'agent_input', issues: { model: stepModelAlias } });
|
|
222
|
+
const stepTools = filterActiveTools(allToolSpecs, prepared?.activeTools, args.agentId);
|
|
223
|
+
const stepMessages = prepared?.messages ? [...prepared.messages] : modelMessages;
|
|
224
|
+
const stepInstructions = prepared?.instructions ?? instructions;
|
|
204
225
|
const response = await model.object({
|
|
205
226
|
messages: [
|
|
206
|
-
{ role: 'system', content:
|
|
207
|
-
...
|
|
227
|
+
{ role: 'system', content: stepInstructions },
|
|
228
|
+
...stepMessages
|
|
208
229
|
],
|
|
209
|
-
tools:
|
|
210
|
-
schema: z.toJSONSchema(outputSchema)
|
|
230
|
+
tools: stepTools,
|
|
231
|
+
schema: z.toJSONSchema(outputSchema),
|
|
232
|
+
...(prepared?.call ? { call: prepared.call } : {})
|
|
211
233
|
}, args.signal, {
|
|
212
234
|
harnessName: args.harnessName,
|
|
213
235
|
sessionId: args.sessionId,
|
|
214
236
|
runId: args.runId,
|
|
215
237
|
...(args.workflowId ? { workflowId: args.workflowId } : {}),
|
|
216
|
-
agentId: args.agentId
|
|
238
|
+
agentId: args.agentId,
|
|
239
|
+
modelAlias: stepModelAlias
|
|
217
240
|
});
|
|
218
241
|
// Emit one usage-bearing model event per model round-trip (including
|
|
219
242
|
// tool-call steps) so run-summary modelCalls and tokenTotals are accurate
|
|
@@ -223,11 +246,17 @@ async function runDefaultAgentInner(args) {
|
|
|
223
246
|
runId: args.runId,
|
|
224
247
|
agentId: args.agentId,
|
|
225
248
|
...(args.workflowId ? { workflowId: args.workflowId } : {}),
|
|
226
|
-
modelAlias:
|
|
249
|
+
modelAlias: stepModelAlias,
|
|
227
250
|
object: (response.object ?? null),
|
|
228
251
|
usage: response.usage
|
|
229
252
|
});
|
|
230
253
|
const toolCalls = (response.toolCalls ?? []);
|
|
254
|
+
if (await shouldStopAgentLoop(args, parsedInput, stepModelAlias, steps, modelMessages, allToolSpecs, response, toolCalls)) {
|
|
255
|
+
const validated = parseAgentSchema(outputSchema, response.object, 'agent_output');
|
|
256
|
+
emitted.push({ id: `msg_${ulid()}_a`, sessionId: args.sessionId, runId: args.runId, role: 'assistant', content: JSON.stringify(validated), timestamp: new Date().toISOString() });
|
|
257
|
+
await args.emitEvent?.({ type: 'agent.finished', runId: args.runId, agentId: args.agentId, at: new Date().toISOString(), output: validated, ...agentEventMeta });
|
|
258
|
+
return { output: validated, emitted };
|
|
259
|
+
}
|
|
231
260
|
if (toolCalls.length === 0) {
|
|
232
261
|
const validated = parseAgentSchema(outputSchema, response.object, 'agent_output');
|
|
233
262
|
emitted.push({ id: `msg_${ulid()}_a`, sessionId: args.sessionId, runId: args.runId, role: 'assistant', content: JSON.stringify(validated), timestamp: new Date().toISOString() });
|
|
@@ -262,6 +291,41 @@ async function runDefaultAgentInner(args) {
|
|
|
262
291
|
throw error;
|
|
263
292
|
}
|
|
264
293
|
}
|
|
294
|
+
function filterActiveTools(tools, activeTools, agentId) {
|
|
295
|
+
if (!activeTools)
|
|
296
|
+
return [...tools];
|
|
297
|
+
const requested = new Set(activeTools);
|
|
298
|
+
const filtered = tools.filter((tool) => requested.has(tool.name));
|
|
299
|
+
if (filtered.length !== requested.size) {
|
|
300
|
+
const available = new Set(tools.map((tool) => tool.name));
|
|
301
|
+
const unknown = [...requested].filter((name) => !available.has(name));
|
|
302
|
+
throw new ValidationError('prepareStep referenced an unknown active tool.', {
|
|
303
|
+
where: 'agent_input',
|
|
304
|
+
issues: { agentId, activeTools: unknown }
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return filtered;
|
|
308
|
+
}
|
|
309
|
+
async function shouldStopAgentLoop(args, input, selectedModelAlias, step, messages, tools, response, toolCalls) {
|
|
310
|
+
if (!args.agent.stopWhen)
|
|
311
|
+
return false;
|
|
312
|
+
return args.agent.stopWhen({
|
|
313
|
+
input,
|
|
314
|
+
runId: args.runId,
|
|
315
|
+
sessionId: args.sessionId,
|
|
316
|
+
history: { list: async () => args.history },
|
|
317
|
+
memory: args.memory,
|
|
318
|
+
checkpoints: args.checkpoints,
|
|
319
|
+
metadata: args.metadata ?? {},
|
|
320
|
+
metrics: args.metrics,
|
|
321
|
+
step,
|
|
322
|
+
model: selectedModelAlias,
|
|
323
|
+
messages,
|
|
324
|
+
tools,
|
|
325
|
+
response,
|
|
326
|
+
toolCalls
|
|
327
|
+
});
|
|
328
|
+
}
|
|
265
329
|
/** Runs `fn` over `items` with bounded concurrency, preserving input order. */
|
|
266
330
|
export async function runLimited(items, limit, fn) {
|
|
267
331
|
const concurrency = Math.max(1, Math.min(limit, items.length));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { type Logger } from '../logger/index.js';
|
|
3
|
-
import type { ModelAlias, ModelCapability, TokenUsage } from '../ports/model-provider.js';
|
|
3
|
+
import type { ModelAlias, ModelCapability, ObjectResponse, ToolCallSpec, ModelMessage, ModelToolSpec, TokenUsage, ModelCallOptions } from '../ports/model-provider.js';
|
|
4
4
|
import type { StateStore } from '../ports/state.js';
|
|
5
5
|
import type { Metrics, TelemetryShim } from '../telemetry/index.js';
|
|
6
6
|
import type { HarnessAdapterContext } from '../ports/harness-context.js';
|
|
@@ -15,6 +15,7 @@ import type { HarnessError } from '../errors/harness-error.js';
|
|
|
15
15
|
import { type Sandbox } from '../sandbox/index.js';
|
|
16
16
|
import type { ModelHandle } from '../models/registry.js';
|
|
17
17
|
import { type AdapterCapability, type DurableRuntimeAdapter, type HarnessInspection } from '../ports/capabilities.js';
|
|
18
|
+
import type { DurableStepOptions } from '../runtime/steps.js';
|
|
18
19
|
/** Stable harness version string for diagnostics and generated documentation. */
|
|
19
20
|
export { HARNESS_VERSION } from '../version.js';
|
|
20
21
|
/** OpenTelemetry capture controls used by the harness. */
|
|
@@ -365,6 +366,41 @@ export interface AgentContextMinimal<S extends BuilderState, I> {
|
|
|
365
366
|
metadata: Readonly<Record<string, JsonValue>>;
|
|
366
367
|
metrics: Metrics;
|
|
367
368
|
}
|
|
369
|
+
/** Context passed before each default agent loop model call. */
|
|
370
|
+
export interface AgentPrepareStepContext<S extends BuilderState, I> extends AgentContextMinimal<S, I> {
|
|
371
|
+
/** Zero-based model-call step in the default loop. */
|
|
372
|
+
step: number;
|
|
373
|
+
/** Model alias selected for this step before overrides are applied. */
|
|
374
|
+
model: keyof NonNullable<S['models']> & string;
|
|
375
|
+
/** Messages that would be sent to the model for this step. */
|
|
376
|
+
messages: readonly ModelMessage[];
|
|
377
|
+
/** Model-facing tools that would be available for this step. */
|
|
378
|
+
tools: readonly ModelToolSpec[];
|
|
379
|
+
}
|
|
380
|
+
/** Per-step overrides returned from `AgentDefinition.prepareStep`. */
|
|
381
|
+
export interface AgentPrepareStepResult<S extends BuilderState> {
|
|
382
|
+
/** Optional model alias override for this model call. */
|
|
383
|
+
model?: keyof NonNullable<S['models']> & string;
|
|
384
|
+
/** Optional instruction override for this model call only. */
|
|
385
|
+
instructions?: string;
|
|
386
|
+
/** Optional model-facing tool names to keep active for this model call. */
|
|
387
|
+
activeTools?: readonly string[];
|
|
388
|
+
/** Optional message override for this model call only. */
|
|
389
|
+
messages?: readonly ModelMessage[];
|
|
390
|
+
/** Optional generation settings for this model call only. */
|
|
391
|
+
call?: ModelCallOptions;
|
|
392
|
+
}
|
|
393
|
+
/** Context passed after a default agent loop model call to decide whether to stop. */
|
|
394
|
+
export interface AgentStopWhenContext<S extends BuilderState, I> extends AgentPrepareStepContext<S, I> {
|
|
395
|
+
/** Raw provider-normalized object response from the current model call. */
|
|
396
|
+
response: ObjectResponse<JsonValue>;
|
|
397
|
+
/** Tool calls requested by the current model response. */
|
|
398
|
+
toolCalls: readonly ToolCallSpec[];
|
|
399
|
+
}
|
|
400
|
+
/** Hook used to prepare each model call in the default agent loop. */
|
|
401
|
+
export type AgentPrepareStep<S extends BuilderState, I> = (ctx: AgentPrepareStepContext<S, I>) => AgentPrepareStepResult<S> | Promise<AgentPrepareStepResult<S> | void> | void;
|
|
402
|
+
/** Hook used to stop the default loop after a model call. */
|
|
403
|
+
export type AgentStopWhen<S extends BuilderState, I> = (ctx: AgentStopWhenContext<S, I>) => boolean | Promise<boolean>;
|
|
368
404
|
/** Run-bound facade for explicit long-horizon context checkpoints. */
|
|
369
405
|
export interface ContextCheckpoints {
|
|
370
406
|
write(input: {
|
|
@@ -404,7 +440,7 @@ export interface WorkflowContext<S extends BuilderState, I, O> {
|
|
|
404
440
|
* checkpointed and replayed on resume without re-running `fn`; otherwise it is
|
|
405
441
|
* a transparent pass-through. See spec 10 "Durable steps".
|
|
406
442
|
*/
|
|
407
|
-
step<T extends JsonValue>(stepId: string, fn: () => Promise<T
|
|
443
|
+
step<T extends JsonValue>(stepId: string, fn: () => Promise<T>, options?: DurableStepOptions): Promise<T>;
|
|
408
444
|
output?: O;
|
|
409
445
|
}
|
|
410
446
|
/** Invoke options accepted by workflow-local child-agent calls. */
|
|
@@ -434,6 +470,24 @@ export interface AgentDefinition<S extends BuilderState, I extends z.ZodTypeAny
|
|
|
434
470
|
permissions?: AgentPermissions;
|
|
435
471
|
onPermission?: OnPermission;
|
|
436
472
|
maxSteps?: number;
|
|
473
|
+
/**
|
|
474
|
+
* Optional hook for per-round loop control in the default agent loop.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* ```ts
|
|
478
|
+
* prepareStep: ({ step }) => step === 0 ? { activeTools: ['lookup'] } : {}
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
prepareStep?: AgentPrepareStep<S, z.infer<I>>;
|
|
482
|
+
/**
|
|
483
|
+
* Optional hook that can stop the default loop after a model call.
|
|
484
|
+
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```ts
|
|
487
|
+
* stopWhen: ({ step }) => step >= 2
|
|
488
|
+
* ```
|
|
489
|
+
*/
|
|
490
|
+
stopWhen?: AgentStopWhen<S, z.infer<I>>;
|
|
437
491
|
handler?: (ctx: AgentContext<S, z.infer<I>, z.infer<O>>) => Promise<z.infer<O>>;
|
|
438
492
|
}
|
|
439
493
|
/** Workflow definition registered inline within `.workflows(...)`. */
|
|
@@ -458,6 +512,8 @@ type AgentDefinitionResolved<S extends BuilderState, I extends z.ZodTypeAny, O e
|
|
|
458
512
|
permissions?: AgentPermissions;
|
|
459
513
|
onPermission?: OnPermission;
|
|
460
514
|
maxSteps?: number;
|
|
515
|
+
prepareStep?: AgentPrepareStep<S, z.infer<I>>;
|
|
516
|
+
stopWhen?: AgentStopWhen<S, z.infer<I>>;
|
|
461
517
|
handler?: (ctx: AgentContext<S, z.infer<I>, z.infer<O>>) => Promise<z.infer<O>>;
|
|
462
518
|
};
|
|
463
519
|
type AgentDefinitionFor<S extends BuilderState, D> = D extends {
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type { DurableReplayCheckpoint, DurableWorkspacePolicy, DurableWorkspaceS
|
|
|
25
25
|
export { InMemoryDurableWorkspaceStore, inMemoryDurableWorkspaceStore } from './workspace/index.js';
|
|
26
26
|
export type { ContextCheckpoint, ContextCheckpointQuery, ContextCheckpointRef, ContextCheckpointStore, ContextCheckpointStoreInfo } from './ports/context-checkpoints.js';
|
|
27
27
|
export { createDurableWorkflowContext, DurableStepError, DurableRunLeaseError, DurableTerminalRunError, inMemoryDurableRuntime, isResumeBlockingRunStatus, isTerminalRunStatus } from './runtime/index.js';
|
|
28
|
-
export type { DurableActiveRunStatus, DurableWorkflowContext, DurableWorkflowContextOptions, DurableStepCommit, DurableRunLease, DurableRunStart, DurableRunStatus, DurableRuntime, DurableTerminalRunStatus, FinishRunPatch, InMemoryDurableRuntimeOptions, RunCheckpoint } from './runtime/index.js';
|
|
28
|
+
export type { DurableActiveRunStatus, DurableWorkflowContext, DurableWorkflowContextOptions, DurableStepCommit, DurableStepOptions, DurableStepRetryPolicy, DurableStepRetrySetting, DurableRunLease, DurableRunStart, DurableRunStatus, DurableRuntime, DurableTerminalRunStatus, FinishRunPatch, InMemoryDurableRuntimeOptions, RunCheckpoint } from './runtime/index.js';
|
|
29
29
|
export { bashSandbox, inMemorySandbox } from './sandbox/index.js';
|
|
30
30
|
export type { ExecCapableSandboxSession, HibernateCapableSandbox, ResumeCapableSandbox, Sandbox, SandboxProcess, SandboxResumeOptions, SandboxSession, SandboxSessionBase, SandboxSessionFor, SnapshotCapableSandbox, SnapshotResult, SpawnCapableSandboxSession, SpawnOptions } from './sandbox/index.js';
|
|
31
31
|
export type { DirEntry, ExecOptions, ExecResult, FileStat } from './harness/types.js';
|
|
@@ -35,4 +35,4 @@ export { discoverSkills } from './skills/index.js';
|
|
|
35
35
|
export { evaluateDeterministicScorer, evaluatePromptCandidates } from './eval/index.js';
|
|
36
36
|
export type { CandidateScore, DeterministicScorerDefinition, EvaluatePromptCandidatesInput, EvaluationItem, PromptCandidate, ScorerResult, ScorerTarget } from './eval/index.js';
|
|
37
37
|
export { defineHarness } from './harness/defineHarness.js';
|
|
38
|
-
export type { AgentContext, AgentContextMinimal, AgentDefinition, AgentDefinitionHelpers, AgentInput, AgentInvoker, AgentOutput, AgentPermissions, AgentsConfig, BuilderState, BuiltinToolName, ContentCaptureMode, ContextCheckpoints, ConversationHistory, DelegationDefaults, DiscoveredSkills, DiscoverSkillsOptions, DurableInvokeOptions, Harness, HarnessBuilder, HarnessDefaults, HarnessOptions, InferTypes, InvokeOptions, McpAuth, McpHttpToolDefinition, McpStdioToolDefinition, ModelHandles, ModelsConfig, OnPermission, PermissionContext, PermissionDecision, PermissionMode, PermissionPolicy, ResolvedSkill, RunEvent, RunSummary, SerializedError, Session, SkillDefinition, SkillDiagnostic, SkillFrontmatter, SkillsConfig, SkillValidationMode, TelemetryFlavor, TelemetryOptions, ToolDefinition, ToolHandlerContext, ToolsConfig, TsToolDefinition, WorkflowAgentInvokeOptions, WorkflowContext, WorkflowDefinition, WorkflowDefinitionHelpers, WorkflowDelegationPolicy, WorkflowInput, WorkflowInvoker, WorkflowOutput, WorkflowsConfig } from './harness/defineHarness.js';
|
|
38
|
+
export type { AgentContext, AgentContextMinimal, AgentDefinition, AgentDefinitionHelpers, AgentInput, AgentInvoker, AgentOutput, AgentPermissions, AgentPrepareStep, AgentPrepareStepContext, AgentPrepareStepResult, AgentStopWhen, AgentStopWhenContext, AgentsConfig, BuilderState, BuiltinToolName, ContentCaptureMode, ContextCheckpoints, ConversationHistory, DelegationDefaults, DiscoveredSkills, DiscoverSkillsOptions, DurableInvokeOptions, Harness, HarnessBuilder, HarnessDefaults, HarnessOptions, InferTypes, InvokeOptions, McpAuth, McpHttpToolDefinition, McpStdioToolDefinition, ModelHandles, ModelsConfig, OnPermission, PermissionContext, PermissionDecision, PermissionMode, PermissionPolicy, ResolvedSkill, RunEvent, RunSummary, SerializedError, Session, SkillDefinition, SkillDiagnostic, SkillFrontmatter, SkillsConfig, SkillValidationMode, TelemetryFlavor, TelemetryOptions, ToolDefinition, ToolHandlerContext, ToolsConfig, TsToolDefinition, WorkflowAgentInvokeOptions, WorkflowContext, WorkflowDefinition, WorkflowDefinitionHelpers, WorkflowDelegationPolicy, WorkflowInput, WorkflowInvoker, WorkflowOutput, WorkflowsConfig } from './harness/defineHarness.js';
|
package/dist/runtime/steps.d.ts
CHANGED
|
@@ -18,6 +18,26 @@ export interface DurableWorkflowContextOptions {
|
|
|
18
18
|
*/
|
|
19
19
|
readonly onStepCommit?: (commit: DurableStepCommit) => Promise<DurableReplayCheckpoint | undefined>;
|
|
20
20
|
}
|
|
21
|
+
/** Retry policy for a single explicit workflow step. */
|
|
22
|
+
export type DurableStepRetrySetting = boolean | DurableStepRetryPolicy;
|
|
23
|
+
/** Provider-neutral retry policy for `ctx.step(...)` boundaries. */
|
|
24
|
+
export interface DurableStepRetryPolicy {
|
|
25
|
+
/** Total attempts including the first call. Default: `3`. */
|
|
26
|
+
readonly maxAttempts?: number;
|
|
27
|
+
/** Base delay before retrying in milliseconds. Default: `100`. */
|
|
28
|
+
readonly minDelayMs?: number;
|
|
29
|
+
/** Maximum delay before retrying in milliseconds. Default: `1_000`. */
|
|
30
|
+
readonly maxDelayMs?: number;
|
|
31
|
+
/** Delay strategy. Default: `exponential`. */
|
|
32
|
+
readonly backoff?: 'fixed' | 'exponential';
|
|
33
|
+
/** Optional predicate to suppress retries for non-transient failures. */
|
|
34
|
+
readonly shouldRetry?: (error: unknown, attempt: number) => boolean | Promise<boolean>;
|
|
35
|
+
}
|
|
36
|
+
/** Per-call options for an explicit workflow step. */
|
|
37
|
+
export interface DurableStepOptions {
|
|
38
|
+
/** Retry failed step functions before a checkpoint is committed. Default: no retry. */
|
|
39
|
+
readonly retry?: DurableStepRetrySetting;
|
|
40
|
+
}
|
|
21
41
|
/** Durable workflow context that exposes explicit checkpoint boundaries. */
|
|
22
42
|
export interface DurableWorkflowContext {
|
|
23
43
|
/** Current durable run lease. */
|
|
@@ -30,7 +50,7 @@ export interface DurableWorkflowContext {
|
|
|
30
50
|
* const prepared = await ctx.step('prepare-inputs', async () => ({ ok: true }))
|
|
31
51
|
* ```
|
|
32
52
|
*/
|
|
33
|
-
step<T extends JsonValue>(stepId: string, fn: () => Promise<T
|
|
53
|
+
step<T extends JsonValue>(stepId: string, fn: () => Promise<T>, options?: DurableStepOptions): Promise<T>;
|
|
34
54
|
}
|
|
35
55
|
/** Error thrown when a durable step id is invalid or duplicated. */
|
|
36
56
|
export declare class DurableStepError extends Error {
|
|
@@ -38,3 +58,4 @@ export declare class DurableStepError extends Error {
|
|
|
38
58
|
}
|
|
39
59
|
/** Creates a durable workflow context bound to an acquired runtime lease. */
|
|
40
60
|
export declare function createDurableWorkflowContext(runtime: DurableRuntime, lease: DurableRunLease, options?: DurableWorkflowContextOptions): DurableWorkflowContext;
|
|
61
|
+
export declare function runStepWithRetry<T>(fn: () => Promise<T>, retry: DurableStepRetrySetting | undefined): Promise<T>;
|
package/dist/runtime/steps.js
CHANGED
|
@@ -18,7 +18,7 @@ export function createDurableWorkflowContext(runtime, lease, options = {}) {
|
|
|
18
18
|
let sequence = (lease.checkpoints ?? []).reduce((max, checkpoint) => Math.max(max, checkpoint.sequence), 0);
|
|
19
19
|
return {
|
|
20
20
|
lease,
|
|
21
|
-
async step(stepId, fn) {
|
|
21
|
+
async step(stepId, fn, stepOptions = {}) {
|
|
22
22
|
validateStepId(stepId);
|
|
23
23
|
if (completed.has(stepId)) {
|
|
24
24
|
throw new DurableStepError(`Duplicate durable step id "${stepId}".`);
|
|
@@ -29,7 +29,7 @@ export function createDurableWorkflowContext(runtime, lease, options = {}) {
|
|
|
29
29
|
if (replay.has(stepId)) {
|
|
30
30
|
return replay.get(stepId);
|
|
31
31
|
}
|
|
32
|
-
const output = await fn
|
|
32
|
+
const output = await runStepWithRetry(fn, stepOptions.retry);
|
|
33
33
|
assertJsonSerializable(output, stepId);
|
|
34
34
|
sequence += 1;
|
|
35
35
|
// Workspace state is written before the runtime checkpoint (spec 21 §10),
|
|
@@ -54,6 +54,57 @@ export function createDurableWorkflowContext(runtime, lease, options = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
|
+
export async function runStepWithRetry(fn, retry) {
|
|
58
|
+
const policy = normalizeRetryPolicy(retry);
|
|
59
|
+
let attempt = 0;
|
|
60
|
+
let lastError;
|
|
61
|
+
while (attempt < policy.maxAttempts) {
|
|
62
|
+
attempt += 1;
|
|
63
|
+
try {
|
|
64
|
+
return await fn();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
lastError = error;
|
|
68
|
+
if (attempt >= policy.maxAttempts)
|
|
69
|
+
break;
|
|
70
|
+
if (policy.shouldRetry && !await policy.shouldRetry(error, attempt))
|
|
71
|
+
break;
|
|
72
|
+
await sleep(retryDelayMs(policy, attempt));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw lastError;
|
|
76
|
+
}
|
|
77
|
+
function normalizeRetryPolicy(retry) {
|
|
78
|
+
if (!retry) {
|
|
79
|
+
return { maxAttempts: 1, minDelayMs: 0, maxDelayMs: 0, backoff: 'fixed' };
|
|
80
|
+
}
|
|
81
|
+
if (retry === true) {
|
|
82
|
+
return { maxAttempts: 3, minDelayMs: 100, maxDelayMs: 1_000, backoff: 'exponential' };
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
maxAttempts: clampPositiveInteger(retry.maxAttempts ?? 3),
|
|
86
|
+
minDelayMs: Math.max(0, retry.minDelayMs ?? 100),
|
|
87
|
+
maxDelayMs: Math.max(0, retry.maxDelayMs ?? 1_000),
|
|
88
|
+
backoff: retry.backoff ?? 'exponential',
|
|
89
|
+
...(retry.shouldRetry ? { shouldRetry: retry.shouldRetry } : {})
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function clampPositiveInteger(value) {
|
|
93
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 1;
|
|
94
|
+
}
|
|
95
|
+
function retryDelayMs(policy, attempt) {
|
|
96
|
+
if (policy.maxDelayMs === 0)
|
|
97
|
+
return 0;
|
|
98
|
+
const base = policy.backoff === 'fixed'
|
|
99
|
+
? policy.minDelayMs
|
|
100
|
+
: policy.minDelayMs * 2 ** Math.max(0, attempt - 1);
|
|
101
|
+
return Math.min(policy.maxDelayMs, base);
|
|
102
|
+
}
|
|
103
|
+
function sleep(ms) {
|
|
104
|
+
if (ms <= 0)
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
107
|
+
}
|
|
57
108
|
function validateStepId(stepId) {
|
|
58
109
|
if (!STEP_ID_PATTERN.test(stepId)) {
|
|
59
110
|
throw new DurableStepError(`Invalid durable step id "${stepId}".`);
|
package/dist/sessions/index.d.ts
CHANGED
|
@@ -40,9 +40,9 @@ type HarnessDefinition<S extends BuilderState> = {
|
|
|
40
40
|
* promise-notified rather than time-polled, so there is no fixed per-event
|
|
41
41
|
* latency or periodic timer.
|
|
42
42
|
*
|
|
43
|
-
* Abandoning the stream (`break` / `iterator.return()`)
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* Abandoning the stream (`break` / `iterator.return()`) only detaches that
|
|
44
|
+
* consumer. It does not abort `relaySignal`; callers must pass `opts.signal`
|
|
45
|
+
* when they intend to cancel the underlying run.
|
|
46
46
|
*/
|
|
47
47
|
export declare function relayRunEvents(run: (onEvent: (event: RunEvent) => Promise<void>, relaySignal: AbortSignal) => Promise<unknown>): AsyncIterable<RunEvent>;
|
|
48
48
|
export declare function createSessionHarness<S extends BuilderState>(definition: HarnessDefinition<S>): Harness<S>;
|
package/dist/sessions/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { runDefaultAgent } from '../agents/index.js';
|
|
|
4
4
|
import { runWorkflow } from '../workflows/index.js';
|
|
5
5
|
import { createMemoryFacade, createSessionMemory } from '../ports/memory.js';
|
|
6
6
|
import { beginDurableWorkflow, DURABLE_RUN_ID_PATTERN, isExecutableDurableRuntime } from '../runtime/sessionDurable.js';
|
|
7
|
+
import { runStepWithRetry } from '../runtime/steps.js';
|
|
7
8
|
import { HarnessConfigError } from '../errors/catalog.js';
|
|
8
9
|
import { loadSkillsSync } from '../skills/index.js';
|
|
9
10
|
import { createModelRegistry } from '../models/registry.js';
|
|
@@ -49,9 +50,9 @@ const STREAM_UNDROPPABLE_EVENT_TYPES = new Set(['run.finished']);
|
|
|
49
50
|
* promise-notified rather than time-polled, so there is no fixed per-event
|
|
50
51
|
* latency or periodic timer.
|
|
51
52
|
*
|
|
52
|
-
* Abandoning the stream (`break` / `iterator.return()`)
|
|
53
|
-
*
|
|
54
|
-
*
|
|
53
|
+
* Abandoning the stream (`break` / `iterator.return()`) only detaches that
|
|
54
|
+
* consumer. It does not abort `relaySignal`; callers must pass `opts.signal`
|
|
55
|
+
* when they intend to cancel the underlying run.
|
|
55
56
|
*/
|
|
56
57
|
export async function* relayRunEvents(run) {
|
|
57
58
|
const queue = [];
|
|
@@ -61,6 +62,7 @@ export async function* relayRunEvents(run) {
|
|
|
61
62
|
let failure;
|
|
62
63
|
let wake;
|
|
63
64
|
const relayController = new AbortController();
|
|
65
|
+
let completedNormally = false;
|
|
64
66
|
const notify = () => {
|
|
65
67
|
const resolve = wake;
|
|
66
68
|
wake = undefined;
|
|
@@ -110,6 +112,7 @@ export async function* relayRunEvents(run) {
|
|
|
110
112
|
}
|
|
111
113
|
if (queue.length === 0 && dropped === 0) {
|
|
112
114
|
if (done) {
|
|
115
|
+
completedNormally = true;
|
|
113
116
|
break;
|
|
114
117
|
}
|
|
115
118
|
// No await between the empty check and installing `wake`, so a producer
|
|
@@ -121,10 +124,12 @@ export async function* relayRunEvents(run) {
|
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
finally {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
if (completedNormally) {
|
|
128
|
+
await result.catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
void result.catch(() => undefined);
|
|
132
|
+
}
|
|
128
133
|
}
|
|
129
134
|
if (failure)
|
|
130
135
|
throw failure;
|
|
@@ -922,8 +927,8 @@ export function createSessionHarness(definition) {
|
|
|
922
927
|
}
|
|
923
928
|
}
|
|
924
929
|
/** Pass-through step used when a workflow runs without durable execution. */
|
|
925
|
-
function passthroughStep(_stepId, fn) {
|
|
926
|
-
return fn
|
|
930
|
+
function passthroughStep(_stepId, fn, options = {}) {
|
|
931
|
+
return runStepWithRetry(fn, options.retry);
|
|
927
932
|
}
|
|
928
933
|
function resolveDelegationPolicy(workflow) {
|
|
929
934
|
const configured = workflow.delegation;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Harness package version, used as the OpenTelemetry instrumentation scope version. */
|
|
2
|
-
export declare const HARNESS_VERSION = "1.5.
|
|
2
|
+
export declare const HARNESS_VERSION = "1.5.1";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Harness package version, used as the OpenTelemetry instrumentation scope version. */
|
|
2
|
-
export const HARNESS_VERSION = '1.5.
|
|
2
|
+
export const HARNESS_VERSION = '1.5.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purista/harness",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.1",
|
|
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",
|
|
@@ -69,12 +69,12 @@
|
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
72
|
-
"@opentelemetry/context-async-hooks": "^2.
|
|
73
|
-
"@types/node": "^25.9.
|
|
74
|
-
"@vitest/coverage-v8": "^4.1.
|
|
72
|
+
"@opentelemetry/context-async-hooks": "^2.8.0",
|
|
73
|
+
"@types/node": "^25.9.3",
|
|
74
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
75
75
|
"just-bash": "^3.0.1",
|
|
76
76
|
"typescript": "^6.0.3",
|
|
77
|
-
"vitest": "^4.1.
|
|
77
|
+
"vitest": "^4.1.9"
|
|
78
78
|
},
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=24.15.0"
|