eve 0.81.0 → 0.81.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/src/compiled/.vendor-stamp.json +2 -2
- package/dist/src/compiled/@eve/catalog/index.js +1 -1
- package/dist/src/eve-channel/create-request.d.ts +2 -1
- package/dist/src/eve-channel/index.js +1 -1
- package/dist/src/eve-channel/request.d.ts +2 -1
- package/dist/src/eve-channel/request.js +1 -2
- package/dist/src/eve-channel/support.d.ts +9 -1
- package/dist/src/eve-channel/support.js +2 -1
- package/dist/src/eve-channel/types.d.ts +18 -0
- package/dist/src/execution/dynamic-workflow/tool.js +1 -1
- package/dist/src/execution/hook-ownership.d.ts +8 -0
- package/dist/src/execution/hook-ownership.js +1 -1
- package/dist/src/execution/session/entry-input.d.ts +19 -5
- package/dist/src/execution/session/entry-input.js +1 -1
- package/dist/src/execution/session/entry.js +1 -1
- package/dist/src/execution/session/handoff-steps.d.ts +75 -13
- package/dist/src/execution/session/handoff-steps.js +1 -1
- package/dist/src/execution/session/handoff.d.ts +69 -27
- package/dist/src/execution/session/handoff.js +1 -1
- package/dist/src/execution/session/program.js +1 -1
- package/dist/src/execution/session/run-watch.d.ts +22 -0
- package/dist/src/execution/session/run-watch.js +1 -0
- package/dist/src/execution/session-inbox/address.d.ts +4 -1
- package/dist/src/execution/session-inbox/end-stranded-session.js +1 -1
- package/dist/src/execution/session-inbox/hook-release.d.ts +9 -2
- package/dist/src/execution/session-inbox/hook-release.js +1 -1
- package/dist/src/execution/session-inbox/inbox.d.ts +66 -6
- package/dist/src/execution/session-inbox/inbox.js +1 -1
- package/dist/src/execution/session-inbox/resume.d.ts +4 -3
- package/dist/src/execution/tools/agent-router-workflow.js +1 -1
- package/dist/src/execution/workflow-runtime.d.ts +6 -11
- package/dist/src/execution/workflow-runtime.js +1 -1
- package/dist/src/harness/model-call/call.js +1 -1
- package/dist/src/instrumentation/content.d.ts +1 -1
- package/dist/src/instrumentation/content.js +1 -1
- package/dist/src/instrumentation/lifecycle.d.ts +4 -2
- package/dist/src/internal/application/stamped-package-version.js +1 -1
- package/dist/src/internal/authored-definition/connection.js +1 -1
- package/dist/src/internal/mcp/skills.js +1 -1
- package/dist/src/internal/workflow/development-world-client.js +1 -1
- package/dist/src/public/channels/chat-sdk/messageToUserContent.d.ts +2 -1
- package/dist/src/public/channels/chat-sdk/messageToUserContent.js +1 -1
- package/dist/src/public/channels/photon/attachments.d.ts +5 -2
- package/dist/src/public/channels/photon/attachments.js +1 -1
- package/dist/src/public/channels/photon/photonIMessageChannel.js +1 -1
- package/dist/src/public/channels/slack/api.d.ts +8 -1
- package/dist/src/public/channels/slack/api.js +1 -1
- package/dist/src/public/channels/slack/slackChannel.js +1 -1
- package/dist/src/setup/scaffold/create/project.js +1 -1
- package/dist/src/shared/gateway-routing.d.ts +20 -0
- package/dist/src/shared/gateway-routing.js +1 -0
- package/dist/src/shared/skill-package.js +1 -1
- package/dist/src/tools/provided/ask-question-workflow.js +1 -1
- package/dist/src/tools/provided/sleep-workflow.js +1 -1
- package/dist/src/tracing/eve/agent-otel-provider.js +1 -1
- package/dist/src/tracing/eve/gateway.d.ts +5 -0
- package/dist/src/tracing/eve/gateway.js +1 -1
- package/dist/src/tracing/eve/operation-input.d.ts +0 -7
- package/dist/src/tracing/eve/operation-input.js +1 -1
- package/dist/src/tracing/eve/policy.d.ts +1 -1
- package/dist/src/tracing/eve/policy.js +1 -1
- package/dist/src/tracing/eve/session.d.ts +2 -0
- package/dist/src/tracing/eve/session.js +1 -1
- package/docs/README.md +1 -0
- package/docs/agent-config.md +22 -0
- package/docs/channels/chat-sdk.mdx +4 -2
- package/docs/channels/eve.mdx +27 -0
- package/docs/channels/photon.mdx +4 -2
- package/docs/channels/slack.mdx +2 -0
- package/docs/concepts/execution-model-and-durability.mdx +16 -7
- package/docs/guides/instrumentation/otel.mdx +20 -0
- package/docs/patterns/meta.json +1 -0
- package/docs/patterns/voice-notes.md +67 -0
- package/docs/sandbox/index.mdx +2 -0
- package/package.json +1 -1
|
@@ -1,30 +1,68 @@
|
|
|
1
|
+
import type { SessionHandoverPayload } from "#execution/session-inbox/inbox.js";
|
|
1
2
|
import type { SessionCheckpointMigration } from "#execution/session/checkpoint-migrations.js";
|
|
2
3
|
import { type DurableSessionState } from "#execution/durable-session-store.js";
|
|
3
|
-
import { type SessionOwnerActivation } from "#execution/session/handoff.js";
|
|
4
|
+
import { type SessionCheckpoint, type SessionOwnerActivation } from "#execution/session/handoff.js";
|
|
5
|
+
import { type SessionHandoffStart } from "#execution/session/entry-input.js";
|
|
4
6
|
/** Parses retained work with this deployment's code before deciding whether it can move. */
|
|
5
7
|
export declare function isSessionStateIdleForHandoff(input: {
|
|
6
8
|
readonly sessionState: DurableSessionState;
|
|
7
9
|
}): boolean;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
export type SuccessorStartInput = SessionHandoffStart & {
|
|
11
|
+
readonly activationToken: string;
|
|
12
|
+
/** Original run whose stream stays the public session stream. */
|
|
13
|
+
readonly anchorRunId: string;
|
|
14
|
+
readonly attemptKey: string;
|
|
15
|
+
readonly checkpoint: SessionCheckpoint;
|
|
16
|
+
readonly hookTokens: readonly string[];
|
|
17
|
+
/** The owner starting the successor, which hands over what it accepts meanwhile. */
|
|
18
|
+
readonly sourceRunId: string;
|
|
19
|
+
readonly targetDeploymentId: string;
|
|
20
|
+
};
|
|
21
|
+
export type SuccessorStart = {
|
|
22
|
+
readonly kind: "not-idle";
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: "unsupported";
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: "started";
|
|
27
|
+
readonly runId: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Starts a successor owner bound to the original session stream, unless this
|
|
31
|
+
* deployment's code finds work in the checkpoint that cannot move yet, or the
|
|
32
|
+
* World cannot move session addresses between runs without releasing them.
|
|
33
|
+
*/
|
|
34
|
+
export declare function startSuccessorStep(input: SuccessorStartInput): Promise<SuccessorStart>;
|
|
35
|
+
/** Whether `runId` has ended; a run not readable yet has not. */
|
|
36
|
+
export declare function hasRunEndedStep(input: {
|
|
37
|
+
readonly runId: string;
|
|
11
38
|
}): Promise<boolean>;
|
|
12
39
|
export type SessionCheckpointValidation = {
|
|
13
40
|
readonly kind: "valid";
|
|
14
41
|
} | {
|
|
15
42
|
readonly kind: "incompatible";
|
|
16
43
|
readonly reason: "checkpoint-version";
|
|
44
|
+
}
|
|
45
|
+
/** The source no longer holds every address it handed over, so none may be taken. */
|
|
46
|
+
| {
|
|
47
|
+
readonly kind: "source-gone";
|
|
17
48
|
};
|
|
18
49
|
/**
|
|
19
|
-
* Validates an upgraded checkpoint
|
|
20
|
-
*
|
|
50
|
+
* Validates an upgraded checkpoint, resolves the target deployment's compiled
|
|
51
|
+
* bundle, and records this owner's identity before it claims any hook.
|
|
21
52
|
*
|
|
22
53
|
* A checkpoint this deployment cannot read is a settled answer, not a fault,
|
|
23
54
|
* so it returns rather than throws: retrying the step can never change it.
|
|
24
55
|
*/
|
|
25
|
-
export declare function
|
|
56
|
+
export declare function prepareSuccessorStep(input: {
|
|
57
|
+
/** Absent for version 1 sources, which predate the field. */
|
|
58
|
+
readonly handoffVersion: number | undefined;
|
|
26
59
|
readonly migration: SessionCheckpointMigration;
|
|
27
60
|
readonly sessionId: string;
|
|
61
|
+
/** Version 3: the run that still holds the session and the addresses it hands over. */
|
|
62
|
+
readonly source?: {
|
|
63
|
+
readonly runId: string;
|
|
64
|
+
readonly hookTokens: readonly string[];
|
|
65
|
+
};
|
|
28
66
|
}): Promise<SessionCheckpointValidation>;
|
|
29
67
|
/**
|
|
30
68
|
* Stops idle subagent sessions an older owner kept resumable, after this owner
|
|
@@ -43,12 +81,12 @@ export declare function reportSessionHandoffRetainedStep(input: {
|
|
|
43
81
|
readonly targetDeploymentId: string;
|
|
44
82
|
}): Promise<void>;
|
|
45
83
|
/**
|
|
46
|
-
* Records the identity of
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
84
|
+
* Records the identity of a legacy importer before it claims any hook;
|
|
85
|
+
* handoff successors do the same inside `prepareSuccessorStep`. Ingress reads
|
|
86
|
+
* `$eve.version` to decide whether the owner can run, so a missing value would
|
|
87
|
+
* strand a healthy session. The value comes from this step's code, not the
|
|
88
|
+
* previous owner's. Unlike observability attributes, a failed write fails the
|
|
89
|
+
* boot.
|
|
52
90
|
*/
|
|
53
91
|
export declare function recordSessionOwnerStep(input: {
|
|
54
92
|
readonly sessionId: string;
|
|
@@ -57,6 +95,30 @@ export declare function signalSessionOwnerActivationStep(input: {
|
|
|
57
95
|
readonly activation: SessionOwnerActivation;
|
|
58
96
|
readonly token: string;
|
|
59
97
|
}): Promise<void>;
|
|
98
|
+
/** Makes a takeover with no attempt open visible: every forced claim belongs to one. */
|
|
99
|
+
export declare function reportSessionTakenOverOutsideAttemptStep(input: {
|
|
100
|
+
readonly sessionId: string;
|
|
101
|
+
}): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Makes a move whose handover part keeps failing visible; the owner keeps
|
|
104
|
+
* retrying, because dropping the part would lose an accepted message.
|
|
105
|
+
*/
|
|
106
|
+
export declare function reportHandoverPartStuckStep(input: {
|
|
107
|
+
readonly attemptKey: string;
|
|
108
|
+
readonly error: string | undefined;
|
|
109
|
+
readonly failures: number;
|
|
110
|
+
readonly index: number;
|
|
111
|
+
readonly sessionId: string;
|
|
112
|
+
}): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Sends one handover part through the stable session address, which reaches
|
|
115
|
+
* whichever run owns the session. A part that keeps failing fails the step,
|
|
116
|
+
* for the caller to retry until it is delivered.
|
|
117
|
+
*/
|
|
118
|
+
export declare function sendSessionHandoverPartStep(input: {
|
|
119
|
+
readonly part: Omit<SessionHandoverPayload, "kind">;
|
|
120
|
+
readonly sessionId: string;
|
|
121
|
+
}): Promise<"delivered" | "session-ended">;
|
|
60
122
|
export declare function signalSessionAnchorStep(input: {
|
|
61
123
|
readonly result: {
|
|
62
124
|
readonly output: unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{openInputs,openSignIns}from"#protocol/session-projection.js";import{createLogger,logError}from"#internal/logging.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{walkCauseChain}from"#shared/errors.js";import{getSandboxEnvironmentRuntime}from"#shared/sandbox-environment.js";import{BundleKey}from"#runtime/sessions/runtime-context-keys.js";import{queuedInput,storedProjection}from"#harness/session-machine/view.js";import{cancelRun,getWorld,resumeHook}from"#internal/workflow/runtime.js";import{getResolvedRuntimeAgentNode}from"#runtime/graph.js";import{readDurableSession}from"#execution/durable-session-store.js";import{getBlockingWorkflowToolRuns}from"#harness/workflow-tool-runs.js";import{IncompatibleStateLayoutError,deserializeContext}from"#context/serialize.js";import{holdsHitlRequests}from"#harness/hitl/index.js";import{EntityConflictError,RunExpiredError,WorkflowRunNotFoundError}from"#compiled/@workflow/errors/index.js";import{EVE_SESSION_ATTRIBUTE,EVE_VERSION_ATTRIBUTE}from"#execution/eve-workflow-attributes.js";import{SESSION_CHECKPOINT_VERSION}from"#execution/session/handoff.js";const log=createLogger(`execution.handoff`);function isSessionStateIdleForHandoff(e){let{state:t}=readDurableSession(e.sessionState),n=getBlockingWorkflowToolRuns(t);if(holdsHitlRequests(t)||[`eve.runtime.pendingInputBatch`,`eve.runtime.pendingCoordinationBatch`,`eve.runtime.deferredStepInput`,`eve.harness.pendingWorkflowInterrupt`].some(e=>t?.[e]!==void 0))return!1;let r=t?.[`eve.runtime.pendingInputBatches`];if(r!==void 0&&(!Array.isArray(r)||r.length>0))return!1;let i=storedProjection(t);return n.length===0&&i.activeTurnId===void 0&&openInputs(i).length===0&&openSignIns(i).length===0&&queuedInput(t)===void 0}async function
|
|
1
|
+
import{openInputs,openSignIns}from"#protocol/session-projection.js";import{createLogger,logError}from"#internal/logging.js";import{sessionCommandHookToken,sessionInboxHookToken}from"#execution/session-inbox/address.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{walkCauseChain}from"#shared/errors.js";import{getSandboxEnvironmentRuntime}from"#shared/sandbox-environment.js";import{BundleKey}from"#runtime/sessions/runtime-context-keys.js";import{queuedInput,storedProjection}from"#harness/session-machine/view.js";import{cancelRun,getRun,getWorld,resumeHook}from"#internal/workflow/runtime.js";import{hasRunEnded,startWorkflowOnDeployment,workflowEntryReference}from"#execution/workflow-runtime.js";import{getResolvedRuntimeAgentNode}from"#runtime/graph.js";import{readDurableSession}from"#execution/durable-session-store.js";import{getBlockingWorkflowToolRuns}from"#harness/workflow-tool-runs.js";import{IncompatibleStateLayoutError,deserializeContext}from"#context/serialize.js";import{holdsHitlRequests}from"#harness/hitl/index.js";import{EntityConflictError,HookNotFoundError,RunExpiredError,WorkflowRunNotFoundError}from"#compiled/@workflow/errors/index.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";import{EVE_SESSION_ATTRIBUTE,EVE_VERSION_ATTRIBUTE}from"#execution/eve-workflow-attributes.js";import{SESSION_CHECKPOINT_VERSION}from"#execution/session/handoff.js";import{mapWithConcurrency}from"#shared/map-with-concurrency.js";import{SESSION_HANDOFF_VERSION}from"#execution/session/entry-input.js";const log=createLogger(`execution.handoff`);function isSessionStateIdleForHandoff(e){let{state:t}=readDurableSession(e.sessionState),n=getBlockingWorkflowToolRuns(t);if(holdsHitlRequests(t)||[`eve.runtime.pendingInputBatch`,`eve.runtime.pendingCoordinationBatch`,`eve.runtime.deferredStepInput`,`eve.harness.pendingWorkflowInterrupt`].some(e=>t?.[e]!==void 0))return!1;let r=t?.[`eve.runtime.pendingInputBatches`];if(r!==void 0&&(!Array.isArray(r)||r.length>0))return!1;let i=storedProjection(t);return n.length===0&&i.activeTurnId===void 0&&openInputs(i).length===0&&openSignIns(i).length===0&&queuedInput(t)===void 0}async function startSuccessorStep(e){"use step";let{activationToken:t,anchorRunId:n,checkpoint:r,targetDeploymentId:i,...a}=e,{capabilities:o}=await getWorld();if(o?.hookForceClaim!==!0||o.hookRetention?.active!==!0)return log.warn(`session handoff unavailable: the Workflow world cannot force-claim hooks`,{sessionId:n}),{kind:`unsupported`};if(!isSessionStateIdleForHandoff(r))return{kind:`not-idle`};let s={...a,activationToken:t,checkpoint:r,handoffVersion:SESSION_HANDOFF_VERSION,kind:`handoff`,ownerDeploymentId:i,sessionWritable:getRun(n).getWritable(),sessionId:n};return{kind:`started`,runId:(await startWorkflowOnDeployment(workflowEntryReference,[s],i,r.retention===void 0?void 0:{experimental_retention:r.retention})).runId}}async function hasRunEndedStep(e){"use step";return await hasRunEnded(e.runId)}async function prepareSuccessorStep(e){"use step";let{migration:t}=e,unreadable=t=>(log.warn(`session handoff refused: this deployment cannot read the checkpoint`,{detail:t,readableCheckpointVersion:SESSION_CHECKPOINT_VERSION,sessionId:e.sessionId}),{kind:`incompatible`,reason:`checkpoint-version`});if(t.kind===`incompatible`)return unreadable(t.detail);let{checkpoint:n}=t,r=n.sessionTimeoutMs;if(r!==!1&&(typeof r!=`number`||!Number.isFinite(r)||r<0))throw Error(`Session checkpoint contains an invalid timeout duration.`);let i;try{i=await deserializeContext(n.serializedContext)}catch(e){if(e instanceof IncompatibleStateLayoutError)return unreadable(e.message);throw e}let a=i.require(BundleKey),o=readDurableSession(n.sessionState).sandboxState?.session;if(o!=null&&o.providerName!==void 0){let e=getResolvedRuntimeAgentNode(a.graph,a.nodeId).sandboxRegistry.sandbox.inheritance?.definition??getResolvedRuntimeAgentNode(a.graph,a.nodeId).sandboxRegistry.sandbox.definition;if(e.kind!==`independent`)throw Error(`Session checkpoint sandbox has no resolved provider.`);let t=getSandboxEnvironmentRuntime(e.environment);if(o.providerName!==t.providerName||o.stateProtocolVersion!==t.stateProtocolVersion)throw Error(`Session checkpoint sandbox provider state is incompatible.`)}if(!isSessionStateIdleForHandoff(n))throw Error(`Session checkpoint contains pending work and cannot be handed off.`);return e.source!==void 0&&!await holdsEveryAddress(e.source)?{kind:`source-gone`}:(await recordSessionOwner(e.sessionId),(e.handoffVersion??1)<SESSION_HANDOFF_VERSION&&log.info(`session moved from an owner that releases its addresses first`,{handoffVersion:e.handoffVersion??1,sessionId:e.sessionId}),{kind:`valid`})}async function holdsEveryAddress(e){return(await mapWithConcurrency(e.hookTokens,16,e=>findSessionHookHolder(sessionInboxHookToken(e)))).every(t=>t?.runId===e.runId)}async function stopUntrackedChildSessionsStep(e){"use step";let t=await getWorld();for(let n of e.runIds)if(n!==e.sessionId)try{await cancelRun(t,n,{cancelReason:`Session upgraded`})}catch(t){[...walkCauseChain(t)].some(e=>EntityConflictError.is(e)||RunExpiredError.is(e)||WorkflowRunNotFoundError.is(e))||logError(log,`could not stop a subagent session after handoff`,t,{childRunId:n,sessionId:e.sessionId})}}async function reportSessionHandoffRetainedStep(e){"use step";log.warn(`session handoff failed; the current owner keeps the session`,{...e,checkpointVersion:SESSION_CHECKPOINT_VERSION})}async function recordSessionOwnerStep(e){"use step";await recordSessionOwner(e.sessionId)}async function recordSessionOwner(e){let{setAttributes:t}=await import(`#compiled/@workflow/core/index.js`);await t({[EVE_SESSION_ATTRIBUTE]:e,[EVE_VERSION_ATTRIBUTE]:resolveInstalledPackageInfo().version},{allowReservedAttributes:!0})}async function signalSessionOwnerActivationStep(e){"use step";try{await resumeHook(e.token,e.activation)}catch(e){if(!HookNotFoundError.is(e))throw e}}async function reportSessionTakenOverOutsideAttemptStep(e){"use step";log.error(`session taken over with no handoff attempt open; this run leaves without finalizing`,{sessionId:e.sessionId})}async function reportHandoverPartStuckStep(e){"use step";log.warn(`session handover part keeps failing; the previous owner keeps retrying`,e)}async function sendSessionHandoverPartStep(e){"use step";try{return await resumeHook(sessionInboxHookToken(sessionCommandHookToken(e.sessionId)),{...e.part,kind:`handover`}),`delivered`}catch(e){if(HookNotFoundError.is(e))return`session-ended`;throw e}}async function signalSessionAnchorStep(e){"use step";await resumeHook(e.token,e.result)}export{hasRunEndedStep,isSessionStateIdleForHandoff,prepareSuccessorStep,recordSessionOwnerStep,reportHandoverPartStuckStep,reportSessionHandoffRetainedStep,reportSessionTakenOverOutsideAttemptStep,sendSessionHandoverPartStep,signalSessionAnchorStep,signalSessionOwnerActivationStep,startSuccessorStep,stopUntrackedChildSessionsStep};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SessionCapabilities } from "#channel/types.js";
|
|
2
2
|
import type { DurableSessionState } from "#execution/durable-session-store.js";
|
|
3
3
|
import type { HarnessModelMessage } from "#harness/messages.js";
|
|
4
4
|
import type { SessionInboxHandle, SessionInboxPayload } from "#execution/session-inbox/inbox.js";
|
|
@@ -29,11 +29,20 @@ export type SessionOwnerActivation = {
|
|
|
29
29
|
} | {
|
|
30
30
|
readonly kind: "incompatible";
|
|
31
31
|
readonly reason: "checkpoint-version";
|
|
32
|
+
/** Version 3: the successor run that gave this verdict. */
|
|
33
|
+
readonly runId?: string;
|
|
32
34
|
/** Commands accepted by partial successor claims before it rejected the checkpoint. */
|
|
33
35
|
readonly payloads: readonly SessionInboxPayload[];
|
|
34
36
|
} | {
|
|
35
37
|
readonly kind: "failed";
|
|
38
|
+
/**
|
|
39
|
+
* Version 3: the successor took some of the session's hooks before it
|
|
40
|
+
* failed. The owner takes them back and waits for their handover.
|
|
41
|
+
*/
|
|
42
|
+
readonly claimedHooks?: boolean;
|
|
36
43
|
readonly error: unknown;
|
|
44
|
+
/** Version 3: the successor run that gave this verdict. */
|
|
45
|
+
readonly runId?: string;
|
|
37
46
|
/** Commands accepted by partial successor claims before startup failed. */
|
|
38
47
|
readonly payloads: readonly SessionInboxPayload[];
|
|
39
48
|
};
|
|
@@ -45,65 +54,98 @@ interface SessionHandoffInput {
|
|
|
45
54
|
readonly sessionId: string;
|
|
46
55
|
}
|
|
47
56
|
export declare function sessionAnchorToken(sessionId: string): string;
|
|
57
|
+
/** Only one run per attempt can hold this; a successor that loses it exits untouched. */
|
|
58
|
+
export declare function handoffLeaseToken(attemptKey: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Measured from the lease's creation and far beyond any queue delay, so a
|
|
61
|
+
* duplicate candidate that boots late still finds the token taken.
|
|
62
|
+
*/
|
|
63
|
+
export declare const HANDOFF_LEASE_RETENTION = "7d";
|
|
48
64
|
type SessionTransferOutcome = {
|
|
49
65
|
readonly kind: "transferred";
|
|
50
66
|
} | {
|
|
51
67
|
readonly kind: "retained";
|
|
52
68
|
readonly reason: SessionRetainedReason;
|
|
53
69
|
};
|
|
54
|
-
type SessionRetainedReason = "same-deployment" | "missing-deployment" | "not-idle" | "busy" | "
|
|
70
|
+
type SessionRetainedReason = "same-deployment" | "missing-deployment" | "not-idle" | "busy" | "activation-failed" | "checkpoint-incompatible" | "known-incompatible" | "unsupported-world";
|
|
55
71
|
/** A compaction handoff the owner owes; the successor keeps this deadline. */
|
|
56
72
|
export interface CompactionHandoff {
|
|
57
73
|
readonly sessionTimeoutDeadline: Date | undefined;
|
|
58
74
|
}
|
|
59
75
|
type SessionTransferState = Pick<SessionCheckpoint, "history" | "serializedContext" | "sessionState">;
|
|
76
|
+
/**
|
|
77
|
+
* Sends `payloads` as handover parts, one payload per part: each was accepted
|
|
78
|
+
* on its own, so no part exceeds what the World already took. Every part is
|
|
79
|
+
* retried with backoff until it is delivered or the session has ended: the
|
|
80
|
+
* receiver reads nothing until the final part lands, and dropping a part
|
|
81
|
+
* would lose a message the session accepted.
|
|
82
|
+
*/
|
|
83
|
+
export declare function sendSessionHandover(input: {
|
|
84
|
+
readonly attemptKey: string;
|
|
85
|
+
readonly payloads: readonly SessionInboxPayload[];
|
|
86
|
+
readonly sessionId: string;
|
|
87
|
+
}): Promise<void>;
|
|
60
88
|
/**
|
|
61
89
|
* The sole boundary for moving an idle session to a successor run: on another
|
|
62
90
|
* exact deployment when newer code accepts a delivery, or on this deployment
|
|
63
91
|
* after compaction so no single run's event log grows with the session.
|
|
64
92
|
* Constructed once per owner; each `try*` method attempts one transfer.
|
|
65
|
-
* When the upstream atomic hook-handoff primitive lands, only this class changes.
|
|
66
93
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* retries.
|
|
94
|
+
* The owner never releases its hooks. The successor force-claims every one of
|
|
95
|
+
* them, and the takeover itself is the success signal: the owner then hands
|
|
96
|
+
* over whatever it accepted meanwhile and exits. Until the attempt settles
|
|
97
|
+
* the owner runs no turn.
|
|
72
98
|
*/
|
|
73
99
|
export declare class SessionHandoff {
|
|
74
100
|
private readonly input;
|
|
75
101
|
private anchor;
|
|
76
102
|
/** Deployments that rejected this session's checkpoint during this owner run. */
|
|
77
103
|
private readonly incompatibleTargetDeploymentIds;
|
|
104
|
+
/** Counts this run's attempts; replay repeats the same sequence. */
|
|
105
|
+
private attempts;
|
|
106
|
+
/** The World cannot force-claim hooks, so no attempt can ever succeed. */
|
|
107
|
+
private worldUnsupported;
|
|
78
108
|
constructor(input: SessionHandoffInput);
|
|
79
109
|
/**
|
|
80
|
-
* Attempts to move the session to the selected delivery's deployment.
|
|
81
|
-
*
|
|
82
|
-
* session
|
|
83
|
-
* successor activated or this owner retained the session. Candidate failures
|
|
84
|
-
* recover locally rather than escaping.
|
|
110
|
+
* Attempts to move the session to the selected delivery's deployment. The
|
|
111
|
+
* outcome states whether the successor took over or this owner retained the
|
|
112
|
+
* session. Candidate failures recover locally rather than escaping.
|
|
85
113
|
*/
|
|
86
|
-
tryTransfer(selection: TurnSelection, state: SessionTransferState
|
|
87
|
-
readonly compaction?: CompactionHandoff;
|
|
88
|
-
}): Promise<SessionTransferOutcome>;
|
|
114
|
+
tryTransfer(selection: TurnSelection, state: SessionTransferState): Promise<SessionTransferOutcome>;
|
|
89
115
|
/**
|
|
90
116
|
* Attempts to move a session that compacted to a fresh run on this
|
|
91
|
-
* deployment.
|
|
92
|
-
*
|
|
93
|
-
* the session's deadline.
|
|
117
|
+
* deployment. The caller guarantees no input is waiting; the successor
|
|
118
|
+
* parks until the next one arrives and keeps the session's deadline.
|
|
94
119
|
*/
|
|
95
|
-
tryCompactionTransfer(state: SessionTransferState, compaction: CompactionHandoff
|
|
96
|
-
readonly delivery?: DeliverHookPayload;
|
|
97
|
-
}): Promise<SessionTransferOutcome>;
|
|
120
|
+
tryCompactionTransfer(state: SessionTransferState, compaction: CompactionHandoff): Promise<SessionTransferOutcome>;
|
|
98
121
|
private transfer;
|
|
122
|
+
/**
|
|
123
|
+
* The session was taken over while no attempt was open. This run leaves as
|
|
124
|
+
* after a transfer, without finalizing the stream the new owner now writes.
|
|
125
|
+
*/
|
|
126
|
+
adoptTakeover(): Promise<{
|
|
127
|
+
readonly kind: "transferred";
|
|
128
|
+
}>;
|
|
129
|
+
/**
|
|
130
|
+
* Claims the attempt's lease. `gaveUpRunId` is the candidate whose outcome
|
|
131
|
+
* the owner is acting on: holding the lease, it can no longer take over.
|
|
132
|
+
*/
|
|
133
|
+
private fenceAttempt;
|
|
134
|
+
/** Forwards what this run accepted but never read to the run that took the session over. */
|
|
135
|
+
private handOver;
|
|
136
|
+
/**
|
|
137
|
+
* Takes back every address a candidate that gave up may hold. Forced claims
|
|
138
|
+
* replace this run's own hooks, so this holds whether or not a pump has
|
|
139
|
+
* observed the candidate's claim yet.
|
|
140
|
+
*/
|
|
141
|
+
private holdEveryToken;
|
|
142
|
+
private recoverFromSilentCandidate;
|
|
143
|
+
/** Keeps the session after a candidate's verdict, taking back any hook it claimed. */
|
|
144
|
+
private recover;
|
|
145
|
+
private ensureAnchor;
|
|
99
146
|
/** After a transfer, the original run parks until the final owner reports the session result. */
|
|
100
147
|
awaitAnchoredResult(): Promise<WorkflowEntryResult>;
|
|
101
148
|
disposeAnchor(): Promise<void>;
|
|
102
|
-
/** Starts the candidate and waits for it to activate or fail. */
|
|
103
|
-
private startAndActivate;
|
|
104
|
-
/** Reclaims the exact hook set and replays payloads accepted while it was released. */
|
|
105
|
-
private recover;
|
|
106
149
|
private reportRetained;
|
|
107
|
-
private ensureAnchor;
|
|
108
150
|
}
|
|
109
151
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{isObject}from"#shared/guards.js";import{
|
|
1
|
+
import{isObject}from"#shared/guards.js";import{createHook,getWorkflowMetadata,sleep}from"#compiled/@workflow/core/index.js";import{claimHookOwnership,disposeHook,isHookConflictError}from"#execution/hook-ownership.js";import{hasRunEndedStep,reportHandoverPartStuckStep,reportSessionHandoffRetainedStep,reportSessionTakenOverOutsideAttemptStep,sendSessionHandoverPartStep,startSuccessorStep}from"#execution/session/handoff-steps.js";import{abandonHandoverIfRunEnds,watchRunEnd}from"#execution/session/run-watch.js";import{readAcceptedDeploymentId}from"#execution/session/accepted-deployment.js";const SESSION_CHECKPOINT_VERSION=15;function sessionAnchorToken(e){return`${e}:anchor`}function handoffLeaseToken(e){return`eve:handoff:lease:${e}`}const HANDOFF_LEASE_RETENTION=`7d`;function verdictRunId(e){return e.kind===`verdict`&&e.verdict.kind!==`active`?e.verdict.runId:void 0}async function sendSessionHandover(e){let{attemptKey:t,sessionId:n}=e,r=e.payloads.filter(e=>e.kind!==`handover`);for(let[e,i]of r.entries())if(await sendHandoverPart({attemptKey:t,index:e,payloads:[i]},n)===`session-ended`)return;await sendHandoverPart({attemptKey:t,final:!0,index:r.length,payloads:[]},n)}async function sendHandoverPart(e,t){for(let n=1,r=1;;n++)try{return await sendSessionHandoverPartStep({part:e,sessionId:t})}catch(i){n===HANDOVER_PART_FAILURES_BEFORE_REPORT&&await reportHandoverPartStuckStep({attemptKey:e.attemptKey,error:isObject(i)&&typeof i.message==`string`?i.message:void 0,failures:n,index:e.index,sessionId:t}).catch(()=>{}),await sleep(r*1e3),r=Math.min(r*2,60)}}const HANDOVER_PART_FAILURES_BEFORE_REPORT=5;var SessionHandoff=class{input;anchor;incompatibleTargetDeploymentIds=new Set;attempts=0;worldUnsupported=!1;constructor(e){this.input=e}async tryTransfer(e,t){let{delivery:n}=e,r=readAcceptedDeploymentId(n);return r===void 0?{kind:`retained`,reason:`missing-deployment`}:r===this.input.deploymentId?{kind:`retained`,reason:`same-deployment`}:this.incompatibleTargetDeploymentIds.has(r)?{kind:`retained`,reason:`known-incompatible`}:e.handoffEligible?await this.transfer(t,r,{delivery:n}):{kind:`retained`,reason:`busy`}}async tryCompactionTransfer(e,t){let{deploymentId:n}=this.input;return this.incompatibleTargetDeploymentIds.has(n)?{kind:`retained`,reason:`known-incompatible`}:await this.transfer(e,n,{...t,reason:`compaction`})}async transfer(e,t,n){let{inbox:r}=this.input;if(this.worldUnsupported)return{kind:`retained`,reason:`unsupported-world`};if(r.awaitingHandover)return{kind:`retained`,reason:`busy`};let i=getWorkflowMetadata().workflowRunId,a=this.attempts++,o=`${i}:${String(a)}`,s=r.claimedTokens,c=createHook({token:`${i}:handoff:${String(a)}`});this.ensureAnchor();let l=new AbortController,u=Date.now();try{let a;try{let r=await startSuccessorStep({...n,activationToken:c.token,anchorRunId:this.input.sessionId,attemptKey:o,checkpoint:{...this.input.checkpoint,...e,version:15},hookTokens:s,sourceRunId:i,targetDeploymentId:t});if(r.kind===`not-idle`)return{kind:`retained`,reason:`not-idle`};if(r.kind===`unsupported`)return this.worldUnsupported=!0,{kind:`retained`,reason:`unsupported-world`};a=r.runId}catch(e){let n=await this.fenceAttempt(o);if(n.kind!==`live`)return n.kind===`done`&&await this.holdEveryToken(s),await this.reportRetained(`activation-failed`,t,e),{kind:`retained`,reason:`activation-failed`};a=n.runId}let d=r.whenTakenOver(l.signal).then(()=>({kind:`taken-over`})),f,p;for(;;){if(f===void 0&&p===void 0&&r.hasPending()){let e=u+3e5-Date.now();p=sleep(Math.max(0,e)).then(()=>({kind:`deadline`}))}let e;try{e=await Promise.race([d,c.then(e=>({kind:`verdict`,verdict:e})),f===void 0?p??r.whenArrival().then(()=>({kind:`arrival`})):f.ended.then(()=>({kind:`candidate-ended`}))])}finally{f?.stop()}if(e.kind===`taken-over`)return await this.handOver(o);if(e.kind===`arrival`)continue;let n=e.kind===`candidate-ended`?a:verdictRunId(e),i=await this.fenceAttempt(o,n);if(i.kind===`live`){a=i.runId,f=watchRunEnd(a);continue}i.kind===`done`&&i.runId!==verdictRunId(e)&&await this.holdEveryToken(s);let l=e.kind===`verdict`?await this.recover(e.verdict,{attemptKey:o,candidateRunId:a,hookTokens:s,targetDeploymentId:t}):this.recoverFromSilentCandidate(e.kind);return await this.reportRetained(l.reason,t,l.error),{kind:`retained`,reason:l.reason}}}finally{l.abort(),await disposeHook(c)}}async adoptTakeover(){return await reportSessionTakenOverOutsideAttemptStep({sessionId:this.input.sessionId}),this.ensureAnchor(),{kind:`transferred`}}async fenceAttempt(e,t){let n=createHook({experimental_minRetention:`7d`,token:handoffLeaseToken(e)});try{await claimHookOwnership(n)}catch(e){if(!isHookConflictError(e)||typeof e.conflictingRunId!=`string`)throw e;let n=e.conflictingRunId;return n===t||await hasRunEndedStep({runId:n})?{kind:`done`,runId:n}:{kind:`live`,runId:n}}return{kind:`won`}}async handOver(e){return await sendSessionHandover({attemptKey:e,payloads:this.input.inbox.drain(),sessionId:this.input.sessionId}),{kind:`transferred`}}async holdEveryToken(e){await this.input.inbox.claimSessionHooks(e,{force:!0})}recoverFromSilentCandidate(e){return{error:Error(e===`deadline`?`The successor run did not report within 5 minutes.`:`The successor run ended before the handoff settled.`),reason:`activation-failed`}}async recover(e,t){let{inbox:n}=this.input;switch(e.kind){case`active`:return{error:Error(`Successor reported active without a takeover.`),reason:`activation-failed`};case`incompatible`:return this.incompatibleTargetDeploymentIds.add(t.targetDeploymentId),e.payloads.length>0&&n.restore(e.payloads),{reason:`checkpoint-incompatible`};case`failed`:return e.claimedHooks===!0?(await n.claimSessionHooks(t.hookTokens,{force:!0,handover:t.attemptKey}),abandonHandoverIfRunEnds(n,{attemptKey:t.attemptKey,sourceRunId:e.runId??t.candidateRunId})):isHookConflictError(e.error)&&this.incompatibleTargetDeploymentIds.add(t.targetDeploymentId),e.payloads.length>0&&n.restore(e.payloads),{error:e.error,reason:`activation-failed`}}}ensureAnchor(){this.input.isInitialOwner&&this.anchor===void 0&&(this.anchor=createHook({token:sessionAnchorToken(this.input.sessionId)}))}async awaitAnchoredResult(){if(this.anchor===void 0)throw Error(`Session anchor was never claimed.`);try{return await this.anchor}finally{await this.disposeAnchor()}}async disposeAnchor(){if(this.anchor===void 0)return;let e=this.anchor;this.anchor=void 0,await disposeHook(e)}async reportRetained(e,t,n){let r=isObject(n)&&typeof n.message==`string`?n.message:void 0;await reportSessionHandoffRetainedStep({error:r,reason:e,sessionId:this.input.sessionId,sourceDeploymentId:this.input.deploymentId,targetDeploymentId:t})}};export{HANDOFF_LEASE_RETENTION,SESSION_CHECKPOINT_VERSION,SessionHandoff,handoffLeaseToken,sendSessionHandover,sessionAnchorToken};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{STUB_CONTEXT_KEY}from"#tool-stubs/types.js";import{workingTasks}from"#execution/tasks/table.js";import{settleCancelledTurnStep}from"#execution/settle-cancelled-turn-step.js";import"#execution/session-inbox/inbox.js";import{signalSessionAnchorStep}from"#execution/session/handoff-steps.js";import{SessionInputQueue}from"#execution/session/input-queue.js";import{createTurnControl}from"#execution/session/turn-control.js";import{SessionHandoff,sessionAnchorToken}from"#execution/session/handoff.js";import{notifyCancelledTaskCallerStep,notifyTurnCallerStep,resolveInitialTurnCallerStep}from"#subagents/parent-notification.js";import{withStubPlayback}from"#execution/tool-stubs/playback.js";import{nextTurnDelivery}from"#execution/session/next-input.js";import{SessionExecution}from"#execution/session/turn.js";import{SessionStateCursor}from"#execution/session/state-cursor.js";import{cancelWorkingTasks,sessionTaskTable}from"#execution/tasks/session.js";import{finalizeSession}from"#execution/session/finalization.js";function createSafeOuterWorkflowError(){let e=Error(`Agent workflow failed. Inspect the private session trace for details.`);return e.name=`EveWorkflowFailure`,e}async function runPreparedSession(e,t){let n=e.serializedContext[STUB_CONTEXT_KEY];n!==void 0&&n.rootSessionId===void 0&&(e.serializedContext[STUB_CONTEXT_KEY]={...n,rootSessionId:e.sessionId});let r=new SessionStateCursor({history:e.history,inbox:t,sessionWritable:e.sessionWritable,serializedContext:e.serializedContext,sessionState:e.sessionState}),i={caller:e.caller,terminalEmitted:!1},a=new SessionHandoff({checkpoint:{capabilities:e.capabilities,retention:e.retention,sessionTimeoutMs:e.sessionTimeoutMs},deploymentId:e.deploymentId,inbox:t,isInitialOwner:e.anchor.kind===`self`,sessionId:e.sessionId}),o={output:``,isError:!0},s;try{return o=await withStubPlayback(n,e.sessionId,async()=>{try{s=await runSessionLoop(e,{cursor:r,handoff:a,inbox:t,progress:i})}finally{await t.dispose()}return s.kind===`transferred`?e.anchor.kind===`self`?(o=await a.awaitAnchoredResult(),o):{output:``}:(o=await finalizeSession(s.outcome,{caller:i.caller,cursor:r,sessionWritable:e.sessionWritable}),i.terminalEmitted=!0,o)}),o}catch(t){throw i.terminalEmitted||await finalizeSession({error:t,kind:`failed`,turnId:i.turnId},{caller:i.caller,cursor:r,sessionWritable:e.sessionWritable}),createSafeOuterWorkflowError()}finally{await t.dispose(),await reportResultToAnchor(e,o,a,s)}}async function failSession(e){let t;try{t=await resolveInitialTurnCallerStep({serializedContext:e.serializedContext})}catch{}throw await finalizeSession({error:e.error,kind:`failed`},{caller:t,cursor:{serializedContext:e.serializedContext,sessionState:e.sessionState},sessionWritable:e.sessionWritable}),createSafeOuterWorkflowError()}async function reportResultToAnchor(e,t,n,r){switch(e.anchor.kind){case`self`:await n.disposeAnchor(),await e.anchor.notify?.(t);return;case`successor`:if(r?.kind===`transferred`)return;await signalSessionAnchorStep({result:t,token:sessionAnchorToken(e.sessionId)});return}}async function runSessionLoop(e,t){let{cursor:n,handoff:r,inbox:i,progress:a}=t,o=new SessionInputQueue,s=new SessionExecution({capabilities:e.capabilities,cursor:n,inbox:i,queue:o,sessionId:e.sessionId}),c=e.sessionTimeoutControl,nextParkedActivity=async()=>{for(;;){let e=await nextTurnDelivery({cursor:n,hasWorkingTasks:()=>workingTasks(sessionTaskTable(n)).length>0,inbox:i,queue:o});if(e.kind===`workflow`){await s.handleWorkflowMessage(e.message);continue}if(e.kind===`cancel-working-tasks`){await cancelWorkingTasks(n,`turn_cancelled`);continue}return e}},l=e.initialTurnControl??(e.anchor.kind===`self`?createTurnControl():void 0),u=0,d=!1,runTurn=async e=>{let t=a.caller,n=l;l=void 0,a.turnId=`turn_${String(u++)}`;let r=await s.runTurn(e,{caller:t,control:n});return r.caller!==void 0&&(a.caller=r.caller),r.compacted===!0&&(d=!0),r},transferState=()=>({history:n.history,serializedContext:n.serializedContext,sessionState:n.sessionState}),dueCompactionHandoff=()=>d&&a.caller===void 0&&workingTasks(sessionTaskTable(n)).length===0?{sessionTimeoutDeadline:e.sessionTimeoutDeadline}:void 0,tryCompactionHandoff=async()=>{let e=dueCompactionHandoff();if(e===void 0||o.pendingCount>0||i.hasPending())return;let t=await r.tryCompactionTransfer(transferState(),e);return t.kind===`transferred`?t:void 0},runDeliveredTurn=async e=>{let t=await r.tryTransfer(e,transferState()
|
|
1
|
+
import{STUB_CONTEXT_KEY}from"#tool-stubs/types.js";import{workingTasks}from"#execution/tasks/table.js";import{settleCancelledTurnStep}from"#execution/settle-cancelled-turn-step.js";import"#execution/session-inbox/inbox.js";import{signalSessionAnchorStep}from"#execution/session/handoff-steps.js";import{SessionInputQueue}from"#execution/session/input-queue.js";import{createTurnControl}from"#execution/session/turn-control.js";import{SessionHandoff,sessionAnchorToken}from"#execution/session/handoff.js";import{notifyCancelledTaskCallerStep,notifyTurnCallerStep,resolveInitialTurnCallerStep}from"#subagents/parent-notification.js";import{withStubPlayback}from"#execution/tool-stubs/playback.js";import{nextTurnDelivery}from"#execution/session/next-input.js";import{SessionExecution}from"#execution/session/turn.js";import{SessionStateCursor}from"#execution/session/state-cursor.js";import{cancelWorkingTasks,sessionTaskTable}from"#execution/tasks/session.js";import{finalizeSession}from"#execution/session/finalization.js";function createSafeOuterWorkflowError(){let e=Error(`Agent workflow failed. Inspect the private session trace for details.`);return e.name=`EveWorkflowFailure`,e}async function runPreparedSession(e,t){let n=e.serializedContext[STUB_CONTEXT_KEY];n!==void 0&&n.rootSessionId===void 0&&(e.serializedContext[STUB_CONTEXT_KEY]={...n,rootSessionId:e.sessionId});let r=new SessionStateCursor({history:e.history,inbox:t,sessionWritable:e.sessionWritable,serializedContext:e.serializedContext,sessionState:e.sessionState}),i={caller:e.caller,terminalEmitted:!1},a=new SessionHandoff({checkpoint:{capabilities:e.capabilities,retention:e.retention,sessionTimeoutMs:e.sessionTimeoutMs},deploymentId:e.deploymentId,inbox:t,isInitialOwner:e.anchor.kind===`self`,sessionId:e.sessionId}),o={output:``,isError:!0},s;try{return o=await withStubPlayback(n,e.sessionId,async()=>{try{s=await runSessionLoop(e,{cursor:r,handoff:a,inbox:t,progress:i})}finally{await t.dispose()}return s.kind===`transferred`?e.anchor.kind===`self`?(o=await a.awaitAnchoredResult(),o):{output:``}:(o=await finalizeSession(s.outcome,{caller:i.caller,cursor:r,sessionWritable:e.sessionWritable}),i.terminalEmitted=!0,o)}),o}catch(t){throw i.terminalEmitted||await finalizeSession({error:t,kind:`failed`,turnId:i.turnId},{caller:i.caller,cursor:r,sessionWritable:e.sessionWritable}),createSafeOuterWorkflowError()}finally{await t.dispose(),await reportResultToAnchor(e,o,a,s)}}async function failSession(e){let t;try{t=await resolveInitialTurnCallerStep({serializedContext:e.serializedContext})}catch{}throw await finalizeSession({error:e.error,kind:`failed`},{caller:t,cursor:{serializedContext:e.serializedContext,sessionState:e.sessionState},sessionWritable:e.sessionWritable}),createSafeOuterWorkflowError()}async function reportResultToAnchor(e,t,n,r){switch(e.anchor.kind){case`self`:await n.disposeAnchor(),await e.anchor.notify?.(t);return;case`successor`:if(r?.kind===`transferred`)return;await signalSessionAnchorStep({result:t,token:sessionAnchorToken(e.sessionId)});return}}async function runSessionLoop(e,t){let{cursor:n,handoff:r,inbox:i,progress:a}=t,o=new SessionInputQueue,s=new SessionExecution({capabilities:e.capabilities,cursor:n,inbox:i,queue:o,sessionId:e.sessionId}),c=e.sessionTimeoutControl,nextParkedActivity=async()=>{for(;;){let e=await nextTurnDelivery({cursor:n,hasWorkingTasks:()=>workingTasks(sessionTaskTable(n)).length>0,inbox:i,queue:o});if(e.kind===`workflow`){await s.handleWorkflowMessage(e.message);continue}if(e.kind===`cancel-working-tasks`){await cancelWorkingTasks(n,`turn_cancelled`);continue}return e}},l=e.initialTurnControl??(e.anchor.kind===`self`?createTurnControl():void 0),u=0,d=!1,runTurn=async e=>{let t=a.caller,n=l;l=void 0,a.turnId=`turn_${String(u++)}`;let r=await s.runTurn(e,{caller:t,control:n});return r.caller!==void 0&&(a.caller=r.caller),r.compacted===!0&&(d=!0),r},transferState=()=>({history:n.history,serializedContext:n.serializedContext,sessionState:n.sessionState}),dueCompactionHandoff=()=>d&&a.caller===void 0&&workingTasks(sessionTaskTable(n)).length===0?{sessionTimeoutDeadline:e.sessionTimeoutDeadline}:void 0,tryCompactionHandoff=async()=>{let e=dueCompactionHandoff();if(e===void 0||o.pendingCount>0||i.hasPending())return;let t=await r.tryCompactionTransfer(transferState(),e);return t.kind===`transferred`?t:void 0},runDeliveredTurn=async e=>{let t=await r.tryTransfer(e,transferState());return t.kind===`transferred`?t:(e.delivery.caller!==void 0&&(a.caller=e.delivery.caller),{action:await runTurn({delivery:e.delivery}),kind:`action`})},settleCancelledTurn=async e=>{let t=await n.advanceWithHistory(t=>settleCancelledTurnStep({...t,reportUsage:e}));return a.caller=void 0,t},awaitPrewarmedAction=async()=>{for(;;){let e=await nextParkedActivity();switch(e.kind){case`closed`:return i.takenOver?await r.adoptTakeover():{kind:`terminal`,outcome:{kind:`expired`}};case`expired`:case`reset`:return{kind:`terminal`,outcome:{kind:`expired`}};case`clear`:case`compact`:continue;case`turn`:return await runDeliveredTurn(e);case`cancel-turn`:continue}}},runInitialAction=async()=>{switch(e.start.kind){case`first-message`:return await awaitPrewarmedAction();case`parked`:return{action:{kind:`park`},kind:`action`};case`turn`:{let{input:t}=e.start;return{action:await runTurn(t===void 0?void 0:{delivery:t}),kind:`action`}}}};try{let[t,n]=await Promise.allSettled([runInitialAction(),c?.start()]);if(n.status===`rejected`)throw n.reason;if(t.status===`rejected`)throw t.reason;let o=t.value;if(o.kind!==`action`)return o;let l=o.action;for(;;){if(l.kind===`done`)return{kind:`terminal`,outcome:{action:l,kind:`done`}};if(l.cancelled===!0){let{caller:t}=a,n=await settleCancelledTurn(t!==void 0);if(t!==void 0){let r={caller:t,sessionId:e.sessionId};await notifyCancelledTaskCallerStep(n.usage===void 0?r:{...r,usage:n.usage})}}else l.settled!==void 0&&(a.caller!==void 0&&await notifyTurnCallerStep({caller:a.caller,lifecycle:`parked`,sessionId:e.sessionId,settled:l.settled}),a.caller=void 0);let t=await tryCompactionHandoff();if(t!==void 0)return t;let n=await nextParkedActivity();switch(n.kind){case`closed`:return i.takenOver?await r.adoptTakeover():{kind:`terminal`,outcome:{kind:`expired`}};case`expired`:case`reset`:return{kind:`terminal`,outcome:{kind:`expired`}};case`clear`:case`compact`:l=await runTurn({control:n.kind});continue;case`cancel-turn`:await s.cancelTurnWork(),await settleCancelledTurn(!1),l={...l,settled:void 0};continue;case`turn`:{let e=await runDeliveredTurn(n);if(e.kind!==`action`)return e;l=e.action;continue}}}}finally{l?.dispose(),await c?.dispose()}}export{failSession,runPreparedSession};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { SessionInboxHandle } from "#execution/session-inbox/inbox.js";
|
|
2
|
+
export interface RunWatch {
|
|
3
|
+
/** Resolves once the run has ended; never settles once stopped. */
|
|
4
|
+
readonly ended: Promise<void>;
|
|
5
|
+
stop(): void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Watches a counterpart run so a wait on it has a way out when that run is
|
|
9
|
+
* gone. Durable sleeps between checks keep it deterministic under replay;
|
|
10
|
+
* `stop()` ends it before its next check.
|
|
11
|
+
*/
|
|
12
|
+
export declare function watchRunEnd(runId: string): RunWatch;
|
|
13
|
+
/**
|
|
14
|
+
* Opens the inbox's handover gate with nothing restored if `sourceRunId` ends
|
|
15
|
+
* before its handover arrives: an ended run can never send it. The watch
|
|
16
|
+
* starts only once a read waits on the gate, so a handover that arrives
|
|
17
|
+
* before it is needed costs nothing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function abandonHandoverIfRunEnds(inbox: SessionInboxHandle, handover: {
|
|
20
|
+
readonly attemptKey: string;
|
|
21
|
+
readonly sourceRunId: string;
|
|
22
|
+
}): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{sleep}from"#compiled/@workflow/core/index.js";import{hasRunEndedStep}from"#execution/session/handoff-steps.js";function watchRunEnd(e){let t=!1;return{ended:(async()=>{for(;!t&&(await sleep(`10s`),!t);)if(await hasRunEndedStep({runId:e}).catch(()=>!1)&&!t)return;return await new Promise(()=>{})})(),stop(){t=!0}}}function abandonHandoverIfRunEnds(e,t){e.whenBlockedOnHandover().then(()=>{if(!e.awaitingHandover)return;let n=watchRunEnd(t.sourceRunId);e.whenHandoverSettled().then(()=>n.stop()),n.ended.then(()=>e.abandonHandover(t.attemptKey))})}export{abandonHandoverIfRunEnds,watchRunEnd};
|
|
@@ -21,5 +21,8 @@ export declare function sessionInboxHookToken(token: string): string;
|
|
|
21
21
|
export declare function isSessionInboxHookToken(token: string): boolean;
|
|
22
22
|
/** Logical address behind a physical session inbox hook token; `undefined` for any other hook. */
|
|
23
23
|
export declare function logicalSessionInboxToken(token: string): string | undefined;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Marker an owner older than handoff version 3 leaves while it releases its
|
|
26
|
+
* hooks, so ingress can tell "handoff in progress" from "no session".
|
|
27
|
+
*/
|
|
25
28
|
export declare function sessionHandoffMarkerToken(token: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createLogger}from"#internal/logging.js";import{logicalSessionInboxToken,sessionCommandHookToken,sessionInboxHookToken}from"#execution/session-inbox/address.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{encodeMessageStreamEvent,stampMessageStreamEvent}from"#protocol/message.js";import{cancelRun,getRun,getWorld}from"#internal/workflow/runtime.js";import{isInactiveWorkflowRunError}from"#internal/workflow/is-inactive-workflow-run-error.js";import{logicalSessionToken,requireSessionId}from"#execution/session-inbox/resume.js";import{sessionFailed}from"#harness/session-machine/transitions.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";import{EVE_SESSION_ATTRIBUTE,EVE_TYPE_ATTRIBUTE}from"#execution/eve-workflow-attributes.js";import{waitForSessionHooksRelease}from"#execution/session-inbox/hook-release.js";const log=createLogger(`execution.session-inbox`);async function endStrandedSession(e,t,n){let r=typeof t==`string`?await resolveStrandedSessionId(e):t.sessionId,{ownerRunId:i}=e,a=await getWorld();await assertStillServesSession(r,i);let o=await listOwnedSessionTokens(a,i);return await publishTerminalEvent(r,n),await cancelRuns(a,[i,r],`Session ended: its deployment was retired`),await waitForSessionHooksRelease([sessionCommandHookToken(r),logicalSessionToken(t),...o],i),log.warn(`Reset stranded session`,{currentEveVersion:resolveInstalledPackageInfo().version,previousEveVersion:e.eveVersion??`unknown`,previousSessionId:r,trigger:n}),r}async function assertStillServesSession(e,t){let n=await findSessionHookHolder(sessionInboxHookToken(sessionCommandHookToken(e)));if(n!==void 0&&n.runId!==t)throw Error(`Session "${e}" changed owners while it was being reset. Nothing was ended; retry the request.`)}async function publishTerminalEvent(e,t){let n=getRun(e).getWritable().getWriter(),r=(async()=>(await n.write(encodeMessageStreamEvent(stampMessageStreamEvent(sessionFailed({code:`session_stranded`,details:{trigger:t},message:`This session is no longer available.`,sessionId:e,usage:void 0})))),await n.close(),!0))();r.catch(()=>{});let i,a=new Promise(e=>{i=setTimeout(()=>e(!1),5e3)});try{await Promise.race([r,a])||log.warn(`Timed out finishing the stranded session stream`,{sessionId:e})}catch{log.warn(`Could not finish the stranded session stream`,{sessionId:e})}finally{clearTimeout(i),n.releaseLock()}}async function cancelRuns(e,t,n){for(let r of new Set(t))try{await cancelRun(e,r,{cancelReason:n})}catch(e){if(!isInactiveWorkflowRunError(e))throw e}}async function resolveStrandedSessionId(e){let{attributes:t,runId:n}=e.ownerRun,r=t[EVE_SESSION_ATTRIBUTE];if(r!==void 0)return r;if(e.hook!==void 0)try{return requireSessionId(await e.hook.metadata)}catch{}if(t[EVE_TYPE_ATTRIBUTE]!==void 0)return n;throw Error(`Cannot reset stranded session owner run "${n}": it does not record which session it serves. Cancel the run directly.`)}async function listOwnedSessionTokens(e,t){let n=[],r;for(let i=0;i<10;i++){let i=await e.hooks.list({pagination:{cursor:r,limit:100},resolveData:`none`,runId:t});for(let e of i.data){let t=logicalSessionInboxToken(e.token);t!==void 0&&n.push(t)}if(!i.hasMore||i.cursor===null)break;r=i.cursor}return n}export{endStrandedSession};
|
|
1
|
+
import{createLogger}from"#internal/logging.js";import{logicalSessionInboxToken,sessionCommandHookToken,sessionInboxHookToken}from"#execution/session-inbox/address.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{encodeMessageStreamEvent,stampMessageStreamEvent}from"#protocol/message.js";import{cancelRun,getRun,getWorld}from"#internal/workflow/runtime.js";import{isInactiveWorkflowRunError}from"#internal/workflow/is-inactive-workflow-run-error.js";import{logicalSessionToken,requireSessionId}from"#execution/session-inbox/resume.js";import{sessionFailed}from"#harness/session-machine/transitions.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";import{EVE_SESSION_ATTRIBUTE,EVE_TYPE_ATTRIBUTE}from"#execution/eve-workflow-attributes.js";import{waitForSessionHooksRelease}from"#execution/session-inbox/hook-release.js";const log=createLogger(`execution.session-inbox`);async function endStrandedSession(e,t,n){let r=typeof t==`string`?await resolveStrandedSessionId(e):t.sessionId,{ownerRunId:i}=e,a=await getWorld();await assertStillServesSession(r,i);let o=await listOwnedSessionTokens(a,i);return await publishTerminalEvent(r,n),await cancelRuns(a,[i,r],`Session ended: its deployment was retired`),await waitForSessionHooksRelease([sessionCommandHookToken(r),logicalSessionToken(t),...o],{ownerRunId:i,sessionId:r}),log.warn(`Reset stranded session`,{currentEveVersion:resolveInstalledPackageInfo().version,previousEveVersion:e.eveVersion??`unknown`,previousSessionId:r,trigger:n}),r}async function assertStillServesSession(e,t){let n=await findSessionHookHolder(sessionInboxHookToken(sessionCommandHookToken(e)));if(n!==void 0&&n.runId!==t)throw Error(`Session "${e}" changed owners while it was being reset. Nothing was ended; retry the request.`)}async function publishTerminalEvent(e,t){let n=getRun(e).getWritable().getWriter(),r=(async()=>(await n.write(encodeMessageStreamEvent(stampMessageStreamEvent(sessionFailed({code:`session_stranded`,details:{trigger:t},message:`This session is no longer available.`,sessionId:e,usage:void 0})))),await n.close(),!0))();r.catch(()=>{});let i,a=new Promise(e=>{i=setTimeout(()=>e(!1),5e3)});try{await Promise.race([r,a])||log.warn(`Timed out finishing the stranded session stream`,{sessionId:e})}catch{log.warn(`Could not finish the stranded session stream`,{sessionId:e})}finally{clearTimeout(i),n.releaseLock()}}async function cancelRuns(e,t,n){for(let r of new Set(t))try{await cancelRun(e,r,{cancelReason:n})}catch(e){if(!isInactiveWorkflowRunError(e))throw e}}async function resolveStrandedSessionId(e){let{attributes:t,runId:n}=e.ownerRun,r=t[EVE_SESSION_ATTRIBUTE];if(r!==void 0)return r;if(e.hook!==void 0)try{return requireSessionId(await e.hook.metadata)}catch{}if(t[EVE_TYPE_ATTRIBUTE]!==void 0)return n;throw Error(`Cannot reset stranded session owner run "${n}": it does not record which session it serves. Cancel the run directly.`)}async function listOwnedSessionTokens(e,t){let n=[],r;for(let i=0;i<10;i++){let i=await e.hooks.list({pagination:{cursor:r,limit:100},resolveData:`none`,runId:t});for(let e of i.data){let t=logicalSessionInboxToken(e.token);t!==void 0&&n.push(t)}if(!i.hasMore||i.cursor===null)break;r=i.cursor}return n}export{endStrandedSession};
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
/** Longest ingress waits for a session hook to change hands. */
|
|
2
2
|
export declare const SESSION_HOOK_HANDOVER_TIMEOUT_MS = 30000;
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Waits until no run serving `session` holds any of the logical addresses. A
|
|
5
|
+
* handoff successor serves the same session, so only the session's end (or
|
|
6
|
+
* an unrelated run claiming the address afterwards) satisfies the wait.
|
|
7
|
+
*/
|
|
8
|
+
export declare function waitForSessionHooksRelease(logicalTokens: Iterable<string>, session: {
|
|
9
|
+
readonly ownerRunId: string;
|
|
10
|
+
readonly sessionId: string;
|
|
11
|
+
}): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sessionInboxHookToken}from"#execution/session-inbox/address.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";const SESSION_HOOK_HANDOVER_TIMEOUT_MS=3e4;async function waitForSessionHooksRelease(e,t){await Promise.all([...new Set(e)].map(e=>waitForHookRelease(sessionInboxHookToken(e),t)))}async function waitForHookRelease(e,t){let n=Date.now()+SESSION_HOOK_HANDOVER_TIMEOUT_MS;for(;;){if(
|
|
1
|
+
import{sessionInboxHookToken}from"#execution/session-inbox/address.js";import{requireSessionId}from"#execution/session-inbox/resume.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";const SESSION_HOOK_HANDOVER_TIMEOUT_MS=3e4;async function waitForSessionHooksRelease(e,t){await Promise.all([...new Set(e)].map(e=>waitForHookRelease(sessionInboxHookToken(e),t)))}async function waitForHookRelease(e,t){let n=Date.now()+SESSION_HOOK_HANDOVER_TIMEOUT_MS;for(;;){if(!await servesSession(e,t))return;if(Date.now()>=n)throw Error(`Timed out waiting for session "${t.sessionId}" to release inbox "${e}".`);await new Promise(e=>setTimeout(e,20))}}async function servesSession(e,t){let n=await findSessionHookHolder(e);if(n===void 0)return!1;try{if(n.hook!==void 0)return requireSessionId(await n.hook.metadata)===t.sessionId}catch{}return n.runId===t.ownerRunId}export{SESSION_HOOK_HANDOVER_TIMEOUT_MS,waitForSessionHooksRelease};
|
|
@@ -11,12 +11,31 @@ export interface SessionFailurePayload {
|
|
|
11
11
|
readonly kind: "session-failure";
|
|
12
12
|
readonly error: string;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* One part of the previous owner's accepted-but-unread payloads after a
|
|
16
|
+
* handoff. The runtime sends these commands; channel request bodies cannot
|
|
17
|
+
* supply them.
|
|
18
|
+
*/
|
|
19
|
+
export interface SessionHandoverPayload {
|
|
20
|
+
readonly kind: "handover";
|
|
21
|
+
/** The handoff attempt this answers; see {@link SessionHookBatchClaimOptions.handover}. */
|
|
22
|
+
readonly attemptKey: string;
|
|
23
|
+
/** Parts apply in index order; a retried send repeats its index. */
|
|
24
|
+
readonly index: number;
|
|
25
|
+
/** The last part; reads resume once every part before it has been applied. */
|
|
26
|
+
readonly final?: true;
|
|
27
|
+
readonly payloads: readonly Exclude<SessionInboxPayload, SessionHandoverPayload>[];
|
|
28
|
+
}
|
|
29
|
+
export type SessionInboxPayload = SessionFailurePayload | SessionHandoverPayload | HookPayload | SessionCommand | WorkflowToolRunMessage | AuthorizationCallbackPayload;
|
|
30
|
+
/**
|
|
31
|
+
* While a handover is awaited (see {@link SessionHookBatchClaimOptions.handover}),
|
|
32
|
+
* reads expose nothing; interrupt handlers still fire on arrival.
|
|
33
|
+
*/
|
|
15
34
|
export interface SessionInboxReader {
|
|
16
35
|
/**
|
|
17
36
|
* Waits for and removes the next accepted payload in arrival order, or
|
|
18
|
-
* `undefined` once
|
|
19
|
-
* time; the owner program is strictly sequential.
|
|
37
|
+
* `undefined` once no hook can deliver any more (released or taken over).
|
|
38
|
+
* Only one consumer waits at a time; the owner program is strictly sequential.
|
|
20
39
|
*/
|
|
21
40
|
next(): Promise<SessionInboxPayload | undefined>;
|
|
22
41
|
/** Removes every payload accepted so far, in arrival order. */
|
|
@@ -33,21 +52,62 @@ export interface SessionInboxReader {
|
|
|
33
52
|
* consumer still processes it in order.
|
|
34
53
|
*/
|
|
35
54
|
onInterrupt(handler: (payload: SessionInboxPayload) => void): () => void;
|
|
36
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Observes readable deliveries without consuming them; replays unread
|
|
57
|
+
* deliveries on subscription and when a handover arrives, so handlers must
|
|
58
|
+
* be idempotent.
|
|
59
|
+
*/
|
|
37
60
|
onDelivery(handler: (payload: SessionInboxPayload) => void): () => void;
|
|
38
61
|
/** Requeues payloads a successor handed back, ahead of everything pumped since. */
|
|
39
62
|
restore(payloads: readonly SessionInboxPayload[]): void;
|
|
40
63
|
}
|
|
64
|
+
export interface SessionHookClaimOptions {
|
|
65
|
+
/**
|
|
66
|
+
* Take the address over from the run that holds it instead of conflicting.
|
|
67
|
+
* Still rejects with `HookConflictError` when the holder's runtime cannot be
|
|
68
|
+
* taken from; throws `FatalError` when the World does not support it. A
|
|
69
|
+
* token this inbox already holds is claimed again on a fresh hook.
|
|
70
|
+
*/
|
|
71
|
+
readonly force?: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface SessionHookBatchClaimOptions extends SessionHookClaimOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Expect a `handover` for this attempt key. Before any hook is created,
|
|
76
|
+
* reads stop exposing anything; as its parts arrive on any hook, their
|
|
77
|
+
* payloads take their place after what was queued at this call and ahead of
|
|
78
|
+
* everything accepted since. Reads resume after the final part.
|
|
79
|
+
*/
|
|
80
|
+
readonly handover?: string;
|
|
81
|
+
}
|
|
41
82
|
export interface SessionInboxOwnership {
|
|
42
83
|
/** Logical tokens currently claimed, in claim order. */
|
|
43
84
|
readonly claimedTokens: readonly string[];
|
|
44
|
-
claimSessionHook(token: string): Promise<void>;
|
|
85
|
+
claimSessionHook(token: string, options?: SessionHookClaimOptions): Promise<void>;
|
|
45
86
|
/** Registers every token as one batch; all claims settle before the first failure propagates. */
|
|
46
|
-
claimSessionHooks(tokens: readonly string[]): Promise<void>;
|
|
87
|
+
claimSessionHooks(tokens: readonly string[], options?: SessionHookBatchClaimOptions): Promise<void>;
|
|
47
88
|
}
|
|
48
89
|
export interface SessionInbox extends SessionInboxReader, SessionInboxOwnership {
|
|
49
90
|
}
|
|
50
91
|
export interface SessionInboxHandle extends SessionInbox {
|
|
92
|
+
/** A handover is expected and has not arrived; the session must not move again until it does. */
|
|
93
|
+
readonly awaitingHandover: boolean;
|
|
94
|
+
/** Resolves once no handover is awaited: it arrived, was abandoned, or the inbox closed. */
|
|
95
|
+
whenHandoverSettled(): Promise<void>;
|
|
96
|
+
/** Stops waiting for `attemptKey`'s handover, which can no longer arrive; queued payloads become readable. */
|
|
97
|
+
abandonHandover(attemptKey: string): void;
|
|
98
|
+
/** Resolves the first time a read waits on an awaited handover. */
|
|
99
|
+
whenBlockedOnHandover(): Promise<void>;
|
|
100
|
+
/** Resolves when the next payload arrives on any hook, readable or not. */
|
|
101
|
+
whenArrival(): Promise<void>;
|
|
102
|
+
/** Another run force-claimed every hook this inbox held. */
|
|
103
|
+
readonly takenOver: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Resolves once another run has force-claimed every hook this inbox held.
|
|
106
|
+
* Every payload delivered before the takeover is queued by then. Rejects if
|
|
107
|
+
* a reader fails, if the inbox is released or disposed first, or once
|
|
108
|
+
* `signal` aborts.
|
|
109
|
+
*/
|
|
110
|
+
whenTakenOver(signal?: AbortSignal): Promise<void>;
|
|
51
111
|
dispose(): Promise<void>;
|
|
52
112
|
/** Disposes every hook and returns each payload the hooks accepted but the owner never read. */
|
|
53
113
|
release(): Promise<SessionInboxPayload[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{sessionInboxHookToken}from"#execution/session-inbox/address.js";import{createHook,getWorkflowMetadata}from"#compiled/@workflow/core/index.js";import{claimHookOwnership,disposeHook}from"#execution/hook-ownership.js";import{releaseSessionHooksStep}from"#execution/session-inbox/release-step.js";function isInterrupt(e){return e.kind===`cancel`||e.kind===`reset`||e.kind===`session-timeout`||e.kind===`session-failure`}function createSessionInbox(e){let t=[],n=[],r=new Set,i=new Set,a=new Set,o,notify=()=>{for(let e of r)e();r.clear()},wait=()=>new Promise(e=>r.add(e)),closed=()=>t.every(e=>e.closed||e.stopping),pump=async e=>{let
|
|
1
|
+
import{sessionInboxHookToken}from"#execution/session-inbox/address.js";import{createHook,getWorkflowMetadata}from"#compiled/@workflow/core/index.js";import{claimHookOwnership,disposeHook,isHookForceClaimedError}from"#execution/hook-ownership.js";import{releaseSessionHooksStep}from"#execution/session-inbox/release-step.js";function isInterrupt(e){return e.kind===`cancel`||e.kind===`reset`||e.kind===`session-timeout`||e.kind===`session-failure`}function isDelivery(e){return e.kind===`send`||e.kind===`deliver`}function createSessionInbox(e){let t=[],n=[],r=new Set,i=new Set,a=new Set,o,s,c=new Set,l=new Set,u=!1,d=!1,notify=()=>{for(let e of r)e();r.clear()},wait=()=>new Promise(e=>r.add(e)),closed=()=>t.every(e=>e.closed||e.stopping),gated=()=>s!==void 0&&t.length>0,hasReadable=()=>!gated()&&n.length>0,throwIfFailed=()=>{if(o!==void 0)throw o.error},observe=(e,t,n)=>{for(let r of t)if(n(r))for(let t of e)t(r)},openHandover=()=>{s=void 0,observe(a,n,isDelivery),notify()},blockedOnHandover=()=>{if(gated()){for(let e of c)e();c.clear()}},acceptHandover=e=>{let t=s;if(t===void 0||e.attemptKey!==t.attemptKey||e.index<t.nextPart||t.parts.has(e.index))return;t.parts.set(e.index,e);let r=[];for(;;){let e=t.parts.get(t.nextPart);if(e===void 0)break;if(e.final===!0){observe(i,r,isInterrupt),openHandover();return}t.parts.delete(t.nextPart),t.nextPart+=1,n.splice(t.index,0,...e.payloads),t.index+=e.payloads.length,r.push(...e.payloads)}observe(i,r,isInterrupt),notify()},pump=async e=>{let r=e.hook[Symbol.asyncIterator]();try{for(;!e.stopping;){let e=await r.next();if(e.done)break;let t=e.value;for(let e of l)e();if(l.clear(),t.kind===`workflow-batch`){n.push(...t.messages),notify();continue}if(t.kind===`handover`){acceptHandover(t);continue}n.push(t),gated()||observe(a,[t],isDelivery),observe(i,[t],isInterrupt),notify()}}catch(n){if(e.stopping||e.replaced)return;isHookForceClaimedError(n)?(t.splice(t.indexOf(e),1),u=!0):o={error:n}}finally{e.closed=!0,notify()}},stop=async()=>{let e=t.splice(0);s=void 0,u=!1,d=!0;for(let t of e)t.stopping=!0;notify(),await Promise.all(e.map(({hook:e})=>disposeHook(e)));let r=n.splice(0);return notify(),r},claimSessionHook=async(n,r={})=>{if(!n)throw Error(`A session alias requires a nonempty continuation token.`);let i=r.force===!0,a=t.find(e=>e.token===n);if(a!==void 0&&!i)return await a.registered;if(a===void 0&&t.length>=256)throw Error(`A session may claim at most 256 addresses.`);let o={token:n,hook:createHook({token:sessionInboxHookToken(n),metadata:{sessionId:e},...i&&{experimental_force:!0}}),stopping:!1,closed:!1,replaced:!1};a===void 0?t.push(o):(a.replaced=!0,t.splice(t.indexOf(a),1,o)),d=!1;try{o.registered=claimHookOwnership(o.hook),await o.registered,u=!1,pump(o)}catch(e){throw a===void 0||a.closed?t.splice(t.indexOf(o),1):(a.replaced=!1,t.splice(t.indexOf(o),1,a)),e}};return{get claimedTokens(){return t.map(({token:e})=>e)},get awaitingHandover(){return s!==void 0},async whenHandoverSettled(){for(;s!==void 0;)await wait()},abandonHandover(e){s?.attemptKey===e&&openHandover()},whenBlockedOnHandover(){return new Promise(e=>c.add(e))},whenArrival(){return new Promise(e=>l.add(e))},get takenOver(){return u&&t.length===0},claimSessionHook,async claimSessionHooks(e,t={}){t.handover!==void 0&&(s={attemptKey:t.handover,index:n.length,nextPart:0,parts:new Map});let r=await Promise.allSettled([...new Set(e)].map(e=>claimSessionHook(e,t)));for(let e of r)if(e.status===`rejected`)throw e.reason},async whenTakenOver(e){for(e?.addEventListener(`abort`,notify,{once:!0});t.length>0||!u;){if(throwIfFailed(),d)throw Error(`The session inbox was released before a takeover.`);if(e?.aborted===!0)throw e.reason;await wait()}},async next(){for(;;){if(throwIfFailed(),hasReadable())return n.shift();if(closed())return;blockedOnHandover(),await wait()}},drain(){return throwIfFailed(),gated()?[]:n.splice(0)},takeLeading(e){throwIfFailed();let t=[];if(gated())return t;for(let r=n[0];r!==void 0&&e(r);r=n[0])t.push(r),n.shift();return t},hasPending(){return throwIfFailed(),hasReadable()},async whenPending(){for(;o===void 0&&!hasReadable()&&!closed();)blockedOnHandover(),await wait()},onInterrupt(e){return i.add(e),()=>i.delete(e)},onDelivery(e){if(a.add(e),!gated())for(let t of n)isDelivery(t)&&e(t);return()=>a.delete(e)},restore(e){if(t.length===0)throw Error(`Cannot restore session commands before reclaiming the session hooks.`);n.unshift(...e),s!==void 0&&(s.index+=e.length),notify()},async dispose(){await stop()},async release(){return t.length>0&&await releaseSessionHooksStep({ownerRunId:getWorkflowMetadata().workflowRunId,tokens:t.map(({hook:e})=>e.token)}),await stop()}}}function isWorkflowMessage(e){return e.kind===`agent-started`||e.kind===`started`||e.kind===`report`||e.kind===`reply`||e.kind===`request`||e.kind===`withdraw`||e.kind===`usage`||e.kind===`outcome`}export{createSessionInbox,isInterrupt,isWorkflowMessage};
|
|
@@ -7,9 +7,10 @@ export interface ResumedSessionInboxHook {
|
|
|
7
7
|
readonly sessionId: Promise<string>;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Resumes the current owner with the current command shape.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Resumes the current owner with the current command shape. Owners on
|
|
11
|
+
* handoff version 3 never leave an address unowned. An older owner releases
|
|
12
|
+
* its hooks before its successor claims them and leaves a marker for that
|
|
13
|
+
* interval, so delivery retries instead of
|
|
13
14
|
* reporting the session gone and letting a channel start a replacement.
|
|
14
15
|
* The retry window bounds this request, not the handoff: a marker that
|
|
15
16
|
* outlives it throws {@link SessionHandoffPendingError}.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{decide}from"#ai/decide.js";async function runAgentRouterTask(e,t){"use workflow";let n=await chooseTarget(e.message,descriptions(t),t.abortSignal),{message:r,status:i}=await(await t.agent(n).send(e.message,{signal:t.abortSignal})).result();if(i===`failed`)throw Error(`Agent "${n}" failed to handle the task.`);return r??null}async function chooseTarget(e,t,n){"use step";let r=Object.keys(t);if(r.length===0)throw Error(`agentRouter requires at least one available agent with a description.`);return r.length===1?r[0]:(await decide({abortSignal:n,state:{message:e},questions:{route:{type:`choice`,instructions:`Which subagent should handle this task?`,criteria:t}}})).answers.route.choice}function descriptions(e){return Object.fromEntries(Object.entries(e.agents).flatMap(([e,t])=>{let n=t.description.trim();return n.length===0?[]:[[e,n]]}))}runAgentRouterTask.workflowId=`workflow//eve@0.81.
|
|
1
|
+
import{decide}from"#ai/decide.js";async function runAgentRouterTask(e,t){"use workflow";let n=await chooseTarget(e.message,descriptions(t),t.abortSignal),{message:r,status:i}=await(await t.agent(n).send(e.message,{signal:t.abortSignal})).result();if(i===`failed`)throw Error(`Agent "${n}" failed to handle the task.`);return r??null}async function chooseTarget(e,t,n){"use step";let r=Object.keys(t);if(r.length===0)throw Error(`agentRouter requires at least one available agent with a description.`);return r.length===1?r[0]:(await decide({abortSignal:n,state:{message:e},questions:{route:{type:`choice`,instructions:`Which subagent should handle this task?`,criteria:t}}})).answers.route.choice}function descriptions(e){return Object.fromEntries(Object.entries(e.agents).flatMap(([e,t])=>{let n=t.description.trim();return n.length===0?[]:[[e,n]]}))}runAgentRouterTask.workflowId=`workflow//eve@0.81.1//runAgentRouterTask`;export{runAgentRouterTask};
|