@cynodia/axiom-runtime 0.7.0-alpha.2 → 0.8.0-alpha.1
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/dist/dom.d.ts +21 -0
- package/dist/runtime.d.ts +44 -0
- package/dist/runtime.js +142 -3
- package/package.json +2 -2
package/dist/dom.d.ts
CHANGED
|
@@ -52,6 +52,17 @@ export interface ConfirmationRequest {
|
|
|
52
52
|
/** A single line for hosts that can only ask a plain question. */
|
|
53
53
|
message: string;
|
|
54
54
|
}
|
|
55
|
+
export interface IntegrationQuerySuccess {
|
|
56
|
+
ok: true;
|
|
57
|
+
value: unknown;
|
|
58
|
+
}
|
|
59
|
+
export interface IntegrationQueryFailure {
|
|
60
|
+
ok: false;
|
|
61
|
+
code: string;
|
|
62
|
+
message: string;
|
|
63
|
+
retryable?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export type IntegrationQueryOutcome = IntegrationQuerySuccess | IntegrationQueryFailure;
|
|
55
66
|
/** Everything the runtime needs from its environment, so nothing is read from globals. */
|
|
56
67
|
export interface HostEnvironment {
|
|
57
68
|
document: DomDocument;
|
|
@@ -65,5 +76,15 @@ export interface HostEnvironment {
|
|
|
65
76
|
uuid(): string;
|
|
66
77
|
storage?: StorageAdapter;
|
|
67
78
|
report?(message: string): void;
|
|
79
|
+
/**
|
|
80
|
+
* Executes an integration query operation by id and returns its result.
|
|
81
|
+
*
|
|
82
|
+
* Only the authoritative runtime ever calls this, executing an `integration-query`
|
|
83
|
+
* operation: no client-compiled action ever contains one (integrations default
|
|
84
|
+
* server-only), so a browser host never needs to implement it.
|
|
85
|
+
*/
|
|
86
|
+
queryIntegration?(operationId: string, args: Record<string, unknown>, options: {
|
|
87
|
+
timeoutMs?: number;
|
|
88
|
+
}): Promise<IntegrationQueryOutcome>;
|
|
68
89
|
}
|
|
69
90
|
//# sourceMappingURL=dom.d.ts.map
|
package/dist/runtime.d.ts
CHANGED
|
@@ -36,6 +36,14 @@ export declare const RUNTIME_DIAGNOSTIC_CODES: {
|
|
|
36
36
|
readonly REMOTE_ACTION_UNAVAILABLE: "REMOTE_ACTION_UNAVAILABLE";
|
|
37
37
|
/** The authority could not be reached. Authoritative state was not loaded. */
|
|
38
38
|
readonly AUTHORITY_UNREACHABLE: "AUTHORITY_UNREACHABLE";
|
|
39
|
+
/** No host capable of executing an integration query is configured. */
|
|
40
|
+
readonly INTEGRATION_UNAVAILABLE: "INTEGRATION_UNAVAILABLE";
|
|
41
|
+
/** An integration query did not answer within its declared timeout. */
|
|
42
|
+
readonly INTEGRATION_TIMEOUT: "INTEGRATION_TIMEOUT";
|
|
43
|
+
/** A provider's response did not conform to the operation's declared result type. */
|
|
44
|
+
readonly INTEGRATION_RESULT_INVALID: "INTEGRATION_RESULT_INVALID";
|
|
45
|
+
/** An integration query failed. Never carries a provider secret. */
|
|
46
|
+
readonly INTEGRATION_QUERY_FAILED: "INTEGRATION_QUERY_FAILED";
|
|
39
47
|
};
|
|
40
48
|
export type RuntimeDiagnosticCode = (typeof RUNTIME_DIAGNOSTIC_CODES)[keyof typeof RUNTIME_DIAGNOSTIC_CODES];
|
|
41
49
|
/**
|
|
@@ -56,6 +64,24 @@ export interface RuntimeDiagnostic {
|
|
|
56
64
|
/** Structured context, so an agent never has to read the message. */
|
|
57
65
|
details?: Record<string, unknown>;
|
|
58
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Recorded intent to perform an external effect — appended when an `integration-effect`
|
|
69
|
+
* operation is reached, and discarded on rollback exactly like a mutation log entry is.
|
|
70
|
+
* Distinct from `MutationLogEntry`: an effect is not a state mutation (spec §73), and
|
|
71
|
+
* `outcome` here means whether the **intent was committed**, not whether the external
|
|
72
|
+
* effect itself succeeded — that is a separate, later question a host answers.
|
|
73
|
+
*/
|
|
74
|
+
export interface EffectIntentRecord {
|
|
75
|
+
id: string;
|
|
76
|
+
transactionId?: string;
|
|
77
|
+
actionId?: NodeId;
|
|
78
|
+
operationId: NodeId;
|
|
79
|
+
arguments: Record<string, unknown>;
|
|
80
|
+
idempotencyKey?: string;
|
|
81
|
+
succeededEventId?: NodeId;
|
|
82
|
+
failedEventId?: NodeId;
|
|
83
|
+
outcome?: 'committed' | 'rolled-back';
|
|
84
|
+
}
|
|
59
85
|
export interface ActionResult {
|
|
60
86
|
ok: boolean;
|
|
61
87
|
diagnostics: RuntimeDiagnostic[];
|
|
@@ -175,6 +201,12 @@ export interface AxiomRuntime {
|
|
|
175
201
|
getActionOutcome(id: NodeId): ActionOutcome | undefined;
|
|
176
202
|
/** Every mutation this runtime has applied, in order, with its semantic location. */
|
|
177
203
|
getMutationLog(): MutationLogEntry[];
|
|
204
|
+
/**
|
|
205
|
+
* Every `integration-effect` intent recorded so far, in order — a log distinct from the
|
|
206
|
+
* mutation log because an effect is not a state mutation. `outcome` reflects whether the
|
|
207
|
+
* *intent* was committed, not whether the external effect itself has run yet.
|
|
208
|
+
*/
|
|
209
|
+
getEffectIntents(): EffectIntentRecord[];
|
|
178
210
|
registerNativeOperation(implementationId: string, implementation: NativeImplementation): void;
|
|
179
211
|
/**
|
|
180
212
|
* Invokes an action and waits for its outcome. For a remote action this awaits the
|
|
@@ -207,6 +239,18 @@ export interface AxiomRuntime {
|
|
|
207
239
|
ok: false;
|
|
208
240
|
diagnostic: RuntimeDiagnostic;
|
|
209
241
|
};
|
|
242
|
+
/**
|
|
243
|
+
* Evaluates an expression with extra ids bound in scope, keyed by id — how a trigger's
|
|
244
|
+
* `arguments`/`enabledWhen` resolve `ref()` of the trigger's own id to read an event
|
|
245
|
+
* payload, the same way a `for-each` body resolves its scope id.
|
|
246
|
+
*/
|
|
247
|
+
evaluateWithBindings(expression: Expression, bindings: Record<string, unknown>): {
|
|
248
|
+
ok: true;
|
|
249
|
+
value: unknown;
|
|
250
|
+
} | {
|
|
251
|
+
ok: false;
|
|
252
|
+
diagnostic: RuntimeDiagnostic;
|
|
253
|
+
};
|
|
210
254
|
}
|
|
211
255
|
export declare function createAxiomRuntime(options: AxiomRuntimeOptions): AxiomRuntime;
|
|
212
256
|
/** Builds a host bound to the browser globals. Used by generated pages. */
|
package/dist/runtime.js
CHANGED
|
@@ -40,6 +40,14 @@ export const RUNTIME_DIAGNOSTIC_CODES = {
|
|
|
40
40
|
REMOTE_ACTION_UNAVAILABLE: 'REMOTE_ACTION_UNAVAILABLE',
|
|
41
41
|
/** The authority could not be reached. Authoritative state was not loaded. */
|
|
42
42
|
AUTHORITY_UNREACHABLE: 'AUTHORITY_UNREACHABLE',
|
|
43
|
+
/** No host capable of executing an integration query is configured. */
|
|
44
|
+
INTEGRATION_UNAVAILABLE: 'INTEGRATION_UNAVAILABLE',
|
|
45
|
+
/** An integration query did not answer within its declared timeout. */
|
|
46
|
+
INTEGRATION_TIMEOUT: 'INTEGRATION_TIMEOUT',
|
|
47
|
+
/** A provider's response did not conform to the operation's declared result type. */
|
|
48
|
+
INTEGRATION_RESULT_INVALID: 'INTEGRATION_RESULT_INVALID',
|
|
49
|
+
/** An integration query failed. Never carries a provider secret. */
|
|
50
|
+
INTEGRATION_QUERY_FAILED: 'INTEGRATION_QUERY_FAILED',
|
|
43
51
|
};
|
|
44
52
|
const MISSING = Symbol('missing');
|
|
45
53
|
function unwrapType(type) {
|
|
@@ -125,6 +133,7 @@ export function createAxiomRuntime(options) {
|
|
|
125
133
|
let started = false;
|
|
126
134
|
let transactionCounter = 0;
|
|
127
135
|
const mutationLog = [];
|
|
136
|
+
const effectIntentLog = [];
|
|
128
137
|
const inputValidation = options.inputValidation ?? 'immediate';
|
|
129
138
|
const remote = options.remote;
|
|
130
139
|
const remoteActionIds = new Set(ir.remoteActionIds ?? []);
|
|
@@ -436,6 +445,11 @@ export function createAxiomRuntime(options) {
|
|
|
436
445
|
entry.outcome = outcome;
|
|
437
446
|
}
|
|
438
447
|
}
|
|
448
|
+
for (const entry of effectIntentLog) {
|
|
449
|
+
if (entry.transactionId === transaction.id && entry.outcome === undefined) {
|
|
450
|
+
entry.outcome = outcome;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
439
453
|
}
|
|
440
454
|
/**
|
|
441
455
|
* Classifies an evaluation failure. The code says what kind of failure it was, so an
|
|
@@ -1068,6 +1082,33 @@ export function createAxiomRuntime(options) {
|
|
|
1068
1082
|
}
|
|
1069
1083
|
return;
|
|
1070
1084
|
}
|
|
1085
|
+
case 'integration-query':
|
|
1086
|
+
// The result was already resolved and bound into the action's scope before this
|
|
1087
|
+
// transaction opened — see `runActionAsync`. Nothing to do here; this case exists
|
|
1088
|
+
// only so the kind is recognized rather than falling to `default`.
|
|
1089
|
+
return;
|
|
1090
|
+
case 'integration-effect': {
|
|
1091
|
+
// Never calls an adapter here: reaching this operation only records intent, in the
|
|
1092
|
+
// same log a mutation is recorded in, discarded on rollback the same way. Dispatch
|
|
1093
|
+
// to the adapter happens only after the surrounding transaction commits.
|
|
1094
|
+
const args = {};
|
|
1095
|
+
for (const [key, argument] of Object.entries(operation.arguments ?? {})) {
|
|
1096
|
+
args[key] = cloneValue(evaluate(argument, scope));
|
|
1097
|
+
}
|
|
1098
|
+
effectIntentLog.push({
|
|
1099
|
+
id: host.uuid(),
|
|
1100
|
+
transactionId: context.transactionId,
|
|
1101
|
+
...(context.sourceNodeId ? { actionId: context.sourceNodeId } : {}),
|
|
1102
|
+
operationId: operation.operationId,
|
|
1103
|
+
arguments: args,
|
|
1104
|
+
...(operation.idempotencyKey
|
|
1105
|
+
? { idempotencyKey: toText(evaluate(operation.idempotencyKey, scope)) }
|
|
1106
|
+
: {}),
|
|
1107
|
+
...(operation.succeededEventId ? { succeededEventId: operation.succeededEventId } : {}),
|
|
1108
|
+
...(operation.failedEventId ? { failedEventId: operation.failedEventId } : {}),
|
|
1109
|
+
});
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1071
1112
|
default:
|
|
1072
1113
|
result.push({
|
|
1073
1114
|
code: RUNTIME_DIAGNOSTIC_CODES.UNSUPPORTED_OPERATION,
|
|
@@ -1204,14 +1245,14 @@ export function createAxiomRuntime(options) {
|
|
|
1204
1245
|
}
|
|
1205
1246
|
renderApplication();
|
|
1206
1247
|
}
|
|
1207
|
-
function runAction(actionId, args = {}) {
|
|
1248
|
+
function runAction(actionId, args = {}, presetBindings) {
|
|
1208
1249
|
const remoteAction = remoteActionIds.has(actionId) ? ir.actions[actionId] : undefined;
|
|
1209
1250
|
if (remoteAction) {
|
|
1210
1251
|
return runRemoteAction(remoteAction, args);
|
|
1211
1252
|
}
|
|
1212
1253
|
return collecting((collected) => {
|
|
1213
1254
|
const started = collected.length;
|
|
1214
|
-
const result = runActionCollecting(actionId, args, collected);
|
|
1255
|
+
const result = runActionCollecting(actionId, args, collected, presetBindings);
|
|
1215
1256
|
if (ir.actions[actionId]) {
|
|
1216
1257
|
// The record is this invocation's own diagnostics, so a later invocation of another
|
|
1217
1258
|
// action can never appear to belong to this one.
|
|
@@ -1226,9 +1267,87 @@ export function createAxiomRuntime(options) {
|
|
|
1226
1267
|
return result;
|
|
1227
1268
|
});
|
|
1228
1269
|
}
|
|
1270
|
+
function actionHasIntegrationQuery(action) {
|
|
1271
|
+
return (action.operations ?? []).some((operation) => operation.kind === 'integration-query');
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Resolves every top-level `integration-query` operation before the transaction opens,
|
|
1275
|
+
* then runs the action exactly as `runAction` would with the results bound into scope.
|
|
1276
|
+
*
|
|
1277
|
+
* Queries are awaited here, ahead of guards and the transaction — the same "none of this
|
|
1278
|
+
* mutates anything" phase preconditions already occupy — because a query's own validation
|
|
1279
|
+
* scope proves guards can never reference `ref(bindAs)`. `integration-effect` operations
|
|
1280
|
+
* need no async pre-step: they only record intent, synchronously, during the ordinary
|
|
1281
|
+
* operation loop.
|
|
1282
|
+
*/
|
|
1283
|
+
async function runActionAsync(actionId, args = {}) {
|
|
1284
|
+
const action = ir.actions[actionId];
|
|
1285
|
+
if (!action || !actionHasIntegrationQuery(action) || remoteActionIds.has(actionId)) {
|
|
1286
|
+
return runAction(actionId, args);
|
|
1287
|
+
}
|
|
1288
|
+
const scope = rootScope();
|
|
1289
|
+
for (const parameter of action.parameters ?? []) {
|
|
1290
|
+
scope.values.set(parameter.id, args[parameter.id] ?? null);
|
|
1291
|
+
}
|
|
1292
|
+
const presetBindings = new Map();
|
|
1293
|
+
for (const operation of action.operations ?? []) {
|
|
1294
|
+
if (operation.kind !== 'integration-query') {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const queryArgs = {};
|
|
1298
|
+
for (const [key, argument] of Object.entries(operation.arguments ?? {})) {
|
|
1299
|
+
queryArgs[key] = evaluate(argument, scope);
|
|
1300
|
+
}
|
|
1301
|
+
let outcome;
|
|
1302
|
+
try {
|
|
1303
|
+
outcome = await host.queryIntegration?.(operation.operationId, queryArgs, {
|
|
1304
|
+
...(operation.timeoutMs !== undefined ? { timeoutMs: operation.timeoutMs } : {}),
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
catch (error) {
|
|
1308
|
+
outcome = {
|
|
1309
|
+
ok: false,
|
|
1310
|
+
code: RUNTIME_DIAGNOSTIC_CODES.INTEGRATION_QUERY_FAILED,
|
|
1311
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
if (!outcome) {
|
|
1315
|
+
const diagnostic = {
|
|
1316
|
+
code: RUNTIME_DIAGNOSTIC_CODES.INTEGRATION_UNAVAILABLE,
|
|
1317
|
+
message: `${action.name ?? action.id} calls an integration query, but this host cannot perform one`,
|
|
1318
|
+
severity: 'error',
|
|
1319
|
+
nodeId: action.id,
|
|
1320
|
+
actionId: action.id,
|
|
1321
|
+
};
|
|
1322
|
+
report(diagnostic);
|
|
1323
|
+
recordOutcome(actionId, 'failed', [diagnostic]);
|
|
1324
|
+
renderApplication();
|
|
1325
|
+
return { ok: false, diagnostics: [diagnostic] };
|
|
1326
|
+
}
|
|
1327
|
+
if (!outcome.ok) {
|
|
1328
|
+
const diagnostic = {
|
|
1329
|
+
code: RUNTIME_DIAGNOSTIC_CODES[outcome.code]
|
|
1330
|
+
? outcome.code
|
|
1331
|
+
: RUNTIME_DIAGNOSTIC_CODES.INTEGRATION_QUERY_FAILED,
|
|
1332
|
+
message: outcome.message,
|
|
1333
|
+
severity: 'error',
|
|
1334
|
+
nodeId: action.id,
|
|
1335
|
+
actionId: action.id,
|
|
1336
|
+
details: { operationId: String(operation.operationId), retryable: outcome.retryable === true },
|
|
1337
|
+
};
|
|
1338
|
+
report(diagnostic);
|
|
1339
|
+
recordOutcome(actionId, 'failed', [diagnostic]);
|
|
1340
|
+
renderApplication();
|
|
1341
|
+
return { ok: false, diagnostics: [diagnostic] };
|
|
1342
|
+
}
|
|
1343
|
+
scope.values.set(operation.bindAs, outcome.value);
|
|
1344
|
+
presetBindings.set(operation.bindAs, outcome.value);
|
|
1345
|
+
}
|
|
1346
|
+
return runAction(actionId, args, presetBindings);
|
|
1347
|
+
}
|
|
1229
1348
|
/** Set when the most recent invocation stopped because a confirmation was declined. */
|
|
1230
1349
|
let cancelled = false;
|
|
1231
|
-
function runActionCollecting(actionId, args, collected) {
|
|
1350
|
+
function runActionCollecting(actionId, args, collected, presetBindings) {
|
|
1232
1351
|
const action = ir.actions[actionId];
|
|
1233
1352
|
if (!action) {
|
|
1234
1353
|
const failure = {
|
|
@@ -1243,6 +1362,11 @@ export function createAxiomRuntime(options) {
|
|
|
1243
1362
|
for (const parameter of action.parameters ?? []) {
|
|
1244
1363
|
scope.values.set(parameter.id, args[parameter.id] ?? null);
|
|
1245
1364
|
}
|
|
1365
|
+
// Integration query results resolved before this transaction opened (see
|
|
1366
|
+
// `runActionAsync`) — bound the same way a parameter is, so `ref(bindAs)` resolves.
|
|
1367
|
+
for (const [id, value] of presetBindings ?? []) {
|
|
1368
|
+
scope.values.set(id, value);
|
|
1369
|
+
}
|
|
1246
1370
|
const failures = [];
|
|
1247
1371
|
for (const parameter of action.parameters ?? []) {
|
|
1248
1372
|
if (parameter.required && !isPresent(scope.values.get(parameter.id))) {
|
|
@@ -2323,10 +2447,17 @@ export function createAxiomRuntime(options) {
|
|
|
2323
2447
|
getMutationLog() {
|
|
2324
2448
|
return mutationLog.map((entry) => ({ ...entry }));
|
|
2325
2449
|
},
|
|
2450
|
+
getEffectIntents() {
|
|
2451
|
+
return effectIntentLog.map((entry) => ({ ...entry }));
|
|
2452
|
+
},
|
|
2326
2453
|
registerNativeOperation(implementationId, implementation) {
|
|
2327
2454
|
natives.set(implementationId, implementation);
|
|
2328
2455
|
},
|
|
2329
2456
|
async invokeActionAsync(id, args = {}) {
|
|
2457
|
+
const action = ir.actions[id];
|
|
2458
|
+
if (action && actionHasIntegrationQuery(action) && !remoteActionIds.has(id)) {
|
|
2459
|
+
return runActionAsync(id, args);
|
|
2460
|
+
}
|
|
2330
2461
|
const result = runAction(id, args);
|
|
2331
2462
|
if (!result.pending) {
|
|
2332
2463
|
return result;
|
|
@@ -2343,6 +2474,14 @@ export function createAxiomRuntime(options) {
|
|
|
2343
2474
|
const outcome = tryEvaluate(expression, rootScope(), {});
|
|
2344
2475
|
return outcome.ok ? { ok: true, value: outcome.value } : { ok: false, diagnostic: outcome.diagnostic };
|
|
2345
2476
|
},
|
|
2477
|
+
evaluateWithBindings(expression, bindings) {
|
|
2478
|
+
let scope = rootScope();
|
|
2479
|
+
for (const [id, value] of Object.entries(bindings)) {
|
|
2480
|
+
scope = childScope(scope, id, value);
|
|
2481
|
+
}
|
|
2482
|
+
const outcome = tryEvaluate(expression, scope, {});
|
|
2483
|
+
return outcome.ok ? { ok: true, value: outcome.value } : { ok: false, diagnostic: outcome.diagnostic };
|
|
2484
|
+
},
|
|
2346
2485
|
};
|
|
2347
2486
|
}
|
|
2348
2487
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cynodia/axiom-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-alpha.1",
|
|
4
4
|
"description": "Domain-independent runtime that executes an Axiom application graph.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "AskTech AS",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
}
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@cynodia/axiom-core": "0.
|
|
34
|
+
"@cynodia/axiom-core": "0.8.0-alpha.1"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc -b tsconfig.json tsconfig.test.json",
|