Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1082,7 +1082,7 @@ jobs:
"$playbook/events")" = 200
ended_runs closing 2 150
test "$(jq --compact-output '[.runs[] | [.status, .started_by]]' runs.json)" = \
'[["succeeded","playbook:smoke"],["succeeded","playbook:smoke"]]'
'[["succeeded",{"id":"playbook:smoke","name":"smoke"}],["succeeded",{"id":"playbook:smoke","name":"smoke"}]]'
for run in $(jq --raw-output '.runs[].run_id' runs.json); do
test "$(call "history-$run.json" "$playbook/runs/$run/history")" = 200
test "$(call "run-$run.json" "$playbook/runs/$run")" = 200
Expand Down
2 changes: 1 addition & 1 deletion capabilities/computation/src/testing/computation-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const run: RunContext = {
id: '0199a3c4-7d2e-7c1a-9b3f-2f1e0d9c8b7a',
org: 'acme',
playbook: 'alpha',
caller: { id: 'acme-admin', org: 'acme', permissions: allPermissions, playbooks: '*' },
caller: { id: 'acme-admin', name: 'acme-admin', org: 'acme', permissions: allPermissions, playbooks: '*' },
definition: { name: 'pace', version: 1 },
journal: recordingJournal(),
lineage: { startId: '5d0e9f6a-1b2c-5d3e-8f4a-6b7c8d9e0f1a', correlationId: '0199a3c4-7d2e-7c1a-9b3f-2f1e0d9c8b7a' },
Expand Down
2 changes: 1 addition & 1 deletion capabilities/coordination/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Parsing keeps the document with the locator of its YAML and its compiled input s

## A run of a workflow

`run_definition` of a workflow, once [its input schema](#the-input-of-a-workflow) takes the input of a caller, starts its run with the `started` input: the document, each expression in it the JavaScript its save's check stripped of its types where stripping changed it, at its place (`runnableDocument`, `src/document/workflow-document.ts`), so the engine evaluates what the check passed and strips nothing, the input, the run's limits, a random seed for the draws of the run, and as attributes the org, the playbook, the run id, the definition's name and version, the caller who started it, its reaction depth, `depth`, its call depth, `call_depth`, the call it answers, `called_by`, the trigger that started it, `trigger`, the interval after which the run of each step that calls a definition whose runs resume may be resumed, by the step's reference, `resumes_after_ms_by_task`, through the run context's `resumesAfterOf`, and `lineage`: the id of the `run_started` that began the run and the run at the top of its tree, which the workflow host writes with each record of the run (`@on.auto/workflow-host`). The host builds each record's context from these attributes (`runLogContextOf`), so every record of the run log carries the run's definition, its caller as `by`, its chain and the time of its input, and the engine's state format governs its data alone. The run log is `run-logs/<run id>` under the playbook, beside the run's own stream, `runs/<run id>`, which records that the run finishes later, with an empty record, so the run stays `started` until its workflow settles it; a workflow that ends in its first input settles the run before the deferral, and `run_definition` answers its ending. The limits name, for each task that calls `run_definition` with a `type` and a `name` written out, the longest a run of that definition may take, for the task's `input` when it is written out with no expression in it, `{}` when it is left out, through the run context's `longestRunFor`, and for any input otherwise, through its `longestRunOf`, plus `callMarginMs` (`src/runs/call-limits.ts`), read when the run starts, so a call of a prediction function waits 10 s and a minute for a question written out and its build's longest run and a minute for a build or an input computed at run time, which `src/capability/prediction-calls.test.ts` runs against the prediction capability itself; whether the input holds an expression is the engine's own walk of a template, `holdsAnExpression`; every other call has `longestCallMs`. A retry with the run id of a run that is going answers the run as it stands. A run never starts twice in one log, so a retry with the run id of a run that ended without a final result, `unavailable` or `failed`, is rejected with `conflict`: run the workflow again under a new run id. While the server is stopping, `run_definition` of a workflow is rejected with `unavailable`.
`run_definition` of a workflow, once [its input schema](#the-input-of-a-workflow) takes the input of a caller, starts its run with the `started` input: the document, each expression in it the JavaScript its save's check stripped of its types where stripping changed it, at its place (`runnableDocument`, `src/document/workflow-document.ts`), so the engine evaluates what the check passed and strips nothing, the input, the run's limits, a random seed for the draws of the run, and as attributes the org, the playbook, the run id, the definition's name and version, the caller who started it, with its name, which every run a call of the workflow starts records as its `started_by`, its reaction depth, `depth`, its call depth, `call_depth`, the call it answers, `called_by`, the trigger that started it, `trigger`, the interval after which the run of each step that calls a definition whose runs resume may be resumed, by the step's reference, `resumes_after_ms_by_task`, through the run context's `resumesAfterOf`, and `lineage`: the id of the `run_started` that began the run and the run at the top of its tree, which the workflow host writes with each record of the run (`@on.auto/workflow-host`). The host builds each record's context from these attributes (`runLogContextOf`), so every record of the run log carries the run's definition, its caller as `by`, its chain and the time of its input, and the engine's state format governs its data alone. The run log is `run-logs/<run id>` under the playbook, beside the run's own stream, `runs/<run id>`, which records that the run finishes later, with an empty record, so the run stays `started` until its workflow settles it; a workflow that ends in its first input settles the run before the deferral, and `run_definition` answers its ending. The limits name, for each task that calls `run_definition` with a `type` and a `name` written out, the longest a run of that definition may take, for the task's `input` when it is written out with no expression in it, `{}` when it is left out, through the run context's `longestRunFor`, and for any input otherwise, through its `longestRunOf`, plus `callMarginMs` (`src/runs/call-limits.ts`), read when the run starts, so a call of a prediction function waits 10 s and a minute for a question written out and its build's longest run and a minute for a build or an input computed at run time, which `src/capability/prediction-calls.test.ts` runs against the prediction capability itself; whether the input holds an expression is the engine's own walk of a template, `holdsAnExpression`; every other call has `longestCallMs`. A retry with the run id of a run that is going answers the run as it stands. A run never starts twice in one log, so a retry with the run id of a run that ended without a final result, `unavailable` or `failed`, is rejected with `conflict`: run the workflow again under a new run id. While the server is stopping, `run_definition` of a workflow is rejected with `unavailable`.

`send_run_event` gives the run an `event_received` input, with an id made when the event has none, the time it was sent, and, when it has no source, `/callers/<caller id>` of the caller who sent it, a source under a prefix the playbook keeps for itself, so that a sent event always says where it came from and no event from outside poses as one. An event the run took before is answered as delivered, since an event is taken once by its id; a run that has not started yet, or has ended, is `not_found`; and a run that cannot take the event at that moment, because its log kept changing or the server is stopping, is `unavailable`. Since a `listen` filter matches on the event's attributes, an event may not take a type or a source of what the playbook records itself: `refusingThePlaybooksOwnAttributes` of `@on.auto/definitions` refuses them with `invalid_input` at `/event/type` and `/event/source`, as `publish_event` does. Its `source` is a URI reference that is not empty, `EventSourceSchema`, and its text follows the rules of a published event too, through `refusingForbiddenCharacters` and `refusingBlankText` of `@on.auto/definitions`: no control character, lone surrogate or noncharacter, and a type, id and subject that hold a character that is not a space.

Expand Down
8 changes: 7 additions & 1 deletion capabilities/coordination/src/calls/function-calls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ const run = {
playbook: 'alpha',
run_id: workflowRun,
definition: { name: 'triage', version: 2 },
caller: { id: 'acme-admin', org: 'acme', permissions: ['playbook:read', 'playbook:write'], playbooks: '*' },
caller: {
id: 'acme-admin',
name: 'acme-admin',
org: 'acme',
permissions: ['playbook:read', 'playbook:write'],
playbooks: '*',
},
},
};

Expand Down
8 changes: 7 additions & 1 deletion capabilities/coordination/src/calls/waiting-calls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ const run = {
playbook: 'alpha',
run_id: workflowRun,
definition: { name: 'triage', version: 2 },
caller: { id: 'acme-admin', org: 'acme', permissions: ['playbook:read', 'playbook:write'], playbooks: '*' },
caller: {
id: 'acme-admin',
name: 'acme-admin',
org: 'acme',
permissions: ['playbook:read', 'playbook:write'],
playbooks: '*',
},
call_depth: 3,
},
};
Expand Down
8 changes: 7 additions & 1 deletion capabilities/coordination/src/capability/started-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ describe('the run a run of a workflow starts', () => {
playbook: 'alpha',
run_id: runId,
definition: { name: 'flow', version: 1 },
caller: { id: 'acme-admin', org: 'acme', playbooks: '*', permissions: ['playbook:read', 'playbook:write'] },
caller: {
id: 'acme-admin',
name: 'acme-admin',
org: 'acme',
playbooks: '*',
permissions: ['playbook:read', 'playbook:write'],
},
depth: 0,
call_depth: 0,
lineage: { correlation: runId },
Expand Down
1 change: 1 addition & 0 deletions capabilities/coordination/src/runs/run-attributes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const attributes: RunAttributes = {
definition: { name: 'payment', version: 2 },
caller: {
id: 'operator',
name: 'operator',
org: 'acme',
permissions: ['org:read', 'org:write', 'playbook:read', 'playbook:write'],
playbooks: ['accounts'],
Expand Down
1 change: 1 addition & 0 deletions capabilities/coordination/src/testing/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface InterpretOptions {

export const acmeCaller: CallerIdentity = {
id: 'acme-admin',
name: 'acme-admin',
org: 'acme',
permissions: ['playbook:read', 'playbook:write'],
playbooks: '*',
Expand Down
2 changes: 1 addition & 1 deletion capabilities/interaction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Attempts follow `attemptSchedule` (`src/schedule/attempt-schedule.ts`): five, th

## Answering and listing

`defineAnswerInteraction(capabilities)` is `answer_interaction`, `POST /runs/{run_id}/answer` under `playbook:write`, for a request a function's run waits on, made over the capabilities the server composes. The answer is checked against the answer schema the request recorded, 64 KiB as JSON and 512 levels at most, its issues in the words of `wordedIssuesOf` of `@on.auto/definitions` with pointers under `/answer` (`src/requests/answer-check.ts`), then against the `answerIssues` of the capability of the run that asked, given the run's work, so a prediction build refuses a candidate it was never sent, each issue `invalid_input` under `/answer`, and recorded on the run as one fact, `request_answered { answer, claimed_for? }`, its actor the caller and its time its own, through `playbookBoundAnswerer` of `@on.auto/definitions`; `claimed_for` is what the caller says it answers for, at most 256 bytes and never checked. The operation answers the recorded fact, `{ run_id, answer_id, answer, claimed_for?, reply?, answered_by, answered_at }`. The run decider takes one answer: the same answer again records nothing and answers the fact the run took, and another one is `conflict`, as is an answer to a request that has ended or whose run was asked to cancel. The run is settled from the fact by the open requests' due work within the host's sweep, a second by default, `succeeded` with the answer as its output and an empty record, as the caller that answered; a settlement that says nobody answered is refused once the run took an answer, and a cancel settles with it. `listInteractions` is `list_interactions`, `GET /interactions` under `playbook:read`: the open requests of the playbook, newest first, filtered by `to`, `type` and `function`, the last naming one function with `type`, since a name is unique within its type, in pages, each with the `type` of the function that asked, its `delivery`, its `answer_schema`, and for a request whose function reads replies its `conversation`, `answerer` and `reply_refusals`.
`defineAnswerInteraction(capabilities)` is `answer_interaction`, `POST /runs/{run_id}/answer` under `playbook:write`, for a request a function's run waits on, made over the capabilities the server composes. The answer is checked against the answer schema the request recorded, 64 KiB as JSON and 512 levels at most, its issues in the words of `wordedIssuesOf` of `@on.auto/definitions` with pointers under `/answer` (`src/requests/answer-check.ts`), then against the `answerIssues` of the capability of the run that asked, given the run's work, so a prediction build refuses a candidate it was never sent, each issue `invalid_input` under `/answer`, and recorded on the run as one fact, `request_answered { answer, claimed_for? }`, its actor the caller and its time its own, through `playbookBoundAnswerer` of `@on.auto/definitions`; `claimed_for` is what the caller says it answers for, at most 256 bytes and never checked. The operation answers the recorded fact, `{ run_id, answer_id, answer, claimed_for?, reply?, answered_by, answered_at }`, `answered_by` the caller who answered, by `id` and `name`, which `request_answered` holds beside the context's `by`, the id alone, and for a reply the playbook acting as itself. The run decider takes one answer: the same answer again records nothing and answers the fact the run took, and another one is `conflict`, as is an answer to a request that has ended or whose run was asked to cancel. The run is settled from the fact by the open requests' due work within the host's sweep, a second by default, `succeeded` with the answer as its output and an empty record, as the caller that answered; a settlement that says nobody answered is refused once the run took an answer, and a cancel settles with it. `listInteractions` is `list_interactions`, `GET /interactions` under `playbook:read`: the open requests of the playbook, newest first, filtered by `to`, `type` and `function`, the last naming one function with `type`, since a name is unique within its type, in pages, each with the `type` of the function that asked, its `delivery`, its `answer_schema`, and for a request whose function reads replies its `conversation`, `answerer` and `reply_refusals`.

## Words

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const asked = { run: address, kind: 'requested', reason: 'Not needed any more' }

const cancelledAsAsked = { status: 'rejected', reason: 'cancelled', kind: 'requested', detail: asked.reason };

const taken = { answer: 'yes', reply: takenReply, by: 'playbook:alpha', at };
const taken = { answer: 'yes', reply: takenReply, by: { id: 'playbook:alpha', name: 'alpha' }, at };

const delivered = {
type: 'delivery_succeeded',
Expand Down
4 changes: 3 additions & 1 deletion capabilities/interaction/src/replies/reply-races.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ describe('a reply to a request answered while the conversation was read', () =>

expect(
(await recordsOf(playbook.ledger, 'request_answered', 'reply_refused')).map(({ type, data }) => [type, data]),
).toEqual([['request_answered', { answer: { choice: 'reject' } }]]);
).toEqual([
['request_answered', { answer: { choice: 'reject' }, answered_by: { id: 'acme-admin', name: 'acme-admin' } }],
]);
expect(await playbook.runOf(runId)).toMatchObject({ output: { output: { choice: 'reject' } } });
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ async function askedApproval(document = approvalDocument()) {
return { playbook, asked };
}

const thePlaybook = { id: 'playbook:alpha', name: 'alpha' };

describe('a request asked through the inbox', () => {
it('waits as a started run, listed among the open requests of the playbook', async () => {
const { playbook, asked } = await askedApproval();
Expand Down Expand Up @@ -115,7 +117,7 @@ describe('an answer to a request', () => {
answer_id: someText,
answer: { choice: 'approve' },
claimed_for: 'ada',
answered_by: acmeAdmin.id,
answered_by: { id: acmeAdmin.id, name: acmeAdmin.name },
answered_at: someText,
},
});
Expand Down Expand Up @@ -153,7 +155,7 @@ describe('an answer to a request a reply answered first', () => {

expect(await playbook.call(answer, { run_id: askedRunId, answer: { choice: 'approve' } })).toMatchObject({
status: 'succeeded',
output: { answer: { choice: 'approve' }, reply: takenReply, answered_by: 'playbook:alpha' },
output: { answer: { choice: 'approve' }, reply: takenReply, answered_by: thePlaybook },
});
});
});
Expand Down
6 changes: 4 additions & 2 deletions capabilities/interaction/src/requests/answer-interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
Conflict,
InvalidInput,
NotFound,
RecordedCallerSchema,
defineCommand,
recordedCallerOf,
} from '@on.auto/operations';
import { Effect, Schema } from 'effect';

Expand Down Expand Up @@ -54,7 +56,7 @@ const AnsweredRequestSchema = Schema.Struct({
description: 'The reply that brought the answer, when a reply did',
}),
),
answered_by: Schema.String.annotate({ description: 'The id of the caller the answer came from' }),
answered_by: RecordedCallerSchema.annotate({ description: 'The caller who answered it, by id and name' }),
answered_at: Schema.String.annotate({ description: 'When the answer was recorded, in ISO 8601 UTC' }),
}).annotate({ identifier: 'AnsweredRequest', description: 'The answer a request took, as its run recorded it' });

Expand Down Expand Up @@ -115,7 +117,7 @@ function shownOf(id: string, { id: answerId, answer, claimedFor, reply, by, at }
}

const answered = Effect.fnUntraced(function* (checks: AnswerChecks, { id, answer, claimedFor }: Answering) {
const { id: answeredBy } = yield* Caller;
const answeredBy = recordedCallerOf(yield* Caller);
const row = yield* openRequestRowIn(id);
const run = yield* recordedRunInPlaybook(yield* PlaybookReader, id);
if (run === undefined) {
Expand Down
6 changes: 5 additions & 1 deletion capabilities/interaction/src/requests/open-requests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,11 @@ const delivered: Fact = { type: 'delivery_succeeded', data: { number: 1, ...answ

const taken: Fact = {
type: 'request_answered',
data: { answer: { choice: 'approve' }, reply: { id: '1699.2', sender: 'ada' } },
data: {
answer: { choice: 'approve' },
reply: { id: '1699.2', sender: 'ada' },
answered_by: { id: 'playbook:alpha', name: 'alpha' },
},
};

describe('the open request of a run asked to cancel', () => {
Expand Down
2 changes: 1 addition & 1 deletion capabilities/interaction/src/run/interaction-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const aRun: RunContext = {
id: runId,
org: 'acme',
playbook: 'alpha',
caller: { id: 'acme-admin', org: 'acme', permissions: allPermissions, playbooks: '*' },
caller: { id: 'acme-admin', name: 'acme-admin', org: 'acme', permissions: allPermissions, playbooks: '*' },
definition: { name: 'approve-brief', version: 1 },
journal: recordingJournal(),
lineage: { startId: '5d0e9f6a-1b2c-5d3e-8f4a-6b7c8d9e0f1a', correlationId: runId },
Expand Down
2 changes: 1 addition & 1 deletion capabilities/interaction/src/schedule/request-endings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function expiredSettlement({ expires_at: expiresAt }: { readonly expires_
}

export function answeredSettlement({ answer, by }: Pick<RecordedAnswer, 'answer' | 'by'>): Settlement {
return { status: 'succeeded', output: answer, record: {}, by };
return { status: 'succeeded', output: answer, record: {}, by: by.id };
}

export function deliveredSettlement(at: string): Settlement {
Expand Down
2 changes: 1 addition & 1 deletion capabilities/interaction/src/testing/call-runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function contextOf(journal: RecordingJournal): RunContext {
id: callRunId,
org: 'acme',
playbook: 'alpha',
caller: { id: 'acme-admin', org: 'acme', permissions: allPermissions, playbooks: '*' },
caller: { id: 'acme-admin', name: 'acme-admin', org: 'acme', permissions: allPermissions, playbooks: '*' },
definition: { name: 'thread-replies', version: 1 },
journal,
lineage: { startId: '5d0e9f6a-1b2c-5d3e-8f4a-6b7c8d9e0f1a', correlationId: callRunId },
Expand Down
Loading
Loading