@inflexa-ai/harness 0.37.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/dist/auth/local-run-authorizer.js +5 -4
- package/dist/auth/types.d.ts +28 -9
- package/dist/auth/types.js +5 -3
- package/dist/billing/noop-run-charge.js +3 -2
- package/dist/billing/noop-usage-recorder.js +2 -1
- package/dist/billing/run-charge.d.ts +18 -5
- package/dist/billing/run-charge.js +5 -1
- package/dist/billing/usage-recorder.d.ts +11 -9
- package/dist/billing/usage-recorder.js +2 -3
- package/dist/contracts/chat-events.d.ts +4 -1
- package/dist/contracts/chat-parts.d.ts +1 -1
- package/dist/contracts/schemas/chat-parts.d.ts +4 -4
- package/dist/execution/artifact-registration.d.ts +6 -1
- package/dist/execution/artifact-registration.js +12 -4
- package/dist/execution/artifact-registry.d.ts +10 -3
- package/dist/execution/noop-artifact-registry.js +3 -4
- package/dist/execution/post-step-pipeline.d.ts +14 -3
- package/dist/execution/post-step-pipeline.js +21 -8
- package/dist/execution/run-authorizer.d.ts +5 -3
- package/dist/execution/run-canceler.js +4 -20
- package/dist/index.d.ts +13 -7
- package/dist/index.js +12 -6
- package/dist/lib/hooks.d.ts +49 -0
- package/dist/lib/hooks.js +39 -0
- package/dist/lib/result.d.ts +22 -11
- package/dist/lib/result.js +24 -14
- package/dist/loop/run-agent.d.ts +2 -2
- package/dist/loop/run-agent.js +5 -4
- package/dist/provenance/seam.d.ts +1 -1
- package/dist/providers/ai-sdk.d.ts +43 -5
- package/dist/providers/ai-sdk.js +55 -52
- package/dist/providers/anthropic.d.ts +2 -3
- package/dist/providers/anthropic.js +2 -2
- package/dist/providers/embedding.d.ts +13 -7
- package/dist/providers/embedding.js +26 -6
- package/dist/providers/errors.d.ts +67 -37
- package/dist/providers/errors.js +57 -58
- package/dist/providers/request-headers.d.ts +18 -0
- package/dist/providers/request-headers.js +22 -0
- package/dist/report-model/production-resolver.d.ts +5 -2
- package/dist/report-model/production-resolver.js +12 -2
- package/dist/runtime/assemble.d.ts +3 -2
- package/dist/runtime/dbos.d.ts +4 -3
- package/dist/runtime/dbos.js +18 -3
- package/dist/runtime/result-serialization.d.ts +29 -0
- package/dist/runtime/result-serialization.js +45 -0
- package/dist/sandbox/client.d.ts +11 -3
- package/dist/sandbox/create-sandbox.d.ts +5 -2
- package/dist/sandbox/create-sandbox.js +51 -25
- package/dist/sandbox/docker-client.d.ts +4 -3
- package/dist/sandbox/docker-client.js +23 -31
- package/dist/sandbox/k8s-client.d.ts +4 -13
- package/dist/sandbox/k8s-client.js +28 -61
- package/dist/sandbox/labels.d.ts +34 -0
- package/dist/sandbox/labels.js +47 -0
- package/dist/sandbox/mount-plan.d.ts +3 -1
- package/dist/sandbox/mount-plan.js +9 -0
- package/dist/sandbox/sandbox-error.d.ts +36 -13
- package/dist/sandbox/sandbox-error.js +32 -17
- package/dist/sandbox/types.d.ts +13 -9
- package/dist/schemas/validate-plan.js +13 -0
- package/dist/schemas/workflow-state.d.ts +4 -4
- package/dist/state/analyses.d.ts +4 -4
- package/dist/state/analyses.js +4 -4
- package/dist/state/schema.d.ts +5 -5
- package/dist/tasks/data-profile.d.ts +5 -8
- package/dist/tasks/data-profile.js +88 -43
- package/dist/tasks/derive-table-exec.d.ts +10 -6
- package/dist/tasks/derive-table-exec.js +17 -12
- package/dist/tasks/extract-values.d.ts +16 -10
- package/dist/tasks/extract-values.js +34 -18
- package/dist/tools/execute-analysis.js +22 -15
- package/dist/tools/report-session/derive-table.d.ts +9 -1
- package/dist/tools/report-session/derive-table.js +25 -22
- package/dist/tools/research/inspect-run.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +25 -16
- package/dist/workflows/execute-analysis.js +223 -216
- package/dist/workflows/metrics.d.ts +4 -3
- package/dist/workflows/metrics.js +4 -3
- package/dist/workflows/sandbox-step.d.ts +20 -29
- package/dist/workflows/sandbox-step.js +71 -127
- package/dist/workflows/suspension.d.ts +32 -0
- package/dist/workflows/suspension.js +38 -0
- package/package.json +1 -1
- package/dist/billing/noop-resolver.d.ts +0 -6
- package/dist/billing/noop-resolver.js +0 -7
- package/dist/billing/resolver.d.ts +0 -64
- package/dist/billing/resolver.js +0 -24
- package/dist/loop/budget-exceeded.d.ts +0 -38
- package/dist/loop/budget-exceeded.js +0 -88
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* callback unroutable and it lands as a `400`. The pull backstop still settles such an exec, but it pays
|
|
19
19
|
* the quiet interval first.
|
|
20
20
|
*/
|
|
21
|
+
import { type Result } from "neverthrow";
|
|
21
22
|
import type { Logger } from "../lib/logger.js";
|
|
22
23
|
import type { SandboxClient } from "../sandbox/client.js";
|
|
24
|
+
import { type Suspension } from "../workflows/suspension.js";
|
|
23
25
|
import type { ExecResult } from "../sandbox/types.js";
|
|
24
26
|
import { type DeriveTableExecInput } from "../tools/report-session/derive-table.js";
|
|
25
27
|
/** The construction-time deps of the body. The registration closes over them, thus the trigger holds none. */
|
|
@@ -35,15 +37,17 @@ export interface DeriveTableExecDeps {
|
|
|
35
37
|
/**
|
|
36
38
|
* The body, exported so a test drives it without a registered workflow.
|
|
37
39
|
*
|
|
38
|
-
* A fault of any seam call throws. The tool reads a rejection as one short detail
|
|
39
|
-
* result
|
|
40
|
+
* A fault of any seam call throws. The tool reads a rejection as one short detail. A chat turn awaits this
|
|
41
|
+
* workflow, thus a suspension is the `err` of the result, and the workflow does not cancel
|
|
42
|
+
* (workflow-suspension spec). The body does not mark the analysis: the tool reports the suspension to the
|
|
43
|
+
* conversation agent.
|
|
40
44
|
*/
|
|
41
|
-
export declare function runDeriveTableExecBody(input: DeriveTableExecInput, deps: DeriveTableExecDeps): Promise<ExecResult
|
|
45
|
+
export declare function runDeriveTableExecBody(input: DeriveTableExecInput, deps: DeriveTableExecDeps): Promise<Result<ExecResult, Suspension>>;
|
|
42
46
|
/**
|
|
43
47
|
* Register the derivation workflow with DBOS. It gives back the registered callable, thus a trigger
|
|
44
48
|
* dispatches with `DBOS.startWorkflow`.
|
|
45
49
|
*/
|
|
46
|
-
export declare function registerDeriveTableExecWorkflow(deps: DeriveTableExecDeps): (input: DeriveTableExecInput) => Promise<ExecResult
|
|
50
|
+
export declare function registerDeriveTableExecWorkflow(deps: DeriveTableExecDeps): (input: DeriveTableExecInput) => Promise<Result<ExecResult, Suspension>>;
|
|
47
51
|
/**
|
|
48
52
|
* The workflow id of one derivation.
|
|
49
53
|
*
|
|
@@ -55,6 +59,6 @@ export declare function deriveTableExecWorkflowId(executionId: string): string;
|
|
|
55
59
|
* The async edge. It starts the registered workflow and it awaits the terminal result.
|
|
56
60
|
*
|
|
57
61
|
* The tool authorizes the derivation before this call and it revokes after, thus this edge holds no
|
|
58
|
-
* lifecycle of its own. A workflow fault rejects the returned promise
|
|
62
|
+
* lifecycle of its own. A workflow fault rejects the returned promise, and a suspension is the `err`.
|
|
59
63
|
*/
|
|
60
|
-
export declare function triggerDeriveTableExec(workflow: (input: DeriveTableExecInput) => Promise<ExecResult
|
|
64
|
+
export declare function triggerDeriveTableExec(workflow: (input: DeriveTableExecInput) => Promise<Result<ExecResult, Suspension>>, input: DeriveTableExecInput): Promise<Result<ExecResult, Suspension>>;
|
|
@@ -19,16 +19,22 @@
|
|
|
19
19
|
* the quiet interval first.
|
|
20
20
|
*/
|
|
21
21
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
22
|
+
import { err, ok } from "neverthrow";
|
|
23
|
+
import { forStep } from "../auth/types.js";
|
|
22
24
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
23
25
|
import { mintSandboxIdentity } from "../sandbox/identity.js";
|
|
26
|
+
import { keepSuspendingRefusal } from "../sandbox/sandbox-error.js";
|
|
27
|
+
import { suspensionOfSpawnRefusal } from "../workflows/suspension.js";
|
|
24
28
|
import { buildDerivationExec, DERIVATION_DEADLINE_MS, DERIVATION_RESOURCES, DERIVE_RUN_LITERAL, DERIVE_STEP_LITERAL, } from "../tools/report-session/derive-table.js";
|
|
25
29
|
/** The awaitExec callback. A derivation reports no live activity, thus the callback drops each event. */
|
|
26
30
|
const noopEmit = () => { };
|
|
27
31
|
/**
|
|
28
32
|
* The body, exported so a test drives it without a registered workflow.
|
|
29
33
|
*
|
|
30
|
-
* A fault of any seam call throws. The tool reads a rejection as one short detail
|
|
31
|
-
* result
|
|
34
|
+
* A fault of any seam call throws. The tool reads a rejection as one short detail. A chat turn awaits this
|
|
35
|
+
* workflow, thus a suspension is the `err` of the result, and the workflow does not cancel
|
|
36
|
+
* (workflow-suspension spec). The body does not mark the analysis: the tool reports the suspension to the
|
|
37
|
+
* conversation agent.
|
|
32
38
|
*/
|
|
33
39
|
export async function runDeriveTableExecBody(input, deps) {
|
|
34
40
|
const logger = (deps.logger ?? createNoopLogger()).named("derive-table-exec").with({ analysisId: input.analysisId });
|
|
@@ -36,14 +42,13 @@ export async function runDeriveTableExecBody(input, deps) {
|
|
|
36
42
|
// One workflow submits one exec, thus the function id is a constant. The three segments are what a
|
|
37
43
|
// callback host parses to find the workflow that awaits this exec.
|
|
38
44
|
const execId = `${workflowId}:${DERIVE_STEP_LITERAL}:fn-0`;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}, mintSandboxIdentity(DERIVE_RUN_LITERAL));
|
|
45
|
+
const spawned = keepSuspendingRefusal(await deps.sandboxClient.createSandbox(forStep(input.runSession, DERIVE_STEP_LITERAL), { childWorkflowId: workflowId, resources: DERIVATION_RESOURCES, writableTail: input.writableTail }, mintSandboxIdentity(DERIVE_RUN_LITERAL)));
|
|
46
|
+
if (spawned.isErr()) {
|
|
47
|
+
const suspension = suspensionOfSpawnRefusal(spawned.error);
|
|
48
|
+
logger.warn("derivation suspended", { reason: suspension.reason });
|
|
49
|
+
return err(suspension);
|
|
50
|
+
}
|
|
51
|
+
const sandbox = spawned.value;
|
|
47
52
|
try {
|
|
48
53
|
// A checkpointed clock, not `Date.now()`: the await gates on this absolute deadline, and a
|
|
49
54
|
// wall-clock deadline that grew on replay would shift which loop iteration crosses it.
|
|
@@ -55,7 +60,7 @@ export async function runDeriveTableExecBody(input, deps) {
|
|
|
55
60
|
inputs: input.inputs,
|
|
56
61
|
output: input.output,
|
|
57
62
|
}));
|
|
58
|
-
return await deps.sandboxClient.awaitExec(sandbox, execId, noopEmit, deadline);
|
|
63
|
+
return ok(await deps.sandboxClient.awaitExec(sandbox, execId, noopEmit, deadline));
|
|
59
64
|
}
|
|
60
65
|
finally {
|
|
61
66
|
try {
|
|
@@ -86,7 +91,7 @@ export function deriveTableExecWorkflowId(executionId) {
|
|
|
86
91
|
* The async edge. It starts the registered workflow and it awaits the terminal result.
|
|
87
92
|
*
|
|
88
93
|
* The tool authorizes the derivation before this call and it revokes after, thus this edge holds no
|
|
89
|
-
* lifecycle of its own. A workflow fault rejects the returned promise
|
|
94
|
+
* lifecycle of its own. A workflow fault rejects the returned promise, and a suspension is the `err`.
|
|
90
95
|
*/
|
|
91
96
|
export async function triggerDeriveTableExec(workflow, input) {
|
|
92
97
|
const handle = (await DBOS.startWorkflow(workflow, {
|
|
@@ -11,11 +11,13 @@
|
|
|
11
11
|
* authorization on every terminal path. The container is ephemeral, and it goes away with the work, thus no
|
|
12
12
|
* standing machine exists.
|
|
13
13
|
*/
|
|
14
|
-
import
|
|
14
|
+
import { type Result } from "neverthrow";
|
|
15
|
+
import { type AuthContext, type RunSession } from "../auth/types.js";
|
|
15
16
|
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
16
17
|
import type { Logger } from "../lib/logger.js";
|
|
17
18
|
import type { ExtractionArm, ExtractionArtifact, ExtractionRequest } from "../report-model/production-resolver.js";
|
|
18
19
|
import type { SandboxClient } from "../sandbox/client.js";
|
|
20
|
+
import { type Suspension } from "../workflows/suspension.js";
|
|
19
21
|
import type { ExecResult, SubmitExecBody } from "../sandbox/types.js";
|
|
20
22
|
import { type ExtractValuesResult } from "./extract-values-script.js";
|
|
21
23
|
export type { ExtractValuesResult } from "./extract-values-script.js";
|
|
@@ -71,13 +73,16 @@ export declare function parseExtractionOutput(result: ExecResult): ExtractValues
|
|
|
71
73
|
* The body, extracted so a test drives it without a registered workflow. It creates the ephemeral
|
|
72
74
|
* container, submits one exec, awaits the result durably, and gives the value map back. It revokes the run
|
|
73
75
|
* authorization on every terminal path, and it tears the container down on both paths.
|
|
76
|
+
*
|
|
77
|
+
* A chat turn awaits this workflow, thus a suspension is the `err` of the result, and the workflow does not
|
|
78
|
+
* cancel (workflow-suspension spec). The body does not mark the analysis: the caller reports the suspension.
|
|
74
79
|
*/
|
|
75
|
-
export declare function runExtractValuesBody(input: ExtractValuesWorkflowInput, deps: ExtractValuesDeps): Promise<ExtractValuesResult
|
|
80
|
+
export declare function runExtractValuesBody(input: ExtractValuesWorkflowInput, deps: ExtractValuesDeps): Promise<Result<ExtractValuesResult, Suspension>>;
|
|
76
81
|
/**
|
|
77
82
|
* Register the extraction workflow with DBOS. It gives back the registered callable, thus a trigger
|
|
78
83
|
* dispatches with `DBOS.startWorkflow`.
|
|
79
84
|
*/
|
|
80
|
-
export declare function registerExtractValuesWorkflow(deps: ExtractValuesDeps): (input: ExtractValuesWorkflowInput) => Promise<ExtractValuesResult
|
|
85
|
+
export declare function registerExtractValuesWorkflow(deps: ExtractValuesDeps): (input: ExtractValuesWorkflowInput) => Promise<Result<ExtractValuesResult, Suspension>>;
|
|
81
86
|
/** The per-attempt workflow id. A fresh nonce keeps each pass under a distinct id. */
|
|
82
87
|
export declare function extractValuesWorkflowId(analysisId: string, nonce: string): string;
|
|
83
88
|
/**
|
|
@@ -86,7 +91,7 @@ export declare function extractValuesWorkflowId(analysisId: string, nonce: strin
|
|
|
86
91
|
*/
|
|
87
92
|
export interface ExtractValuesTriggerDeps {
|
|
88
93
|
readonly runAuthorizer: RunAuthorizer;
|
|
89
|
-
readonly workflow: (input: ExtractValuesWorkflowInput) => Promise<ExtractValuesResult
|
|
94
|
+
readonly workflow: (input: ExtractValuesWorkflowInput) => Promise<Result<ExtractValuesResult, Suspension>>;
|
|
90
95
|
}
|
|
91
96
|
/** The identity and the requests for one extraction pass. */
|
|
92
97
|
export interface ExtractValuesTriggerParams {
|
|
@@ -96,12 +101,13 @@ export interface ExtractValuesTriggerParams {
|
|
|
96
101
|
}
|
|
97
102
|
/**
|
|
98
103
|
* The async edge. It authorizes through the `RunAuthorizer`, starts the workflow, and awaits the outcome.
|
|
99
|
-
* The body owns the revoke, thus the trigger holds no lifecycle. A workflow fault
|
|
100
|
-
* promise.
|
|
104
|
+
* The body owns the revoke, thus the trigger holds no lifecycle. A workflow fault and a refused
|
|
105
|
+
* authorization reject the returned promise. A suspension is the `err` of the result: of the workflow, or of
|
|
106
|
+
* an authorization that the host refused with the suspend flag.
|
|
101
107
|
*/
|
|
102
|
-
export declare function triggerExtractValues(deps: ExtractValuesTriggerDeps, params: ExtractValuesTriggerParams): Promise<ExtractValuesResult
|
|
103
|
-
/** Runs one extraction pass for the bound analysis, and gives the raw value map back. */
|
|
104
|
-
export type ExtractionRunner = (requests: readonly ExtractionRequest[]) => Promise<ExtractValuesResult
|
|
108
|
+
export declare function triggerExtractValues(deps: ExtractValuesTriggerDeps, params: ExtractValuesTriggerParams): Promise<Result<ExtractValuesResult, Suspension>>;
|
|
109
|
+
/** Runs one extraction pass for the bound analysis, and gives the raw value map back, or the suspension. */
|
|
110
|
+
export type ExtractionRunner = (requests: readonly ExtractionRequest[]) => Promise<Result<ExtractValuesResult, Suspension>>;
|
|
105
111
|
/**
|
|
106
112
|
* Bind the trigger to one analysis and one auth context. The result runs one pass with every request in it.
|
|
107
113
|
*/
|
|
@@ -119,6 +125,6 @@ export declare function extractionArtifactsFromResult(result: ExtractValuesResul
|
|
|
119
125
|
/**
|
|
120
126
|
* Make the extraction arm over a runner. One `extract` call makes one run with every request in it, and it
|
|
121
127
|
* gives the value map. A run fault rejects the promise, and the resolver turns the rejection into a failed
|
|
122
|
-
* reference.
|
|
128
|
+
* reference. A suspension is the `err`, and the resolver reports its reason on each reference.
|
|
123
129
|
*/
|
|
124
130
|
export declare function createExtractionArm(run: ExtractionRunner): ExtractionArm;
|
|
@@ -12,10 +12,15 @@
|
|
|
12
12
|
* standing machine exists.
|
|
13
13
|
*/
|
|
14
14
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
15
|
+
import { err, ok } from "neverthrow";
|
|
15
16
|
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { forStep } from "../auth/types.js";
|
|
16
18
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
19
|
+
import { deliverNotice, passGate } from "../lib/hooks.js";
|
|
17
20
|
import { generateExecutionId } from "../sandbox/execution-id.js";
|
|
18
21
|
import { mintSandboxIdentity } from "../sandbox/identity.js";
|
|
22
|
+
import { keepSuspendingRefusal } from "../sandbox/sandbox-error.js";
|
|
23
|
+
import { suspensionOfRefusal, suspensionOfSpawnRefusal } from "../workflows/suspension.js";
|
|
19
24
|
import { EXTRACTION_INPUT_ENV, EXTRACTION_SCRIPT, ExtractValuesResultSchema } from "./extract-values-script.js";
|
|
20
25
|
/** The synthetic run id and step id of the extraction pass. Both are constants shared by every analysis. */
|
|
21
26
|
const EXTRACT_VALUES_RUN_LITERAL = "extract-values";
|
|
@@ -85,24 +90,28 @@ export function parseExtractionOutput(result) {
|
|
|
85
90
|
* The body, extracted so a test drives it without a registered workflow. It creates the ephemeral
|
|
86
91
|
* container, submits one exec, awaits the result durably, and gives the value map back. It revokes the run
|
|
87
92
|
* authorization on every terminal path, and it tears the container down on both paths.
|
|
93
|
+
*
|
|
94
|
+
* A chat turn awaits this workflow, thus a suspension is the `err` of the result, and the workflow does not
|
|
95
|
+
* cancel (workflow-suspension spec). The body does not mark the analysis: the caller reports the suspension.
|
|
88
96
|
*/
|
|
89
97
|
export async function runExtractValuesBody(input, deps) {
|
|
90
98
|
const logger = (deps.logger ?? createNoopLogger()).named("extract-values").with({ analysisId: input.analysisId });
|
|
91
99
|
const { analysisId, runSession, requests, ownsMandate = true } = input; // oss-core-managed-ok
|
|
92
100
|
const authorization = { runSession, ownsMandate }; // oss-core-managed-ok
|
|
101
|
+
const revoke = (reason) => deliverNotice(logger, "RunAuthorizer.revoke", deps.runAuthorizer.revoke(authorization, reason));
|
|
93
102
|
try {
|
|
94
103
|
const executionId = generateExecutionId(EXTRACT_VALUES_AGENT_ID);
|
|
95
104
|
const workflowId = DBOS.workflowID ?? `${EXTRACT_VALUES_RUN_LITERAL}:${executionId}`;
|
|
96
105
|
// The container mounts the analysis tree read-only. The extraction pass only reads, thus it needs
|
|
97
106
|
// no writable step mount.
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const spawned = keepSuspendingRefusal(await deps.sandboxClient.createSandbox(forStep(runSession, EXTRACT_VALUES_STEP_LITERAL), { childWorkflowId: workflowId, resources: EXTRACTION_RESOURCES, readOnly: true }, mintSandboxIdentity(EXTRACT_VALUES_RUN_LITERAL)));
|
|
108
|
+
if (spawned.isErr()) {
|
|
109
|
+
const suspension = suspensionOfSpawnRefusal(spawned.error);
|
|
110
|
+
logger.warn("extraction suspended", { reason: suspension.reason });
|
|
111
|
+
await revoke("extract-values-suspended");
|
|
112
|
+
return err(suspension);
|
|
113
|
+
}
|
|
114
|
+
const sandbox = spawned.value;
|
|
106
115
|
try {
|
|
107
116
|
// A checkpointed clock, not `Date.now()`: the await gates on this absolute deadline, and a
|
|
108
117
|
// wall-clock deadline that grew on replay would shift which loop iteration crosses it.
|
|
@@ -110,8 +119,8 @@ export async function runExtractValuesBody(input, deps) {
|
|
|
110
119
|
await deps.sandboxClient.submitExec(sandbox, buildExtractionExec(analysisId, requests, executionId));
|
|
111
120
|
const result = await deps.sandboxClient.awaitExec(sandbox, executionId, noopEmit, deadlineAbs);
|
|
112
121
|
const map = parseExtractionOutput(result);
|
|
113
|
-
await
|
|
114
|
-
return map;
|
|
122
|
+
await revoke("extract-values-completed");
|
|
123
|
+
return ok(map);
|
|
115
124
|
}
|
|
116
125
|
finally {
|
|
117
126
|
try {
|
|
@@ -124,7 +133,7 @@ export async function runExtractValuesBody(input, deps) {
|
|
|
124
133
|
}
|
|
125
134
|
catch (err) {
|
|
126
135
|
logger.error("extraction failed", logger.errorFields(err));
|
|
127
|
-
await
|
|
136
|
+
await revoke("extract-values-failed");
|
|
128
137
|
throw err;
|
|
129
138
|
}
|
|
130
139
|
}
|
|
@@ -141,18 +150,25 @@ export function extractValuesWorkflowId(analysisId, nonce) {
|
|
|
141
150
|
}
|
|
142
151
|
/**
|
|
143
152
|
* The async edge. It authorizes through the `RunAuthorizer`, starts the workflow, and awaits the outcome.
|
|
144
|
-
* The body owns the revoke, thus the trigger holds no lifecycle. A workflow fault
|
|
145
|
-
* promise.
|
|
153
|
+
* The body owns the revoke, thus the trigger holds no lifecycle. A workflow fault and a refused
|
|
154
|
+
* authorization reject the returned promise. A suspension is the `err` of the result: of the workflow, or of
|
|
155
|
+
* an authorization that the host refused with the suspend flag.
|
|
146
156
|
*/
|
|
147
157
|
export async function triggerExtractValues(deps, params) {
|
|
148
158
|
const { auth, analysisId, requests } = params;
|
|
149
|
-
const
|
|
150
|
-
// oss-core-managed-ok
|
|
159
|
+
const authorized = await passGate("RunAuthorizer.authorize", deps.runAuthorizer.authorize({
|
|
151
160
|
auth,
|
|
152
161
|
scope: { kind: "analysis", analysisId },
|
|
153
162
|
provenance: { agentId: EXTRACT_VALUES_AGENT_ID, callPath: [EXTRACT_VALUES_AGENT_ID] },
|
|
154
163
|
frame: { runId: EXTRACT_VALUES_RUN_LITERAL, stepId: EXTRACT_VALUES_STEP_LITERAL },
|
|
155
|
-
});
|
|
164
|
+
}));
|
|
165
|
+
if (authorized.isErr()) {
|
|
166
|
+
const suspension = suspensionOfRefusal(authorized.error);
|
|
167
|
+
if (suspension !== undefined)
|
|
168
|
+
return err(suspension);
|
|
169
|
+
throw new Error(`the extraction was not authorized: ${authorized.error.reason}`);
|
|
170
|
+
}
|
|
171
|
+
const { runSession, ownsMandate } = authorized.value; // oss-core-managed-ok
|
|
156
172
|
const handle = (await DBOS.startWorkflow(deps.workflow, {
|
|
157
173
|
workflowID: extractValuesWorkflowId(analysisId, randomUUID()),
|
|
158
174
|
})({ analysisId, runSession, requests: [...requests], ownsMandate })); // oss-core-managed-ok
|
|
@@ -182,12 +198,12 @@ export function extractionArtifactsFromResult(result) {
|
|
|
182
198
|
/**
|
|
183
199
|
* Make the extraction arm over a runner. One `extract` call makes one run with every request in it, and it
|
|
184
200
|
* gives the value map. A run fault rejects the promise, and the resolver turns the rejection into a failed
|
|
185
|
-
* reference.
|
|
201
|
+
* reference. A suspension is the `err`, and the resolver reports its reason on each reference.
|
|
186
202
|
*/
|
|
187
203
|
export function createExtractionArm(run) {
|
|
188
204
|
return {
|
|
189
205
|
async extract(requests) {
|
|
190
|
-
return
|
|
206
|
+
return (await run(requests)).map(extractionArtifactsFromResult);
|
|
191
207
|
},
|
|
192
208
|
};
|
|
193
209
|
}
|
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
* hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
|
|
4
4
|
*/
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { ok } from "neverthrow";
|
|
6
|
+
import { err, ok } from "neverthrow";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
|
|
9
9
|
import { DEFAULT_SANDBOX_MAX_STEPS } from "../config/resource-limits.js";
|
|
10
10
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
11
|
+
import { deliverNotice, passGate } from "../lib/hooks.js";
|
|
11
12
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
12
13
|
import { buildRunCardData } from "../memory/card-builders.js";
|
|
13
14
|
import { formatQuery, parseQuery } from "../sandbox/package-identity.js";
|
|
14
15
|
import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
|
|
15
16
|
import { validatePlan } from "../schemas/validate-plan.js";
|
|
16
|
-
import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, reserveRunById, updateRunStatus, upsertPlan } from "../state/index.js";
|
|
17
|
+
import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, reserveRunById, suspendAnalysis, updateRunStatus, upsertPlan } from "../state/index.js";
|
|
17
18
|
import { routeAdHocRequest } from "./ad-hoc-router.js";
|
|
18
19
|
import { adHocPlanId, adHocRunId } from "./analysis-invocation.js";
|
|
19
20
|
import { defineTool } from "./define-tool.js";
|
|
@@ -383,19 +384,25 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
383
384
|
return ok({ runId: reservation.row.runId, status: "in_progress" });
|
|
384
385
|
}
|
|
385
386
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
387
|
+
const authorized = await passGate("RunAuthorizer.authorize", deps.runAuthorizer.authorize({
|
|
388
|
+
auth: ctx.session.auth,
|
|
389
|
+
scope: ctx.session.scope,
|
|
390
|
+
provenance: ctx.session.provenance,
|
|
391
|
+
frame: { runId },
|
|
392
|
+
}));
|
|
393
|
+
if (authorized.isErr()) {
|
|
394
|
+
// The refusal ends the run before a workflow exists, thus the
|
|
395
|
+
// reserved row must not stay `running`.
|
|
396
|
+
const { kind, reason } = authorized.error;
|
|
397
|
+
if (kind === "suspended") {
|
|
398
|
+
await updateRunStatus(deps.pool, runId, "canceled", reason).match(() => { }, () => { });
|
|
399
|
+
unwrapOrThrow(await suspendAnalysis(deps.pool, analysisId));
|
|
400
|
+
return err({ error: `The analysis is suspended: ${reason}`, retryable: false });
|
|
401
|
+
}
|
|
402
|
+
await updateRunStatus(deps.pool, runId, "failed", reason).match(() => { }, () => { });
|
|
403
|
+
return err({ error: `The run was not authorized: ${reason}`, retryable: false });
|
|
398
404
|
}
|
|
405
|
+
const authorization = authorized.value;
|
|
399
406
|
const wfInput = workflowInput(planId, plan, {
|
|
400
407
|
analysisId,
|
|
401
408
|
threadId,
|
|
@@ -408,7 +415,7 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
408
415
|
await deps.runLauncher.launch(deps.executeAnalysisWorkflow, { workflowId: runId }, wfInput);
|
|
409
416
|
}
|
|
410
417
|
catch (error) {
|
|
411
|
-
await deps.runAuthorizer.revoke(authorization, "workflow-start-failed")
|
|
418
|
+
await deliverNotice(logger, "RunAuthorizer.revoke", deps.runAuthorizer.revoke(authorization, "workflow-start-failed"));
|
|
412
419
|
await updateRunStatus(deps.pool, runId, "failed", "workflow start failed").match(() => { }, () => { });
|
|
413
420
|
throw error;
|
|
414
421
|
}
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
* under the callback transport, and a report turn is not a body. Thus the tool takes an injected runner,
|
|
34
34
|
* and a registered workflow owns the container. The tool holds no sandbox client, and it imports no DBOS.
|
|
35
35
|
*/
|
|
36
|
+
import { type Result } from "neverthrow";
|
|
36
37
|
import { z } from "zod";
|
|
38
|
+
import type { RunSession } from "../../auth/types.js";
|
|
39
|
+
import type { Suspension } from "../../workflows/suspension.js";
|
|
37
40
|
import type { RunAuthorizer } from "../../execution/run-authorizer.js";
|
|
38
41
|
import { type Logger } from "../../lib/logger.js";
|
|
39
42
|
import type { ResourceSpec } from "../../config/resource-limits.js";
|
|
@@ -90,6 +93,10 @@ export type DeriveTableResult = {
|
|
|
90
93
|
} | {
|
|
91
94
|
outcome: "exec-failed";
|
|
92
95
|
detail: string;
|
|
96
|
+
} | {
|
|
97
|
+
outcome: "suspended";
|
|
98
|
+
reason: string;
|
|
99
|
+
detail: string;
|
|
93
100
|
} | {
|
|
94
101
|
outcome: "no-output";
|
|
95
102
|
detail: string;
|
|
@@ -172,6 +179,7 @@ export declare function describeExecFailure(result: ExecResult): string | undefi
|
|
|
172
179
|
*/
|
|
173
180
|
export interface DeriveTableExecInput {
|
|
174
181
|
readonly analysisId: string;
|
|
182
|
+
readonly runSession: RunSession;
|
|
175
183
|
readonly executionId: string;
|
|
176
184
|
readonly script: string;
|
|
177
185
|
readonly writableTail: string;
|
|
@@ -186,7 +194,7 @@ export interface DeriveTableExecInput {
|
|
|
186
194
|
* and the await is legal under each transport. A fault of the sandbox rejects the promise, and the tool
|
|
187
195
|
* turns that rejection into one short detail.
|
|
188
196
|
*/
|
|
189
|
-
export type DeriveTableRunner = (input: DeriveTableExecInput) => Promise<ExecResult
|
|
197
|
+
export type DeriveTableRunner = (input: DeriveTableExecInput) => Promise<Result<ExecResult, Suspension>>;
|
|
190
198
|
/**
|
|
191
199
|
* Make the derivation tool over the session-state gateway, the derivation ledger, and the sandbox rails.
|
|
192
200
|
*
|
|
@@ -38,6 +38,7 @@ import { unlink } from "node:fs/promises";
|
|
|
38
38
|
import { join } from "node:path";
|
|
39
39
|
import { z } from "zod";
|
|
40
40
|
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
41
|
+
import { deliverNotice, passGate } from "../../lib/hooks.js";
|
|
41
42
|
import { classifyWithinRoot, computeSha256, computeSha256File } from "../../lib/fs-helpers.js";
|
|
42
43
|
import { defaultErrorFields } from "../../lib/logger.js";
|
|
43
44
|
import { snapshotEntry } from "../../report-model/reference-resolver.js";
|
|
@@ -223,19 +224,20 @@ export function createDeriveTableTool(deps) {
|
|
|
223
224
|
logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
224
225
|
return ok({ outcome: "unavailable", detail: "the workspace root did not resolve, thus the derived table has no place to land" });
|
|
225
226
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return ok({ outcome: "unavailable", detail:
|
|
227
|
+
const authorized = await passGate("RunAuthorizer.authorize", runAuthorizer.authorize({
|
|
228
|
+
auth: ctx.session.auth,
|
|
229
|
+
scope: { kind: "analysis", analysisId },
|
|
230
|
+
provenance: { agentId: DERIVE_AGENT_ID, callPath: [DERIVE_AGENT_ID] },
|
|
231
|
+
frame: { runId: DERIVE_RUN_LITERAL, stepId: DERIVE_STEP_LITERAL },
|
|
232
|
+
}));
|
|
233
|
+
if (authorized.isErr()) {
|
|
234
|
+
const { kind, reason } = authorized.error;
|
|
235
|
+
logger.error("the derivation was not authorized", { threadId, analysisId, reason, suspend: kind === "suspended" });
|
|
236
|
+
if (kind === "suspended")
|
|
237
|
+
return ok({ outcome: "suspended", reason, detail: `the analysis is suspended (${reason}), thus the derivation did not run` });
|
|
238
|
+
return ok({ outcome: "unavailable", detail: `the derivation was not authorized: ${reason}` });
|
|
238
239
|
}
|
|
240
|
+
const authorization = authorized.value;
|
|
239
241
|
// The work sits inside the `try`, and the revoke sits in the `finally`. Thus the authorization
|
|
240
242
|
// revokes on every terminal path, the thrown one included: a path builder refuses a hostile
|
|
241
243
|
// stored path with a throw, and that throw must not leave an authorization standing.
|
|
@@ -247,6 +249,7 @@ export function createDeriveTableTool(deps) {
|
|
|
247
249
|
root,
|
|
248
250
|
threadId,
|
|
249
251
|
analysisId,
|
|
252
|
+
runSession: authorization.runSession,
|
|
250
253
|
script: input.script,
|
|
251
254
|
sources,
|
|
252
255
|
writableTail,
|
|
@@ -270,14 +273,9 @@ export function createDeriveTableTool(deps) {
|
|
|
270
273
|
return ok(result);
|
|
271
274
|
}
|
|
272
275
|
finally {
|
|
273
|
-
//
|
|
274
|
-
// result means that the work threw, and a throw is a failed derivation.
|
|
275
|
-
|
|
276
|
-
await runAuthorizer.revoke(authorization, result?.outcome === "derived" ? "derive-table-completed" : "derive-table-failed");
|
|
277
|
-
}
|
|
278
|
-
catch (cause) {
|
|
279
|
-
logger.warn("the derivation authorization did not revoke", { threadId, analysisId, ...defaultErrorFields(cause) });
|
|
280
|
-
}
|
|
276
|
+
// The revoke is a notice: its failure changes no outcome of the call, thus it reaches the log
|
|
277
|
+
// alone. An absent result means that the work threw, and a throw is a failed derivation.
|
|
278
|
+
await deliverNotice(logger.with({ threadId, analysisId }), "RunAuthorizer.revoke", runAuthorizer.revoke(authorization, result?.outcome === "derived" ? "derive-table-completed" : "derive-table-failed"));
|
|
281
279
|
}
|
|
282
280
|
},
|
|
283
281
|
});
|
|
@@ -318,6 +316,7 @@ async function derive(args) {
|
|
|
318
316
|
// thus that refusal stays a throw and it never reads as a fault of the container.
|
|
319
317
|
const execInput = {
|
|
320
318
|
analysisId: args.analysisId,
|
|
319
|
+
runSession: args.runSession,
|
|
321
320
|
executionId,
|
|
322
321
|
script: args.script,
|
|
323
322
|
writableTail: args.writableTail,
|
|
@@ -325,14 +324,18 @@ async function derive(args) {
|
|
|
325
324
|
inputs: args.sources.map((source) => ({ path: toSandboxPath(args.root, args.analysisId, join(args.root, source.path)), hash: source.hash })),
|
|
326
325
|
output: toSandboxPath(args.root, args.analysisId, absolute),
|
|
327
326
|
};
|
|
328
|
-
let
|
|
327
|
+
let ran;
|
|
329
328
|
try {
|
|
330
|
-
|
|
329
|
+
ran = await args.runDerivation(execInput);
|
|
331
330
|
}
|
|
332
331
|
catch (cause) {
|
|
333
332
|
args.logger.error("the derivation exec did not complete", { threadId: args.threadId, ...defaultErrorFields(cause) });
|
|
334
333
|
return { outcome: "exec-failed", detail: "the derivation exec did not complete" };
|
|
335
334
|
}
|
|
335
|
+
if (ran.isErr()) {
|
|
336
|
+
return { outcome: "suspended", reason: ran.error.reason, detail: `the analysis is suspended (${ran.error.reason}), thus the derivation did not run` };
|
|
337
|
+
}
|
|
338
|
+
const executed = ran.value;
|
|
336
339
|
const failure = describeExecFailure(executed);
|
|
337
340
|
if (failure !== undefined) {
|
|
338
341
|
return { outcome: "exec-failed", detail: failure };
|
|
@@ -53,7 +53,7 @@ declare function formatRun(r: CortexRunRow, verbose: boolean, exposePaths?: bool
|
|
|
53
53
|
synthesisReason?: string | undefined;
|
|
54
54
|
runId: string;
|
|
55
55
|
workflowName: string;
|
|
56
|
-
status: "
|
|
56
|
+
status: "failed" | "running" | "completed" | "partial" | "canceled" | "suspended_insufficient_funds";
|
|
57
57
|
startedAt: string;
|
|
58
58
|
completedAt: string | null;
|
|
59
59
|
planId: string | null;
|
|
@@ -31,23 +31,30 @@
|
|
|
31
31
|
* - await child results via `getResult` (these are themselves cached
|
|
32
32
|
* in DBOS, so parent recovery does NOT re-run completed children)
|
|
33
33
|
* 3. Failure isolation
|
|
34
|
-
* - a child settling `failed`/`blocked` (or throwing for a
|
|
35
|
-
*
|
|
36
|
-
* dependency-satisfied, are marked `skipped` on the dag-state
|
|
37
|
-
* are never dispatched. In-flight siblings and independent
|
|
38
|
-
* continue; the loop drains and the run finalises (typically
|
|
34
|
+
* - a child settling `failed`/`blocked` (or throwing for a cause that is
|
|
35
|
+
* not a suspension) dooms only its transitive dependents: they can never
|
|
36
|
+
* become dependency-satisfied, are marked `skipped` on the dag-state
|
|
37
|
+
* part, and are never dispatched. In-flight siblings and independent
|
|
38
|
+
* ready steps continue; the loop drains and the run finalises (typically
|
|
39
|
+
* `partial`).
|
|
39
40
|
* - the halt cascade (cancel in-flight via
|
|
40
41
|
* `Promise.allSettled(handles.map(h => DBOS.cancelWorkflow(h.workflowID)))`,
|
|
41
|
-
* stop scheduling) survives only for
|
|
42
|
-
* `
|
|
43
|
-
* 4.
|
|
44
|
-
* -
|
|
45
|
-
*
|
|
46
|
-
*
|
|
42
|
+
* stop scheduling) survives only for a suspension of a child and the
|
|
43
|
+
* `neverFits` plan-validation guard.
|
|
44
|
+
* 4. Suspension cascade (workflow-suspension spec)
|
|
45
|
+
* - a child that suspends sends a typed message on `CHILD_SUSPENDED_TOPIC`
|
|
46
|
+
* and cancels itself. The parent reads the kind of the message, never a
|
|
47
|
+
* reason string: it cancels the siblings, then self-cancels to
|
|
48
|
+
* `CANCELLED` (NOT `ERROR` — ERROR isn't resumable; NOTES #3)
|
|
49
|
+
* - a refusal of the `RunCharge.open` gate with the suspend flag suspends
|
|
50
|
+
* the run before any step starts
|
|
51
|
+
* - a `suspend` error of a synthesis model request suspends the run in
|
|
52
|
+
* place of the synthesis failure
|
|
47
53
|
* - the analysis flips to `suspended_insufficient_funds` in
|
|
48
54
|
* `collectAndComplete`. The paused parent lands in `CANCELLED`, a
|
|
49
|
-
* durably reschedulable state; re-driving it
|
|
50
|
-
*
|
|
55
|
+
* durably reschedulable state; re-driving it is a future enhancement,
|
|
56
|
+
* not yet wired here. The reason of the host rides to the run row, the
|
|
57
|
+
* charge close, the metric, and the terminal part, unread.
|
|
51
58
|
* 5. `synthesizeFindings` (single sequential block, no sandbox)
|
|
52
59
|
* 6. `collectAndComplete` (terminal — runs on ALL paths)
|
|
53
60
|
* - determine final status from completed/cancelled/failed counts
|
|
@@ -77,6 +84,7 @@ import type { RunCharge } from "../billing/run-charge.js";
|
|
|
77
84
|
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
78
85
|
import { type PlanStep } from "./execute-analysis-scheduler.js";
|
|
79
86
|
import { type SandboxStepInput, type SandboxStepResult } from "./sandbox-step.js";
|
|
87
|
+
import { type Suspension } from "./suspension.js";
|
|
80
88
|
/** Registered child sandbox-step callable the parent's child-dispatch closes over. */
|
|
81
89
|
type SandboxStepCallable = (input: SandboxStepInput) => Promise<SandboxStepResult>;
|
|
82
90
|
/** Quick-display finding on the run-completed card. Mirrors the wire schema's RunCompletedFinding. */
|
|
@@ -320,13 +328,14 @@ export declare function composeStepSeed(args: {
|
|
|
320
328
|
* its reason in `blockedReason` — an honest agent-declared blocker, not an
|
|
321
329
|
* error — and a failure carries the same reason string
|
|
322
330
|
* `persist-synthesis-outcome` writes to `cortex_runs.synthesis_reason`, so the
|
|
323
|
-
* two ledgers cannot disagree about why synthesis died.
|
|
324
|
-
*
|
|
331
|
+
* two ledgers cannot disagree about why synthesis died. A suspension is not an
|
|
332
|
+
* outcome: the row is canceled, as the row of a suspended step. Exported for
|
|
333
|
+
* the projection tests (the `buildChildInput` pattern).
|
|
325
334
|
*/
|
|
326
335
|
export declare function synthesisRowUpdate(outcome: {
|
|
327
336
|
status: SynthesisStatus;
|
|
328
337
|
reason: string | null;
|
|
329
|
-
}, durationMs: number): UpdateStepExecutionInput;
|
|
338
|
+
} | Suspension, durationMs: number): UpdateStepExecutionInput;
|
|
330
339
|
/**
|
|
331
340
|
* Run-level literature-grounded synthesis (no sandbox). Returns the
|
|
332
341
|
* quick-display findings for the run-completed card; empty when synthesis is
|