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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anbaric-tsapi",
3
- "version": "1.18.1",
3
+ "version": "1.20.0",
4
4
  "description": "These are the shared libraries - types, classes, etc - that both the public state-machine and hosting services share",
5
5
  "license": "MIT",
6
6
  "author": "chris@anbaric.ai",
@@ -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 };
@@ -4,7 +4,7 @@ interface Actor {
4
4
 
5
5
  type : ActorType;
6
6
  id : string;
7
- role : string;
7
+ roles : Array<string>;
8
8
 
9
9
  }
10
10
 
@@ -4,7 +4,7 @@ class SystemActor implements Actor {
4
4
 
5
5
  type : ActorType = "SYSTEM";
6
6
  id = "_SYSTEM";
7
- role = "_SYSTEM";
7
+ roles = ["_SYSTEM"];
8
8
 
9
9
  static actor = new SystemActor();
10
10
 
@@ -8,12 +8,12 @@ class Agent implements Actor {
8
8
 
9
9
  readonly type = "AGENT" as const;
10
10
  readonly id : string;
11
- readonly role : string;
11
+ readonly roles : Array<string>;
12
12
  readonly client : Agent.Client;
13
13
 
14
- constructor(id : string, role : string, client : Agent.Client) {
14
+ constructor(id : string, roles : Array<string> | string, client : Agent.Client) {
15
15
  this.id = id;
16
- this.role = role;
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, role : 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
- export type { WorkflowDefinition }
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 }
@@ -1,4 +1,4 @@
1
- import {Transition} from "../transitions/Transition";
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 }
@@ -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"