depa-actor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * depa-actor — Core Type Definitions
3
+ *
4
+ * Synthesizes:
5
+ * - depa-data-graph ActorSystem (true actor model, mailbox queue, microtask drain)
6
+ * - depa-processor (dispatch routing, DOP pipeline)
7
+ * - OneAgentActor domain needs (multi-mailbox, typed tags)
8
+ */
9
+
10
+ // ─── MailboxSchema ───────────────────────────────────────────────────
11
+ // Record<tag, payload> — each key is a mailbox name, value is payload type.
12
+ // Example: { cancel: boolean; human_input: string; tool_call: ToolCallData }
13
+
14
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
15
+ export type MailboxSchema = Record<string, unknown>;
16
+
17
+ // ─── ActorEnvelope ───────────────────────────────────────────────────
18
+ // tag + payload replaces the old single `msg` field.
19
+
20
+ export interface ActorEnvelope<TSchema extends MailboxSchema = MailboxSchema> {
21
+ id: number;
22
+ ts: number;
23
+ from: string;
24
+ to: string;
25
+ tag: keyof TSchema & string;
26
+ payload: TSchema[keyof TSchema & string];
27
+ }
28
+
29
+ /** Type-safe envelope for a specific tag */
30
+ export type TaggedEnvelope<
31
+ TSchema extends MailboxSchema,
32
+ TTag extends keyof TSchema & string,
33
+ > = Omit<ActorEnvelope<TSchema>, 'tag' | 'payload'> & {
34
+ tag: TTag;
35
+ payload: TSchema[TTag];
36
+ };
37
+
38
+ // ─── Mailbox Priority ────────────────────────────────────────────────
39
+
40
+ export type MailboxPriority<TSchema extends MailboxSchema> = {
41
+ [K in keyof TSchema & string]?: number;
42
+ };
43
+
44
+ // ─── ActorRef ────────────────────────────────────────────────────────
45
+ // Capability-based reference: can send, no management.
46
+
47
+ export interface ActorRef<TSchema extends MailboxSchema = MailboxSchema> {
48
+ readonly id: string;
49
+ send<TTag extends keyof TSchema & string>(tag: TTag, payload: TSchema[TTag]): void;
50
+ }
51
+
52
+ // ─── ActorSelf ───────────────────────────────────────────────────────
53
+ // Handler-side reference with state access and selective receive.
54
+
55
+ export interface ActorSelf<
56
+ TRuntime,
57
+ TSchema extends MailboxSchema,
58
+ TState = void,
59
+ > {
60
+ readonly id: string;
61
+ readonly ref: ActorRef<TSchema>;
62
+ readonly runtime: TRuntime;
63
+ state: TState;
64
+
65
+ /** Send to a specific actor */
66
+ send(to: string, tag: keyof TSchema & string, payload: TSchema[keyof TSchema & string]): void;
67
+
68
+ /** Broadcast to all actors */
69
+ broadcast(
70
+ tag: keyof TSchema & string,
71
+ payload: TSchema[keyof TSchema & string],
72
+ opts?: { excludeSelf?: boolean },
73
+ ): void;
74
+
75
+ // ── Selective Receive ──
76
+ /** Check if there are pending messages for a specific tag */
77
+ hasPending<TTag extends keyof TSchema & string>(tag: TTag): boolean;
78
+
79
+ /** Drain all pending messages for a specific tag (removes them from queue) */
80
+ drainMailbox<TTag extends keyof TSchema & string>(tag: TTag): TaggedEnvelope<TSchema, TTag>[];
81
+ }
82
+
83
+ // ─── ActorHandler ────────────────────────────────────────────────────
84
+
85
+ /** Unified handler — receives all envelopes, switch on tag */
86
+ export type ActorHandler<
87
+ TRuntime,
88
+ TSchema extends MailboxSchema,
89
+ TState = void,
90
+ > = (
91
+ self: ActorSelf<TRuntime, TSchema, TState>,
92
+ envelope: ActorEnvelope<TSchema>,
93
+ ) => void | Promise<void>;
94
+
95
+ /** Per-tag handler — handles a single tag */
96
+ export type TagHandler<
97
+ TRuntime,
98
+ TSchema extends MailboxSchema,
99
+ TState,
100
+ TTag extends keyof TSchema & string,
101
+ > = (
102
+ self: ActorSelf<TRuntime, TSchema, TState>,
103
+ envelope: TaggedEnvelope<TSchema, TTag>,
104
+ ) => void | Promise<void>;
105
+
106
+ // ─── ActorDef ────────────────────────────────────────────────────────
107
+ // Definition for registering an actor. Supports both unified and per-tag handlers.
108
+
109
+ export interface ActorDef<
110
+ TRuntime,
111
+ TSchema extends MailboxSchema,
112
+ TState = void,
113
+ > {
114
+ initialState: TState;
115
+
116
+ /** Mailbox priority — lower number = higher priority. Default: 100 */
117
+ priority?: MailboxPriority<TSchema>;
118
+
119
+ /** Unified handler (fallback for tags not in `handlers`) */
120
+ handler?: ActorHandler<TRuntime, TSchema, TState>;
121
+
122
+ /** Per-tag handlers — takes precedence over `handler` for matching tags */
123
+ handlers?: {
124
+ [TTag in keyof TSchema & string]?: TagHandler<TRuntime, TSchema, TState, TTag>;
125
+ };
126
+ }
127
+
128
+ // ─── Log ─────────────────────────────────────────────────────────────
129
+
130
+ export type ActorLogKind = 'send' | 'deliver' | 'error';
131
+
132
+ export interface ActorLogEntry<TSchema extends MailboxSchema = MailboxSchema>
133
+ extends ActorEnvelope<TSchema> {
134
+ kind: ActorLogKind;
135
+ error?: string;
136
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * depa-actor — Dispatch Bridge
3
+ *
4
+ * Optional bridge to depa-processor DispatchEngine.
5
+ * Only this file imports from depa-processor concepts.
6
+ * No hard dependency — uses structural typing.
7
+ */
8
+
9
+ import type {
10
+ MailboxSchema,
11
+ ActorSelf,
12
+ ActorEnvelope,
13
+ ActorHandler,
14
+ } from '../core/types';
15
+
16
+ // ─── DispatchRoute (structural, no import) ───────────────────────────
17
+
18
+ /**
19
+ * A dispatch route maps a tag to a dispatch key and handler.
20
+ * This is a structural interface — no dependency on depa-processor.
21
+ */
22
+ export interface DispatchRoute<
23
+ TRuntime,
24
+ TSchema extends MailboxSchema,
25
+ TState,
26
+ > {
27
+ /** Which tags this route handles */
28
+ tags: (keyof TSchema & string)[];
29
+
30
+ /** Resolve dispatch key from envelope */
31
+ resolveKey: (envelope: ActorEnvelope<TSchema>) => string;
32
+
33
+ /** Route table: dispatch key → handler */
34
+ routes: Record<string, (
35
+ self: ActorSelf<TRuntime, TSchema, TState>,
36
+ envelope: ActorEnvelope<TSchema>,
37
+ ) => void | Promise<void>>;
38
+
39
+ /** Fallback if no route matches */
40
+ fallback?: (
41
+ self: ActorSelf<TRuntime, TSchema, TState>,
42
+ envelope: ActorEnvelope<TSchema>,
43
+ key: string,
44
+ ) => void | Promise<void>;
45
+ }
46
+
47
+ // ─── createDispatchHandler ───────────────────────────────────────────
48
+
49
+ /**
50
+ * Creates an ActorHandler that routes envelopes through dispatch routes.
51
+ * Falls back to `defaultHandler` for tags not covered by any route.
52
+ */
53
+ export function createDispatchHandler<
54
+ TRuntime,
55
+ TSchema extends MailboxSchema,
56
+ TState,
57
+ >(
58
+ routes: DispatchRoute<TRuntime, TSchema, TState>[],
59
+ defaultHandler?: ActorHandler<TRuntime, TSchema, TState>,
60
+ ): ActorHandler<TRuntime, TSchema, TState> {
61
+ // Build tag → route index for O(1) lookup
62
+ const tagIndex = new Map<string, DispatchRoute<TRuntime, TSchema, TState>>();
63
+ for (const route of routes) {
64
+ for (const tag of route.tags) {
65
+ tagIndex.set(tag, route);
66
+ }
67
+ }
68
+
69
+ return async (self, envelope) => {
70
+ const route = tagIndex.get(envelope.tag);
71
+
72
+ if (route) {
73
+ const key = route.resolveKey(envelope);
74
+ const handler = route.routes[key];
75
+ if (handler) {
76
+ await handler(self, envelope);
77
+ } else if (route.fallback) {
78
+ await route.fallback(self, envelope, key);
79
+ }
80
+ } else if (defaultHandler) {
81
+ await defaultHandler(self, envelope);
82
+ }
83
+ };
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ // depa-actor — Core
2
+ export type {
3
+ MailboxSchema,
4
+ ActorEnvelope,
5
+ TaggedEnvelope,
6
+ MailboxPriority,
7
+ ActorRef,
8
+ ActorSelf,
9
+ ActorHandler,
10
+ TagHandler,
11
+ ActorDef,
12
+ ActorLogKind,
13
+ ActorLogEntry,
14
+ } from './core/types';
15
+
16
+ export { ActorSystem } from './core/ActorSystem';
17
+
18
+ // depa-actor — Runtime
19
+ export type { ActorPlugin } from './runtime/ActorRuntime';
20
+ export { ActorRuntime } from './runtime/ActorRuntime';
21
+ export {
22
+ CompletionSignalRegistry,
23
+ CompletionBindingRegistry,
24
+ createCompletionSignalRegistry,
25
+ createCompletionBindingRegistry,
26
+ } from './runtime/completion';
27
+ export type {
28
+ CompletionWaiter,
29
+ } from './runtime/completion';
30
+ export type {
31
+ SnapshotRecoveryState,
32
+ RuntimeSnapshotManifestBase,
33
+ RuntimeRootSnapshotBase,
34
+ ActorSnapshotBase,
35
+ FiberSnapshotBase,
36
+ SnapshotCodec,
37
+ RecoveryHooks,
38
+ PersistenceEffectPort,
39
+ } from './runtime/snapshot';
40
+ export {
41
+ createSnapshotCodec,
42
+ createRecoveryHooks,
43
+ createPersistenceEffectPort,
44
+ } from './runtime/snapshot';
45
+ export { RuntimeIndexHook, createRuntimeIndexHook } from './runtime/indexing';
46
+
47
+ // depa-actor — Pipeline (DOP bridge)
48
+ export type {
49
+ ActorPipelineDef,
50
+ PipelineDerivedAdapter,
51
+ PipelineInnerRuntimeAdapter,
52
+ PipelineInnerInputAdapter,
53
+ PipelineInnerConfigAdapter,
54
+ PipelineCoreLogic,
55
+ PipelineOutputAdapter,
56
+ } from './pipeline/ActorPipeline';
57
+ export { createPipelineHandler } from './pipeline/ActorPipeline';
58
+
59
+ // depa-actor — Dispatch bridge
60
+ export type { DispatchRoute } from './dispatch/ActorDispatchAdapter';
61
+ export { createDispatchHandler } from './dispatch/ActorDispatchAdapter';
62
+
63
+ // depa-actor — Orchestration (Fiber Scheduling)
64
+ export type {
65
+ FiberId,
66
+ FiberStatus,
67
+ FiberWaitingReason,
68
+ SuspendPolicy,
69
+ SchedulerHooks,
70
+ FiberStep,
71
+ FiberRecord,
72
+ SpawnFiberInput,
73
+ DeadLetterRecord,
74
+ OrchestratorOptions,
75
+ OrchestratorState,
76
+ FiberAction,
77
+ FiberEffect,
78
+ ReduceResult,
79
+ ScheduleResult,
80
+ } from './orchestration';
81
+
82
+ export {
83
+ DEFAULT_ORCHESTRATOR_OPTIONS,
84
+ createOrchestratorState,
85
+ reduceOrchestrator,
86
+ applyFailure,
87
+ computeEffectivePriority,
88
+ selectNextFiberId,
89
+ scheduleOne,
90
+ createAiAgentSchedulerHooks,
91
+ dispatchEffects,
92
+ } from './orchestration';
@@ -0,0 +1,35 @@
1
+ // depa-actor — Orchestration (Fiber Scheduling)
2
+
3
+ export type {
4
+ FiberId,
5
+ FiberStatus,
6
+ FiberWaitingReason,
7
+ SuspendPolicy,
8
+ SchedulerHooks,
9
+ FiberStep,
10
+ FiberRecord,
11
+ SpawnFiberInput,
12
+ DeadLetterRecord,
13
+ OrchestratorOptions,
14
+ OrchestratorState,
15
+ FiberAction,
16
+ FiberEffect,
17
+ ReduceResult,
18
+ } from './types';
19
+
20
+ export { DEFAULT_ORCHESTRATOR_OPTIONS } from './types';
21
+
22
+ export { createOrchestratorState, reduceOrchestrator } from './reducer';
23
+
24
+ export { applyFailure } from './recovery';
25
+
26
+ export {
27
+ computeEffectivePriority,
28
+ selectNextFiberId,
29
+ scheduleOne,
30
+ } from './scheduler';
31
+ export type { ScheduleResult } from './scheduler';
32
+
33
+ export { createAiAgentSchedulerHooks } from './presets/aiAgent';
34
+
35
+ export { dispatchEffects } from './runtimeAdapter';
@@ -0,0 +1,48 @@
1
+ import type { MailboxSchema } from '../../core/types';
2
+
3
+ import type { FiberRecord, OrchestratorState, SchedulerHooks, SuspendPolicy } from '../types';
4
+
5
+ function isAiHumanWaitReason(reason: unknown): boolean {
6
+ return reason === 'human_clarification' || reason === 'human_approval' || reason === 'human_answer';
7
+ }
8
+
9
+ function getEffectiveSuspendPolicy(fiber: FiberRecord<MailboxSchema>): SuspendPolicy | undefined {
10
+ const raw = (fiber as any)?.suspendPolicy;
11
+ return raw === 'continue_others' || raw === 'pause_all' ? raw : undefined;
12
+ }
13
+
14
+ function isBlockingLaneForPauseAll(lane: unknown): boolean {
15
+ // Treat any non-background lane as blocking by default.
16
+ if (lane === 'background' || lane === 'collective') {
17
+ return false;
18
+ }
19
+ return true;
20
+ }
21
+
22
+ function isAllowedLaneDuringPauseAll(lane: unknown): boolean {
23
+ return lane === 'background' || lane === 'collective';
24
+ }
25
+
26
+ function hasPauseAllHumanWaitInBlockingLane<TSchema extends MailboxSchema>(state: OrchestratorState<TSchema>): boolean {
27
+ return Object.values(state.fibers as any).some((fiber: any) => {
28
+ return (
29
+ fiber?.status === 'suspended' &&
30
+ isAiHumanWaitReason(fiber?.waitingReason) &&
31
+ getEffectiveSuspendPolicy(fiber as FiberRecord<MailboxSchema>) === 'pause_all' &&
32
+ isBlockingLaneForPauseAll(fiber?.lane)
33
+ );
34
+ });
35
+ }
36
+
37
+ // AIAgent-compatible scheduling hooks:
38
+ // - When a pause_all human wait exists in a blocking lane (interactive/member), only allow background/collective lanes.
39
+ export function createAiAgentSchedulerHooks<TSchema extends MailboxSchema>(): SchedulerHooks<TSchema> {
40
+ return {
41
+ filterCandidate: ({ state, fiber }) => {
42
+ if (!hasPauseAllHumanWaitInBlockingLane(state)) {
43
+ return true;
44
+ }
45
+ return isAllowedLaneDuringPauseAll((fiber as any)?.lane);
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,144 @@
1
+ import type { MailboxSchema } from '../core/types';
2
+ import type { FiberEffect, FiberId, FiberRecord, OrchestratorState, ReduceResult } from './types';
3
+
4
+ function cloneFiberMap<TSchema extends MailboxSchema>(
5
+ state: OrchestratorState<TSchema>,
6
+ ): Record<FiberId, FiberRecord<TSchema>> {
7
+ return { ...state.fibers };
8
+ }
9
+
10
+ function buildDeadLetterEffects<TSchema extends MailboxSchema>(
11
+ state: OrchestratorState<TSchema>,
12
+ fiber: FiberRecord<TSchema>,
13
+ reason: string,
14
+ ): FiberEffect<TSchema>[] {
15
+ const effects: FiberEffect<TSchema>[] = [
16
+ {
17
+ kind: 'dead_letter',
18
+ fiberId: fiber.id,
19
+ reason,
20
+ },
21
+ ];
22
+
23
+ const route = state.options.deadLetterFactory?.(fiber, reason);
24
+ if (route) {
25
+ effects.push({
26
+ kind: 'dead_letter',
27
+ fiberId: fiber.id,
28
+ reason,
29
+ to: route.to,
30
+ step: route.step,
31
+ });
32
+ }
33
+
34
+ return effects;
35
+ }
36
+
37
+ function computeRetryDelay(
38
+ retryDelayMs: number,
39
+ retryBackoffMultiplier: number,
40
+ attempts: number,
41
+ ): number {
42
+ const base = Math.max(0, retryDelayMs);
43
+ const multiplier = Math.max(1, retryBackoffMultiplier);
44
+ return Math.floor(base * Math.pow(multiplier, Math.max(0, attempts - 1)));
45
+ }
46
+
47
+ export function applyFailure<TSchema extends MailboxSchema>(
48
+ state: OrchestratorState<TSchema>,
49
+ fiberId: FiberId,
50
+ now: number,
51
+ error: string,
52
+ ): ReduceResult<TSchema> {
53
+ const fiber = state.fibers[fiberId];
54
+ if (!fiber) {
55
+ return { state, effects: [] };
56
+ }
57
+
58
+ if (fiber.status === 'completed' || fiber.status === 'cancelled' || fiber.status === 'dead_letter') {
59
+ return { state, effects: [] };
60
+ }
61
+
62
+ const nextFibers = cloneFiberMap(state);
63
+
64
+ if (state.options.retryEnabled && fiber.attempts < fiber.maxAttempts) {
65
+ const nextAttempts = fiber.attempts + 1;
66
+ const delay = computeRetryDelay(
67
+ state.options.retryDelayMs,
68
+ state.options.retryBackoffMultiplier,
69
+ nextAttempts,
70
+ );
71
+ const updated: FiberRecord<TSchema> = {
72
+ ...fiber,
73
+ status: 'suspended',
74
+ waitingReason: 'retry_backoff',
75
+ suspendPolicy: undefined,
76
+ retryAt: now + delay,
77
+ timeoutAt: undefined,
78
+ attempts: nextAttempts,
79
+ lastError: error,
80
+ updatedAt: now,
81
+ };
82
+ nextFibers[fiberId] = updated;
83
+ return {
84
+ state: {
85
+ ...state,
86
+ fibers: nextFibers,
87
+ },
88
+ effects: [],
89
+ };
90
+ }
91
+
92
+ if (state.options.deadLetterEnabled) {
93
+ const updated: FiberRecord<TSchema> = {
94
+ ...fiber,
95
+ status: 'dead_letter',
96
+ waitingReason: undefined,
97
+ suspendPolicy: undefined,
98
+ retryAt: undefined,
99
+ lastError: error,
100
+ updatedAt: now,
101
+ };
102
+ nextFibers[fiberId] = updated;
103
+
104
+ const deadLetters = [
105
+ ...state.deadLetters,
106
+ {
107
+ fiberId: fiber.id,
108
+ actorId: fiber.actorId,
109
+ reason: error,
110
+ at: now,
111
+ attempts: fiber.attempts,
112
+ step: fiber.step,
113
+ },
114
+ ];
115
+
116
+ return {
117
+ state: {
118
+ ...state,
119
+ fibers: nextFibers,
120
+ deadLetters,
121
+ },
122
+ effects: buildDeadLetterEffects(state, updated, error),
123
+ };
124
+ }
125
+
126
+ const updated: FiberRecord<TSchema> = {
127
+ ...fiber,
128
+ status: 'failed',
129
+ waitingReason: undefined,
130
+ suspendPolicy: undefined,
131
+ retryAt: undefined,
132
+ lastError: error,
133
+ updatedAt: now,
134
+ };
135
+ nextFibers[fiberId] = updated;
136
+
137
+ return {
138
+ state: {
139
+ ...state,
140
+ fibers: nextFibers,
141
+ },
142
+ effects: [],
143
+ };
144
+ }