Skip to content

Commit bbbf2e8

Browse files
committed
feat: Add OTLP tracing exporter and Module.preflight() method, and standardize audit and retry logging to warn level."
1 parent f54d2f6 commit bbbf2e8

File tree

9 files changed

+28
-29
lines changed

9 files changed

+28
-29
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.12.0] - 2026-03-10
8+
## [0.12.0] - 2026-03-11
99

1010
### Added
1111
- **`Module.preflight()`** — Optional method for domain-specific pre-execution warnings (spec §5.6)
@@ -81,7 +81,7 @@ Built-in `system.*` modules that allow AI agents to query, monitor
8181
- **Optional methods** added to `Module` interface: `stream?()`, `validate?()`, `onLoad?()`, `onUnload?()`.
8282

8383
#### Error Hierarchy
84-
- **`NotImplementedError`** — New error class for `GENERAL_NOT_IMPLEMENTED` code.
84+
- **`FeatureNotImplementedError`** — New error class for `GENERAL_NOT_IMPLEMENTED` code.
8585
- **`DependencyNotFoundError`** — New error class for `DEPENDENCY_NOT_FOUND` code.
8686

8787
### Changed
@@ -427,6 +427,7 @@ Built-in `system.*` modules that allow AI agents to query, monitor
427427

428428
---
429429

430+
[0.12.0]: https://github.com/aipartnerup/apcore-typescript/compare/v0.11.0...v0.12.0
430431
[0.11.0]: https://github.com/aipartnerup/apcore-typescript/compare/v0.10.0...v0.11.0
431432
[0.10.0]: https://github.com/aipartnerup/apcore-typescript/compare/v0.9.0...v0.10.0
432433
[0.9.0]: https://github.com/aipartnerup/apcore-typescript/compare/v0.8.0...v0.9.0

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ src/
128128
cancel.ts # Cancellation token support
129129
decorator.ts # FunctionModule class and helpers
130130
bindings.ts # YAML binding loader
131-
errors.ts # Error hierarchy (35 typed errors)
131+
errors.ts # Error hierarchy (36 typed errors)
132132
error-code-registry.ts # Custom error code registration with collision detection
133133
extensions.ts # Extension manager
134134
module.ts # Module types and annotations

src/events/subscribers.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,16 @@ export class WebhookSubscriber implements EventSubscriber {
4545
let lastError: unknown = null;
4646

4747
for (let attempt = 0; attempt < attempts; attempt++) {
48+
const controller = new AbortController();
49+
const timer = setTimeout(() => controller.abort(), this._timeoutMs);
4850
try {
49-
const controller = new AbortController();
50-
const timer = setTimeout(() => controller.abort(), this._timeoutMs);
51-
5251
const response = await fetch(this._url, {
5352
method: 'POST',
5453
headers: mergedHeaders,
5554
body: JSON.stringify(payload),
5655
signal: controller.signal,
5756
});
5857

59-
clearTimeout(timer);
60-
6158
if (response.status < 500) {
6259
if (response.status >= 400) {
6360
console.warn(
@@ -81,6 +78,8 @@ export class WebhookSubscriber implements EventSubscriber {
8178
`Webhook ${this._url} failed (attempt ${attempt + 1}/${attempts}):`,
8279
err,
8380
);
81+
} finally {
82+
clearTimeout(timer);
8483
}
8584
}
8685

@@ -145,19 +144,16 @@ export class A2ASubscriber implements EventSubscriber {
145144
}
146145
}
147146

147+
const controller = new AbortController();
148+
const timer = setTimeout(() => controller.abort(), this._timeoutMs);
148149
try {
149-
const controller = new AbortController();
150-
const timer = setTimeout(() => controller.abort(), this._timeoutMs);
151-
152150
const response = await fetch(this._platformUrl, {
153151
method: 'POST',
154152
headers,
155153
body: JSON.stringify(payload),
156154
signal: controller.signal,
157155
});
158156

159-
clearTimeout(timer);
160-
161157
if (response.status >= 400) {
162158
console.warn(
163159
'[apcore:events]',
@@ -170,6 +166,8 @@ export class A2ASubscriber implements EventSubscriber {
170166
`A2A delivery to ${this._platformUrl} failed for event ${event.eventType}:`,
171167
err,
172168
);
169+
} finally {
170+
clearTimeout(timer);
173171
}
174172
}
175173
}

src/executor.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
import { AfterMiddleware, BeforeMiddleware, Middleware } from './middleware/index.js';
2828
import { MiddlewareChainError, MiddlewareManager } from './middleware/manager.js';
2929
import { guardCallChain } from './utils/call-chain.js';
30-
import type { ModuleAnnotations, PreflightCheckResult, PreflightResult } from './module.js';
30+
import type { Module, ModuleAnnotations, PreflightCheckResult, PreflightResult } from './module.js';
3131
import { DEFAULT_ANNOTATIONS, createPreflightResult } from './module.js';
3232
import { MODULE_ID_PATTERN } from './registry/registry.js';
3333
import type { Registry } from './registry/registry.js';
@@ -554,9 +554,10 @@ export class Executor {
554554
}
555555

556556
// Check 7: module-level preflight (optional)
557-
if (typeof (mod as any).preflight === 'function') {
557+
const modWithPreflight = mod as { preflight?: Module['preflight'] };
558+
if (typeof modWithPreflight.preflight === 'function') {
558559
try {
559-
const preflightWarnings = (mod as any).preflight(effectiveInputs, ctx);
560+
const preflightWarnings = modWithPreflight.preflight(effectiveInputs, ctx);
560561
if (Array.isArray(preflightWarnings) && preflightWarnings.length > 0) {
561562
checks.push({ check: 'module_preflight', passed: true, warnings: preflightWarnings });
562563
} else {
@@ -642,7 +643,7 @@ export class Executor {
642643

643644
/** Emit an audit event for the approval decision (logging + span event). */
644645
private _emitApprovalEvent(result: ApprovalResult, moduleId: string, ctx: Context): void {
645-
console.info(
646+
console.warn(
646647
`[apcore:executor] Approval decision: module=${moduleId} status=${result.status} approved_by=${result.approvedBy} reason=${result.reason}`,
647648
);
648649

src/middleware/retry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class RetryMiddleware extends Middleware {
6565
const delayMs = this._calculateDelay(retryCount);
6666
context.data[retryKey] = retryCount + 1;
6767

68-
console.info(
68+
console.warn(
6969
`[apcore:retry] Retrying module '${moduleId}' (attempt ${retryCount + 1}/${this._config.maxRetries}) after ${Math.round(delayMs)}ms`,
7070
);
7171

src/observability/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { TracingMiddleware, StdoutExporter, InMemoryExporter, createSpan } from './tracing.js';
1+
export { TracingMiddleware, StdoutExporter, InMemoryExporter, OTLPExporter, createSpan } from './tracing.js';
22
export type { Span, SpanExporter } from './tracing.js';
33
export { MetricsCollector, MetricsMiddleware } from './metrics.js';
44
export { ContextLogger, ObsLoggingMiddleware } from './context-logger.js';

src/observability/tracing.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ export class OTLPExporter implements SpanExporter {
125125
...this._headers,
126126
},
127127
body: JSON.stringify(payload),
128-
}).catch(() => {
129-
// Silently ignore network errors
128+
}).catch((err: unknown) => {
129+
console.warn('[apcore:tracing] OTLP export failed:', err);
130130
});
131131
}
132132
}

src/registry/schema-export.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
* Schema query and export functions for the registry system.
33
*/
44

5-
import type { TSchema } from '@sinclair/typebox';
65
import yaml from 'js-yaml';
76
import type { ModuleAnnotations, ModuleExample } from '../module.js';
87
import { InvalidInputError, ModuleNotFoundError } from '../errors.js';

tests/test-approval-executor.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ describe('ApprovalAuditEvents', () => {
431431
it('emits audit log on approved', async () => {
432432
const registry = createTestRegistry();
433433
const executor = new Executor({ registry, approvalHandler: new AutoApproveHandler() });
434-
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
434+
const infoSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
435435
try {
436436
await executor.call('test.approval_required');
437437
const approvalLogs = infoSpy.mock.calls.filter(
@@ -448,7 +448,7 @@ describe('ApprovalAuditEvents', () => {
448448
it('emits audit log on denied', async () => {
449449
const registry = createTestRegistry();
450450
const executor = new Executor({ registry, approvalHandler: new AlwaysDenyHandler() });
451-
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
451+
const infoSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
452452
try {
453453
await expect(executor.call('test.approval_required')).rejects.toThrow(ApprovalDeniedError);
454454
const approvalLogs = infoSpy.mock.calls.filter(
@@ -467,7 +467,7 @@ describe('ApprovalAuditEvents', () => {
467467
createApprovalResult({ status: 'pending', approvalId: 'tok-123' }),
468468
);
469469
const executor = new Executor({ registry, approvalHandler: handler });
470-
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
470+
const infoSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
471471
try {
472472
await expect(executor.call('test.approval_required')).rejects.toThrow(ApprovalPendingError);
473473
const approvalLogs = infoSpy.mock.calls.filter(
@@ -483,7 +483,7 @@ describe('ApprovalAuditEvents', () => {
483483
it('does not emit audit log when gate is skipped', async () => {
484484
const registry = createTestRegistry();
485485
const executor = new Executor({ registry });
486-
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
486+
const infoSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
487487
try {
488488
await executor.call('test.approval_required');
489489
const approvalLogs = infoSpy.mock.calls.filter(
@@ -504,7 +504,7 @@ describe('ApprovalAuditEvents', () => {
504504
createApprovalResult({ status: 'approved', approvedBy: 'test-user', reason: 'looks good' }),
505505
);
506506
const executor = new Executor({ registry, approvalHandler: handler });
507-
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
507+
const infoSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
508508

509509
try {
510510
const ctx = Context.create(executor);
@@ -529,7 +529,7 @@ describe('ApprovalAuditEvents', () => {
529529
const mockSpan = { events: mockSpanEvents };
530530

531531
const executor = new Executor({ registry, approvalHandler: new AlwaysDenyHandler() });
532-
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
532+
const infoSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
533533

534534
try {
535535
const ctx = Context.create(executor);
@@ -547,7 +547,7 @@ describe('ApprovalAuditEvents', () => {
547547
it('works without tracing spans', async () => {
548548
const registry = createTestRegistry();
549549
const executor = new Executor({ registry, approvalHandler: new AutoApproveHandler() });
550-
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
550+
const infoSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
551551
try {
552552
const result = await executor.call('test.approval_required');
553553
expect(result['status']).toBe('executed');

0 commit comments

Comments
 (0)