@inflexa-ai/harness 0.36.4 → 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/message.d.ts +15 -0
- 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/memory/conversation-display-replay.js +14 -1
- package/dist/memory/thread-history.d.ts +24 -0
- package/dist/memory/thread-history.js +19 -6
- 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/init.js +11 -0
- 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
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* Mirrors the `SessionPagePublisher` seam: a narrow interface in the harness, the managed
|
|
15
15
|
* realization injected at the composition root.
|
|
16
16
|
*/
|
|
17
|
+
import type { ResultAsync } from "neverthrow";
|
|
17
18
|
import type { AuthContext, Provenance, RunFrame, RunSession, Scope } from "../auth/types.js";
|
|
19
|
+
import type { GateFailure, NoticeFailure } from "../lib/hooks.js";
|
|
18
20
|
/**
|
|
19
21
|
* Locates a run credential when no `RunAuthorization` exists: the persisted
|
|
20
22
|
* jti from `cortex_runs` plus the revoking caller's opaque `auth` — the sole
|
|
@@ -48,8 +50,8 @@ export interface RunAuthorization {
|
|
|
48
50
|
* path (a no-op for reused or local authorizations).
|
|
49
51
|
*/
|
|
50
52
|
export interface RunAuthorizer {
|
|
51
|
-
authorize(input: AuthorizeRunInput):
|
|
52
|
-
revoke(authorization: RunAuthorization, reason: string):
|
|
53
|
+
authorize(input: AuthorizeRunInput): ResultAsync<RunAuthorization, GateFailure>;
|
|
54
|
+
revoke(authorization: RunAuthorization, reason: string): ResultAsync<void, NoticeFailure>;
|
|
53
55
|
/**
|
|
54
56
|
* Out-of-band revocation for paths holding no `RunAuthorization`. Normal
|
|
55
57
|
* terminal paths use `revoke(authorization, reason)` under the run
|
|
@@ -57,5 +59,5 @@ export interface RunAuthorizer {
|
|
|
57
59
|
* credential's JWT was never persisted and only its jti survives on
|
|
58
60
|
* `cortex_runs`. Local/OSS authorizers no-op — they mint no jti.
|
|
59
61
|
*/
|
|
60
|
-
revokeByJti(ref: RevokeByJtiRef, reason: string):
|
|
62
|
+
revokeByJti(ref: RevokeByJtiRef, reason: string): ResultAsync<void, NoticeFailure>;
|
|
61
63
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* terminal frame read-side.
|
|
19
19
|
*/
|
|
20
20
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
21
|
+
import { deliverNotice } from "../lib/hooks.js";
|
|
21
22
|
import { elapsedSinceIso, recordRunCompleted, recordStepCompleted } from "../lib/metrics.js";
|
|
22
23
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
23
24
|
import { markRunCanceledIfActive, queryRun } from "../state/runs.js";
|
|
@@ -113,26 +114,9 @@ export function createRunCanceler(deps) {
|
|
|
113
114
|
catch (err) {
|
|
114
115
|
logger.error("pending-step sweep failed", { runId, ...logger.errorFields(err) });
|
|
115
116
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
await runCharge.close({ analysisId: row.analysisId, runId, reason: "canceled", session });
|
|
121
|
-
charge = true;
|
|
122
|
-
}
|
|
123
|
-
catch (err) {
|
|
124
|
-
logger.error("running-charge close failed", { runId, ...logger.errorFields(err) });
|
|
125
|
-
}
|
|
126
|
-
let mandate = row.mandateJti === null;
|
|
127
|
-
if (row.mandateJti !== null) {
|
|
128
|
-
try {
|
|
129
|
-
await runAuthorizer.revokeByJti({ jti: row.mandateJti, auth: session.auth }, EXTERNAL_CANCEL_REASON);
|
|
130
|
-
mandate = true;
|
|
131
|
-
}
|
|
132
|
-
catch (err) {
|
|
133
|
-
logger.error("mandate revoke failed", { runId, jti: row.mandateJti, ...logger.errorFields(err) });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
117
|
+
const charge = await deliverNotice(logger.with({ runId }), "RunCharge.close", runCharge.close({ analysisId: row.analysisId, runId, outcome: { kind: "canceled" }, session }));
|
|
118
|
+
const mandate = row.mandateJti === null ||
|
|
119
|
+
(await deliverNotice(logger.with({ runId, jti: row.mandateJti }), "RunAuthorizer.revokeByJti", runAuthorizer.revokeByJti({ jti: row.mandateJti, auth: session.auth }, EXTERNAL_CANCEL_REASON)));
|
|
136
120
|
// No transition means the run reached a terminal state on its own (or the
|
|
137
121
|
// write failed) — report the row as it stands rather than claiming `canceled`.
|
|
138
122
|
let finalStatus = "canceled";
|
package/dist/index.d.ts
CHANGED
|
@@ -24,10 +24,10 @@ export type { ConversationAgentDeps } from "./agents/conversation-agent.js";
|
|
|
24
24
|
export type { BioToolKeys } from "./tools/bio/keys.js";
|
|
25
25
|
export { createLocalRunAuthorizer } from "./auth/local-run-authorizer.js";
|
|
26
26
|
export type { RunAuthorizer, AuthorizeRunInput, RunAuthorization, RevokeByJtiRef } from "./execution/run-authorizer.js";
|
|
27
|
-
export {
|
|
28
|
-
export type {
|
|
27
|
+
export type { GateFailure, NoticeFailure } from "./lib/hooks.js";
|
|
28
|
+
export type { RequestHeaders, ResolveRequestHeaders } from "./providers/request-headers.js";
|
|
29
29
|
export { createNoopRunCharge } from "./billing/noop-run-charge.js";
|
|
30
|
-
export type { RunCharge } from "./billing/run-charge.js";
|
|
30
|
+
export type { RunCharge, RunChargeOutcome } from "./billing/run-charge.js";
|
|
31
31
|
export { createNoopUsageRecorder } from "./billing/noop-usage-recorder.js";
|
|
32
32
|
export type { UsageRecorder, LlmUsageRecord } from "./billing/usage-recorder.js";
|
|
33
33
|
export { createNoopArtifactRegistry } from "./execution/noop-artifact-registry.js";
|
|
@@ -63,12 +63,12 @@ export type { RunAgentOptions, RunAgentResult, AgentFinish } from "./loop/run-ag
|
|
|
63
63
|
export type { AgentDefinition, RunStep, EmitFn, EmitEvent, EventSource } from "./loop/types.js";
|
|
64
64
|
export { passthroughStep } from "./loop/run-step.js";
|
|
65
65
|
export { forStep, forSubAgent, scopeResource, scopeWorkloadId } from "./auth/types.js";
|
|
66
|
-
export type { AgentSession, RequestSession, RunSession, Identity, Scope, Credential, Provenance, RunFrame, AuthContext, ResourceCoordinates, } from "./auth/types.js";
|
|
66
|
+
export type { AgentSession, RequestSession, RunSession, Identity, Scope, Credential, Provenance, RunFrame, AuthContext, ResourceCoordinates, HookSessionView, ResolvableSession, } from "./auth/types.js";
|
|
67
67
|
export { makeLocalAuth } from "./auth/local-auth-context.js";
|
|
68
68
|
export { createAnthropicProvider } from "./providers/anthropic.js";
|
|
69
69
|
export type { AnthropicProviderDeps } from "./providers/anthropic.js";
|
|
70
70
|
export { createConfiguredAiSdkProvider, DEFAULT_MAX_OUTPUT_TOKENS } from "./providers/ai-sdk.js";
|
|
71
|
-
export type { AiSdkProviderConfig, ConfiguredAiSdkProviderDeps } from "./providers/ai-sdk.js";
|
|
71
|
+
export type { AiSdkProviderConfig, ConfiguredAiSdkProviderDeps, ProviderHostPolicy } from "./providers/ai-sdk.js";
|
|
72
72
|
export { createEmbeddingProvider } from "./providers/embedding.js";
|
|
73
73
|
export type { EmbeddingProviderDeps } from "./providers/embedding.js";
|
|
74
74
|
export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ChatUsage, ModelMessage, PromptCachePolicy, ReasoningPolicy, } from "./providers/types.js";
|
|
@@ -76,6 +76,10 @@ export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions, withPromptCacheBreakp
|
|
|
76
76
|
export { DEFAULT_REASONING } from "./providers/reasoning.js";
|
|
77
77
|
export { toProviderError, isProviderError } from "./providers/errors.js";
|
|
78
78
|
export type { ProviderError } from "./providers/errors.js";
|
|
79
|
+
export { DEFAULT_SUSPEND_ON } from "./providers/errors.js";
|
|
80
|
+
export type { SuspendOn } from "./providers/errors.js";
|
|
81
|
+
export { suspensionOfFailure } from "./workflows/suspension.js";
|
|
82
|
+
export type { Suspension } from "./workflows/suspension.js";
|
|
79
83
|
export { createStreamingChat } from "./providers/streaming-chat.js";
|
|
80
84
|
export { prepareChatTurn } from "./app/chat-turn.js";
|
|
81
85
|
export type { PrepareChatTurnDeps, PrepareChatTurnParams, PrepareChatTurnResult } from "./app/chat-turn.js";
|
|
@@ -137,8 +141,10 @@ export type { ResolveWorkspaceRoot } from "./workspace/paths.js";
|
|
|
137
141
|
export { createSandboxClient } from "./sandbox/create-sandbox.js";
|
|
138
142
|
export type { CreateSandboxClientConfig, SandboxBackendConfig } from "./sandbox/create-sandbox.js";
|
|
139
143
|
export type { SandboxClient } from "./sandbox/client.js";
|
|
140
|
-
export {
|
|
141
|
-
export type {
|
|
144
|
+
export type { ResolveSandboxLabels, SandboxLabels, SandboxSpec } from "./sandbox/types.js";
|
|
145
|
+
export type { SpawnSession } from "./auth/types.js";
|
|
146
|
+
export { describeSandboxError, keepSuspendingRefusal, SandboxFailure } from "./sandbox/sandbox-error.js";
|
|
147
|
+
export type { SandboxError, SuspendingRefusal } from "./sandbox/sandbox-error.js";
|
|
142
148
|
export type { ExtendAnalysisFarm, FarmLocation, FarmResolution, FarmSource, PackageRequestOutcome, ResolveAnalysisFarm, ToolchainSource, } from "./sandbox/types.js";
|
|
143
149
|
export { formatQuery, identityAddress, identityKey, identityOf, parseIdentityKey, parseQuery, pythonIdentity, resolveQuery, rIdentity, } from "./sandbox/package-identity.js";
|
|
144
150
|
export type { PackageIdentity, PackageQuery, ParseQueryError, PoolIndex, QueryResolution, Track } from "./sandbox/package-identity.js";
|
package/dist/index.js
CHANGED
|
@@ -29,13 +29,11 @@ export { bootHarness } from "./runtime/boot.js";
|
|
|
29
29
|
export { createConversationAgent } from "./agents/conversation-agent.js";
|
|
30
30
|
// Seam: run authorization.
|
|
31
31
|
export { createLocalRunAuthorizer } from "./auth/local-run-authorizer.js";
|
|
32
|
-
// Seam: billing resolution.
|
|
33
|
-
export { createNoopBillingResolver } from "./billing/noop-resolver.js";
|
|
34
32
|
// Seam: run-level billing bracket.
|
|
35
33
|
export { createNoopRunCharge } from "./billing/noop-run-charge.js";
|
|
36
|
-
// Seam: per-call LLM usage accounting. `record` is
|
|
37
|
-
//
|
|
38
|
-
//
|
|
34
|
+
// Seam: per-call LLM usage accounting. `record` is a notice: the loop does not
|
|
35
|
+
// wait for its result, and it logs the reason of an err when the result
|
|
36
|
+
// arrives. Wired as the optional `usageRecorder` of `assembleCoreRuntime`;
|
|
39
37
|
// unwired, every call is dropped by `createNoopUsageRecorder`.
|
|
40
38
|
export { createNoopUsageRecorder } from "./billing/noop-usage-recorder.js";
|
|
41
39
|
// Seam: artifact registration.
|
|
@@ -135,6 +133,12 @@ export { DEFAULT_REASONING } from "./providers/reasoning.js";
|
|
|
135
133
|
// directly needs: `chatStream` throws a `ProviderError` value (a plain object,
|
|
136
134
|
// not an `Error`), so a `catch` recognizes it by shape, not `instanceof`.
|
|
137
135
|
export { toProviderError, isProviderError } from "./providers/errors.js";
|
|
136
|
+
// The suspend map of a provider: an HTTP status to the suspend reason of the
|
|
137
|
+
// host. A map from the host replaces `DEFAULT_SUSPEND_ON`, which holds `402`.
|
|
138
|
+
export { DEFAULT_SUSPEND_ON } from "./providers/errors.js";
|
|
139
|
+
// The suspension of work. A host reads the suspend reason of a failed chat turn
|
|
140
|
+
// with `suspensionOfFailure`, by the kind of the error, never by its text.
|
|
141
|
+
export { suspensionOfFailure } from "./workflows/suspension.js";
|
|
138
142
|
// `createStreamingChat` wraps a `ChatProvider` as a streaming `AgentChat` (the
|
|
139
143
|
// type `runAgent`'s `provider` option takes): its `chat` drives the provider's
|
|
140
144
|
// `chatStream` and forwards each text delta to `onText`, so a same-process host
|
|
@@ -248,7 +252,9 @@ export { computeInputSignature } from "./execution/input-signature.js";
|
|
|
248
252
|
export { createSandboxClient } from "./sandbox/create-sandbox.js";
|
|
249
253
|
// The throw of the client seam. An embedder matches the class and reads the
|
|
250
254
|
// typed variant on `error`, instead of a string match on the message.
|
|
251
|
-
|
|
255
|
+
// `createSandbox` gives each failure as an `err`, and `keepSuspendingRefusal`
|
|
256
|
+
// keeps a refusal of the label hook with the suspend flag as a value and throws each other failure.
|
|
257
|
+
export { describeSandboxError, keepSuspendingRefusal, SandboxFailure } from "./sandbox/sandbox-error.js";
|
|
252
258
|
// The identity of a package, the query that asks for one, and the resolution
|
|
253
259
|
// between the two. An embedder reads `by_name` of the dependency graph with
|
|
254
260
|
// these, thus the host lookup, the census, and the graph key never disagree on
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two kinds of host hook: a gate and a notice.
|
|
3
|
+
*
|
|
4
|
+
* A gate gives a value or a permission that the harness must have before it
|
|
5
|
+
* can continue an operation. A notice reports a fact after the operation. The
|
|
6
|
+
* type of each hook shows its kind:
|
|
7
|
+
*
|
|
8
|
+
* gate: (input) => ResultAsync<T, GateFailure>
|
|
9
|
+
* notice: (input) => ResultAsync<void, NoticeFailure>
|
|
10
|
+
*
|
|
11
|
+
* The harness calls each hook through `passGate` or `deliverNotice`, thus no
|
|
12
|
+
* call site selects its own failure policy, and no call site puts a `try` or a
|
|
13
|
+
* `catch` around a hook. A hook gives a failure as an `err`. A hook whose
|
|
14
|
+
* promise rejects has a defect, and the rejection passes through the helpers
|
|
15
|
+
* with no change.
|
|
16
|
+
*
|
|
17
|
+
* The reason of a failure is the text of the host. The harness carries it and
|
|
18
|
+
* never reads it. The flag `suspend` is part of the contract, thus the harness
|
|
19
|
+
* branches on it.
|
|
20
|
+
*/
|
|
21
|
+
import { ResultAsync } from "neverthrow";
|
|
22
|
+
import type { Logger } from "./logger.js";
|
|
23
|
+
export interface GateFailure {
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
/** True when the host asks the harness to suspend the work in place of a failure. */
|
|
26
|
+
readonly suspend: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface NoticeFailure {
|
|
29
|
+
readonly reason: string;
|
|
30
|
+
}
|
|
31
|
+
export type GateName = "RunAuthorizer.authorize" | "RunCharge.open" | "ArtifactRegistry.register" | "resolveRequestHeaders" | "resolveSandboxLabels";
|
|
32
|
+
export type NoticeName = "RunAuthorizer.revoke" | "RunAuthorizer.revokeByJti" | "RunCharge.close" | "ArtifactRegistry.sync" | "UsageRecorder.record";
|
|
33
|
+
export type GateRefusal = {
|
|
34
|
+
readonly kind: "failed";
|
|
35
|
+
readonly gate: GateName;
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: "suspended";
|
|
39
|
+
readonly gate: GateName;
|
|
40
|
+
readonly reason: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The host can build its `Result` with its own copy of neverthrow. The helper
|
|
44
|
+
* gives the outcome again as a `Result` of the copy of the harness. Thus the
|
|
45
|
+
* checkpoint recipes (`runtime/result-serialization.ts`) recognize the class,
|
|
46
|
+
* and a replay reads the same outcome.
|
|
47
|
+
*/
|
|
48
|
+
export declare function passGate<T>(gate: GateName, result: ResultAsync<T, GateFailure>): ResultAsync<T, GateRefusal>;
|
|
49
|
+
export declare function deliverNotice(log: Logger, notice: NoticeName, result: ResultAsync<void, NoticeFailure>): Promise<boolean>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two kinds of host hook: a gate and a notice.
|
|
3
|
+
*
|
|
4
|
+
* A gate gives a value or a permission that the harness must have before it
|
|
5
|
+
* can continue an operation. A notice reports a fact after the operation. The
|
|
6
|
+
* type of each hook shows its kind:
|
|
7
|
+
*
|
|
8
|
+
* gate: (input) => ResultAsync<T, GateFailure>
|
|
9
|
+
* notice: (input) => ResultAsync<void, NoticeFailure>
|
|
10
|
+
*
|
|
11
|
+
* The harness calls each hook through `passGate` or `deliverNotice`, thus no
|
|
12
|
+
* call site selects its own failure policy, and no call site puts a `try` or a
|
|
13
|
+
* `catch` around a hook. A hook gives a failure as an `err`. A hook whose
|
|
14
|
+
* promise rejects has a defect, and the rejection passes through the helpers
|
|
15
|
+
* with no change.
|
|
16
|
+
*
|
|
17
|
+
* The reason of a failure is the text of the host. The harness carries it and
|
|
18
|
+
* never reads it. The flag `suspend` is part of the contract, thus the harness
|
|
19
|
+
* branches on it.
|
|
20
|
+
*/
|
|
21
|
+
import { err, ok, ResultAsync } from "neverthrow";
|
|
22
|
+
/**
|
|
23
|
+
* The host can build its `Result` with its own copy of neverthrow. The helper
|
|
24
|
+
* gives the outcome again as a `Result` of the copy of the harness. Thus the
|
|
25
|
+
* checkpoint recipes (`runtime/result-serialization.ts`) recognize the class,
|
|
26
|
+
* and a replay reads the same outcome.
|
|
27
|
+
*/
|
|
28
|
+
export function passGate(gate, result) {
|
|
29
|
+
return new ResultAsync(Promise.resolve(result).then((settled) => settled.isOk()
|
|
30
|
+
? ok(settled.value)
|
|
31
|
+
: err({ kind: settled.error.suspend ? "suspended" : "failed", gate, reason: settled.error.reason })));
|
|
32
|
+
}
|
|
33
|
+
export async function deliverNotice(log, notice, result) {
|
|
34
|
+
const delivered = await result;
|
|
35
|
+
if (delivered.isOk())
|
|
36
|
+
return true;
|
|
37
|
+
log.error("a notice of the host failed", { notice, reason: delivered.error.reason });
|
|
38
|
+
return false;
|
|
39
|
+
}
|
package/dist/lib/result.d.ts
CHANGED
|
@@ -4,20 +4,31 @@
|
|
|
4
4
|
* The agent loop and execution boundary model failure as values, not throws.
|
|
5
5
|
* The house rules:
|
|
6
6
|
*
|
|
7
|
-
* 1. `try/catch` lives ONLY
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 1. `try/catch` lives ONLY at a boundary:
|
|
8
|
+
* - the thin wrappers at calls to code external to ours — a third-party
|
|
9
|
+
* SDK, the `pg` driver, `fs`, the DBOS runtime. Those wrappers turn a
|
|
10
|
+
* throw into an `err(DomainError)`.
|
|
11
|
+
* - an API entry point, where a caller outside the harness gets the
|
|
12
|
+
* outcome.
|
|
13
|
+
* - a workflow body that gets the throw of a failed step or a failed
|
|
14
|
+
* child workflow, and runs its failure path.
|
|
15
|
+
* - the dispatch catch of the loop.
|
|
16
|
+
* Everything else flows `Result` / `ResultAsync`. A host hook has no
|
|
17
|
+
* `try/catch` around it: `passGate` / `deliverNotice` (`lib/hooks.ts`)
|
|
18
|
+
* consume its `Result`.
|
|
11
19
|
* 2. Absence is NOT an error. A "not found" stays in the ok channel as a
|
|
12
20
|
* data variant (`ok({ found: false })`), never an `err`. `err` is
|
|
13
21
|
* reserved for what used to be `throw`n — an unexpected upstream failure.
|
|
14
22
|
* 3. A `Result` is unwrapped at exactly three edges, each speaking its own
|
|
15
23
|
* dialect of failure:
|
|
16
24
|
* - HTTP routes `.match(...)` a `Result` into a response.
|
|
17
|
-
* - The DBOS step boundary throws on `err`
|
|
18
|
-
* `resultStep` in `loop/run-step.ts`):
|
|
19
|
-
* failed — and retries / fails fast —
|
|
20
|
-
*
|
|
25
|
+
* - The DBOS step boundary throws on an `err` that must fail or retry
|
|
26
|
+
* the step (`unwrapOrThrow` / `resultStep` in `loop/run-step.ts`):
|
|
27
|
+
* durability records a step as failed — and retries / fails fast —
|
|
28
|
+
* ONLY on a thrown exception. A step can also return a `Result` as
|
|
29
|
+
* its value: `runtime/result-serialization.ts` keeps the `Ok` / `Err`
|
|
30
|
+
* class across the checkpoint, DBOS records the step as a success,
|
|
31
|
+
* and the body MUST use the `err`.
|
|
21
32
|
* - Tool dispatch maps `err` into an `is_error` `tool_result`
|
|
22
33
|
* (`loop/run-agent.ts`).
|
|
23
34
|
*
|
|
@@ -43,9 +54,9 @@ export interface DomainError {
|
|
|
43
54
|
/**
|
|
44
55
|
* A thrown wrapper for a structured error value that is not already an
|
|
45
56
|
* `Error`. The original value rides on `.cause`, so the cause-walking
|
|
46
|
-
* classifiers (`classifyProviderError`, `
|
|
47
|
-
* `status` / `code` signals they look for after a `Result` is
|
|
48
|
-
* boundary.
|
|
57
|
+
* classifiers (`classifyProviderError`, `findSuspendError`) still reach the
|
|
58
|
+
* `status` / `code` / `type` signals they look for after a `Result` is
|
|
59
|
+
* rethrown at a boundary.
|
|
49
60
|
*/
|
|
50
61
|
export declare class ResultError extends Error {
|
|
51
62
|
readonly value: unknown;
|
package/dist/lib/result.js
CHANGED
|
@@ -4,20 +4,31 @@
|
|
|
4
4
|
* The agent loop and execution boundary model failure as values, not throws.
|
|
5
5
|
* The house rules:
|
|
6
6
|
*
|
|
7
|
-
* 1. `try/catch` lives ONLY
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* 1. `try/catch` lives ONLY at a boundary:
|
|
8
|
+
* - the thin wrappers at calls to code external to ours — a third-party
|
|
9
|
+
* SDK, the `pg` driver, `fs`, the DBOS runtime. Those wrappers turn a
|
|
10
|
+
* throw into an `err(DomainError)`.
|
|
11
|
+
* - an API entry point, where a caller outside the harness gets the
|
|
12
|
+
* outcome.
|
|
13
|
+
* - a workflow body that gets the throw of a failed step or a failed
|
|
14
|
+
* child workflow, and runs its failure path.
|
|
15
|
+
* - the dispatch catch of the loop.
|
|
16
|
+
* Everything else flows `Result` / `ResultAsync`. A host hook has no
|
|
17
|
+
* `try/catch` around it: `passGate` / `deliverNotice` (`lib/hooks.ts`)
|
|
18
|
+
* consume its `Result`.
|
|
11
19
|
* 2. Absence is NOT an error. A "not found" stays in the ok channel as a
|
|
12
20
|
* data variant (`ok({ found: false })`), never an `err`. `err` is
|
|
13
21
|
* reserved for what used to be `throw`n — an unexpected upstream failure.
|
|
14
22
|
* 3. A `Result` is unwrapped at exactly three edges, each speaking its own
|
|
15
23
|
* dialect of failure:
|
|
16
24
|
* - HTTP routes `.match(...)` a `Result` into a response.
|
|
17
|
-
* - The DBOS step boundary throws on `err`
|
|
18
|
-
* `resultStep` in `loop/run-step.ts`):
|
|
19
|
-
* failed — and retries / fails fast —
|
|
20
|
-
*
|
|
25
|
+
* - The DBOS step boundary throws on an `err` that must fail or retry
|
|
26
|
+
* the step (`unwrapOrThrow` / `resultStep` in `loop/run-step.ts`):
|
|
27
|
+
* durability records a step as failed — and retries / fails fast —
|
|
28
|
+
* ONLY on a thrown exception. A step can also return a `Result` as
|
|
29
|
+
* its value: `runtime/result-serialization.ts` keeps the `Ok` / `Err`
|
|
30
|
+
* class across the checkpoint, DBOS records the step as a success,
|
|
31
|
+
* and the body MUST use the `err`.
|
|
21
32
|
* - Tool dispatch maps `err` into an `is_error` `tool_result`
|
|
22
33
|
* (`loop/run-agent.ts`).
|
|
23
34
|
*
|
|
@@ -32,9 +43,9 @@ import { Err, Ok } from "neverthrow";
|
|
|
32
43
|
/**
|
|
33
44
|
* A thrown wrapper for a structured error value that is not already an
|
|
34
45
|
* `Error`. The original value rides on `.cause`, so the cause-walking
|
|
35
|
-
* classifiers (`classifyProviderError`, `
|
|
36
|
-
* `status` / `code` signals they look for after a `Result` is
|
|
37
|
-
* boundary.
|
|
46
|
+
* classifiers (`classifyProviderError`, `findSuspendError`) still reach the
|
|
47
|
+
* `status` / `code` / `type` signals they look for after a `Result` is
|
|
48
|
+
* rethrown at a boundary.
|
|
38
49
|
*/
|
|
39
50
|
export class ResultError extends Error {
|
|
40
51
|
value;
|
|
@@ -49,9 +60,8 @@ function describe(value) {
|
|
|
49
60
|
return value.message;
|
|
50
61
|
if (value && typeof value === "object" && "type" in value) {
|
|
51
62
|
// Prefer a human `message` field when the structured error carries one
|
|
52
|
-
// (e.g. a `ProviderError`) so the wrapped `Error.message`
|
|
53
|
-
//
|
|
54
|
-
// to the `type` discriminant otherwise.
|
|
63
|
+
// (e.g. a `ProviderError`) so the wrapped `Error.message` names the
|
|
64
|
+
// failure; fall back to the `type` discriminant otherwise.
|
|
55
65
|
const message = value.message;
|
|
56
66
|
if (typeof message === "string" && message.length > 0)
|
|
57
67
|
return message;
|
package/dist/loop/run-agent.d.ts
CHANGED
|
@@ -100,8 +100,8 @@ export interface RunAgentOptions {
|
|
|
100
100
|
readonly logger?: Logger;
|
|
101
101
|
/**
|
|
102
102
|
* Per-call LLM usage-accounting seam. Omitted falls back to the no-op
|
|
103
|
-
* recorder.
|
|
104
|
-
*
|
|
103
|
+
* recorder. `record` is a notice: the loop does not wait for its result,
|
|
104
|
+
* and it logs the reason of an `err` when the result arrives.
|
|
105
105
|
*/
|
|
106
106
|
readonly usageRecorder?: UsageRecorder;
|
|
107
107
|
/**
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsonSchema, tool as aiTool, } from "ai";
|
|
|
3
3
|
import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
|
|
4
4
|
import { stripNulCharacters } from "../input-sanitization.js";
|
|
5
5
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
6
|
+
import { deliverNotice } from "../lib/hooks.js";
|
|
6
7
|
import { ATTR_INFLEXA_TOOL_USE_ID, passThroughSpan, stableSpan } from "../lib/otel-spans.js";
|
|
7
8
|
import { ResultError } from "../lib/result.js";
|
|
8
9
|
import { describeZodIssueShapes } from "../lib/zod-issue-shape.js";
|
|
@@ -150,8 +151,8 @@ async function runAgentLoop(agent, initial, session, opts) {
|
|
|
150
151
|
* reported nothing to account for, so it is folded (to no effect) and left
|
|
151
152
|
* out of the ledger rather than entered as an all-absent record.
|
|
152
153
|
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
154
|
+
* `record` is a notice. The loop does not wait for its result, thus a
|
|
155
|
+
* recorder that blocks does not make the run slower.
|
|
155
156
|
*/
|
|
156
157
|
const accountForCall = (reply, stepName) => {
|
|
157
158
|
addChatUsage(usage, reply.usage);
|
|
@@ -159,7 +160,7 @@ async function runAgentLoop(agent, initial, session, opts) {
|
|
|
159
160
|
const reported = reply.usage;
|
|
160
161
|
if (reported === undefined || !hasReportedUsage(reported))
|
|
161
162
|
return;
|
|
162
|
-
usageRecorder.record({
|
|
163
|
+
void deliverNotice(log, "UsageRecorder.record", usageRecorder.record({
|
|
163
164
|
recordKey: recordKeyFor(session, opts.invocationId, stepName),
|
|
164
165
|
agentId: source.agentId,
|
|
165
166
|
callPath: source.callPath,
|
|
@@ -169,7 +170,7 @@ async function runAgentLoop(agent, initial, session, opts) {
|
|
|
169
170
|
...(reply.requestedModelId === undefined ? {} : { requestedModelId: reply.requestedModelId }),
|
|
170
171
|
...(reply.servedModelId === undefined ? {} : { servedModelId: reply.servedModelId }),
|
|
171
172
|
usage: reported,
|
|
172
|
-
});
|
|
173
|
+
}));
|
|
173
174
|
};
|
|
174
175
|
/** The rollups this loop stamps on its finish — each absent when nothing reported. */
|
|
175
176
|
const finishUsage = () => ({
|
|
@@ -23,7 +23,20 @@ export function storedMessagesToCortex(messages) {
|
|
|
23
23
|
let append = null;
|
|
24
24
|
for (const row of messages) {
|
|
25
25
|
if (row.displayEnvelope) {
|
|
26
|
-
|
|
26
|
+
// The author and the creation time are facts about the ROW, the same as
|
|
27
|
+
// the rollup below, and the projection holds neither. Both ride the row
|
|
28
|
+
// that opens the append, and the author reaches the `user` messages
|
|
29
|
+
// alone, because `role` already names the sender of a reply. Spread
|
|
30
|
+
// conditionally: an absent value must leave no key, or a consumer that
|
|
31
|
+
// spreads the message acquires one that overwrites a real value. `Date`
|
|
32
|
+
// does not survive the JSON crossing typed, hence the ISO string here.
|
|
33
|
+
const createdAt = row.createdAt === undefined ? {} : { createdAt: row.createdAt.toISOString() };
|
|
34
|
+
const author = row.author === undefined ? {} : { author: row.author };
|
|
35
|
+
append = conversationUIToCortexMessages(row.displayEnvelope.messages).map((message) => ({
|
|
36
|
+
...message,
|
|
37
|
+
...createdAt,
|
|
38
|
+
...(message.role === "user" ? author : {}),
|
|
39
|
+
}));
|
|
27
40
|
out.push(...append);
|
|
28
41
|
}
|
|
29
42
|
// The reported rollup of the turn and its duration both ride the model row that
|
|
@@ -55,6 +55,20 @@ export interface StoredMessage {
|
|
|
55
55
|
* reported no quantity still reads back with the duration that it took.
|
|
56
56
|
*/
|
|
57
57
|
readonly durationMs?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Who sent the user message of the append this row OPENED — present only on a
|
|
60
|
+
* genuine-user-start row of a turn appended with one. A row written before the
|
|
61
|
+
* column existed carries none, because no sender was ever recorded for it.
|
|
62
|
+
*/
|
|
63
|
+
readonly author?: string;
|
|
64
|
+
/**
|
|
65
|
+
* When the store wrote this row: the START time of the append transaction,
|
|
66
|
+
* thus every row of one append holds the same value, and `seq` alone orders
|
|
67
|
+
* rows. The read sets it on every row, because the column is `NOT NULL`. It is
|
|
68
|
+
* optional because a fake or a fixture builds a `StoredMessage` with no row
|
|
69
|
+
* behind it.
|
|
70
|
+
*/
|
|
71
|
+
readonly createdAt?: Date;
|
|
58
72
|
}
|
|
59
73
|
/**
|
|
60
74
|
* One atomic append: the exact provider-facing history, the display projection of
|
|
@@ -86,6 +100,16 @@ export interface ConversationTurn {
|
|
|
86
100
|
* reported no quantity still keeps the duration that it took.
|
|
87
101
|
*/
|
|
88
102
|
readonly turnDurationMs?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Who sent the user message of this turn — the identity the host holds at the
|
|
105
|
+
* append site. Stored on the FIRST row of the append, and only when that row
|
|
106
|
+
* is a genuine user start: that row also carries the display projection, thus
|
|
107
|
+
* the write and the transcript replay read one row under one rule. A record
|
|
108
|
+
* append opens on a synthetic row, so it stores no author however a caller
|
|
109
|
+
* spreads one in. The value is NUL-stripped on the way in, the same as the
|
|
110
|
+
* envelope, because a `text` parameter carrying 0x00 fails the statement.
|
|
111
|
+
*/
|
|
112
|
+
readonly author?: string;
|
|
89
113
|
}
|
|
90
114
|
/**
|
|
91
115
|
* The append a host uses to record out-of-band work in a thread — an analysis
|
|
@@ -181,7 +181,7 @@ export const EVICTION_BLOCK_TURNS = 4;
|
|
|
181
181
|
*/
|
|
182
182
|
export function createThreadHistory(pool, logger) {
|
|
183
183
|
function appendTurn(threadId, turn) {
|
|
184
|
-
const { modelMessages: messages, displayMessages, turnUsage, turnDurationMs } = turn;
|
|
184
|
+
const { modelMessages: messages, displayMessages, turnUsage, turnDurationMs, author } = turn;
|
|
185
185
|
if (messages.length === 0)
|
|
186
186
|
return okVoid();
|
|
187
187
|
// The display projection rides the append's FIRST row, so one SELECT of a
|
|
@@ -206,6 +206,13 @@ export function createThreadHistory(pool, logger) {
|
|
|
206
206
|
// the turn. -1 (a turn that persisted no reply — an abort before any output)
|
|
207
207
|
// matches no index below, so nothing is written and the append still succeeds.
|
|
208
208
|
const lastAssistantRow = messages.reduce((last, m, i) => (m.role === "assistant" ? i : last), -1);
|
|
209
|
+
// The author rides the row the display projection rides, and only when that
|
|
210
|
+
// row is a message a person actually sent — an assistant row, a tool row,
|
|
211
|
+
// and the synthetic opening row of a record append are not. NUL is dropped
|
|
212
|
+
// rather than refused: a 0x00 byte in a `text` parameter fails the
|
|
213
|
+
// statement, and one byte in a label would roll the whole turn back.
|
|
214
|
+
const turnAuthor = author === undefined ? null : stripNulCharacters(author);
|
|
215
|
+
const authorRow = messages[0] !== undefined && isGenuineUserStart(messages[0]) ? 0 : -1;
|
|
209
216
|
return withTransaction(pool, "thread-history.appendTurn", (client) =>
|
|
210
217
|
// Serialize concurrent appends on this thread — without the lock, two
|
|
211
218
|
// transactions can both read the same MAX(seq) and collide on the
|
|
@@ -224,14 +231,15 @@ export function createThreadHistory(pool, logger) {
|
|
|
224
231
|
// `message_envelope::json`, never `::jsonb`; `display_envelope` and
|
|
225
232
|
// `reported_usage::jsonb`, never `::json` — see the column comments
|
|
226
233
|
// in the state-init DDL.
|
|
227
|
-
`INSERT INTO messages (thread_id, seq, message_envelope, display_envelope, tokens, reported_usage, turn_duration_ms)
|
|
228
|
-
VALUES ($1, $2, $3::json, $4::jsonb, $5, $6::jsonb, $7)
|
|
234
|
+
`INSERT INTO messages (thread_id, seq, message_envelope, display_envelope, tokens, reported_usage, turn_duration_ms, author)
|
|
235
|
+
VALUES ($1, $2, $3::json, $4::jsonb, $5, $6::jsonb, $7, $8)
|
|
229
236
|
ON CONFLICT (thread_id, seq) DO UPDATE
|
|
230
237
|
SET message_envelope = EXCLUDED.message_envelope,
|
|
231
238
|
display_envelope = EXCLUDED.display_envelope,
|
|
232
239
|
tokens = EXCLUDED.tokens,
|
|
233
240
|
reported_usage = EXCLUDED.reported_usage,
|
|
234
|
-
turn_duration_ms = EXCLUDED.turn_duration_ms
|
|
241
|
+
turn_duration_ms = EXCLUDED.turn_duration_ms,
|
|
242
|
+
author = EXCLUDED.author`, [
|
|
235
243
|
threadId,
|
|
236
244
|
startSeq + i,
|
|
237
245
|
serializeEnvelope(message),
|
|
@@ -239,6 +247,7 @@ export function createThreadHistory(pool, logger) {
|
|
|
239
247
|
countTokens(message.content),
|
|
240
248
|
i === lastAssistantRow ? rollup : null,
|
|
241
249
|
i === lastAssistantRow ? duration : null,
|
|
250
|
+
i === authorRow ? turnAuthor : null,
|
|
242
251
|
])).map(() => undefined)), okVoid()))
|
|
243
252
|
.andThen(() =>
|
|
244
253
|
// Thread listings sort on `cortex_analysis_threads.updated_at`, and
|
|
@@ -368,12 +377,14 @@ export function createThreadHistory(pool, logger) {
|
|
|
368
377
|
// `seq::text AS seq` output alias (Postgres resolves an unqualified
|
|
369
378
|
// ORDER BY name to the output column), sorting the bigint as text:
|
|
370
379
|
// "10" before "2". The qualified name forces the bigint column.
|
|
371
|
-
`SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage, turn_duration_ms::text AS turn_duration_ms
|
|
380
|
+
`SELECT seq::text AS seq, message_envelope, display_envelope, reported_usage, turn_duration_ms::text AS turn_duration_ms, author, created_at
|
|
372
381
|
FROM messages WHERE thread_id = $1
|
|
373
382
|
ORDER BY messages.seq ASC`, [threadId]);
|
|
374
383
|
// Spread rather than `usage: r.reported_usage ?? undefined`, so a row with
|
|
375
384
|
// no rollup carries no `usage` KEY at all — absent, not present-and-undefined.
|
|
376
|
-
// The duration
|
|
385
|
+
// The duration and the author obey the same rule, each from its own column.
|
|
386
|
+
// The creation time is unconditional: its column is NOT NULL, thus it has no
|
|
387
|
+
// absence to represent.
|
|
377
388
|
const stored = await Promise.all(rows.map(async (r) => {
|
|
378
389
|
const envelope = parseStoredMessageEnvelope(r.message_envelope, `${threadId}/${r.seq}`);
|
|
379
390
|
const displayEnvelope = r.display_envelope == null ? undefined : await parseStoredDisplayEnvelope(r.display_envelope, `${threadId}/${r.seq}/display`, logger);
|
|
@@ -384,6 +395,8 @@ export function createThreadHistory(pool, logger) {
|
|
|
384
395
|
...(displayEnvelope ? { displayEnvelope } : {}),
|
|
385
396
|
...(r.reported_usage === null ? {} : { usage: r.reported_usage }),
|
|
386
397
|
...(r.turn_duration_ms === null ? {} : { durationMs: Number(r.turn_duration_ms) }),
|
|
398
|
+
...(r.author === null ? {} : { author: r.author }),
|
|
399
|
+
createdAt: r.created_at,
|
|
387
400
|
};
|
|
388
401
|
}));
|
|
389
402
|
return groupTurns(stored, (row) => isGenuineUserStart(row.message));
|
|
@@ -69,7 +69,7 @@ export type RunProvenanceEvent = {
|
|
|
69
69
|
runId: string;
|
|
70
70
|
/**
|
|
71
71
|
* The body's terminal status. Both boundary sites resolve it through `deriveFinalStatus`,
|
|
72
|
-
* which records a
|
|
72
|
+
* which records a suspension as `"canceled"` — so `"suspended_insufficient_funds"` (a
|
|
73
73
|
* `RunStatus` member) is never emitted here and is deliberately absent from this narrower
|
|
74
74
|
* `ExecuteAnalysisFinalStatus`-minus-`"running"` set.
|
|
75
75
|
*/
|