apcore-js 0.16.0 → 0.17.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/README.md +1 -1
- package/dist/builtin-steps.d.ts +49 -16
- package/dist/builtin-steps.d.ts.map +1 -1
- package/dist/builtin-steps.js +123 -144
- package/dist/builtin-steps.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/executor.d.ts +27 -30
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +232 -395
- package/dist/executor.js.map +1 -1
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline-config.d.ts +68 -0
- package/dist/pipeline-config.d.ts.map +1 -0
- package/dist/pipeline-config.js +153 -0
- package/dist/pipeline-config.js.map +1 -0
- package/dist/pipeline.d.ts +22 -0
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +106 -2
- package/dist/pipeline.js.map +1 -1
- package/package.json +6 -6
package/dist/executor.js
CHANGED
|
@@ -4,20 +4,16 @@
|
|
|
4
4
|
* Async-only execution pipeline. Python's call() + call_async() merge into one async call().
|
|
5
5
|
* Timeout uses Promise.race instead of threading.
|
|
6
6
|
*/
|
|
7
|
-
import { Kind } from '@sinclair/typebox';
|
|
8
|
-
import { Value } from '@sinclair/typebox/value';
|
|
9
|
-
import { jsonSchemaToTypeBox } from './schema/loader.js';
|
|
10
|
-
import { createApprovalRequest } from './approval.js';
|
|
11
7
|
import { Context } from './context.js';
|
|
12
8
|
import { ExecutionCancelledError } from './cancel.js';
|
|
13
|
-
import {
|
|
9
|
+
import { InvalidInputError, ModuleError, } from './errors.js';
|
|
14
10
|
import { AfterMiddleware, BeforeMiddleware } from './middleware/index.js';
|
|
15
11
|
import { MiddlewareChainError, MiddlewareManager } from './middleware/manager.js';
|
|
16
|
-
import {
|
|
17
|
-
import { DEFAULT_ANNOTATIONS, createPreflightResult } from './module.js';
|
|
12
|
+
import { createPreflightResult } from './module.js';
|
|
18
13
|
import { MODULE_ID_PATTERN } from './registry/registry.js';
|
|
19
|
-
import { PipelineEngine, StrategyNotFoundError } from './pipeline.js';
|
|
20
|
-
import { buildStandardStrategy, buildInternalStrategy, buildTestingStrategy, buildPerformanceStrategy, } from './builtin-steps.js';
|
|
14
|
+
import { ExecutionStrategy, PipelineEngine, PipelineAbortError, StrategyNotFoundError } from './pipeline.js';
|
|
15
|
+
import { BuiltinACLCheck, BuiltinApprovalGate, buildStandardStrategy, buildInternalStrategy, buildTestingStrategy, buildPerformanceStrategy, buildMinimalStrategy, } from './builtin-steps.js';
|
|
16
|
+
import { propagateError } from './utils/error-propagation.js';
|
|
21
17
|
export const REDACTED_VALUE = '***REDACTED***';
|
|
22
18
|
/** Well-known context.data keys used internally by the runtime. */
|
|
23
19
|
export const CTX_GLOBAL_DEADLINE = '_apcore.executor.global_deadline';
|
|
@@ -76,27 +72,6 @@ function redactSecretPrefix(data) {
|
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
74
|
}
|
|
79
|
-
/**
|
|
80
|
-
* Normalize a dict-form annotations object into a ModuleAnnotations interface.
|
|
81
|
-
* Handles both camelCase and snake_case keys (parallel to Python's
|
|
82
|
-
* ``ModuleAnnotations(**{k: v for k, v in annotations.items() if k in valid_fields})``).
|
|
83
|
-
*/
|
|
84
|
-
function dictToAnnotations(dict) {
|
|
85
|
-
return {
|
|
86
|
-
readonly: Boolean(dict['readonly'] ?? false),
|
|
87
|
-
destructive: Boolean(dict['destructive'] ?? false),
|
|
88
|
-
idempotent: Boolean(dict['idempotent'] ?? false),
|
|
89
|
-
requiresApproval: Boolean(dict['requiresApproval'] ?? dict['requires_approval'] ?? false),
|
|
90
|
-
openWorld: Boolean(dict['openWorld'] ?? dict['open_world'] ?? true),
|
|
91
|
-
streaming: Boolean(dict['streaming'] ?? false),
|
|
92
|
-
cacheable: Boolean(dict['cacheable'] ?? false),
|
|
93
|
-
cacheTtl: Number(dict['cacheTtl'] ?? dict['cache_ttl'] ?? 0),
|
|
94
|
-
cacheKeyFields: (dict['cacheKeyFields'] ?? dict['cache_key_fields'] ?? null),
|
|
95
|
-
paginated: Boolean(dict['paginated'] ?? false),
|
|
96
|
-
paginationStyle: (dict['paginationStyle'] ?? dict['pagination_style'] ?? 'cursor'),
|
|
97
|
-
extra: Object.freeze(dict['extra'] ?? {}),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
75
|
export class Executor {
|
|
101
76
|
_registry;
|
|
102
77
|
_middlewareManager;
|
|
@@ -108,6 +83,7 @@ export class Executor {
|
|
|
108
83
|
_maxCallDepth;
|
|
109
84
|
_maxModuleRepeat;
|
|
110
85
|
_strategy;
|
|
86
|
+
_pipelineEngine;
|
|
111
87
|
/** Global strategy registry for name-based resolution. */
|
|
112
88
|
static _strategyRegistry = new Map();
|
|
113
89
|
constructor(options) {
|
|
@@ -133,11 +109,11 @@ export class Executor {
|
|
|
133
109
|
this._maxCallDepth = 32;
|
|
134
110
|
this._maxModuleRepeat = 3;
|
|
135
111
|
}
|
|
136
|
-
// Resolve strategy option
|
|
112
|
+
// Resolve strategy option (default to standard)
|
|
137
113
|
const strategyOpt = options.strategy;
|
|
138
114
|
if (strategyOpt === undefined || strategyOpt === null) {
|
|
139
|
-
//
|
|
140
|
-
this._strategy =
|
|
115
|
+
// Default to standard strategy (pipeline-first)
|
|
116
|
+
this._strategy = buildStandardStrategy(this._buildStrategyDeps());
|
|
141
117
|
}
|
|
142
118
|
else if (typeof strategyOpt === 'string') {
|
|
143
119
|
// Resolve by name from the global registry
|
|
@@ -150,6 +126,7 @@ export class Executor {
|
|
|
150
126
|
internal: buildInternalStrategy,
|
|
151
127
|
testing: buildTestingStrategy,
|
|
152
128
|
performance: buildPerformanceStrategy,
|
|
129
|
+
minimal: buildMinimalStrategy,
|
|
153
130
|
};
|
|
154
131
|
const factory = builtinFactories[strategyOpt];
|
|
155
132
|
if (factory !== undefined) {
|
|
@@ -167,6 +144,7 @@ export class Executor {
|
|
|
167
144
|
// ExecutionStrategy instance
|
|
168
145
|
this._strategy = strategyOpt;
|
|
169
146
|
}
|
|
147
|
+
this._pipelineEngine = new PipelineEngine();
|
|
170
148
|
}
|
|
171
149
|
/** Build the dependency bag for strategy factories. */
|
|
172
150
|
_buildStrategyDeps() {
|
|
@@ -196,7 +174,7 @@ export class Executor {
|
|
|
196
174
|
return null;
|
|
197
175
|
return target.info();
|
|
198
176
|
}
|
|
199
|
-
/** Get the current execution strategy
|
|
177
|
+
/** Get the current execution strategy. */
|
|
200
178
|
get currentStrategy() {
|
|
201
179
|
return this._strategy;
|
|
202
180
|
}
|
|
@@ -209,13 +187,25 @@ export class Executor {
|
|
|
209
187
|
get middlewares() {
|
|
210
188
|
return this._middlewareManager.snapshot();
|
|
211
189
|
}
|
|
212
|
-
/** Set the access control provider. */
|
|
190
|
+
/** Set the access control provider. Updates both the executor field and the strategy's ACL step. */
|
|
213
191
|
setAcl(acl) {
|
|
214
192
|
this._acl = acl;
|
|
193
|
+
for (const step of this._strategy.steps) {
|
|
194
|
+
if (step.name === 'acl_check' && step instanceof BuiltinACLCheck) {
|
|
195
|
+
step.setAcl(acl);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
215
199
|
}
|
|
216
|
-
/** Set the approval handler for Step 5 gate. */
|
|
200
|
+
/** Set the approval handler for Step 5 gate. Updates both the executor field and the strategy's approval step. */
|
|
217
201
|
setApprovalHandler(handler) {
|
|
218
202
|
this._approvalHandler = handler;
|
|
203
|
+
for (const step of this._strategy.steps) {
|
|
204
|
+
if (step.name === 'approval_gate' && step instanceof BuiltinApprovalGate) {
|
|
205
|
+
step.setApprovalHandler(handler);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
219
209
|
}
|
|
220
210
|
use(middleware) {
|
|
221
211
|
this._middlewareManager.add(middleware);
|
|
@@ -232,9 +222,45 @@ export class Executor {
|
|
|
232
222
|
remove(middleware) {
|
|
233
223
|
return this._middlewareManager.remove(middleware);
|
|
234
224
|
}
|
|
235
|
-
async call(moduleId, inputs, context,
|
|
236
|
-
|
|
237
|
-
|
|
225
|
+
async call(moduleId, inputs, context, versionHint) {
|
|
226
|
+
this._validateModuleId(moduleId);
|
|
227
|
+
const ctx = context != null ? context.child(moduleId) : Context.create(this).child(moduleId);
|
|
228
|
+
const pipeCtx = {
|
|
229
|
+
moduleId,
|
|
230
|
+
inputs: inputs ?? {},
|
|
231
|
+
context: ctx,
|
|
232
|
+
module: null,
|
|
233
|
+
validatedInputs: null,
|
|
234
|
+
output: null,
|
|
235
|
+
validatedOutput: null,
|
|
236
|
+
stream: false,
|
|
237
|
+
outputStream: null,
|
|
238
|
+
strategy: this._strategy,
|
|
239
|
+
trace: null,
|
|
240
|
+
versionHint: versionHint ?? null,
|
|
241
|
+
};
|
|
242
|
+
try {
|
|
243
|
+
const [output, _trace] = await this._pipelineEngine.run(this._strategy, pipeCtx);
|
|
244
|
+
return (output ?? {});
|
|
245
|
+
}
|
|
246
|
+
catch (exc) {
|
|
247
|
+
if (exc instanceof ExecutionCancelledError)
|
|
248
|
+
throw exc;
|
|
249
|
+
// Pipeline errors propagate with their original types (ModuleNotFoundError, etc.)
|
|
250
|
+
// because builtin steps now throw directly.
|
|
251
|
+
const ctxObj = pipeCtx.context;
|
|
252
|
+
const wrapped = propagateError(exc, moduleId, ctxObj);
|
|
253
|
+
const executedMw = pipeCtx.executedMiddlewares;
|
|
254
|
+
if (executedMw && executedMw.length > 0) {
|
|
255
|
+
const recovery = this._middlewareManager.executeOnError(moduleId, pipeCtx.inputs, wrapped, ctxObj, executedMw);
|
|
256
|
+
if (recovery !== null)
|
|
257
|
+
return recovery;
|
|
258
|
+
}
|
|
259
|
+
if (exc instanceof MiddlewareChainError) {
|
|
260
|
+
throw new ModuleError('MODULE_EXECUTE_ERROR', String(exc), { moduleId });
|
|
261
|
+
}
|
|
262
|
+
throw wrapped;
|
|
263
|
+
}
|
|
238
264
|
}
|
|
239
265
|
/**
|
|
240
266
|
* Alias for call(). Provided for compatibility with MCP bridge packages
|
|
@@ -250,9 +276,6 @@ export class Executor {
|
|
|
250
276
|
*/
|
|
251
277
|
async callWithTrace(moduleId, inputs, context, options) {
|
|
252
278
|
const strategy = options?.strategy ?? this._strategy;
|
|
253
|
-
if (strategy === null) {
|
|
254
|
-
throw new InvalidInputError('callWithTrace requires a pipeline strategy. Set one on the Executor or pass via options.');
|
|
255
|
-
}
|
|
256
279
|
const ctx = context ?? Context.create(this).child(moduleId);
|
|
257
280
|
const pipelineCtx = {
|
|
258
281
|
moduleId,
|
|
@@ -267,8 +290,7 @@ export class Executor {
|
|
|
267
290
|
strategy,
|
|
268
291
|
trace: null,
|
|
269
292
|
};
|
|
270
|
-
const
|
|
271
|
-
const [output, trace] = await engine.run(strategy, pipelineCtx);
|
|
293
|
+
const [output, trace] = await this._pipelineEngine.run(strategy, pipelineCtx);
|
|
272
294
|
return [(output ?? {}), trace];
|
|
273
295
|
}
|
|
274
296
|
/**
|
|
@@ -282,191 +304,103 @@ export class Executor {
|
|
|
282
304
|
* validation/side-effects but its return value is not yielded since chunks were already
|
|
283
305
|
* emitted. In the non-streaming fallback, after-middleware can transform the output.
|
|
284
306
|
*/
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Streaming execution pipeline using split-pipeline design.
|
|
309
|
+
*
|
|
310
|
+
* Phase 1: Pipeline runs all steps with ctx.stream=true. BuiltinExecute detects
|
|
311
|
+
* stream mode and sets ctx.outputStream if module has stream().
|
|
312
|
+
* Phase 2: Iterate stream, accumulate chunks and yield each.
|
|
313
|
+
* Phase 3: Run output_validation + middleware_after on accumulated output.
|
|
314
|
+
*
|
|
315
|
+
* If the module has no stream(), the pipeline executes normally and yields ctx.output.
|
|
316
|
+
*/
|
|
317
|
+
async *stream(moduleId, inputs, context, versionHint) {
|
|
318
|
+
this._validateModuleId(moduleId);
|
|
319
|
+
const ctx = context != null ? context.child(moduleId) : Context.create(this).child(moduleId);
|
|
320
|
+
const pipeCtx = {
|
|
321
|
+
moduleId,
|
|
322
|
+
inputs: inputs ?? {},
|
|
323
|
+
context: ctx,
|
|
324
|
+
module: null,
|
|
325
|
+
validatedInputs: null,
|
|
326
|
+
output: null,
|
|
327
|
+
validatedOutput: null,
|
|
328
|
+
stream: true,
|
|
329
|
+
outputStream: null,
|
|
330
|
+
strategy: this._strategy,
|
|
331
|
+
trace: null,
|
|
332
|
+
versionHint: versionHint ?? null,
|
|
333
|
+
};
|
|
334
|
+
// Phase 1: Run the full pipeline. BuiltinExecute detects ctx.stream=true.
|
|
292
335
|
try {
|
|
293
|
-
|
|
294
|
-
[effectiveInputs, executedMiddlewares] = this._middlewareManager.executeBefore(moduleId, effectiveInputs, ctx);
|
|
295
|
-
}
|
|
296
|
-
catch (e) {
|
|
297
|
-
if (e instanceof MiddlewareChainError) {
|
|
298
|
-
executedMiddlewares = e.executedMiddlewares;
|
|
299
|
-
const recovery = this._middlewareManager.executeOnError(moduleId, effectiveInputs, e.original, ctx, executedMiddlewares);
|
|
300
|
-
if (recovery !== null) {
|
|
301
|
-
yield recovery;
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
executedMiddlewares = [];
|
|
305
|
-
throw e.original;
|
|
306
|
-
}
|
|
307
|
-
throw e;
|
|
308
|
-
}
|
|
309
|
-
// Cancel check before execution
|
|
310
|
-
if (ctx.cancelToken !== null) {
|
|
311
|
-
ctx.cancelToken.check();
|
|
312
|
-
}
|
|
313
|
-
const streamFn = mod['stream'];
|
|
314
|
-
if (typeof streamFn === 'function') {
|
|
315
|
-
// Module has a stream() method: iterate and yield each chunk
|
|
316
|
-
let accumulated = {};
|
|
317
|
-
for await (const chunk of streamFn.call(mod, effectiveInputs, ctx)) {
|
|
318
|
-
accumulated = { ...accumulated, ...chunk };
|
|
319
|
-
yield chunk;
|
|
320
|
-
}
|
|
321
|
-
// Validate accumulated output against output schema
|
|
322
|
-
this._validateOutput(mod, accumulated);
|
|
323
|
-
// Run after-middleware on the accumulated result
|
|
324
|
-
this._middlewareManager.executeAfter(moduleId, effectiveInputs, accumulated, ctx);
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
327
|
-
// Fallback: execute normally and yield single chunk
|
|
328
|
-
let output = await this._executeWithTimeout(mod, moduleId, effectiveInputs, ctx);
|
|
329
|
-
this._validateOutput(mod, output);
|
|
330
|
-
output = this._middlewareManager.executeAfter(moduleId, effectiveInputs, output, ctx);
|
|
331
|
-
yield output;
|
|
332
|
-
}
|
|
336
|
+
await this._pipelineEngine.run(this._strategy, pipeCtx);
|
|
333
337
|
}
|
|
334
338
|
catch (exc) {
|
|
335
339
|
if (exc instanceof ExecutionCancelledError)
|
|
336
340
|
throw exc;
|
|
337
|
-
|
|
338
|
-
|
|
341
|
+
const ctxObj = pipeCtx.context;
|
|
342
|
+
const wrapped = propagateError(exc, moduleId, ctxObj);
|
|
343
|
+
if (pipeCtx.executedMiddlewares && pipeCtx.executedMiddlewares.length > 0) {
|
|
344
|
+
const recovery = this._middlewareManager.executeOnError(moduleId, pipeCtx.inputs, wrapped, ctxObj, pipeCtx.executedMiddlewares);
|
|
339
345
|
if (recovery !== null) {
|
|
340
346
|
yield recovery;
|
|
341
347
|
return;
|
|
342
348
|
}
|
|
343
349
|
}
|
|
344
|
-
throw
|
|
350
|
+
throw wrapped;
|
|
345
351
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
*/
|
|
350
|
-
async _prepareExecution(moduleId, inputs, context) {
|
|
351
|
-
let effectiveInputs = inputs ?? {};
|
|
352
|
-
const ctx = this._createContext(moduleId, context);
|
|
353
|
-
// Set global deadline on root call only
|
|
354
|
-
if (!(CTX_GLOBAL_DEADLINE in ctx.data) && this._globalTimeout > 0) {
|
|
355
|
-
ctx.data[CTX_GLOBAL_DEADLINE] = Date.now() + this._globalTimeout;
|
|
356
|
-
}
|
|
357
|
-
this._checkSafety(moduleId, ctx);
|
|
358
|
-
const mod = this._lookupModule(moduleId);
|
|
359
|
-
this._checkAcl(moduleId, ctx);
|
|
360
|
-
// Step 5 -- Approval Gate (strips internal keys like _approval_token)
|
|
361
|
-
effectiveInputs = await this._checkApproval(mod, moduleId, effectiveInputs, ctx);
|
|
362
|
-
effectiveInputs = this._validateInputs(mod, effectiveInputs, ctx);
|
|
363
|
-
return { mod, effectiveInputs, ctx };
|
|
364
|
-
}
|
|
365
|
-
_createContext(moduleId, context) {
|
|
366
|
-
if (context == null) {
|
|
367
|
-
return Context.create(this).child(moduleId);
|
|
368
|
-
}
|
|
369
|
-
return context.child(moduleId);
|
|
370
|
-
}
|
|
371
|
-
_lookupModule(moduleId) {
|
|
372
|
-
const module = this._registry.get(moduleId);
|
|
373
|
-
if (module === null) {
|
|
374
|
-
throw new ModuleNotFoundError(moduleId);
|
|
375
|
-
}
|
|
376
|
-
return module;
|
|
377
|
-
}
|
|
378
|
-
_checkAcl(moduleId, ctx) {
|
|
379
|
-
if (this._acl !== null) {
|
|
380
|
-
const allowed = this._acl.check(ctx.callerId, moduleId, ctx);
|
|
381
|
-
if (!allowed) {
|
|
382
|
-
throw new ACLDeniedError(ctx.callerId, moduleId);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
_validateInputs(mod, inputs, ctx) {
|
|
387
|
-
const inputSchema = this._resolveSchema(mod, 'inputSchema');
|
|
388
|
-
if (inputSchema == null)
|
|
389
|
-
return inputs;
|
|
390
|
-
this._validateSchema(inputSchema, inputs, 'Input');
|
|
391
|
-
ctx.redactedInputs = redactSensitive(inputs, inputSchema);
|
|
392
|
-
return inputs;
|
|
393
|
-
}
|
|
394
|
-
_resolveSchema(mod, key) {
|
|
395
|
-
const schema = mod[key];
|
|
396
|
-
if (schema == null)
|
|
397
|
-
return null;
|
|
398
|
-
if (Kind in schema)
|
|
399
|
-
return schema;
|
|
400
|
-
const converted = jsonSchemaToTypeBox(schema);
|
|
401
|
-
mod[key] = converted;
|
|
402
|
-
return converted;
|
|
403
|
-
}
|
|
404
|
-
_validateSchema(schema, data, direction) {
|
|
405
|
-
if (Value.Check(schema, data))
|
|
352
|
+
// If no outputStream, pipeline already executed normally — yield single result
|
|
353
|
+
if (pipeCtx.outputStream == null) {
|
|
354
|
+
yield (pipeCtx.output ?? {});
|
|
406
355
|
return;
|
|
407
|
-
const errors = [];
|
|
408
|
-
for (const error of Value.Errors(schema, data)) {
|
|
409
|
-
errors.push({
|
|
410
|
-
field: error.path || '/',
|
|
411
|
-
code: String(error.type),
|
|
412
|
-
message: error.message,
|
|
413
|
-
});
|
|
414
356
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
let effectiveInputs = inputs;
|
|
419
|
-
let executedMiddlewares = [];
|
|
357
|
+
// Phase 2: Iterate stream, accumulate chunks
|
|
358
|
+
const outputStream = pipeCtx.outputStream;
|
|
359
|
+
let accumulated = {};
|
|
420
360
|
try {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
catch (e) {
|
|
425
|
-
if (e instanceof MiddlewareChainError) {
|
|
426
|
-
executedMiddlewares = e.executedMiddlewares;
|
|
427
|
-
const recovery = this._middlewareManager.executeOnError(moduleId, effectiveInputs, e.original, ctx, executedMiddlewares);
|
|
428
|
-
if (recovery !== null)
|
|
429
|
-
return recovery;
|
|
430
|
-
executedMiddlewares = [];
|
|
431
|
-
throw e.original;
|
|
432
|
-
}
|
|
433
|
-
throw e;
|
|
361
|
+
for await (const chunk of outputStream) {
|
|
362
|
+
accumulated = { ...accumulated, ...chunk };
|
|
363
|
+
yield chunk;
|
|
434
364
|
}
|
|
435
|
-
// Cancel check before execution
|
|
436
|
-
if (ctx.cancelToken !== null) {
|
|
437
|
-
ctx.cancelToken.check();
|
|
438
|
-
}
|
|
439
|
-
let output = await this._executeWithTimeout(mod, moduleId, effectiveInputs, ctx);
|
|
440
|
-
this._validateOutput(mod, output);
|
|
441
|
-
output = this._middlewareManager.executeAfter(moduleId, effectiveInputs, output, ctx);
|
|
442
|
-
return output;
|
|
443
365
|
}
|
|
444
366
|
catch (exc) {
|
|
445
367
|
if (exc instanceof ExecutionCancelledError)
|
|
446
368
|
throw exc;
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
369
|
+
const ctxObj = pipeCtx.context;
|
|
370
|
+
const wrapped = propagateError(exc, moduleId, ctxObj);
|
|
371
|
+
if (pipeCtx.executedMiddlewares && pipeCtx.executedMiddlewares.length > 0) {
|
|
372
|
+
const recovery = this._middlewareManager.executeOnError(moduleId, pipeCtx.inputs, wrapped, ctxObj, pipeCtx.executedMiddlewares);
|
|
373
|
+
if (recovery !== null) {
|
|
374
|
+
yield recovery;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
451
377
|
}
|
|
452
|
-
throw
|
|
378
|
+
throw wrapped;
|
|
453
379
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
458
|
-
|
|
380
|
+
// Phase 3: Output validation + middleware_after on accumulated result
|
|
381
|
+
pipeCtx.output = accumulated;
|
|
382
|
+
const postSteps = this._strategy.steps.filter((s) => s.name === 'output_validation' || s.name === 'middleware_after' || s.name === 'return_result');
|
|
383
|
+
if (postSteps.length > 0) {
|
|
384
|
+
const postStrategy = new ExecutionStrategy('post_stream', postSteps);
|
|
385
|
+
try {
|
|
386
|
+
await this._pipelineEngine.run(postStrategy, pipeCtx);
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
// Post-stream validation errors are non-fatal for already-yielded chunks
|
|
390
|
+
}
|
|
459
391
|
}
|
|
460
392
|
}
|
|
461
393
|
/**
|
|
462
|
-
* Non-destructive preflight check
|
|
463
|
-
*
|
|
394
|
+
* Non-destructive preflight check using pipeline dry_run mode.
|
|
395
|
+
*
|
|
396
|
+
* Runs all pure steps (context creation, call chain guard, module lookup,
|
|
397
|
+
* ACL, input validation). Steps with pure=false (approval, middleware,
|
|
398
|
+
* execute) are automatically skipped. Returns a PreflightResult.
|
|
464
399
|
*/
|
|
465
|
-
validate(moduleId, inputs, context) {
|
|
400
|
+
async validate(moduleId, inputs, context) {
|
|
466
401
|
const effectiveInputs = inputs ?? {};
|
|
467
402
|
const checks = [];
|
|
468
|
-
|
|
469
|
-
// Check 1: module_id format
|
|
403
|
+
// Check 0: module_id format (before pipeline)
|
|
470
404
|
if (!MODULE_ID_PATTERN.test(moduleId)) {
|
|
471
405
|
checks.push({
|
|
472
406
|
check: 'module_id', passed: false,
|
|
@@ -475,100 +409,117 @@ export class Executor {
|
|
|
475
409
|
return createPreflightResult(checks);
|
|
476
410
|
}
|
|
477
411
|
checks.push({ check: 'module_id', passed: true });
|
|
478
|
-
//
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
412
|
+
// Run pipeline in dry_run mode — pure=false steps are skipped
|
|
413
|
+
const pipeCtx = {
|
|
414
|
+
moduleId,
|
|
415
|
+
inputs: effectiveInputs,
|
|
416
|
+
context: context ?? Context.create(this).child(moduleId),
|
|
417
|
+
module: null,
|
|
418
|
+
validatedInputs: null,
|
|
419
|
+
output: null,
|
|
420
|
+
validatedOutput: null,
|
|
421
|
+
stream: false,
|
|
422
|
+
outputStream: null,
|
|
423
|
+
strategy: this._strategy,
|
|
424
|
+
trace: null,
|
|
425
|
+
dryRun: true,
|
|
426
|
+
};
|
|
427
|
+
let trace = null;
|
|
491
428
|
try {
|
|
492
|
-
this.
|
|
493
|
-
|
|
429
|
+
const [, t] = await this._pipelineEngine.run(this._strategy, pipeCtx);
|
|
430
|
+
trace = t;
|
|
494
431
|
}
|
|
495
432
|
catch (e) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
checks.push({ check: 'call_chain', passed: false, error: err });
|
|
500
|
-
}
|
|
501
|
-
// Check 4: ACL
|
|
502
|
-
if (this._acl !== null) {
|
|
503
|
-
const allowed = this._acl.check(ctx.callerId, moduleId, ctx);
|
|
504
|
-
if (!allowed) {
|
|
505
|
-
checks.push({
|
|
506
|
-
check: 'acl', passed: false,
|
|
507
|
-
error: { code: 'ACL_DENIED', message: `Access denied: ${ctx.callerId} -> ${moduleId}` },
|
|
508
|
-
});
|
|
433
|
+
// Step raised a domain error (ModuleNotFoundError, ACLDeniedError, etc.)
|
|
434
|
+
if (e instanceof PipelineAbortError) {
|
|
435
|
+
trace = e.pipelineTrace;
|
|
509
436
|
}
|
|
510
437
|
else {
|
|
511
|
-
|
|
438
|
+
const errorDict = (e instanceof ModuleError)
|
|
439
|
+
? { code: e.code, message: e.message }
|
|
440
|
+
: { code: e.constructor?.name ?? 'Error', message: String(e) };
|
|
441
|
+
const code = (e instanceof ModuleError) ? e.code : e.constructor?.name ?? 'Error';
|
|
442
|
+
let checkName;
|
|
443
|
+
if (code === 'MODULE_NOT_FOUND')
|
|
444
|
+
checkName = 'module_lookup';
|
|
445
|
+
else if (code === 'ACL_DENIED')
|
|
446
|
+
checkName = 'acl';
|
|
447
|
+
else if (code === 'SCHEMA_VALIDATION_ERROR' || code === 'INVALID_INPUT')
|
|
448
|
+
checkName = 'schema';
|
|
449
|
+
else if (code === 'CALL_DEPTH_EXCEEDED' || code === 'CIRCULAR_CALL' || code === 'CALL_FREQUENCY_EXCEEDED')
|
|
450
|
+
checkName = 'call_chain';
|
|
451
|
+
else
|
|
452
|
+
checkName = 'unknown';
|
|
453
|
+
checks.push({ check: checkName, passed: false, error: errorDict });
|
|
512
454
|
}
|
|
513
455
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
// Check 5: approval detection (report only, no handler invocation)
|
|
518
|
-
if (this._needsApproval(mod)) {
|
|
519
|
-
requiresApproval = true;
|
|
456
|
+
// Convert pipeline trace to PreflightResult checks
|
|
457
|
+
if (trace !== null) {
|
|
458
|
+
checks.push(...this._traceToChecks(trace));
|
|
520
459
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
460
|
+
// Detect requires_approval
|
|
461
|
+
let requiresApproval = false;
|
|
462
|
+
if (pipeCtx.module != null) {
|
|
463
|
+
requiresApproval = this._needsApproval(pipeCtx.module);
|
|
464
|
+
}
|
|
465
|
+
// Module-level preflight (optional)
|
|
466
|
+
if (pipeCtx.module != null) {
|
|
467
|
+
const mod = pipeCtx.module;
|
|
468
|
+
const modWithPreflight = mod;
|
|
469
|
+
if (typeof modWithPreflight.preflight === 'function') {
|
|
470
|
+
try {
|
|
471
|
+
const preflightWarnings = modWithPreflight.preflight(effectiveInputs, pipeCtx.context);
|
|
472
|
+
if (Array.isArray(preflightWarnings) && preflightWarnings.length > 0) {
|
|
473
|
+
checks.push({ check: 'module_preflight', passed: true, warnings: preflightWarnings });
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
checks.push({ check: 'module_preflight', passed: true });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (exc) {
|
|
480
|
+
const excName = exc instanceof Error ? exc.constructor.name : 'Error';
|
|
481
|
+
const excMsg = exc instanceof Error ? exc.message : String(exc);
|
|
482
|
+
checks.push({
|
|
483
|
+
check: 'module_preflight',
|
|
484
|
+
passed: true,
|
|
485
|
+
warnings: [`preflight() raised ${excName}: ${excMsg}`],
|
|
535
486
|
});
|
|
536
487
|
}
|
|
537
|
-
checks.push({
|
|
538
|
-
check: 'schema', passed: false,
|
|
539
|
-
error: { code: 'SCHEMA_VALIDATION_ERROR', errors },
|
|
540
|
-
});
|
|
541
488
|
}
|
|
542
489
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
}
|
|
490
|
+
return createPreflightResult(checks, requiresApproval);
|
|
491
|
+
}
|
|
492
|
+
/** Map pipeline step names to PreflightResult check names. */
|
|
493
|
+
static _STEP_TO_CHECK = {
|
|
494
|
+
context_creation: 'context',
|
|
495
|
+
call_chain_guard: 'call_chain',
|
|
496
|
+
module_lookup: 'module_lookup',
|
|
497
|
+
acl_check: 'acl',
|
|
498
|
+
approval_gate: 'approval',
|
|
499
|
+
middleware_before: 'middleware',
|
|
500
|
+
input_validation: 'schema',
|
|
501
|
+
};
|
|
502
|
+
/** Convert PipelineTrace steps to PreflightCheckResult list. */
|
|
503
|
+
_traceToChecks(trace) {
|
|
504
|
+
const checks = [];
|
|
505
|
+
for (const st of trace.steps) {
|
|
506
|
+
if (st.skipped)
|
|
507
|
+
continue;
|
|
508
|
+
const checkName = Executor._STEP_TO_CHECK[st.name] ?? st.name;
|
|
509
|
+
const passed = st.result.action !== 'abort';
|
|
510
|
+
let error;
|
|
511
|
+
if (!passed && st.result.explanation) {
|
|
512
|
+
error = { code: `STEP_${st.name.toUpperCase()}_FAILED`, message: st.result.explanation };
|
|
566
513
|
}
|
|
514
|
+
checks.push({ check: checkName, passed, error });
|
|
567
515
|
}
|
|
568
|
-
return
|
|
516
|
+
return checks;
|
|
569
517
|
}
|
|
570
|
-
|
|
571
|
-
|
|
518
|
+
/** Validate module_id format at public entry points. */
|
|
519
|
+
_validateModuleId(moduleId) {
|
|
520
|
+
if (!moduleId || !MODULE_ID_PATTERN.test(moduleId)) {
|
|
521
|
+
throw new InvalidInputError(`Invalid module ID: '${moduleId}'. Must match pattern: ${MODULE_ID_PATTERN.source}`);
|
|
522
|
+
}
|
|
572
523
|
}
|
|
573
524
|
/** Check if a module requires approval, handling both interface and dict annotations. */
|
|
574
525
|
_needsApproval(mod) {
|
|
@@ -577,127 +528,13 @@ export class Executor {
|
|
|
577
528
|
return false;
|
|
578
529
|
if (typeof annotations !== 'object')
|
|
579
530
|
return false;
|
|
580
|
-
// ModuleAnnotations interface (camelCase)
|
|
581
531
|
if ('requiresApproval' in annotations) {
|
|
582
532
|
return Boolean(annotations.requiresApproval);
|
|
583
533
|
}
|
|
584
|
-
// Dict-form annotations (snake_case)
|
|
585
534
|
if ('requires_approval' in annotations) {
|
|
586
535
|
return Boolean(annotations['requires_approval']);
|
|
587
536
|
}
|
|
588
537
|
return false;
|
|
589
538
|
}
|
|
590
|
-
/** Build an ApprovalRequest from module metadata. */
|
|
591
|
-
_buildApprovalRequest(mod, moduleId, inputs, ctx) {
|
|
592
|
-
const annotations = mod['annotations'];
|
|
593
|
-
let ann;
|
|
594
|
-
if (annotations != null && typeof annotations === 'object' && 'requiresApproval' in annotations) {
|
|
595
|
-
ann = annotations;
|
|
596
|
-
}
|
|
597
|
-
else if (annotations != null && typeof annotations === 'object') {
|
|
598
|
-
ann = dictToAnnotations(annotations);
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
ann = DEFAULT_ANNOTATIONS;
|
|
602
|
-
}
|
|
603
|
-
return createApprovalRequest({
|
|
604
|
-
moduleId,
|
|
605
|
-
arguments: inputs,
|
|
606
|
-
context: ctx,
|
|
607
|
-
annotations: ann,
|
|
608
|
-
description: mod['description'] ?? null,
|
|
609
|
-
tags: mod['tags'] ?? [],
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
/** Map an ApprovalResult status to the appropriate action or error. */
|
|
613
|
-
_handleApprovalResult(result, moduleId) {
|
|
614
|
-
if (result.status === 'approved')
|
|
615
|
-
return;
|
|
616
|
-
if (result.status === 'rejected') {
|
|
617
|
-
throw new ApprovalDeniedError(result, moduleId);
|
|
618
|
-
}
|
|
619
|
-
if (result.status === 'timeout') {
|
|
620
|
-
throw new ApprovalTimeoutError(result, moduleId);
|
|
621
|
-
}
|
|
622
|
-
if (result.status === 'pending') {
|
|
623
|
-
throw new ApprovalPendingError(result, moduleId);
|
|
624
|
-
}
|
|
625
|
-
// Unknown status treated as denied
|
|
626
|
-
console.warn(`[apcore:executor] Unknown approval status '${result.status}' for module ${moduleId}, treating as denied`);
|
|
627
|
-
throw new ApprovalDeniedError(result, moduleId);
|
|
628
|
-
}
|
|
629
|
-
/** Emit an audit event for the approval decision (logging + span event). */
|
|
630
|
-
_emitApprovalEvent(result, moduleId, ctx) {
|
|
631
|
-
console.warn(`[apcore:executor] Approval decision: module=${moduleId} status=${result.status} approved_by=${result.approvedBy} reason=${result.reason}`);
|
|
632
|
-
const spansStack = ctx.data[CTX_TRACING_SPANS];
|
|
633
|
-
if (spansStack && spansStack.length > 0) {
|
|
634
|
-
spansStack[spansStack.length - 1].events.push({
|
|
635
|
-
name: 'approval_decision',
|
|
636
|
-
module_id: moduleId,
|
|
637
|
-
status: result.status,
|
|
638
|
-
approved_by: result.approvedBy ?? '',
|
|
639
|
-
reason: result.reason ?? '',
|
|
640
|
-
approval_id: result.approvalId ?? '',
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
/** Step 5: Approval gate. Returns inputs with internal keys stripped. */
|
|
645
|
-
async _checkApproval(mod, moduleId, inputs, ctx) {
|
|
646
|
-
if (this._approvalHandler === null)
|
|
647
|
-
return inputs;
|
|
648
|
-
if (!this._needsApproval(mod))
|
|
649
|
-
return inputs;
|
|
650
|
-
let result;
|
|
651
|
-
let cleanInputs = inputs;
|
|
652
|
-
if ('_approval_token' in inputs) {
|
|
653
|
-
const token = inputs['_approval_token'];
|
|
654
|
-
const { _approval_token: _, ...rest } = inputs;
|
|
655
|
-
cleanInputs = rest;
|
|
656
|
-
result = await this._approvalHandler.checkApproval(token);
|
|
657
|
-
}
|
|
658
|
-
else {
|
|
659
|
-
const request = this._buildApprovalRequest(mod, moduleId, inputs, ctx);
|
|
660
|
-
result = await this._approvalHandler.requestApproval(request);
|
|
661
|
-
}
|
|
662
|
-
this._emitApprovalEvent(result, moduleId, ctx);
|
|
663
|
-
this._handleApprovalResult(result, moduleId);
|
|
664
|
-
return cleanInputs;
|
|
665
|
-
}
|
|
666
|
-
async _executeWithTimeout(mod, moduleId, inputs, ctx) {
|
|
667
|
-
let timeoutMs = this._defaultTimeout;
|
|
668
|
-
// Respect global deadline: use whichever is shorter
|
|
669
|
-
const globalDeadline = ctx.data[CTX_GLOBAL_DEADLINE];
|
|
670
|
-
if (globalDeadline !== undefined) {
|
|
671
|
-
const remaining = globalDeadline - Date.now();
|
|
672
|
-
if (remaining <= 0) {
|
|
673
|
-
throw new ModuleTimeoutError(moduleId, 0);
|
|
674
|
-
}
|
|
675
|
-
if (timeoutMs === 0 || remaining < timeoutMs) {
|
|
676
|
-
timeoutMs = remaining;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
if (timeoutMs < 0) {
|
|
680
|
-
throw new InvalidInputError(`Negative timeout: ${timeoutMs}ms`);
|
|
681
|
-
}
|
|
682
|
-
const executeFn = mod['execute'];
|
|
683
|
-
if (typeof executeFn !== 'function') {
|
|
684
|
-
throw new InvalidInputError(`Module '${moduleId}' has no execute method`);
|
|
685
|
-
}
|
|
686
|
-
const executionPromise = Promise.resolve(executeFn
|
|
687
|
-
.call(mod, inputs, ctx));
|
|
688
|
-
if (timeoutMs === 0) {
|
|
689
|
-
console.warn('[apcore:executor] Timeout is 0, timeout limit disabled');
|
|
690
|
-
return executionPromise;
|
|
691
|
-
}
|
|
692
|
-
let timer;
|
|
693
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
694
|
-
timer = setTimeout(() => {
|
|
695
|
-
reject(new ModuleTimeoutError(moduleId, timeoutMs));
|
|
696
|
-
}, timeoutMs);
|
|
697
|
-
});
|
|
698
|
-
return Promise.race([executionPromise, timeoutPromise]).finally(() => {
|
|
699
|
-
clearTimeout(timer);
|
|
700
|
-
});
|
|
701
|
-
}
|
|
702
539
|
}
|
|
703
540
|
//# sourceMappingURL=executor.js.map
|