anbaric-state-machine 1.7.0 → 2.0.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-state-machine",
3
- "version": "1.7.0",
3
+ "version": "2.0.0",
4
4
  "description": "This is the state machine library that one can use to add state management to an application. It will, by default, be locally runnable but can deployed to the Anbaric Cloud via the CLI",
5
5
  "license": "MIT",
6
6
  "author": "chris@anbaric.ai",
@@ -15,8 +15,8 @@
15
15
  "typescript": "^7.0.2"
16
16
  },
17
17
  "dependencies": {
18
- "anbaric-impl-cloud": "^1.7.0",
19
- "anbaric-tsapi": "^1.7.0"
18
+ "anbaric-impl-cloud": "^2.0.0",
19
+ "anbaric-tsapi": "^2.0.0"
20
20
  },
21
21
  "files": [
22
22
  "src"
@@ -1,10 +1,18 @@
1
- import {Action, Actor, Auditor, Consumer, Job, JobPersistence, PropertyDefinition, Queue, State} from "anbaric-tsapi";
1
+ import {
2
+ Action,
3
+ Actor,
4
+ AuditInteraction,
5
+ Consumer,
6
+ Job,
7
+ JobPersistence,
8
+ PropertyDefinition,
9
+ Queue,
10
+ State
11
+ } from "anbaric-tsapi";
2
12
  import {JobPersistenceFactory} from "./persistence/JobPersistenceFactory";
3
13
  import {QueueFactory} from "./scheduling/QueueFactory";
4
14
  import {ConsumerFactory} from "./scheduling/ConsumerFactory";
5
15
  import {Code} from "./actors/Code";
6
- import {AuditorFactory} from "./auditing/AuditorFactory";
7
- import {AuditorTransaction} from "./auditing/AuditorTransaction";
8
16
 
9
17
  class StateMachine {
10
18
 
@@ -15,10 +23,9 @@ class StateMachine {
15
23
  private persistence: JobPersistence;
16
24
  private queue: Queue;
17
25
  private consumer: Consumer;
18
- private auditor: Auditor;
19
26
  private machineActor: Code;
20
27
 
21
- constructor(workflowId : string, states : Array<State>, startState : string, dataSchema : Array<PropertyDefinition>, persistence : JobPersistence = JobPersistenceFactory.instance(), queue : Queue = QueueFactory.instance(), auditor : Auditor = AuditorFactory.instance()) {
28
+ constructor(workflowId : string, states : Array<State>, startState : string, dataSchema : Array<PropertyDefinition>, persistence : JobPersistence = JobPersistenceFactory.instance(), queue : Queue = QueueFactory.instance()) {
22
29
 
23
30
  this.workflowId = workflowId;
24
31
  this.states = new Map(states.map(state => [state.id, state]));
@@ -26,29 +33,21 @@ class StateMachine {
26
33
  this.dataSchema = new Map(dataSchema.map(property => [property.id, property]));
27
34
  this.persistence = persistence;
28
35
  this.queue = queue;
29
- this.auditor = auditor;
30
36
  this.machineActor = new Code(workflowId, "state-machine");
31
37
 
32
38
  this.consumer = ConsumerFactory.instance(queue)
33
39
  this.consumer.subscribe(workflowId, jobId => this.progressJob(jobId));
34
40
  }
35
41
 
36
- async startJob(properties?: Map<string, any>, actor? : Actor): Promise<Job> {
42
+ async startJob(properties?: Map<string, any>, actor : Actor = this.machineActor): Promise<Job> {
37
43
 
38
44
  const job = new Job(crypto.randomUUID(), properties, this.startState, this.workflowId,
39
45
  actor?.id ?? this.workflowId);
40
46
 
41
47
  if (! this.validateProperties(properties ?? new Map(), true)) throw new Error("Invalid properties");
48
+ if (! this.authorizeActor(actor, job)) throw new Error("Unauthorized");
42
49
 
43
- if (! this.authorizeActor(actor, job)) {
44
- await this.auditor.audit(job.id, actor ?? this.machineActor, "CREATE", "Unauthorized start", null);
45
- throw new Error("Unauthorized");
46
- }
47
-
48
- await this.persistence.save(job);
49
- await this.auditor.audit(job.id, actor ?? this.machineActor, "CREATE", "Job started",
50
- Object.fromEntries(job.properties));
51
-
50
+ await this.persistence.create(actor, job);
52
51
  await this.queue.enqueue(job.id, this.workflowId);
53
52
 
54
53
  return job;
@@ -56,40 +55,35 @@ class StateMachine {
56
55
 
57
56
  async updateJob(jobId : string, properties : Map<string, any>, actor : Actor) : Promise<void> {
58
57
 
59
- if (! this.authorizeActor(actor, await this.persistence.retrieve(jobId))) {
60
- await this.auditor.audit(jobId, actor, "UPDATE_PROPERTIES", "Unauthorized update", null);
61
- throw new Error("Unauthorized");
62
- }
63
-
64
- if (! this.validateProperties(properties, false)) throw new Error("Invalid properties");
58
+ const job = await this.persistence.retrieve(jobId, actor);
65
59
 
66
- await this.persistence.updateProperties(jobId, properties);
67
- await this.auditor.audit(jobId, actor, "UPDATE_PROPERTIES", "Properties updated", Object.fromEntries(properties));
60
+ if (!this.authorizeActor(actor, job)) throw new Error("Unauthorized");
68
61
 
69
- await this.queue.enqueue(jobId, this.workflowId);
62
+ await this.updateJobInternal(actor, "Properties Updated", job, properties);
70
63
  }
71
64
 
72
65
  async executeAction(jobId : string, action : Action) : Promise<void> {
73
- const job = await this.persistence.retrieve(jobId);
74
66
 
75
- if (! action.predicate(job)) throw new Error("Action predicate unmet");
67
+ const job = await this.persistence.retrieve(jobId, action.actor);
76
68
 
77
- if (! this.authorizeActor(action.actor, job)) {
78
- await this.auditor.audit(jobId, action.actor, "UPDATE_PROPERTIES", "Unauthorized update", null);
79
- throw new Error("Actor not authorized to execute action");
80
- }
69
+ if (! action.predicate(job)) throw new Error("Action predicate unmet");
70
+ if (! this.authorizeActor(action.actor, job)) throw new Error("Actor not authorized to execute action");
81
71
 
82
72
  const newProperties = await action.run(job);
83
73
 
84
- if (! this.validateProperties(newProperties, false)) {
85
- await this.auditor.audit(jobId, action.actor, "UPDATE_PROPERTIES", "Invalid properties", Object.fromEntries(newProperties));
86
- throw new Error("The action generated invalid properties");
74
+ try {
75
+ await this.updateJobInternal(action.actor, `Action ${action.name} executed on job ${job.id}: ${action.description}`, job, newProperties);
76
+ } catch (error) {
77
+ throw new Error('The action generated invalid properties');
87
78
  }
79
+ }
88
80
 
89
- await this.persistence.updateProperties(jobId, newProperties);
90
- await this.auditor.audit(jobId, action.actor, "UPDATE_PROPERTIES", "Properties updated", Object.fromEntries(newProperties));
81
+ private async updateJobInternal(actor: Actor, message : string, job: Job, newProperties?: Map<string, any>, newState? : string) {
91
82
 
92
- await this.queue.enqueue(jobId, this.workflowId);
83
+ if (newProperties && !this.validateProperties(newProperties, false)) throw new Error("Invalid properties");
84
+
85
+ await this.persistence.save(actor, "Properties Updated", job, newProperties, newState)
86
+ await this.queue.enqueue(job.id, this.workflowId);
93
87
  }
94
88
 
95
89
  private validateProperties(properties : Map<string, any>, isNew : boolean) : boolean {
@@ -108,49 +102,44 @@ class StateMachine {
108
102
  }
109
103
 
110
104
  private async progressJob(jobId : string) : Promise<void> {
111
- const auditTransaction = new AuditorTransaction(this.auditor);
112
105
  let pristine = true;
113
106
 
114
- const job = await this.persistence.retrieve(jobId);
115
- const currentState = this.states.get(job.stateId!);
107
+ const job = await this.persistence.retrieve(jobId, this.machineActor);
108
+ const currentState = this.states.get(job.state);
109
+
110
+ const involvedActors : Actor[] = [];
116
111
 
117
112
  for (const action of currentState?.actions ?? []) {
118
113
  if (! action.predicate(job)) continue;
119
-
120
- if (! this.authorizeActor(action.actor, job)) {
121
- auditTransaction.audit(jobId, action.actor, "UPDATE_PROPERTIES", "Unauthorized update", null);
122
- continue;
123
- }
114
+ if (! this.authorizeActor(action.actor, job)) continue;
124
115
 
125
116
  const newProperties = await action.run(job);
126
117
 
127
- if (! this.validateProperties(newProperties, false)) {
128
- auditTransaction.audit(jobId, action.actor, "UPDATE_PROPERTIES", "Invalid properties", Object.fromEntries(newProperties));
129
- continue;
130
- }
118
+ if (! this.validateProperties(newProperties, false)) continue;
119
+
120
+ involvedActors.push(action.actor);
131
121
 
132
- auditTransaction.audit(jobId, action.actor, "UPDATE_PROPERTIES", "Properties updated", Object.fromEntries(newProperties));
133
122
  newProperties.forEach((value, key) => {
134
123
  pristine = false;
135
124
  job.properties.set(key, value);
136
125
  });
137
126
  }
138
127
 
128
+ let newState : string | undefined = undefined;
139
129
  for (const transition of currentState?.transitions ?? []) {
140
130
  if (! this.states.has(transition.to)) continue;
141
- if (job.transition(transition)) {
142
- auditTransaction.audit(jobId, this.machineActor, "CHANGE_STATE", `Transitioned to "${transition.to}"`, null);
131
+ if (transition.predicate(job)) {
132
+ newState = transition.to;
143
133
  pristine = false;
144
134
  break;
145
135
  }
146
136
  }
147
137
 
148
138
  if (! pristine) {
149
- await this.persistence.save(job);
139
+ // TODO: persistence.save should have the ability to accept multiple actors for this case.
140
+ await this.persistence.save(involvedActors[0] ?? this.machineActor, `Job ${job.id} progressed automatically`, job, job.properties, newState);
150
141
  await this.queue.enqueue(job.id, this.workflowId);
151
142
  }
152
-
153
- await auditTransaction.flush();
154
143
  }
155
144
 
156
145
  async cleanUp() : Promise<void> {
@@ -0,0 +1,27 @@
1
+ import {Agent, AgenticAction, AgentMessage, AgentRequest, Job} from "anbaric-tsapi";
2
+
3
+ /* An OpenAI-API-compatible agentic action: it carries a prompt as an array of
4
+ chat messages and a JSON Schema, appends the job's properties as the final
5
+ user message, and lets its agent's client generate the properties to write. */
6
+ class RemoteLLMAgenticAction extends AgenticAction {
7
+
8
+ private messages : Array<AgentMessage>;
9
+ private outputSchema : object;
10
+
11
+ constructor(name : string, agent : Agent, messages : Array<AgentMessage>, outputSchema : object,
12
+ description : string = "", id? : string) {
13
+ super(name, agent, description, id);
14
+ this.messages = messages;
15
+ this.outputSchema = outputSchema;
16
+ }
17
+
18
+ protected requestFor(job : Job) : AgentRequest {
19
+ return {
20
+ messages: [...this.messages, { role: "user", content: JSON.stringify(Object.fromEntries(job.properties)) }],
21
+ outputSchema: this.outputSchema,
22
+ };
23
+ }
24
+
25
+ }
26
+
27
+ export { RemoteLLMAgenticAction }
@@ -0,0 +1,83 @@
1
+ import {Agent, AgentRequest} from "anbaric-tsapi";
2
+
3
+ type FetchFn = (url : string, init : RequestInit) => Promise<Response>;
4
+
5
+ type OpenAIConnection = {
6
+ apiKey : string,
7
+ model : string,
8
+ baseUrl? : string,
9
+ organization? : string,
10
+ };
11
+
12
+ const strictSchema = (schema : any) : any => {
13
+ if (schema?.type === "object" && schema.properties) {
14
+ return {
15
+ ...schema,
16
+ required: Object.keys(schema.properties),
17
+ additionalProperties: false,
18
+ properties: Object.fromEntries(
19
+ Object.entries(schema.properties).map(([key, property]) => [key, strictSchema(property)])),
20
+ };
21
+ }
22
+ if (schema?.type === "array" && schema.items) {
23
+ return { ...schema, items: strictSchema(schema.items) };
24
+ }
25
+ return schema;
26
+ };
27
+
28
+ /* The functional half of an OpenAI agent: asks an OpenAI-compatible chat
29
+ completions endpoint for JSON conforming to the request's schema via
30
+ structured outputs. */
31
+ class OpenAIClient extends Agent.Client {
32
+
33
+ private baseUrl : string;
34
+
35
+ constructor(private connection : OpenAIConnection,
36
+ private fetchFn : FetchFn = (url, init) => fetch(url, init)) {
37
+ super();
38
+ this.baseUrl = connection.baseUrl ?? "https://api.openai.com/v1";
39
+ }
40
+
41
+ async generate(request : AgentRequest) : Promise<Record<string, any>> {
42
+ const response = await this.fetchFn(`${this.baseUrl}/chat/completions`, {
43
+ method: "POST",
44
+ headers: {
45
+ "content-type": "application/json",
46
+ "authorization": `Bearer ${this.connection.apiKey}`,
47
+ ...(this.connection.organization ? { "openai-organization": this.connection.organization } : {}),
48
+ },
49
+ body: JSON.stringify({
50
+ model: this.connection.model,
51
+ messages: request.messages,
52
+ response_format: {
53
+ type: "json_schema",
54
+ json_schema: { name: "properties", strict: true, schema: strictSchema(request.outputSchema) },
55
+ },
56
+ }),
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const problem = await response.json().catch(() => ({}));
61
+ throw new Error(`The OpenAI request failed with status ${response.status}${problem.error?.message ? `: ${problem.error.message}` : ""}`);
62
+ }
63
+
64
+ const body = await response.json();
65
+ const content = body.choices?.[0]?.message?.content;
66
+ if (typeof content !== "string") throw new Error("The OpenAI response carried no structured content");
67
+ return JSON.parse(content);
68
+ }
69
+
70
+ }
71
+
72
+ /* A light agent backed by an OpenAI-compatible chat completions endpoint. */
73
+ class OpenAIAgent extends Agent {
74
+
75
+ constructor(id : string, role : string, connection : OpenAIConnection,
76
+ fetchFn : FetchFn = (url, init) => fetch(url, init)) {
77
+ super(id, role, new OpenAIClient(connection, fetchFn));
78
+ }
79
+
80
+ }
81
+
82
+ export { OpenAIAgent, OpenAIClient }
83
+ export type { OpenAIConnection }
@@ -1,10 +1,10 @@
1
- import {Actor, AuditChange, Auditor} from "anbaric-tsapi";
1
+ import {Actor, AuditInteraction, Auditor} from "anbaric-tsapi";
2
2
 
3
3
  class ConsoleAuditor implements Auditor {
4
4
 
5
- async audit(jobId : string, actor : Actor, change : AuditChange,
6
- changeDescription : string, details : any) : Promise<void> {
7
- console.log(`[${jobId}] ${actor.id} ${change} ${changeDescription} ${JSON.stringify(details ?? null)}`);
5
+ async audit(resourceType : string, resourceId : string, actor : Actor, interaction : AuditInteraction[],
6
+ description : string, details : any) : Promise<void> {
7
+ console.log(`[${resourceType} ${resourceId}] ${actor.id} ${interaction.join(",")} ${description} ${JSON.stringify(details ?? null)}`);
8
8
  }
9
9
 
10
10
  }
package/src/index.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export * from "./StateMachine";
2
- export * from "./actors/Agent";
2
+ export * from "./actions/RemoteLLMAgenticAction";
3
+ export * from "./agents/OpenAIAgent";
3
4
  export * from "./actors/Code";
4
5
  export * from "./actors/Human";
5
6
  export * from "./auditing/AuditorFactory";
6
- export * from "./auditing/AuditorTransaction";
7
7
  export * from "./auditing/ConsoleAuditor";
8
8
  export * from "./persistence/InMemoryJobPersistence";
9
9
  export * from "./persistence/JobPersistenceFactory";
@@ -1,14 +1,18 @@
1
- import {Job, JobPersistence} from "anbaric-tsapi";
1
+ import {Auditor, Job, JobPersistence, NoOpAuditor} from "anbaric-tsapi";
2
2
 
3
- class InMemoryJobPersistence implements JobPersistence {
3
+ class InMemoryJobPersistence extends JobPersistence {
4
4
 
5
5
  private jobs = new Map<string, Job>();
6
6
 
7
- async save(job : Job) : Promise<void> {
7
+ constructor(auditor : Auditor = new NoOpAuditor()) {
8
+ super(auditor);
9
+ }
10
+
11
+ protected async saveInternal(job : Job) : Promise<void> {
8
12
  this.jobs.set(job.id, job);
9
13
  }
10
14
 
11
- async retrieve(id : string) : Promise<Job> {
15
+ protected async retrieveInternal(id : string) : Promise<Job> {
12
16
  const job = this.jobs.get(id);
13
17
  if (!job) {
14
18
  throw new Error(`No job found with id "${id}"`);
@@ -16,22 +20,14 @@ class InMemoryJobPersistence implements JobPersistence {
16
20
  return job;
17
21
  }
18
22
 
19
- async delete(id : string) : Promise<void> {
23
+ protected async deleteInternal(id : string) : Promise<void> {
20
24
  this.jobs.delete(id);
21
25
  }
22
26
 
23
- async list(pageSize : number = 100, page : number = 0) : Promise<Array<Job>> {
27
+ protected async listInternal(pageSize : number = 100, page : number = 0) : Promise<Array<Job>> {
24
28
  return Array.from(this.jobs.values()).slice(page * pageSize, (page + 1) * pageSize);
25
29
  }
26
30
 
27
- async updateProperties(id : string, properties : Map<string, any>) : Promise<void> {
28
- const job = await this.retrieve(id);
29
- for (const [key, value] of properties) {
30
- job.properties.set(key, value);
31
- }
32
- job.lastUpdated = new Date();
33
- }
34
-
35
31
  }
36
32
 
37
33
  export { InMemoryJobPersistence }
@@ -1,15 +1,17 @@
1
1
  import {JobPersistence} from "anbaric-tsapi";
2
2
  import {CloudJobPersistence} from "anbaric-impl-cloud";
3
3
  import {InMemoryJobPersistence} from "./InMemoryJobPersistence";
4
+ import {AuditorFactory} from "../auditing/AuditorFactory";
4
5
 
5
6
  const JobPersistenceFactory = {
6
7
  instance() : JobPersistence {
8
+ const auditor = AuditorFactory.instance();
7
9
  switch (process.env.ANBARIC_JOB_PERSISTENCE_TYPE) {
8
10
  case "cloud":
9
- return new CloudJobPersistence();
11
+ return new CloudJobPersistence(undefined, auditor);
10
12
  case "memory":
11
13
  default:
12
- return new InMemoryJobPersistence();
14
+ return new InMemoryJobPersistence(auditor);
13
15
  }
14
16
  }
15
17
  }
@@ -1,16 +0,0 @@
1
- import {Actor} from "anbaric-tsapi";
2
-
3
- class Agent implements Actor {
4
-
5
- readonly type = "AGENT" as const;
6
- readonly id : string;
7
- readonly role : string;
8
-
9
- constructor(id : string, role : string) {
10
- this.id = id;
11
- this.role = role;
12
- }
13
-
14
- }
15
-
16
- export { Agent }
@@ -1,31 +0,0 @@
1
- import {Actor, AuditChange, Auditor} from "anbaric-tsapi";
2
-
3
- type BufferedEntry = {
4
- jobId : string,
5
- actor : Actor,
6
- change : AuditChange,
7
- changeDescription : string,
8
- details : any,
9
- };
10
-
11
- class AuditorTransaction {
12
-
13
- private entries = new Array<BufferedEntry>();
14
-
15
- constructor(private auditor : Auditor) {}
16
-
17
- audit(jobId : string, actor : Actor, change : AuditChange, changeDescription : string, details : any) : void {
18
- this.entries.push({ jobId, actor, change, changeDescription, details });
19
- }
20
-
21
- async flush() : Promise<void> {
22
- const flushing = this.entries;
23
- this.entries = [];
24
- for (const entry of flushing) {
25
- await this.auditor.audit(entry.jobId, entry.actor, entry.change, entry.changeDescription, entry.details);
26
- }
27
- }
28
-
29
- }
30
-
31
- export { AuditorTransaction }