anbaric-tsapi 1.18.1 → 1.20.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/package.json +1 -1
- package/src/api/actions/Await.ts +37 -0
- package/src/api/actions/WaitForInput.ts +43 -0
- package/src/api/actors/Actor.ts +1 -1
- package/src/api/actors/SystemActor.ts +1 -1
- package/src/api/actors/agents/Agent.ts +3 -3
- package/src/api/cloud/WorkflowDefinition.ts +31 -3
- package/src/api/jobs/Job.ts +24 -2
- package/src/api/jobs/JobPersistence.ts +11 -2
- package/src/api/jobs/JobSerialization.ts +11 -1
- package/src/api/sessions/SessionResolver.ts +20 -0
- package/src/api/states/State.ts +4 -3
- package/src/index.ts +3 -0
package/package.json
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {Job} from "../jobs/Job";
|
|
2
|
+
import {AwaitParty, WaitForInput} from "./WaitForInput";
|
|
3
|
+
|
|
4
|
+
/* A pause point in a state's action list. When a job reaches an Await it runs
|
|
5
|
+
nothing: the job is parked in the "Awaiting input" status and is not
|
|
6
|
+
re-enqueued until an update to its properties arrives (a human filling in a
|
|
7
|
+
form, say). An Await carries no actor - which actor eventually provides the
|
|
8
|
+
input is not known until it does. It describes what is being waited on
|
|
9
|
+
through the WaitForInput it assembles: the fields expected, a resolve URL
|
|
10
|
+
(static, or derived per job) and any extra metadata. That WaitForInput is
|
|
11
|
+
stored against the job and audited, and cleared once a transition moves the
|
|
12
|
+
job on to another state. */
|
|
13
|
+
class Await {
|
|
14
|
+
|
|
15
|
+
readonly id : string;
|
|
16
|
+
name : string;
|
|
17
|
+
description : string;
|
|
18
|
+
waitingFor? : AwaitParty;
|
|
19
|
+
fields : Array<string> = [];
|
|
20
|
+
resolveUrl : string | ((job : Job) => string) = "";
|
|
21
|
+
metadata : (job : Job) => Map<string, any> = (_job : Job) => new Map();
|
|
22
|
+
|
|
23
|
+
constructor(name : string, waitingFor? : AwaitParty, description : string = "", id : string = crypto.randomUUID()) {
|
|
24
|
+
this.name = name;
|
|
25
|
+
this.waitingFor = waitingFor;
|
|
26
|
+
this.description = description;
|
|
27
|
+
this.id = id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
waitForInput(job : Job) : WaitForInput {
|
|
31
|
+
const resolveUrl = typeof this.resolveUrl === "function" ? this.resolveUrl(job) : this.resolveUrl;
|
|
32
|
+
return new WaitForInput(this.fields, resolveUrl, this.metadata(job), this.waitingFor);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { Await }
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
type AwaitParty = "HUMAN" | "EXTERNAL_SYSTEM";
|
|
2
|
+
|
|
3
|
+
/* What an Await is waiting for: which kind of party (a human or an external
|
|
4
|
+
system), the input fields it expects, a URL a client can send the user to in
|
|
5
|
+
order to provide them, and any extra metadata. A pure data class - it carries
|
|
6
|
+
no behaviour. */
|
|
7
|
+
class WaitForInput {
|
|
8
|
+
|
|
9
|
+
fields : Array<string>;
|
|
10
|
+
resolveUrl : string;
|
|
11
|
+
metadataMap : Map<string, any>;
|
|
12
|
+
waitingFor? : AwaitParty;
|
|
13
|
+
|
|
14
|
+
constructor(fields : Array<string> = [], resolveUrl : string = "",
|
|
15
|
+
metadataMap : Map<string, any> = new Map(), waitingFor? : AwaitParty) {
|
|
16
|
+
this.fields = fields;
|
|
17
|
+
this.resolveUrl = resolveUrl;
|
|
18
|
+
this.metadataMap = metadataMap;
|
|
19
|
+
this.waitingFor = waitingFor;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type SerializedWaitForInput = {
|
|
25
|
+
fields : Array<string>,
|
|
26
|
+
resolveUrl : string,
|
|
27
|
+
metadataMap : Record<string, any>,
|
|
28
|
+
waitingFor? : AwaitParty,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const serializeWaitForInput = (wait : WaitForInput) : SerializedWaitForInput => ({
|
|
32
|
+
fields: wait.fields,
|
|
33
|
+
resolveUrl: wait.resolveUrl,
|
|
34
|
+
metadataMap: Object.fromEntries(wait.metadataMap),
|
|
35
|
+
waitingFor: wait.waitingFor,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const deserializeWaitForInput = (serialized : SerializedWaitForInput) : WaitForInput =>
|
|
39
|
+
new WaitForInput(serialized.fields ?? [], serialized.resolveUrl ?? "",
|
|
40
|
+
new Map(Object.entries(serialized.metadataMap ?? {})), serialized.waitingFor);
|
|
41
|
+
|
|
42
|
+
export { WaitForInput, serializeWaitForInput, deserializeWaitForInput };
|
|
43
|
+
export type { SerializedWaitForInput, AwaitParty };
|
package/src/api/actors/Actor.ts
CHANGED
|
@@ -8,12 +8,12 @@ class Agent implements Actor {
|
|
|
8
8
|
|
|
9
9
|
readonly type = "AGENT" as const;
|
|
10
10
|
readonly id : string;
|
|
11
|
-
readonly
|
|
11
|
+
readonly roles : Array<string>;
|
|
12
12
|
readonly client : Agent.Client;
|
|
13
13
|
|
|
14
|
-
constructor(id : string,
|
|
14
|
+
constructor(id : string, roles : Array<string> | string, client : Agent.Client) {
|
|
15
15
|
this.id = id;
|
|
16
|
-
this.
|
|
16
|
+
this.roles = typeof roles === "string" ? [roles] : roles;
|
|
17
17
|
this.client = client;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import {State} from "../states/State";
|
|
2
|
+
import {PropertyDefinition} from "../jobs/PropertyDefinition";
|
|
3
|
+
import {Await} from "../actions/Await";
|
|
4
|
+
|
|
1
5
|
/* The serialisable graph of a state machine: its states, each state's actions
|
|
2
6
|
(identity only — the run/predicate bodies aren't persistable) and
|
|
3
7
|
transitions. Recorded as the details of an audit record when a state machine
|
|
4
|
-
initialises.
|
|
8
|
+
initialises. Awaits are pause points rather than actors acting, so they are
|
|
9
|
+
not part of the graph - awaiting jobs are surfaced separately. */
|
|
5
10
|
type WorkflowDefinition = {
|
|
6
11
|
|
|
7
12
|
workflowId : string,
|
|
@@ -10,10 +15,33 @@ type WorkflowDefinition = {
|
|
|
10
15
|
states : Array<{
|
|
11
16
|
id : string,
|
|
12
17
|
isTerminal : boolean,
|
|
13
|
-
actions : Array<{ name : string, description : string, actor : { id : string, type : string,
|
|
18
|
+
actions : Array<{ name : string, description : string, actor : { id : string, type : string, roles : Array<string> } }>,
|
|
14
19
|
transitions : Array<{ to : string }>,
|
|
15
20
|
}>,
|
|
16
21
|
|
|
17
22
|
};
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
namespace WorkflowDefinition {
|
|
25
|
+
|
|
26
|
+
export const describe = (workflowId : string, startState : string,
|
|
27
|
+
states : Array<State>, dataSchema : Array<PropertyDefinition>) : WorkflowDefinition => ({
|
|
28
|
+
workflowId,
|
|
29
|
+
startState,
|
|
30
|
+
dataSchema: dataSchema.map(property => ({ id: property.id, required: property.required })),
|
|
31
|
+
states: states.map(state => ({
|
|
32
|
+
id: state.id,
|
|
33
|
+
isTerminal: state.isTerminal,
|
|
34
|
+
actions: state.actions
|
|
35
|
+
.filter((action) : action is Exclude<typeof action, Await> => ! (action instanceof Await))
|
|
36
|
+
.map(action => ({
|
|
37
|
+
name: action.name,
|
|
38
|
+
description: action.description,
|
|
39
|
+
actor: { id: action.actor.id, type: action.actor.type, roles: action.actor.roles },
|
|
40
|
+
})),
|
|
41
|
+
transitions: state.transitions.map(transition => ({ to: transition.to })),
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { WorkflowDefinition }
|
package/src/api/jobs/Job.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {WaitForInput} from "../actions/WaitForInput";
|
|
2
2
|
|
|
3
3
|
class Job {
|
|
4
4
|
|
|
@@ -10,10 +10,20 @@ class Job {
|
|
|
10
10
|
readonly startedBy : string;
|
|
11
11
|
readonly lastUpdated : Date;
|
|
12
12
|
readonly killed : boolean;
|
|
13
|
+
// Distinct from state: the job's processing status. A job whose current
|
|
14
|
+
// state reaches an Await is parked in "Awaiting input" until an update
|
|
15
|
+
// resumes it. waitingFor is the id of the await it is parked on (a foreign
|
|
16
|
+
// key to the awaits table server-side); awaitMetadata is what that Await
|
|
17
|
+
// stored (e.g. the input required). Both clear once the job moves on.
|
|
18
|
+
status : string;
|
|
19
|
+
waitingFor? : string;
|
|
20
|
+
awaitMetadata? : WaitForInput;
|
|
13
21
|
|
|
14
22
|
constructor(id : string, properties : Map<string, any> = new Map(), state: string, workflowId? : string,
|
|
15
23
|
startedBy : string = "system", startedAt : Date = new Date(),
|
|
16
|
-
lastUpdated : Date = startedAt, killed : boolean = false
|
|
24
|
+
lastUpdated : Date = startedAt, killed : boolean = false,
|
|
25
|
+
status : string = Job.Status.ACTIVE, awaitMetadata? : WaitForInput,
|
|
26
|
+
waitingFor? : string) {
|
|
17
27
|
|
|
18
28
|
this.id = id;
|
|
19
29
|
this.properties = properties;
|
|
@@ -23,8 +33,20 @@ class Job {
|
|
|
23
33
|
this.startedAt = startedAt;
|
|
24
34
|
this.lastUpdated = lastUpdated;
|
|
25
35
|
this.killed = killed;
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.awaitMetadata = awaitMetadata;
|
|
38
|
+
this.waitingFor = waitingFor;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
}
|
|
29
42
|
|
|
43
|
+
namespace Job {
|
|
44
|
+
|
|
45
|
+
export const Status = {
|
|
46
|
+
ACTIVE: "active",
|
|
47
|
+
AWAITING_INPUT: "Awaiting input",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
export { Job }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {Actor} from "../actors/Actor";
|
|
2
2
|
import {Auditor} from "../auditing/Auditor";
|
|
3
3
|
import {Job} from "./Job";
|
|
4
|
+
import {serializeWaitForInput} from "../actions/WaitForInput";
|
|
4
5
|
|
|
5
6
|
/* Persists jobs and audits every interaction. Public methods record the
|
|
6
7
|
interaction against the injected auditor and then defer to the abstract
|
|
@@ -24,7 +25,7 @@ abstract class JobPersistence {
|
|
|
24
25
|
properties? : Map<string, any>,
|
|
25
26
|
state? : string) : Promise<void> {
|
|
26
27
|
|
|
27
|
-
const change = {
|
|
28
|
+
const change : { properties? : any, state? : any, metadata? : Record<string, any> } = {
|
|
28
29
|
properties: properties ? this.generatePropertiesDiff(job.properties, properties) : undefined,
|
|
29
30
|
state: state ? {from : job.state, to : state} : undefined
|
|
30
31
|
}
|
|
@@ -32,6 +33,10 @@ abstract class JobPersistence {
|
|
|
32
33
|
const interaction : Array<string> = [];
|
|
33
34
|
if (properties) interaction.push(JobPersistence.Interaction.UPDATE_PROPERTIES);
|
|
34
35
|
if (state) interaction.push(JobPersistence.Interaction.CHANGE_STATE);
|
|
36
|
+
if (job.status === Job.Status.AWAITING_INPUT && job.awaitMetadata) {
|
|
37
|
+
interaction.push(JobPersistence.Interaction.AWAIT);
|
|
38
|
+
change.metadata = serializeWaitForInput(job.awaitMetadata);
|
|
39
|
+
}
|
|
35
40
|
|
|
36
41
|
await this.auditor.audit("job", job.id, actor, interaction, changeDescription, change);
|
|
37
42
|
|
|
@@ -43,7 +48,10 @@ abstract class JobPersistence {
|
|
|
43
48
|
job.startedBy,
|
|
44
49
|
job.startedAt,
|
|
45
50
|
new Date(),
|
|
46
|
-
job.killed
|
|
51
|
+
job.killed,
|
|
52
|
+
job.status,
|
|
53
|
+
job.awaitMetadata,
|
|
54
|
+
job.waitingFor
|
|
47
55
|
)
|
|
48
56
|
|
|
49
57
|
this.saveInternal(updatedJob);
|
|
@@ -113,6 +121,7 @@ namespace JobPersistence {
|
|
|
113
121
|
CREATE = "CREATE",
|
|
114
122
|
UPDATE_PROPERTIES = "UPDATE_PROPERTIES",
|
|
115
123
|
CHANGE_STATE = "CHANGE_STATE",
|
|
124
|
+
AWAIT = "AWAIT",
|
|
116
125
|
DELETE = "DELETE",
|
|
117
126
|
READ = "READ",
|
|
118
127
|
LIST = "LIST",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {Job} from "./Job";
|
|
2
|
+
import {SerializedWaitForInput, serializeWaitForInput, deserializeWaitForInput} from "../actions/WaitForInput";
|
|
2
3
|
|
|
3
4
|
type SerializedJob = {
|
|
4
5
|
id : string,
|
|
@@ -9,6 +10,9 @@ type SerializedJob = {
|
|
|
9
10
|
startedBy? : string,
|
|
10
11
|
lastUpdated? : string,
|
|
11
12
|
killed? : boolean,
|
|
13
|
+
status? : string,
|
|
14
|
+
waitingFor? : string,
|
|
15
|
+
awaitMetadata? : SerializedWaitForInput,
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
const serializeJob = (job : Job) : SerializedJob => ({
|
|
@@ -20,13 +24,19 @@ const serializeJob = (job : Job) : SerializedJob => ({
|
|
|
20
24
|
startedBy: job.startedBy,
|
|
21
25
|
lastUpdated: job.lastUpdated.toISOString(),
|
|
22
26
|
killed: job.killed,
|
|
27
|
+
status: job.status,
|
|
28
|
+
waitingFor: job.waitingFor,
|
|
29
|
+
awaitMetadata: job.awaitMetadata ? serializeWaitForInput(job.awaitMetadata) : undefined,
|
|
23
30
|
});
|
|
24
31
|
|
|
25
32
|
const deserializeJob = (serialized : SerializedJob) : Job => {
|
|
26
33
|
const startedAt = serialized.startedAt ? new Date(serialized.startedAt) : new Date();
|
|
27
34
|
return new Job(serialized.id, new Map(Object.entries(serialized.properties)), serialized.state,
|
|
28
35
|
serialized.workflowId, serialized.startedBy ?? "system", startedAt,
|
|
29
|
-
serialized.lastUpdated ? new Date(serialized.lastUpdated) : startedAt, serialized.killed ?? false
|
|
36
|
+
serialized.lastUpdated ? new Date(serialized.lastUpdated) : startedAt, serialized.killed ?? false,
|
|
37
|
+
serialized.status ?? Job.Status.ACTIVE,
|
|
38
|
+
serialized.awaitMetadata ? deserializeWaitForInput(serialized.awaitMetadata) : undefined,
|
|
39
|
+
serialized.waitingFor);
|
|
30
40
|
};
|
|
31
41
|
|
|
32
42
|
export { serializeJob, deserializeJob };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* The identity behind a platform session token, as resolved by the platform. */
|
|
2
|
+
type ResolvedSession = {
|
|
3
|
+
|
|
4
|
+
id : string,
|
|
5
|
+
roles : Array<string>,
|
|
6
|
+
tenant? : string,
|
|
7
|
+
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/* Resolves a platform session token (the anbaric_session cookie) into the
|
|
11
|
+
user's identity. A deployed app cannot verify the token itself - it is signed
|
|
12
|
+
with a platform-only secret - so the resolution is a call to the platform.
|
|
13
|
+
Returns undefined when the token is missing, invalid or expired. */
|
|
14
|
+
interface SessionResolver {
|
|
15
|
+
|
|
16
|
+
resolve(sessionToken : string) : Promise<ResolvedSession | undefined>;
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type { SessionResolver, ResolvedSession }
|
package/src/api/states/State.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import {Action} from "../actions/Action";
|
|
2
|
+
import {Await} from "../actions/Await";
|
|
2
3
|
import {Transition} from "../transitions/Transition";
|
|
3
4
|
|
|
4
5
|
class State {
|
|
5
6
|
|
|
6
7
|
readonly id : string;
|
|
7
|
-
actions : Array<Action>;
|
|
8
|
+
actions : Array<Action | Await>;
|
|
8
9
|
transitions : Array<Transition>;
|
|
9
10
|
readonly isTerminal : boolean;
|
|
10
11
|
|
|
11
|
-
constructor(id : string, actions : Array<Action> = [], transitions : Array<Transition> = [], isTerminal : boolean = false) {
|
|
12
|
+
constructor(id : string, actions : Array<Action | Await> = [], transitions : Array<Transition> = [], isTerminal : boolean = false) {
|
|
12
13
|
this.id = id;
|
|
13
14
|
this.actions = actions;
|
|
14
15
|
this.transitions = transitions;
|
|
15
16
|
this.isTerminal = isTerminal;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
subscribe(action : Action) : void {
|
|
19
|
+
subscribe(action : Action | Await) : void {
|
|
19
20
|
this.actions.push(action);
|
|
20
21
|
}
|
|
21
22
|
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@ export * from "./api/jobs/PropertyDefinition"
|
|
|
5
5
|
export * from "./api/states/State"
|
|
6
6
|
export * from "./api/states/Terminal"
|
|
7
7
|
export * from "./api/actions/Action"
|
|
8
|
+
export * from "./api/actions/Await"
|
|
9
|
+
export * from "./api/actions/WaitForInput"
|
|
8
10
|
export * from "./api/actions/AgenticAction"
|
|
9
11
|
export * from "./api/actors/agents/AgentMessage"
|
|
10
12
|
export * from "./api/actors/agents/AgentRequest"
|
|
@@ -14,6 +16,7 @@ export * from "./api/auditing/NoOpAuditor"
|
|
|
14
16
|
export * from "./api/transitions/Transition"
|
|
15
17
|
export * from "./api/actors/Actor"
|
|
16
18
|
export * from "./api/actors/SystemActor"
|
|
19
|
+
export * from "./api/sessions/SessionResolver"
|
|
17
20
|
export * from "./api/documents/JsonStore"
|
|
18
21
|
export * from "./api/secrets/SecretStore"
|
|
19
22
|
export * from "./api/sql/SqlStore"
|