@donkeylabs/server 2.0.21 → 2.0.23
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/docs/workflows.md +107 -7
- package/package.json +2 -2
- package/src/admin/dashboard.ts +74 -3
- package/src/admin/routes.ts +62 -0
- package/src/core/cron.ts +17 -10
- package/src/core/index.ts +23 -0
- package/src/core/jobs.ts +8 -2
- package/src/core/logger.ts +14 -0
- package/src/core/logs-adapter-kysely.ts +287 -0
- package/src/core/logs-transport.ts +83 -0
- package/src/core/logs.ts +398 -0
- package/src/core/subprocess-bootstrap.ts +241 -0
- package/src/core/workflow-executor.ts +50 -36
- package/src/core/workflow-socket.ts +1 -0
- package/src/core/workflows.test.ts +56 -0
- package/src/core/workflows.ts +350 -33
- package/src/core.ts +83 -3
- package/src/harness.ts +4 -0
- package/src/index.ts +10 -0
- package/src/server.ts +53 -5
- /package/{CLAUDE.md → agents.md} +0 -0
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// Workflow Executor - Subprocess Entry Point
|
|
3
|
-
//
|
|
4
|
-
// The state machine owns all execution logic and persistence.
|
|
3
|
+
// Bootstraps core services and plugins locally for isolated workflows.
|
|
5
4
|
|
|
6
5
|
import { connect } from "node:net";
|
|
7
6
|
import type { Socket } from "node:net";
|
|
8
|
-
import { Kysely } from "kysely";
|
|
9
|
-
import { BunSqliteDialect } from "kysely-bun-sqlite";
|
|
10
|
-
import Database from "bun:sqlite";
|
|
11
7
|
import type { WorkflowEvent } from "./workflow-socket";
|
|
12
|
-
import { WorkflowProxyConnection, createPluginsProxy, createCoreServicesProxy } from "./workflow-proxy";
|
|
13
8
|
import type { WorkflowDefinition } from "./workflows";
|
|
14
|
-
import { KyselyWorkflowAdapter } from "./workflow-adapter-kysely";
|
|
15
9
|
import { WorkflowStateMachine, type StateMachineEvents } from "./workflow-state-machine";
|
|
10
|
+
import { bootstrapSubprocess } from "./subprocess-bootstrap";
|
|
16
11
|
|
|
17
12
|
// ============================================
|
|
18
13
|
// Types
|
|
@@ -26,6 +21,10 @@ interface ExecutorConfig {
|
|
|
26
21
|
tcpPort?: number;
|
|
27
22
|
modulePath: string;
|
|
28
23
|
dbPath: string;
|
|
24
|
+
pluginNames: string[];
|
|
25
|
+
pluginModulePaths: Record<string, string>;
|
|
26
|
+
pluginConfigs: Record<string, any>;
|
|
27
|
+
coreConfig?: Record<string, any>;
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
// ============================================
|
|
@@ -37,19 +36,23 @@ async function main(): Promise<void> {
|
|
|
37
36
|
const stdin = await Bun.stdin.text();
|
|
38
37
|
const config: ExecutorConfig = JSON.parse(stdin);
|
|
39
38
|
|
|
40
|
-
const {
|
|
39
|
+
const {
|
|
40
|
+
instanceId,
|
|
41
|
+
workflowName,
|
|
42
|
+
socketPath,
|
|
43
|
+
tcpPort,
|
|
44
|
+
modulePath,
|
|
45
|
+
dbPath,
|
|
46
|
+
pluginNames,
|
|
47
|
+
pluginModulePaths,
|
|
48
|
+
pluginConfigs,
|
|
49
|
+
coreConfig,
|
|
50
|
+
} = config;
|
|
41
51
|
|
|
42
|
-
// Connect to IPC socket
|
|
43
52
|
const socket = await connectToSocket(socketPath, tcpPort);
|
|
44
|
-
const proxyConnection = new WorkflowProxyConnection(socket);
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
dialect: new BunSqliteDialect({
|
|
49
|
-
database: new Database(dbPath),
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
52
|
-
const adapter = new KyselyWorkflowAdapter(db, { cleanupDays: 0 });
|
|
54
|
+
let cleanup: (() => Promise<void>) | undefined;
|
|
55
|
+
let exitCode = 0;
|
|
53
56
|
|
|
54
57
|
// Start heartbeat
|
|
55
58
|
const heartbeatInterval = setInterval(() => {
|
|
@@ -61,30 +64,41 @@ async function main(): Promise<void> {
|
|
|
61
64
|
}, 5000);
|
|
62
65
|
|
|
63
66
|
try {
|
|
64
|
-
// Send started event
|
|
65
|
-
sendEvent(socket, {
|
|
66
|
-
type: "started",
|
|
67
|
-
instanceId,
|
|
68
|
-
timestamp: Date.now(),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
67
|
// Import the workflow module to get the definition
|
|
72
68
|
const module = await import(modulePath);
|
|
73
69
|
const definition = findWorkflowDefinition(module, workflowName, modulePath);
|
|
74
70
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
const bootstrap = await bootstrapSubprocess({
|
|
72
|
+
dbPath,
|
|
73
|
+
coreConfig,
|
|
74
|
+
pluginMetadata: {
|
|
75
|
+
names: pluginNames,
|
|
76
|
+
modulePaths: pluginModulePaths,
|
|
77
|
+
configs: pluginConfigs,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
cleanup = bootstrap.cleanup;
|
|
81
|
+
|
|
82
|
+
sendEvent(socket, {
|
|
83
|
+
type: "ready",
|
|
84
|
+
instanceId,
|
|
85
|
+
timestamp: Date.now(),
|
|
86
|
+
});
|
|
78
87
|
|
|
79
|
-
// Create state machine with IPC event bridge
|
|
80
88
|
const sm = new WorkflowStateMachine({
|
|
81
|
-
adapter,
|
|
82
|
-
core:
|
|
83
|
-
plugins,
|
|
89
|
+
adapter: bootstrap.workflowAdapter,
|
|
90
|
+
core: bootstrap.core as any,
|
|
91
|
+
plugins: bootstrap.manager.getServices(),
|
|
84
92
|
events: createIpcEventBridge(socket, instanceId),
|
|
85
93
|
pollInterval: 1000,
|
|
86
94
|
});
|
|
87
95
|
|
|
96
|
+
sendEvent(socket, {
|
|
97
|
+
type: "started",
|
|
98
|
+
instanceId,
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
});
|
|
101
|
+
|
|
88
102
|
// Run the state machine to completion
|
|
89
103
|
const result = await sm.run(instanceId, definition);
|
|
90
104
|
|
|
@@ -103,16 +117,16 @@ async function main(): Promise<void> {
|
|
|
103
117
|
timestamp: Date.now(),
|
|
104
118
|
error: error instanceof Error ? error.message : String(error),
|
|
105
119
|
});
|
|
106
|
-
|
|
120
|
+
exitCode = 1;
|
|
107
121
|
} finally {
|
|
108
122
|
clearInterval(heartbeatInterval);
|
|
109
|
-
proxyConnection.close();
|
|
110
123
|
socket.end();
|
|
111
|
-
|
|
112
|
-
|
|
124
|
+
if (cleanup) {
|
|
125
|
+
await cleanup();
|
|
126
|
+
}
|
|
113
127
|
}
|
|
114
128
|
|
|
115
|
-
process.exit(
|
|
129
|
+
process.exit(exitCode);
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
// ============================================
|
|
@@ -412,6 +412,62 @@ describe("WorkflowDefinition", () => {
|
|
|
412
412
|
expect(isolatedWf.isolated).toBe(true);
|
|
413
413
|
expect(inlineWf.isolated).toBe(false);
|
|
414
414
|
});
|
|
415
|
+
|
|
416
|
+
it("should auto-detect sourceModule as a valid file:// URL after build()", () => {
|
|
417
|
+
const wf = workflow("auto-detect")
|
|
418
|
+
.task("s", { handler: async () => 1 })
|
|
419
|
+
.build();
|
|
420
|
+
|
|
421
|
+
expect(wf.sourceModule).toBeDefined();
|
|
422
|
+
expect(wf.sourceModule).toMatch(/^file:\/\//);
|
|
423
|
+
// Should point to this test file
|
|
424
|
+
expect(wf.sourceModule).toContain("workflows.test.ts");
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("register() with auto-detected sourceModule", () => {
|
|
429
|
+
let workflows: ReturnType<typeof createWorkflows>;
|
|
430
|
+
let adapter: MemoryWorkflowAdapter;
|
|
431
|
+
|
|
432
|
+
beforeEach(() => {
|
|
433
|
+
adapter = new MemoryWorkflowAdapter();
|
|
434
|
+
workflows = createWorkflows({ adapter });
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
afterEach(async () => {
|
|
438
|
+
await workflows.stop();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("should not warn when registering isolated workflow with auto-detected sourceModule", () => {
|
|
442
|
+
const wf = workflow("auto-isolated")
|
|
443
|
+
.task("s", { handler: async () => 1 })
|
|
444
|
+
.build();
|
|
445
|
+
|
|
446
|
+
// sourceModule should be set by build()
|
|
447
|
+
expect(wf.sourceModule).toBeDefined();
|
|
448
|
+
|
|
449
|
+
const warnings: string[] = [];
|
|
450
|
+
const origWarn = console.warn;
|
|
451
|
+
console.warn = (...args: any[]) => warnings.push(args.join(" "));
|
|
452
|
+
try {
|
|
453
|
+
workflows.register(wf);
|
|
454
|
+
} finally {
|
|
455
|
+
console.warn = origWarn;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
expect(warnings.filter((w) => w.includes("no modulePath"))).toHaveLength(0);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("should prefer explicit modulePath over auto-detected sourceModule", () => {
|
|
462
|
+
const wf = workflow("explicit-path")
|
|
463
|
+
.task("s", { handler: async () => 1 })
|
|
464
|
+
.build();
|
|
465
|
+
|
|
466
|
+
// Register with explicit modulePath
|
|
467
|
+
expect(() => {
|
|
468
|
+
workflows.register(wf, { modulePath: "file:///explicit/path.ts" });
|
|
469
|
+
}).not.toThrow();
|
|
470
|
+
});
|
|
415
471
|
});
|
|
416
472
|
|
|
417
473
|
describe("Choice steps (inline)", () => {
|