@bevel-software/ael 0.0.0-stage → 0.1.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/LICENSE +13 -0
- package/README.md +32 -3
- package/bin/bevel-agent.mjs +15 -0
- package/executor-entrypoint.sh +41 -0
- package/executor.Dockerfile +37 -0
- package/package.json +61 -6
- package/src/agent/agent-file.ts +242 -0
- package/src/cli/main.ts +73 -0
- package/src/cli/run-local.ts +224 -0
- package/src/cli/run-step.ts +147 -0
- package/src/executor/activities.ts +295 -0
- package/src/executor/docker-cli.ts +148 -0
- package/src/executor/driver.ts +80 -0
- package/src/executor/harness-mount.ts +66 -0
- package/src/executor/in-place.ts +180 -0
- package/src/executor/main.ts +197 -0
- package/src/executor/personal.ts +355 -0
- package/src/executor/slots.ts +60 -0
- package/src/harness/claude-code.ts +397 -0
- package/src/harness/pi-in-process.ts +444 -0
- package/src/harness/session-state.ts +86 -0
- package/src/harness/stall-guard.ts +97 -0
- package/src/index.ts +164 -0
- package/src/interpreter/contract.ts +403 -0
- package/src/interpreter/dispatcher.workflow.ts +412 -0
- package/src/interpreter/interpret.workflow.ts +907 -0
- package/src/interpreter/scheduled.workflow.ts +28 -0
- package/src/interpreter/workflows.ts +8 -0
- package/src/run/contract.ts +150 -0
- package/src/run/outcome.ts +63 -0
- package/src/run/reporter.ts +127 -0
- package/src/run/system-prompt.ts +35 -0
- package/src/run/verdict.ts +46 -0
- package/src/shared/front-matter.ts +27 -0
- package/src/shared/redact.ts +78 -0
- package/src/shared/schedule.ts +93 -0
- package/src/temporal/payload-codec.ts +84 -0
- package/src/transcript/render.ts +106 -0
- package/src/workflow/definition.ts +196 -0
- package/src/workflow/failure-policy.ts +100 -0
- package/src/workflow/parameters.ts +175 -0
- package/src/workflow/workflow-file.ts +457 -0
- package/src/workitem/node.ts +170 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@bevel-software/ael` — the agentic execution layer.
|
|
3
|
+
*
|
|
4
|
+
* One library, imported by the platform and shipped into run containers,
|
|
5
|
+
* holding everything that reads an `.agent` or a `.workflow` file and
|
|
6
|
+
* everything that runs a session from them. The platform never parses either
|
|
7
|
+
* file kind itself; the runner never knows what a step does. See the design
|
|
8
|
+
* node `idea-bevel-platform-background-agents` in the knowledge base.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Files
|
|
12
|
+
export {
|
|
13
|
+
AGENT_FILE_EXTENSION,
|
|
14
|
+
AgentParseError,
|
|
15
|
+
MAX_ENV_VARIABLES,
|
|
16
|
+
agentSlug,
|
|
17
|
+
normalizeEntryPath,
|
|
18
|
+
parseAgentFile,
|
|
19
|
+
type AgentDefinition,
|
|
20
|
+
type AgentEnvVar,
|
|
21
|
+
type EnvSource,
|
|
22
|
+
type HarnessName,
|
|
23
|
+
} from './agent/agent-file.js';
|
|
24
|
+
export {
|
|
25
|
+
ERROR_NAMES,
|
|
26
|
+
TERMINAL_TARGETS,
|
|
27
|
+
WORKFLOW_FILE_EXTENSION,
|
|
28
|
+
WorkflowParseError,
|
|
29
|
+
environmentOf,
|
|
30
|
+
evaluate,
|
|
31
|
+
isTerminalTarget,
|
|
32
|
+
nextAfter,
|
|
33
|
+
parseWorkflowFile,
|
|
34
|
+
PERSONAL_EXECUTOR_NAME,
|
|
35
|
+
stepByName,
|
|
36
|
+
stepSlug,
|
|
37
|
+
stripReservedKeys,
|
|
38
|
+
type CatchRule,
|
|
39
|
+
type ChoiceRule,
|
|
40
|
+
type Comparator,
|
|
41
|
+
type EntryBudget,
|
|
42
|
+
type Environment,
|
|
43
|
+
type ErrorName,
|
|
44
|
+
type ParamSource,
|
|
45
|
+
type ParametersDef,
|
|
46
|
+
type RequiredParam,
|
|
47
|
+
type RetryRule,
|
|
48
|
+
type StepDef,
|
|
49
|
+
type StepKind,
|
|
50
|
+
type SystemPromptSpec,
|
|
51
|
+
type TerminalTarget,
|
|
52
|
+
type WorkflowDefinition,
|
|
53
|
+
} from './workflow/workflow-file.js';
|
|
54
|
+
export {
|
|
55
|
+
ATTEMPTS_KEY,
|
|
56
|
+
ENTRIES_KEY,
|
|
57
|
+
RETRIES_KEY,
|
|
58
|
+
decide,
|
|
59
|
+
errorForVerdict,
|
|
60
|
+
isRetryKeyOf,
|
|
61
|
+
retryKey,
|
|
62
|
+
type FailureContext,
|
|
63
|
+
type FailureDecision,
|
|
64
|
+
} from './workflow/failure-policy.js';
|
|
65
|
+
export {
|
|
66
|
+
ParameterResolutionError,
|
|
67
|
+
firstLinkTarget,
|
|
68
|
+
firstUrl,
|
|
69
|
+
getSection,
|
|
70
|
+
graphFor,
|
|
71
|
+
interpolate,
|
|
72
|
+
parseKeyValueField,
|
|
73
|
+
resolveParameters,
|
|
74
|
+
type GraphReader,
|
|
75
|
+
type ParameterGraph,
|
|
76
|
+
type ResolvedParameters,
|
|
77
|
+
} from './workflow/parameters.js';
|
|
78
|
+
|
|
79
|
+
// Runs
|
|
80
|
+
export {
|
|
81
|
+
type ActorKey,
|
|
82
|
+
type Harness,
|
|
83
|
+
type PlatformEndpoints,
|
|
84
|
+
type RunEvent,
|
|
85
|
+
type RunInput,
|
|
86
|
+
type SessionResult,
|
|
87
|
+
type SessionStartParams,
|
|
88
|
+
type SessionState,
|
|
89
|
+
type Verdict,
|
|
90
|
+
} from './run/contract.js';
|
|
91
|
+
export { VERDICT_NUDGE, head, parseOutputs, parseVerdict, tail } from './run/verdict.js';
|
|
92
|
+
export { outcomeFromResult, threadStatusOf } from './run/outcome.js';
|
|
93
|
+
export { PlatformRunReporter, type FetchLike, type RunReporterOptions } from './run/reporter.js';
|
|
94
|
+
export { SkillNotReadable, composeSystemPrompt } from './run/system-prompt.js';
|
|
95
|
+
export { ENCRYPTED_ENCODING, EncryptingPayloadCodec, dataConverterFor, isEncrypted } from './temporal/payload-codec.js';
|
|
96
|
+
export { OUTCOME_MARKER, parseOutcomeLine, runStep, type RunStepDeps } from './cli/run-step.js';
|
|
97
|
+
export { formatEvent, runLocally, unrunnableLocally, type LocalRunOptions, type LocalRunResult } from './cli/run-local.js';
|
|
98
|
+
|
|
99
|
+
// Executors: the container half of the contract, on a host of its own.
|
|
100
|
+
export { DEPLOYMENT_LABEL, IMAGE_TAG_PREFIX, OWNER_LABEL, VOLUME_LABEL, createExecutor, harnessNodeCommand, imageTagFor, volumeNameFor, type Executor, type ExecutorOptions } from './executor/activities.js';
|
|
101
|
+
export { createInPlaceExecutor, type InPlaceExecutorOptions } from './executor/in-place.js';
|
|
102
|
+
export { EXECUTOR_INSTANCE, PERSONAL_EXECUTOR_API, personalExecutorHooks, runPersonalExecutor, runWorkerStarter, type PersonalExecutorOptions, type WaitingRun, type WorkerStarterOptions, type PersonalJob, type PersonalJobResult } from './executor/personal.js';
|
|
103
|
+
export type { ContainerDriver, ContainerLimits, ContainerMount, ContainerRunResult, ContainerRunSpec, ContainerVolume } from './executor/driver.js';
|
|
104
|
+
export { Slots } from './executor/slots.js';
|
|
105
|
+
export { MIN_REDACTABLE_LENGTH, NO_REDACTION, Redactor } from './shared/redact.js';
|
|
106
|
+
// The schedule grammar, also served browser-side as `@bevel-software/ael/schedule`.
|
|
107
|
+
export { durationMs, isCron, isDuration, isTimeZone, isoToInstant, parseIsoDateTime, wallClockToInstant, type IsoDateTime } from './shared/schedule.js';
|
|
108
|
+
export { renderTranscript, type TranscriptMeta } from './transcript/render.js';
|
|
109
|
+
|
|
110
|
+
// Work items: the knowledge-base node a multi-step run keeps up to date.
|
|
111
|
+
export {
|
|
112
|
+
DEFAULT_FIELDS,
|
|
113
|
+
appendToSection,
|
|
114
|
+
applyWorkItemUpdate,
|
|
115
|
+
filedAtOf,
|
|
116
|
+
getSection as getNodeSection,
|
|
117
|
+
headingName,
|
|
118
|
+
parseState,
|
|
119
|
+
parseWorkItem,
|
|
120
|
+
renderProgress,
|
|
121
|
+
serializeState,
|
|
122
|
+
setSection,
|
|
123
|
+
type FieldNames,
|
|
124
|
+
type ParsedWorkItem,
|
|
125
|
+
type WorkItemState,
|
|
126
|
+
type WorkItemUpdate,
|
|
127
|
+
} from './workitem/node.js';
|
|
128
|
+
|
|
129
|
+
// Harnesses
|
|
130
|
+
export { PiInProcessHarness, type PiInProcessOptions } from './harness/pi-in-process.js';
|
|
131
|
+
export { ClaudeCodeHarness, type ClaudeCodeOptions } from './harness/claude-code.js';
|
|
132
|
+
|
|
133
|
+
// The interpreter's contract with the platform and the executors. The
|
|
134
|
+
// workflows themselves are the `@bevel-software/ael/workflows` entry, which a
|
|
135
|
+
// Temporal worker bundles on its own.
|
|
136
|
+
export {
|
|
137
|
+
EXECUTOR_TASK_QUEUE,
|
|
138
|
+
INTERPRETER_STATE_KEYS,
|
|
139
|
+
resumableState,
|
|
140
|
+
MAX_STEP_RESTARTS,
|
|
141
|
+
PLATFORM_RESTART,
|
|
142
|
+
PLATFORM_TASK_QUEUE,
|
|
143
|
+
CONTAINER_STEP_HEARTBEAT_MS,
|
|
144
|
+
PERSONAL_EXECUTOR_SILENCE_MS,
|
|
145
|
+
personalExecutorQueue,
|
|
146
|
+
deploymentExecutorQueue,
|
|
147
|
+
namedExecutorQueue,
|
|
148
|
+
type Actor,
|
|
149
|
+
type AgentStepInput,
|
|
150
|
+
type AgentStepOutcome,
|
|
151
|
+
type ContainerStepInput,
|
|
152
|
+
type ExecutorActivities,
|
|
153
|
+
type PrepareContainerStepResult,
|
|
154
|
+
type InterpretInput,
|
|
155
|
+
type InterpretResult,
|
|
156
|
+
type InterpreterActivities,
|
|
157
|
+
type RunStatus,
|
|
158
|
+
type ToolChainInput,
|
|
159
|
+
type ToolChainOutcome,
|
|
160
|
+
type Transition,
|
|
161
|
+
} from './interpreter/contract.js';
|
|
162
|
+
// What a dispatcher answers to its `dispatcherState` query (a type alone: the
|
|
163
|
+
// workflow itself stays in the `./workflows` entry).
|
|
164
|
+
export type { DispatchSettings, DispatcherState } from './interpreter/dispatcher.workflow.js';
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import type { AgentDefinition } from '../agent/agent-file.js';
|
|
2
|
+
import type { ActorKey, PlatformEndpoints, RunInput, SessionState, Verdict } from '../run/contract.js';
|
|
3
|
+
import type { WorkflowDefinition } from '../workflow/definition.js';
|
|
4
|
+
import type { FieldNames } from '../workitem/node.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The seam between the interpreter (a Temporal workflow, deterministic, no
|
|
8
|
+
* I/O) and the world. Everything with a side effect is an activity the
|
|
9
|
+
* platform implements on its own worker, or an executor implements on its
|
|
10
|
+
* queue; the interpreter only ever sees the values they return. This file is
|
|
11
|
+
* types only: it is loaded inside the workflow sandbox too.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface Actor {
|
|
15
|
+
userId: string;
|
|
16
|
+
email: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** What starts one run of a workflow. */
|
|
20
|
+
export interface InterpretInput {
|
|
21
|
+
/** The run's thread id; the parent of every step thread. */
|
|
22
|
+
runId: string;
|
|
23
|
+
workflowPath: string;
|
|
24
|
+
actor: Actor;
|
|
25
|
+
/** The trigger's input: a webhook body, Run now text, the kb node, the `ask` prompt. */
|
|
26
|
+
input: RunInput;
|
|
27
|
+
/**
|
|
28
|
+
* The executor this run's container steps run on, by name (`laptop`,
|
|
29
|
+
* `gpu-1`), chosen where the run was started (Run now,
|
|
30
|
+
* `schedule_workflow`, the trigger's settings) — not by the file, so one
|
|
31
|
+
* workflow runs anywhere. Every step runs there and nowhere else, waiting
|
|
32
|
+
* while it is not connected. Absent: any of the deployment's executors.
|
|
33
|
+
*/
|
|
34
|
+
executor?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Whose executor `executor` is, resolved when the run started: one of the
|
|
37
|
+
* deployment's own, or the actor's personal one (no run reaches another
|
|
38
|
+
* person's). Absent with `executor` set: personal (a run started before
|
|
39
|
+
* deployment executors had names).
|
|
40
|
+
*/
|
|
41
|
+
executorKind?: 'deployment' | 'personal';
|
|
42
|
+
/**
|
|
43
|
+
* The node a kb trigger fired on: where the run's parameters are read from.
|
|
44
|
+
* A `status` condition claims it, so it is the work item too; a `change`
|
|
45
|
+
* condition only watches it — the run reads the node and writes nothing to
|
|
46
|
+
* it. Absent for every other start.
|
|
47
|
+
*/
|
|
48
|
+
subjectPath?: string;
|
|
49
|
+
/** The work item that tracks this run, when the workflow has more than one step. */
|
|
50
|
+
workItemPath: string | null;
|
|
51
|
+
/** The item's field names, when its trigger chose them (a kb trigger's status and owner fields); the defaults otherwise. */
|
|
52
|
+
workItemFields?: FieldNames;
|
|
53
|
+
/** STATE to start from (a released item keeps its counters cleared, but a resumed one keeps its outputs). */
|
|
54
|
+
initialState?: Record<string, unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* A thread whose stored session state the first step starts from: a
|
|
57
|
+
* continued `ask` conversation. The step still gets a thread of its own;
|
|
58
|
+
* only the harness's context is carried over.
|
|
59
|
+
*/
|
|
60
|
+
continueFromThreadId?: string;
|
|
61
|
+
/** The task queue container steps go to when no executor has claimed this run yet. */
|
|
62
|
+
executorQueue?: string;
|
|
63
|
+
/** The structure version a continued execution started under: the file is checked against it, not re-baselined. */
|
|
64
|
+
workflowVersion?: string;
|
|
65
|
+
/** How often a gate with a probe re-checks, in seconds. */
|
|
66
|
+
gatePollSeconds?: number;
|
|
67
|
+
/** How long a gate without a probe may wait for its signal, in seconds. */
|
|
68
|
+
gateTimeoutSeconds?: number;
|
|
69
|
+
/** Test hook: continue as new after this many steps, as if Temporal had suggested it; production leaves it unset. */
|
|
70
|
+
continueAfterSteps?: number;
|
|
71
|
+
/**
|
|
72
|
+
* Set by the interpreter alone when it continues as new inside a gate:
|
|
73
|
+
* the gate's deadline (epoch milliseconds), so a gate polled across
|
|
74
|
+
* executions still times out when it would have.
|
|
75
|
+
*/
|
|
76
|
+
gateDeadline?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Set by the interpreter alone when it continues as new while a step is
|
|
79
|
+
* paused on an account limit: the step and the thread it paused on, so
|
|
80
|
+
* the step goes on on that thread, from the state it saved, instead of a
|
|
81
|
+
* new one beside a thread left `waiting`.
|
|
82
|
+
*/
|
|
83
|
+
pausedThread?: { step: string; threadId: string };
|
|
84
|
+
/**
|
|
85
|
+
* Set by the interpreter alone when it continues as new: the steps this
|
|
86
|
+
* run has taken so far, so `MAX_RUN_STEPS` counts the whole run and a
|
|
87
|
+
* cycling workflow cannot start afresh with every execution.
|
|
88
|
+
*/
|
|
89
|
+
stepsTaken?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Set by the interpreter alone when it continues as new inside a probed
|
|
92
|
+
* gate: the probe's errors in a row so far, so the cap on consecutive
|
|
93
|
+
* errors counts the same across executions.
|
|
94
|
+
*/
|
|
95
|
+
probeErrors?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Set by the dispatcher alone on the runs it starts: the run counts
|
|
98
|
+
* against its trigger's cap, so it gives its slot back while it waits at a
|
|
99
|
+
* gate and asks for one before the step after it. A run nobody caps (Run
|
|
100
|
+
* now, a schedule) has no slot to give or ask for.
|
|
101
|
+
*/
|
|
102
|
+
capped?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Set by the interpreter alone when it continues as new at a gate: the
|
|
105
|
+
* run gave its slot back already, and asks for one when it goes on.
|
|
106
|
+
*/
|
|
107
|
+
slotGivenBack?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type RunStatus = 'done' | 'failed' | 'blocked' | 'cancelled';
|
|
111
|
+
|
|
112
|
+
export interface InterpretResult {
|
|
113
|
+
status: RunStatus;
|
|
114
|
+
state: Record<string, unknown>;
|
|
115
|
+
/** Why it stopped, for a failed or blocked run. */
|
|
116
|
+
reason?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A step attempt's input: everything an agent step needs, already interpolated. */
|
|
120
|
+
export interface AgentStepInput {
|
|
121
|
+
runId: string;
|
|
122
|
+
workflowPath: string;
|
|
123
|
+
stepName: string;
|
|
124
|
+
stepIndex: number;
|
|
125
|
+
attempt: number;
|
|
126
|
+
actor: Actor;
|
|
127
|
+
/** The `.agent` to run as; null means the deployment's default configuration. */
|
|
128
|
+
agentPath: string | null;
|
|
129
|
+
systemPrompt: { skill?: string; extend?: string } | null;
|
|
130
|
+
instructions: string;
|
|
131
|
+
verdict: string | null;
|
|
132
|
+
expectedOutputs: string[];
|
|
133
|
+
workItemPath: string | null;
|
|
134
|
+
input: RunInput;
|
|
135
|
+
state: Record<string, unknown>;
|
|
136
|
+
params: Record<string, string>;
|
|
137
|
+
/** The step's thread, opened before the step by `openStepThread` (or the resumed one): its identity is on record before anything runs. */
|
|
138
|
+
threadId: string;
|
|
139
|
+
/** A step thread to resume (the state is in the thread store), after a crash. */
|
|
140
|
+
resumeThreadId: string | null;
|
|
141
|
+
/** A thread whose stored session state this new step thread starts from; set on the first attempt of the first step only. */
|
|
142
|
+
continueFromThreadId: string | null;
|
|
143
|
+
/** The executor this run is pinned to, when a container step ran before. */
|
|
144
|
+
executorQueue: string | null;
|
|
145
|
+
environment: WorkflowDefinition['environment'];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type AgentStepOutcome =
|
|
149
|
+
| { kind: 'verdict'; verdict: Verdict; threadId: string; outputs: Record<string, unknown> | null; reason: string | null; executorQueue?: string }
|
|
150
|
+
| { kind: 'no-verdict'; threadId: string; reason: string; executorQueue?: string }
|
|
151
|
+
| { kind: 'timeout'; threadId: string | null; reason: string; executorQueue?: string }
|
|
152
|
+
| { kind: 'harness-error'; threadId: string | null; reason: string; executorQueue?: string }
|
|
153
|
+
| { kind: 'blocked'; threadId: string | null; reason: string }
|
|
154
|
+
/** The provider refused the ACCOUNT: the executor pauses, the step is re-run after `resetAt`. */
|
|
155
|
+
| { kind: 'account-limit'; threadId: string | null; resetAt: number | null; text: string; executorQueue?: string };
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Everything a container session needs, prepared by the platform before the
|
|
159
|
+
* step goes to an executor queue: the step, its thread, the run key, where
|
|
160
|
+
* the platform is from outside, the configuration, the composed messages and
|
|
161
|
+
* the environment, the state to resume from, and what to run in — the image,
|
|
162
|
+
* or the Dockerfile's content (the executor has no knowledge base to read it
|
|
163
|
+
* from). Serializable: it travels through Temporal.
|
|
164
|
+
*/
|
|
165
|
+
export interface ContainerStepInput {
|
|
166
|
+
step: AgentStepInput;
|
|
167
|
+
threadId: string;
|
|
168
|
+
actorKey: ActorKey;
|
|
169
|
+
platform: PlatformEndpoints & {
|
|
170
|
+
/** The platform's API root, where the session reports events and state. */
|
|
171
|
+
apiUrl: string;
|
|
172
|
+
};
|
|
173
|
+
agent: AgentDefinition;
|
|
174
|
+
systemPrompt: string;
|
|
175
|
+
/** The first user message, composed by the platform as it is for an in-process step. */
|
|
176
|
+
message: string;
|
|
177
|
+
/** The configuration's `env:`, resolved. Visible to the agent by construction; redacted from everything stored. */
|
|
178
|
+
env: Record<string, string>;
|
|
179
|
+
state: SessionState | null;
|
|
180
|
+
/** An `image` environment: the reference to pull. */
|
|
181
|
+
image: string | null;
|
|
182
|
+
/** A `dockerfile` environment: the file's content; the executor builds it, tagged by its hash. */
|
|
183
|
+
dockerfile: string | null;
|
|
184
|
+
expectVerdict: boolean;
|
|
185
|
+
timeoutMs: number;
|
|
186
|
+
/** The run moved executors: the workspace is new, and the session is told so. */
|
|
187
|
+
freshWorkspace: boolean;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export type PrepareContainerStepResult = { kind: 'ready'; input: ContainerStepInput } | { kind: 'blocked'; threadId: string | null; reason: string };
|
|
191
|
+
|
|
192
|
+
/** What an executor implements, on the shared queue and on its own. */
|
|
193
|
+
/** The signal a run sends its dispatcher as it ends, with its run id. A name, not a definition: the two workflows must not import each other. */
|
|
194
|
+
export const RUN_SETTLED_SIGNAL = 'runSettled';
|
|
195
|
+
/** The signal a capped run sends its dispatcher as it reaches a gate, with its run id: its slot under the cap is free, its key stays held. */
|
|
196
|
+
export const RUN_AT_GATE_SIGNAL = 'runAtGate';
|
|
197
|
+
/** The signal a capped run sends its dispatcher when it is past its gate and has work to do, with its run id: it waits for `SLOT_GRANTED_SIGNAL`. */
|
|
198
|
+
export const RUN_NEEDS_SLOT_SIGNAL = 'runNeedsSlot';
|
|
199
|
+
/** The dispatcher's answer to `RUN_NEEDS_SLOT_SIGNAL`, sent to the run: it holds a slot again. */
|
|
200
|
+
export const SLOT_GRANTED_SIGNAL = 'slotGranted';
|
|
201
|
+
|
|
202
|
+
export interface ExecutorActivities {
|
|
203
|
+
runContainerStep(input: ContainerStepInput): Promise<AgentStepOutcome>;
|
|
204
|
+
/** The run is over: its workspace volume on this executor goes. */
|
|
205
|
+
releaseRun(input: { runId: string }): Promise<void>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface ToolChainInput {
|
|
209
|
+
runId: string;
|
|
210
|
+
workflowPath: string;
|
|
211
|
+
stepName: string;
|
|
212
|
+
actor: Actor;
|
|
213
|
+
/** The chain's source, interpolated. */
|
|
214
|
+
chain: string;
|
|
215
|
+
state: Record<string, unknown>;
|
|
216
|
+
params: Record<string, string>;
|
|
217
|
+
workItemPath: string | null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export type ToolChainOutcome =
|
|
221
|
+
| { kind: 'outcome'; outcome: string; threadId: string | null }
|
|
222
|
+
| { kind: 'error'; reason: string; threadId: string | null };
|
|
223
|
+
|
|
224
|
+
/** What the interpreter tells the platform at every transition; the platform writes the work item and the run thread. */
|
|
225
|
+
export interface Transition {
|
|
226
|
+
runId: string;
|
|
227
|
+
workflowPath: string;
|
|
228
|
+
workItemPath: string | null;
|
|
229
|
+
workItemFields?: FieldNames;
|
|
230
|
+
actor: Actor;
|
|
231
|
+
/** The step the run is now in, or the terminal status. */
|
|
232
|
+
status: string;
|
|
233
|
+
stepName: string | null;
|
|
234
|
+
/** The run is parked in this step — on a gate, an account limit — until something outside it answers. */
|
|
235
|
+
waiting: boolean;
|
|
236
|
+
/** One line for the work item's Log, or null when the transition is silent. */
|
|
237
|
+
log: string | null;
|
|
238
|
+
state: Record<string, unknown>;
|
|
239
|
+
/** Thread ids per step attempt, for the work item's transcript links. */
|
|
240
|
+
threadIds: Record<string, string>;
|
|
241
|
+
happyPath: string[];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The activities the interpreter calls. The platform implements every one on
|
|
246
|
+
* its worker; an executor implements `runAgentStep` for container
|
|
247
|
+
* environments on its queue, with the same signature.
|
|
248
|
+
*/
|
|
249
|
+
export interface InterpreterActivities {
|
|
250
|
+
/** Read and parse the workflow from the default branch. Throws when missing or invalid. */
|
|
251
|
+
loadWorkflow(input: { workflowPath: string; actor: Actor }): Promise<WorkflowDefinition>;
|
|
252
|
+
/** Resolve the workflow's parameters from the node the run is about: the trigger's node, or the work item. */
|
|
253
|
+
resolveRunParameters(input: { definition: WorkflowDefinition; nodePath: string | null; actor: Actor }): Promise<{ values: Record<string, string>; missing: string[] }>;
|
|
254
|
+
/**
|
|
255
|
+
* The step's thread, a child of the run, before the step runs. The
|
|
256
|
+
* interpreter chooses its id (`threadId`), so a retried call — the row was
|
|
257
|
+
* inserted, the completion was lost — opens no second thread: the same id
|
|
258
|
+
* is answered.
|
|
259
|
+
*/
|
|
260
|
+
openStepThread(input: { runId: string; workflowPath: string; actor: Actor; stepName: string; attempt: number; workItemPath: string | null; threadId: string }): Promise<{ threadId: string }>;
|
|
261
|
+
/**
|
|
262
|
+
* A step thread whose activity never answered (the worker died, the task
|
|
263
|
+
* timed out): closed as failed with the reason, so the interpreter's
|
|
264
|
+
* harness error leaves no thread running. Nothing happens to a thread
|
|
265
|
+
* already closed.
|
|
266
|
+
*/
|
|
267
|
+
closeStepThread(input: { threadId: string; reason: string }): Promise<void>;
|
|
268
|
+
/**
|
|
269
|
+
* An in-process step ended, however it ended: every key it may still hold
|
|
270
|
+
* is revoked, what the key used is charged to the key the run names (read
|
|
271
|
+
* from the run's own thread, the one place it is kept), and the thread's
|
|
272
|
+
* status is written from the outcome — all retried until the stores
|
|
273
|
+
* answer, the charge made once however many times. The step's own
|
|
274
|
+
* activity revokes as it ends too, but cannot be retried, and writes no
|
|
275
|
+
* status and charges nothing; this one is the record.
|
|
276
|
+
*/
|
|
277
|
+
finishAgentStep(input: { threadId: string; outcome: AgentStepOutcome }): Promise<void>;
|
|
278
|
+
runAgentStep(input: AgentStepInput): Promise<AgentStepOutcome>;
|
|
279
|
+
/**
|
|
280
|
+
* A container step, before it goes to an executor: the thread, the run key,
|
|
281
|
+
* the configuration and its environment, the Dockerfile's content. Blocked
|
|
282
|
+
* when the configuration cannot run (an unreadable file, a missing vault entry).
|
|
283
|
+
*/
|
|
284
|
+
prepareContainerStep(input: AgentStepInput): Promise<PrepareContainerStepResult>;
|
|
285
|
+
/** A container step ended, however it ended: the run key is revoked, what it used charged once to the key the run names, the step thread closed. */
|
|
286
|
+
finishContainerStep(input: { threadId: string; keyId: string | null; outcome: AgentStepOutcome | null; error: string | null }): Promise<void>;
|
|
287
|
+
runToolChain(input: ToolChainInput): Promise<ToolChainOutcome>;
|
|
288
|
+
/** A gate's probe: a tool chain answering one of the gate's outcomes or `wait`. */
|
|
289
|
+
runProbe(input: ToolChainInput): Promise<ToolChainOutcome>;
|
|
290
|
+
recordTransition(input: Transition): Promise<void>;
|
|
291
|
+
notifyRunFailed(input: { runId: string; workflowPath: string; actor: Actor; status: RunStatus; reason: string }): Promise<void>;
|
|
292
|
+
/** A firing that will never run (replaced by a newer one, or waiting when its trigger was removed): its run thread is closed with the reason. */
|
|
293
|
+
withdrawRun(input: { runId: string; reason: string }): Promise<void>;
|
|
294
|
+
/**
|
|
295
|
+
* Which of these runs still have an execution running. The dispatcher asks
|
|
296
|
+
* about the runs it inherited from the execution before it: it started none
|
|
297
|
+
* of them, so their end reaches it only as the run's own signal — and a run
|
|
298
|
+
* terminated, or ended without finishing, sends none.
|
|
299
|
+
*/
|
|
300
|
+
runsInFlight(input: { runIds: string[] }): Promise<{ running: string[] }>;
|
|
301
|
+
/**
|
|
302
|
+
* A schedule fired: create the run (its thread, its work item) and start
|
|
303
|
+
* the interpreter. A Temporal schedule can only start a workflow, so the
|
|
304
|
+
* `startScheduledRun` workflow calls this, then runs the interpreter on
|
|
305
|
+
* the answer as its child and waits for it. Null when the trigger is gone
|
|
306
|
+
* or paused: nothing to run. The workflow chooses the run's id (`runId`,
|
|
307
|
+
* replay-safe), so a retried call — the thread and the node were made,
|
|
308
|
+
* the completion was lost — prepares the same run, not a second one
|
|
309
|
+
* beside a first left running.
|
|
310
|
+
*/
|
|
311
|
+
startTriggeredRun(input: { triggerId: string; firedAt: string; runId: string }): Promise<{ run: InterpretInput | null }>;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The interpreter's own STATE keys. A step's outputs may never write them. */
|
|
315
|
+
export const INTERPRETER_STATE_KEYS = [
|
|
316
|
+
'step',
|
|
317
|
+
'verdict',
|
|
318
|
+
'attempt',
|
|
319
|
+
'attempts',
|
|
320
|
+
'retries',
|
|
321
|
+
'entries',
|
|
322
|
+
'assignee',
|
|
323
|
+
'blocker',
|
|
324
|
+
'lastError',
|
|
325
|
+
'sessionIds',
|
|
326
|
+
'workflowId',
|
|
327
|
+
'workflowVersion',
|
|
328
|
+
'resumeStep',
|
|
329
|
+
] as const;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* What a run takes over from a work item an earlier run left its STATE on (a
|
|
333
|
+
* ticket released back to its trigger's status): the steps' own outputs (a
|
|
334
|
+
* branch, a pull request's address) and the step the earlier run stopped in,
|
|
335
|
+
* as `resumeStep`. Everything a runner keeps for itself is left behind —
|
|
336
|
+
* counters, sessions, the last error, and the keys the workflow reserves — so
|
|
337
|
+
* the new run has its budgets whole and resumes no session of another run.
|
|
338
|
+
* The position is `resumeStep` when the earlier run recorded one, else `step`
|
|
339
|
+
* (a runner that kept the step's name there); the interpreter starts at the
|
|
340
|
+
* top when no step has that name.
|
|
341
|
+
*/
|
|
342
|
+
export function resumableState(state: Record<string, unknown>, reservedKeys: readonly string[] = []): Record<string, unknown> {
|
|
343
|
+
const own = new Set<string>([...INTERPRETER_STATE_KEYS, ...reservedKeys]);
|
|
344
|
+
const kept = Object.fromEntries(Object.entries(state).filter(([key]) => !own.has(key)));
|
|
345
|
+
const at = typeof state.resumeStep === 'string' ? state.resumeStep : typeof state.step === 'string' ? state.step : null;
|
|
346
|
+
return at ? { ...kept, resumeStep: at } : kept;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* The most steps one run executes before it is blocked as looping: a file
|
|
351
|
+
* whose `then:` routing cycles while every step passes would otherwise start
|
|
352
|
+
* paid model sessions without end. The interpreter's guard and the local
|
|
353
|
+
* runner's, one number.
|
|
354
|
+
*/
|
|
355
|
+
export const MAX_RUN_STEPS = 500;
|
|
356
|
+
|
|
357
|
+
/** The queue the platform's own worker polls: `environment: none` steps and every non-step activity. */
|
|
358
|
+
export const PLATFORM_TASK_QUEUE = 'platform';
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* The failure an in-process step raises when the platform stopped under it (a deploy, a restart): not the step's
|
|
362
|
+
* own failure, so the interpreter runs it again on the worker that comes up, on its own thread, from its saved
|
|
363
|
+
* session (`MAX_STEP_RESTARTS` times at most). A worker that died without a word is the same thing, seen as a
|
|
364
|
+
* missed heartbeat.
|
|
365
|
+
*/
|
|
366
|
+
export const PLATFORM_RESTART = 'PlatformRestart';
|
|
367
|
+
/** How often one step goes on after the platform restarted under it, before the restart counts as its failure. */
|
|
368
|
+
export const MAX_STEP_RESTARTS = 2;
|
|
369
|
+
/** The queue the deployment's executors share: a step no run has pinned or named an executor for. */
|
|
370
|
+
export const EXECUTOR_TASK_QUEUE = 'executors';
|
|
371
|
+
|
|
372
|
+
/** The queue of the deployment's executor by that name: the runs pinned to it, and the runs that name it. */
|
|
373
|
+
export function deploymentExecutorQueue(name: string): string {
|
|
374
|
+
return `executor-${name}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The queue a run that names an executor sends every container step to. */
|
|
378
|
+
export function namedExecutorQueue(input: Pick<InterpretInput, 'executor' | 'executorKind' | 'actor'>): string | null {
|
|
379
|
+
if (!input.executor) return null;
|
|
380
|
+
return input.executorKind === 'deployment' ? deploymentExecutorQueue(input.executor) : personalExecutorQueue(input.actor.userId, input.executor);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The queue of a person's executor by that name. Derived from the run's own
|
|
385
|
+
* person, never from anything a file says about whom: a workflow naming
|
|
386
|
+
* `executor: laptop` reaches the laptop of whoever the run acts for, and no
|
|
387
|
+
* file can send a step to somebody else's machine.
|
|
388
|
+
*/
|
|
389
|
+
export function personalExecutorQueue(userId: string, name: string): string {
|
|
390
|
+
return `personal-${userId}-${name}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* How long a step an executor took may go without a "still alive" before the
|
|
395
|
+
* platform gives it up (the executor reports every 30 s). Every executor
|
|
396
|
+
* reaches the platform over HTTPS, personal or the deployment's. Under
|
|
397
|
+
* the container step's heartbeat limit on purpose, so the platform's own
|
|
398
|
+
* reason ("stopped answering") is the one the run records, not Temporal's.
|
|
399
|
+
*/
|
|
400
|
+
export const PERSONAL_EXECUTOR_SILENCE_MS = 90_000;
|
|
401
|
+
|
|
402
|
+
/** A container step's heartbeat limit on Temporal; above {@link PERSONAL_EXECUTOR_SILENCE_MS}. */
|
|
403
|
+
export const CONTAINER_STEP_HEARTBEAT_MS = 2 * 60_000;
|