@amalgm/automations 0.2.1 → 0.2.2
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/AXIOMS.md +17 -1
- package/PURPOSE.md +11 -1
- package/README.md +5 -2
- package/dist/host/main.js +15 -4
- package/dist/host/server.d.ts +2 -0
- package/dist/host/server.js +25 -7
- package/dist/src/automations.d.ts +4 -7
- package/dist/src/automations.js +13 -65
- package/dist/src/client.d.ts +1 -0
- package/dist/src/client.js +3 -2
- package/dist/src/contract.d.ts +2 -29
- package/dist/src/events-http.js +4 -0
- package/dist/src/executor.d.ts +12 -3
- package/dist/src/executor.js +163 -44
- package/dist/src/http.js +4 -0
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +4 -2
- package/dist/src/machine-client.d.ts +1 -0
- package/dist/src/machine-client.js +2 -0
- package/dist/src/machine.d.ts +2 -1
- package/dist/src/machine.js +6 -1
- package/dist/src/mcp.d.ts +3 -0
- package/dist/src/mcp.js +53 -50
- package/dist/src/plan.d.ts +3 -0
- package/dist/src/plan.js +48 -0
- package/dist/src/run-contract.d.ts +46 -0
- package/dist/src/run-contract.js +1 -0
- package/dist/src/run-journal.d.ts +8 -0
- package/dist/src/run-journal.js +56 -0
- package/dist/src/runner.d.ts +14 -0
- package/dist/src/runner.js +70 -0
- package/dist/src/schema.d.ts +6 -6
- package/dist/src/schema.js +17 -2
- package/dist/src/supabase-crud/mappers.js +2 -0
- package/dist/src/supabase-crud/rows.d.ts +2 -0
- package/dist/src/supabase-machine.js +2 -0
- package/dist/src/supabase-store.d.ts +1 -4
- package/dist/src/supabase-store.js +0 -24
- package/dist/src/tool-surface.d.ts +46 -0
- package/dist/src/tool-surface.js +125 -0
- package/dist/src/types.d.ts +1 -16
- package/package.json +3 -3
- package/skills/automations/SKILL.md +20 -15
- package/supabase/migrations/20260830010000_durable_step_retries.sql +332 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function emptyJournal() {
|
|
2
|
+
return { version: 1, steps: [] };
|
|
3
|
+
}
|
|
4
|
+
export function runJournal(value, plan) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return emptyJournal();
|
|
7
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
8
|
+
throw new Error('Run step journal is invalid');
|
|
9
|
+
const source = value;
|
|
10
|
+
if (source.version !== 1 || !Array.isArray(source.steps))
|
|
11
|
+
throw new Error('Run step journal is invalid');
|
|
12
|
+
const planIds = new Set(plan.map(({ id }) => id));
|
|
13
|
+
const steps = source.steps.map(runStep);
|
|
14
|
+
const ids = new Set();
|
|
15
|
+
for (const step of steps) {
|
|
16
|
+
if (!planIds.has(step.id))
|
|
17
|
+
throw new Error(`Run step is not in the immutable plan: ${step.id}`);
|
|
18
|
+
if (ids.has(step.id))
|
|
19
|
+
throw new Error(`Run step journal has a duplicate id: ${step.id}`);
|
|
20
|
+
ids.add(step.id);
|
|
21
|
+
}
|
|
22
|
+
return { version: 1, steps };
|
|
23
|
+
}
|
|
24
|
+
export function completed(journal, stepId) {
|
|
25
|
+
return journal.steps.some((step) => step.id === stepId && step.status === 'completed');
|
|
26
|
+
}
|
|
27
|
+
export function stepAttempt(journal, stepId) {
|
|
28
|
+
return journal.steps.find((step) => step.id === stepId)?.attempts ?? 0;
|
|
29
|
+
}
|
|
30
|
+
export function putStep(journal, step) {
|
|
31
|
+
const index = journal.steps.findIndex(({ id }) => id === step.id);
|
|
32
|
+
const steps = [...journal.steps];
|
|
33
|
+
if (index === -1)
|
|
34
|
+
steps.push(step);
|
|
35
|
+
else
|
|
36
|
+
steps[index] = step;
|
|
37
|
+
return { version: 1, steps };
|
|
38
|
+
}
|
|
39
|
+
/** Journals are constructed from JSON fields only; this is the persistence boundary. */
|
|
40
|
+
export function journalJson(journal) {
|
|
41
|
+
return journal;
|
|
42
|
+
}
|
|
43
|
+
function runStep(value) {
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
45
|
+
throw new Error('Run step is invalid');
|
|
46
|
+
const item = value;
|
|
47
|
+
if (typeof item.id !== 'string' || !item.id)
|
|
48
|
+
throw new Error('Run step id is invalid');
|
|
49
|
+
if (item.status !== 'running' && item.status !== 'completed' && item.status !== 'failed') {
|
|
50
|
+
throw new Error(`Run step status is invalid: ${item.id}`);
|
|
51
|
+
}
|
|
52
|
+
if (!Number.isInteger(item.attempts) || Number(item.attempts) < 1 || typeof item.startedAt !== 'string') {
|
|
53
|
+
throw new Error(`Run step attempt is invalid: ${item.id}`);
|
|
54
|
+
}
|
|
55
|
+
return item;
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ClaimedAutomationRun, MachineRuns } from './machine.js';
|
|
2
|
+
export interface AutomationMachineRunnerOptions {
|
|
3
|
+
readonly runs: MachineRuns;
|
|
4
|
+
readonly execute: (run: ClaimedAutomationRun) => Promise<void>;
|
|
5
|
+
readonly batchSize?: number;
|
|
6
|
+
readonly leaseSeconds?: number;
|
|
7
|
+
readonly pollIntervalMs?: number;
|
|
8
|
+
readonly maxBackoffMs?: number;
|
|
9
|
+
readonly log?: (event: string, details?: Readonly<Record<string, unknown>>) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function createAutomationMachineRunner(options: AutomationMachineRunnerOptions): Readonly<{
|
|
12
|
+
start(): void;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export function createAutomationMachineRunner(options) {
|
|
2
|
+
const batchSize = integer(options.batchSize ?? 8, 1, 20, 'batchSize');
|
|
3
|
+
const leaseSeconds = integer(options.leaseSeconds ?? 60, 30, 900, 'leaseSeconds');
|
|
4
|
+
const pollIntervalMs = integer(options.pollIntervalMs ?? 1_000, 1, 60_000, 'pollIntervalMs');
|
|
5
|
+
const maxBackoffMs = integer(options.maxBackoffMs ?? Math.max(30_000, pollIntervalMs), pollIntervalMs, 300_000, 'maxBackoffMs');
|
|
6
|
+
let active = null;
|
|
7
|
+
let timer = null;
|
|
8
|
+
let stopped = true;
|
|
9
|
+
let failures = 0;
|
|
10
|
+
let nextDelay = pollIntervalMs;
|
|
11
|
+
const schedule = (delay) => {
|
|
12
|
+
if (stopped)
|
|
13
|
+
return;
|
|
14
|
+
timer = setTimeout(drain, delay);
|
|
15
|
+
timer.unref?.();
|
|
16
|
+
};
|
|
17
|
+
const drain = () => {
|
|
18
|
+
if (stopped || active)
|
|
19
|
+
return active;
|
|
20
|
+
active = options.runs.claim({ limit: batchSize, leaseSeconds })
|
|
21
|
+
.then(async (runs) => {
|
|
22
|
+
failures = 0;
|
|
23
|
+
if (runs.length)
|
|
24
|
+
options.log?.('automations.claimed', { count: runs.length });
|
|
25
|
+
const settled = await Promise.allSettled(runs.map(options.execute));
|
|
26
|
+
settled.forEach((result) => {
|
|
27
|
+
if (result.status === 'rejected') {
|
|
28
|
+
options.log?.('automations.run.failed', { error: safe(result.reason) });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
nextDelay = runs.length === batchSize ? 0 : pollIntervalMs;
|
|
32
|
+
})
|
|
33
|
+
.catch((error) => {
|
|
34
|
+
failures += 1;
|
|
35
|
+
const delayMs = Math.min(maxBackoffMs, pollIntervalMs * (2 ** Math.min(failures - 1, 10)));
|
|
36
|
+
options.log?.('automations.poll.failed', { error: safe(error), retryInMs: delayMs });
|
|
37
|
+
nextDelay = delayMs;
|
|
38
|
+
})
|
|
39
|
+
.finally(() => {
|
|
40
|
+
active = null;
|
|
41
|
+
schedule(nextDelay);
|
|
42
|
+
});
|
|
43
|
+
return active;
|
|
44
|
+
};
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
start() {
|
|
47
|
+
if (!stopped)
|
|
48
|
+
return;
|
|
49
|
+
stopped = false;
|
|
50
|
+
failures = 0;
|
|
51
|
+
void drain();
|
|
52
|
+
},
|
|
53
|
+
async close() {
|
|
54
|
+
stopped = true;
|
|
55
|
+
if (timer)
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = null;
|
|
58
|
+
await active;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function integer(value, minimum, maximum, name) {
|
|
63
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
64
|
+
throw new Error(`${name} must be an integer from ${minimum} to ${maximum}`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function safe(error) {
|
|
69
|
+
return (error instanceof Error ? error.message : String(error)).replace(/[\r\n]+/g, ' ').slice(0, 500);
|
|
70
|
+
}
|
package/dist/src/schema.d.ts
CHANGED
|
@@ -173,15 +173,15 @@ export declare const createWorkflowSchema: z.ZodObject<{
|
|
|
173
173
|
}, "strict", z.ZodTypeAny, {
|
|
174
174
|
script: string;
|
|
175
175
|
name?: string | undefined;
|
|
176
|
-
id?: string | undefined;
|
|
177
176
|
compiled?: Json | undefined;
|
|
177
|
+
id?: string | undefined;
|
|
178
178
|
allowlist?: Json | undefined;
|
|
179
179
|
limits?: Json | undefined;
|
|
180
180
|
}, {
|
|
181
181
|
script: string;
|
|
182
182
|
name?: string | undefined;
|
|
183
|
-
id?: string | undefined;
|
|
184
183
|
compiled?: Json | undefined;
|
|
184
|
+
id?: string | undefined;
|
|
185
185
|
allowlist?: Json | undefined;
|
|
186
186
|
limits?: Json | undefined;
|
|
187
187
|
}>;
|
|
@@ -193,26 +193,26 @@ export declare const updateWorkflowSchema: z.ZodEffects<z.ZodObject<{
|
|
|
193
193
|
limits: z.ZodOptional<z.ZodNullable<z.ZodType<Json, z.ZodTypeDef, Json>>>;
|
|
194
194
|
}, "strict", z.ZodTypeAny, {
|
|
195
195
|
name?: string | null | undefined;
|
|
196
|
-
script?: string | undefined;
|
|
197
196
|
compiled?: Json | undefined;
|
|
197
|
+
script?: string | undefined;
|
|
198
198
|
allowlist?: Json | undefined;
|
|
199
199
|
limits?: Json | undefined;
|
|
200
200
|
}, {
|
|
201
201
|
name?: string | null | undefined;
|
|
202
|
-
script?: string | undefined;
|
|
203
202
|
compiled?: Json | undefined;
|
|
203
|
+
script?: string | undefined;
|
|
204
204
|
allowlist?: Json | undefined;
|
|
205
205
|
limits?: Json | undefined;
|
|
206
206
|
}>, {
|
|
207
207
|
name?: string | null | undefined;
|
|
208
|
-
script?: string | undefined;
|
|
209
208
|
compiled?: Json | undefined;
|
|
209
|
+
script?: string | undefined;
|
|
210
210
|
allowlist?: Json | undefined;
|
|
211
211
|
limits?: Json | undefined;
|
|
212
212
|
}, {
|
|
213
213
|
name?: string | null | undefined;
|
|
214
|
-
script?: string | undefined;
|
|
215
214
|
compiled?: Json | undefined;
|
|
215
|
+
script?: string | undefined;
|
|
216
216
|
allowlist?: Json | undefined;
|
|
217
217
|
limits?: Json | undefined;
|
|
218
218
|
}>;
|
package/dist/src/schema.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod/v3';
|
|
2
2
|
import { ValidationError } from './errors.js';
|
|
3
|
+
import { compiledAutomationPlan } from './plan.js';
|
|
3
4
|
import { IDENTIFIER } from './validation.js';
|
|
4
5
|
const maxPageSize = 100;
|
|
5
6
|
const identifierMessage = 'Identifier is invalid';
|
|
@@ -102,10 +103,16 @@ export function parseUpdateWebhookTrigger(value) {
|
|
|
102
103
|
return parse(updateWebhookTriggerSchema, value);
|
|
103
104
|
}
|
|
104
105
|
export function parseCreateWorkflow(value) {
|
|
105
|
-
|
|
106
|
+
const workflow = parse(createWorkflowSchema, value);
|
|
107
|
+
if (workflow.compiled !== undefined)
|
|
108
|
+
validateCompiledPlan(workflow.compiled);
|
|
109
|
+
return workflow;
|
|
106
110
|
}
|
|
107
111
|
export function parseUpdateWorkflow(value) {
|
|
108
|
-
|
|
112
|
+
const workflow = parse(updateWorkflowSchema, value);
|
|
113
|
+
if (workflow.compiled !== undefined && workflow.compiled !== null)
|
|
114
|
+
validateCompiledPlan(workflow.compiled);
|
|
115
|
+
return workflow;
|
|
109
116
|
}
|
|
110
117
|
export function parsePageQuery(value) {
|
|
111
118
|
return parse(pageQuerySchema, value);
|
|
@@ -119,3 +126,11 @@ export function parse(schema, value) {
|
|
|
119
126
|
return result.data;
|
|
120
127
|
throw new ValidationError(result.error.issues[0]?.message || 'Input is invalid');
|
|
121
128
|
}
|
|
129
|
+
function validateCompiledPlan(value) {
|
|
130
|
+
try {
|
|
131
|
+
compiledAutomationPlan(value);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
throw new ValidationError(error instanceof Error ? error.message : String(error));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -58,6 +58,8 @@ export function run(row) {
|
|
|
58
58
|
workflowId: row.workflow_id,
|
|
59
59
|
targetId: row.target_id,
|
|
60
60
|
status: row.status,
|
|
61
|
+
attempts: row.attempts,
|
|
62
|
+
...(row.retry_at ? { retryAt: timestamp(row.retry_at) } : {}),
|
|
61
63
|
input: row.input,
|
|
62
64
|
createdAt: timestamp(row.created_at),
|
|
63
65
|
...(row.sent_at ? { sentAt: timestamp(row.sent_at) } : {}),
|
|
@@ -43,6 +43,7 @@ function run(row) {
|
|
|
43
43
|
input: row.input,
|
|
44
44
|
status: row.status,
|
|
45
45
|
attempts: row.attempts,
|
|
46
|
+
...(row.retry_at ? { retryAt: timestamp(row.retry_at) } : {}),
|
|
46
47
|
leaseToken: row.lease_token,
|
|
47
48
|
leaseExpiresAt: timestamp(row.lease_expires_at),
|
|
48
49
|
createdAt: timestamp(row.created_at),
|
|
@@ -56,6 +57,7 @@ function run(row) {
|
|
|
56
57
|
function update(value) {
|
|
57
58
|
return {
|
|
58
59
|
status: value.status,
|
|
60
|
+
...(value.retryAt === undefined ? {} : { retryAt: value.retryAt }),
|
|
59
61
|
...(value.output === undefined ? {} : { output: value.output }),
|
|
60
62
|
...(value.error === undefined ? {} : { error: value.error }),
|
|
61
63
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AutomationStore, AutomationTarget, AutomationRun, DueCronTrigger, RunInput,
|
|
1
|
+
import type { AutomationStore, AutomationTarget, AutomationRun, DueCronTrigger, RunInput, StoredEventTrigger } from './types.js';
|
|
2
2
|
export interface SupabaseRpcClient {
|
|
3
3
|
rpc(functionName: string, arguments_: Record<string, unknown>): PromiseLike<{
|
|
4
4
|
data: unknown;
|
|
@@ -17,7 +17,4 @@ export declare class SupabaseStore implements AutomationStore {
|
|
|
17
17
|
kind: 'event';
|
|
18
18
|
}>, now: Date): Promise<AutomationRun | null>;
|
|
19
19
|
enqueueCron(trigger: DueCronTrigger, nextRunAt: string, now: Date): Promise<AutomationRun | null>;
|
|
20
|
-
pendingRuns(target: AutomationTarget): Promise<AutomationRun[]>;
|
|
21
|
-
markSent(runId: string, target: AutomationTarget, now: Date): Promise<boolean>;
|
|
22
|
-
updateRun(target: AutomationTarget, runId: string, update: RunUpdate): Promise<AutomationRun | null>;
|
|
23
20
|
}
|
|
@@ -61,30 +61,6 @@ export class SupabaseStore {
|
|
|
61
61
|
scheduledFor: trigger.nextRunAt,
|
|
62
62
|
}, now, nextRunAt);
|
|
63
63
|
}
|
|
64
|
-
async pendingRuns(target) {
|
|
65
|
-
const rows = await this.#call('list_amalgm_pending_runs', {
|
|
66
|
-
p_user_id: target.userId,
|
|
67
|
-
p_target_id: target.targetId,
|
|
68
|
-
});
|
|
69
|
-
return rows.map(automationRun);
|
|
70
|
-
}
|
|
71
|
-
markSent(runId, target, now) {
|
|
72
|
-
return this.#call('mark_amalgm_run_sent', {
|
|
73
|
-
p_run_id: runId,
|
|
74
|
-
p_user_id: target.userId,
|
|
75
|
-
p_target_id: target.targetId,
|
|
76
|
-
p_now: now.toISOString(),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
async updateRun(target, runId, update) {
|
|
80
|
-
const rows = await this.#call('update_amalgm_run', {
|
|
81
|
-
p_run_id: runId,
|
|
82
|
-
p_user_id: target.userId,
|
|
83
|
-
p_target_id: target.targetId,
|
|
84
|
-
p_update: update,
|
|
85
|
-
});
|
|
86
|
-
return rows[0] ? automationRun(rows[0]) : null;
|
|
87
|
-
}
|
|
88
64
|
async #enqueue(trigger, input, now, nextRunAt) {
|
|
89
65
|
const rows = await this.#call('enqueue_amalgm_run', {
|
|
90
66
|
p_kind: trigger.kind,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Automation, AutomationCrud, AutomationRun, CreateAutomation, CreateScheduleTrigger, CreateWebhookTrigger, CreateWorkflow, ListAutomations, ListRuns, Page, Trigger, UpdateAutomation, UpdateScheduleTrigger, UpdateWebhookTrigger, UpdateWorkflow, Workflow } from './contract.js';
|
|
2
|
+
export interface AutomationView {
|
|
3
|
+
automation: Automation;
|
|
4
|
+
triggers: Trigger[];
|
|
5
|
+
workflow: Workflow | null;
|
|
6
|
+
runs?: Page<AutomationRun>;
|
|
7
|
+
}
|
|
8
|
+
export interface CreateAutomationDefinition extends CreateAutomation {
|
|
9
|
+
schedules?: CreateScheduleTrigger[];
|
|
10
|
+
webhooks?: CreateWebhookTrigger[];
|
|
11
|
+
workflow?: CreateWorkflow;
|
|
12
|
+
}
|
|
13
|
+
interface Changes<Create extends {
|
|
14
|
+
id?: string;
|
|
15
|
+
}, Update> {
|
|
16
|
+
create?: Create[];
|
|
17
|
+
update?: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
patch: Update;
|
|
20
|
+
}>;
|
|
21
|
+
delete?: string[];
|
|
22
|
+
}
|
|
23
|
+
export type WorkflowChange = {
|
|
24
|
+
create: CreateWorkflow;
|
|
25
|
+
} | {
|
|
26
|
+
update: UpdateWorkflow;
|
|
27
|
+
} | {
|
|
28
|
+
delete: true;
|
|
29
|
+
};
|
|
30
|
+
export interface UpdateAutomationDefinition {
|
|
31
|
+
patch?: UpdateAutomation;
|
|
32
|
+
schedules?: Changes<CreateScheduleTrigger, UpdateScheduleTrigger>;
|
|
33
|
+
webhooks?: Changes<CreateWebhookTrigger, UpdateWebhookTrigger>;
|
|
34
|
+
workflow?: WorkflowChange;
|
|
35
|
+
}
|
|
36
|
+
export interface AutomationToolSurface {
|
|
37
|
+
create(input: CreateAutomationDefinition): Promise<AutomationView>;
|
|
38
|
+
list(query?: ListAutomations): Promise<Page<Automation>>;
|
|
39
|
+
get(automationId: string, runs?: false | ListRuns): Promise<AutomationView>;
|
|
40
|
+
update(automationId: string, input: UpdateAutomationDefinition): Promise<AutomationView>;
|
|
41
|
+
delete(automationId: string): Promise<{
|
|
42
|
+
deleted: string;
|
|
43
|
+
}>;
|
|
44
|
+
}
|
|
45
|
+
export declare function createAutomationToolSurface(sdk: AutomationCrud): AutomationToolSurface;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { NotFoundError, ValidationError } from './errors.js';
|
|
2
|
+
export function createAutomationToolSurface(sdk) {
|
|
3
|
+
const view = async (automationId, runs = false) => {
|
|
4
|
+
const automation = await sdk.automations.get(automationId);
|
|
5
|
+
if (!automation)
|
|
6
|
+
throw new NotFoundError('Automation');
|
|
7
|
+
const [triggers, workflow, history] = await Promise.all([
|
|
8
|
+
sdk.triggers.list(automationId),
|
|
9
|
+
sdk.workflow.get(automationId),
|
|
10
|
+
runs === false ? undefined : sdk.runs.list(automationId, runs),
|
|
11
|
+
]);
|
|
12
|
+
return { automation, triggers, workflow, ...(history ? { runs: history } : {}) };
|
|
13
|
+
};
|
|
14
|
+
const surface = {
|
|
15
|
+
async create(input) {
|
|
16
|
+
const { schedules = [], webhooks = [], workflow, ...automationInput } = input;
|
|
17
|
+
validateCreateIds(schedules, webhooks);
|
|
18
|
+
const staged = schedules.length > 0 || webhooks.length > 0 || workflow !== undefined;
|
|
19
|
+
const requestedEnabled = automationInput.enabled !== false;
|
|
20
|
+
const automation = await sdk.automations.create({
|
|
21
|
+
...automationInput,
|
|
22
|
+
...(staged ? { enabled: false } : {}),
|
|
23
|
+
});
|
|
24
|
+
try {
|
|
25
|
+
if (workflow)
|
|
26
|
+
await sdk.workflow.create(automation.id, workflow);
|
|
27
|
+
for (const schedule of schedules)
|
|
28
|
+
await sdk.triggers.schedule.create(automation.id, schedule);
|
|
29
|
+
for (const webhook of webhooks)
|
|
30
|
+
await sdk.triggers.webhook.create(automation.id, webhook);
|
|
31
|
+
if (staged && requestedEnabled)
|
|
32
|
+
await sdk.automations.update(automation.id, { enabled: true });
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw disabledDraftError(automation.id, error);
|
|
36
|
+
}
|
|
37
|
+
return view(automation.id);
|
|
38
|
+
},
|
|
39
|
+
list: (query = {}) => sdk.automations.list(query),
|
|
40
|
+
get: view,
|
|
41
|
+
async update(automationId, input) {
|
|
42
|
+
requireChanges(input);
|
|
43
|
+
validateChanges(input.schedules, 'schedule');
|
|
44
|
+
validateChanges(input.webhooks, 'webhook');
|
|
45
|
+
const current = await sdk.automations.get(automationId);
|
|
46
|
+
if (!current)
|
|
47
|
+
throw new NotFoundError('Automation');
|
|
48
|
+
const changesResources = hasChanges(input.schedules) || hasChanges(input.webhooks) || Boolean(input.workflow);
|
|
49
|
+
const finalEnabled = input.patch?.enabled ?? current.enabled;
|
|
50
|
+
const { enabled: _enabled, ...metadata } = input.patch ?? {};
|
|
51
|
+
if (!changesResources) {
|
|
52
|
+
await sdk.automations.update(automationId, input.patch);
|
|
53
|
+
return view(automationId);
|
|
54
|
+
}
|
|
55
|
+
if (current.enabled)
|
|
56
|
+
await sdk.automations.update(automationId, { enabled: false });
|
|
57
|
+
try {
|
|
58
|
+
await applyTriggerChanges(automationId, input.schedules, sdk.triggers.schedule);
|
|
59
|
+
await applyTriggerChanges(automationId, input.webhooks, sdk.triggers.webhook);
|
|
60
|
+
await applyWorkflowChange(automationId, input.workflow, sdk.workflow);
|
|
61
|
+
await sdk.automations.update(automationId, { ...metadata, enabled: finalEnabled });
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw disabledDraftError(automationId, error);
|
|
65
|
+
}
|
|
66
|
+
return view(automationId);
|
|
67
|
+
},
|
|
68
|
+
async delete(automationId) {
|
|
69
|
+
await sdk.automations.delete(automationId);
|
|
70
|
+
return { deleted: automationId };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
return Object.freeze(surface);
|
|
74
|
+
}
|
|
75
|
+
async function applyTriggerChanges(automationId, changes, operations) {
|
|
76
|
+
if (!changes)
|
|
77
|
+
return;
|
|
78
|
+
for (const triggerId of changes.delete ?? [])
|
|
79
|
+
await operations.delete(automationId, triggerId);
|
|
80
|
+
for (const item of changes.update ?? [])
|
|
81
|
+
await operations.update(automationId, item.id, item.patch);
|
|
82
|
+
for (const input of changes.create ?? [])
|
|
83
|
+
await operations.create(automationId, input);
|
|
84
|
+
}
|
|
85
|
+
async function applyWorkflowChange(automationId, change, workflow) {
|
|
86
|
+
if (!change)
|
|
87
|
+
return;
|
|
88
|
+
if ('create' in change)
|
|
89
|
+
await workflow.create(automationId, change.create);
|
|
90
|
+
else if ('update' in change)
|
|
91
|
+
await workflow.update(automationId, change.update);
|
|
92
|
+
else
|
|
93
|
+
await workflow.delete(automationId);
|
|
94
|
+
}
|
|
95
|
+
function requireChanges(input) {
|
|
96
|
+
if (!input.patch && !hasChanges(input.schedules) && !hasChanges(input.webhooks) && !input.workflow) {
|
|
97
|
+
throw new ValidationError('Automation update must include a change');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function hasChanges(changes) {
|
|
101
|
+
return Boolean(changes && [changes.create, changes.update, changes.delete]
|
|
102
|
+
.some((items) => items && items.length > 0));
|
|
103
|
+
}
|
|
104
|
+
function validateChanges(changes, label) {
|
|
105
|
+
if (!changes)
|
|
106
|
+
return;
|
|
107
|
+
const ids = [
|
|
108
|
+
...changes.create?.flatMap(({ id }) => id ? [id] : []) ?? [],
|
|
109
|
+
...changes.update?.map(({ id }) => id) ?? [],
|
|
110
|
+
...changes.delete ?? [],
|
|
111
|
+
];
|
|
112
|
+
if (new Set(ids).size !== ids.length) {
|
|
113
|
+
throw new ValidationError(`The same ${label} trigger cannot be changed twice`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function validateCreateIds(schedules, webhooks) {
|
|
117
|
+
const ids = [...schedules, ...webhooks].flatMap(({ id }) => id ? [id] : []);
|
|
118
|
+
if (new Set(ids).size !== ids.length) {
|
|
119
|
+
throw new ValidationError('Trigger ids must be unique within an automation');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function disabledDraftError(automationId, error) {
|
|
123
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
124
|
+
return new Error(`Automation ${automationId} remains disabled because configuration failed: ${reason}`);
|
|
125
|
+
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -51,13 +51,6 @@ export interface AutomationRun {
|
|
|
51
51
|
output?: Json;
|
|
52
52
|
error?: string;
|
|
53
53
|
}
|
|
54
|
-
export interface RunUpdate {
|
|
55
|
-
status: 'running' | 'completed' | 'failed';
|
|
56
|
-
startedAt?: string;
|
|
57
|
-
finishedAt?: string;
|
|
58
|
-
output?: Json;
|
|
59
|
-
error?: string;
|
|
60
|
-
}
|
|
61
54
|
export interface AutomationStore {
|
|
62
55
|
eventTriggers(target: AutomationTarget): Promise<StoredEventTrigger[]>;
|
|
63
56
|
dueCronTriggers(now: Date): Promise<DueCronTrigger[]>;
|
|
@@ -65,13 +58,5 @@ export interface AutomationStore {
|
|
|
65
58
|
kind: 'event';
|
|
66
59
|
}>, now: Date): Promise<AutomationRun | null>;
|
|
67
60
|
enqueueCron(trigger: DueCronTrigger, nextRunAt: string, now: Date): Promise<AutomationRun | null>;
|
|
68
|
-
pendingRuns(target: AutomationTarget): Promise<AutomationRun[]>;
|
|
69
|
-
markSent(runId: string, target: AutomationTarget, now: Date): Promise<boolean>;
|
|
70
|
-
updateRun(target: AutomationTarget, runId: string, update: RunUpdate): Promise<AutomationRun | null>;
|
|
71
|
-
}
|
|
72
|
-
/** The complete transport capability Automations needs from Core. */
|
|
73
|
-
export interface AutomationTransport {
|
|
74
|
-
isOnline(target: AutomationTarget): boolean;
|
|
75
|
-
send(run: AutomationRun): Promise<boolean>;
|
|
76
61
|
}
|
|
77
|
-
export type AutomationLog = (event: 'event.rejected' | 'run.pending'
|
|
62
|
+
export type AutomationLog = (event: 'event.rejected' | 'run.pending', details: Readonly<Record<string, string>>) => void;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amalgm/automations",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Amalgm's
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Amalgm's automation SDK: durable trigger admission and target-machine execution.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
41
|
"skills",
|
|
42
|
-
"supabase",
|
|
42
|
+
"supabase/migrations",
|
|
43
43
|
"AXIOMS.md",
|
|
44
44
|
"PURPOSE.md",
|
|
45
45
|
"README.md"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: automations
|
|
3
|
-
description: Create, inspect, change, or delete Amalgm
|
|
3
|
+
description: Create, inspect, change, or delete complete Amalgm automation definitions and inspect their run history through the Automations MCP tools.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Amalgm Automations
|
|
@@ -11,11 +11,8 @@ the agent adapter over the same hosted SDK used by the UI.
|
|
|
11
11
|
## Create a scheduled notification
|
|
12
12
|
|
|
13
13
|
For a request such as “remind me to call my mom every minute for the next ten
|
|
14
|
-
minutes,”
|
|
15
|
-
|
|
16
|
-
1. `amalgm_automations_create` with a clear name and the current machine target.
|
|
17
|
-
2. `amalgm_workflow_create` with a readable script summary and this compiled
|
|
18
|
-
plan:
|
|
14
|
+
minutes,” make one `amalgm_automations_create` call containing the complete
|
|
15
|
+
definition. Include a readable workflow summary and this compiled plan:
|
|
19
16
|
|
|
20
17
|
```json
|
|
21
18
|
{
|
|
@@ -30,19 +27,27 @@ minutes,” create three resources for the current machine:
|
|
|
30
27
|
}
|
|
31
28
|
```
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
Include one schedule with cron `* * * * *`, the user's timezone, and
|
|
31
|
+
`maxOccurrences: 10`. Omit `targetId` in a machine-bound session; never guess
|
|
32
|
+
an opaque target id.
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
The tool stages multi-resource configuration disabled and enables it only after
|
|
35
|
+
setup succeeds. If setup fails, report the returned disabled draft id. Never
|
|
36
|
+
replace a bounded occurrence count with an unbounded schedule plus a promise to
|
|
37
|
+
clean it up later.
|
|
40
38
|
|
|
41
39
|
## Read and change
|
|
42
40
|
|
|
43
|
-
Use
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
Use `amalgm_automations_list` or `amalgm_automations_get` before changing an
|
|
42
|
+
existing automation. Use `amalgm_automations_update` for grouped metadata,
|
|
43
|
+
schedule, webhook, or workflow changes, and `amalgm_automations_delete` only
|
|
44
|
+
after identifying the exact automation. Schedule and webhook triggers remain
|
|
45
|
+
distinct resources inside the definition. A workflow is zero-or-one per
|
|
46
|
+
automation. Run history is read-only, requested through `get`, and remains
|
|
47
|
+
after configuration deletion.
|
|
48
|
+
|
|
49
|
+
Action discovery belongs to the Tools product, not Automations. Manual
|
|
50
|
+
execution is not advertised until the delivery SDK supports it.
|
|
46
51
|
|
|
47
52
|
`pending` means a run is durable and waiting for its selected machine. `sent`
|
|
48
53
|
or `running` means that machine holds a lease. `completed` and `failed` are
|