@nylorun/harness 0.12.0-beta → 0.15.0-beta
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/CHANGELOG.md +71 -0
- package/HOST_CONTRACT.md +27 -0
- package/README.md +86 -5
- package/dist/compatibility.d.ts +3 -0
- package/dist/compatibility.js +3 -0
- package/dist/definition/agent-definition.d.ts +19 -0
- package/dist/definition/agent-definition.js +20 -0
- package/dist/definition/compatibility.d.ts +11 -0
- package/dist/definition/compatibility.js +30 -0
- package/dist/definition/registry.d.ts +10 -0
- package/dist/definition/registry.js +49 -0
- package/dist/index.d.ts +4 -15
- package/dist/index.js +1 -6
- package/dist/loop/dispatch.d.ts +2 -0
- package/dist/loop/dispatch.js +327 -0
- package/dist/loop/host-suspension.d.ts +6 -0
- package/dist/loop/host-suspension.js +10 -0
- package/dist/loop/initial-state.d.ts +13 -0
- package/dist/loop/initial-state.js +30 -0
- package/dist/loop/invocation.d.ts +24 -0
- package/dist/loop/invocation.js +37 -0
- package/dist/{model → loop/model}/adapters.d.ts +4 -2
- package/dist/{model → loop/model}/adapters.js +3 -2
- package/dist/{model → loop/model}/normalize.d.ts +2 -2
- package/dist/{model → loop/model}/normalize.js +2 -2
- package/dist/{model → loop/model}/prepared.d.ts +3 -3
- package/dist/{step → loop/model}/project.d.ts +1 -1
- package/dist/{step → loop/model}/project.js +2 -10
- package/dist/loop/observe.d.ts +17 -0
- package/dist/loop/observe.js +53 -0
- package/dist/loop/options.d.ts +11 -0
- package/dist/loop/options.js +69 -0
- package/dist/loop/resume.d.ts +4 -0
- package/dist/loop/resume.js +52 -0
- package/dist/loop/run.d.ts +5 -0
- package/dist/loop/run.js +264 -0
- package/dist/loop/state.d.ts +8 -0
- package/dist/loop/state.js +218 -0
- package/dist/{step → loop/step}/canonicalize.d.ts +2 -2
- package/dist/{step → loop/step}/canonicalize.js +2 -2
- package/dist/{step → loop/step}/compose.d.ts +3 -2
- package/dist/{step → loop/step}/compose.js +8 -5
- package/dist/loop/step/context-draft.d.ts +8 -0
- package/dist/{step → loop/step}/context-draft.js +5 -22
- package/dist/loop/step/describe.d.ts +9 -0
- package/dist/loop/step/describe.js +22 -0
- package/dist/loop/step/dynamics.d.ts +29 -0
- package/dist/loop/step/dynamics.js +147 -0
- package/dist/{step → loop/step}/model-configuration.d.ts +4 -3
- package/dist/{step → loop/step}/model-configuration.js +3 -3
- package/dist/loop/step/resolve.d.ts +11 -0
- package/dist/{step → loop/step}/resolve.js +5 -3
- package/dist/loop/step/run.d.ts +30 -0
- package/dist/{step → loop/step}/run.js +104 -53
- package/dist/loop/step/runtime.d.ts +40 -0
- package/dist/{step → loop/step}/seal.d.ts +13 -8
- package/dist/{step → loop/step}/seal.js +8 -6
- package/dist/{step → loop/step}/slot-assembly.d.ts +4 -4
- package/dist/{step → loop/step}/slot-assembly.js +4 -4
- package/dist/{step → loop/step}/step-context.d.ts +11 -12
- package/dist/{step → loop/step}/step-context.js +5 -27
- package/dist/loop/tool-result.d.ts +4 -0
- package/dist/loop/tool-result.js +30 -0
- package/dist/loop/transcript.d.ts +2 -0
- package/dist/{session/seed.js → loop/transcript.js} +11 -41
- package/dist/loop/waits.d.ts +50 -0
- package/dist/loop/waits.js +11 -0
- package/dist/run/durable.d.ts +71 -0
- package/dist/run/durable.js +164 -0
- package/dist/run/index.d.ts +33 -0
- package/dist/run/index.js +38 -0
- package/dist/types/client.d.ts +88 -0
- package/dist/types/execution.d.ts +114 -0
- package/dist/utils/ids.d.ts +1 -0
- package/dist/utils/ids.js +11 -1
- package/package.json +18 -6
- package/dist/build/agent.d.ts +0 -17
- package/dist/build/agent.js +0 -32
- package/dist/build/assemble.d.ts +0 -7
- package/dist/build/assemble.js +0 -41
- package/dist/build/bind-tool.d.ts +0 -3
- package/dist/build/bind-tool.js +0 -18
- package/dist/build/builder.d.ts +0 -38
- package/dist/build/builder.js +0 -136
- package/dist/build/helpers.d.ts +0 -6
- package/dist/build/helpers.js +0 -4
- package/dist/build/manifest.d.ts +0 -7
- package/dist/build/manifest.js +0 -20
- package/dist/build/schema.d.ts +0 -14
- package/dist/build/schema.js +0 -205
- package/dist/errors.d.ts +0 -17
- package/dist/errors.js +0 -17
- package/dist/session/capability-state.d.ts +0 -14
- package/dist/session/capability-state.js +0 -67
- package/dist/session/event-log.d.ts +0 -12
- package/dist/session/event-log.js +0 -63
- package/dist/session/input-queue.d.ts +0 -41
- package/dist/session/input-queue.js +0 -94
- package/dist/session/output-contract.d.ts +0 -6
- package/dist/session/output-contract.js +0 -12
- package/dist/session/record.d.ts +0 -11
- package/dist/session/record.js +0 -26
- package/dist/session/scheduler.d.ts +0 -62
- package/dist/session/scheduler.js +0 -475
- package/dist/session/seed.d.ts +0 -10
- package/dist/session/session.d.ts +0 -17
- package/dist/session/session.js +0 -101
- package/dist/session/state.d.ts +0 -15
- package/dist/session/state.js +0 -48
- package/dist/session/submission-stream.d.ts +0 -16
- package/dist/session/submission-stream.js +0 -46
- package/dist/step/context-draft.d.ts +0 -10
- package/dist/step/resolve.d.ts +0 -10
- package/dist/step/run.d.ts +0 -33
- package/dist/turn/plan-runner.d.ts +0 -72
- package/dist/turn/plan-runner.js +0 -389
- package/dist/turn/runner.d.ts +0 -69
- package/dist/turn/runner.js +0 -159
- package/dist/types/manifest.d.ts +0 -19
- package/dist/types/middleware.d.ts +0 -95
- package/dist/types/model.d.ts +0 -184
- package/dist/types/session.d.ts +0 -230
- package/dist/types/session.js +0 -1
- package/dist/types/shared.d.ts +0 -249
- package/dist/types/shared.js +0 -1
- package/dist/types/tool.d.ts +0 -139
- package/dist/types/tool.js +0 -1
- package/dist/utils/immutable.d.ts +0 -5
- package/dist/utils/immutable.js +0 -68
- package/dist/utils/maps.d.ts +0 -1
- package/dist/utils/maps.js +0 -37
- package/dist/utils/observe.d.ts +0 -3
- package/dist/utils/observe.js +0 -13
- /package/dist/{model → loop/model}/prepared.js +0 -0
- /package/dist/{types/manifest.js → loop/step/runtime.js} +0 -0
- /package/dist/types/{middleware.js → client.js} +0 -0
- /package/dist/types/{model.js → execution.js} +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { checkCompatibility } from "../definition/compatibility.js";
|
|
2
|
+
import { initializeExecutionState } from "./initial-state.js";
|
|
3
|
+
import { canonical } from "@nylorun/core/define";
|
|
4
|
+
import { HarnessError } from "@nylorun/core/define";
|
|
5
|
+
import { normalizeInput, validateExecutionState } from "./state.js";
|
|
6
|
+
const acceptedOptions = new Set([
|
|
7
|
+
"state",
|
|
8
|
+
"input",
|
|
9
|
+
"onModelCall",
|
|
10
|
+
"info",
|
|
11
|
+
"signal",
|
|
12
|
+
"onEvent",
|
|
13
|
+
"record",
|
|
14
|
+
]);
|
|
15
|
+
export function validateRunOptions(options) {
|
|
16
|
+
if (!options || typeof options.onModelCall !== "function")
|
|
17
|
+
throw new HarnessError("execution.invalid-input", "run() requires input and onModelCall");
|
|
18
|
+
for (const key of Object.keys(options))
|
|
19
|
+
if (!acceptedOptions.has(key))
|
|
20
|
+
throw new HarnessError("execution.invalid-input", `Unknown run option '${key}'`);
|
|
21
|
+
for (const key of ["record", "onEvent"])
|
|
22
|
+
if (options[key] !== undefined && typeof options[key] !== "function")
|
|
23
|
+
throw new HarnessError("execution.invalid-input", `${key} must be a function`);
|
|
24
|
+
if (options.signal !== undefined &&
|
|
25
|
+
(!options.signal ||
|
|
26
|
+
typeof options.signal.throwIfAborted !== "function" ||
|
|
27
|
+
typeof options.signal.addEventListener !== "function"))
|
|
28
|
+
throw new HarnessError("execution.invalid-input", "signal must be an AbortSignal");
|
|
29
|
+
}
|
|
30
|
+
export function openExecution(agent, options) {
|
|
31
|
+
const state = options.state === undefined
|
|
32
|
+
? initializeExecutionState(agent)
|
|
33
|
+
: validateExecutionState(options.state);
|
|
34
|
+
if (state.agentId !== agent.id)
|
|
35
|
+
throw new HarnessError("execution.incompatible", "Saved state requires the original compatible agent and outputSchema");
|
|
36
|
+
if (canonical(state.outputContract) !== canonical(agent.output?.schema.jsonSchema))
|
|
37
|
+
throw new HarnessError("execution.incompatible", "Saved state requires the original compatible agent and outputSchema");
|
|
38
|
+
if (state.manifestHash) {
|
|
39
|
+
const compatibility = checkCompatibility(agent.manifest, state);
|
|
40
|
+
if (!compatibility.ok)
|
|
41
|
+
throw compatibility.error;
|
|
42
|
+
}
|
|
43
|
+
const hasActive = state.status === "active" || state.plan?.calls.some((call) => call.status === "active");
|
|
44
|
+
// Crash-continue: allow resume when a prior run left active work; mark redelivery.
|
|
45
|
+
if (hasActive && options.input && typeof options.input === "object" && "kind" in options.input) {
|
|
46
|
+
const kind = options.input.kind;
|
|
47
|
+
if (kind !== "continue" && kind !== "settle" && kind !== "approve" && kind !== "respond")
|
|
48
|
+
throw new HarnessError("execution.invalid-state", "Execution was interrupted with possibly active effects. Resume with continue/settle/approve/respond; automatic replay is not supported.");
|
|
49
|
+
}
|
|
50
|
+
else if (hasActive) {
|
|
51
|
+
throw new HarnessError("execution.invalid-state", "Execution was interrupted with possibly active effects. Reconcile it before starting another run; automatic replay is not supported.");
|
|
52
|
+
}
|
|
53
|
+
if (state.status !== "paused" && state.plan !== undefined && !hasActive)
|
|
54
|
+
throw new HarnessError("execution.invalid-state", "Execution was interrupted with possibly active effects. Reconcile it before starting another run; automatic replay is not supported.");
|
|
55
|
+
const definitions = new Map();
|
|
56
|
+
for (const call of state.plan?.calls ?? []) {
|
|
57
|
+
const definition = agent.registry.restore(call.reference);
|
|
58
|
+
const validation = definition.inputSchema.validate(call.rawArgs);
|
|
59
|
+
if (!validation.ok || canonical(validation.value) !== canonical(call.args))
|
|
60
|
+
throw new HarnessError("execution.incompatible", "Saved tool arguments differ from their accepted contract");
|
|
61
|
+
definitions.set(call.invocationId, definition);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
state,
|
|
65
|
+
input: normalizeInput(options.input),
|
|
66
|
+
definitions,
|
|
67
|
+
redelivery: Boolean(hasActive),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BoundToolDefinition } from "@nylorun/core/define";
|
|
2
|
+
import type { ExecutionState } from "../types/execution.js";
|
|
3
|
+
import type { normalizeInput } from "./state.js";
|
|
4
|
+
export declare function applyResume(state: ExecutionState, input: ReturnType<typeof normalizeInput>, definitions: Map<string, BoundToolDefinition>): ExecutionState;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { HarnessError } from "@nylorun/core/define";
|
|
2
|
+
import { copyJson } from "@nylorun/core/define";
|
|
3
|
+
import { toolResult } from "./tool-result.js";
|
|
4
|
+
export function applyResume(state, input, definitions) {
|
|
5
|
+
const plan = state.plan;
|
|
6
|
+
let matched = false;
|
|
7
|
+
const calls = plan.calls.map((call) => {
|
|
8
|
+
if (input.kind === "settle" &&
|
|
9
|
+
call.status === "deferred" &&
|
|
10
|
+
call.invocationId === input.invocationId) {
|
|
11
|
+
matched = true;
|
|
12
|
+
return {
|
|
13
|
+
...call,
|
|
14
|
+
status: "settled",
|
|
15
|
+
result: toolResult(call, definitions.get(call.invocationId), input.outcome),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if ((input.kind === "approve" || input.kind === "respond") &&
|
|
19
|
+
call.status === "interaction" &&
|
|
20
|
+
call.interaction.id === input.interactionId) {
|
|
21
|
+
if ((input.kind === "approve") !== (call.interaction.kind === "approval"))
|
|
22
|
+
throw new HarnessError("execution.invalid-input", "Interaction response kind does not match the saved request");
|
|
23
|
+
matched = true;
|
|
24
|
+
const { interaction: _, interactionPhase: __, resume: ___, ...rest } = call;
|
|
25
|
+
if (input.kind === "approve" && !input.approved)
|
|
26
|
+
return {
|
|
27
|
+
...rest,
|
|
28
|
+
status: "settled",
|
|
29
|
+
result: {
|
|
30
|
+
kind: "denied",
|
|
31
|
+
callId: call.callId,
|
|
32
|
+
toolName: call.toolName,
|
|
33
|
+
reason: "Approval rejected",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
...rest,
|
|
38
|
+
status: "pending",
|
|
39
|
+
resume: {
|
|
40
|
+
interactionId: input.interactionId,
|
|
41
|
+
kind: input.kind === "approve" ? "approval" : "response",
|
|
42
|
+
...(input.kind === "approve" ? { approved: input.approved } : { value: input.value }),
|
|
43
|
+
...(call.token === undefined ? {} : { token: call.token }),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return call;
|
|
48
|
+
});
|
|
49
|
+
if (!matched)
|
|
50
|
+
throw new HarnessError("execution.invalid-input", "Input does not match a pending approval, response, or deferred invocation");
|
|
51
|
+
return copyJson({ ...state, plan: { ...plan, calls } });
|
|
52
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../definition/agent-definition.js";
|
|
2
|
+
import type { RunOptions, RunResult } from "../types/execution.js";
|
|
3
|
+
import type { JsonValue } from "@nylorun/core/define";
|
|
4
|
+
/** All mutable progress belongs to this invocation and is discarded when it settles. */
|
|
5
|
+
export declare function execute(agent: AgentDefinition, options: RunOptions<any>): Promise<RunResult<JsonValue>>;
|
package/dist/loop/run.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { HostSuspension } from "./host-suspension.js";
|
|
2
|
+
import { HarnessError, isHarnessError } from "@nylorun/core/define";
|
|
3
|
+
import { runStep } from "./step/run.js";
|
|
4
|
+
import { createId } from "../utils/ids.js";
|
|
5
|
+
import { dispatchPlan } from "./dispatch.js";
|
|
6
|
+
import { createInvocation } from "./invocation.js";
|
|
7
|
+
import { openExecution, validateRunOptions } from "./options.js";
|
|
8
|
+
import { applyResume } from "./resume.js";
|
|
9
|
+
/** All mutable progress belongs to this invocation and is discarded when it settles. */
|
|
10
|
+
export async function execute(agent, options) {
|
|
11
|
+
validateRunOptions(options);
|
|
12
|
+
const opened = openExecution(agent, options);
|
|
13
|
+
let { state } = opened;
|
|
14
|
+
const { input, definitions } = opened;
|
|
15
|
+
if (state.status === "paused" && !(input.kind === "continue" && options.signal?.aborted)) {
|
|
16
|
+
state = applyResume(state, input, definitions);
|
|
17
|
+
if (input.kind === "approve" || input.kind === "respond")
|
|
18
|
+
state = {
|
|
19
|
+
...state,
|
|
20
|
+
transcript: [
|
|
21
|
+
...state.transcript,
|
|
22
|
+
{ kind: "input", turnId: state.plan.turnId, event: input },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
else if (input.kind === "approve" || input.kind === "respond" || input.kind === "settle") {
|
|
27
|
+
throw new HarnessError("execution.invalid-input", "There is no matching paused operation for this input");
|
|
28
|
+
}
|
|
29
|
+
const invocation = createInvocation({ agent, options, state, definitions });
|
|
30
|
+
const { signal, observe, record } = invocation;
|
|
31
|
+
const finish = (result) => finishInvocation(invocation, result);
|
|
32
|
+
try {
|
|
33
|
+
signal.throwIfAborted();
|
|
34
|
+
let turnId;
|
|
35
|
+
let stepNumber;
|
|
36
|
+
let arrivals = [];
|
|
37
|
+
let toolResults = [];
|
|
38
|
+
if (invocation.state.plan) {
|
|
39
|
+
turnId = invocation.state.plan.turnId;
|
|
40
|
+
stepNumber = invocation.state.plan.stepNumber + 1;
|
|
41
|
+
invocation.state = { ...invocation.state, status: "active" };
|
|
42
|
+
await record();
|
|
43
|
+
if ((await dispatchPlan(invocation)) === "paused")
|
|
44
|
+
return finish({ status: "paused" });
|
|
45
|
+
toolResults = lastResults(invocation.state);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
turnId = createId("turn");
|
|
49
|
+
stepNumber = 1;
|
|
50
|
+
if (input.kind !== "continue" && input.kind !== "settle")
|
|
51
|
+
arrivals = [input];
|
|
52
|
+
invocation.state = {
|
|
53
|
+
...invocation.state,
|
|
54
|
+
status: "active",
|
|
55
|
+
turnCount: invocation.state.turnCount + 1,
|
|
56
|
+
transcript: [
|
|
57
|
+
...invocation.state.transcript,
|
|
58
|
+
...arrivals.map((event) => ({ kind: "input", turnId, event })),
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
await record();
|
|
62
|
+
}
|
|
63
|
+
for (;; stepNumber++) {
|
|
64
|
+
signal.throwIfAborted();
|
|
65
|
+
const stepId = createId("step");
|
|
66
|
+
const snapshot = {
|
|
67
|
+
id: invocation.state.executionId,
|
|
68
|
+
status: "running",
|
|
69
|
+
revision: invocation.state.revision,
|
|
70
|
+
turnCount: invocation.state.turnCount,
|
|
71
|
+
transcript: invocation.state.transcript,
|
|
72
|
+
};
|
|
73
|
+
const result = await runStep({
|
|
74
|
+
agent: {
|
|
75
|
+
middleware: agent.middleware,
|
|
76
|
+
invoke: options.onModelCall,
|
|
77
|
+
registry: agent.registry,
|
|
78
|
+
definition: agent,
|
|
79
|
+
sessionState: invocation.sessionBag,
|
|
80
|
+
recordAfterDynamics: async () => {
|
|
81
|
+
await record();
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
state: snapshot,
|
|
85
|
+
executionId: invocation.state.executionId,
|
|
86
|
+
turnId,
|
|
87
|
+
stepId,
|
|
88
|
+
turnNumber: invocation.state.turnCount,
|
|
89
|
+
stepNumber,
|
|
90
|
+
arrivals,
|
|
91
|
+
toolResults,
|
|
92
|
+
signal,
|
|
93
|
+
info: options.info,
|
|
94
|
+
observe,
|
|
95
|
+
output: agent.output,
|
|
96
|
+
});
|
|
97
|
+
signal.throwIfAborted();
|
|
98
|
+
if (result.candidate) {
|
|
99
|
+
invocation.state = {
|
|
100
|
+
...invocation.state,
|
|
101
|
+
transcript: [
|
|
102
|
+
...invocation.state.transcript,
|
|
103
|
+
{ kind: "candidate", turnId, stepId, candidate: result.candidate },
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
observe({
|
|
107
|
+
type: "model.completed",
|
|
108
|
+
turnId,
|
|
109
|
+
stepId,
|
|
110
|
+
...(result.modelInvocationId ? { invocationId: result.modelInvocationId } : {}),
|
|
111
|
+
...(result.requestedModelId === undefined
|
|
112
|
+
? {}
|
|
113
|
+
: { requestedModelId: result.requestedModelId }),
|
|
114
|
+
attributes: result.candidate,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (result.output.kind === "tripwire") {
|
|
118
|
+
observe({
|
|
119
|
+
type: "tripwire",
|
|
120
|
+
turnId,
|
|
121
|
+
stepId,
|
|
122
|
+
code: result.output.tripwire.code,
|
|
123
|
+
scope: result.output.tripwire.scope ?? "step",
|
|
124
|
+
attributes: { message: result.output.tripwire.message },
|
|
125
|
+
});
|
|
126
|
+
return finish({ status: "failed", error: result.output.tripwire });
|
|
127
|
+
}
|
|
128
|
+
if (result.output.kind === "final") {
|
|
129
|
+
invocation.state = {
|
|
130
|
+
...invocation.state,
|
|
131
|
+
transcript: [
|
|
132
|
+
...invocation.state.transcript,
|
|
133
|
+
{ kind: "final", turnId, stepId, output: result.output.output },
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
observe({
|
|
137
|
+
type: "turn.completed",
|
|
138
|
+
turnId,
|
|
139
|
+
stepId,
|
|
140
|
+
attributes: { output: result.output.output },
|
|
141
|
+
});
|
|
142
|
+
return finish({ status: "completed", output: result.output.output });
|
|
143
|
+
}
|
|
144
|
+
const plan = result.output.plan;
|
|
145
|
+
const calls = plan.executable.map((entry) => {
|
|
146
|
+
const reference = agent.registry.reference(entry.definition);
|
|
147
|
+
definitions.set(entry.invocationId, agent.registry.restore(reference));
|
|
148
|
+
return {
|
|
149
|
+
callId: entry.call.callId,
|
|
150
|
+
toolName: entry.call.toolName,
|
|
151
|
+
args: entry.call.args,
|
|
152
|
+
rawArgs: entry.rawArgs,
|
|
153
|
+
invocationId: entry.invocationId,
|
|
154
|
+
reference,
|
|
155
|
+
status: entry.interaction ? "interaction" : "pending",
|
|
156
|
+
...(entry.interaction
|
|
157
|
+
? { interaction: entry.interaction, interactionPhase: "before" }
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
invocation.state = {
|
|
162
|
+
...invocation.state,
|
|
163
|
+
plan: {
|
|
164
|
+
turnId,
|
|
165
|
+
stepId,
|
|
166
|
+
stepNumber,
|
|
167
|
+
order: plan.order,
|
|
168
|
+
immediate: plan.immediateResults,
|
|
169
|
+
calls,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
observe({
|
|
173
|
+
type: "tool.sealed",
|
|
174
|
+
turnId,
|
|
175
|
+
stepId,
|
|
176
|
+
attributes: {
|
|
177
|
+
executable: plan.executable.map((entry) => ({
|
|
178
|
+
callId: entry.call.callId,
|
|
179
|
+
toolName: entry.call.toolName,
|
|
180
|
+
args: entry.call.args,
|
|
181
|
+
rawArgs: entry.rawArgs,
|
|
182
|
+
invocationId: entry.invocationId,
|
|
183
|
+
owner: entry.owner,
|
|
184
|
+
...(entry.interaction ? { interaction: entry.interaction } : {}),
|
|
185
|
+
})),
|
|
186
|
+
immediate: plan.immediateResults,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
await record();
|
|
190
|
+
if ((await dispatchPlan(invocation)) === "paused")
|
|
191
|
+
return finish({ status: "paused" });
|
|
192
|
+
toolResults = lastResults(invocation.state);
|
|
193
|
+
arrivals = [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof HostSuspension)
|
|
198
|
+
throw error;
|
|
199
|
+
if (isHarnessError(error) && error.code === "execution.record-failed")
|
|
200
|
+
throw error;
|
|
201
|
+
if (signal.aborted)
|
|
202
|
+
return finish({ status: "cancelled" });
|
|
203
|
+
return finish({
|
|
204
|
+
status: "failed",
|
|
205
|
+
error: {
|
|
206
|
+
code: isHarnessError(error) ? error.code : "execution.failed",
|
|
207
|
+
message: error instanceof Error ? error.message : String(error),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function finishInvocation(invocation, result) {
|
|
213
|
+
if (result.status === "cancelled" && invocation.state.plan) {
|
|
214
|
+
const plan = invocation.state.plan;
|
|
215
|
+
const results = [
|
|
216
|
+
...plan.immediate,
|
|
217
|
+
...plan.calls.map((call) => call.result ?? {
|
|
218
|
+
kind: "failed",
|
|
219
|
+
callId: call.callId,
|
|
220
|
+
toolName: call.toolName,
|
|
221
|
+
code: "tool.cancelled",
|
|
222
|
+
message: "Invocation cancelled; external effects are not automatically retried",
|
|
223
|
+
}),
|
|
224
|
+
];
|
|
225
|
+
const { plan: _, ...rest } = invocation.state;
|
|
226
|
+
invocation.state = {
|
|
227
|
+
...rest,
|
|
228
|
+
cancelledCalls: [
|
|
229
|
+
...(rest.cancelledCalls ?? []),
|
|
230
|
+
...plan.calls.filter((call) => !call.result),
|
|
231
|
+
],
|
|
232
|
+
transcript: [
|
|
233
|
+
...invocation.state.transcript,
|
|
234
|
+
{
|
|
235
|
+
kind: "tool-results",
|
|
236
|
+
turnId: plan.turnId,
|
|
237
|
+
stepId: plan.stepId,
|
|
238
|
+
results: plan.order.map((item) => results.find((result) => result.callId === item.callId && result.toolName === item.toolName)),
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
invocation.state = { ...invocation.state, status: result.status };
|
|
244
|
+
await invocation.record();
|
|
245
|
+
invocation.emit({
|
|
246
|
+
type: `execution.${result.status}`,
|
|
247
|
+
...(result.status === "completed"
|
|
248
|
+
? { attributes: { output: result.output } }
|
|
249
|
+
: result.status === "failed"
|
|
250
|
+
? { attributes: { error: result.error } }
|
|
251
|
+
: {}),
|
|
252
|
+
});
|
|
253
|
+
if (result.status === "paused")
|
|
254
|
+
return {
|
|
255
|
+
status: "paused",
|
|
256
|
+
state: invocation.state,
|
|
257
|
+
pending: invocation.state.plan.calls.filter((call) => call.status === "interaction" || call.status === "deferred"),
|
|
258
|
+
};
|
|
259
|
+
return { ...result, state: invocation.state };
|
|
260
|
+
}
|
|
261
|
+
function lastResults(state) {
|
|
262
|
+
const entry = state.transcript[state.transcript.length - 1];
|
|
263
|
+
return entry?.kind === "tool-results" ? entry.results : [];
|
|
264
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BuiltAgent } from "@nylorun/core/define";
|
|
2
|
+
import type { ExecutionInput, ExecutionState } from "../types/execution.js";
|
|
3
|
+
import type { InputEvent } from "@nylorun/core/define";
|
|
4
|
+
export declare function createExecutionState<Info, Output>(agent: BuiltAgent<Info, Output>): ExecutionState;
|
|
5
|
+
export declare function validateExecutionState(value: unknown): ExecutionState;
|
|
6
|
+
export declare function normalizeInput(value: ExecutionInput): InputEvent | Extract<ExecutionInput, {
|
|
7
|
+
kind: "continue" | "settle";
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { HarnessError } from "@nylorun/core/define";
|
|
3
|
+
import { assertJson, copyJson } from "@nylorun/core/define";
|
|
4
|
+
import { definitionFor } from "../definition/agent-definition.js";
|
|
5
|
+
import { initializeExecutionState } from "./initial-state.js";
|
|
6
|
+
import { validateTranscript } from "./transcript.js";
|
|
7
|
+
const id = z.string().min(1);
|
|
8
|
+
const json = z.unknown().refine((value) => {
|
|
9
|
+
try {
|
|
10
|
+
assertJson(value);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}, "Expected JSON data");
|
|
17
|
+
const interaction = z
|
|
18
|
+
.object({
|
|
19
|
+
id,
|
|
20
|
+
kind: z.enum(["approval", "response"]),
|
|
21
|
+
prompt: z.string(),
|
|
22
|
+
metadata: json.optional(),
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
const outcome = z.discriminatedUnion("kind", [
|
|
26
|
+
z.object({ kind: z.literal("completed"), output: json }).strict(),
|
|
27
|
+
z.object({ kind: z.literal("denied"), reason: z.string() }).strict(),
|
|
28
|
+
z.object({ kind: z.literal("failed"), code: id, message: z.string() }).strict(),
|
|
29
|
+
]);
|
|
30
|
+
const reference = z
|
|
31
|
+
.object({
|
|
32
|
+
capabilityId: id,
|
|
33
|
+
toolName: id,
|
|
34
|
+
descriptor: z
|
|
35
|
+
.object({
|
|
36
|
+
name: id,
|
|
37
|
+
description: z.string().optional(),
|
|
38
|
+
inputSchema: z.record(z.string(), json),
|
|
39
|
+
outputSchema: z.record(z.string(), json).optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict(),
|
|
42
|
+
})
|
|
43
|
+
.strict();
|
|
44
|
+
const call = z
|
|
45
|
+
.object({
|
|
46
|
+
callId: id,
|
|
47
|
+
invocationId: id,
|
|
48
|
+
toolName: id,
|
|
49
|
+
args: json,
|
|
50
|
+
rawArgs: json,
|
|
51
|
+
reference,
|
|
52
|
+
status: z.enum(["pending", "active", "settled", "interaction", "deferred"]),
|
|
53
|
+
result: json.optional(),
|
|
54
|
+
interaction: interaction.optional(),
|
|
55
|
+
interactionPhase: z.enum(["before", "execute"]).optional(),
|
|
56
|
+
resume: z
|
|
57
|
+
.discriminatedUnion("kind", [
|
|
58
|
+
z
|
|
59
|
+
.object({
|
|
60
|
+
kind: z.literal("approval"),
|
|
61
|
+
interactionId: id,
|
|
62
|
+
approved: z.boolean(),
|
|
63
|
+
token: json.optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict(),
|
|
66
|
+
z
|
|
67
|
+
.object({
|
|
68
|
+
kind: z.literal("response"),
|
|
69
|
+
interactionId: id,
|
|
70
|
+
value: json,
|
|
71
|
+
token: json.optional(),
|
|
72
|
+
})
|
|
73
|
+
.strict(),
|
|
74
|
+
])
|
|
75
|
+
.optional(),
|
|
76
|
+
token: json.optional(),
|
|
77
|
+
wait: z
|
|
78
|
+
.object({
|
|
79
|
+
kind: z.enum(["ask", "approve", "sleep", "waitFor"]),
|
|
80
|
+
waitId: id,
|
|
81
|
+
name: z.string().optional(),
|
|
82
|
+
})
|
|
83
|
+
.strict()
|
|
84
|
+
.optional(),
|
|
85
|
+
})
|
|
86
|
+
.strict();
|
|
87
|
+
const schema = z
|
|
88
|
+
.object({
|
|
89
|
+
version: z.literal(1),
|
|
90
|
+
agentId: id,
|
|
91
|
+
manifestHash: id.optional(),
|
|
92
|
+
executionId: id,
|
|
93
|
+
outputContract: z.record(z.string(), json).optional(),
|
|
94
|
+
revision: z.number().int().nonnegative(),
|
|
95
|
+
turnCount: z.number().int().nonnegative(),
|
|
96
|
+
transcript: z.array(json),
|
|
97
|
+
status: z.enum(["ready", "active", "completed", "paused", "cancelled", "failed"]),
|
|
98
|
+
cancelledCalls: z.array(call).optional(),
|
|
99
|
+
state: z.record(z.string(), json).optional(),
|
|
100
|
+
plan: z
|
|
101
|
+
.object({
|
|
102
|
+
turnId: id,
|
|
103
|
+
stepId: id,
|
|
104
|
+
stepNumber: z.number().int().positive(),
|
|
105
|
+
order: z.array(z.object({ callId: id, toolName: id }).strict()),
|
|
106
|
+
immediate: z.array(json),
|
|
107
|
+
calls: z.array(call),
|
|
108
|
+
})
|
|
109
|
+
.strict()
|
|
110
|
+
.optional(),
|
|
111
|
+
})
|
|
112
|
+
.strict();
|
|
113
|
+
export function createExecutionState(agent) {
|
|
114
|
+
return initializeExecutionState(definitionFor(agent));
|
|
115
|
+
}
|
|
116
|
+
function omitLegacyExecutionVersion(value) {
|
|
117
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
118
|
+
return value;
|
|
119
|
+
if (!("executionVersion" in value))
|
|
120
|
+
return value;
|
|
121
|
+
const { executionVersion: _ignored, ...rest } = value;
|
|
122
|
+
return rest;
|
|
123
|
+
}
|
|
124
|
+
export function validateExecutionState(value) {
|
|
125
|
+
try {
|
|
126
|
+
assertJson(value);
|
|
127
|
+
const parsed = schema.parse(omitLegacyExecutionVersion(value));
|
|
128
|
+
validateTranscript(parsed.transcript);
|
|
129
|
+
const plan = parsed.plan;
|
|
130
|
+
if (parsed.status === "paused" && !plan)
|
|
131
|
+
throw new HarnessError("execution.invalid-state", "Paused execution requires a pending plan");
|
|
132
|
+
if (plan) {
|
|
133
|
+
const ids = new Set();
|
|
134
|
+
const invocations = new Set();
|
|
135
|
+
const interactions = new Set();
|
|
136
|
+
const ordered = new Set(plan.order.map((item) => item.callId));
|
|
137
|
+
if (ordered.size !== plan.order.length ||
|
|
138
|
+
plan.order.length !== plan.calls.length + plan.immediate.length)
|
|
139
|
+
throw new HarnessError("execution.invalid-state", "Plan order must identify every result exactly once");
|
|
140
|
+
for (const result of plan.immediate) {
|
|
141
|
+
if (ids.has(result.callId) ||
|
|
142
|
+
!plan.order.some((item) => item.callId === result.callId && item.toolName === result.toolName))
|
|
143
|
+
throw new HarnessError("execution.invalid-state", "Invalid immediate result identity");
|
|
144
|
+
ids.add(result.callId);
|
|
145
|
+
}
|
|
146
|
+
for (const item of plan.calls) {
|
|
147
|
+
if (ids.has(item.callId) || invocations.has(item.invocationId))
|
|
148
|
+
throw new HarnessError("execution.invalid-state", "Duplicate pending call identity");
|
|
149
|
+
ids.add(item.callId);
|
|
150
|
+
invocations.add(item.invocationId);
|
|
151
|
+
if (!plan.order.some((order) => order.callId === item.callId && order.toolName === item.toolName))
|
|
152
|
+
throw new HarnessError("execution.invalid-state", "Pending call missing from plan order");
|
|
153
|
+
if (item.toolName !== item.reference.descriptor.name)
|
|
154
|
+
throw new HarnessError("execution.invalid-state", "Pending tool name differs from its contract");
|
|
155
|
+
if (item.toolName !== item.reference.toolName)
|
|
156
|
+
throw new HarnessError("execution.invalid-state", "Pending tool name differs from its reference");
|
|
157
|
+
if (item.interaction) {
|
|
158
|
+
if (interactions.has(item.interaction.id))
|
|
159
|
+
throw new HarnessError("execution.invalid-state", "Duplicate interaction identity");
|
|
160
|
+
interactions.add(item.interaction.id);
|
|
161
|
+
}
|
|
162
|
+
if (item.status !== "settled" && item.result)
|
|
163
|
+
throw new HarnessError("execution.invalid-state", "Unsettled call cannot contain a result");
|
|
164
|
+
if (item.status === "settled" && !item.result)
|
|
165
|
+
throw new HarnessError("execution.invalid-state", "Settled call requires a result");
|
|
166
|
+
if (item.status === "interaction" && (!item.interaction || !item.interactionPhase))
|
|
167
|
+
throw new HarnessError("execution.invalid-state", "Missing interaction state");
|
|
168
|
+
if (item.result &&
|
|
169
|
+
(item.result.callId !== item.callId || item.result.toolName !== item.toolName))
|
|
170
|
+
throw new HarnessError("execution.invalid-state", "Result identity mismatch");
|
|
171
|
+
}
|
|
172
|
+
validateTranscript([
|
|
173
|
+
{
|
|
174
|
+
kind: "tool-results",
|
|
175
|
+
turnId: plan.turnId,
|
|
176
|
+
stepId: plan.stepId,
|
|
177
|
+
results: [
|
|
178
|
+
...plan.immediate,
|
|
179
|
+
...plan.calls.flatMap((item) => (item.result ? [item.result] : [])),
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
183
|
+
if (parsed.status === "paused" &&
|
|
184
|
+
(plan.calls.some((item) => item.status === "active") ||
|
|
185
|
+
!plan.calls.some((item) => item.status === "interaction" || item.status === "deferred")))
|
|
186
|
+
throw new HarnessError("execution.invalid-state", "Invalid paused plan");
|
|
187
|
+
}
|
|
188
|
+
return copyJson(parsed);
|
|
189
|
+
}
|
|
190
|
+
catch (cause) {
|
|
191
|
+
throw new HarnessError("execution.invalid-state", `Invalid execution state: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export function normalizeInput(value) {
|
|
195
|
+
try {
|
|
196
|
+
if (typeof value === "string")
|
|
197
|
+
return { kind: "user-message", text: value };
|
|
198
|
+
assertJson(value);
|
|
199
|
+
if ("kind" in value && value.kind === "continue")
|
|
200
|
+
return z
|
|
201
|
+
.object({ kind: z.literal("continue") })
|
|
202
|
+
.strict()
|
|
203
|
+
.parse(value);
|
|
204
|
+
if ("kind" in value && value.kind === "settle")
|
|
205
|
+
return z
|
|
206
|
+
.object({ kind: z.literal("settle"), invocationId: id, outcome })
|
|
207
|
+
.strict()
|
|
208
|
+
.parse(value);
|
|
209
|
+
const event = "kind" in value ? value : { kind: "user-message", ...value };
|
|
210
|
+
const validated = validateTranscript([
|
|
211
|
+
{ kind: "input", turnId: "validation", event: event },
|
|
212
|
+
]);
|
|
213
|
+
return validated[0].event;
|
|
214
|
+
}
|
|
215
|
+
catch (cause) {
|
|
216
|
+
throw new HarnessError("execution.invalid-input", `Invalid input: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ModelCandidate, ModelOutputBlock, ModelToolCall } from "
|
|
2
|
-
import type { JsonObject } from "
|
|
1
|
+
import type { ModelCandidate, ModelOutputBlock, ModelToolCall } from "@nylorun/core/define";
|
|
2
|
+
import type { JsonObject } from "@nylorun/core/define";
|
|
3
3
|
export interface CanonicalCall {
|
|
4
4
|
readonly id: string;
|
|
5
5
|
readonly name: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createId } from "
|
|
2
|
-
import { copyJson } from "
|
|
1
|
+
import { createId } from "../../utils/ids.js";
|
|
2
|
+
import { copyJson } from "@nylorun/core/define";
|
|
3
3
|
export function canonicalizeCalls(calls) {
|
|
4
4
|
const idCounts = new Map();
|
|
5
5
|
for (const call of calls) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BoundMiddleware
|
|
2
|
-
import type {
|
|
1
|
+
import type { BoundMiddleware } from "@nylorun/core/define";
|
|
2
|
+
import type { StepResponse } from "@nylorun/core/define";
|
|
3
|
+
import type { ObserveEmit } from "../observe.js";
|
|
3
4
|
import { StepContext } from "./step-context.js";
|
|
4
5
|
export declare function runMiddleware(middleware: readonly BoundMiddleware[], context: StepContext, terminal: () => Promise<StepResponse>, observe: ObserveEmit): Promise<StepResponse>;
|